diff --git a/changelog/unreleased/kong/instana-header-support.yml b/changelog/unreleased/kong/instana-header-support.yml new file mode 100644 index 00000000000..337fd04a966 --- /dev/null +++ b/changelog/unreleased/kong/instana-header-support.yml @@ -0,0 +1,4 @@ +message: | + **OpenTelemetry**: include instana header support in propagation +type: feature +scope: Plugin diff --git a/kong-3.10.0-0.rockspec b/kong-3.10.0-0.rockspec index f1b503720b8..f14f65d7481 100644 --- a/kong-3.10.0-0.rockspec +++ b/kong-3.10.0-0.rockspec @@ -699,6 +699,7 @@ build = { ["kong.observability.tracing.propagation.extractors.gcp"] = "kong/observability/tracing/propagation/extractors/gcp.lua", ["kong.observability.tracing.propagation.extractors.aws"] = "kong/observability/tracing/propagation/extractors/aws.lua", ["kong.observability.tracing.propagation.extractors.datadog"] = "kong/observability/tracing/propagation/extractors/datadog.lua", + ["kong.observability.tracing.propagation.extractors.instana"] = "kong/observability/tracing/propagation/extractors/instana.lua", ["kong.observability.tracing.propagation.injectors._base"] = "kong/observability/tracing/propagation/injectors/_base.lua", ["kong.observability.tracing.propagation.injectors.w3c"] = "kong/observability/tracing/propagation/injectors/w3c.lua", ["kong.observability.tracing.propagation.injectors.b3"] = "kong/observability/tracing/propagation/injectors/b3.lua", @@ -708,6 +709,7 @@ build = { ["kong.observability.tracing.propagation.injectors.gcp"] = "kong/observability/tracing/propagation/injectors/gcp.lua", ["kong.observability.tracing.propagation.injectors.aws"] = "kong/observability/tracing/propagation/injectors/aws.lua", ["kong.observability.tracing.propagation.injectors.datadog"] = "kong/observability/tracing/propagation/injectors/datadog.lua", + ["kong.observability.tracing.propagation.injectors.instana"] = "kong/observability/tracing/propagation/injectors/instana.lua", ["kong.observability.tracing.request_id"] = "kong/observability/tracing/request_id.lua", ["kong.observability.tracing.tracing_context"] = "kong/observability/tracing/tracing_context.lua", diff --git a/kong/observability/tracing/propagation/extractors/instana.lua b/kong/observability/tracing/propagation/extractors/instana.lua new file mode 100644 index 00000000000..4031a326079 --- /dev/null +++ b/kong/observability/tracing/propagation/extractors/instana.lua @@ -0,0 +1,67 @@ +local _EXTRACTOR = require "kong.observability.tracing.propagation.extractors._base" +local propagation_utils = require "kong.observability.tracing.propagation.utils" +local from_hex = propagation_utils.from_hex + +local INSTANA_EXTRACTOR = _EXTRACTOR:new({ + headers_validate = { + any = { + "x-instana-t", + "x-instana-s", + "x-instana-l", + } + } +}) + +function INSTANA_EXTRACTOR:get_context(headers) + + local trace_id_raw = headers["x-instana-t"] + + if type(trace_id_raw) ~= "string" then + return + end + + if trace_id_raw then + trace_id_raw = trace_id_raw:match("^(%x+)") + local trace_id_len = trace_id_raw and #trace_id_raw or 0 + if not trace_id_raw or + not(trace_id_len == 16 or trace_id_len == 32) + then + kong.log.warn("x-instana-t header invalid; ignoring.") + end + end + + local span_id_raw = headers["x-instana-s"] + + if type(span_id_raw) ~= "string" then + return + end + + if span_id_raw then + span_id_raw = span_id_raw:match("^(%x+)") + if not span_id_raw then + kong.log.warn("x-instana-s header invalid; ignoring.") + end + end + + local level_id_raw = headers["x-instana-l"] + + if level_id_raw then + -- the flag can come in as "0" or "1" + -- or something like the following format + -- "1,correlationType=web;correlationId=1234567890abcdef" + -- here we only care about the first value + level_id_raw = level_id_raw:sub(1, 1) + end + local should_sample = level_id_raw == "1" + + local trace_id = trace_id_raw and from_hex(trace_id_raw) or nil + local span_id = span_id_raw and from_hex(span_id_raw) or nil + + return { + trace_id = trace_id, + span_id = span_id, + should_sample = should_sample, + } +end + +return INSTANA_EXTRACTOR diff --git a/kong/observability/tracing/propagation/init.lua b/kong/observability/tracing/propagation/init.lua index e9568573f5a..d7411da31a6 100644 --- a/kong/observability/tracing/propagation/init.lua +++ b/kong/observability/tracing/propagation/init.lua @@ -47,7 +47,7 @@ local function get_plugin_params(config) formats.DATADOG, formats.AWS, formats.GCP, - + formats.INSTANA } propagation_config.inject = { "preserve" } @@ -63,6 +63,7 @@ local function get_plugin_params(config) formats.DATADOG, formats.AWS, formats.GCP, + formats.INSTANA, } propagation_config.inject = { -- the old logic used to propagate the "found" incoming format diff --git a/kong/observability/tracing/propagation/injectors/instana.lua b/kong/observability/tracing/propagation/injectors/instana.lua new file mode 100644 index 00000000000..087e61d1164 --- /dev/null +++ b/kong/observability/tracing/propagation/injectors/instana.lua @@ -0,0 +1,31 @@ +local _INJECTOR = require "kong.observability.tracing.propagation.injectors._base" +local to_hex = require "resty.string".to_hex + +local INSTANA_INJECTOR = _INJECTOR:new({ + name = "instana", + context_validate = {}, -- all fields are optional + trace_id_allowed_sizes = { 16, 8 }, + span_id_size_bytes = 8, +}) + + +function INSTANA_INJECTOR:create_headers(out_tracing_ctx) + local headers = { + ["x-instana-t"] = to_hex(out_tracing_ctx.trace_id) or nil, + ["x-instana-s"] = to_hex(out_tracing_ctx.span_id) or nil, + } + + if out_tracing_ctx.should_sample ~= nil then + headers["x-instana-l"] = out_tracing_ctx.should_sample and "1" or "0" + end + + return headers +end + + +function INSTANA_INJECTOR:get_formatted_trace_id(trace_id) + return { instana = to_hex(trace_id) } +end + + +return INSTANA_INJECTOR diff --git a/kong/observability/tracing/propagation/utils.lua b/kong/observability/tracing/propagation/utils.lua index 9efd9e11aca..4d8835f229c 100644 --- a/kong/observability/tracing/propagation/utils.lua +++ b/kong/observability/tracing/propagation/utils.lua @@ -18,6 +18,7 @@ local FORMATS = { DATADOG = "datadog", AWS = "aws", GCP = "gcp", + INSTANA = "instana", } local function hex_to_char(c) diff --git a/kong/plugins/opentelemetry/schema.lua b/kong/plugins/opentelemetry/schema.lua index 41a127859c6..60ecd3d9dcf 100644 --- a/kong/plugins/opentelemetry/schema.lua +++ b/kong/plugins/opentelemetry/schema.lua @@ -78,7 +78,7 @@ return { old_default = "preserve" }, required = false, default = "preserve", - one_of = { "preserve", "ignore", "b3", "b3-single", "w3c", "jaeger", "ot", "aws", "gcp", "datadog" } } }, + one_of = { "preserve", "ignore", "b3", "b3-single", "w3c", "jaeger", "ot", "aws", "gcp", "datadog", "instana" } } }, { sampling_rate = { description = "Tracing sampling rate for configuring the probability-based sampler. When set, this value supersedes the global `tracing_sampling_rate` setting from kong.conf.", type = "number", diff --git a/kong/plugins/zipkin/schema.lua b/kong/plugins/zipkin/schema.lua index da4b0aa8d3e..0d19d47c63a 100644 --- a/kong/plugins/zipkin/schema.lua +++ b/kong/plugins/zipkin/schema.lua @@ -56,11 +56,11 @@ return { { include_credential = { description = "Specify whether the credential of the currently authenticated consumer should be included in metadata sent to the Zipkin server.", type = "boolean", required = true, default = true } }, { traceid_byte_count = { description = "The length in bytes of each request's Trace ID.", type = "integer", required = true, default = 16, one_of = { 8, 16 } } }, { header_type = { description = "All HTTP requests going through the plugin are tagged with a tracing HTTP request. This property codifies what kind of tracing header the plugin expects on incoming requests", type = "string", required = true, default = "preserve", - one_of = { "preserve", "ignore", "b3", "b3-single", "w3c", "jaeger", "ot", "aws", "datadog", "gcp" }, + one_of = { "preserve", "ignore", "b3", "b3-single", "w3c", "jaeger", "ot", "aws", "datadog", "gcp", "instana" }, deprecation = { message = "zipkin: config.header_type is deprecated, please use config.propagation options instead", removal_in_version = "4.0", old_default = "preserve" } } }, { default_header_type = { description = "Allows specifying the type of header to be added to requests with no pre-existing tracing headers and when `config.header_type` is set to `\"preserve\"`. When `header_type` is set to any other value, `default_header_type` is ignored.", type = "string", required = true, default = "b3", - one_of = { "b3", "b3-single", "w3c", "jaeger", "ot", "aws", "datadog", "gcp" }, + one_of = { "b3", "b3-single", "w3c", "jaeger", "ot", "aws", "datadog", "gcp", "instana" }, deprecation = { message = "zipkin: config.default_header_type is deprecated, please use config.propagation.default_format instead", removal_in_version = "4.0", old_default = "b3" } } }, { tags_header = { description = "The Zipkin plugin will add extra headers to the tags associated with any HTTP requests that come with a header named as configured by this property.", type = "string", required = true, default = "Zipkin-Tags" } }, diff --git a/spec/01-unit/26-observability/02-propagation_strategies_spec.lua b/spec/01-unit/26-observability/02-propagation_strategies_spec.lua index ca780242101..ff27dad1227 100644 --- a/spec/01-unit/26-observability/02-propagation_strategies_spec.lua +++ b/spec/01-unit/26-observability/02-propagation_strategies_spec.lua @@ -1637,6 +1637,114 @@ local test_data = { { }, err = "gcp injector context is invalid: field span_id not found in context" } } +}, { + extractor = "instana", + injector = "instana", + headers_data = { { + description = "base case", + extract = true, + inject = true, + trace_id = trace_id_16, + headers = { + ["x-instana-t"] = trace_id_16, + ["x-instana-s"] = span_id_8_1, + ["x-instana-l"] = "1", + }, + ctx = { + trace_id = trace_id_16, + span_id = span_id_8_1, + should_sample = true, + trace_id_original_size = 16, + } + }, { + description = "sampled = false", + extract = true, + inject = true, + trace_id = trace_id_16, + headers = { + ["x-instana-t"] = trace_id_16, + ["x-instana-s"] = span_id_8_1, + ["x-instana-l"] = "0", + }, + ctx = { + trace_id = trace_id_16, + span_id = span_id_8_1, + should_sample = false, + trace_id_original_size = 16, + } + }, { + description = "8B trace id", + extract = true, + inject = true, + trace_id = trace_id_8, + headers = { + ["x-instana-t"] = trace_id_8, + ["x-instana-s"] = span_id_8_1, + ["x-instana-l"] = "0", + }, + ctx = { + trace_id = padding_prefix .. trace_id_8, + span_id = span_id_8_1, + should_sample = false, + trace_id_original_size = 8, + } + }, { + description = "big 16B trace ID", + inject = true, + trace_id = big_trace_id_16, + headers = { + ["x-instana-t"] = big_trace_id_16, + ["x-instana-s"] = span_id_8_1, + }, + ctx = { + trace_id = big_trace_id_16, + span_id = span_id_8_1, + } + }, { + description = "invalid flag", + extractor = true, + trace_id = trace_id_16, + headers = { + ["x-instana-t"] = trace_id_16, + ["x-instana-s"] = span_id_8_1, + ["x-instana-l"] = "5", + }, + ctx = { + trace_id = big_trace_id_16, + span_id = span_id_8_1, + should_sample = true, + } + },{ -- extraction error cases + description = "invalid trace ID (non hex)", + extract = true, + trace_id = "abcdefghijklmnop", + headers = { + ["x-instana-t"] = "abcdefghijklmnop", + ["x-instana-s"] = span_id_8_1, + }, + err = "x-instana-t header invalid; ignoring." + }, { + description = "invalid trace ID (too long)", + extract = true, + headers = { + ["x-instana-t"] = too_long_id, + }, + err = "x-instana-t header invalid; ignoring." + }, { + description = "invalid trace ID (too short)", + extract = true, + headers = { + ["x-instana-t"] = "abc", + }, + err = "x-instana-t header invalid; ignoring." + }, { + description = "empty header", + extract = true, + headers = { + ["x-instana-t"] = "", + }, + err = "x-instana-t header invalid; ignoring." + },} } } diff --git a/spec/03-plugins/34-zipkin/zipkin_spec.lua b/spec/03-plugins/34-zipkin/zipkin_spec.lua index df9e4590555..d90bdc7e6d2 100644 --- a/spec/03-plugins/34-zipkin/zipkin_spec.lua +++ b/spec/03-plugins/34-zipkin/zipkin_spec.lua @@ -12,6 +12,7 @@ local http_route_host = "http-route" local http_route_ignore_host = "http-route-ignore" local http_route_w3c_host = "http-route-w3c" local http_route_dd_host = "http-route-dd" +local http_route_ins_host = "http-route-ins" local http_route_clear_host = "http-clear-route" local http_route_no_preserve_host = "http-no-preserve-route" @@ -671,6 +672,21 @@ local function setup_zipkin_old_propagation(bp, service, traceid_byte_count) default_header_type = "datadog", } }) + + -- header_type = "instana" + bp.plugins:insert({ + name = "zipkin", + route = {id = bp.routes:insert({ + service = service, + hosts = { http_route_ins_host }, + }).id}, + config = { + sample_ratio = 1, + http_endpoint = fmt("http://%s:%d/api/v2/spans", ZIPKIN_HOST, ZIPKIN_PORT), + header_type = "instana", + default_header_type = "instana", + } + }) end local function setup_zipkin_new_propagation(bp, service, traceid_byte_count) @@ -687,7 +703,7 @@ local function setup_zipkin_new_propagation(bp, service, traceid_byte_count) { name = "static", value = "ok" }, }, propagation = { - extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp" }, + extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp", "instana" }, inject = { "preserve" }, default_format = "b3-single", }, @@ -723,7 +739,7 @@ local function setup_zipkin_new_propagation(bp, service, traceid_byte_count) sample_ratio = 1, http_endpoint = fmt("http://%s:%d/api/v2/spans", ZIPKIN_HOST, ZIPKIN_PORT), propagation = { - extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp" }, + extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp", "instana" }, inject = { "preserve", "w3c" }, default_format = "b3-single", }, @@ -741,13 +757,31 @@ local function setup_zipkin_new_propagation(bp, service, traceid_byte_count) sample_ratio = 1, http_endpoint = fmt("http://%s:%d/api/v2/spans", ZIPKIN_HOST, ZIPKIN_PORT), propagation = { - extract = { "b3", "w3c", "jaeger", "ot", "aws", "datadog", "gcp" }, + extract = { "b3", "w3c", "jaeger", "ot", "aws", "datadog", "gcp", "instana" }, inject = { "preserve", "datadog" }, default_format = "datadog", }, } }) + -- header_type = "instana" + bp.plugins:insert({ + name = "zipkin", + route = {id = bp.routes:insert({ + service = service, + hosts = { http_route_ins_host }, + }).id}, + config = { + sample_ratio = 1, + http_endpoint = fmt("http://%s:%d/api/v2/spans", ZIPKIN_HOST, ZIPKIN_PORT), + propagation = { + extract = { "b3", "w3c", "jaeger", "ot", "aws", "datadog", "gcp", "instana" }, + inject = { "preserve", "instana" }, + default_format = "instana", + }, + } + }) + -- available with new configuration only: -- no preserve bp.plugins:insert({ @@ -1579,6 +1613,35 @@ describe("http integration tests with zipkin server [#" end) end) + describe("propagates instana tracing headers", function() + it("with instana headers in client request", function() + local trace_id = gen_trace_id(16) + local span_id = gen_span_id() + local r = proxy_client:get("/", { + headers = { + ["x-instana-t"] = trace_id, + ["x-instana-s"] = span_id, + host = http_route_host, + }, + }) + local body = assert.response(r).has.status(200) + local json = cjson.decode(body) + + assert.equals(trace_id, json.headers["x-instana-t"]) + end) + + it("without instana headers in client request", function() + local r = proxy_client:get("/", { + headers = { host = http_route_ins_host }, + }) + local body = assert.response(r).has.status(200) + local json = cjson.decode(body) + + assert.is_not_nil(json.headers["x-instana-t"]) + assert.is_not_nil(json.headers["x-instana-s"]) + end) + end) + if propagation_config == "new" then it("clears non-propagated headers when configured to do so", function() local trace_id = gen_trace_id(16) diff --git a/spec/03-plugins/37-opentelemetry/03-propagation_spec.lua b/spec/03-plugins/37-opentelemetry/03-propagation_spec.lua index dd34df4f151..0f8f1b9e4ac 100644 --- a/spec/03-plugins/37-opentelemetry/03-propagation_spec.lua +++ b/spec/03-plugins/37-opentelemetry/03-propagation_spec.lua @@ -13,6 +13,7 @@ local http_route_ignore_host = "http-route-ignore" local http_route_w3c_host = "http-route-w3c" local http_route_dd_host = "http-route-dd" local http_route_b3_single_host = "http-route-b3-single" +local http_route_ins_host = "http-route-ins" local http_route_clear_host = "http-clear-route" local http_route_no_preserve_host = "http-no-preserve-route" @@ -124,6 +125,18 @@ local function setup_otel_old_propagation(bp, service) header_type = "b3-single", } }) + + bp.plugins:insert({ + name = "opentelemetry", + route = {id = bp.routes:insert({ + service = service, + hosts = { http_route_ins_host }, + }).id}, + config = { + traces_endpoint = "http://localhost:8080/v1/traces", + header_type = "instana", + } + }) end -- same configurations as "setup_otel_old_propagation", using the new @@ -138,7 +151,7 @@ local function setup_otel_new_propagation(bp, service) config = { traces_endpoint = "http://localhost:8080/v1/traces", propagation = { - extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp" }, + extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp", "instana" }, inject = { "preserve" }, default_format = "w3c", } @@ -170,7 +183,7 @@ local function setup_otel_new_propagation(bp, service) config = { traces_endpoint = "http://localhost:8080/v1/traces", propagation = { - extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp" }, + extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp", "instana" }, inject = { "preserve", "w3c" }, default_format = "w3c", } @@ -186,7 +199,7 @@ local function setup_otel_new_propagation(bp, service) config = { traces_endpoint = "http://localhost:8080/v1/traces", propagation = { - extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp" }, + extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp", "instana" }, inject = { "preserve", "datadog" }, default_format = "datadog", } @@ -202,13 +215,28 @@ local function setup_otel_new_propagation(bp, service) config = { traces_endpoint = "http://localhost:8080/v1/traces", propagation = { - extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp" }, + extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp", "instana" }, inject = { "preserve", "b3-single" }, default_format = "w3c", } } }) + bp.plugins:insert({ + name = "opentelemetry", + route = {id = bp.routes:insert({ + service = service, + hosts = { http_route_ins_host }, + }).id}, + config = { + traces_endpoint = "http://localhost:8080/v1/traces", + propagation = { + extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp", "instana" }, + inject = { "preserve", "instana" }, + default_format = "instana", + } + } + }) -- available with new configuration only: -- no preserve bp.plugins:insert({ @@ -511,6 +539,21 @@ describe("propagation tests #" .. strategy .. assert.equals(trace_id, json.headers["ot-tracer-traceid"]) end) + it("propagates instana headers", function() + local trace_id = gen_trace_id() + local span_id = gen_span_id() + local r = proxy_client:get("/", { + headers = { + ["x-instana-t"] = trace_id, + ["x-instana-s"] = span_id, + host = http_route_ins_host, + }, + }) + local body = assert.response(r).has.status(200) + local json = cjson.decode(body) + + assert.equals(trace_id, json.headers["x-instana-t"]) + end) describe("propagates datadog tracing headers", function() it("with datadog headers in client request", function()