diff --git a/.github/workflows/build_and_test_full.yml b/.github/workflows/build_and_test_full.yml index 74e04c5..973675a 100644 --- a/.github/workflows/build_and_test_full.yml +++ b/.github/workflows/build_and_test_full.yml @@ -5,6 +5,7 @@ on: push: branches: - main + - mbt-plugin env: MLM_LICENSE_TOKEN: ${{ secrets.MLM_LICENSE_TOKEN }} jobs: @@ -40,7 +41,7 @@ jobs: - name: Install MATLAB uses: matlab-actions/setup-matlab@v2 with: - release: R2025a + release: latest-including-prerelease products: MATLAB_Compiler MATLAB_Compiler_SDK - name: Build OpenTelemetry-Matlab working-directory: opentelemetry-matlab diff --git a/CMakeLists.txt b/CMakeLists.txt index abcd2e1..af2e3ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -599,6 +599,7 @@ set(METRICS_SDK_MATLAB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/sdk/metrics/+opentele 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(INSTRUMENTATION_MBT_MATLAB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/instrumentation/buildtool/+matlab) 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 @@ -634,6 +635,7 @@ 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(DIRECTORY ${INSTRUMENTATION_MBT_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/codecov.yml b/codecov.yml index e16f6b0..01fd55b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -11,4 +11,5 @@ fixes: - "\\+opentelemetry/\\+baggage/::api/baggage/+opentelemetry/+baggage/" - "\\+opentelemetry/\\+exporters/\\+otlp/::exporters/otlp/+opentelemetry/+exporters/+otlp/" - "\\+opentelemetry/\\+sdk/\\+logs::sdk/logs/+opentelemetry/+sdk/+logs/" - - "\\+opentelemetry/\\+logs/::api/logs/+opentelemetry/+logs/" \ No newline at end of file + - "\\+opentelemetry/\\+logs/::api/logs/+opentelemetry/+logs/" + - "\\+matlab/\\+buildtool/::instrumentation/buildtool/+matlab/+buildtool" diff --git a/instrumentation/buildtool/+matlab/+buildtool/+internal/+services/+plugins/OpenTelemetryPluginService.m b/instrumentation/buildtool/+matlab/+buildtool/+internal/+services/+plugins/OpenTelemetryPluginService.m new file mode 100644 index 0000000..1a36bce --- /dev/null +++ b/instrumentation/buildtool/+matlab/+buildtool/+internal/+services/+plugins/OpenTelemetryPluginService.m @@ -0,0 +1,15 @@ +classdef OpenTelemetryPluginService < matlab.buildtool.internal.services.plugins.BuildRunnerPluginService + % This class is unsupported and might change or be removed without notice + % in a future version. + + % Copyright 2026 The MathWorks, Inc. + + methods + function plugins = providePlugins(~, ~) + plugins = matlab.buildtool.plugins.BuildRunnerPlugin.empty(1,0); + if ~isMATLABReleaseOlderThan("R2026a") + plugins = matlab.buildtool.plugins.OpenTelemetryPlugin(); + end + end + end +end \ No newline at end of file diff --git a/instrumentation/buildtool/+matlab/+buildtool/+plugins/OpenTelemetryPlugin.m b/instrumentation/buildtool/+matlab/+buildtool/+plugins/OpenTelemetryPlugin.m new file mode 100644 index 0000000..f0a3a85 --- /dev/null +++ b/instrumentation/buildtool/+matlab/+buildtool/+plugins/OpenTelemetryPlugin.m @@ -0,0 +1,190 @@ +classdef OpenTelemetryPlugin < matlab.buildtool.plugins.BuildRunnerPlugin + + % Copyright 2026 The MathWorks, Inc. + + methods(Access = protected) + function runBuild(plugin, pluginData) + % Configure by attaching to span if passed in via environment + % variable, and propagating baggage + configureOTel(); + + tr = opentelemetry.trace.getTracer("buildtool"); + sp = tr.startSpan("buildtool"); + scope = makeCurrent(sp); %#ok + + % Run build + runBuild@matlab.buildtool.plugins.BuildRunnerPlugin(plugin, pluginData); + + % Update status + if pluginData.BuildResult.Failed + sp.setStatus("Error", "Build completed, results not successful"); + else + sp.setStatus("Ok"); + end + + % Results-based attributes + taskResults = pluginData.BuildResult.TaskResults; + successful = [taskResults([taskResults.Successful]).Name]; + failed = [taskResults([taskResults.Failed]).Name]; + skipped = [taskResults([taskResults.Skipped]).Name]; + + sp.setAttributes( ... + "buildtool.tasks", numel(pluginData.BuildResult.TaskResults), ... + "buildtool.tasks.successful", successful, ... + "buildtool.tasks.failed", failed, ... + "buildtool.tasks.skipped", skipped, ... + "buildtool.build.successes", numel(successful), ... + "buildtool.build.failures", numel(failed), ... + "buildtool.build.skips", numel(skipped) ... + ); + + % Update metrics + meter = opentelemetry.metrics.getMeter("buildtool"); + successes = meter.createCounter("buildtool.tasks.successful"); + failures = meter.createCounter("buildtool.tasks.failed"); + skips = meter.createCounter("buildtool.tasks.skipped"); + buildSuccesses = meter.createCounter("buildtool.build.successes"); + buildFailures = meter.createCounter("buildtool.build.failures"); + + successes.add(numel(successful)); + failures.add(numel(failed)); + skips.add(numel(skipped)); + buildSuccesses.add(double(~pluginData.BuildResult.Failed)); + buildFailures.add(double(pluginData.BuildResult.Failed)); + + cleanupOTel(sp); + end + + function runTask(plugin, pluginData) + % TODO: + % - buildtool.task.outputs + % - buildtool.task.inputs + + % Definitions + task = pluginData.TaskGraph.Tasks; + taskName = pluginData.Name; + taskDescription = task.Description; + + % Attributes + otelAttributes = dictionary( ... + [ ... + "buildtool.task.name", ... + "buildtool.task.description", ... + ], ... + [ ... + taskName, ... + taskDescription ... + ] ... + ); + + tr = opentelemetry.trace.getTracer(taskName); + sp = tr.startSpan(taskName, Attributes=otelAttributes); + scope = makeCurrent(sp); %#ok + + % Run task + runTask@matlab.buildtool.plugins.BuildRunnerPlugin(plugin, pluginData); + + % Set results-based attributes + resultAttributes = dictionary( ... + [ ... + "buildtool.task.successful", ... + "buildtool.task.failed", ... + "buildtool.task.skipped" ... + ], ... + [ ... + pluginData.TaskResults.Successful, ... + pluginData.TaskResults.Failed, ... + pluginData.TaskResults.Skipped ... + ] ... + ); + sp.setAttributes(resultAttributes); + + % Update span status + if pluginData.TaskResults.Successful + sp.setStatus("Ok"); + else + sp.setStatus("Error", "Task completed, results not successful"); + end + + sp.endSpan(); + end + end +end + +% Use the same configuration as PADV +function extcontextscope = configureOTel() + +% Skip configuration if NO_MBT_OTEL_CONFIG set +if (getenv("NO_MBT_OTEL_CONFIG")) + return; +end + +% populate resource attributes +otelservicename = "buildtool"; +otelresource = dictionary("service.name", otelservicename); + +% baggage propagation +otelbaggage = getenv("BAGGAGE"); +if ~isempty(otelbaggage) + otelbaggage = split(split(string(otelbaggage),','), "="); + otelresource = insert(otelresource, otelbaggage(:,1), otelbaggage(:,2)); +end + +% check for passed in external context +extcontextscope = []; +traceid = getenv("TRACE_ID"); +spanid = getenv("SPAN_ID"); +if ~isempty(traceid) && ~isempty(spanid) + spcontext = opentelemetry.trace.SpanContext(traceid, spanid); + extcontextscope = makeCurrent(spcontext); +end + +% tracer provider +otelspexp = opentelemetry.exporters.otlp.OtlpGrpcSpanExporter; % use gRPC because Otel plugin for Jenkins only use gRPC +otelspproc = opentelemetry.sdk.trace.BatchSpanProcessor(otelspexp); +oteltp = opentelemetry.sdk.trace.TracerProvider(otelspproc, Resource=otelresource); +setTracerProvider(oteltp); + +% meter provider +otelmexp = opentelemetry.exporters.otlp.OtlpGrpcMetricExporter; % use gRPC because Otel plugin for Jenkins only use gRPC +otelmread = opentelemetry.sdk.metrics.PeriodicExportingMetricReader(otelmexp); +otelmp = opentelemetry.sdk.metrics.MeterProvider(otelmread, Resource=otelresource); +setMeterProvider(otelmp); + +% logger provider +otellgexp = opentelemetry.exporters.otlp.OtlpGrpcLogRecordExporter; % use gRPC because Otel plugin for Jenkins only use gRPC +otellgproc = opentelemetry.sdk.logs.BatchLogRecordProcessor(otellgexp); +otellp = opentelemetry.sdk.logs.LoggerProvider(otellgproc, Resource=otelresource); +setLoggerProvider(otellp); +end + +% Use the same cleanup as PADV +function cleanupOTel(span) + +% Skip cleanup if NO_MBT_OTEL_CONFIG set +if (getenv("NO_MBT_OTEL_CONFIG")) + return; +end + +timeout = 5; + +% end the input span before cleaning up +if nargin > 0 + endSpan(span); +end + +% tracer provider +oteltp = opentelemetry.trace.Provider.getTracerProvider; +opentelemetry.sdk.common.Cleanup.forceFlush(oteltp, timeout); +opentelemetry.sdk.common.Cleanup.shutdown(oteltp); + +% meter provider +otelmp = opentelemetry.metrics.Provider.getMeterProvider; +opentelemetry.sdk.common.Cleanup.forceFlush(otelmp, timeout); +opentelemetry.sdk.common.Cleanup.shutdown(otelmp); + +% logger provider +otellp = opentelemetry.logs.Provider.getLoggerProvider; +opentelemetry.sdk.common.Cleanup.forceFlush(otellp, timeout); +opentelemetry.sdk.common.Cleanup.shutdown(otellp); +end \ No newline at end of file diff --git a/test/tbuildtoolplugin.m b/test/tbuildtoolplugin.m new file mode 100644 index 0000000..8f538fc --- /dev/null +++ b/test/tbuildtoolplugin.m @@ -0,0 +1,477 @@ +classdef tbuildtoolplugin < matlab.unittest.TestCase + properties + OtelConfigFile + JsonFile + PidFile + ListPid + ReadPidList + ExtractPid + Sigint + Sigterm + OtelcolName + Otelcol + + BuildRunner + end + + methods (TestClassSetup) + function setupOnce(testCase) + % add the utils, callbacks, and fixtures folders to the path + folders = fullfile(fileparts(mfilename('fullpath')), ["utils" "callbacks" "fixtures"]); + testCase.applyFixture(matlab.unittest.fixtures.PathFixture(folders)); + commonSetupOnce(testCase); + end + end + + methods (TestMethodSetup) + function setup(testCase) + commonSetup(testCase); + end + + function onlyTestIfgRPCIsInstalled(testCase) + testCase.assumeTrue(logical(exist("opentelemetry.exporters.otlp.OtlpGrpcSpanExporter", "class")), ... + "Otlp gRPC exporter must be installed."); + end + + function createBuildRunner(testCase) + plugin = matlab.buildtool.plugins.OpenTelemetryPlugin(); + testCase.BuildRunner = matlab.buildtool.BuildRunner.withNoPlugins(); + testCase.BuildRunner.addPlugin(plugin); + end + end + + methods (TestMethodTeardown) + function teardown(testCase) + commonTeardown(testCase); + end + end + + methods (Test) + function pluginIsAddedAsADefaultPluginInNewReleases(testCase) + testCase.assumeFalse(isMATLABReleaseOlderThan("R2026a")); + + runner = matlab.buildtool.BuildRunner.withDefaultPlugins(); + tf = arrayfun(@(x)isa(x, "matlab.buildtool.plugins.OpenTelemetryPlugin"), runner.Plugins); + testCase.verifyTrue(any(tf)); + end + + function pluginIsNotAddedAsADefaultPluginInOlderReleases(testCase) + testCase.assumeTrue(isMATLABReleaseOlderThan("R2026a")); + + runner = matlab.buildtool.BuildRunner.withDefaultPlugins(); + tf = arrayfun(@(x)isa(x, "matlab.buildtool.plugins.OpenTelemetryPlugin"), runner.Plugins); + testCase.verifyFalse(any(tf)); + end + + function runOneTaskHasCorrectSpans(testCase) + testCase.assumeFalse(isMATLABReleaseOlderThan("R2026a")); + + % Create plan with 1 successful task + plan = buildplan(); + plan("task") = matlab.buildtool.Task(); + + % Run build + testCase.BuildRunner.run(plan, "task"); + + % Get results + results = readJsonResults(testCase); + spanData = results{1}.resourceSpans; + + % Verify spans + spans = spanData.scopeSpans; + testCase.verifyEqual(numel(spans), 2); + + % First span is overall build span + buildSpan = spanData.scopeSpans(1).spans; + testCase.verifyEqual(string(buildSpan.name), "buildtool"); + testCase.verifyEqual(buildSpan.status.code, 1); + + % Build span attributes + att1.key = 'buildtool.tasks'; + att1.value.doubleValue = 1; + + att2.key = 'buildtool.tasks.successful'; + att2.value.stringValue = 'task'; + + att3.key = 'buildtool.tasks.failed'; + att3.value.arrayValue = struct(); + + sizeZero.doubleValue = 0; + att4.key = 'buildtool.tasks.failed.size'; + att4.value.arrayValue.values = [sizeZero; sizeZero]; + + att5.key = 'buildtool.tasks.skipped'; + att5.value.arrayValue = struct(); + + att6.key = 'buildtool.tasks.skipped.size'; + att6.value.arrayValue.values = [sizeZero; sizeZero]; + + att7.key = 'buildtool.build.successes'; + att7.value.doubleValue = 1; + + att8.key = 'buildtool.build.failures'; + att8.value.doubleValue = 0; + + att9.key = 'buildtool.build.skips'; + att9.value.doubleValue = 0; + + expected = [ ... + att1, ... + att2, ... + att3, ... + att4, ... + att5, ... + att6, ... + att7, ... + att8, ... + att9, ... + ]'; + + testCase.verifyEqual(buildSpan.attributes, expected); + + % Second span is "task" span + taskSpan = spanData.scopeSpans(2).spans; + + testCase.verifyEqual(string(taskSpan.name), "task"); + testCase.verifyEqual(taskSpan.status.code, 1); + testCase.verifyEqual(taskSpan.parentSpanId, buildSpan.spanId); + + % Task span attributes + tAtt1.key = 'buildtool.task.name'; + tAtt1.value.stringValue = 'task'; + + tAtt2.key = 'buildtool.task.description'; + tAtt2.value.stringValue = ''; + + tAtt3.key = 'buildtool.task.successful'; + tAtt3.value.boolValue = true; + + tAtt4.key = 'buildtool.task.failed'; + tAtt4.value.boolValue = false; + + tAtt5.key = 'buildtool.task.skipped'; + tAtt5.value.boolValue = false; + + expected = [ ... + tAtt1, ... + tAtt2, ... + tAtt3, ... + tAtt4, ... + tAtt5, ... + ]'; + + testCase.verifyEqual(taskSpan.attributes, expected); + end + + function runOneTaskHasCorrectMetrics(testCase) + testCase.assumeFalse(isMATLABReleaseOlderThan("R2026a")); + + % Create plan with 1 successful task + plan = buildplan(); + plan("task") = matlab.buildtool.Task(); + + % Run build + testCase.BuildRunner.run(plan, "task"); + + % Get results + results = readJsonResults(testCase); + metricData = results{2}.resourceMetrics; + + metrics = metricData.scopeMetrics.metrics; + + % Order appears to be deterministic on my machine. + testCase.verifyEqual(metrics(1).name, 'buildtool.build.failures'); + testCase.verifyEqual(metrics(1).sum.dataPoints.asDouble, 0); + + testCase.verifyEqual(metrics(2).name, 'buildtool.build.successes'); + testCase.verifyEqual(metrics(2).sum.dataPoints.asDouble, 1); + + testCase.verifyEqual(metrics(3).name, 'buildtool.tasks.skipped'); + testCase.verifyEqual(metrics(3).sum.dataPoints.asDouble, 0); + + testCase.verifyEqual(metrics(4).name, 'buildtool.tasks.failed'); + testCase.verifyEqual(metrics(4).sum.dataPoints.asDouble, 0); + + testCase.verifyEqual(metrics(5).name, 'buildtool.tasks.successful'); + testCase.verifyEqual(metrics(5).sum.dataPoints.asDouble, 1); + end + + function runningSeveralTasksProducesCorrectSpans(testCase) + testCase.assumeFalse(isMATLABReleaseOlderThan("R2026a")); + + % Create plan with 3 successful tasks and a failing task + plan = buildplan(); + plan("t1") = matlab.buildtool.Task(); + plan("t2") = matlab.buildtool.Task(Dependencies="t1"); + plan("t3") = matlab.buildtool.Task(Dependencies="t2"); + plan("error") = matlab.buildtool.Task(Actions=@(~)error("bam"), Dependencies="t3"); + + % Run build + testCase.BuildRunner.run(plan, "error"); + + % Get results + results = readJsonResults(testCase); + spanData = results{1}.resourceSpans; + + % Verify spans + spans = spanData.scopeSpans; + testCase.verifyEqual(numel(spans), 5); + + % First span is overall build span + buildSpan = findSpan("buildtool", spans); + testCase.verifyEqual(string(buildSpan.name), "buildtool"); + testCase.verifyEqual(buildSpan.status.code, 2); % Build should fail + + % Build span attributes + att1.key = 'buildtool.tasks'; + att1.value.doubleValue = 4; + + val1.stringValue = 't1'; + val2.stringValue = 't2'; + val3.stringValue = 't3'; + att2.key = 'buildtool.tasks.successful'; + att2.value.arrayValue.values = [val1; val2; val3]; + + size1.doubleValue = 1; + size3.doubleValue = 3; + att3.key = 'buildtool.tasks.successful.size'; + att3.value.arrayValue.values = [size1; size3]; + + att4.key = 'buildtool.tasks.failed'; + att4.value.stringValue = 'error'; + + att5.key = 'buildtool.tasks.skipped'; + att5.value.arrayValue = struct(); + + sizeZero.doubleValue = 0; + att6.key = 'buildtool.tasks.skipped.size'; + att6.value.arrayValue.values = [sizeZero; sizeZero]; + + att7.key = 'buildtool.build.successes'; + att7.value.doubleValue = 3; + + att8.key = 'buildtool.build.failures'; + att8.value.doubleValue = 1; + + att9.key = 'buildtool.build.skips'; + att9.value.doubleValue = 0; + + expected = [ ... + att1, ... + att2, ... + att3, ... + att4, ... + att5, ... + att6, ... + att7, ... + att8, ... + att9, ... + ]'; + + testCase.verifyEqual(buildSpan.attributes, expected); + + % "t1" span + taskSpan = findSpan("t1", spans); + + testCase.verifyEqual(string(taskSpan.name), "t1"); + testCase.verifyEqual(taskSpan.status.code, 1); + testCase.verifyEqual(taskSpan.parentSpanId, buildSpan.spanId); + + % Task span attributes + tAtt1.key = 'buildtool.task.name'; + tAtt1.value.stringValue = 't1'; + + tAtt2.key = 'buildtool.task.description'; + tAtt2.value.stringValue = ''; + + tAtt3.key = 'buildtool.task.successful'; + tAtt3.value.boolValue = true; + + tAtt4.key = 'buildtool.task.failed'; + tAtt4.value.boolValue = false; + + tAtt5.key = 'buildtool.task.skipped'; + tAtt5.value.boolValue = false; + + expected = [ ... + tAtt1, ... + tAtt2, ... + tAtt3, ... + tAtt4, ... + tAtt5, ... + ]'; + + testCase.verifyEqual(taskSpan.attributes, expected); + + % "t2" span + taskSpan = findSpan("t2", spans); + + testCase.verifyEqual(string(taskSpan.name), "t2"); + testCase.verifyEqual(taskSpan.status.code, 1); + testCase.verifyEqual(taskSpan.parentSpanId, buildSpan.spanId); + + % Task span attributes + tAtt1.key = 'buildtool.task.name'; + tAtt1.value.stringValue = 't2'; + + tAtt2.key = 'buildtool.task.description'; + tAtt2.value.stringValue = ''; + + tAtt3.key = 'buildtool.task.successful'; + tAtt3.value.boolValue = true; + + tAtt4.key = 'buildtool.task.failed'; + tAtt4.value.boolValue = false; + + tAtt5.key = 'buildtool.task.skipped'; + tAtt5.value.boolValue = false; + + expected = [ ... + tAtt1, ... + tAtt2, ... + tAtt3, ... + tAtt4, ... + tAtt5, ... + ]'; + + testCase.verifyEqual(taskSpan.attributes, expected); + + % "t3" span + taskSpan = findSpan("t3", spans); + + testCase.verifyEqual(string(taskSpan.name), "t3"); + testCase.verifyEqual(taskSpan.status.code, 1); + testCase.verifyEqual(taskSpan.parentSpanId, buildSpan.spanId); + + % Task span attributes + tAtt1.key = 'buildtool.task.name'; + tAtt1.value.stringValue = 't3'; + + tAtt2.key = 'buildtool.task.description'; + tAtt2.value.stringValue = ''; + + tAtt3.key = 'buildtool.task.successful'; + tAtt3.value.boolValue = true; + + tAtt4.key = 'buildtool.task.failed'; + tAtt4.value.boolValue = false; + + tAtt5.key = 'buildtool.task.skipped'; + tAtt5.value.boolValue = false; + + expected = [ ... + tAtt1, ... + tAtt2, ... + tAtt3, ... + tAtt4, ... + tAtt5, ... + ]'; + + testCase.verifyEqual(taskSpan.attributes, expected); + + % "error" span + taskSpan = findSpan("error", spans); + + testCase.verifyEqual(string(taskSpan.name), "error"); + testCase.verifyEqual(taskSpan.status.code, 2); % Should error + testCase.verifyEqual(taskSpan.parentSpanId, buildSpan.spanId); + + % Task span attributes + tAtt1.key = 'buildtool.task.name'; + tAtt1.value.stringValue = 'error'; + + tAtt2.key = 'buildtool.task.description'; + tAtt2.value.stringValue = ''; + + tAtt3.key = 'buildtool.task.successful'; + tAtt3.value.boolValue = false; + + tAtt4.key = 'buildtool.task.failed'; + tAtt4.value.boolValue = true; + + tAtt5.key = 'buildtool.task.skipped'; + tAtt5.value.boolValue = false; + + expected = [ ... + tAtt1, ... + tAtt2, ... + tAtt3, ... + tAtt4, ... + tAtt5, ... + ]'; + + testCase.verifyEqual(taskSpan.attributes, expected); + end + + function runningSeveralTasksProducesCorrectMetrics(testCase) + testCase.assumeFalse(isMATLABReleaseOlderThan("R2026a")); + + % Create plan with 3 successful tasks and a failing task + plan = buildplan(); + plan("t1") = matlab.buildtool.Task(); + plan("t2") = matlab.buildtool.Task(Dependencies="t1"); + plan("t3") = matlab.buildtool.Task(Dependencies="t2"); + plan("error") = matlab.buildtool.Task(Actions=@(~)error("bam"), Dependencies="t3"); + + % Run build + testCase.BuildRunner.run(plan, "error"); + + % Get results + results = readJsonResults(testCase); + metricData = results{2}.resourceMetrics; + + metrics = metricData.scopeMetrics.metrics; + + % Order appears to be deterministic on my machine. + testCase.verifyEqual(metrics(1).name, 'buildtool.build.failures'); + testCase.verifyEqual(metrics(1).sum.dataPoints.asDouble, 1); + + testCase.verifyEqual(metrics(2).name, 'buildtool.build.successes'); + testCase.verifyEqual(metrics(2).sum.dataPoints.asDouble, 0); + + testCase.verifyEqual(metrics(3).name, 'buildtool.tasks.skipped'); + testCase.verifyEqual(metrics(3).sum.dataPoints.asDouble, 0); + + testCase.verifyEqual(metrics(4).name, 'buildtool.tasks.failed'); + testCase.verifyEqual(metrics(4).sum.dataPoints.asDouble, 1); + + testCase.verifyEqual(metrics(5).name, 'buildtool.tasks.successful'); + testCase.verifyEqual(metrics(5).sum.dataPoints.asDouble, 3); + end + + function buildToolDoesNotConfigureOTelWhenEnvVariableSet(testCase) + testCase.assumeFalse(isMATLABReleaseOlderThan("R2026a")); + + % Restore environment variable + testCase.addTeardown(@()setenv("NO_MBT_OTEL_CONFIG", "")); + + % Set environment variable + setenv("NO_MBT_OTEL_CONFIG", "1"); + + % Create plan with 1 successful task + plan = buildplan(); + plan("task") = matlab.buildtool.Task(); + + % Run build + testCase.BuildRunner.run(plan, "task"); + + % Get results + results = readJsonResults(testCase); + + % Verify results are empty since we never set up any providers + testCase.verifyEmpty(results); + end + end +end + +function span = findSpan(name, spans) + for s = spans' + realSpan = s.spans; + if (strcmp(name, realSpan.name)) + span = realSpan; + return; + end + end + + error("No span found"); +end \ No newline at end of file