diff --git a/include/datadog/opentracing.h b/include/datadog/opentracing.h index 76ef4116..63f234d7 100644 --- a/include/datadog/opentracing.h +++ b/include/datadog/opentracing.h @@ -29,7 +29,7 @@ struct TracerOptions { // Port on which the Datadog agent is running. Can also be set by the environment variable // DD_TRACE_AGENT_PORT uint32_t agent_port = 8126; - // The name of the service being traced. + // The name of the service being traced. Can also be set by the environment variable DD_SERVICE. // See: // https://help.datadoghq.com/hc/en-us/articles/115000702546-What-is-the-Difference-Between-Type-Service-Resource-and-Name- std::string service; @@ -76,6 +76,12 @@ struct TracerOptions { // value for analytics sampling rate. Can also be set by the environment variable // DD_TRACE_ANALYTICS_SAMPLE_RATE double analytics_rate = std::nan(""); + // Tags that are applied to all spans reported by this tracer. Can also be set by the environment + // variable DD_TAGS. + std::map tags = {}; + // The version of the overall application being traced. Can also be set by the environment + // variable DD_VERSION. + std::string version = ""; }; // TraceEncoder exposes the data required to encode and submit traces to the diff --git a/include/datadog/tags.h b/include/datadog/tags.h index da9428f0..59602997 100644 --- a/include/datadog/tags.h +++ b/include/datadog/tags.h @@ -12,6 +12,7 @@ const std::string resource_name = "resource.name"; const std::string analytics_event = "analytics.event"; const std::string manual_keep = "manual.keep"; const std::string manual_drop = "manual.drop"; +const std::string version = "version"; } // namespace tags } // namespace datadog diff --git a/src/tracer.cpp b/src/tracer.cpp index d5381b4c..fc5c1aa1 100644 --- a/src/tracer.cpp +++ b/src/tracer.cpp @@ -167,6 +167,12 @@ std::unique_ptr Tracer::StartSpanWithOptions(ot::string_view operation opts_.service, opts_.type, operation_name, operation_name, opts_.operation_name_override}}; bool is_trace_root = parent_id == 0; + if (!opts_.version.empty()) { + span->SetTag(datadog::tags::version, opts_.version); + } + for (auto &tag : opts_.tags) { + span->SetTag(tag.first, tag.second); + } for (auto &tag : options.tags) { if (tag.first == ::ot::ext::sampling_priority && span->getSamplingPriority() != nullptr) { // Do not apply this tag if sampling priority is already assigned. @@ -174,7 +180,7 @@ std::unique_ptr Tracer::StartSpanWithOptions(ot::string_view operation } span->SetTag(tag.first, tag.second); } - if (is_trace_root && opts_.environment != "") { + if (is_trace_root && !opts_.environment.empty()) { span->SetTag(tags::environment, opts_.environment); } return span; diff --git a/src/tracer_factory.cpp b/src/tracer_factory.cpp index f45ed23d..8294341d 100644 --- a/src/tracer_factory.cpp +++ b/src/tracer_factory.cpp @@ -22,12 +22,10 @@ ot::expected optionsFromConfig(const char *configuration, return ot::make_unexpected(std::make_error_code(std::errc::invalid_argument)); } try { - // Mandatory config. - if (config.find("service") == config.end()) { - error_message = "configuration argument 'service' is missing"; - return ot::make_unexpected(std::make_error_code(std::errc::invalid_argument)); + // Mandatory config, but may be set via environment. + if (config.find("service") != config.end()) { + config.at("service").get_to(options.service); } - config.at("service").get_to(options.service); // Optional. if (config.find("agent_host") != config.end()) { config.at("agent_host").get_to(options.agent_host); @@ -91,7 +89,16 @@ ot::expected optionsFromConfig(const char *configuration, error_message = maybe_options.error(); return ot::make_unexpected(std::make_error_code(std::errc::invalid_argument)); } - return maybe_options.value(); + + options = maybe_options.value(); + // sanity-check for final option values + if (options.service.empty()) { + error_message = + "tracer option 'service' has not been set via config or DD_SERVICE environment variable"; + return ot::make_unexpected(std::make_error_code(std::errc::invalid_argument)); + } + + return options; } // Accepts configuration in JSON format, with the following keys: diff --git a/src/tracer_options.cpp b/src/tracer_options.cpp index 0ae7c4ac..9691763a 100644 --- a/src/tracer_options.cpp +++ b/src/tracer_options.cpp @@ -3,25 +3,116 @@ #include #include +#include +#include + namespace ot = opentracing; namespace datadog { namespace opentracing { +// Extracts key-value pairs from a string. +// Duplicates are overwritten. Empty keys are ignored. +// Intended use is for settings tags from DD_TAGS options. +std::map keyvalues(std::string text, char itemsep, char tokensep, + char escape) { + // early-return if empty + if (text.empty()) { + return {}; + } + + bool esc = false; + std::map kvp; + std::string key; + std::string val; + bool keyfound = false; + auto assignchar = [&](char c) { + if (keyfound) { + val += c; + } else { + key += c; + } + esc = false; + }; + auto addkv = [&](std::string key, std::string val) { + if (key.empty()) { + return; + } + if (val.empty()) { + return; + } + kvp[key] = val; + }; + for (auto ch : text) { + if (esc) { + assignchar(ch); + continue; + } + if (ch == escape) { + esc = true; + continue; + } + if (ch == tokensep) { + addkv(key, val); + key = ""; + val = ""; + keyfound = false; + continue; + } + if (ch == itemsep) { + keyfound = true; + continue; + } + assignchar(ch); + } + if (!key.empty()) { + addkv(key, val); + } + + return kvp; +} + ot::expected applyTracerOptionsFromEnvironment( const TracerOptions &input) { TracerOptions opts = input; - auto agent_host = std::getenv("DD_AGENT_HOST"); - if (agent_host != nullptr && std::strlen(agent_host) > 0) { - opts.agent_host = agent_host; - } - auto environment = std::getenv("DD_ENV"); if (environment != nullptr && std::strlen(environment) > 0) { opts.environment = environment; } + auto service = std::getenv("DD_SERVICE"); + if (service != nullptr && std::strlen(service) > 0) { + opts.service = service; + } + + auto version = std::getenv("DD_VERSION"); + if (version != nullptr && std::strlen(version) > 0) { + opts.version = version; + } + + auto tags = std::getenv("DD_TAGS"); + if (tags != nullptr && std::strlen(tags) > 0) { + opts.tags = keyvalues(tags, ':', ',', '\\'); + // Special cases for env, version and sampling priority + if (environment != nullptr && std::strlen(environment) > 0 && + opts.tags.find(datadog::tags::environment) != opts.tags.end()) { + opts.tags.erase(datadog::tags::environment); + } + if (version != nullptr && std::strlen(version) > 0 && + opts.tags.find(datadog::tags::version) != opts.tags.end()) { + opts.tags.erase(datadog::tags::version); + } + if (opts.tags.find(ot::ext::sampling_priority) != opts.tags.end()) { + opts.tags.erase(ot::ext::sampling_priority); + } + } + + auto agent_host = std::getenv("DD_AGENT_HOST"); + if (agent_host != nullptr && std::strlen(agent_host) > 0) { + opts.agent_host = agent_host; + } + auto trace_agent_port = std::getenv("DD_TRACE_AGENT_PORT"); if (trace_agent_port != nullptr && std::strlen(trace_agent_port) > 0) { try { diff --git a/test/tracer_factory_test.cpp b/test/tracer_factory_test.cpp index 33d78f93..c794f9c8 100644 --- a/test/tracer_factory_test.cpp +++ b/test/tracer_factory_test.cpp @@ -165,7 +165,9 @@ TEST_CASE("tracer factory") { )"}; // Missing service std::string error = ""; auto result = factory.MakeTracer(input.c_str(), error); - REQUIRE(error == "configuration argument 'service' is missing"); + REQUIRE( + error == + "tracer option 'service' has not been set via config or DD_SERVICE environment variable"); REQUIRE(!result); REQUIRE(result.error() == std::make_error_code(std::errc::invalid_argument)); } @@ -248,6 +250,42 @@ TEST_CASE("tracer factory") { } SECTION("injected environment variables") { + SECTION("DD_ENV overrides default") { + ::setenv("DD_ENV", "injected-env", 0); + std::string input{R"( + { + "service": "my-service", + "environment": "env" + } + )"}; + std::string error = ""; + auto result = factory.MakeTracer(input.c_str(), error); + ::unsetenv("DD_ENV"); + + REQUIRE(error == ""); + REQUIRE(result->get() != nullptr); + auto tracer = dynamic_cast(result->get()); + REQUIRE(tracer->opts.service == "my-service"); + REQUIRE(tracer->opts.environment == "injected-env"); + } + + SECTION("DD_SERVICE overrides default") { + ::setenv("DD_SERVICE", "injected-service", 0); + std::string input{R"( + { + "service": "my-service" + } + )"}; + std::string error = ""; + auto result = factory.MakeTracer(input.c_str(), error); + ::unsetenv("DD_SERVICE"); + + REQUIRE(error == ""); + REQUIRE(result->get() != nullptr); + auto tracer = dynamic_cast(result->get()); + REQUIRE(tracer->opts.service == "injected-service"); + } + SECTION("DD_AGENT_HOST overrides default") { ::setenv("DD_AGENT_HOST", "injected-hostname", 0); std::string input{R"( diff --git a/test/tracer_options_test.cpp b/test/tracer_options_test.cpp index bed96af5..ac4bc46c 100644 --- a/test/tracer_options_test.cpp +++ b/test/tracer_options_test.cpp @@ -1,5 +1,6 @@ #include "../src/tracer_options.h" +#include #include using namespace datadog::opentracing; @@ -36,6 +37,7 @@ void requireTracerOptionsResultsMatch(const ot::expectedanalytics_rate == rhs->analytics_rate); } + REQUIRE(lhs->tags == rhs->tags); }; TEST_CASE("tracer options from environment variables") { @@ -51,26 +53,36 @@ TEST_CASE("tracer options from environment variables") { {"DD_TRACE_AGENT_PORT", "420"}, {"DD_ENV", "env"}, {"DD_TRACE_SAMPLING_RULES", "rules"}, + {"DD_SERVICE", "service"}, {"DD_PROPAGATION_STYLE_EXTRACT", "B3 Datadog"}, {"DD_PROPAGATION_STYLE_INJECT", "Datadog B3"}, {"DD_TRACE_REPORT_HOSTNAME", "true"}, {"DD_TRACE_ANALYTICS_ENABLED", "true"}, - {"DD_TRACE_ANALYTICS_SAMPLE_RATE", "0.5"}}, - TracerOptions{"host", - 420, - "", - "web", - "env", - std::nan(""), - true, - "rules", - 1000, - "", - {PropagationStyle::Datadog, PropagationStyle::B3}, - {PropagationStyle::Datadog, PropagationStyle::B3}, - true, - true, - 0.5}}, + {"DD_TRACE_ANALYTICS_SAMPLE_RATE", "0.5"}, + {"DD_TAGS", "host:my-host-name,region:us-east-1,datacenter:us,partition:5"}}, + TracerOptions{ + "host", + 420, + "service", + "web", + "env", + std::nan(""), + true, + "rules", + 1000, + "", + {PropagationStyle::Datadog, PropagationStyle::B3}, + {PropagationStyle::Datadog, PropagationStyle::B3}, + true, + true, + 0.5, + { + {"host", "my-host-name"}, + {"region", "us-east-1"}, + {"datacenter", "us"}, + {"partition", "5"}, + }, + }}, {{{"DD_PROPAGATION_STYLE_EXTRACT", "Not even a real style"}}, ot::make_unexpected("Value for DD_PROPAGATION_STYLE_EXTRACT is invalid")}, {{{"DD_PROPAGATION_STYLE_INJECT", "Not even a real style"}}, @@ -100,3 +112,34 @@ TEST_CASE("tracer options from environment variables") { REQUIRE(unsetenv(env_var.first.c_str()) == 0); } } + +TEST_CASE("exceptions for DD_TAGS") { + // + TracerOptions input{}; + struct TestCase { + std::map environment_variables; + std::map tags; + }; + + auto test_case = GENERATE(values({ + {{{"DD_ENV", "foo"}, {"DD_TAGS", "env:bar"}}, {}}, + {{{"DD_TAGS", std::string(ot::ext::sampling_priority) + ":1"}}, {}}, + {{{"DD_TAGS", ":,:,:,:"}}, {}}, // repeatedly handles missing keys + {{{"DD_TAGS", "keywithoutvalue:"}}, {}}, + {{{"DD_VERSION", "awesomeapp v1.2.3"}, {"DD_TAGS", "version:abcd"}}, {}}, + })); + + // Setup + for (const auto &env_var : test_case.environment_variables) { + REQUIRE(setenv(env_var.first.c_str(), env_var.second.c_str(), 1) == 0); + } + + auto got = applyTracerOptionsFromEnvironment(input); + REQUIRE(got); + REQUIRE(test_case.tags == got->tags); + + // Teardown + for (const auto &env_var : test_case.environment_variables) { + REQUIRE(unsetenv(env_var.first.c_str()) == 0); + } +} diff --git a/test/tracer_test.cpp b/test/tracer_test.cpp index d8baf0dd..a552106f 100644 --- a/test/tracer_test.cpp +++ b/test/tracer_test.cpp @@ -6,6 +6,7 @@ #include "../src/tracer_options.h" #include "mocks.h" +#include #include using namespace datadog::opentracing; @@ -143,6 +144,8 @@ TEST_CASE("env overrides") { std::string hostname; double rate; bool error; + std::string version; + std::map extra_tags; }; char buf[256]; @@ -151,15 +154,27 @@ TEST_CASE("env overrides") { auto env_test = GENERATE_COPY(values({ // Normal cases - {"DD_TRACE_REPORT_HOSTNAME", "true", hostname, std::nan(""), false}, - {"DD_TRACE_ANALYTICS_ENABLED", "true", "", 1.0, false}, - {"DD_TRACE_ANALYTICS_ENABLED", "false", "", 0.0, false}, - {"DD_TRACE_ANALYTICS_SAMPLE_RATE", "0.5", "", 0.5, false}, - {"", "", "", std::nan(""), false}, + {"DD_TRACE_REPORT_HOSTNAME", "true", hostname, std::nan(""), false, "", {}}, + {"DD_TRACE_ANALYTICS_ENABLED", "true", "", 1.0, false, "", {}}, + {"DD_TRACE_ANALYTICS_ENABLED", "false", "", 0.0, false, "", {}}, + {"DD_TRACE_ANALYTICS_SAMPLE_RATE", "0.5", "", 0.5, false, "", {}}, + {"DD_TAGS", + "host:my-host-name,region:us-east-1,datacenter:us,partition:5", + "", + std::nan(""), + false, + "", + { + {"host", "my-host-name"}, + {"region", "us-east-1"}, + {"datacenter", "us"}, + {"partition", "5"}, + }}, + {"", "", "", std::nan(""), false, "", {}}, // Unexpected values handled gracefully - {"DD_TRACE_ANALYTICS_ENABLED", "yes please", "", std::nan(""), true}, - {"DD_TRACE_ANALYTICS_SAMPLE_RATE", "1.1", "", std::nan(""), true}, - {"DD_TRACE_ANALYTICS_SAMPLE_RATE", "half", "", std::nan(""), true}, + {"DD_TRACE_ANALYTICS_ENABLED", "yes please", "", std::nan(""), true, "", {}}, + {"DD_TRACE_ANALYTICS_SAMPLE_RATE", "1.1", "", std::nan(""), true, "", {}}, + {"DD_TRACE_ANALYTICS_SAMPLE_RATE", "half", "", std::nan(""), true, "", {}}, })); SECTION("set correct tags and metrics") { @@ -180,7 +195,7 @@ TEST_CASE("env overrides") { span->FinishWithOptions(finish_options); auto& result = mwriter->traces[0][0]; - // Check the analytics rate matches the expected value. + // Check the hostname matches the expected value. if (env_test.hostname.empty()) { REQUIRE(result->meta.find("_dd.hostname") == result->meta.end()); } else { @@ -192,6 +207,18 @@ TEST_CASE("env overrides") { } else { REQUIRE(result->metrics["_dd1.sr.eausr"] == env_test.rate); } + // Check the version matches the expected value. + if (env_test.version.empty()) { + REQUIRE(result->meta.find(datadog::tags::version) == result->meta.end()); + } else { + REQUIRE(result->meta[datadog::tags::version] == env_test.hostname); + } + // Check spans are tagged with values from DD_TAGS + for (auto& tag : env_test.extra_tags) { + REQUIRE(result->meta.find(tag.first) != result->meta.end()); + REQUIRE(result->meta[tag.first] == tag.second); + } + // Tear-down ::unsetenv(env_test.env.c_str()); }