Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autotrace #161

Merged
merged 7 commits into from
Oct 23, 2024
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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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})
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ otelcol --config <otelcol-config-yaml>

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 \<function_name>\". For example,
```
Expand Down
175 changes: 175 additions & 0 deletions auto-instrumentation/+opentelemetry/+autoinstrument/AutoTrace.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
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.
%
% 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:
% "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("opentelemetry:autoinstrument:AutoTrace:AnonymousFunction", ...
replace(f, "\", "\\") + " is an anonymous function and is not supported.");
end
[~,~,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
Binary file not shown.
35 changes: 35 additions & 0 deletions auto-instrumentation/README.md
Original file line number Diff line number Diff line change
@@ -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`.
7 changes: 7 additions & 0 deletions test/autotrace_examples/example1/best_fit_line.m
Original file line number Diff line number Diff line change
@@ -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);
8 changes: 8 additions & 0 deletions test/autotrace_examples/example1/example1.m
Original file line number Diff line number Diff line change
@@ -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);
12 changes: 12 additions & 0 deletions test/autotrace_examples/example1/example1_trycatch.m
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions test/autotrace_examples/example1/generate_data.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function [x, y] = generate_data(n)
% example code for testing auto instrumentation

% 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;
sigma = 5;
x = 1:n;
y = a * x + b + sigma * randn(1, n);
Loading
Loading