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

zwoop pushed a commit to branch 9.2.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/9.2.x by this push:
     new 0e96610ac Adds new plugin, http_stats (#8797)
0e96610ac is described below

commit 0e96610ac471fce02bfc862e8d906534a11e2ac8
Author: Randall Meyer <[email protected]>
AuthorDate: Wed Apr 20 12:50:56 2022 -0700

    Adds new plugin, http_stats (#8797)
    
    This is an origin intercept-based remap plugin for exposing ATS's stats
    over HTTP
    
    (cherry picked from commit 2aa1d3106398eb082e5a454212b0273c63d5f69d)
---
 doc/admin-guide/plugins/http_stats.en.rst     |  78 +++
 doc/admin-guide/plugins/index.en.rst          |   4 +
 plugins/Makefile.am                           |   1 +
 plugins/experimental/http_stats/Makefile.inc  |  20 +
 plugins/experimental/http_stats/http_stats.cc | 767 ++++++++++++++++++++++++++
 5 files changed, 870 insertions(+)

diff --git a/doc/admin-guide/plugins/http_stats.en.rst 
b/doc/admin-guide/plugins/http_stats.en.rst
new file mode 100644
index 000000000..aa4b48c8f
--- /dev/null
+++ b/doc/admin-guide/plugins/http_stats.en.rst
@@ -0,0 +1,78 @@
+.. Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.
+
+.. include:: ../../common.defs
+
+.. _admin-plugins-http-stsatus:
+
+HTTP Stats Plugin
+**********************
+
+This plugin implements an HTTP interface to all Traffic Server statistics. The
+metrics returned are in a JSON format by default. You can also configure the 
output
+to be CSV as well.
+
+Since this is a remap plugin, you can chain other plugins to affect the output 
(eg
+compressing the output via the :doc:`compress.en`.
+
+Enabling HTTP Stats
+========================
+
+To use this plugin, add a mapping to the :file:`remap.config` file::
+
+    map /_stats http://localhost @plugin=http_stats.so
+
+After starting Traffic Server, the JSON metrics are now available at the URL::
+
+    http://host:port/_stats
+
+where host and port is the hostname/IP and port number of |TS|.
+
+This will expose the stats to anyone who could access the |TS| instance. It's 
recommended you
+use one of the ACL features in |TS|. For example::
+
+    map /_stats \
+        http://127.0.0.1 \
+        @plugin=http_stats.so \
+        @src_ip=127.0.0.1 @src_ip=::1 \
+        @src_ip=10.0.0.0-10.255.255.255 \
+        @action=allow
+
+Plugin Options
+==============
+
+.. option:: --integer-counters
+
+This option causes the plugin to emit floating point and integral
+metric values as JSON numbers, rather then JSON strings. This can
+cause interoperability problems since integer metrics have a 64-bit
+unsigned range.
+
+.. option:: --wrap-counters
+
+This option wraps 64-bit unsigned values to the 64-bit signed range.
+This aids interoperability with Java, since prior to the Java SE 8
+release, Java did not have a 64-bit unsigned type.
+
+.. option:: --csv
+
+By default, `http_stats.so` will output all the stats in JSON format. Specify 
this
+option on the remap command-line to enable this.
+
+.. option:: --max-age=<int>
+
+If set, this will result in a ``Last-Modified`` header and a ``Cache-Control`` 
header of ``max-age=<int>``
diff --git a/doc/admin-guide/plugins/index.en.rst 
b/doc/admin-guide/plugins/index.en.rst
index 12ff6ad50..92d1d32a2 100644
--- a/doc/admin-guide/plugins/index.en.rst
+++ b/doc/admin-guide/plugins/index.en.rst
@@ -60,6 +60,7 @@ Plugins that are considered stable are installed by default 
in |TS| releases.
    Generator <generator.en>
    Header Rewrite <header_rewrite.en>
    Health Checks <healthchecks.en>
+   HTTP Stats <http_stats.en>
    Lua <lua.en>
    Regex Remap <regex_remap.en>
    Regex Revalidate <regex_revalidate.en>
@@ -113,6 +114,9 @@ Plugins that are considered stable are installed by default 
in |TS| releases.
 :doc:`Health Checks <healthchecks.en>`
     Define service health check links.
 
+:doc:`HTTP Stats <http_stats.en>`
+    A remap-based plugin that provides an HTTP interface to all |TS| 
statistics.
+
 :doc:`Lua <lua.en>`
    Allows plugins to be written in Lua instead of C code.
 
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index fff6fdbd6..16bc11569 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -71,6 +71,7 @@ include experimental/geoip_acl/Makefile.inc
 include experimental/header_freq/Makefile.inc
 include experimental/parent_select/Makefile.inc
 include experimental/hook-trace/Makefile.inc
+include experimental/http_stats/Makefile.inc
 include experimental/icap/Makefile.inc
 include experimental/inliner/Makefile.inc
 
diff --git a/plugins/experimental/http_stats/Makefile.inc 
b/plugins/experimental/http_stats/Makefile.inc
new file mode 100644
index 000000000..eae099a89
--- /dev/null
+++ b/plugins/experimental/http_stats/Makefile.inc
@@ -0,0 +1,20 @@
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+
+pkglib_LTLIBRARIES += experimental/http_stats/http_stats.la
+
+experimental_http_stats_http_stats_la_SOURCES = \
+  experimental/http_stats/http_stats.cc
diff --git a/plugins/experimental/http_stats/http_stats.cc 
b/plugins/experimental/http_stats/http_stats.cc
new file mode 100644
index 000000000..d4b4ea0f2
--- /dev/null
+++ b/plugins/experimental/http_stats/http_stats.cc
@@ -0,0 +1,767 @@
+/** @file
+
+  Stats Over HTTP as an origin intercept
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include <cerrno>
+#include <cinttypes>
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <unistd.h>
+
+#include <fstream>
+#include <sstream>
+
+#include <string>
+#include <getopt.h>
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+constexpr char PLUGIN[] = "http_stats";
+
+#define VDEBUG(fmt, ...) TSDebug(PLUGIN, fmt, ##__VA_ARGS__)
+
+#if DEBUG
+#define VERROR(fmt, ...) TSDebug(PLUGIN, fmt, ##__VA_ARGS__)
+#else
+#define VERROR(fmt, ...) TSError("[%s] %s: " fmt, PLUGIN, __FUNCTION__, 
##__VA_ARGS__)
+#endif
+
+#define VIODEBUG(vio, fmt, ...)                                                
                                              \
+  VDEBUG("vio=%p vio.cont=%p, vio.cont.data=%p, vio.vc=%p " fmt, (vio), 
TSVIOContGet(vio), TSContDataGet(TSVIOContGet(vio)), \
+         TSVIOVConnGet(vio), ##__VA_ARGS__)
+
+static int StatCountBytes     = -1;
+static int StatCountResponses = -1;
+
+static int HTTPStatsInterceptHook(TSCont contp, TSEvent event, void *edata);
+static int HTTPStatsTxnHook(TSCont contp, TSEvent event, void *edata);
+
+struct HTTPStatsFormatter {
+  HTTPStatsFormatter(bool csv, bool integer_counters, bool wrap_counters)
+    : csv(csv), integer_counters(integer_counters), 
wrap_counters(wrap_counters)
+  {
+  }
+
+  uint64_t wrap_unsigned_counter(uint64_t value);
+  std::string output();
+
+  bool csv              = false;
+  bool integer_counters = false;
+  bool wrap_counters    = false;
+  std::string buf;
+};
+
+struct HTTPStatsConfig {
+  explicit HTTPStatsConfig() {}
+
+  ~HTTPStatsConfig() { TSContDestroy(cont); }
+  std::string mimeType;
+
+  int maxAge            = 0;
+  bool csv              = false;
+  bool integer_counters = false;
+  bool wrap_counters    = false;
+
+  TSCont cont;
+};
+
+struct HTTPStatsRequest;
+
+union argument_type {
+  void *ptr;
+  intptr_t ecode;
+  TSVConn vc;
+  TSVIO vio;
+  TSHttpTxn txn;
+  HTTPStatsRequest *trq;
+
+  argument_type(void *_p) : ptr(_p) {}
+};
+
+// This structure represents the state of a streaming I/O request. It
+// is directional (ie. either a read or a write). We need two of these
+// for each TSVConn; one to push data into the TSVConn and one to pull
+// data out.
+struct IOChannel {
+  TSVIO vio = nullptr;
+  TSIOBuffer iobuf;
+  TSIOBufferReader reader;
+
+  IOChannel() : iobuf(TSIOBufferSizedCreate(TS_IOBUFFER_SIZE_INDEX_32K)), 
reader(TSIOBufferReaderAlloc(iobuf)) {}
+  ~IOChannel()
+  {
+    if (this->reader) {
+      TSIOBufferReaderFree(this->reader);
+    }
+
+    if (this->iobuf) {
+      TSIOBufferDestroy(this->iobuf);
+    }
+  }
+
+  void
+  read(TSVConn vc, TSCont contp)
+  {
+    this->vio = TSVConnRead(vc, contp, this->iobuf, INT64_MAX);
+  }
+
+  void
+  write(TSVConn vc, TSCont contp)
+  {
+    this->vio = TSVConnWrite(vc, contp, this->reader, INT64_MAX);
+  }
+};
+
+struct HTTPStatsHttpHeader {
+  TSMBuffer buffer;
+  TSMLoc header;
+  TSHttpParser parser;
+
+  HTTPStatsHttpHeader()
+  {
+    this->buffer = TSMBufferCreate();
+    this->header = TSHttpHdrCreate(this->buffer);
+    this->parser = TSHttpParserCreate();
+  }
+
+  ~HTTPStatsHttpHeader()
+  {
+    if (this->parser) {
+      TSHttpParserDestroy(this->parser);
+    }
+
+    TSHttpHdrDestroy(this->buffer, this->header);
+    TSHandleMLocRelease(this->buffer, TS_NULL_MLOC, this->header);
+    TSMBufferDestroy(this->buffer);
+  }
+};
+
+struct HTTPStatsRequest {
+  HTTPStatsRequest() {}
+
+  off_t nbytes        = 0; // Number of bytes to generate.
+  unsigned maxAge     = 0; // Max age for cache responses.
+  unsigned statusCode = 200;
+  IOChannel readio;
+  IOChannel writeio;
+  HTTPStatsHttpHeader rqheader;
+
+  std::string mimeType;
+  std::string body;
+
+  static HTTPStatsRequest *
+  createHTTPStatsRequest(HTTPStatsConfig *cfg)
+  {
+    HTTPStatsRequest *shr = new HTTPStatsRequest;
+
+    HTTPStatsFormatter fmtr(cfg->csv, cfg->integer_counters, 
cfg->wrap_counters);
+    shr->body = fmtr.output();
+
+    shr->nbytes     = shr->body.size();
+    shr->mimeType   = cfg->mimeType;
+    shr->statusCode = 200;
+    shr->maxAge     = cfg->maxAge;
+
+    return shr;
+  }
+
+  ~HTTPStatsRequest() = default;
+};
+
+// Destroy a HTTPStatsRequest, including the per-txn continuation.
+static void
+HTTPStatsRequestDestroy(HTTPStatsRequest *trq, TSVIO vio, TSCont contp)
+{
+  if (vio) {
+    TSVConnClose(TSVIOVConnGet(vio));
+  }
+
+  TSContDestroy(contp);
+  delete trq;
+}
+
+// NOTE: This will always append a new "field_name: value"
+static void
+HeaderFieldDateSet(const HTTPStatsHttpHeader &http, const char *field_name, 
int64_t field_len, time_t value)
+{
+  TSMLoc field;
+
+  TSMimeHdrFieldCreateNamed(http.buffer, http.header, field_name, field_len, 
&field);
+  TSMimeHdrFieldValueDateSet(http.buffer, http.header, field, value);
+  TSMimeHdrFieldAppend(http.buffer, http.header, field);
+  TSHandleMLocRelease(http.buffer, http.header, field);
+}
+
+// NOTE: This will always append a new "field_name: value"
+static void
+HeaderFieldIntSet(const HTTPStatsHttpHeader &http, const char *field_name, 
int64_t field_len, int64_t value)
+{
+  TSMLoc field;
+
+  TSMimeHdrFieldCreateNamed(http.buffer, http.header, field_name, field_len, 
&field);
+  TSMimeHdrFieldValueInt64Set(http.buffer, http.header, field, -1, value);
+  TSMimeHdrFieldAppend(http.buffer, http.header, field);
+  TSHandleMLocRelease(http.buffer, http.header, field);
+}
+
+// NOTE: This will always append a new "field_name: value"
+static void
+HeaderFieldStringSet(const HTTPStatsHttpHeader &http, const char *field_name, 
int64_t field_len, const char *value)
+{
+  TSMLoc field;
+
+  TSMimeHdrFieldCreateNamed(http.buffer, http.header, field_name, field_len, 
&field);
+  TSMimeHdrFieldValueStringSet(http.buffer, http.header, field, -1, value, -1);
+  TSMimeHdrFieldAppend(http.buffer, http.header, field);
+  TSHandleMLocRelease(http.buffer, http.header, field);
+}
+
+static TSReturnCode
+WriteResponseHeader(HTTPStatsRequest *trq, TSCont contp, TSHttpStatus status)
+{
+  HTTPStatsHttpHeader response;
+
+  VDEBUG("writing response header");
+
+  if (TSHttpHdrTypeSet(response.buffer, response.header, 
TS_HTTP_TYPE_RESPONSE) != TS_SUCCESS) {
+    VERROR("failed to set type");
+    return TS_ERROR;
+  }
+  if (TSHttpHdrVersionSet(response.buffer, response.header, TS_HTTP_VERSION(1, 
1)) != TS_SUCCESS) {
+    VERROR("failed to set HTTP version");
+    return TS_ERROR;
+  }
+  if (TSHttpHdrStatusSet(response.buffer, response.header, status) != 
TS_SUCCESS) {
+    VERROR("failed to set HTTP status");
+    return TS_ERROR;
+  }
+
+  TSHttpHdrReasonSet(response.buffer, response.header, 
TSHttpHdrReasonLookup(status), -1);
+
+  if (status == TS_HTTP_STATUS_OK) {
+    // Set the Content-Length header.
+    HeaderFieldIntSet(response, TS_MIME_FIELD_CONTENT_LENGTH, 
TS_MIME_LEN_CONTENT_LENGTH, trq->nbytes);
+
+    // Set the Cache-Control header.
+    if (trq->maxAge > 0) {
+      char buf[64];
+
+      snprintf(buf, sizeof(buf), "max-age=%u", trq->maxAge);
+      HeaderFieldStringSet(response, TS_MIME_FIELD_CACHE_CONTROL, 
TS_MIME_LEN_CACHE_CONTROL, buf);
+      HeaderFieldDateSet(response, TS_MIME_FIELD_LAST_MODIFIED, 
TS_MIME_LEN_LAST_MODIFIED, time(nullptr));
+
+    } else {
+      HeaderFieldStringSet(response, TS_MIME_FIELD_CACHE_CONTROL, 
TS_MIME_LEN_CACHE_CONTROL, "no-cache");
+    }
+
+    HeaderFieldStringSet(response, TS_MIME_FIELD_CONTENT_TYPE, 
TS_MIME_LEN_CONTENT_TYPE, trq->mimeType.c_str());
+  }
+
+  // Write the header to the IO buffer. Set the VIO bytes so that we can get a 
WRITE_COMPLETE
+  // event when this is done.
+  int hdrlen = TSHttpHdrLengthGet(response.buffer, response.header);
+
+  TSHttpHdrPrint(response.buffer, response.header, trq->writeio.iobuf);
+  TSVIONBytesSet(trq->writeio.vio, hdrlen);
+  TSVIOReenable(trq->writeio.vio);
+
+  TSStatIntIncrement(StatCountBytes, hdrlen);
+
+  return TS_SUCCESS;
+}
+
+static bool
+HTTPStatsParseRequest(HTTPStatsRequest *trq)
+{
+  const char *path;
+  int pathsz;
+
+  // Make sure this is a GET request
+  path = TSHttpHdrMethodGet(trq->rqheader.buffer, trq->rqheader.header, 
&pathsz);
+  if (path != TS_HTTP_METHOD_GET) {
+    VDEBUG("%.*s method is not supported", pathsz, path);
+    return false;
+  }
+
+  return true;
+}
+
+// Handle events from TSHttpTxnServerIntercept. The intercept
+// starts with TS_EVENT_NET_ACCEPT, and then continues with
+// TSVConn events.
+static int
+HTTPStatsInterceptHook(TSCont contp, TSEvent event, void *edata)
+{
+  VDEBUG("HTTPStatsInterceptHook: %p ", edata);
+
+  argument_type arg(edata);
+
+  VDEBUG("contp=%p, event=%s (%d), edata=%p", contp, 
TSHttpEventNameLookup(event), event, arg.ptr);
+
+  switch (event) {
+  case TS_EVENT_NET_ACCEPT: {
+    // TS_EVENT_NET_ACCEPT will be delivered when the server intercept
+    // is set up by the core. We just need to allocate a stats_over_http2
+    // request state and start reading the VC.
+    HTTPStatsRequest *trq = static_cast<HTTPStatsRequest 
*>(TSContDataGet(contp));
+
+    TSStatIntIncrement(StatCountResponses, 1);
+    VDEBUG("allocated server intercept stats_over_http2 trq=%p", trq);
+
+    // This continuation was allocated in HTTPStatsTxnHook. Reset the
+    // data to keep track of this generator request.
+    TSContDataSet(contp, trq);
+
+    // Start reading the request from the server intercept VC.
+    trq->readio.read(arg.vc, contp);
+    VIODEBUG(trq->readio.vio, "started reading stats_over_http2 request");
+
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_NET_ACCEPT_FAILED: {
+    // TS_EVENT_NET_ACCEPT_FAILED will be delivered if the
+    // transaction is cancelled before we start tunnelling
+    // through the server intercept. One way that this can happen
+    // is if the intercept is attached early, and then we serve
+    // the document out of cache.
+
+    // There's nothing to do here except nuke the continuation
+    // that was allocated in HTTPStatsTxnHook().
+
+    HTTPStatsRequest *trq = static_cast<HTTPStatsRequest 
*>(TSContDataGet(contp));
+    delete trq;
+
+    TSContDestroy(contp);
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_VCONN_READ_READY: {
+    argument_type cdata           = TSContDataGet(contp);
+    HTTPStatsHttpHeader &rqheader = cdata.trq->rqheader;
+
+    VDEBUG("reading vio=%p vc=%p, trq=%p", arg.vio, TSVIOVConnGet(arg.vio), 
cdata.trq);
+
+    TSIOBufferBlock blk;
+    TSParseResult result = TS_PARSE_CONT;
+
+    for (blk = TSIOBufferReaderStart(cdata.trq->readio.reader); blk; blk = 
TSIOBufferBlockNext(blk)) {
+      const char *ptr;
+      const char *end;
+      int64_t nbytes;
+      TSHttpStatus status = static_cast<TSHttpStatus>(cdata.trq->statusCode);
+
+      ptr = TSIOBufferBlockReadStart(blk, cdata.trq->readio.reader, &nbytes);
+      if (ptr == nullptr || nbytes == 0) {
+        continue;
+      }
+
+      end    = ptr + nbytes;
+      result = TSHttpHdrParseReq(rqheader.parser, rqheader.buffer, 
rqheader.header, &ptr, end);
+      switch (result) {
+      case TS_PARSE_ERROR:
+        // If we got a bad request, just shut it down.
+        VDEBUG("bad request on trq=%p, sending an error", cdata.trq);
+        HTTPStatsRequestDestroy(cdata.trq, arg.vio, contp);
+        return TS_EVENT_ERROR;
+
+      case TS_PARSE_DONE:
+        // Check the response.
+        VDEBUG("parsed request on trq=%p, sending a response", cdata.trq);
+        if (!HTTPStatsParseRequest(cdata.trq)) {
+          status = TS_HTTP_STATUS_METHOD_NOT_ALLOWED;
+        }
+
+        // Start the vconn write.
+        cdata.trq->writeio.write(TSVIOVConnGet(arg.vio), contp);
+        TSVIONBytesSet(cdata.trq->writeio.vio, 0);
+
+        if (WriteResponseHeader(cdata.trq, contp, status) != TS_SUCCESS) {
+          VERROR("failure writing response");
+          return TS_EVENT_ERROR;
+        }
+
+        return TS_EVENT_NONE;
+
+      case TS_PARSE_CONT:
+        break;
+      }
+    }
+
+    TSReleaseAssert(result == TS_PARSE_CONT);
+
+    // Reenable the read VIO to get more events.
+    TSVIOReenable(arg.vio);
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_VCONN_WRITE_READY: {
+    argument_type cdata = TSContDataGet(contp);
+
+    if (cdata.trq->nbytes) {
+      int64_t nbytes = cdata.trq->nbytes;
+
+      VIODEBUG(arg.vio, "writing %" PRId64 " bytes for trq=%p", nbytes, 
cdata.trq);
+
+      nbytes = TSIOBufferWrite(cdata.trq->writeio.iobuf, 
cdata.trq->body.c_str(), nbytes);
+
+      cdata.trq->nbytes -= nbytes;
+      TSStatIntIncrement(StatCountBytes, nbytes);
+
+      // Update the number of bytes to write.
+      TSVIONBytesSet(arg.vio, TSVIONBytesGet(arg.vio) + nbytes);
+      TSVIOReenable(arg.vio);
+    }
+
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_ERROR:
+  case TS_EVENT_VCONN_EOS: {
+    argument_type cdata = TSContDataGet(contp);
+
+    VIODEBUG(arg.vio, "received EOS or ERROR for trq=%p", cdata.trq);
+    HTTPStatsRequestDestroy(cdata.trq, arg.vio, contp);
+    return event == TS_EVENT_ERROR ? TS_EVENT_ERROR : TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_VCONN_READ_COMPLETE:
+    // We read data forever, so we should never get a READ_COMPLETE.
+    VIODEBUG(arg.vio, "unexpected TS_EVENT_VCONN_READ_COMPLETE");
+    return TS_EVENT_NONE;
+
+  case TS_EVENT_VCONN_WRITE_COMPLETE: {
+    argument_type cdata = TSContDataGet(contp);
+
+    // If we still have bytes to write, kick off a new write operation, 
otherwise
+    // we are done and we can shut down the VC.
+    if (cdata.trq->nbytes) {
+      cdata.trq->writeio.write(TSVIOVConnGet(arg.vio), contp);
+      TSVIONBytesSet(cdata.trq->writeio.vio, cdata.trq->nbytes);
+    } else {
+      VIODEBUG(arg.vio, "TS_EVENT_VCONN_WRITE_COMPLETE %" PRId64 " todo", 
TSVIONTodoGet(arg.vio));
+      HTTPStatsRequestDestroy(cdata.trq, arg.vio, contp);
+    }
+
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_TIMEOUT: {
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_VCONN_INACTIVITY_TIMEOUT:
+    VERROR("unexpected event %s (%d) edata=%p", TSHttpEventNameLookup(event), 
event, arg.ptr);
+    return TS_EVENT_ERROR;
+
+  default:
+    VERROR("unexpected event %s (%d) edata=%p", TSHttpEventNameLookup(event), 
event, arg.ptr);
+    return TS_EVENT_ERROR;
+  }
+}
+
+static void
+HTTPStatsSetupIntercept(HTTPStatsConfig *cfg, TSHttpTxn txn)
+{
+  HTTPStatsRequest *req = HTTPStatsRequest::createHTTPStatsRequest(cfg);
+  if (req == nullptr) {
+    VERROR("could not create request!");
+    return;
+  }
+
+  TSCont cnt = TSContCreate(HTTPStatsInterceptHook, TSMutexCreate());
+  TSContDataSet(cnt, req);
+
+  TSHttpTxnServerIntercept(cnt, txn);
+
+  return;
+}
+
+// Handle events that occur on the TSHttpTxn.
+static int
+HTTPStatsTxnHook(TSCont contp, TSEvent event, void *edata)
+{
+  argument_type arg(edata);
+
+  VDEBUG("contp=%p, event=%s (%d), edata=%p", contp, 
TSHttpEventNameLookup(event), event, edata);
+
+  switch (event) {
+  case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE: {
+    int method_length, status;
+    TSMBuffer bufp;
+    TSMLoc hdr_loc;
+    const char *method;
+
+    if (TSHttpTxnCacheLookupStatusGet(arg.txn, &status) != TS_SUCCESS) {
+      VERROR("failed to get client request handle");
+      goto done;
+    }
+
+    if (TSHttpTxnClientReqGet(arg.txn, &bufp, &hdr_loc) != TS_SUCCESS) {
+      VERROR("Couldn't retrieve client request header");
+      goto done;
+    }
+
+    method = TSHttpHdrMethodGet(bufp, hdr_loc, &method_length);
+    if (nullptr == method) {
+      VERROR("Couldn't retrieve client request method");
+      goto done;
+    }
+
+    if (status != TS_CACHE_LOOKUP_HIT_FRESH || method != TS_HTTP_METHOD_GET) {
+      HTTPStatsSetupIntercept(static_cast<HTTPStatsConfig 
*>(TSContDataGet(contp)), arg.txn);
+    }
+
+    break;
+  }
+
+  default:
+    VERROR("unexpected event %s (%d)", TSHttpEventNameLookup(event), event);
+    break;
+  }
+
+done:
+  TSHttpTxnReenable(arg.txn, TS_EVENT_HTTP_CONTINUE);
+  return TS_EVENT_NONE;
+}
+
+TSReturnCode
+TSRemapInit(TSRemapInterface * /* api_info */, char * /* errbuf */, int /* 
errbuf_size */)
+{
+  if (TSStatFindName("stats_over_http2.response_bytes", &StatCountBytes) == 
TS_ERROR) {
+    StatCountBytes =
+      TSStatCreate("stats_over_http2.response_bytes", 
TS_RECORDDATATYPE_COUNTER, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+  }
+
+  if (TSStatFindName("stats_over_http2.response_count", &StatCountResponses) 
== TS_ERROR) {
+    StatCountResponses =
+      TSStatCreate("stats_over_http2.response_count", 
TS_RECORDDATATYPE_COUNTER, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_COUNT);
+  }
+  return TS_SUCCESS;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+  const TSHttpStatus txnstat = TSHttpTxnStatusGet(rh);
+  if (txnstat != TS_HTTP_STATUS_NONE && txnstat != TS_HTTP_STATUS_OK) {
+    VDEBUG("transaction status_code=%d already set; skipping processing", 
static_cast<int>(txnstat));
+    return TSREMAP_NO_REMAP;
+  }
+
+  HTTPStatsConfig *cfg = static_cast<HTTPStatsConfig *>(ih);
+
+  if (!cfg) {
+    VERROR("No remap context available, check code / config");
+    TSHttpTxnStatusSet(rh, TS_HTTP_STATUS_INTERNAL_SERVER_ERROR);
+    return TSREMAP_NO_REMAP;
+  }
+
+  if (!cfg->maxAge) {
+    TSHttpTxnConfigIntSet(rh, TS_CONFIG_HTTP_CACHE_HTTP, 0);
+    HTTPStatsSetupIntercept(static_cast<HTTPStatsConfig *>(ih), rh);
+  } else {
+    TSHttpTxnHookAdd(rh, TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, 
static_cast<HTTPStatsConfig *>(ih)->cont);
+  }
+
+  return TSREMAP_NO_REMAP; // This plugin never rewrites anything.
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf 
ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */)
+{
+  static const struct option longopt[] = {{"csv", no_argument, nullptr, 'c'},
+                                          {"integer-counters", no_argument, 
NULL, 'i'},
+                                          {"wrap-counters", no_argument, NULL, 
'w'},
+                                          {"max-age", required_argument, 
nullptr, 'a'},
+                                          {nullptr, no_argument, nullptr, 
'\0'}};
+
+  HTTPStatsConfig *cfg = new HTTPStatsConfig;
+
+  // argv contains the "to" and "from" URLs. Skip the first so that the
+  // second one poses as the program name.
+  --argc;
+  ++argv;
+  optind = 0;
+
+  while (true) {
+    int opt = getopt_long(argc, (char *const *)argv, "ciw", longopt, nullptr);
+
+    switch (opt) {
+    case 'i':
+      cfg->integer_counters = true;
+      break;
+    case 'w':
+      cfg->wrap_counters = true;
+      break;
+    case 'c':
+      cfg->csv = true;
+      break;
+    case 'a':
+      cfg->maxAge = atoi(optarg);
+      break;
+    }
+    if (opt == -1) {
+      break;
+    }
+  }
+
+  if (cfg->csv) {
+    cfg->mimeType = "text/csv";
+  } else {
+    cfg->mimeType = "application/json";
+  }
+
+  // Finally, create the continuation to use for this remap rule, tracking the 
config as cont data.
+  cfg->cont = TSContCreate(HTTPStatsTxnHook, nullptr);
+  TSContDataSet(cfg->cont, cfg);
+
+  *ih = static_cast<void *>(cfg);
+
+  return TS_SUCCESS;
+}
+
+void
+TSRemapDeleteInstance(void *ih)
+{
+  HTTPStatsConfig *cfg = static_cast<HTTPStatsConfig *>(ih);
+  delete cfg;
+}
+
+// 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.
+uint64_t
+HTTPStatsFormatter::wrap_unsigned_counter(uint64_t value)
+{
+  if (wrap_counters) {
+    return (value > INT64_MAX) ? value % INT64_MAX : value;
+  }
+  return value;
+};
+
+#define APPEND(b, a) b += a
+#define APPEND_STAT_JSON(c, a, fmt, v)                                         
  \
+  do {                                                                         
  \
+    char b[256];                                                               
  \
+    if (snprintf(b, sizeof(b), "\"%s\": \"" fmt "\",\n", a, v) < 
(int)sizeof(b)) \
+      APPEND(c->buf, b);                                                       
  \
+  } while (0)
+#define APPEND_STAT_JSON_NUMERIC(f, a, fmt, v)                                 
      \
+  do {                                                                         
      \
+    char b[256];                                                               
      \
+    if (f->integer_counters) {                                                 
      \
+      if (snprintf(b, sizeof(b), "\"%s\": " fmt ",\n", a, v) < (int)sizeof(b)) 
{     \
+        APPEND(f->buf, b);                                                     
      \
+      }                                                                        
      \
+    } else {                                                                   
      \
+      if (snprintf(b, sizeof(b), "\"%s\": \"" fmt "\",\n", a, v) < 
(int)sizeof(b)) { \
+        APPEND(f->buf, b);                                                     
      \
+      }                                                                        
      \
+    }                                                                          
      \
+  } while (0)
+
+#define APPEND_STAT_CSV(f, a, fmt, v)                                  \
+  do {                                                                 \
+    char b[256];                                                       \
+    if (snprintf(b, sizeof(b), "%s," fmt "\n", a, v) < (int)sizeof(b)) \
+      APPEND(f->buf, b);                                               \
+  } while (0)
+#define APPEND_STAT_CSV_NUMERIC(f, a, fmt, v)                            \
+  do {                                                                   \
+    char b[256];                                                         \
+    if (snprintf(b, sizeof(b), "%s," fmt "\n", a, v) < (int)sizeof(b)) { \
+      APPEND(f->buf, b);                                                 \
+    }                                                                    \
+  } while (0)
+
+static void
+json_out_stat(TSRecordType rec_type, void *edata, int registered, const char 
*name, TSRecordDataType data_type, TSRecordData *datum)
+{
+  HTTPStatsFormatter *fmtr = static_cast<HTTPStatsFormatter *>(edata);
+
+  switch (data_type) {
+  case TS_RECORDDATATYPE_COUNTER:
+    APPEND_STAT_JSON_NUMERIC(fmtr, name, "%" PRIu64, 
fmtr->wrap_unsigned_counter(datum->rec_counter));
+    break;
+  case TS_RECORDDATATYPE_INT:
+    APPEND_STAT_JSON_NUMERIC(fmtr, name, "%" PRIu64, 
fmtr->wrap_unsigned_counter(datum->rec_int));
+    break;
+  case TS_RECORDDATATYPE_FLOAT:
+    APPEND_STAT_JSON_NUMERIC(fmtr, name, "%f", datum->rec_float);
+    break;
+  case TS_RECORDDATATYPE_STRING:
+    APPEND_STAT_JSON(fmtr, name, "%s", datum->rec_string);
+    break;
+  default:
+    TSDebug(PLUGIN, "unknown type for %s: %d", name, data_type);
+    break;
+  }
+}
+
+static void
+csv_out_stat(TSRecordType rec_type, void *edata, int registered, const char 
*name, TSRecordDataType data_type, TSRecordData *datum)
+{
+  HTTPStatsFormatter *fmtr = static_cast<HTTPStatsFormatter *>(edata);
+
+  switch (data_type) {
+  case TS_RECORDDATATYPE_COUNTER:
+    APPEND_STAT_CSV_NUMERIC(fmtr, name, "%" PRIu64, 
fmtr->wrap_unsigned_counter(datum->rec_counter));
+    break;
+  case TS_RECORDDATATYPE_INT:
+    APPEND_STAT_CSV_NUMERIC(fmtr, name, "%" PRIu64, 
fmtr->wrap_unsigned_counter(datum->rec_int));
+    break;
+  case TS_RECORDDATATYPE_FLOAT:
+    APPEND_STAT_CSV_NUMERIC(fmtr, name, "%f", datum->rec_float);
+    break;
+  case TS_RECORDDATATYPE_STRING:
+    APPEND_STAT_CSV(fmtr, name, "%s", datum->rec_string);
+    break;
+  default:
+    TSDebug(PLUGIN, "unknown type for %s: %d", name, data_type);
+    break;
+  }
+}
+
+std::string
+HTTPStatsFormatter::output()
+{
+  if (csv) {
+    TSRecordDump((TSRecordType)(TS_RECORDTYPE_PLUGIN | TS_RECORDTYPE_NODE | 
TS_RECORDTYPE_PROCESS), csv_out_stat, this);
+    APPEND_STAT_CSV(this, "version", "%s", TSTrafficServerVersionGet());
+  } else {
+    APPEND(buf, "{ \"global\": {\n");
+
+    TSRecordDump((TSRecordType)(TS_RECORDTYPE_PLUGIN | TS_RECORDTYPE_NODE | 
TS_RECORDTYPE_PROCESS), json_out_stat, this);
+
+    APPEND(buf, "\"server\": \"");
+    APPEND(buf, TSTrafficServerVersionGet());
+    APPEND(buf, "\"\n");
+    APPEND(buf, "  }\n}\n");
+  }
+  return buf;
+}

Reply via email to