plugins/experimental/money_trace/money_trace.cc (459 lines of code) (raw):
/*
* 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 "tsutil/ts_bw_format.h"
#include "ts/ts.h"
#include "ts/remap.h"
#include "ts/remap_version.h"
#include <getopt.h>
#include <string_view>
#include <string>
#include "money_trace.h"
namespace
{
std::string_view const DefaultMimeHeader = "X-MoneyTrace";
enum PluginType { REMAP, GLOBAL };
struct Config {
std::string header; // override header
std::string pregen_header; // generate request header during remap
std::string global_skip_header; // skip global plugin
bool create_if_none = false;
bool passthru = false; // transparen mode: pass any headers through
};
Config *
config_from_args(int const argc, char const *argv[], PluginType const ptype)
{
Config *const conf = new Config;
static const struct option longopt[] = {
{const_cast<char *>("passthru"), required_argument, nullptr, 'a'},
{const_cast<char *>("create-if-none"), required_argument, nullptr, 'c'},
{const_cast<char *>("global-skip-header"), required_argument, nullptr, 'g'},
{const_cast<char *>("header"), required_argument, nullptr, 'h'},
{const_cast<char *>("pregen-header"), required_argument, nullptr, 'p'},
{nullptr, 0, nullptr, 0 },
};
// getopt assumes args start at '1' so this hack is needed
do {
int const opt = getopt_long(argc, const_cast<char *const *>(argv), "a:c:h:l:p:", longopt, nullptr);
if (-1 == opt) {
break;
}
LOG_DEBUG("Opt: %c", opt);
switch (opt) {
case 'a':
if ("true" == std::string_view{optarg}) {
LOG_DEBUG("Plugin acts as passthrough");
conf->passthru = true;
}
break;
case 'c':
if ("true" == std::string_view{optarg}) {
LOG_DEBUG("Plugin will create header if missing");
conf->create_if_none = true;
}
break;
case 'g':
LOG_DEBUG("Using global-skip-header: '%s'", optarg);
conf->global_skip_header.assign(optarg);
break;
case 'h':
LOG_DEBUG("Using custom header: '%s'", optarg);
conf->header.assign(optarg);
break;
case 'p':
LOG_DEBUG("Using pregen_header '%s'", optarg);
conf->pregen_header.assign(optarg);
break;
default:
break;
}
} while (true);
if (conf->header.empty()) {
conf->header.assign(DefaultMimeHeader);
LOG_DEBUG("Using default header name: '%.*s'", (int)DefaultMimeHeader.length(), DefaultMimeHeader.data());
}
if (conf->passthru && conf->create_if_none) {
LOG_ERROR("passthru conflicts with create-if-none, disabling create-if-none!");
conf->create_if_none = false;
}
if (REMAP == ptype && !conf->global_skip_header.empty()) {
LOG_ERROR("--global-skip-header inappropriate for remap plugin, removing option!");
conf->global_skip_header.clear();
}
return conf;
}
struct TxnData {
std::string client_trace;
std::string this_trace;
Config const *config = nullptr;
};
std::string_view const traceid{"trace-id="};
std::string_view const parentid{"parent-id="};
std::string_view const spanid{"span-id="};
std::string_view const zerospan{"0"};
char const sep{';'};
std::string
next_trace(std::string_view const request_hdr, TSHttpTxn const txnp)
{
std::string nexttrace;
LOG_DEBUG("next_trace with '%.*s'", (int)request_hdr.length(), request_hdr.data());
std::string_view view = request_hdr;
// trace-id must be first
if (0 != view.rfind(traceid, 0)) {
LOG_DEBUG("Expected to find prefix '%.*s' in '%.*s'", (int)traceid.length(), traceid.data(), (int)view.length(), view.data());
return nexttrace;
}
view = view.substr(traceid.length());
// look for separator
size_t seppos = view.find_first_of(sep);
if (0 == seppos) {
LOG_DEBUG("Trace is empty for '%.*s'", (int)request_hdr.length(), request_hdr.data());
return nexttrace;
}
std::string_view trace = view.substr(0, seppos);
if (std::string_view::npos == seppos) {
LOG_DEBUG("Expected to find separator '%c' in %.*s", sep, (int)request_hdr.length(), request_hdr.data());
view = std::string_view{};
}
std::string_view span;
// scan remaining tokens
while (!view.empty() && span.empty()) {
// skip any leading whitespace
while (!view.empty() && ' ' == view.front()) {
view = view.substr(1);
}
// check for span-id field
if (0 == view.rfind(spanid, 0)) {
span = view.substr(spanid.length());
seppos = span.find_first_of(sep);
span = span.substr(0, seppos);
// remove any trailing white space
while (!span.empty() && ' ' == span.back()) {
span = span.substr(0, span.length() - 1);
}
} else {
LOG_DEBUG("Non '%.*s' found in '%.*s'", (int)spanid.length(), spanid.data(), (int)view.length(), view.data());
}
if (span.empty()) {
seppos = view.find_first_of(sep);
// move forward past sep
if (std::string_view::npos != seppos) {
view = view.substr(seppos + 1);
LOG_DEBUG("Trimming view to '%.*s'", (int)view.length(), view.data());
} else {
view = std::string_view{};
}
}
}
if (span.empty()) {
LOG_DEBUG("No span found, using default '%.*s'", (int)zerospan.length(), zerospan.data());
span = zerospan;
}
// span becomes new parent
swoc::LocalBufferWriter<8192> bwriter;
bwriter.write(traceid);
bwriter.write(trace);
bwriter.write(sep);
bwriter.write(parentid);
bwriter.write(span);
bwriter.write(sep);
bwriter.write(spanid);
bwriter.print("{}", TSHttpTxnIdGet(txnp));
nexttrace = std::string(bwriter.data(), bwriter.size());
return nexttrace;
}
std::string
create_trace(TSHttpTxn const txnp)
{
std::string header;
constexpr char new_parent{'0'};
TSUuid const uuid = TSUuidCreate();
if (nullptr != uuid) {
if (TS_SUCCESS == TSUuidInitialize(uuid, TS_UUID_V4)) {
char const *const uuidstr = TSUuidStringGet(uuid);
if (nullptr != uuidstr) {
swoc::LocalBufferWriter<8192> bwriter;
bwriter.write(traceid);
bwriter.write(uuidstr, strlen(uuidstr));
bwriter.write(sep);
bwriter.write(parentid);
bwriter.write(new_parent);
bwriter.write(sep);
bwriter.write(spanid);
bwriter.print("{}", TSHttpTxnIdGet(txnp));
header = std::string(bwriter.data(), bwriter.size());
} else {
LOG_ERROR("Error getting uuid string");
}
} else {
LOG_ERROR("Error initializing uuid");
}
TSUuidDestroy(uuid);
} else {
LOG_ERROR("Error calling TSUuidCreate");
}
return header;
}
/**
* Creates header if necessary, sets given value.
*/
bool
set_header(TSMBuffer const bufp, TSMLoc const hdr_loc, std::string const &hdr, std::string const &val)
{
bool isset = false;
TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, hdr.data(), hdr.length());
if (TS_NULL_MLOC == field_loc) {
// No existing header, so create one
if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(bufp, hdr_loc, hdr.data(), hdr.length(), &field_loc)) {
if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(bufp, hdr_loc, field_loc, -1, val.data(), val.length())) {
TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc);
LOG_DEBUG("header/value added: '%s' '%s'", hdr.c_str(), val.c_str());
isset = true;
} else {
LOG_DEBUG("unable to set: '%s' to '%s'", hdr.c_str(), val.c_str());
}
TSHandleMLocRelease(bufp, hdr_loc, field_loc);
} else {
LOG_DEBUG("unable to create: '%s'", hdr.c_str());
}
} else {
bool first = true;
while (field_loc) {
TSMLoc const tmp = TSMimeHdrFieldNextDup(bufp, hdr_loc, field_loc);
if (first) {
first = false;
if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(bufp, hdr_loc, field_loc, -1, val.data(), val.length())) {
LOG_DEBUG("header/value set: '%s' '%s'", hdr.c_str(), val.c_str());
isset = true;
} else {
LOG_DEBUG("unable to set: '%s' to '%s'", hdr.c_str(), val.c_str());
}
} else {
TSMimeHdrFieldDestroy(bufp, hdr_loc, field_loc);
}
TSHandleMLocRelease(bufp, hdr_loc, field_loc);
field_loc = tmp;
}
}
return isset;
}
/**
* The TS_EVENT_HTTP_POST_REMAP callback.
*
* If global_skip_header is set the global plugin
* will check for it here.
*/
void
global_skip_check(TSCont const contp, TSHttpTxn const txnp, TxnData *const txn_data)
{
Config const *const conf = txn_data->config;
if (conf->global_skip_header.empty()) {
LOG_ERROR("Called in error, no global skip header defined!");
return;
}
// Check for a money trace header. Route accordingly.
TSMBuffer bufp = nullptr;
TSMLoc hdr_loc = TS_NULL_MLOC;
if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) {
TSMLoc const field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, conf->global_skip_header.c_str(), conf->global_skip_header.length());
if (TS_NULL_MLOC != field_loc) {
LOG_DEBUG("global_skip_header found, disabling for the rest of this transaction");
TSHandleMLocRelease(bufp, hdr_loc, field_loc);
} else { // schedule continuations
if (conf->create_if_none || !txn_data->client_trace.empty()) {
TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_REQUEST_HDR_HOOK, contp);
}
if (!txn_data->client_trace.empty()) {
TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, contp);
}
}
}
}
/**
* The TS_EVENT_HTTP_SEND_REQUEST_HDR callback.
*
* When a parent request is made, this function adds the new
* money trace header to the parent request headers.
*/
void
send_server_request(TSHttpTxn const txnp, TxnData *const txn_data)
{
Config const *const conf = txn_data->config;
if (txn_data->this_trace.empty()) {
if (conf->passthru) {
txn_data->this_trace = txn_data->client_trace;
} else if (!txn_data->client_trace.empty()) {
txn_data->this_trace = next_trace(txn_data->client_trace, txnp);
} else if (conf->create_if_none) {
txn_data->this_trace = create_trace(txnp);
}
}
if (txn_data->this_trace.empty()) {
if (conf->create_if_none) {
LOG_DEBUG("Unable to deal with client trace '%s', creating new", txn_data->client_trace.c_str());
txn_data->this_trace = create_trace(txnp);
} else {
LOG_DEBUG("Unable to deal with client trace '%s', passing through!", txn_data->client_trace.c_str());
txn_data->this_trace = txn_data->client_trace;
}
}
TSMBuffer bufp = nullptr;
TSMLoc hdr_loc = TS_NULL_MLOC;
if (TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &bufp, &hdr_loc)) {
if (!set_header(bufp, hdr_loc, txn_data->config->header, txn_data->this_trace)) {
LOG_ERROR("Unable to set the server request trace header '%s'", txn_data->this_trace.c_str());
}
} else {
LOG_ERROR("Unable to get the txn server request");
}
}
/**
* The TS_EVENT_HTTP_SEND_RESPONSE_HDR callback.
*
* Adds the money trace header received in the client request to the
* client response headers.
*/
void
send_client_response(TSHttpTxn const txnp, TxnData *const txn_data)
{
LOG_DEBUG("send_client_response");
if (txn_data->client_trace.empty()) {
LOG_DEBUG("no client trace data to return.");
return;
}
// send back the original money trace header received in the
// client request back in the response to the client.
TSMBuffer bufp = nullptr;
TSMLoc hdr_loc = TS_NULL_MLOC;
if (TS_SUCCESS == TSHttpTxnClientRespGet(txnp, &bufp, &hdr_loc)) {
if (!set_header(bufp, hdr_loc, txn_data->config->header, txn_data->client_trace)) {
LOG_ERROR("Unable to set the client response trace header.");
}
} else {
LOG_DEBUG("Unable to get the txn client response");
}
}
/**
* Transaction event handler.
*/
int
transaction_handler(TSCont const contp, TSEvent const event, void *const edata)
{
TSHttpTxn const txnp = static_cast<TSHttpTxn>(edata);
TxnData *const txn_data = static_cast<TxnData *>(TSContDataGet(contp));
switch (event) {
case TS_EVENT_HTTP_POST_REMAP:
LOG_DEBUG("global plugin checking for skip header");
global_skip_check(contp, txnp, txn_data);
break;
case TS_EVENT_HTTP_SEND_REQUEST_HDR:
LOG_DEBUG("updating send request headers.");
send_server_request(txnp, txn_data);
break;
case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
LOG_DEBUG("updating send response headers.");
send_client_response(txnp, txn_data);
break;
case TS_EVENT_HTTP_TXN_CLOSE:
LOG_DEBUG("handling transaction close.");
delete txn_data;
TSContDestroy(contp);
break;
default:
TSAssert(!"Unexpected event");
break;
}
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
return TS_SUCCESS;
}
/**
* Check for the existence of a money trace header.
* If there is one, schedule the continuation to call back and
* process on send request or send response.
* Global plugin may need to schedule a hook to check for skip header.
*/
void
check_request_header(TSHttpTxn const txnp, Config const *const conf, PluginType const ptype)
{
TxnData *txn_data = nullptr;
// Check for a money trace header. Route accordingly.
TSMBuffer bufp = nullptr;
TSMLoc hdr_loc = TS_NULL_MLOC;
if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) {
TSMLoc const field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, conf->header.c_str(), conf->header.length());
if (TS_NULL_MLOC != field_loc) {
int length = 0;
const char *hdr_value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, 0, &length);
if (!hdr_value || length <= 0) {
LOG_DEBUG("ignoring, corrupt trace header.");
} else {
txn_data = new TxnData;
txn_data->config = conf;
txn_data->client_trace.assign(hdr_value, length);
LOG_DEBUG("found monetrace header: '%.*s', length: %d", length, hdr_value, length);
}
TSHandleMLocRelease(bufp, hdr_loc, field_loc);
} else if (!conf->passthru && conf->create_if_none) {
txn_data = new TxnData;
txn_data->config = conf;
txn_data->this_trace = create_trace(txnp);
LOG_DEBUG("created trace header: '%s'", txn_data->this_trace.c_str());
} else {
LOG_DEBUG("no trace header handling for this request.");
}
// Check for pregen_header
if (nullptr != txn_data && !conf->pregen_header.empty()) {
if (txn_data->this_trace.empty()) {
txn_data->this_trace = next_trace(txn_data->client_trace, txnp);
if (txn_data->this_trace.empty()) {
if (conf->create_if_none) {
LOG_DEBUG("Unable to deal with client trace '%s', creating new", txn_data->client_trace.c_str());
txn_data->this_trace = create_trace(txnp);
} else {
LOG_DEBUG("Unable to deal with client trace '%s', passing through!", txn_data->client_trace.c_str());
txn_data->this_trace = txn_data->client_trace;
}
}
}
if (!txn_data->this_trace.empty()) {
if (!set_header(bufp, hdr_loc, conf->pregen_header, txn_data->this_trace)) {
LOG_ERROR("Unable to set the client request pregen trace header.");
}
}
}
} else {
LOG_DEBUG("unable to get the request request");
}
// Schedule appropriate continuations
if (nullptr != txn_data) {
TSCont const contp = TSContCreate(transaction_handler, nullptr);
if (nullptr != contp) {
TSContDataSet(contp, txn_data);
TSHttpTxnHookAdd(txnp, TS_HTTP_TXN_CLOSE_HOOK, contp);
// global plugin may need to check for skip header
if (GLOBAL == ptype && !conf->global_skip_header.empty()) {
TSHttpTxnHookAdd(txnp, TS_HTTP_POST_REMAP_HOOK, contp);
} else {
if (conf->create_if_none || !txn_data->client_trace.empty()) {
TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_REQUEST_HDR_HOOK, contp);
}
if (!txn_data->client_trace.empty()) {
TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, contp);
}
}
} else {
LOG_ERROR("failed to create the transaction handler continuation");
delete txn_data;
}
}
}
int
global_request_header_hook(TSCont const contp, TSEvent const /* event ATS_UNUSED */, void *const edata)
{
TSHttpTxn const txnp = static_cast<TSHttpTxn>(edata);
Config const *const conf = static_cast<Config *>(TSContDataGet(contp));
check_request_header(txnp, conf, GLOBAL);
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
return 0;
}
} // namespace
namespace money_trace_ns
{
DbgCtl dbg_ctl{PLUGIN_NAME};
}
/**
* Remap initialization.
*/
TSReturnCode
TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
{
CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size);
LOG_DEBUG("money_trace remap is successfully initialized.");
return TS_SUCCESS;
}
/**
* not used, one instance per remap and no parameters are used.
*/
TSReturnCode
TSRemapNewInstance(int argc, char *argv[], void **ih, char * /*errbuf */, int /* errbuf_size */)
{
// second arg poses as the program name
--argc;
++argv;
Config *const conf = config_from_args(argc, const_cast<char const **>(argv), REMAP);
*ih = static_cast<void *>(conf);
return TS_SUCCESS;
}
/**
* not used, one instance per remap
*/
void
TSRemapDeleteInstance(void *ih)
{
Config *const conf = static_cast<Config *>(ih);
delete conf;
}
/**
* Remap entry point.
*/
TSRemapStatus
TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo * /* rri */)
{
Config const *const conf = static_cast<Config *>(ih);
check_request_header(txnp, conf, REMAP);
return TSREMAP_NO_REMAP;
}
/**
* global plugin initialization
*/
void
TSPluginInit(int argc, char const *argv[])
{
LOG_DEBUG("Starting global plugin init");
TSPluginRegistrationInfo info;
info.plugin_name = PLUGIN_NAME;
info.vendor_name = "Apache Software Foundation";
info.support_email = "dev@trafficserver.apache.org";
if (TS_SUCCESS != TSPluginRegister(&info)) {
LOG_ERROR("Plugin registration failed");
return;
}
Config const *const conf = config_from_args(argc, argv, GLOBAL);
TSCont const contp = TSContCreate(global_request_header_hook, nullptr);
TSContDataSet(contp, (void *)conf);
// This fires before any remap hooks.
TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, contp);
}