Skip to content
Open
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
3 changes: 2 additions & 1 deletion .github/workflows/build_and_test_full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
push:
branches:
- main
- mbt-plugin
env:
MLM_LICENSE_TOKEN: ${{ secrets.MLM_LICENSE_TOKEN }}
jobs:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})
Expand Down
3 changes: 2 additions & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
- "\\+opentelemetry/\\+logs/::api/logs/+opentelemetry/+logs/"
- "\\+matlab/\\+buildtool/::instrumentation/buildtool/+matlab/+buildtool"
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this will initialize/configure OTel at the start of each build and then clean up everything after. Process Advisor only initializes before the first build of a MATLAB session. If you run multiple builds within a MATLAB session, OTel would only be initialized once. Do you want to do something like that, which is more efficient?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's an interesting question. Only setting up once is nice for improving performance, but if I understand correctly it allows for mutation of the environment in between runs right?

In order to ensure maximum reproducibility and determinism, having a consistent execution path through builds is quite powerful.

Overall, I think unless the performance costs prove to be expensive, I like reconfiguring each build to try and make the build process more hermetic. What are your thoughts there @duncanpo? Are there downsides to reconfiguration outside of the performance costs?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is reasonable to configure/clean up every build if performance cost doesn't prove prohibitive. But I would recommend providing a way to skip config/clean up (such as using an environment variable). That way if a customer already sets up OTel for something else, there would be at least a way to opt out of build tool overriding their config

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense!


tr = opentelemetry.trace.getTracer("buildtool");
sp = tr.startSpan("buildtool");
scope = makeCurrent(sp); %#ok<NASGU>

% Run build
[email protected](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<NASGU>

% Run task
[email protected](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),','), "=");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you took this code from Process Advisor. But as I am reading it, I realize it doesn't work if there is only one parameter in the baggage. Please add a reshape and it will work in all cases:
otelbaggage = reshape(split(split(string(otelbaggage),','), "="), [], 2);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I'll update that.

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
Loading
Loading