From daba015831973b4310ba676a43ca3e0b52773b69 Mon Sep 17 00:00:00 2001 From: duncanpo Date: Thu, 18 Jul 2024 16:43:14 -0400 Subject: [PATCH] Support SpanContext creation --- api/context/+opentelemetry/+context/Context.m | 5 +- api/trace/+opentelemetry/+trace/Context.m | 6 +- api/trace/+opentelemetry/+trace/Link.m | 4 +- api/trace/+opentelemetry/+trace/Scope.m | 5 +- api/trace/+opentelemetry/+trace/SpanContext.m | 111 ++++++++++++++++-- .../trace/SpanContextProxy.h | 16 ++- api/trace/src/SpanContextProxy.cpp | 66 ++++++++++- test/ttrace.m | 73 +++++++++++- 8 files changed, 262 insertions(+), 24 deletions(-) diff --git a/api/context/+opentelemetry/+context/Context.m b/api/context/+opentelemetry/+context/Context.m index 424202c..d6c4d52 100644 --- a/api/context/+opentelemetry/+context/Context.m +++ b/api/context/+opentelemetry/+context/Context.m @@ -5,8 +5,9 @@ % Copyright 2023-2024 The MathWorks, Inc. properties (Access={?opentelemetry.context.propagation.TextMapPropagator, ... - ?opentelemetry.trace.Span, ?opentelemetry.trace.Tracer, ... - ?opentelemetry.logs.Logger, ?opentelemetry.baggage.Baggage}) + ?opentelemetry.trace.Span, ?opentelemetry.trace.SpanContext, ... + ?opentelemetry.trace.Tracer, ?opentelemetry.logs.Logger, ... + ?opentelemetry.baggage.Baggage}) Proxy % Proxy object to interface C++ code end diff --git a/api/trace/+opentelemetry/+trace/Context.m b/api/trace/+opentelemetry/+trace/Context.m index 88acd30..6e85dd2 100644 --- a/api/trace/+opentelemetry/+trace/Context.m +++ b/api/trace/+opentelemetry/+trace/Context.m @@ -1,7 +1,7 @@ classdef Context % Tracing-related actions on context instances -% Copyright 2023 The MathWorks, Inc. +% Copyright 2023-2024 The MathWorks, Inc. methods (Static) function sp = extractSpan(context) @@ -22,12 +22,12 @@ function context = insertSpan(context, span) % Insert span into context % NEWCTXT = OPENTELEMETRY.TRACE.CONTEXT.INSERTSPAN(CTXT, SP) inserts - % span SP into a context object CTXT and returns a new context. + % span or span context SP into a context object CTXT and returns a new context. % % See also EXTRACTSPAN, OPENTELEMETRY.CONTEXT.CONTEXT arguments context (1,1) opentelemetry.context.Context - span (1,1) opentelemetry.trace.Span + span (1,1) {mustBeA(span, ["opentelemetry.trace.Span", "opentelemetry.trace.SpanContext"])} end context = span.insertSpan(context); % call span method end diff --git a/api/trace/+opentelemetry/+trace/Link.m b/api/trace/+opentelemetry/+trace/Link.m index 74e0d1d..04e67c3 100644 --- a/api/trace/+opentelemetry/+trace/Link.m +++ b/api/trace/+opentelemetry/+trace/Link.m @@ -1,10 +1,10 @@ classdef Link % Specifies a link to a span -% Copyright 2023 The MathWorks, Inc. +% Copyright 2023-2024 The MathWorks, Inc. properties (SetAccess=immutable) - Target (1,1) opentelemetry.trace.SpanContext % Target span + Target % Target span context end properties (Access=?opentelemetry.trace.Tracer) diff --git a/api/trace/+opentelemetry/+trace/Scope.m b/api/trace/+opentelemetry/+trace/Scope.m index 03040d5..1a510da 100644 --- a/api/trace/+opentelemetry/+trace/Scope.m +++ b/api/trace/+opentelemetry/+trace/Scope.m @@ -2,13 +2,14 @@ % Controls the duration when a span is current. Deleting a scope object % makes the associated span no longer current. -% Copyright 2023 The MathWorks, Inc. +% Copyright 2023-2024 The MathWorks, Inc. properties (Access=private) Proxy % Proxy object to interface C++ code end - methods (Access=?opentelemetry.trace.Span) + methods (Access={?opentelemetry.trace.Span, ... + ?opentelemetry.trace.SpanContext}) function obj = Scope(proxy) obj.Proxy = proxy; end diff --git a/api/trace/+opentelemetry/+trace/SpanContext.m b/api/trace/+opentelemetry/+trace/SpanContext.m index e426d18..04e840e 100644 --- a/api/trace/+opentelemetry/+trace/SpanContext.m +++ b/api/trace/+opentelemetry/+trace/SpanContext.m @@ -1,7 +1,7 @@ classdef SpanContext < handle % The part of a span that is propagated. -% Copyright 2023 The MathWorks, Inc. +% Copyright 2023-2024 The MathWorks, Inc. properties (Dependent, SetAccess=private) TraceId (1,1) string % Trace identifier represented as a string of 32 hexadecimal digits @@ -14,14 +14,71 @@ Proxy % Proxy object to interface C++ code end - methods (Access={?opentelemetry.trace.Span,?opentelemetry.trace.Link}) - function obj = SpanContext(proxy) - if nargin < 1 + methods + function obj = SpanContext(traceid, spanid, varargin) + % Span context + % SC = OPENTELEMETRY.TRACE.SPANCONTEXT(TRACEID, SPANID) + % creates a span context with the specified trace and span + % IDs. Trace and span IDs must be strings or char vectors + % containing a hexadecimal number. Trace IDs must be 32 + % hexadecimal digits long and span IDs must be 16 + % hexadecimal digits long. Valid IDs must be non-zero. + % + % SC = OPENTELEMETRY.TRACE.SPANCONTEXT(TRACEID, SPANID, + % PARAM1, VALUE1, PARAM2, VALUE2, ...) specifies optional + % parameter name/value pairs. Parameters are: + % "IsSampled" - Whether span is sampled. Default is + % true. + % "IsRemote" - Whether span is created in a remote + % process. Default is true. + + if nargin == 1 && isa(traceid, "libmexclass.proxy.Proxy") + % internal calls to constructor with a proxy + obj.Proxy = traceid; + else + narginchk(2, inf); + traceid_len = 32; + spanid_len = 16; + if ~((isstring(traceid) || (ischar(traceid) && isrow(traceid))) && ... + strlength(traceid) == traceid_len && all(isstrprop(traceid, "xdigit"))) + traceid = repmat('0', 1, traceid_len); % replace any illegal values with an all-zeros invalid ID + end + if ~((isstring(spanid) || (ischar(spanid) && isrow(spanid))) && ... + strlength(spanid) == spanid_len && all(isstrprop(spanid, "xdigit"))) + spanid = repmat('0', 1, spanid_len); % replace any illegal values with an all-zeros invalid ID + end + % convert IDs from string to uint8 array + traceid = uint8(hex2dec(reshape(char(traceid), 2, traceid_len/2).')); + spanid = uint8(hex2dec(reshape(char(spanid), 2, spanid_len/2).')); + + % default option values + issampled = true; + isremote = true; + if nargin > 2 + optionnames = ["IsSampled", "IsRemote"]; + for i = 1:2:length(varargin) + try + namei = validatestring(varargin{i}, optionnames); + catch + % invalid option, ignore + continue + end + valuei = varargin{i+1}; + if strcmp(namei, "IsSampled") + if (isnumeric(valuei) || islogical(valuei)) && isscalar(valuei) + issampled = logical(valuei); + end + else % strcmp(namei, "IsRemote") + if (isnumeric(valuei) || islogical(valuei)) && isscalar(valuei) + isremote = logical(valuei); + end + end + end + end + obj.Proxy = libmexclass.proxy.Proxy("Name", ... "libmexclass.opentelemetry.SpanContextProxy", ... - "ConstructorArguments", {}); - else - obj.Proxy = proxy; + "ConstructorArguments", {traceid, spanid, issampled, isremote}); end end end @@ -70,6 +127,46 @@ % ISSAMPLED, ISVALID tf = obj.Proxy.isRemote(); end + + function scope = makeCurrent(obj) + % MAKECURRENT Make span the current span + % SCOPE = MAKECURRENT(SPCTXT) makes the span represented by + % span context SPCTXT as the current span, by + % inserting it into the current context. Returns a scope + % object SCOPE that determines the duration when span is current. + % When SCOPE is deleted, span will no longer be current. + % + % See also OPENTELEMETRY.CONTEXT.CONTEXT, + % OPENTELEMETRY.GETCURRENTCONTEXT, OPENTELEMETRY.TRACE.SCOPE + + % return a warning if no output specified + if nargout == 0 + warning("opentelemetry:trace:SpanContext:makeCurrent:NoOutputSpecified", ... + "Calling makeCurrent without specifying an output has no effect.") + end + id = obj.Proxy.makeCurrent(); + scopeproxy = libmexclass.proxy.Proxy("Name", ... + "libmexclass.opentelemetry.ScopeProxy", "ID", id); + scope = opentelemetry.trace.Scope(scopeproxy); + end + + function context = insertSpan(obj, context) + % INSERTSPAN Insert span into a context and return a new context. + % NEWCTXT = INSERTSPAN(SPCTXT, CTXT) inserts the span + % represented by span context SPCTXT into context CTXT and + % returns a new context. + % + % NEWCTXT = INSERTSPAN(SPCTXT) inserts into the current context. + % + % See also OPENTELEMETRY.TRACE.CONTEXT.EXTRACTSPAN + if nargin < 2 + context = opentelemetry.context.getCurrentContext(); + end + contextid = obj.Proxy.insertSpan(context.Proxy.ID); + contextproxy = libmexclass.proxy.Proxy("Name", ... + "libmexclass.opentelemetry.ContextProxy", "ID", contextid); + context = opentelemetry.context.Context(contextproxy); + end end end diff --git a/api/trace/include/opentelemetry-matlab/trace/SpanContextProxy.h b/api/trace/include/opentelemetry-matlab/trace/SpanContextProxy.h index a3b0f7c..eb36ea8 100644 --- a/api/trace/include/opentelemetry-matlab/trace/SpanContextProxy.h +++ b/api/trace/include/opentelemetry-matlab/trace/SpanContextProxy.h @@ -1,4 +1,4 @@ -// Copyright 2023 The MathWorks, Inc. +// Copyright 2023-2024 The MathWorks, Inc. #pragma once @@ -6,6 +6,9 @@ #include "libmexclass/proxy/method/Context.h" #include "opentelemetry/trace/span_context.h" +#include "opentelemetry/trace/trace_id.h" +#include "opentelemetry/trace/span_id.h" +#include "opentelemetry/trace/trace_flags.h" namespace trace_api = opentelemetry::trace; namespace nostd = opentelemetry::nostd; @@ -22,12 +25,11 @@ class SpanContextProxy : public libmexclass::proxy::Proxy { REGISTER_METHOD(SpanContextProxy, isSampled); REGISTER_METHOD(SpanContextProxy, isValid); REGISTER_METHOD(SpanContextProxy, isRemote); + REGISTER_METHOD(SpanContextProxy, makeCurrent); + REGISTER_METHOD(SpanContextProxy, insertSpan); } - // dummy make static method, to satisfy proxy registration - static libmexclass::proxy::MakeResult make(const libmexclass::proxy::FunctionArguments& constructor_arguments) { - return std::make_shared(trace_api::SpanContext{false, false}); - } + static libmexclass::proxy::MakeResult make(const libmexclass::proxy::FunctionArguments& constructor_arguments); trace_api::SpanContext getInstance() { return CppSpanContext; @@ -47,6 +49,10 @@ class SpanContextProxy : public libmexclass::proxy::Proxy { void isRemote(libmexclass::proxy::method::Context& context); + void makeCurrent(libmexclass::proxy::method::Context& context); + + void insertSpan(libmexclass::proxy::method::Context& context); + private: trace_api::SpanContext CppSpanContext; diff --git a/api/trace/src/SpanContextProxy.cpp b/api/trace/src/SpanContextProxy.cpp index a09ba18..6b40eab 100644 --- a/api/trace/src/SpanContextProxy.cpp +++ b/api/trace/src/SpanContextProxy.cpp @@ -1,12 +1,37 @@ -// Copyright 2023 The MathWorks, Inc. +// Copyright 2023-2024 The MathWorks, Inc. #include "opentelemetry-matlab/trace/SpanContextProxy.h" +#include "opentelemetry-matlab/trace/ScopeProxy.h" +#include "opentelemetry-matlab/context/ContextProxy.h" #include "libmexclass/proxy/ProxyManager.h" +#include "opentelemetry/trace/default_span.h" +#include "opentelemetry/trace/context.h" +#include "opentelemetry/trace/trace_flags.h" + namespace common = opentelemetry::common; +namespace context_api = opentelemetry::context; namespace libmexclass::opentelemetry { + +libmexclass::proxy::MakeResult SpanContextProxy::make(const libmexclass::proxy::FunctionArguments& constructor_arguments) { + matlab::data::TypedArray traceid_mda = constructor_arguments[0]; + trace_api::TraceId traceid(nostd::span(&(*traceid_mda.cbegin()), 16)); + matlab::data::TypedArray spanid_mda = constructor_arguments[1]; + trace_api::SpanId spanid{nostd::span(&(*spanid_mda.cbegin()), 8)}; + matlab::data::TypedArray issampled_mda = constructor_arguments[2]; + bool issampled = issampled_mda[0]; + matlab::data::TypedArray isremote_mda = constructor_arguments[3]; + bool isremote = isremote_mda[0]; + + uint8_t traceflags = 0; + if (issampled) { + traceflags |= trace_api::TraceFlags::kIsSampled; + } + return std::make_shared(trace_api::SpanContext{traceid, spanid, trace_api::TraceFlags(traceflags), isremote}); +} + void SpanContextProxy::getTraceId(libmexclass::proxy::method::Context& context) { const trace_api::TraceId& tid = CppSpanContext.trace_id(); @@ -90,4 +115,43 @@ void SpanContextProxy::isRemote(libmexclass::proxy::method::Context& context) { context.outputs[0] = remote_mda; } +void SpanContextProxy::makeCurrent(libmexclass::proxy::method::Context& context) { + // create a default span to associate with span context + auto cppspan = nostd::shared_ptr(new trace_api::DefaultSpan(CppSpanContext)); + + // instantiate a ScopeProxy instance + auto scproxy = std::shared_ptr(new ScopeProxy{cppspan}); + + // obtain a proxy ID + libmexclass::proxy::ID proxyid = libmexclass::proxy::ProxyManager::manageProxy(scproxy); + + // return the ID + matlab::data::ArrayFactory factory; + auto proxyid_mda = factory.createScalar(proxyid); + context.outputs[0] = proxyid_mda; +} + +void SpanContextProxy::insertSpan(libmexclass::proxy::method::Context& context) { + matlab::data::TypedArray contextid_mda = context.inputs[0]; + libmexclass::proxy::ID contextid = contextid_mda[0]; + + // create a default span to associate with span context + auto cppspan = nostd::shared_ptr(new trace_api::DefaultSpan(CppSpanContext)); + + context_api::Context ctxt = std::static_pointer_cast( + libmexclass::proxy::ProxyManager::getProxy(contextid))->getInstance(); + context_api::Context newctxt = trace_api::SetSpan(ctxt, cppspan); + + // instantiate a ContextProxy instance + auto ctxtproxy = std::shared_ptr(new ContextProxy(std::move(newctxt))); + + // obtain a proxy ID + libmexclass::proxy::ID proxyid = libmexclass::proxy::ProxyManager::manageProxy(ctxtproxy); + + // return the ID + matlab::data::ArrayFactory factory; + auto proxyid_mda = factory.createScalar(proxyid); + context.outputs[0] = proxyid_mda; +} + } // namespace libmexclass::opentelemetry diff --git a/test/ttrace.m b/test/ttrace.m index 10d0ebc..209be3b 100644 --- a/test/ttrace.m +++ b/test/ttrace.m @@ -240,8 +240,8 @@ function testSpanName(testCase) verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.spans.name), newname); end - function testSpanContext(testCase) - % testSpanContext: getSpanContext + function testGetSpanContext(testCase) + % testGetSpanContext: getSpanContext tp = opentelemetry.sdk.trace.TracerProvider(); tr = getTracer(tp, "foo"); sp = startSpan(tr, "bar"); @@ -256,6 +256,50 @@ function testSpanContext(testCase) verifyEqual(testCase, ctxt.TraceFlags, "01"); % sampled flag should be on end + function testSpanContext(testCase) + % testSpanContext: create a new span context and specify it as + % parent + traceid = "0123456789ABCDEF0123456789abcdef"; + spanid = "0000000000111122"; + issampled = false; + isremote = false; + sc = opentelemetry.trace.SpanContext(traceid, spanid, ... + "IsSampled", issampled, "IsRemote", isremote); + + % verify SpanContext object created correctly + verifyEqual(testCase, sc.TraceId, lower(traceid)); + verifyEqual(testCase, sc.SpanId, spanid); + verifyEqual(testCase, sc.TraceState, ""); + verifyEqual(testCase, sc.TraceFlags, "00"); % sampled flag should be off + verifyEqual(testCase, isRemote(sc), isremote); + + % start a span and pass in context + context = opentelemetry.trace.Context.insertSpan(... + opentelemetry.context.Context, sc); + tp = opentelemetry.sdk.trace.TracerProvider(); + tr = getTracer(tp, "foo"); + sp = startSpan(tr, "bar", "Context", context); + endSpan(sp); + + % start another span and declare parent implicitly + curscope = makeCurrent(sc); %#ok + sp1 = startSpan(tr, "quux"); + endSpan(sp1); + clear("curscope"); + + % perform test comparisons + results = readJsonResults(testCase); + + verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.spans.traceId), ... + lower(traceid)); + verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.spans.parentSpanId), ... + spanid); + verifyEqual(testCase, string(results{2}.resourceSpans.scopeSpans.spans.traceId), ... + lower(traceid)); + verifyEqual(testCase, string(results{2}.resourceSpans.scopeSpans.spans.parentSpanId), ... + spanid); + end + function testTime(testCase) % testTime: specifying start and end times tp = opentelemetry.sdk.trace.TracerProvider(); @@ -734,5 +778,30 @@ function testInvalidStatus(testCase) % check span status verifyEmpty(testCase, fieldnames(results.resourceSpans.scopeSpans.spans.status)); % status unset end + + function testInvalidSpanContext(testCase) + % testInvalidSpanContext: create a span context with an invalid + % trace or span ID + traceid = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; + spanid = "yyyyyyyyyyyyyyyy"; + sc = opentelemetry.trace.SpanContext(traceid, spanid); + + % verify SpanContext object uses default invalid IDs + verifyEqual(testCase, sc.TraceId, string(repmat('0', 1, 32))); + verifyEqual(testCase, sc.SpanId, string(repmat('0', 1, 16))); + + % start a span, pass in context, check it has no effect + context = opentelemetry.trace.Context.insertSpan(... + opentelemetry.context.Context, sc); + tp = opentelemetry.sdk.trace.TracerProvider(); + tr = getTracer(tp, "foo"); + sp = startSpan(tr, "bar", "Context", context); + endSpan(sp); + + % perform test comparisons + results = readJsonResults(testCase); + + verifyEmpty(testCase, results{1}.resourceSpans.scopeSpans.spans.parentSpanId); + end end end