blob: 98d39f2eeee9d8c32a29d6d9e3814f29788536e4 [file]
/** @file
A brief file description
@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.
*/
/* stats.c: expose traffic server stats over http
*/
#include <arpa/inet.h>
#include <cctype>
#include <chrono>
#include <cinttypes>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <fstream>
#include <getopt.h>
#include <netinet/in.h>
#include <string>
#include <string_view>
#include <sys/stat.h>
#include <ts/ts.h>
#include <unordered_map>
#include <utility>
#include <vector>
#include <unistd.h>
#include <zlib.h>
#include <ts/remap.h>
#include "swoc/TextView.h"
#include "tscore/ink_config.h"
#include <tsutil/ts_ip.h>
#include <tsutil/StringCompare.h>
#if HAVE_BROTLI_ENCODE_H
#include <brotli/encode.h>
#endif
#define PLUGIN_NAME "stats_over_http"
#define FREE_TMOUT 300000
#define STR_BUFFER_SIZE 1024
#define SYSTEM_RECORD_TYPE (0x100)
#define DEFAULT_RECORD_TYPES (SYSTEM_RECORD_TYPE | TS_RECORDTYPE_PROCESS | TS_RECORDTYPE_PLUGIN)
static DbgCtl dbg_ctl{PLUGIN_NAME};
static const swoc::IP4Range DEFAULT_IP{swoc::IP4Addr::MIN, swoc::IP4Addr::MAX};
static const swoc::IP6Range DEFAULT_IP6{swoc::IP6Addr::MIN, swoc::IP6Addr::MAX};
/* global holding the path used for access to this JSON data */
std::string const DEFAULT_URL_PATH = "_stats";
// from mod_deflate:
// ZLIB's compression algorithm uses a
// 0-9 based scale that GZIP does where '1' is 'Best speed'
// and '9' is 'Best compression'. Testing has proved level '6'
// to be about the best level to use in an HTTP Server.
const int ZLIB_COMPRESSION_LEVEL = 6;
const char *dictionary = nullptr;
// zlib stuff, see [deflateInit2] at http://www.zlib.net/manual.html
static const int ZLIB_MEMLEVEL = 9; // min=1 (optimize for memory),max=9 (optimized for speed)
static const int WINDOW_BITS_DEFLATE = 15;
static const int WINDOW_BITS_GZIP = 16;
#define DEFLATE_MODE WINDOW_BITS_DEFLATE
#define GZIP_MODE (WINDOW_BITS_DEFLATE | WINDOW_BITS_GZIP)
// brotli compression quality 1-11. Testing proved level '6'
#if HAVE_BROTLI_ENCODE_H
const int BROTLI_COMPRESSION_LEVEL = 6;
const int BROTLI_LGW = 16;
#endif
static bool integer_counters = false;
static bool wrap_counters = false;
#if defined(__cpp_lib_constexpr_string) && __cpp_lib_constexpr_string >= 201907L && (!defined(__clang__) || __clang_major__ > 16)
#define STATS_OVER_HTTP_HAS_CONSTEXPR_STRING 1
#else
#define STATS_OVER_HTTP_HAS_CONSTEXPR_STRING 0
#endif
struct prometheus_v2_metric {
std::string name;
std::string labels;
};
struct prometheus_v2_metric_family {
TSRecordDataType data_type = TS_RECORDDATATYPE_NULL;
std::string help;
std::vector<std::string> samples;
};
using prometheus_v2_metric_family_map = std::unordered_map<std::string, prometheus_v2_metric_family>;
struct config_t {
unsigned int recordTypes;
std::string stats_path;
swoc::IPRangeSet addrs;
};
struct config_holder_t {
char *config_path;
volatile time_t last_load;
config_t *config;
};
enum class output_format_t { JSON_OUTPUT, CSV_OUTPUT, PROMETHEUS_OUTPUT, PROMETHEUS_V2_OUTPUT };
enum class encoding_format_t { NONE, DEFLATE, GZIP, BR };
int configReloadRequests = 0;
int configReloads = 0;
time_t lastReloadRequest = 0;
time_t lastReload = 0;
time_t astatsLoad = 0;
static int free_handler(TSCont cont, TSEvent event, void *edata);
static int config_handler(TSCont cont, TSEvent event, void *edata);
static config_t *get_config(TSCont cont);
static config_holder_t *new_config_holder(const char *path);
static bool is_ipmap_allowed(const config_t *config, const struct sockaddr *addr);
#if HAVE_BROTLI_ENCODE_H
struct b_stream {
BrotliEncoderState *br = nullptr;
uint8_t *next_in = nullptr;
size_t avail_in = 0;
uint8_t *next_out = nullptr;
size_t avail_out = 0;
size_t total_in = 0;
size_t total_out = 0;
~b_stream()
{
if (br) {
BrotliEncoderDestroyInstance(br);
br = nullptr;
}
}
};
#endif
struct stats_state {
TSVConn net_vc = nullptr;
TSVIO read_vio = nullptr;
TSVIO write_vio = nullptr;
TSIOBuffer req_buffer = nullptr;
TSIOBuffer resp_buffer = nullptr;
TSIOBufferReader resp_reader = nullptr;
int64_t output_bytes = 0;
int body_written = 0;
output_format_t output_format = output_format_t::JSON_OUTPUT;
encoding_format_t encoding = encoding_format_t::NONE;
z_stream zstrm;
prometheus_v2_metric_family_map prometheus_v2_families;
std::vector<std::string> prometheus_v2_family_order;
#if HAVE_BROTLI_ENCODE_H
b_stream bstrm;
#endif
stats_state()
{
memset(&zstrm, 0, sizeof(z_stream));
zstrm.zalloc = Z_NULL;
zstrm.zfree = Z_NULL;
zstrm.opaque = Z_NULL;
zstrm.data_type = Z_ASCII;
}
};
static char *
nstr(const char *s)
{
if (s == nullptr) {
return nullptr;
}
char *mys = (char *)TSmalloc(strlen(s) + 1);
strcpy(mys, s);
return mys;
}
#if HAVE_BROTLI_ENCODE_H
encoding_format_t
init_br(stats_state *my_state)
{
my_state->bstrm.br = nullptr;
my_state->bstrm.br = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr);
if (!my_state->bstrm.br) {
Dbg(dbg_ctl, "Brotli Encoder Instance Failed");
return encoding_format_t::NONE;
}
BrotliEncoderSetParameter(my_state->bstrm.br, BROTLI_PARAM_QUALITY, BROTLI_COMPRESSION_LEVEL);
BrotliEncoderSetParameter(my_state->bstrm.br, BROTLI_PARAM_LGWIN, BROTLI_LGW);
my_state->bstrm.next_in = nullptr;
my_state->bstrm.avail_in = 0;
my_state->bstrm.total_in = 0;
my_state->bstrm.next_out = nullptr;
my_state->bstrm.avail_out = 0;
my_state->bstrm.total_out = 0;
return encoding_format_t::BR;
}
#endif
namespace
{
inline uint64_t
ms_since_epoch()
{
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}
} // namespace
encoding_format_t
init_gzip(stats_state *my_state, int mode)
{
my_state->zstrm.next_in = Z_NULL;
my_state->zstrm.avail_in = 0;
my_state->zstrm.total_in = 0;
my_state->zstrm.next_out = Z_NULL;
my_state->zstrm.avail_out = 0;
my_state->zstrm.total_out = 0;
my_state->zstrm.zalloc = Z_NULL;
my_state->zstrm.zfree = Z_NULL;
my_state->zstrm.opaque = Z_NULL;
my_state->zstrm.data_type = Z_ASCII;
int err = deflateInit2(&my_state->zstrm, ZLIB_COMPRESSION_LEVEL, Z_DEFLATED, mode, ZLIB_MEMLEVEL, Z_DEFAULT_STRATEGY);
if (err != Z_OK) {
Dbg(dbg_ctl, "gzip initialization failed");
return encoding_format_t::NONE;
} else {
Dbg(dbg_ctl, "gzip initialized successfully");
if (mode == GZIP_MODE) {
return encoding_format_t::GZIP;
} else if (mode == DEFLATE_MODE) {
return encoding_format_t::DEFLATE;
}
}
return encoding_format_t::NONE;
}
static void
stats_cleanup(TSCont contp, stats_state *my_state)
{
if (my_state->req_buffer) {
TSIOBufferDestroy(my_state->req_buffer);
my_state->req_buffer = nullptr;
}
if (my_state->resp_buffer) {
TSIOBufferDestroy(my_state->resp_buffer);
my_state->resp_buffer = nullptr;
}
if (my_state->net_vc != nullptr) {
TSVConnClose(my_state->net_vc);
}
delete my_state;
TSContDestroy(contp);
}
static void
stats_process_accept(TSCont contp, stats_state *my_state)
{
my_state->req_buffer = TSIOBufferCreate();
my_state->resp_buffer = TSIOBufferCreate();
my_state->resp_reader = TSIOBufferReaderAlloc(my_state->resp_buffer);
my_state->read_vio = TSVConnRead(my_state->net_vc, contp, my_state->req_buffer, INT64_MAX);
}
static int64_t
stats_add_data_to_resp_buffer(const char *s, stats_state *my_state)
{
if (s == nullptr) {
return 0;
}
int64_t s_len = strlen(s);
int64_t bytes_written = TSIOBufferWrite(my_state->resp_buffer, s, s_len);
if (bytes_written == TS_ERROR) {
return 0;
}
return bytes_written;
}
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_JSON_GZIP[] =
"HTTP/1.0 200 OK\r\nContent-Type: text/json\r\nContent-Encoding: gzip\r\nCache-Control: no-cache\r\n\r\n";
static const char RESP_HEADER_JSON_DEFLATE[] =
"HTTP/1.0 200 OK\r\nContent-Type: text/json\r\nContent-Encoding: deflate\r\nCache-Control: no-cache\r\n\r\n";
static const char RESP_HEADER_JSON_BR[] =
"HTTP/1.0 200 OK\r\nContent-Type: text/json\r\nContent-Encoding: br\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 const char RESP_HEADER_CSV_GZIP[] =
"HTTP/1.0 200 OK\r\nContent-Type: text/csv\r\nContent-Encoding: gzip\r\nCache-Control: no-cache\r\n\r\n";
static const char RESP_HEADER_CSV_DEFLATE[] =
"HTTP/1.0 200 OK\r\nContent-Type: text/csv\r\nContent-Encoding: deflate\r\nCache-Control: no-cache\r\n\r\n";
static const char RESP_HEADER_CSV_BR[] =
"HTTP/1.0 200 OK\r\nContent-Type: text/csv\r\nContent-Encoding: br\r\nCache-Control: no-cache\r\n\r\n";
static const char RESP_HEADER_PROMETHEUS[] =
"HTTP/1.0 200 OK\r\nContent-Type: text/plain; version=0.0.4; charset=utf-8\r\nCache-Control: no-cache\r\n\r\n";
static const char RESP_HEADER_PROMETHEUS_GZIP[] = "HTTP/1.0 200 OK\r\nContent-Type: text/plain; version=0.0.4; "
"charset=utf-8\r\nContent-Encoding: gzip\r\nCache-Control: no-cache\r\n\r\n";
static const char RESP_HEADER_PROMETHEUS_DEFLATE[] =
"HTTP/1.0 200 OK\r\nContent-Type: text/plain; version=0.0.4; charset=utf-8\r\nContent-Encoding: deflate\r\nCache-Control: "
"no-cache\r\n\r\n";
static const char RESP_HEADER_PROMETHEUS_BR[] = "HTTP/1.0 200 OK\r\nContent-Type: text/plain; version=0.0.4; "
"charset=utf-8\r\nContent-Encoding: br\r\nCache-Control: no-cache\r\n\r\n";
static const char RESP_HEADER_PROMETHEUS_V2[] =
"HTTP/1.0 200 OK\r\nContent-Type: text/plain; version=2.0.0; charset=utf-8\r\nCache-Control: no-cache\r\n\r\n";
static const char RESP_HEADER_PROMETHEUS_V2_GZIP[] = "HTTP/1.0 200 OK\r\nContent-Type: text/plain; version=2.0.0; "
"charset=utf-8\r\nContent-Encoding: gzip\r\nCache-Control: no-cache\r\n\r\n";
static const char RESP_HEADER_PROMETHEUS_V2_DEFLATE[] =
"HTTP/1.0 200 OK\r\nContent-Type: text/plain; version=2.0.0; charset=utf-8\r\nContent-Encoding: deflate\r\nCache-Control: "
"no-cache\r\n\r\n";
static const char RESP_HEADER_PROMETHEUS_V2_BR[] = "HTTP/1.0 200 OK\r\nContent-Type: text/plain; version=2.0.0; "
"charset=utf-8\r\nContent-Encoding: br\r\nCache-Control: no-cache\r\n\r\n";
static int64_t
stats_add_resp_header(stats_state *my_state)
{
switch (my_state->output_format) {
case output_format_t::JSON_OUTPUT:
if (my_state->encoding == encoding_format_t::GZIP) {
return stats_add_data_to_resp_buffer(RESP_HEADER_JSON_GZIP, my_state);
} else if (my_state->encoding == encoding_format_t::DEFLATE) {
return stats_add_data_to_resp_buffer(RESP_HEADER_JSON_DEFLATE, my_state);
} else if (my_state->encoding == encoding_format_t::BR) {
return stats_add_data_to_resp_buffer(RESP_HEADER_JSON_BR, my_state);
} else {
return stats_add_data_to_resp_buffer(RESP_HEADER_JSON, my_state);
}
break;
case output_format_t::CSV_OUTPUT:
if (my_state->encoding == encoding_format_t::GZIP) {
return stats_add_data_to_resp_buffer(RESP_HEADER_CSV_GZIP, my_state);
} else if (my_state->encoding == encoding_format_t::DEFLATE) {
return stats_add_data_to_resp_buffer(RESP_HEADER_CSV_DEFLATE, my_state);
} else if (my_state->encoding == encoding_format_t::BR) {
return stats_add_data_to_resp_buffer(RESP_HEADER_CSV_BR, my_state);
} else {
return stats_add_data_to_resp_buffer(RESP_HEADER_CSV, my_state);
}
break;
case output_format_t::PROMETHEUS_OUTPUT:
if (my_state->encoding == encoding_format_t::GZIP) {
return stats_add_data_to_resp_buffer(RESP_HEADER_PROMETHEUS_GZIP, my_state);
} else if (my_state->encoding == encoding_format_t::DEFLATE) {
return stats_add_data_to_resp_buffer(RESP_HEADER_PROMETHEUS_DEFLATE, my_state);
} else if (my_state->encoding == encoding_format_t::BR) {
return stats_add_data_to_resp_buffer(RESP_HEADER_PROMETHEUS_BR, my_state);
} else {
return stats_add_data_to_resp_buffer(RESP_HEADER_PROMETHEUS, my_state);
}
break;
case output_format_t::PROMETHEUS_V2_OUTPUT:
if (my_state->encoding == encoding_format_t::GZIP) {
return stats_add_data_to_resp_buffer(RESP_HEADER_PROMETHEUS_V2_GZIP, my_state);
} else if (my_state->encoding == encoding_format_t::DEFLATE) {
return stats_add_data_to_resp_buffer(RESP_HEADER_PROMETHEUS_V2_DEFLATE, my_state);
} else if (my_state->encoding == encoding_format_t::BR) {
return stats_add_data_to_resp_buffer(RESP_HEADER_PROMETHEUS_V2_BR, my_state);
} else {
return stats_add_data_to_resp_buffer(RESP_HEADER_PROMETHEUS_V2, my_state);
}
break;
}
// Not reached.
return stats_add_data_to_resp_buffer(RESP_HEADER_JSON, my_state);
}
static void
stats_process_read(TSCont contp, TSEvent event, stats_state *my_state)
{
Dbg(dbg_ctl, "stats_process_read(%d)", event);
if (event == TS_EVENT_VCONN_READ_READY) {
my_state->output_bytes = stats_add_resp_header(my_state);
TSVConnShutdown(my_state->net_vc, 1, 0);
my_state->write_vio = TSVConnWrite(my_state->net_vc, contp, my_state->resp_reader, INT64_MAX);
} else if (event == TS_EVENT_ERROR) {
TSError("[%s] stats_process_read: Received TS_EVENT_ERROR", PLUGIN_NAME);
} else if (event == TS_EVENT_VCONN_EOS) {
/* client may end the connection, simply return */
return;
} else if (event == TS_EVENT_NET_ACCEPT_FAILED) {
TSError("[%s] stats_process_read: Received TS_EVENT_NET_ACCEPT_FAILED", PLUGIN_NAME);
} else {
printf("Unexpected Event %d\n", event);
TSReleaseAssert(!"Unexpected Event");
}
}
#define APPEND(a) my_state->output_bytes += stats_add_data_to_resp_buffer(a, my_state)
//-----------------------------------------------------------------------------
// JSON Formatters
//-----------------------------------------------------------------------------
#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_JSON_NUMERIC(a, fmt, v) \
do { \
char b[256]; \
if (integer_counters) { \
if (snprintf(b, sizeof(b), "\"%s\": " fmt ",\n", a, v) < (int)sizeof(b)) { \
APPEND(b); \
} \
} else { \
if (snprintf(b, sizeof(b), "\"%s\": \"" fmt "\",\n", a, v) < (int)sizeof(b)) { \
APPEND(b); \
} \
} \
} while (0)
//-----------------------------------------------------------------------------
// CSV Formatters
//-----------------------------------------------------------------------------
#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)
//-----------------------------------------------------------------------------
// Prometheus Formatters
//-----------------------------------------------------------------------------
// Note that Prometheus only supports numeric types.
#define APPEND_STAT_PROMETHEUS_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
wrap_unsigned_counter(uint64_t value)
{
if (wrap_counters) {
return (value > INT64_MAX) ? value % INT64_MAX : value;
} else {
return value;
}
}
static void
json_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 = static_cast<stats_state *>(edata);
switch (data_type) {
case TS_RECORDDATATYPE_COUNTER:
APPEND_STAT_JSON_NUMERIC(name, "%" PRIu64, wrap_unsigned_counter(datum->rec_counter));
break;
case TS_RECORDDATATYPE_INT:
APPEND_STAT_JSON_NUMERIC(name, "%" PRIu64, wrap_unsigned_counter(datum->rec_int));
break;
case TS_RECORDDATATYPE_FLOAT:
APPEND_STAT_JSON_NUMERIC(name, "%f", datum->rec_float);
break;
case TS_RECORDDATATYPE_STRING:
APPEND_STAT_JSON(name, "%s", datum->rec_string);
break;
default:
Dbg(dbg_ctl, "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 = static_cast<stats_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:
Dbg(dbg_ctl, "unknown type for %s: %d", name, data_type);
break;
}
}
/** Replace characters offensive to Prometheus with '_'.
*
* See: https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
*
* > Metric names SHOULD match the regex [a-zA-Z_:][a-zA-Z0-9_:]*
* > for the best experience and compatibility
*
* @param[in] name The metric name to sanitize.
* @return A sanitized metric name.
*/
#if STATS_OVER_HTTP_HAS_CONSTEXPR_STRING
static constexpr std::string
#else
static std::string
#endif
sanitize_metric_name_for_prometheus(std::string_view name)
{
std::string sanitized_name(name);
// If the first character is a digit, prepend an underscore since Prometheus
// doesn't allow digits as the first character.
if (sanitized_name.length() > 0 && sanitized_name[0] >= '0' && sanitized_name[0] <= '9') {
sanitized_name = "_" + sanitized_name;
}
// Convert characters that Prometheus doesn't like to '_'.
// : letters (a-z, A-Z), digits (0-9), underscores (_), and colons (:).
for (auto &c : sanitized_name) {
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == ':')) {
c = '_';
}
}
return sanitized_name;
}
static std::string
escape_prometheus_v2_label_value(std::string_view val)
{
size_t escaped_len = 0;
for (char c : val) {
if (c == '"' || c == '\\' || c == '\n') {
escaped_len += 2;
} else {
escaped_len += 1;
}
}
std::string escaped;
if (escaped_len > 0) {
escaped.reserve(escaped_len);
for (char c : val) {
if (c == '"' || c == '\\') {
escaped += '\\';
escaped += c;
} else if (c == '\n') {
escaped += "\\n";
} else {
escaped += c;
}
}
}
return escaped;
}
static void
append_prometheus_v2_label(std::string &labels, std::string_view key, std::string_view val)
{
if (!labels.empty()) {
labels += ", ";
}
labels += key;
labels += "=\"";
labels += escape_prometheus_v2_label_value(val);
labels += "\"";
}
static bool
contains_prometheus_v2_token(const std::string_view *tokens, size_t size, std::string_view token)
{
for (size_t i = 0; i < size; ++i) {
if (tokens[i] == token) {
return true;
}
}
return false;
}
static swoc::TextView
take_prometheus_v2_token(swoc::TextView &view)
{
size_t sep = view.find_first_of("._[]");
swoc::TextView token;
if (sep == swoc::TextView::npos) {
token = view;
view.clear();
} else {
token = view.prefix(sep);
view.remove_prefix(sep + 1);
}
return token;
}
/** Parse a Prometheus v2 metric name and return the base name and labels.
*
* @param[in] name The metric name to parse.
* @return A prometheus_v2_metric struct containing the base name and labels.
*/
static prometheus_v2_metric
parse_metric_v2(std::string_view name)
{
swoc::TextView name_view{name};
std::string labels;
std::string base_name;
constexpr std::string_view methods[] = {"get", "post", "head", "put", "delete", "options", "trace", "connect", "push", "purge"};
constexpr std::string_view directions[] = {"incoming", "outgoing"};
constexpr std::string_view results[] = {"hit", "miss", "error", "errors", "success", "failure"};
constexpr std::string_view categories[] = {"volume", "thread", "interface", "net", "host", "port"};
while (!name_view.empty()) {
swoc::TextView token = take_prometheus_v2_token(name_view);
if (token.empty()) {
continue;
}
bool token_handled = false;
// Status codes (200, 4xx, etc.)
if (token.length() == 3 && (token[0] >= '0' && token[0] <= '9') && ((token[1] >= '0' && token[1] <= '9') || token[1] == 'x') &&
((token[2] >= '0' && token[2] <= '9') || token[2] == 'x')) {
append_prometheus_v2_label(labels, "status", token);
token_handled = true;
}
// Direction (incoming / outgoing)
else if (contains_prometheus_v2_token(directions, sizeof(directions) / sizeof(directions[0]), token)) {
append_prometheus_v2_label(labels, "direction", token);
token_handled = true;
}
// Multi-token method categories.
else if (token == "extension" || token == "invalid") {
swoc::TextView next = name_view;
swoc::TextView next_token = take_prometheus_v2_token(next);
if (token == "extension" && next_token == "method") {
append_prometheus_v2_label(labels, "method", "extension_method");
name_view = next;
token_handled = true;
} else if (token == "invalid" && next_token == "client") {
append_prometheus_v2_label(labels, "method", "invalid_client");
name_view = next;
token_handled = true;
}
}
// Methods
else if (contains_prometheus_v2_token(methods, sizeof(methods) / sizeof(methods[0]), token)) {
append_prometheus_v2_label(labels, "method", token);
token_handled = true;
}
// Generic Categories + Index (volume, 0, etc.)
else if (contains_prometheus_v2_token(categories, sizeof(categories) / sizeof(categories[0]), token)) {
swoc::TextView next = name_view;
swoc::TextView id = take_prometheus_v2_token(next);
bool is_id = !id.empty();
for (char c : id) {
if (!(c >= '0' && c <= '9') && c != 'x') {
is_id = false;
break;
}
}
if (is_id) {
append_prometheus_v2_label(labels, token, id);
if (!base_name.empty()) {
base_name += ".";
}
base_name += token;
name_view = next;
token_handled = true;
}
}
// Results (hit, miss)
else if (contains_prometheus_v2_token(results, sizeof(results) / sizeof(results[0]), token)) {
// 'hit' and 'miss' are almost always labels.
if (token == "hit" || token == "miss" || !name_view.empty()) {
append_prometheus_v2_label(labels, "result", token);
token_handled = true;
}
}
// Buckets (e.g., 10ms)
else {
constexpr std::string_view units[] = {"ms", "us", "s"};
for (const auto &unit : units) {
size_t unit_len = unit.length();
if (token.length() > unit_len && token.substr(token.length() - unit_len) == unit) {
bool all_digits = true;
for (size_t j = 0; j < token.length() - unit_len; ++j) {
if (!(token[j] >= '0' && token[j] <= '9')) {
all_digits = false;
break;
}
}
if (all_digits && token.length() > unit_len) {
append_prometheus_v2_label(labels, "le", token);
token_handled = true;
break;
}
}
}
}
if (!token_handled) {
if (!base_name.empty()) {
base_name += ".";
}
base_name += token;
}
}
return {std::move(base_name), std::move(labels)};
}
static bool
format_prometheus_v2_sample(std::string &sample, const std::string &name, const std::string &labels, TSRecordDataType data_type,
TSRecordData *datum)
{
char val_buffer[128];
int len = 0;
if (data_type == TS_RECORDDATATYPE_COUNTER) {
len = snprintf(val_buffer, sizeof(val_buffer), "%" PRIu64 "\n", wrap_unsigned_counter(datum->rec_counter));
} else if (data_type == TS_RECORDDATATYPE_INT) {
len = snprintf(val_buffer, sizeof(val_buffer), "%" PRIu64 "\n", wrap_unsigned_counter(datum->rec_int));
} else if (data_type == TS_RECORDDATATYPE_FLOAT) {
len = snprintf(val_buffer, sizeof(val_buffer), "%g\n", datum->rec_float);
}
if (len <= 0 || len >= static_cast<int>(sizeof(val_buffer))) {
return false;
}
sample.reserve(name.size() + labels.size() + static_cast<size_t>(len) + 3);
sample += name;
if (!labels.empty()) {
sample += "{";
sample += labels;
sample += "}";
}
sample += " ";
sample += val_buffer;
return true;
}
static void
prometheus_v2_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 = static_cast<stats_state *>(edata);
if (data_type == TS_RECORDDATATYPE_STRING) {
return; // Prometheus does not support string values.
}
auto v2 = parse_metric_v2(name);
std::string sanitized_name = sanitize_metric_name_for_prometheus(v2.name);
if (sanitized_name.empty()) {
return;
}
std::string sample;
if (!format_prometheus_v2_sample(sample, sanitized_name, v2.labels, data_type, datum)) {
return;
}
// Note: Prometheus requires all metrics with the same name to have the same type.
// If Traffic Server metrics with different types (e.g., COUNTER and INT) are collapsed
// into the same base name, the first one encountered will determine the reported TYPE.
auto [it, inserted] = my_state->prometheus_v2_families.try_emplace(sanitized_name);
if (inserted) {
it->second.data_type = data_type;
it->second.help = name;
my_state->prometheus_v2_family_order.emplace_back(sanitized_name);
} else {
// Validate type consistency (at least between counter and gauge).
bool prev_is_counter = (it->second.data_type == TS_RECORDDATATYPE_COUNTER);
bool curr_is_counter = (data_type == TS_RECORDDATATYPE_COUNTER);
if (prev_is_counter != curr_is_counter) {
Dbg(dbg_ctl, "Inconsistent types for base metric %s: previously %s, now %s. Labels: %s", sanitized_name.c_str(),
prev_is_counter ? "counter" : "gauge", curr_is_counter ? "counter" : "gauge", v2.labels.c_str());
}
}
it->second.samples.emplace_back(std::move(sample));
}
static void
prometheus_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 = static_cast<stats_state *>(edata);
std::string sanitized_name = sanitize_metric_name_for_prometheus(name);
if (sanitized_name.empty()) {
return;
}
switch (data_type) {
case TS_RECORDDATATYPE_COUNTER:
APPEND("# HELP ");
APPEND(sanitized_name.c_str());
APPEND(" ");
APPEND(name);
APPEND("\n");
APPEND("# TYPE ");
APPEND(sanitized_name.c_str());
APPEND(" counter\n");
APPEND_STAT_PROMETHEUS_NUMERIC(sanitized_name.c_str(), "%" PRIu64, wrap_unsigned_counter(datum->rec_counter));
break;
case TS_RECORDDATATYPE_INT:
APPEND("# HELP ");
APPEND(sanitized_name.c_str());
APPEND(" ");
APPEND(name);
APPEND("\n");
APPEND("# TYPE ");
APPEND(sanitized_name.c_str());
APPEND(" gauge\n");
APPEND_STAT_PROMETHEUS_NUMERIC(sanitized_name.c_str(), "%" PRIu64, wrap_unsigned_counter(datum->rec_int));
break;
case TS_RECORDDATATYPE_FLOAT:
APPEND("# HELP ");
APPEND(sanitized_name.c_str());
APPEND(" ");
APPEND(name);
APPEND("\n");
APPEND_STAT_PROMETHEUS_NUMERIC(sanitized_name.c_str(), "%g", datum->rec_float);
break;
case TS_RECORDDATATYPE_STRING:
Dbg(dbg_ctl, "Prometheus does not support string values, skipping: %s", sanitized_name.c_str());
break;
default:
Dbg(dbg_ctl, "unknown type for %s: %d", sanitized_name.c_str(), data_type);
break;
}
}
static void
json_out_stats(stats_state *my_state)
{
const char *version;
APPEND("{ \"global\": {\n");
TSRecordDump((TSRecordType)(TS_RECORDTYPE_PLUGIN | TS_RECORDTYPE_NODE | TS_RECORDTYPE_PROCESS), json_out_stat, my_state);
version = TSTrafficServerVersionGet();
APPEND_STAT_JSON_NUMERIC("current_time_epoch_ms", "%" PRIu64, ms_since_epoch());
APPEND("\"server\": \"");
APPEND(version);
APPEND("\"\n");
APPEND(" }\n}\n");
}
#if HAVE_BROTLI_ENCODE_H
// Takes an input stats state struct holding the uncompressed
// stats values. Compresses and copies it back into the state struct
static void
br_out_stats(stats_state *my_state)
{
size_t outputsize = BrotliEncoderMaxCompressedSize(my_state->output_bytes);
uint8_t inputbuf[my_state->output_bytes];
uint8_t outputbuf[outputsize];
memset(&inputbuf, 0, sizeof(inputbuf));
memset(&outputbuf, 0, sizeof(outputbuf));
int64_t inputbytes = TSIOBufferReaderCopy(my_state->resp_reader, &inputbuf, my_state->output_bytes);
// Consume existing uncompressed buffer now that it has been stored to
// free up the buffer to contain the compressed data
int64_t toconsume = TSIOBufferReaderAvail(my_state->resp_reader);
TSIOBufferReaderConsume(my_state->resp_reader, toconsume);
my_state->output_bytes -= toconsume;
BROTLI_BOOL err = BrotliEncoderCompress(BROTLI_DEFAULT_QUALITY, BROTLI_DEFAULT_WINDOW, BROTLI_DEFAULT_MODE, inputbytes, inputbuf,
&outputsize, outputbuf);
if (err == BROTLI_FALSE) {
Dbg(dbg_ctl, "brotli compress error");
}
my_state->output_bytes += TSIOBufferWrite(my_state->resp_buffer, outputbuf, outputsize);
BrotliEncoderDestroyInstance(my_state->bstrm.br);
my_state->bstrm.br = nullptr;
}
#endif
// Takes an input stats state struct holding the uncompressed
// stats values. Compresses and copies it back into the state struct
static void
gzip_out_stats(stats_state *my_state)
{
char inputbuf[my_state->output_bytes];
char outputbuf[deflateBound(&my_state->zstrm, my_state->output_bytes)];
memset(&inputbuf, 0, sizeof(inputbuf));
memset(&outputbuf, 0, sizeof(outputbuf));
int64_t inputbytes = TSIOBufferReaderCopy(my_state->resp_reader, &inputbuf, my_state->output_bytes);
// Consume existing uncompressed buffer now that it has been stored to
// free up the buffer to contain the compressed data
int64_t toconsume = TSIOBufferReaderAvail(my_state->resp_reader);
TSIOBufferReaderConsume(my_state->resp_reader, toconsume);
my_state->output_bytes -= toconsume;
my_state->zstrm.avail_in = inputbytes;
my_state->zstrm.avail_out = sizeof(outputbuf);
my_state->zstrm.next_in = (Bytef *)inputbuf;
my_state->zstrm.next_out = (Bytef *)outputbuf;
int err = deflate(&my_state->zstrm, Z_FINISH);
if (err != Z_STREAM_END) {
Dbg(dbg_ctl, "deflate error: %d", err);
}
err = deflateEnd(&my_state->zstrm);
if (err != Z_OK) {
Dbg(dbg_ctl, "deflate end err: %d", err);
}
my_state->output_bytes += TSIOBufferWrite(my_state->resp_buffer, outputbuf, my_state->zstrm.total_out);
}
static void
csv_out_stats(stats_state *my_state)
{
TSRecordDump((TSRecordType)(TS_RECORDTYPE_PLUGIN | TS_RECORDTYPE_NODE | TS_RECORDTYPE_PROCESS), csv_out_stat, my_state);
const char *version = TSTrafficServerVersionGet();
APPEND_STAT_CSV_NUMERIC("current_time_epoch_ms", "%" PRIu64, ms_since_epoch());
APPEND_STAT_CSV("version", "%s", version);
}
static void
prometheus_out_stats(stats_state *my_state)
{
TSRecordDump((TSRecordType)(TS_RECORDTYPE_PLUGIN | TS_RECORDTYPE_NODE | TS_RECORDTYPE_PROCESS), prometheus_out_stat, my_state);
APPEND_STAT_PROMETHEUS_NUMERIC("current_time_epoch_ms", "%" PRIu64, ms_since_epoch());
// No version printed, since string stats are not supported by Prometheus.
}
static void
prometheus_v2_out_stats(stats_state *my_state)
{
TSRecordDump((TSRecordType)(TS_RECORDTYPE_PLUGIN | TS_RECORDTYPE_NODE | TS_RECORDTYPE_PROCESS), prometheus_v2_out_stat, my_state);
for (const auto &sanitized_name : my_state->prometheus_v2_family_order) {
const auto &family = my_state->prometheus_v2_families.at(sanitized_name);
APPEND("# HELP ");
APPEND(sanitized_name.c_str());
APPEND(" ");
APPEND(family.help.c_str());
APPEND("\n");
const char *type_str = (family.data_type == TS_RECORDDATATYPE_COUNTER) ? "counter" : "gauge";
APPEND("# TYPE ");
APPEND(sanitized_name.c_str());
APPEND(" ");
APPEND(type_str);
APPEND("\n");
for (const auto &sample : family.samples) {
APPEND(sample.c_str());
}
}
APPEND("# HELP current_time_epoch_ms Current time in milliseconds since epoch.\n");
APPEND("# TYPE current_time_epoch_ms gauge\n");
APPEND_STAT_PROMETHEUS_NUMERIC("current_time_epoch_ms", "%" PRIu64, ms_since_epoch());
}
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) {
my_state->body_written = 1;
switch (my_state->output_format) {
case output_format_t::JSON_OUTPUT:
json_out_stats(my_state);
break;
case output_format_t::CSV_OUTPUT:
csv_out_stats(my_state);
break;
case output_format_t::PROMETHEUS_OUTPUT:
prometheus_out_stats(my_state);
break;
case output_format_t::PROMETHEUS_V2_OUTPUT:
prometheus_v2_out_stats(my_state);
break;
}
if ((my_state->encoding == encoding_format_t::GZIP) || (my_state->encoding == encoding_format_t::DEFLATE)) {
gzip_out_stats(my_state);
}
#if HAVE_BROTLI_ENCODE_H
else if (my_state->encoding == encoding_format_t::BR) {
br_out_stats(my_state);
}
#endif
TSVIONBytesSet(my_state->write_vio, my_state->output_bytes);
}
TSVIOReenable(my_state->write_vio);
} else if (event == TS_EVENT_VCONN_WRITE_COMPLETE) {
stats_cleanup(contp, my_state);
} else if (event == TS_EVENT_ERROR) {
TSError("[%s] stats_process_write: Received TS_EVENT_ERROR", PLUGIN_NAME);
} else {
TSReleaseAssert(!"Unexpected Event");
}
}
static int
stats_dostuff(TSCont contp, TSEvent event, void *edata)
{
stats_state *my_state = static_cast<stats_state *>(TSContDataGet(contp));
if (event == TS_EVENT_NET_ACCEPT) {
my_state->net_vc = (TSVConn)edata;
stats_process_accept(contp, my_state);
} else if (edata == my_state->read_vio) {
stats_process_read(contp, event, my_state);
} else if (edata == my_state->write_vio) {
stats_process_write(contp, event, my_state);
} else {
TSReleaseAssert(!"Unexpected Event");
}
return 0;
}
static int
stats_origin(TSCont contp, TSEvent /* event ATS_UNUSED */, void *edata)
{
TSCont icontp;
stats_state *my_state;
config_t *config;
TSHttpTxn txnp = (TSHttpTxn)edata;
TSMBuffer reqp;
TSMLoc hdr_loc = nullptr, url_loc = nullptr, accept_field = nullptr, accept_encoding_field = nullptr;
TSEvent reenable = TS_EVENT_HTTP_CONTINUE;
int path_len = 0;
const char *path = nullptr;
swoc::TextView request_path;
swoc::TextView request_path_suffix;
output_format_t format_per_path = output_format_t::JSON_OUTPUT;
bool path_had_explicit_format = false;
Dbg(dbg_ctl, "in the read stuff");
config = get_config(contp);
if (TSHttpTxnClientReqGet(txnp, &reqp, &hdr_loc) != TS_SUCCESS) {
goto cleanup;
}
if (TSHttpHdrUrlGet(reqp, hdr_loc, &url_loc) != TS_SUCCESS) {
goto cleanup;
}
path = TSUrlPathGet(reqp, url_loc, &path_len);
Dbg(dbg_ctl, "Path: %.*s", path_len, path);
if (path_len == 0) {
Dbg(dbg_ctl, "Empty path");
goto notforme;
}
request_path = swoc::TextView{path, static_cast<size_t>(path_len)};
if (!request_path.starts_with(config->stats_path)) {
Dbg(dbg_ctl, "Not the configured path for stats: %.*s, expected: %s", path_len, path, config->stats_path.c_str());
goto notforme;
}
if (request_path == config->stats_path) {
Dbg(dbg_ctl, "Exact match for stats path: %s", config->stats_path.c_str());
format_per_path = output_format_t::JSON_OUTPUT;
path_had_explicit_format = false;
} else {
request_path_suffix = request_path.remove_prefix(config->stats_path.length());
if (request_path_suffix == "/json") {
format_per_path = output_format_t::JSON_OUTPUT;
} else if (request_path_suffix == "/csv") {
format_per_path = output_format_t::CSV_OUTPUT;
} else if (request_path_suffix == "/prometheus") {
format_per_path = output_format_t::PROMETHEUS_OUTPUT;
} else if (request_path_suffix == "/prometheus_v2") {
format_per_path = output_format_t::PROMETHEUS_V2_OUTPUT;
} else {
Dbg(dbg_ctl, "Unknown suffix for stats path: %.*s", static_cast<int>(request_path_suffix.length()),
request_path_suffix.data());
goto notforme;
}
path_had_explicit_format = true;
}
if (auto addr = TSHttpTxnClientAddrGet(txnp); !is_ipmap_allowed(config, addr)) {
Dbg(dbg_ctl, "not right ip");
TSHttpTxnStatusSet(txnp, TS_HTTP_STATUS_FORBIDDEN, PLUGIN_NAME);
reenable = TS_EVENT_HTTP_ERROR;
goto notforme;
}
TSHttpTxnCntlSet(txnp, TS_HTTP_CNTL_SKIP_REMAPPING, true); // not strictly necessary, but speed is everything these days
/* This is us -- register our intercept */
Dbg(dbg_ctl, "Intercepting request");
my_state = new stats_state;
icontp = TSContCreate(stats_dostuff, TSMutexCreate());
if (path_had_explicit_format) {
Dbg(dbg_ctl, "Path had explicit format, ignoring any Accept header: %.*s", static_cast<int>(request_path_suffix.size()),
request_path_suffix.data());
my_state->output_format = format_per_path;
} else {
// Check for an Accept header to determine response type.
accept_field = TSMimeHdrFieldFind(reqp, hdr_loc, TS_MIME_FIELD_ACCEPT, TS_MIME_LEN_ACCEPT);
my_state->output_format = output_format_t::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);
std::string_view accept{};
if (str != nullptr && len > 0) {
accept = std::string_view{str, static_cast<std::string_view::size_type>(len)};
}
// Parse the Accept header, default to JSON output unless its another supported format
if (ts::iequals(accept, "text/csv")) {
Dbg(dbg_ctl, "Saw text/csv in accept header, sending CSV output.");
my_state->output_format = output_format_t::CSV_OUTPUT;
} else if (ts::iequals(accept, "text/plain; version=0.0.4")) {
Dbg(dbg_ctl, "Saw text/plain; version=0.0.4 in accept header, sending Prometheus output.");
my_state->output_format = output_format_t::PROMETHEUS_OUTPUT;
} else if (ts::iequals(accept, "text/plain; version=2.0.0")) {
Dbg(dbg_ctl, "Saw text/plain; version=2.0.0 in accept header, sending Prometheus v2 output.");
my_state->output_format = output_format_t::PROMETHEUS_V2_OUTPUT;
} else {
Dbg(dbg_ctl, "Saw %.*s in accept header, defaulting to JSON output.", static_cast<int>(accept.size()),
accept.empty() ? "" : accept.data());
my_state->output_format = output_format_t::JSON_OUTPUT;
}
}
}
// Check for Accept Encoding and init
accept_encoding_field = TSMimeHdrFieldFind(reqp, hdr_loc, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING);
my_state->encoding = encoding_format_t::NONE;
if (accept_encoding_field != TS_NULL_MLOC) {
int len = -1;
const char *str = TSMimeHdrFieldValueStringGet(reqp, hdr_loc, accept_encoding_field, -1, &len);
std::string_view accept_encoding =
(str != nullptr && len > 0) ? std::string_view{str, static_cast<size_t>(len)} : std::string_view{};
if (len >= TS_HTTP_LEN_DEFLATE && accept_encoding.find(TS_HTTP_VALUE_DEFLATE) != std::string_view::npos) {
Dbg(dbg_ctl, "Saw deflate in accept encoding");
my_state->encoding = init_gzip(my_state, DEFLATE_MODE);
} else if (len >= TS_HTTP_LEN_GZIP && accept_encoding.find(TS_HTTP_VALUE_GZIP) != std::string_view::npos) {
Dbg(dbg_ctl, "Saw gzip in accept encoding");
my_state->encoding = init_gzip(my_state, GZIP_MODE);
}
#if HAVE_BROTLI_ENCODE_H
else if (len >= TS_HTTP_LEN_BROTLI && accept_encoding.find(TS_HTTP_VALUE_BROTLI) != std::string_view::npos) {
Dbg(dbg_ctl, "Saw br in accept encoding");
my_state->encoding = init_br(my_state);
}
#endif
else {
my_state->encoding = encoding_format_t::NONE;
}
}
Dbg(dbg_ctl, "Finished AE check");
TSContDataSet(icontp, my_state);
TSHttpTxnIntercept(icontp, txnp);
goto cleanup;
notforme:
cleanup:
if (url_loc) {
TSHandleMLocRelease(reqp, hdr_loc, url_loc);
}
if (hdr_loc) {
TSHandleMLocRelease(reqp, TS_NULL_MLOC, hdr_loc);
}
if (accept_field) {
TSHandleMLocRelease(reqp, TS_NULL_MLOC, accept_field);
}
if (accept_encoding_field) {
TSHandleMLocRelease(reqp, TS_NULL_MLOC, accept_encoding_field);
}
TSHttpTxnReenable(txnp, reenable);
return 0;
}
void
TSPluginInit(int argc, const char *argv[])
{
TSPluginRegistrationInfo info;
static const char usage[] = PLUGIN_NAME ".so [--integer-counters] [PATH]";
static const struct option longopts[] = {
{(char *)("integer-counters"), no_argument, nullptr, 'i'},
{(char *)("wrap-counters"), no_argument, nullptr, 'w'},
{nullptr, 0, nullptr, 0 }
};
TSCont main_cont, config_cont;
config_holder_t *config_holder;
info.plugin_name = PLUGIN_NAME;
info.vendor_name = "Apache Software Foundation";
info.support_email = "dev@trafficserver.apache.org";
if (TSPluginRegister(&info) != TS_SUCCESS) {
TSError("[%s] registration failed", PLUGIN_NAME);
goto done;
}
for (;;) {
switch (getopt_long(argc, (char *const *)argv, "iw", longopts, nullptr)) {
case 'i':
integer_counters = true;
break;
case 'w':
wrap_counters = true;
break;
case -1:
goto init;
default:
TSError("[%s] usage: %s", PLUGIN_NAME, usage);
}
}
init:
argc -= optind;
argv += optind;
config_holder = new_config_holder(argc > 0 ? argv[0] : nullptr);
/* Path was not set during load, so the param was not a config file, we also
have an argument so it must be the path, set it here. Otherwise if no argument
then use the default _stats path */
if ((config_holder->config != nullptr) && (config_holder->config->stats_path.empty()) && (argc > 0) &&
(config_holder->config_path == nullptr)) {
config_holder->config->stats_path = argv[0] + ('/' == argv[0][0] ? 1 : 0);
} else if ((config_holder->config != nullptr) && (config_holder->config->stats_path.empty())) {
config_holder->config->stats_path = DEFAULT_URL_PATH;
}
/* Create a continuation with a mutex as there is a shared global structure
containing the headers to add */
main_cont = TSContCreate(stats_origin, nullptr);
TSContDataSet(main_cont, (void *)config_holder);
TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, main_cont);
/* Create continuation for management updates to re-read config file */
if (config_holder->config_path != nullptr) {
config_cont = TSContCreate(config_handler, TSMutexCreate());
TSContDataSet(config_cont, (void *)config_holder);
TSMgmtUpdateRegister(config_cont, PLUGIN_NAME);
}
if (config_holder->config != nullptr) {
Dbg(dbg_ctl, "stats module registered with path %s", config_holder->config->stats_path.c_str());
}
done:
return;
}
static bool
is_ipmap_allowed(const config_t *config, const struct sockaddr *addr)
{
if (!addr) {
return true;
}
if (config->addrs.contains(swoc::IPAddr(addr))) {
return true;
}
return false;
}
static void
parseIpMap(config_t *config, swoc::TextView txt)
{
// sent null ipstring, fill with default open IPs
if (txt.empty()) {
config->addrs.fill(DEFAULT_IP6);
config->addrs.fill(DEFAULT_IP);
Dbg(dbg_ctl, "Empty allow settings, setting all IPs in allow list");
return;
}
while (txt) {
auto token{txt.take_prefix_at(',')};
if (swoc::IPRange r; r.load(token)) {
config->addrs.fill(r);
Dbg(dbg_ctl, "Added %.*s to allow ip list", int(token.length()), token.data());
}
}
}
static config_t *
new_config(std::fstream &fh)
{
config_t *config = nullptr;
config = new config_t;
config->recordTypes = DEFAULT_RECORD_TYPES;
config->stats_path = "";
std::string cur_line;
if (!fh) {
Dbg(dbg_ctl, "No config file, using defaults");
return config;
}
while (std::getline(fh, cur_line)) {
swoc::TextView line{cur_line};
if (line.ltrim_if(&isspace).empty() || '#' == *line) {
continue; /* # Comments, only at line beginning */
}
size_t p = 0;
static constexpr swoc::TextView PATH_TAG = "path=";
static constexpr swoc::TextView RECORD_TAG = "record_types=";
static constexpr swoc::TextView ADDR_TAG = "allow_ip=";
static constexpr swoc::TextView ADDR6_TAG = "allow_ip6=";
if ((p = line.find(PATH_TAG)) != std::string::npos) {
line.remove_prefix(p + PATH_TAG.size()).ltrim('/');
Dbg(dbg_ctl, "parsing path");
config->stats_path = line;
} else if ((p = line.find(RECORD_TAG)) != std::string::npos) {
Dbg(dbg_ctl, "parsing record types");
line.remove_prefix(p).remove_prefix(RECORD_TAG.size());
config->recordTypes = swoc::svtou(line, nullptr, 16);
} else if ((p = line.find(ADDR_TAG)) != std::string::npos) {
parseIpMap(config, line.remove_prefix(p).remove_prefix(ADDR_TAG.size()));
} else if ((p = line.find(ADDR6_TAG)) != std::string::npos) {
parseIpMap(config, line.remove_prefix(p).remove_prefix(ADDR6_TAG.size()));
}
}
if (config->addrs.count() == 0) {
Dbg(dbg_ctl, "empty ip map found, setting defaults");
parseIpMap(config, nullptr);
}
Dbg(dbg_ctl, "config path=%s", config->stats_path.c_str());
return config;
}
static void
delete_config(config_t *config)
{
Dbg(dbg_ctl, "Freeing config");
delete config;
}
// standard api below...
static config_t *
get_config(TSCont cont)
{
config_holder_t *configh = (config_holder_t *)TSContDataGet(cont);
if (!configh) {
return 0;
}
return configh->config;
}
static void
load_config_file(config_holder_t *config_holder)
{
std::fstream fh;
struct stat s;
config_t *newconfig, *oldconfig;
TSCont free_cont;
configReloadRequests++;
lastReloadRequest = time(nullptr);
// check date
if ((config_holder->config_path == nullptr) || (stat(config_holder->config_path, &s) < 0)) {
Dbg(dbg_ctl, "Could not stat %s", config_holder->config_path);
config_holder->config_path = nullptr;
if (config_holder->config) {
return;
}
} else {
Dbg(dbg_ctl, "s.st_mtime=%lu, last_load=%lu", s.st_mtime, config_holder->last_load);
if (s.st_mtime < config_holder->last_load) {
return;
}
}
if (config_holder->config_path != nullptr) {
Dbg(dbg_ctl, "Opening config file: %s", config_holder->config_path);
fh.open(config_holder->config_path, std::ios::in);
}
if (!fh.is_open() && config_holder->config_path != nullptr) {
TSError("[%s] Unable to open config: %s. Will use the param as the path, or %s if null\n", PLUGIN_NAME,
config_holder->config_path, DEFAULT_URL_PATH.c_str());
if (config_holder->config) {
return;
}
}
newconfig = 0;
newconfig = new_config(fh);
if (newconfig) {
configReloads++;
lastReload = lastReloadRequest;
config_holder->last_load = lastReloadRequest;
config_t **confp = &(config_holder->config);
oldconfig = __sync_lock_test_and_set(confp, newconfig);
if (oldconfig) {
Dbg(dbg_ctl, "scheduling free: %p (%p)", oldconfig, newconfig);
free_cont = TSContCreate(free_handler, TSMutexCreate());
TSContDataSet(free_cont, (void *)oldconfig);
TSContScheduleOnPool(free_cont, FREE_TMOUT, TS_THREAD_POOL_TASK);
}
}
if (fh) {
fh.close();
}
return;
}
static config_holder_t *
new_config_holder(const char *path)
{
config_holder_t *config_holder = static_cast<config_holder_t *>(TSmalloc(sizeof(config_holder_t)));
config_holder->config_path = 0;
config_holder->config = 0;
config_holder->last_load = 0;
if (path) {
config_holder->config_path = nstr(path);
} else {
config_holder->config_path = nullptr;
}
load_config_file(config_holder);
return config_holder;
}
static int
free_handler(TSCont cont, TSEvent /* event ATS_UNUSED */, void * /* edata ATS_UNUSED */)
{
config_t *config;
config = (config_t *)TSContDataGet(cont);
delete_config(config);
TSContDestroy(cont);
return 0;
}
static int
config_handler(TSCont cont, TSEvent /* event ATS_UNUSED */, void * /* edata ATS_UNUSED */)
{
config_holder_t *config_holder;
config_holder = (config_holder_t *)TSContDataGet(cont);
load_config_file(config_holder);
/* We received a reload, check if the path value was removed since it was not set after load.
If unset, then we'll use the default */
if (config_holder->config->stats_path == "") {
config_holder->config->stats_path = DEFAULT_URL_PATH;
}
return 0;
}
//
// Compilation time unit tests.
//
#if defined(DEBUG) && STATS_OVER_HTTP_HAS_CONSTEXPR_STRING
constexpr void
test_sanitize_metric_name_for_prometheus()
{
// Various unchanged names.
static_assert(sanitize_metric_name_for_prometheus("foo") == "foo");
static_assert(sanitize_metric_name_for_prometheus("foo_bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo_bar:baz") == "foo_bar:baz");
static_assert(sanitize_metric_name_for_prometheus("FooBar123") == "FooBar123");
static_assert(sanitize_metric_name_for_prometheus("UPPERCASE_NAME") == "UPPERCASE_NAME");
static_assert(sanitize_metric_name_for_prometheus("lowercase_name") == "lowercase_name");
static_assert(sanitize_metric_name_for_prometheus("Mixed_Case_123") == "Mixed_Case_123");
// Test dots conversion (common in ATS metrics).
static_assert(sanitize_metric_name_for_prometheus("foo.bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("proxy.process.allocator.inuse") == "proxy_process_allocator_inuse");
// Various invalid characters.
static_assert(sanitize_metric_name_for_prometheus("foo-bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo+bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo@bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo#bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo$bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo%bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo^bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo&bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo*bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo(bar)") == "foo_bar_");
static_assert(sanitize_metric_name_for_prometheus("foo=bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo|bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo\\bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo/bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo?bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo<bar>") == "foo_bar_");
static_assert(sanitize_metric_name_for_prometheus("foo,bar;baz") == "foo_bar_baz");
static_assert(sanitize_metric_name_for_prometheus("foo\"bar'baz") == "foo_bar_baz");
static_assert(sanitize_metric_name_for_prometheus("foo`bar~baz") == "foo_bar_baz");
static_assert(sanitize_metric_name_for_prometheus("foo!bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo_bar[baz]") == "foo_bar_baz_");
static_assert(sanitize_metric_name_for_prometheus("proxy.process.allocator.inuse.ioBufAllocator[0]") ==
"proxy_process_allocator_inuse_ioBufAllocator_0_");
// Whitespace and control characters.
static_assert(sanitize_metric_name_for_prometheus("foo bar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo\tbar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo\nbar") == "foo_bar");
static_assert(sanitize_metric_name_for_prometheus("foo\rbar") == "foo_bar");
// Initial digit variations.
static_assert(sanitize_metric_name_for_prometheus("0foo") == "_0foo");
static_assert(sanitize_metric_name_for_prometheus("1.proxy.process.allocator.inuse.ioBufAllocator[0]") ==
"_1_proxy_process_allocator_inuse_ioBufAllocator_0_");
// Complex combinations.
static_assert(sanitize_metric_name_for_prometheus("proxy.process.http.connection_errors[500].rate") ==
"proxy_process_http_connection_errors_500__rate");
static_assert(sanitize_metric_name_for_prometheus("cache.hit_ratio[0-5min]") == "cache_hit_ratio_0_5min_");
static_assert(sanitize_metric_name_for_prometheus("worker.thread[0].cpu.usage%") == "worker_thread_0__cpu_usage_");
static_assert(sanitize_metric_name_for_prometheus("1st.metric.name-with+special@chars") == "_1st_metric_name_with_special_chars");
// Minimal edge cases.
static_assert(sanitize_metric_name_for_prometheus("") == "");
static_assert(sanitize_metric_name_for_prometheus("a") == "a");
static_assert(sanitize_metric_name_for_prometheus(".") == "_");
static_assert(sanitize_metric_name_for_prometheus("1") == "_1");
// Edge cases with multiple consecutive invalid characters.
static_assert(sanitize_metric_name_for_prometheus("foo...bar") == "foo___bar");
static_assert(sanitize_metric_name_for_prometheus("123foo---bar") == "_123foo___bar");
static_assert(sanitize_metric_name_for_prometheus("foo [[[bar]]]") == "foo____bar___");
static_assert(sanitize_metric_name_for_prometheus("foo@#$%bar") == "foo____bar");
}
#endif // defined(DEBUG) && STATS_OVER_HTTP_HAS_CONSTEXPR_STRING