diff --git a/lib/api/ContextFieldsProvider.cpp b/lib/api/ContextFieldsProvider.cpp index e5792cc66..44522c9b8 100644 --- a/lib/api/ContextFieldsProvider.cpp +++ b/lib/api/ContextFieldsProvider.cpp @@ -244,6 +244,70 @@ namespace MAT_NS_BEGIN record.extDevice[0].deviceClass = iter->second.as_string; } + /* Distributed tracing fields. + * + * Part A extension: + * - ext.dt.traceId -> env_dt_traceId + * - ext.dt.spanId -> env_dt_spanId + * - ext.dt.traceFlags -> env_dt_traceFlags + * + * Part B mapping: + * - parentId -> parentId + */ + iter = m_commonContextFields.find(COMMONFIELDS_DT_TRACEID); + + /* Only run the context mapping if TraceId field is set */ + if (iter != m_commonContextFields.end()) + { + /* Collector does not natively support ext.dt extension in CS4.0. + * When it does, fields could eventually be moved to ext.dt in + * CS4.1 or CS5.0. Currently we map fields to corresponding Geneva + * naming convention in order to enable the Distributed Trace View + * experience. This approach is future-proof, since it avoids the + * unnecessary remapping at 1DS Interchange. + */ + CsProtocol::Value strValue; + strValue.stringValue = iter->second.as_string; + // Map to Geneva Part A extension field name. + ext[COMMONFIELDS_DT_TRACEID] = strValue; + + iter = m_commonContextFields.find(COMMONFIELDS_DT_SPANID); + if (iter != m_commonContextFields.end()) + { + strValue.stringValue = iter->second.as_string; + // Map to Geneva Part A extension field name. + ext[COMMONFIELDS_DT_SPANID] = strValue; + } + + iter = m_commonContextFields.find(COMMONFIELDS_DT_PARENTID); + if (iter != m_commonContextFields.end()) + { + strValue.stringValue = iter->second.as_string; + // 1DS pipeline on export to Aria-Kusto does not differentiate + // between Part B and Part C properties. Since Part B (baseData) + // remains hidden below the radar - we populate the field in + // 'data' (Part C). That way the field can be consumed in Aria-Kusto, + // custom Kusto, and in Geneva. + // + // 1DS Interchange maps the field to the same 'parentId' column + // as populated by OpenTelemetry Geneva exporters. That allows us + // to stitch the client and service telemetry in one view. + ext[COMMONFIELDS_DT_PARENTID] = strValue; + } + + iter = m_commonContextFields.find(COMMONFIELDS_DT_TRACEFLAGS); + if (iter != m_commonContextFields.end()) + { + if (iter->second.type == EventProperty::TYPE_INT64) + { + CsProtocol::Value numValue; + numValue.type = CsProtocol::ValueKind::ValueInt64; + numValue.longValue = iter->second.as_int64; + ext[COMMONFIELDS_DT_TRACEFLAGS] = numValue; + } + } + } + iter = m_commonContextFields.find(COMMONFIELDS_COMMERCIAL_ID); if (iter != m_commonContextFields.end()) { diff --git a/lib/include/public/CommonFields.h b/lib/include/public/CommonFields.h index a75a57bba..5e5b3d9a0 100644 --- a/lib/include/public/CommonFields.h +++ b/lib/include/public/CommonFields.h @@ -24,6 +24,15 @@ #define COMMONFIELDS_DEVICE_CLASS "DeviceInfo.Class" #define COMMONFIELDS_DEVICE_ORGID "DeviceInfo.OrgId" +/* Ref. https://www.w3.org/TR/trace-context/#traceparent-header-field-values + - ParentId - parent of the current span received via remote context propagation. + - SpanId - current SpanId sent in 'traceparent' to remote service. + */ +#define COMMONFIELDS_DT_TRACEID "env_dt_traceId" +#define COMMONFIELDS_DT_SPANID "env_dt_spanId" +#define COMMONFIELDS_DT_TRACEFLAGS "env_dt_traceFlags" +#define COMMONFIELDS_DT_PARENTID "parentId" + #define COMMONFIELDS_NETWORK_PROVIDER "DeviceInfo.NetworkProvider" #define COMMONFIELDS_NETWORK_TYPE "DeviceInfo.NetworkType" #define COMMONFIELDS_NETWORK_COST "DeviceInfo.NetworkCost" diff --git a/lib/include/public/ISemanticContext.hpp b/lib/include/public/ISemanticContext.hpp index b0adf421a..284e98f8a 100644 --- a/lib/include/public/ISemanticContext.hpp +++ b/lib/include/public/ISemanticContext.hpp @@ -109,12 +109,39 @@ namespace MAT_NS_BEGIN /// Device class. DECLARE_COMMONFIELD(DeviceClass, COMMONFIELDS_DEVICE_CLASS); - /// + /// /// Set the device orgId context information of telemetry event. /// /// Device orgId DECLARE_COMMONFIELD(DeviceOrgId, COMMONFIELDS_DEVICE_ORGID); + /// + /// Set the W3C TraceContext TraceId information of telemetry event. + /// + /// TraceContext TraceId + DECLARE_COMMONFIELD(TraceId, COMMONFIELDS_DT_TRACEID); + + /// + /// Set the W3C TraceContext SpanId information of telemetry event. + /// + /// TraceContext SpanId + DECLARE_COMMONFIELD(SpanId, COMMONFIELDS_DT_SPANID); + + /// + /// Set the W3C TraceContext TraceFlags information of telemetry event. + /// + /// TraceContext TraceFlags + virtual void SetTraceFlags(uint8_t traceFlags) + { + SetCommonField(COMMONFIELDS_DT_TRACEFLAGS, traceFlags); + } + + /// + /// Set the remote context (parent) SpanId information of telemetry event. + /// + /// ParentId + DECLARE_COMMONFIELD(ParentId, COMMONFIELDS_DT_PARENTID); + /// /// Set the network cost context information of telemetry event. /// diff --git a/lib/shared/SemanticContextCX.cpp b/lib/shared/SemanticContextCX.cpp index 3f81f9fa7..72570d63c 100644 --- a/lib/shared/SemanticContextCX.cpp +++ b/lib/shared/SemanticContextCX.cpp @@ -117,6 +117,27 @@ namespace Microsoft { { m_semanticContextCore->SetUserTimeZone(FromPlatformString(userTimeZone)); } + + void SemanticContextImpl::TraceId::set(String^ traceId) + { + m_semanticContextCore->SetTraceId(FromPlatformString(traceId)); + } + + void SemanticContextImpl::SpanId::set(String ^ spanId) + { + m_semanticContextCore->SetSpanId(FromPlatformString(spanId)); + } + + void SemanticContextImpl::ParentId::set(String ^ parentId) + { + m_semanticContextCore->SetParentId(FromPlatformString(parentId)); + } + + void SemanticContextImpl::TraceFlags::set(unsigned char traceFlags) + { + m_semanticContextCore->SetTraceFlags(static_cast(traceFlags)); + } + } } } diff --git a/lib/shared/SemanticContextCX.hpp b/lib/shared/SemanticContextCX.hpp index a2a97dcd6..489acd3ec 100644 --- a/lib/shared/SemanticContextCX.hpp +++ b/lib/shared/SemanticContextCX.hpp @@ -100,6 +100,23 @@ namespace Microsoft { } virtual void SetUserId(String^ userId, PiiKind piiKind) = 0; + + property String^ TraceId { + virtual void set(String^ traceId) = 0; + } + + property String^ SpanId { + virtual void set(String^ spanId) = 0; + } + + property unsigned char TraceFlags + { + virtual void set(unsigned char traceFlags) = 0; + } + + property String^ ParentId { + virtual void set(String^ parentId) = 0; + } }; /// @cond INTERNAL_DOCS @@ -206,8 +223,25 @@ namespace Microsoft { { virtual void set(String^ appExperimentIds); } - - virtual void SetEventExperimentIds(String^ eventName, String^ experimentIds); + + virtual void SetEventExperimentIds(String^ eventName, String^ experimentIds); + + property String^ TraceId { + virtual void set(String^ traceId); + } + + property String^ SpanId { + virtual void set(String ^ spanId); + } + + property unsigned char TraceFlags + { + virtual void set(unsigned char traceFlags); + } + + property String^ ParentId { + virtual void set(String ^ parentId); + } internal: SemanticContextImpl(MAT::ISemanticContext* semanticContextCore); diff --git a/tests/functests/APITest.cpp b/tests/functests/APITest.cpp index 03ada480f..927c5a04a 100644 --- a/tests/functests/APITest.cpp +++ b/tests/functests/APITest.cpp @@ -1452,6 +1452,49 @@ TEST(APITest, Custom_Decorator) config.AddModule(CFG_MODULE_DECORATOR, nullptr); } +TEST(APITest, TraceContext) +{ + auto& config = LogManager::GetLogConfiguration(); + LogManager::Initialize(TEST_TOKEN, config); + auto logger = LogManager::GetLogger(); + auto context = logger->GetSemanticContext(); + + // Information that is propagated in W3C TraceContext header. + // These fields must be formatted as strings conforming to W3C spec: + // https://www.w3.org/TR/trace-context/#traceparent-header-field-values + context->SetTraceId("0af7651916cd43dd8448eb211c80319c"); + context->SetSpanId("b9c7c989f97918e1"); + context->SetTraceFlags(1); + + // Information that allows to stitch this span to its remote parent, e.g. + // SpanId='00f067aa0ba902b7' is parent of SpanId='b9c7c989f97918e1'. + // ParentId is not sent as part of W3C TraceContext header, but it is + // logged via 1DS pipeline in order to allow attaching local telemetry + // to remote context. + context->SetParentId("00f067aa0ba902b7"); + + // All events emitted by this logger contain the same trace context. + EventProperties myEvent("MyEvent.With.TraceContext", + { + {"message", "Hello, W3CTraceContext!"} + }); + logger->LogEvent(myEvent); + + { + // Verify that individual events are capable of overridding the context. + EventProperties myEvent2("MyEvent2.With.TraceContext", + { + {"message", "Hello again, W3CTraceContext!"} + }); + // This event borrows the logger context TraceId, is manually + // parented to previous SpanId and carries its own SpanId. + myEvent2.SetProperty(COMMONFIELDS_DT_PARENTID, "b9c7c989f97918e1"); + myEvent2.SetProperty(COMMONFIELDS_DT_SPANID, "b9c7c989f97918c2"); + logger->LogEvent(myEvent2); + } + LogManager::FlushAndTeardown(); +} + #endif // HAVE_MAT_DEFAULT_HTTP_CLIENT // TEST_PULL_ME_IN(APITest)