You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by ez...@apache.org on 2020/05/29 14:59:14 UTC

[trafficserver] branch master updated: Add CSV output as an optional format for stats_over_http (#6818)

This is an automated email from the ASF dual-hosted git repository.

eze pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 0eed3a6  Add CSV output as an optional format for stats_over_http (#6818)
0eed3a6 is described below

commit 0eed3a664e3018b6d2cd3747631ba4f81ba457fc
Author: Evan Zelkowitz <ez...@apache.org>
AuthorDate: Fri May 29 07:59:02 2020 -0700

    Add CSV output as an optional format for stats_over_http (#6818)
    
    stats_over_http can now take an `Accept` header.  By default it will still return a json formatted output with the (now correct) `text/json` `Content-Type` header.
    With these changes you can also send a `Accept: text/csv` header and the output will be in CSV format along with the corresponding `Content-Type` header
---
 doc/admin-guide/plugins/stats_over_http.en.rst |  16 +++-
 plugins/stats_over_http/stats_over_http.c      | 113 ++++++++++++++++++++++---
 2 files changed, 116 insertions(+), 13 deletions(-)

diff --git a/doc/admin-guide/plugins/stats_over_http.en.rst b/doc/admin-guide/plugins/stats_over_http.en.rst
index 5bdca0e..01f96db 100644
--- a/doc/admin-guide/plugins/stats_over_http.en.rst
+++ b/doc/admin-guide/plugins/stats_over_http.en.rst
@@ -23,8 +23,9 @@ Stats Over HTTP Plugin
 **********************
 
 This plugin implements an HTTP interface to all Traffic Server statistics. The
-metrics returned are in a JSON format, for easy processing. This plugin is now
-part of the standard ATS build process, and should be available after install.
+metrics returned are in a JSON format by default, for easy processing. You can
+also output the stats in CSV format as well. This plugin is now part of the
+standard ATS build process, and should be available after install.
 
 Enabling Stats Over HTTP
 ========================
@@ -92,3 +93,14 @@ A comma separated white list of ipv4 addresses allowed to accesss the endpoint
 .. option:: allow_ip6=
 
 A comma separated white list of ipv6 addresses allowed to access the endpoint
+
+Output Format
+=============
+
+By default stats_over_http.so will output all the stats in json format. However
+if you wish to have it in CSV format you can do so by passing an ``Accept`` header:
+
+.. option:: Accept: text/csv
+
+In either case the ``Content-Type`` header returned by stats_over_http.so will reflect
+the content that has been returned, either ``text/json`` or ``text/csv``.
diff --git a/plugins/stats_over_http/stats_over_http.c b/plugins/stats_over_http/stats_over_http.c
index cdfc8f9..1be2d54 100644
--- a/plugins/stats_over_http/stats_over_http.c
+++ b/plugins/stats_over_http/stats_over_http.c
@@ -72,6 +72,8 @@ typedef struct {
   config_t *config;
 } config_holder_t;
 
+typedef enum { JSON_OUTPUT, CSV_OUTPUT } output_format;
+
 int configReloadRequests = 0;
 int configReloads        = 0;
 time_t lastReloadRequest = 0;
@@ -95,6 +97,7 @@ typedef struct stats_state_t {
 
   int output_bytes;
   int body_written;
+  output_format output;
 } stats_state;
 
 static char *
@@ -141,12 +144,24 @@ stats_add_data_to_resp_buffer(const char *s, stats_state *my_state)
   return s_len;
 }
 
-static const char RESP_HEADER[] = "HTTP/1.0 200 Ok\r\nContent-Type: text/javascript\r\nCache-Control: no-cache\r\n\r\n";
+static const char RESP_HEADER_JSON[] = "HTTP/1.0 200 Ok\r\nContent-Type: text/json\r\nCache-Control: no-cache\r\n\r\n";
+static const char RESP_HEADER_CSV[]  = "HTTP/1.0 200 Ok\r\nContent-Type: text/csv\r\nCache-Control: no-cache\r\n\r\n";
 
 static int
 stats_add_resp_header(stats_state *my_state)
 {
-  return stats_add_data_to_resp_buffer(RESP_HEADER, my_state);
+  switch (my_state->output) {
+  case JSON_OUTPUT:
+    return stats_add_data_to_resp_buffer(RESP_HEADER_JSON, my_state);
+    break;
+  case CSV_OUTPUT:
+    return stats_add_data_to_resp_buffer(RESP_HEADER_CSV, my_state);
+    break;
+  default:
+    TSError("stats_add_resp_header: Unknown output format");
+    break;
+  }
+  return stats_add_data_to_resp_buffer(RESP_HEADER_JSON, my_state);
 }
 
 static void
@@ -171,13 +186,13 @@ stats_process_read(TSCont contp, TSEvent event, stats_state *my_state)
 }
 
 #define APPEND(a) my_state->output_bytes += stats_add_data_to_resp_buffer(a, my_state)
-#define APPEND_STAT(a, fmt, v)                                                   \
+#define APPEND_STAT_JSON(a, fmt, v)                                              \
   do {                                                                           \
     char b[256];                                                                 \
     if (snprintf(b, sizeof(b), "\"%s\": \"" fmt "\",\n", a, v) < (int)sizeof(b)) \
       APPEND(b);                                                                 \
   } while (0)
-#define APPEND_STAT_NUMERIC(a, fmt, v)                                               \
+#define APPEND_STAT_JSON_NUMERIC(a, fmt, v)                                          \
   do {                                                                               \
     char b[256];                                                                     \
     if (integer_counters) {                                                          \
@@ -191,6 +206,20 @@ stats_process_read(TSCont contp, TSEvent event, stats_state *my_state)
     }                                                                                \
   } while (0)
 
+#define APPEND_STAT_CSV(a, fmt, v)                                     \
+  do {                                                                 \
+    char b[256];                                                       \
+    if (snprintf(b, sizeof(b), "%s," fmt "\n", a, v) < (int)sizeof(b)) \
+      APPEND(b);                                                       \
+  } while (0)
+#define APPEND_STAT_CSV_NUMERIC(a, fmt, v)                               \
+  do {                                                                   \
+    char b[256];                                                         \
+    if (snprintf(b, sizeof(b), "%s," fmt "\n", a, v) < (int)sizeof(b)) { \
+      APPEND(b);                                                         \
+    }                                                                    \
+  } while (0)
+
 // This wraps uint64_t values to the int64_t range to fit into a Java long. Java 8 has an unsigned long which
 // can interoperate with a full uint64_t, but it's unlikely that much of the ecosystem supports that yet.
 static uint64_t
@@ -211,22 +240,47 @@ json_out_stat(TSRecordType rec_type ATS_UNUSED, void *edata, int registered ATS_
 
   switch (data_type) {
   case TS_RECORDDATATYPE_COUNTER:
-    APPEND_STAT_NUMERIC(name, "%" PRIu64, wrap_unsigned_counter(datum->rec_counter));
+    APPEND_STAT_JSON_NUMERIC(name, "%" PRIu64, wrap_unsigned_counter(datum->rec_counter));
     break;
   case TS_RECORDDATATYPE_INT:
-    APPEND_STAT_NUMERIC(name, "%" PRIu64, wrap_unsigned_counter(datum->rec_int));
+    APPEND_STAT_JSON_NUMERIC(name, "%" PRIu64, wrap_unsigned_counter(datum->rec_int));
     break;
   case TS_RECORDDATATYPE_FLOAT:
-    APPEND_STAT_NUMERIC(name, "%f", datum->rec_float);
+    APPEND_STAT_JSON_NUMERIC(name, "%f", datum->rec_float);
     break;
   case TS_RECORDDATATYPE_STRING:
-    APPEND_STAT(name, "%s", datum->rec_string);
+    APPEND_STAT_JSON(name, "%s", datum->rec_string);
     break;
   default:
     TSDebug(PLUGIN_NAME, "unknown type for %s: %d", name, data_type);
     break;
   }
 }
+
+static void
+csv_out_stat(TSRecordType rec_type ATS_UNUSED, void *edata, int registered ATS_UNUSED, const char *name, TSRecordDataType data_type,
+             TSRecordData *datum)
+{
+  stats_state *my_state = edata;
+  switch (data_type) {
+  case TS_RECORDDATATYPE_COUNTER:
+    APPEND_STAT_CSV_NUMERIC(name, "%" PRIu64, wrap_unsigned_counter(datum->rec_counter));
+    break;
+  case TS_RECORDDATATYPE_INT:
+    APPEND_STAT_CSV_NUMERIC(name, "%" PRIu64, wrap_unsigned_counter(datum->rec_int));
+    break;
+  case TS_RECORDDATATYPE_FLOAT:
+    APPEND_STAT_CSV_NUMERIC(name, "%f", datum->rec_float);
+    break;
+  case TS_RECORDDATATYPE_STRING:
+    APPEND_STAT_CSV(name, "%s", datum->rec_string);
+    break;
+  default:
+    TSDebug(PLUGIN_NAME, "unknown type for %s: %d", name, data_type);
+    break;
+  }
+}
+
 static void
 json_out_stats(stats_state *my_state)
 {
@@ -242,13 +296,32 @@ json_out_stats(stats_state *my_state)
 }
 
 static void
+csv_out_stats(stats_state *my_state)
+{
+  const char *version;
+  TSRecordDump((TSRecordType)(TS_RECORDTYPE_PLUGIN | TS_RECORDTYPE_NODE | TS_RECORDTYPE_PROCESS), csv_out_stat, my_state);
+  version = TSTrafficServerVersionGet();
+  APPEND_STAT_CSV("version", "%s", version);
+}
+
+static void
 stats_process_write(TSCont contp, TSEvent event, stats_state *my_state)
 {
   if (event == TS_EVENT_VCONN_WRITE_READY) {
     if (my_state->body_written == 0) {
       TSDebug(PLUGIN_NAME, "plugin adding response body");
       my_state->body_written = 1;
-      json_out_stats(my_state);
+      switch (my_state->output) {
+      case JSON_OUTPUT:
+        json_out_stats(my_state);
+        break;
+      case CSV_OUTPUT:
+        csv_out_stats(my_state);
+        break;
+      default:
+        TSError("stats_process_write: Unknown output type\n");
+        break;
+      }
       TSVIONBytesSet(my_state->write_vio, my_state->output_bytes);
     }
     TSVIOReenable(my_state->write_vio);
@@ -286,7 +359,7 @@ stats_origin(TSCont contp ATS_UNUSED, TSEvent event ATS_UNUSED, void *edata)
   config_t *config;
   TSHttpTxn txnp = (TSHttpTxn)edata;
   TSMBuffer reqp;
-  TSMLoc hdr_loc = NULL, url_loc = NULL;
+  TSMLoc hdr_loc = NULL, url_loc = NULL, accept_field = NULL;
   TSEvent reenable = TS_EVENT_HTTP_CONTINUE;
 
   TSDebug(PLUGIN_NAME, "in the read stuff");
@@ -322,6 +395,22 @@ stats_origin(TSCont contp ATS_UNUSED, TSEvent event ATS_UNUSED, void *edata)
   icontp   = TSContCreate(stats_dostuff, TSMutexCreate());
   my_state = (stats_state *)TSmalloc(sizeof(*my_state));
   memset(my_state, 0, sizeof(*my_state));
+
+  accept_field     = TSMimeHdrFieldFind(reqp, hdr_loc, TS_MIME_FIELD_ACCEPT, TS_MIME_LEN_ACCEPT);
+  my_state->output = JSON_OUTPUT; // default to json output
+  // accept header exists, use it to determine response type
+  if (accept_field != TS_NULL_MLOC) {
+    int len         = -1;
+    const char *str = TSMimeHdrFieldValueStringGet(reqp, hdr_loc, accept_field, -1, &len);
+
+    // Parse the Accept header, default to JSON output unless its another supported format
+    if (!strncasecmp(str, "text/csv", len)) {
+      my_state->output = CSV_OUTPUT;
+    } else {
+      my_state->output = JSON_OUTPUT;
+    }
+  }
+
   TSContDataSet(icontp, my_state);
   TSHttpTxnIntercept(icontp, txnp);
   goto cleanup;
@@ -335,7 +424,9 @@ cleanup:
   if (hdr_loc) {
     TSHandleMLocRelease(reqp, TS_NULL_MLOC, hdr_loc);
   }
-
+  if (accept_field) {
+    TSHandleMLocRelease(reqp, TS_NULL_MLOC, accept_field);
+  }
   TSHttpTxnReenable(txnp, reenable);
   return 0;
 }