Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 66 additions & 4 deletions api/trace/+opentelemetry/+trace/Span.m
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
classdef Span < handle
% A span that represents a unit of work within a trace.

% Copyright 2023-2025 The MathWorks, Inc.
% Copyright 2023-2026 The MathWorks, Inc.

properties
Name (1,1) string % Name of span
Expand Down Expand Up @@ -129,14 +129,76 @@ function addEvent(obj, eventname, varargin)
attrs{1,i} = attrnames(i);
attrs(2,i) = attrvalues(i);
end
obj.Proxy.addEvent(eventname, eventtime, attrs{:});
obj.Proxy.addEvent(eventname, eventtime, attrs{:});
end

function setStatus(obj, status, description)
function recordException(obj, exception, varargin)
% RECORDEXCEPTION Record an exception as an event.
% RECORDEXCEPTION(SP, EXCEPTION) records a MATLAB exception
% (MException object) as an event at the current time.
%
% RECORDEXCEPTION(SP, EXCEPTION, TIME) also specifies the event
% time. If TIME does not have a time zone specified, it is
% interpreted as a UTC time.
%
% RECORDEXCEPTION(..., ATTRIBUTES) or RECORDEXCEPTION(..., ATTRNAME1,
% ATTRVALUE1, ATTRNAME2, ATTRVALUE2, ...) specifies additional
% attribute name/value pairs for the event, either as a
% dictionary or as trailing inputs.
%
% See also ADDEVENT, SETSTATUS

arguments
obj
exception (1,1) MException
end
arguments (Repeating)
varargin
end

% Process event time input first
eventtime = [];
remainingArgs = varargin;
if ~isempty(remainingArgs) && isdatetime(remainingArgs{1})
eventtime = remainingArgs{1};
remainingArgs(1) = []; % remove the time input
end

% Process any additional user-provided attributes
[userAttrNames, userAttrValues] = opentelemetry.common.processAttributes(remainingArgs);

% Build the exception attributes
exceptionAttrs = dictionary();

% Standard exception attributes
exceptionAttrs("exception.identifier") = string(exception.identifier);
exceptionAttrs("exception.message") = string(exception.message);
exceptionAttrs("exception.stacktrace") = jsonencode(exception.stack);
exceptionAttrs("exception.cause") = jsonencode(exception.cause);

% Merge user attributes with exception attributes
% User attributes should not override the standard exception attributes
for i = 1:length(userAttrNames)
attrName = userAttrNames(i);
if ~isKey(exceptionAttrs, attrName)
exceptionAttrs(attrName) = userAttrValues{i};
end
% Silently ignore conflicting attributes
end

% Call addEvent with the exception attributes
if isempty(eventtime)
obj.addEvent("exception", exceptionAttrs);
else
obj.addEvent("exception", eventtime, exceptionAttrs);
end
end

function setStatus(obj, status, description)
% SETSTATUS Set the span status.
% SETSTATUS(SP, STATUS) sets the span status as "Ok" or
% "Error".
%
%
% SETSTATUS(SP, STATUS, DESC) also specifies a description.
% Description is only recorded if status is "Error".
try
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
classdef AutoTrace < handle
% Automatic instrumentation with OpenTelemetry tracing.

% Copyright 2024-2025 The MathWorks, Inc.
% Copyright 2024-2026 The MathWorks, Inc.

properties (SetAccess=private)
StartFunction function_handle % entry function
Expand Down Expand Up @@ -155,7 +155,9 @@ function handleError(obj, ME)
% spans and their corresponding scopes. Rethrow the
% exception ME.
if ~isempty(obj.Instrumentor.Spans)
setStatus(obj.Instrumentor.Spans(end), "Error", ME.message);
errorspan = obj.Instrumentor.Spans(end);
setStatus(errorspan, "Error", ME.message);
recordException(errorspan, ME);
for i = length(obj.Instrumentor.Spans):-1:1
obj.Instrumentor.Spans(i) = [];
obj.Instrumentor.Scopes(i) = [];
Expand Down
86 changes: 70 additions & 16 deletions test/tautotrace.m
Original file line number Diff line number Diff line change
Expand Up @@ -289,24 +289,51 @@ function testError(testCase)
at = opentelemetry.autoinstrument.AutoTrace(@linearfit_example);

% run the example with an invalid input, check for error
verifyError(testCase, @()beginTrace(at, "invalid"), "autotrace_examples:linearfit_example:generate_data:InvalidN");
errorid_expected = "autotrace_examples:linearfit_example:generate_data:InvalidN";
errormsg_expected = "Input must be a numeric scalar";
verifyError(testCase, @()beginTrace(at, "invalid"), errorid_expected);

% perform test comparisons
results = readJsonResults(testCase);
verifyNumElements(testCase, results, 2);

