From 4ccf8ceb8652e4f46c8012823ec4b88394dff5ed Mon Sep 17 00:00:00 2001 From: duncanpo Date: Mon, 21 Oct 2024 11:00:55 -0400 Subject: [PATCH 1/7] Add AutoTrace source code --- CMakeLists.txt | 2 + .../+autoinstrument/AutoTrace.m | 163 ++++++++++++++++++ .../+autoinstrument/AutoTraceInstrumentor.p | Bin 0 -> 4285 bytes 3 files changed, 165 insertions(+) create mode 100644 auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m create mode 100644 auto-instrumentation/+opentelemetry/+autoinstrument/AutoTraceInstrumentor.p diff --git a/CMakeLists.txt b/CMakeLists.txt index 873cde1..67a72ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -476,6 +476,7 @@ set(TRACE_SDK_MATLAB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/sdk/trace/+opentelemetr set(METRICS_SDK_MATLAB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/sdk/metrics/+opentelemetry) set(LOGS_SDK_MATLAB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/sdk/logs/+opentelemetry) set(COMMON_SDK_MATLAB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/sdk/common/+opentelemetry) +set(AUTO_INSTRUMENTATION_MATLAB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/auto-instrumentation/+opentelemetry) set(EXPORTER_MATLAB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/exporters/otlp/+opentelemetry/+exporters/+otlp/defaultSpanExporter.m ${CMAKE_CURRENT_SOURCE_DIR}/exporters/otlp/+opentelemetry/+exporters/+otlp/defaultMetricExporter.m @@ -510,6 +511,7 @@ install(DIRECTORY ${TRACE_SDK_MATLAB_SOURCES} DESTINATION .) install(DIRECTORY ${METRICS_SDK_MATLAB_SOURCES} DESTINATION .) install(DIRECTORY ${LOGS_SDK_MATLAB_SOURCES} DESTINATION .) install(DIRECTORY ${COMMON_SDK_MATLAB_SOURCES} DESTINATION .) +install(DIRECTORY ${AUTO_INSTRUMENTATION_MATLAB_SOURCES} DESTINATION .) install(FILES ${EXPORTER_MATLAB_SOURCES} DESTINATION ${OTLP_EXPORTERS_DIR}) if(WITH_OTLP_HTTP) install(FILES ${OTLP_HTTP_EXPORTER_MATLAB_SOURCES} DESTINATION ${OTLP_EXPORTERS_DIR}) diff --git a/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m b/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m new file mode 100644 index 0000000..4e0298b --- /dev/null +++ b/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m @@ -0,0 +1,163 @@ +classdef AutoTrace < handle + % Automatic instrumentation with OpenTelemetry tracing. + + % Copyright 2024 The MathWorks, Inc. + + properties (SetAccess=private) + StartFunction function_handle % entry function + InstrumentedFiles string % list of M-files that are auto-instrumented + end + + properties (Access=private) + Instrumentor (1,1) opentelemetry.autoinstrument.AutoTraceInstrumentor % helper object + end + + methods + function obj = AutoTrace(startfun, options) + % AutoTrace Automatic instrumentation with OpenTelemetry tracing + % AT = OPENTELEMETRY.AUTOINSTRUMENT.AUTOTRACE(FUN) where FUN + % is a function handle, automatically instruments the function + % and all the functions in the same file, as well as their dependencies. + % For each function, a span is automatically started and made + % current at the beginning, and ended at the end. Returns an + % object AT. When AT is cleared or goes out-of-scope, automatic + % instrumentation will stop and the functions will no longer + % be instrumented. + % + % AT = OPENTELEMETRY.AUTOINSTRUMENT.AUTOTRACE(FUN, NAME1, VALUE1, + % NAME2, VALUE2, ...) specifies optional name-value pairs. + % Supported options are: + % "AdditionalFiles" - List of additional file names to + % include. Specifying additional files + % are useful in cases when automatic + % dependency detection failed to include them. + % For example, MATLAB Toolbox functions + % authored by MathWorks are excluded by default. + % "ExcludeFiles" - List of file names to exclude + % "AutoDetectFiles" - Whether to automatically include dependencies + % of FUN, specified as a logical scalar. + % Default value is true. + % "TracerName" - Specifies the name of the tracer + % the automatic spans are generated from + % "TracerVersion" - The tracer version + % "TracerSchema" - The tracer schema + % "Attributes" - Add attributes to all the automatic spans. + % Attributes must be specified as a dictionary. + % "SpanKind" - Span kind of the automatic spans + arguments + startfun (1,1) function_handle + options.TracerName {mustBeTextScalar} = "AutoTrace" + options.TracerVersion {mustBeTextScalar} = "" + options.TracerSchema {mustBeTextScalar} = "" + options.SpanKind {mustBeTextScalar} + options.Attributes {mustBeA(options.Attributes, "dictionary")} + options.ExcludeFiles {mustBeText} + options.AdditionalFiles {mustBeText} + options.AutoDetectFiles (1,1) {mustBeNumericOrLogical} = true + end + obj.StartFunction = startfun; + startfunname = func2str(startfun); + processFileInput(startfunname); % validate startfun + if options.AutoDetectFiles + if isdeployed + % matlab.codetools.requiredFilesAndProducts is not + % deployable. Instead instrument all files under CTFROOT + fileinfo = dir(fullfile(ctfroot, "**", "*.m")); + files = fullfile(string({fileinfo.folder}), string({fileinfo.name})); + + % filter out internal files in the toolbox directory + files = files(~startsWith(files, fullfile(ctfroot, "toolbox"))); + else + %#exclude matlab.codetools.requiredFilesAndProducts + files = string(matlab.codetools.requiredFilesAndProducts(startfunname)); + end + else + % only include the input file, not its dependencies + files = string(which(startfunname)); + end + % add extra files, this is intended for files + % matlab.codetools.requiredFilesAndProducts somehow missed + if isfield(options, "AdditionalFiles") + incfiles = string(options.AdditionalFiles); + for i = 1:numel(incfiles) + incfiles(i) = which(incfiles(i)); % get the full path + processFileInput(incfiles(i)); % validate additional file + end + files = union(files, incfiles); + end + + % make sure files are unique + files = unique(files); + + % filter out excluded files + if isfield(options, "ExcludeFiles") + excfiles = string(options.ExcludeFiles); + for i = 1:numel(excfiles) + excfiles(i) = which(excfiles(i)); % get the full path + end + files = setdiff(files, excfiles); + end + % filter out OpenTelemetry files, in case manual + % instrumentation is also used + files = files(~contains(files, ["+opentelemetry" "+libmexclass"])); + + for i = 1:length(files) + currfile = files(i); + if currfile =="" % ignore empties + continue + end + obj.Instrumentor.instrument(currfile, options); + obj.InstrumentedFiles(end+1,1) = currfile; + end + end + + function delete(obj) + obj.Instrumentor.cleanup(obj.InstrumentedFiles); + end + + function varargout = beginTrace(obj, varargin) + % beginTrace Run the auto-instrumented function + % [OUT1, OUT2, ...] = BEGINTRACE(AT, IN1, IN2, ...) calls the + % instrumented function with error handling. In case of + % error, all running spans will end and the last span will + % set to an "Error" status. The instrumented function is + % called with the synax [OUT1, OUT2, ...] = FUN(IN1, IN2, ...) + % + % See also OPENTELEMETRY.AUTOINSTRUMENT.AUTOTRACE/HANDLEERROR + try + varargout = cell(1,nargout); + [varargout{:}] = feval(obj.StartFunction, varargin{:}); + catch ME + handleError(obj, ME); + end + end + + function handleError(obj, ME) + % handleError Perform cleanup in case of an error + % HANDLEERROR(AT, ME) performs cleanup by ending all running + % spans and their corresponding scopes. Rethrow the + % exception ME. + if ~isempty(obj.Instrumentor.Spans) + setStatus(obj.Instrumentor.Spans(end), "Error"); + for i = length(obj.Instrumentor.Spans):-1:1 + obj.Instrumentor.Spans(i) = []; + obj.Instrumentor.Scopes(i) = []; + end + end + rethrow(ME); + end + end + + +end + +% check input file is valid +function processFileInput(f) +f = string(f); % force into a string +if startsWith(f, '@') % check for anonymous function + error(f + " is an anonymous function and is not supported."); +end +if exist(f, "file") ~= 2 + error(f + " is not a valid MATLAB file with a .m extension and is not supported.") +end +end \ No newline at end of file diff --git a/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTraceInstrumentor.p b/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTraceInstrumentor.p new file mode 100644 index 0000000000000000000000000000000000000000..ea478ae437870edad3a10b0c959d71ef516f2435 GIT binary patch literal 4285 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 literal 0 HcmV?d00001 From 3f43d34a8009772155d8ae33dbe765585686a105 Mon Sep 17 00:00:00 2001 From: duncanpo Date: Mon, 21 Oct 2024 11:49:48 -0400 Subject: [PATCH 2/7] Add error IDs and update help text about deployable archives --- .../+opentelemetry/+autoinstrument/AutoTrace.m | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m b/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m index 4e0298b..da001fe 100644 --- a/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m +++ b/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m @@ -24,6 +24,9 @@ % instrumentation will stop and the functions will no longer % be instrumented. % + % If called in a deployable archive (CTF file), all M-files + % included in the CTF will be instrumented. + % % AT = OPENTELEMETRY.AUTOINSTRUMENT.AUTOTRACE(FUN, NAME1, VALUE1, % NAME2, VALUE2, ...) specifies optional name-value pairs. % Supported options are: @@ -155,9 +158,11 @@ function handleError(obj, ME) function processFileInput(f) f = string(f); % force into a string if startsWith(f, '@') % check for anonymous function - error(f + " is an anonymous function and is not supported."); + error("opentelemetry:autoinstrument:AutoTrace:AnonymousFunction", ... + f + " is an anonymous function and is not supported."); end if exist(f, "file") ~= 2 - error(f + " is not a valid MATLAB file with a .m extension and is not supported.") + error("opentelemetry:autoinstrument:AutoTrace:InvalidMFile", ... + f + " is not found or is not a valid MATLAB file with a .m extension.") end end \ No newline at end of file From ef0d8e378df2dd32b59b9568e36c2d1a67d15501 Mon Sep 17 00:00:00 2001 From: duncanpo Date: Mon, 21 Oct 2024 14:13:05 -0400 Subject: [PATCH 3/7] Add README file for auto instrumentation --- README.md | 3 +++ auto-instrumentation/README.md | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 auto-instrumentation/README.md diff --git a/README.md b/README.md index df80e89..23ff4cf 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,9 @@ otelcol --config For more examples, see the "examples" folder. +## Automatic Instrumentation +See example [here](auto-instrumentation/README.md). + ## Help To view documentation of individual function, type "help \\". For example, ``` diff --git a/auto-instrumentation/README.md b/auto-instrumentation/README.md new file mode 100644 index 0000000..48622fe --- /dev/null +++ b/auto-instrumentation/README.md @@ -0,0 +1,35 @@ +# Automatic Instrumentation + +Automatic instrumentation provides a way to instrument MATLAB code with OpenTelemetry data without requiring any code changes. + +## AutoTrace +With AutoTrace enabled, spans are automatically started at function beginnings and ended when functions end. By default, AutoTrace instruments the input function and all of its dependencies. An example workflow is as follows: +``` +% The example functions should be on the path when calling AutoTrace +addpath("myexample"); + +% Configure a tracer provider and set it as the global instance +tp = opentelemetry.sdk.trace.TracerProvider; % use default settings +setTracerProvider(tp); + +% Instrument the code +at = opentelemetry.autoinstrument.AutoTrace(@myexample, TracerName="AutoTraceExample"); + +% Start the example +beginTrace(at); +``` +Using the `beginTrace` method ensures proper error handling. In the case of an error, `beginTrace` will end all spans and set the "Error" status. + +Alternatively, you can also get the same behavior by inserting a try-catch in the starting function. +``` +function myexample(at) +% wrap a try catch around the code +try + % example code goes here +catch ME + handleError(at); +end +``` +With the try-catch, `beginTrace` method is no longer necessary and you can simply call `myexample` directly and pass in the AutoTrace object. + +To disable automatic tracing, delete the object returned by `AutoTrace`. From db5ea0b74ff4da03ef1fad4999546e5978ebf57e Mon Sep 17 00:00:00 2001 From: duncanpo Date: Mon, 21 Oct 2024 17:47:21 -0400 Subject: [PATCH 4/7] initial test for AutoTrace --- .../example1/best_fit_line.m | 7 ++ test/autotrace_examples/example1/example1.m | 8 +++ .../example1/generate_data.m | 11 +++ test/tautotrace.m | 69 +++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 test/autotrace_examples/example1/best_fit_line.m create mode 100644 test/autotrace_examples/example1/example1.m create mode 100644 test/autotrace_examples/example1/generate_data.m create mode 100644 test/tautotrace.m diff --git a/test/autotrace_examples/example1/best_fit_line.m b/test/autotrace_examples/example1/best_fit_line.m new file mode 100644 index 0000000..55a404a --- /dev/null +++ b/test/autotrace_examples/example1/best_fit_line.m @@ -0,0 +1,7 @@ +function yf = best_fit_line(x, y) +% example code for testing auto instrumentation + +% Copyright 2024 The MathWorks, Inc. + +coefs = polyfit(x, y, 1); +yf = polyval(coefs , x); diff --git a/test/autotrace_examples/example1/example1.m b/test/autotrace_examples/example1/example1.m new file mode 100644 index 0000000..f47f8aa --- /dev/null +++ b/test/autotrace_examples/example1/example1.m @@ -0,0 +1,8 @@ +function yf = example1(n) +% example code for testing auto instrumentation. Input n is the number of +% data points. + +% Copyright 2024 The MathWorks, Inc. + +[x, y] = generate_data(n); +yf = best_fit_line(x,y); \ No newline at end of file diff --git a/test/autotrace_examples/example1/generate_data.m b/test/autotrace_examples/example1/generate_data.m new file mode 100644 index 0000000..45168c2 --- /dev/null +++ b/test/autotrace_examples/example1/generate_data.m @@ -0,0 +1,11 @@ +function [x, y] = generate_data(n) +% example code for testing auto instrumentation + +% Copyright 2024 The MathWorks, Inc. + +% generate some random data +a = 1.5; +b = 0.8; +sigma = 5; +x = 1:n; +y = a * x + b + sigma * randn(1, n); \ No newline at end of file diff --git a/test/tautotrace.m b/test/tautotrace.m new file mode 100644 index 0000000..3873693 --- /dev/null +++ b/test/tautotrace.m @@ -0,0 +1,69 @@ +classdef tautotrace < matlab.unittest.TestCase + % tests for AutoTrace + + % Copyright 2024 The MathWorks, Inc. + + properties + OtelConfigFile + JsonFile + PidFile + OtelcolName + Otelcol + ListPid + ReadPidList + ExtractPid + Sigint + Sigterm + end + + methods (TestClassSetup) + function setupOnce(testCase) + % add the utils folder to the path + utilsfolder = fullfile(fileparts(mfilename('fullpath')), "utils"); + testCase.applyFixture(matlab.unittest.fixtures.PathFixture(utilsfolder)); + % add the example folder to the path + example1folder = fullfile(fileparts(mfilename('fullpath')), "autotrace_examples", "example1"); + testCase.applyFixture(matlab.unittest.fixtures.PathFixture(example1folder)); + commonSetupOnce(testCase); + end + end + + methods (TestMethodSetup) + function setup(testCase) + commonSetup(testCase); + end + end + + methods (TestMethodTeardown) + function teardown(testCase) + commonTeardown(testCase); + end + end + + methods (Test) + function testBasic(testCase) + % testBasic: instrument a simple example + + % configure the global tracer provider + tp = opentelemetry.sdk.trace.TracerProvider(); + setTracerProvider(tp); + clear("tp"); + + % set up AutoTrace + at = opentelemetry.autoinstrument.AutoTrace(@example1); + + % run the example + [~] = beginTrace(at, 100); + + % perform test comparisons + results = readJsonResults(testCase); + verifyNumElements(testCase, results, 3); + + % check logger name, log body and severity, trace and span IDs + verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.scope.name), "AutoTrace"); + verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.spans.name), "generate_data"); + verifyEqual(testCase, string(results{2}.resourceSpans.scopeSpans.spans.name), "best_fit_line"); + verifyEqual(testCase, string(results{3}.resourceSpans.scopeSpans.spans.name), "example1"); + end + end +end \ No newline at end of file From fb1290825e3f27249ec2238a558a0b3da1e293f4 Mon Sep 17 00:00:00 2001 From: duncanpo Date: Tue, 22 Oct 2024 12:46:40 -0400 Subject: [PATCH 5/7] add tests for AutoTrace --- .../example1/example1_trycatch.m | 12 ++ test/tautotrace.m | 197 +++++++++++++++++- 2 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 test/autotrace_examples/example1/example1_trycatch.m diff --git a/test/autotrace_examples/example1/example1_trycatch.m b/test/autotrace_examples/example1/example1_trycatch.m new file mode 100644 index 0000000..2d58fc1 --- /dev/null +++ b/test/autotrace_examples/example1/example1_trycatch.m @@ -0,0 +1,12 @@ +function yf = example1_trycatch(at, n) +% example code for testing auto instrumentation. This example should not +% use beginTrace method and instead should be called directly. + +% Copyright 2024 The MathWorks, Inc. + +try + [x, y] = generate_data(n); + yf = best_fit_line(x,y); +catch ME + handleError(at, ME); +end \ No newline at end of file diff --git a/test/tautotrace.m b/test/tautotrace.m index 3873693..e59d3fc 100644 --- a/test/tautotrace.m +++ b/test/tautotrace.m @@ -21,10 +21,14 @@ function setupOnce(testCase) % add the utils folder to the path utilsfolder = fullfile(fileparts(mfilename('fullpath')), "utils"); testCase.applyFixture(matlab.unittest.fixtures.PathFixture(utilsfolder)); - % add the example folder to the path + % add the example folders to the path example1folder = fullfile(fileparts(mfilename('fullpath')), "autotrace_examples", "example1"); testCase.applyFixture(matlab.unittest.fixtures.PathFixture(example1folder)); commonSetupOnce(testCase); + + % configure the global tracer provider + tp = opentelemetry.sdk.trace.TracerProvider(); + setTracerProvider(tp); end end @@ -43,12 +47,7 @@ function teardown(testCase) methods (Test) function testBasic(testCase) % testBasic: instrument a simple example - - % configure the global tracer provider - tp = opentelemetry.sdk.trace.TracerProvider(); - setTracerProvider(tp); - clear("tp"); - + % set up AutoTrace at = opentelemetry.autoinstrument.AutoTrace(@example1); @@ -59,11 +58,191 @@ function testBasic(testCase) results = readJsonResults(testCase); verifyNumElements(testCase, results, 3); - % check logger name, log body and severity, trace and span IDs - verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.scope.name), "AutoTrace"); + % check tracer and span names + verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.scope.name), "AutoTrace"); % default name verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.spans.name), "generate_data"); verifyEqual(testCase, string(results{2}.resourceSpans.scopeSpans.spans.name), "best_fit_line"); verifyEqual(testCase, string(results{3}.resourceSpans.scopeSpans.spans.name), "example1"); + + % check they belong to the same trace + verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.traceId, results{2}.resourceSpans.scopeSpans.spans.traceId); + verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.traceId, results{3}.resourceSpans.scopeSpans.spans.traceId); + + % check parent children relationship + verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.parentSpanId, results{3}.resourceSpans.scopeSpans.spans.spanId); + verifyEqual(testCase, results{2}.resourceSpans.scopeSpans.spans.parentSpanId, results{3}.resourceSpans.scopeSpans.spans.spanId); + end + + function testIncludeExcludeFiles(testCase) + % testIncludeExcludeFiles: AdditionalFiles and ExcludeFiles options + + % set up AutoTrace + at = opentelemetry.autoinstrument.AutoTrace(@example1, ... + "AdditionalFiles", "polyfit", "ExcludeFiles", "generate_data"); + + % run the example + [~] = beginTrace(at, 100); + + % perform test comparisons + results = readJsonResults(testCase); + verifyNumElements(testCase, results, 3); + + % check span names + verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.spans.name), "polyfit"); + verifyEqual(testCase, string(results{2}.resourceSpans.scopeSpans.spans.name), "best_fit_line"); + verifyEqual(testCase, string(results{3}.resourceSpans.scopeSpans.spans.name), "example1"); + + % check parent children relationship + verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.parentSpanId, results{2}.resourceSpans.scopeSpans.spans.spanId); + verifyEqual(testCase, results{2}.resourceSpans.scopeSpans.spans.parentSpanId, results{3}.resourceSpans.scopeSpans.spans.spanId); + end + + function testDisableFileDetection(testCase) + % testDisableFileDetection: AutoDetectFiles set to false + + % set up AutoTrace + at = opentelemetry.autoinstrument.AutoTrace(@example1, ... + "AutoDetectFiles", false); + + % run the example + [~] = beginTrace(at, 100); + + % perform test comparisons + results = readJsonResults(testCase); + + % should only be 1 span + verifyNumElements(testCase, results, 1); + verifyEqual(testCase, string(results{1}.resourceSpans.scopeSpans.spans.name), "example1"); + end + + function testNonFileOptions(testCase) + % testNonFileOptions: other options not related to files, + % "TracerName", "TracerVersion", "TracerSchema", "Attributes", + % "SpanKind" + + tracername = "foo"; + tracerversion = "1.1"; + tracerschema = "https://opentelemetry.io/schemas/1.28.0"; + spankind = "consumer"; + attrnames = ["foo" "bar"]; + attrvalues = [1 2]; + attrs = dictionary(attrnames, attrvalues); + % set up AutoTrace + at = opentelemetry.autoinstrument.AutoTrace(@example1, ... + "TracerName", tracername, "TracerVersion", tracerversion, ... + "TracerSchema", tracerschema, "SpanKind", spankind, "Attributes", attrs); + + % run the example + [~] = beginTrace(at, 100); + + % perform test comparisons + results = readJsonResults(testCase); + verifyNumElements(testCase, results, 3); + + % check specified options in each span + for i = 1:numel(results) + verifyEqual(testCase, string(results{i}.resourceSpans.scopeSpans.scope.name), tracername); + verifyEqual(testCase, string(results{i}.resourceSpans.scopeSpans.scope.version), tracerversion); + verifyEqual(testCase, string(results{i}.resourceSpans.scopeSpans.schemaUrl), tracerschema); + verifyEqual(testCase, results{i}.resourceSpans.scopeSpans.spans.kind, 5); % SpanKind consumer + + % attributes + attrkeys = string({results{i}.resourceSpans.scopeSpans.spans.attributes.key}); + + for ii = 1:numel(attrnames) + attrnameii = attrnames(ii); + idxii = find(attrkeys == attrnameii); + verifyNotEmpty(testCase, idxii); + verifyEqual(testCase, results{i}.resourceSpans.scopeSpans.spans.attributes(idxii).value.doubleValue, ... + attrvalues(ii)); + end + end + end + + function testError(testCase) + % testError: handling error situation + + % set up AutoTrace + at = opentelemetry.autoinstrument.AutoTrace(@example1); + + % run the example with an invalid input, check for error + verifyError(testCase, @()beginTrace(at, "invalid"), "MATLAB:colon:inputsMustBeNumericCharLogical"); + + % 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), "example1"); + + % check parent children relationship + verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.parentSpanId, results{2}.resourceSpans.scopeSpans.spans.spanId); + + % check error status + verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.status.code, 2); % error + verifyEmpty(testCase, fieldnames(results{2}.resourceSpans.scopeSpans.spans.status)); % ok, no error + end + + function testHandleError(testCase) + % testHandleError: directly call handleError method rather than using + % beginTrace method. This test should use example1_trycatch, which + % wraps a try-catch in the input function and calls handleError + % in the catch block. + + % set up AutoTrace, using example1_trycatch + at = opentelemetry.autoinstrument.AutoTrace(@example1_trycatch); + + % call example directly instead of calling beginTrace, and pass + % in an invalid input + verifyError(testCase, @()example1_trycatch(at, "invalid"), "MATLAB:colon:inputsMustBeNumericCharLogical"); + + % 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), "example1_trycatch"); + + % check parent children relationship + verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.parentSpanId, results{2}.resourceSpans.scopeSpans.spans.spanId); + + % check error status + verifyEqual(testCase, results{1}.resourceSpans.scopeSpans.spans.status.code, 2); % error + verifyEmpty(testCase, fieldnames(results{2}.resourceSpans.scopeSpans.spans.status)); % ok, no error + end + + function testMultipleInstances(testCase) + % testMultipleInstances: multiple overlapped instances should + % return an error + + % set up AutoTrace + at = opentelemetry.autoinstrument.AutoTrace(@example1); %#ok + + % set up another identical instance, check for error + verifyError(testCase, @()opentelemetry.autoinstrument.AutoTrace(@example1), "opentelemetry:autoinstrument:AutoTrace:OverlappedInstances"); + end + + function testClearInstance(~) + % testClearInstance: clear an instance and recreate a new instance + + % create and instance and then clear + at = opentelemetry.autoinstrument.AutoTrace(@example1); %#ok + clear("at") + + % create a new instance should not result in any error + at = opentelemetry.autoinstrument.AutoTrace(@example1); %#ok + end + + function testInvalidInputFunction(testCase) + % testInvalidInputFunction: negative test for invalid input + + % anonymous function + verifyError(testCase, @()opentelemetry.autoinstrument.AutoTrace(@()example1), "opentelemetry:autoinstrument:AutoTrace:AnonymousFunction"); + + % builtin function + verifyError(testCase, @()opentelemetry.autoinstrument.AutoTrace(@uplus), "opentelemetry:autoinstrument:AutoTrace:InvalidMFile"); end end end \ No newline at end of file From cc03693a3c884e54edd07b9e5ab6df8ac3bc12c2 Mon Sep 17 00:00:00 2001 From: duncanpo Date: Tue, 22 Oct 2024 13:49:07 -0400 Subject: [PATCH 6/7] update expected error ID in tests --- test/autotrace_examples/example1/generate_data.m | 6 ++++++ test/tautotrace.m | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test/autotrace_examples/example1/generate_data.m b/test/autotrace_examples/example1/generate_data.m index 45168c2..82fb09b 100644 --- a/test/autotrace_examples/example1/generate_data.m +++ b/test/autotrace_examples/example1/generate_data.m @@ -3,6 +3,12 @@ % Copyright 2024 The MathWorks, Inc. +% check input is valid +if ~(isnumeric(n) && isscalar(n)) + error("autotrace_examples:example1:generate_data:InvalidN", ... + "Input must be a numeric scalar"); +end + % generate some random data a = 1.5; b = 0.8; diff --git a/test/tautotrace.m b/test/tautotrace.m index e59d3fc..1519ac1 100644 --- a/test/tautotrace.m +++ b/test/tautotrace.m @@ -166,7 +166,7 @@ function testError(testCase) at = opentelemetry.autoinstrument.AutoTrace(@example1); % run the example with an invalid input, check for error - verifyError(testCase, @()beginTrace(at, "invalid"), "MATLAB:colon:inputsMustBeNumericCharLogical"); + verifyError(testCase, @()beginTrace(at, "invalid"), "autotrace_examples:example1:generate_data:InvalidN"); % perform test comparisons results = readJsonResults(testCase); @@ -195,7 +195,7 @@ function testHandleError(testCase) % call example directly instead of calling beginTrace, and pass % in an invalid input - verifyError(testCase, @()example1_trycatch(at, "invalid"), "MATLAB:colon:inputsMustBeNumericCharLogical"); + verifyError(testCase, @()example1_trycatch(at, "invalid"), "autotrace_examples:example1:generate_data:InvalidN"); % perform test comparisons results = readJsonResults(testCase); From e81c79338c7398c8723c71d62c11f09c3c475596 Mon Sep 17 00:00:00 2001 From: duncanpo Date: Wed, 23 Oct 2024 14:35:04 -0400 Subject: [PATCH 7/7] refine error checking in AutoTrace --- .../+opentelemetry/+autoinstrument/AutoTrace.m | 15 +++++++++++---- test/tautotrace.m | 5 ++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m b/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m index da001fe..27b75a3 100644 --- a/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m +++ b/auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m @@ -159,10 +159,17 @@ function processFileInput(f) f = string(f); % force into a string if startsWith(f, '@') % check for anonymous function error("opentelemetry:autoinstrument:AutoTrace:AnonymousFunction", ... - f + " is an anonymous function and is not supported."); + replace(f, "\", "\\") + " is an anonymous function and is not supported."); end -if exist(f, "file") ~= 2 - error("opentelemetry:autoinstrument:AutoTrace:InvalidMFile", ... - f + " is not found or is not a valid MATLAB file with a .m extension.") +[~,~,fext] = fileparts(f); % check file extension +filetype = exist(f, "file"); % check file type +if ~(filetype == 2 && ismember(fext, ["" ".m" ".mlx"])) + if exist(f, "builtin") + error("opentelemetry:autoinstrument:AutoTrace:BuiltinFunction", ... + replace(f, "\", "\\") + " is a builtin function and is not supported."); + else + error("opentelemetry:autoinstrument:AutoTrace:InvalidMFile", ... + replace(f, "\", "\\") + " is not found or is not a valid MATLAB file with a .m or .mlx extension."); + end end end \ No newline at end of file diff --git a/test/tautotrace.m b/test/tautotrace.m index 1519ac1..63f9387 100644 --- a/test/tautotrace.m +++ b/test/tautotrace.m @@ -242,7 +242,10 @@ function testInvalidInputFunction(testCase) verifyError(testCase, @()opentelemetry.autoinstrument.AutoTrace(@()example1), "opentelemetry:autoinstrument:AutoTrace:AnonymousFunction"); % builtin function - verifyError(testCase, @()opentelemetry.autoinstrument.AutoTrace(@uplus), "opentelemetry:autoinstrument:AutoTrace:InvalidMFile"); + verifyError(testCase, @()opentelemetry.autoinstrument.AutoTrace(@uplus), "opentelemetry:autoinstrument:AutoTrace:BuiltinFunction"); + + % nonexistent function + verifyError(testCase, @()opentelemetry.autoinstrument.AutoTrace(@bogus), "opentelemetry:autoinstrument:AutoTrace:InvalidMFile"); end end end \ No newline at end of file