1 /** @file
2 
3   A brief file description
4 
5   @section license License
6 
7   Licensed to the Apache Software Foundation (ASF) under one
8   or more contributor license agreements.  See the NOTICE file
9   distributed with this work for additional information
10   regarding copyright ownership.  The ASF licenses this file
11   to you under the Apache License, Version 2.0 (the
12   "License"); you may not use this file except in compliance
13   with the License.  You may obtain a copy of the License at
14 
15       http://www.apache.org/licenses/LICENSE-2.0
16 
17   Unless required by applicable law or agreed to in writing, software
18   distributed under the License is distributed on an "AS IS" BASIS,
19   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20   See the License for the specific language governing permissions and
21   limitations under the License.
22  */
23 
24 /* stats.c:  expose traffic server stats over http
25  */
26 
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <stdbool.h>
30 #include <ctype.h>
31 #include <limits.h>
32 #include <ts/ts.h>
33 #include <string.h>
34 #include <inttypes.h>
35 #include <getopt.h>
36 
37 #include "tscore/ink_defs.h"
38 
39 #define PLUGIN_NAME "stats_over_http"
40 
41 /* global holding the path used for access to this JSON data */
42 static const char *url_path = "_stats";
43 static int url_path_len;
44 
45 static bool integer_counters = false;
46 static bool wrap_counters    = false;
47 
48 typedef struct stats_state_t {
49   TSVConn net_vc;
50   TSVIO read_vio;
51   TSVIO write_vio;
52 
53   TSIOBuffer req_buffer;
54   TSIOBuffer resp_buffer;
55   TSIOBufferReader resp_reader;
56 
57   int output_bytes;
58   int body_written;
59 } stats_state;
60 
61 static void
stats_cleanup(TSCont contp,stats_state * my_state)62 stats_cleanup(TSCont contp, stats_state *my_state)
63 {
64   if (my_state->req_buffer) {
65     TSIOBufferDestroy(my_state->req_buffer);
66     my_state->req_buffer = NULL;
67   }
68 
69   if (my_state->resp_buffer) {
70     TSIOBufferDestroy(my_state->resp_buffer);
71     my_state->resp_buffer = NULL;
72   }
73   TSVConnClose(my_state->net_vc);
74   TSfree(my_state);
75   TSContDestroy(contp);
76 }
77 
78 static void
stats_process_accept(TSCont contp,stats_state * my_state)79 stats_process_accept(TSCont contp, stats_state *my_state)
80 {
81   my_state->req_buffer  = TSIOBufferCreate();
82   my_state->resp_buffer = TSIOBufferCreate();
83   my_state->resp_reader = TSIOBufferReaderAlloc(my_state->resp_buffer);
84   my_state->read_vio    = TSVConnRead(my_state->net_vc, contp, my_state->req_buffer, INT64_MAX);
85 }
86 
87 static int
stats_add_data_to_resp_buffer(const char * s,stats_state * my_state)88 stats_add_data_to_resp_buffer(const char *s, stats_state *my_state)
89 {
90   int s_len = strlen(s);
91 
92   TSIOBufferWrite(my_state->resp_buffer, s, s_len);
93 
94   return s_len;
95 }
96 
97 static const char RESP_HEADER[] = "HTTP/1.0 200 Ok\r\nContent-Type: text/javascript\r\nCache-Control: no-cache\r\n\r\n";
98 
99 static int
stats_add_resp_header(stats_state * my_state)100 stats_add_resp_header(stats_state *my_state)
101 {
102   return stats_add_data_to_resp_buffer(RESP_HEADER, my_state);
103 }
104 
105 static void
stats_process_read(TSCont contp,TSEvent event,stats_state * my_state)106 stats_process_read(TSCont contp, TSEvent event, stats_state *my_state)
107 {
108   TSDebug(PLUGIN_NAME, "stats_process_read(%d)", event);
109   if (event == TS_EVENT_VCONN_READ_READY) {
110     my_state->output_bytes = stats_add_resp_header(my_state);
111     TSVConnShutdown(my_state->net_vc, 1, 0);
112     my_state->write_vio = TSVConnWrite(my_state->net_vc, contp, my_state->resp_reader, INT64_MAX);
113   } else if (event == TS_EVENT_ERROR) {
114     TSError("[%s] stats_process_read: Received TS_EVENT_ERROR", PLUGIN_NAME);
115   } else if (event == TS_EVENT_VCONN_EOS) {
116     /* client may end the connection, simply return */
117     return;
118   } else if (event == TS_EVENT_NET_ACCEPT_FAILED) {
119     TSError("[%s] stats_process_read: Received TS_EVENT_NET_ACCEPT_FAILED", PLUGIN_NAME);
120   } else {
121     printf("Unexpected Event %d\n", event);
122     TSReleaseAssert(!"Unexpected Event");
123   }
124 }
125 
126 #define APPEND(a) my_state->output_bytes += stats_add_data_to_resp_buffer(a, my_state)
127 #define APPEND_STAT(a, fmt, v)                                                   \
128   do {                                                                           \
129     char b[256];                                                                 \
130     if (snprintf(b, sizeof(b), "\"%s\": \"" fmt "\",\n", a, v) < (int)sizeof(b)) \
131       APPEND(b);                                                                 \
132   } while (0)
133 #define APPEND_STAT_NUMERIC(a, fmt, v)                                               \
134   do {                                                                               \
135     char b[256];                                                                     \
136     if (integer_counters) {                                                          \
137       if (snprintf(b, sizeof(b), "\"%s\": " fmt ",\n", a, v) < (int)sizeof(b)) {     \
138         APPEND(b);                                                                   \
139       }                                                                              \
140     } else {                                                                         \
141       if (snprintf(b, sizeof(b), "\"%s\": \"" fmt "\",\n", a, v) < (int)sizeof(b)) { \
142         APPEND(b);                                                                   \
143       }                                                                              \
144     }                                                                                \
145   } while (0)
146 
147 // This wraps uint64_t values to the int64_t range to fit into a Java long. Java 8 has an unsigned long which
148 // can interoperate with a full uint64_t, but it's unlikely that much of the ecosystem supports that yet.
149 static uint64_t
wrap_unsigned_counter(uint64_t value)150 wrap_unsigned_counter(uint64_t value)
151 {
152   if (wrap_counters) {
153     return (value > INT64_MAX) ? value % INT64_MAX : value;
154   } else {
155     return value;
156   }
157 }
158 
159 static void
json_out_stat(TSRecordType rec_type ATS_UNUSED,void * edata,int registered ATS_UNUSED,const char * name,TSRecordDataType data_type,TSRecordData * datum)160 json_out_stat(TSRecordType rec_type ATS_UNUSED, void *edata, int registered ATS_UNUSED, const char *name,
161               TSRecordDataType data_type, TSRecordData *datum)
162 {
163   stats_state *my_state = edata;
164 
165   switch (data_type) {
166   case TS_RECORDDATATYPE_COUNTER:
167     APPEND_STAT_NUMERIC(name, "%" PRIu64, wrap_unsigned_counter(datum->rec_counter));
168     break;
169   case TS_RECORDDATATYPE_INT:
170     APPEND_STAT_NUMERIC(name, "%" PRIu64, wrap_unsigned_counter(datum->rec_int));
171     break;
172   case TS_RECORDDATATYPE_FLOAT:
173     APPEND_STAT_NUMERIC(name, "%f", datum->rec_float);
174     break;
175   case TS_RECORDDATATYPE_STRING:
176     APPEND_STAT(name, "%s", datum->rec_string);
177     break;
178   default:
179     TSDebug(PLUGIN_NAME, "unknown type for %s: %d", name, data_type);
180     break;
181   }
182 }
183 static void
json_out_stats(stats_state * my_state)184 json_out_stats(stats_state *my_state)
185 {
186   const char *version;
187   APPEND("{ \"global\": {\n");
188 
189   TSRecordDump((TSRecordType)(TS_RECORDTYPE_PLUGIN | TS_RECORDTYPE_NODE | TS_RECORDTYPE_PROCESS), json_out_stat, my_state);
190   version = TSTrafficServerVersionGet();
191   APPEND("\"server\": \"");
192   APPEND(version);
193   APPEND("\"\n");
194   APPEND("  }\n}\n");
195 }
196 
197 static void
stats_process_write(TSCont contp,TSEvent event,stats_state * my_state)198 stats_process_write(TSCont contp, TSEvent event, stats_state *my_state)
199 {
200   if (event == TS_EVENT_VCONN_WRITE_READY) {
201     if (my_state->body_written == 0) {
202       TSDebug(PLUGIN_NAME, "plugin adding response body");
203       my_state->body_written = 1;
204       json_out_stats(my_state);
205       TSVIONBytesSet(my_state->write_vio, my_state->output_bytes);
206     }
207     TSVIOReenable(my_state->write_vio);
208   } else if (event == TS_EVENT_VCONN_WRITE_COMPLETE) {
209     stats_cleanup(contp, my_state);
210   } else if (event == TS_EVENT_ERROR) {
211     TSError("[%s] stats_process_write: Received TS_EVENT_ERROR", PLUGIN_NAME);
212   } else {
213     TSReleaseAssert(!"Unexpected Event");
214   }
215 }
216 
217 static int
stats_dostuff(TSCont contp,TSEvent event,void * edata)218 stats_dostuff(TSCont contp, TSEvent event, void *edata)
219 {
220   stats_state *my_state = TSContDataGet(contp);
221   if (event == TS_EVENT_NET_ACCEPT) {
222     my_state->net_vc = (TSVConn)edata;
223     stats_process_accept(contp, my_state);
224   } else if (edata == my_state->read_vio) {
225     stats_process_read(contp, event, my_state);
226   } else if (edata == my_state->write_vio) {
227     stats_process_write(contp, event, my_state);
228   } else {
229     TSReleaseAssert(!"Unexpected Event");
230   }
231   return 0;
232 }
233 
234 static int
stats_origin(TSCont contp ATS_UNUSED,TSEvent event ATS_UNUSED,void * edata)235 stats_origin(TSCont contp ATS_UNUSED, TSEvent event ATS_UNUSED, void *edata)
236 {
237   TSCont icontp;
238   stats_state *my_state;
239   TSHttpTxn txnp = (TSHttpTxn)edata;
240   TSMBuffer reqp;
241   TSMLoc hdr_loc = NULL, url_loc = NULL;
242   TSEvent reenable = TS_EVENT_HTTP_CONTINUE;
243 
244   TSDebug(PLUGIN_NAME, "in the read stuff");
245 
246   if (TSHttpTxnClientReqGet(txnp, &reqp, &hdr_loc) != TS_SUCCESS) {
247     goto cleanup;
248   }
249 
250   if (TSHttpHdrUrlGet(reqp, hdr_loc, &url_loc) != TS_SUCCESS) {
251     goto cleanup;
252   }
253 
254   int path_len     = 0;
255   const char *path = TSUrlPathGet(reqp, url_loc, &path_len);
256   TSDebug(PLUGIN_NAME, "Path: %.*s", path_len, path);
257 
258   if (!(path_len != 0 && path_len == url_path_len && !memcmp(path, url_path, url_path_len))) {
259     goto notforme;
260   }
261 
262   TSSkipRemappingSet(txnp, 1); // not strictly necessary, but speed is everything these days
263 
264   /* This is us -- register our intercept */
265   TSDebug(PLUGIN_NAME, "Intercepting request");
266 
267   icontp   = TSContCreate(stats_dostuff, TSMutexCreate());
268   my_state = (stats_state *)TSmalloc(sizeof(*my_state));
269   memset(my_state, 0, sizeof(*my_state));
270   TSContDataSet(icontp, my_state);
271   TSHttpTxnIntercept(icontp, txnp);
272   goto cleanup;
273 
274 notforme:
275 
276 cleanup:
277   if (url_loc) {
278     TSHandleMLocRelease(reqp, hdr_loc, url_loc);
279   }
280   if (hdr_loc) {
281     TSHandleMLocRelease(reqp, TS_NULL_MLOC, hdr_loc);
282   }
283 
284   TSHttpTxnReenable(txnp, reenable);
285   return 0;
286 }
287 
288 void
TSPluginInit(int argc,const char * argv[])289 TSPluginInit(int argc, const char *argv[])
290 {
291   TSPluginRegistrationInfo info;
292 
293   static const char usage[]             = PLUGIN_NAME ".so [--integer-counters] [PATH]";
294   static const struct option longopts[] = {{(char *)("integer-counters"), no_argument, NULL, 'i'},
295                                            {(char *)("wrap-counters"), no_argument, NULL, 'w'},
296                                            {NULL, 0, NULL, 0}};
297 
298   info.plugin_name   = PLUGIN_NAME;
299   info.vendor_name   = "Apache Software Foundation";
300   info.support_email = "dev@trafficserver.apache.org";
301 
302   if (TSPluginRegister(&info) != TS_SUCCESS) {
303     TSError("[%s] registration failed", PLUGIN_NAME);
304   }
305 
306   for (;;) {
307     switch (getopt_long(argc, (char *const *)argv, "iw", longopts, NULL)) {
308     case 'i':
309       integer_counters = true;
310       break;
311     case 'w':
312       wrap_counters = true;
313       break;
314     case -1:
315       goto init;
316     default:
317       TSError("[%s] usage: %s", PLUGIN_NAME, usage);
318     }
319   }
320 
321 init:
322   argc -= optind;
323   argv += optind;
324 
325   if (argc > 0) {
326     url_path = TSstrdup(argv[0] + ('/' == argv[0][0] ? 1 : 0)); /* Skip leading / */
327   }
328   url_path_len = strlen(url_path);
329 
330   /* Create a continuation with a mutex as there is a shared global structure
331      containing the headers to add */
332   TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, TSContCreate(stats_origin, TSMutexCreate()));
333   TSDebug(PLUGIN_NAME, "stats module registered");
334 }
335