% check span names
verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.spans.name), "generate_data");
verifyEqual(testCase, string(results{2}.resourceSpans.scopeSpans.spans.name), "linearfit_example");
spannames = cellfun(@(x)string(x.resourceSpans.scopeSpans.spans.name), results);
spannames_expected = ["generate_data" "linearfit_example"];
[lia, locb] = ismember(spannames_expected, spannames);
verifyTrue(testCase, all(lia));
generatedata = results{locb(1)};
linearfitexample = results{locb(2)};

% check parent children relationship
verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.parentSpanId, results{2}.resourceSpans.scopeSpans.spans.spanId);
verifyEqual(testCase, generatedata.resourceSpans.scopeSpans.spans.parentSpanId, linearfitexample.resourceSpans.scopeSpans.spans.spanId);

% check error status
verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.status.code, 2); % error
verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.status.message, ...
'Input must be a numeric scalar');
verifyEmpty(testCase, fieldnames(results{2}.resourceSpans.scopeSpans.spans.status)); % ok, no error
verifyEqual(testCase, generatedata.resourceSpans.scopeSpans.spans.status.code, 2); % error
verifyEqual(testCase, string(generatedata.resourceSpans.scopeSpans.spans.status.message), ...
errormsg_expected);
verifyEmpty(testCase, fieldnames(linearfitexample.resourceSpans.scopeSpans.spans.status)); % ok, no error

% check exception event
verifyTrue(testCase, isfield(generatedata.resourceSpans.scopeSpans.spans, "events")); % exception event in "generate_data" span
exception = generatedata.resourceSpans.scopeSpans.spans.events;
verifyEqual(testCase, string(exception.name), "exception");
% exception attributes
exception_attrkeys = string({exception.attributes.key});
identifieridx = find(exception_attrkeys == "exception.identifier");
verifyNotEmpty(testCase, identifieridx);
verifyEqual(testCase, string(exception.attributes(identifieridx).value.stringValue), errorid_expected);
messageidx = find(exception_attrkeys == "exception.message");
verifyNotEmpty(testCase, messageidx);
verifyEqual(testCase, string(exception.attributes(messageidx).value.stringValue), errormsg_expected);
causeidx = find(exception_attrkeys == "exception.cause");
verifyNotEmpty(testCase, causeidx);
verifyEqual(testCase, string(exception.attributes(causeidx).value.stringValue), "[]");
stacktraceidx = find(exception_attrkeys == "exception.stacktrace");
verifyNotEmpty(testCase, stacktraceidx);
verifyTrue(testCase, contains(string(exception.attributes(stacktraceidx).value.stringValue), "generate_data.m"));

verifyFalse(testCase, isfield(linearfitexample.resourceSpans.scopeSpans.spans, "events")); % no exception event in "linearfit_example" span
end

function testHandleError(testCase)
Expand All @@ -324,25 +351,52 @@ function testHandleError(testCase)

% call example directly instead of calling beginTrace, and pass
% in an invalid input
errorid_expected = "autotrace_examples:linearfit_example:generate_data:InvalidN";
errormsg_expected = "Input must be a numeric scalar";
verifyError(testCase, @()linearfit_example_trycatch(at, "invalid"), ...
"autotrace_examples:linearfit_example:generate_data:InvalidN");
errorid_expected);

% perform test comparisons
results = readJsonResults(testCase);
verifyNumElements(testCase, results, 2);

% check span names
verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.spans.name), "generate_data");
verifyEqual(testCase, string(results{2}.resourceSpans.scopeSpans.spans.name), "linearfit_example_trycatch");
spannames = cellfun(@(x)string(x.resourceSpans.scopeSpans.spans.name), results);
spannames_expected = ["generate_data" "linearfit_example_trycatch"];
[lia, locb] = ismember(spannames_expected, spannames);
verifyTrue(testCase, all(lia));
generatedata = results{locb(1)};
linearfitexample = results{locb(2)};

% check parent children relationship
verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.parentSpanId, results{2}.resourceSpans.scopeSpans.spans.spanId);
verifyEqual(testCase, generatedata.resourceSpans.scopeSpans.spans.parentSpanId, linearfitexample.resourceSpans.scopeSpans.spans.spanId);

% check error status
verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.status.code, 2); % error
verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.status.message, ...
'Input must be a numeric scalar');
verifyEmpty(testCase, fieldnames(results{2}.resourceSpans.scopeSpans.spans.status)); % ok, no error
verifyEqual(testCase, generatedata.resourceSpans.scopeSpans.spans.status.code, 2); % error
verifyEqual(testCase, string(generatedata.resourceSpans.scopeSpans.spans.status.message), ...
errormsg_expected);
verifyEmpty(testCase, fieldnames(linearfitexample.resourceSpans.scopeSpans.spans.status)); % ok, no error

% check exception event
verifyTrue(testCase, isfield(generatedata.resourceSpans.scopeSpans.spans, "events")); % exception event in "generate_data" span
exception = generatedata.resourceSpans.scopeSpans.spans.events;
verifyEqual(testCase, string(exception.name), "exception");
% exception attributes
exception_attrkeys = string({exception.attributes.key});
identifieridx = find(exception_attrkeys == "exception.identifier");
verifyNotEmpty(testCase, identifieridx);
verifyEqual(testCase, string(exception.attributes(identifieridx).value.stringValue), errorid_expected);
messageidx = find(exception_attrkeys == "exception.message");
verifyNotEmpty(testCase, messageidx);
verifyEqual(testCase, string(exception.attributes(messageidx).value.stringValue), errormsg_expected);
causeidx = find(exception_attrkeys == "exception.cause");
verifyNotEmpty(testCase, causeidx);
verifyEqual(testCase, string(exception.attributes(causeidx).value.stringValue), "[]");
stacktraceidx = find(exception_attrkeys == "exception.stacktrace");
verifyNotEmpty(testCase, stacktraceidx);
verifyTrue(testCase, contains(string(exception.attributes(stacktraceidx).value.stringValue), "generate_data.m"));

verifyFalse(testCase, isfield(linearfitexample.resourceSpans.scopeSpans.spans, "events")); % no exception event in "linearfit_example_trycatch" span
end

function testMultipleInstances(testCase)
Expand Down
75 changes: 74 additions & 1 deletion test/ttrace.m
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
classdef ttrace < matlab.unittest.TestCase
% tests for traces and spans

% Copyright 2023-2025 The MathWorks, Inc.
% Copyright 2023-2026 The MathWorks, Inc.

properties
OtelConfigFile
Expand Down Expand Up @@ -633,6 +633,79 @@ function testEvents(testCase)

end

function testRecordException(testCase)
% Test recording a simple exception

% Define exception properties
errid = "TestID:SimpleError";
errmsg = "This is a test error message";

% Create tracer and span
tp = opentelemetry.sdk.trace.TracerProvider();
tracer = getTracer(tp, "test_tracer");
span = startSpan(tracer, "test_span");

% Generate exception
except = generateException(errid, errmsg);
% add a cause
causeid = "TestID:CauseError";
causemsg = "This is the cause";
cause = generateException(causeid, causemsg);
except = addCause(except, cause);

% record exception with an extra attribute
attr1name = "foo";
attr1val = "bar";
recordException(span, except, attr1name, attr1val);

endSpan(span);

% Get results
results = readJsonResults(testCase);

% Verify event exists and has correct name
events = results{1}.resourceSpans.scopeSpans.spans.events;
verifyEqual(testCase, length(events), 1);
verifyEqual(testCase, events(1).name, 'exception');

% Get all attribute keys
eventAttrs = events(1).attributes;
attrKeys = string({eventAttrs.key});

% Verify exception.identifier
idIdx = find(attrKeys == "exception.identifier");
verifyNotEmpty(testCase, idIdx);
verifyEqual(testCase, string(eventAttrs(idIdx).value.stringValue), errid);

% Verify exception.message
msgIdx = find(attrKeys == "exception.message");
verifyNotEmpty(testCase, msgIdx);
verifyEqual(testCase, string(eventAttrs(msgIdx).value.stringValue), errmsg);

% Verify exception.stacktrace
stackIdx = find(attrKeys == "exception.stacktrace");
verifyNotEmpty(testCase, stackIdx);
stack = jsondecode(string(eventAttrs(stackIdx).value.stringValue));
verifyNotEmpty(testCase, stack); % Should have stack frames now
verifyEqual(testCase, stack, except.stack);

% Verify exception.cause
causeIdx = find(attrKeys == "exception.cause");
verifyNotEmpty(testCase, causeIdx);
causeAttr = jsondecode(string(eventAttrs(causeIdx).value.stringValue));
verifyEqual(testCase, causeAttr.identifier, cause.identifier);
verifyEqual(testCase, causeAttr.message, cause.message);
verifyEqual(testCase, causeAttr.stack, cause.stack);
verifyEmpty(testCase, causeAttr.cause);
verifyEmpty(testCase, causeAttr.Correction);

% Verify extra attribute
attrIdx = find(attrKeys == attr1name);
verifyNotEmpty(testCase, attrIdx);
verifyEqual(testCase, string(eventAttrs(attrIdx).value.stringValue), attr1val);
end


function testLinks(testCase)
% testLinks: specifying links between spans

Expand Down
15 changes: 15 additions & 0 deletions test/utils/generateException.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
function me = generateException(errid, errmsg)
% Return a thrown exception

% Copyright 2026 The MathWorks, Inc.

try
exceptionHelper(errid, errmsg);
catch me
end
end

function exceptionHelper(errid, errmsg)
me = MException(errid, errmsg);
throw(me);
end
Loading