From 14e3f7a5b13ef75b6ca5e04634d62a0ab1d55a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Joaqu=C3=ADn=20Atria?= Date: Thu, 16 Nov 2023 18:55:22 +0000 Subject: [PATCH] Add experimental support for logs --- Changes | 1 + META.json | 88 +++++++---- lib/Log/Any/Adapter/OpenTelemetry.pm | 106 +++++++++++++ lib/OpenTelemetry.pm | 34 ++++ lib/OpenTelemetry.pod | 28 ++-- lib/OpenTelemetry/Constants.pm | 116 +++++++++++--- lib/OpenTelemetry/Constants.pod | 145 +++++++++++++++++- lib/OpenTelemetry/Exporter.pod | 36 +++-- lib/OpenTelemetry/Logs/LogRecord/Processor.pm | 10 ++ .../Logs/LogRecord/Processor.pod | 110 +++++++++++++ lib/OpenTelemetry/Logs/Logger.pm | 16 ++ lib/OpenTelemetry/Logs/Logger.pod | 101 ++++++++++++ lib/OpenTelemetry/Logs/LoggerProvider.pm | 16 ++ lib/OpenTelemetry/Logs/LoggerProvider.pod | 98 ++++++++++++ lib/OpenTelemetry/Processor.pm | 22 +++ lib/OpenTelemetry/Processor.pod | 76 +++++++++ lib/OpenTelemetry/Propagator.pod | 4 +- lib/OpenTelemetry/Trace/Span/Processor.pm | 5 +- lib/OpenTelemetry/Trace/Tracer.pod | 6 +- t/OpenTelemetry/Constants.t | 58 +++++++ 20 files changed, 980 insertions(+), 96 deletions(-) create mode 100644 lib/Log/Any/Adapter/OpenTelemetry.pm create mode 100644 lib/OpenTelemetry/Logs/LogRecord/Processor.pm create mode 100644 lib/OpenTelemetry/Logs/LogRecord/Processor.pod create mode 100644 lib/OpenTelemetry/Logs/Logger.pm create mode 100644 lib/OpenTelemetry/Logs/Logger.pod create mode 100644 lib/OpenTelemetry/Logs/LoggerProvider.pm create mode 100644 lib/OpenTelemetry/Logs/LoggerProvider.pod create mode 100644 lib/OpenTelemetry/Processor.pm create mode 100644 lib/OpenTelemetry/Processor.pod diff --git a/Changes b/Changes index 5f7ea21..8ebc8c8 100644 --- a/Changes +++ b/Changes @@ -5,6 +5,7 @@ Revision history for OpenTelemetry * Updated the documentation to remove remaining mentions of the span's 'add_link' method, which was removed to comply with the specification + * Add experimental support for logs 0.023 2024-06-03 10:31:57+01:00 Europe/London diff --git a/META.json b/META.json index 0ee281c..32415ca 100644 --- a/META.json +++ b/META.json @@ -79,133 +79,153 @@ } }, "provides" : { + "Log::Any::Adapter::OpenTelemetry" : { + "file" : "lib/Log/Any/Adapter/OpenTelemetry.pm", + "version" : "0.024" + }, "OpenTelemetry" : { "file" : "lib/OpenTelemetry.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Attributes" : { "file" : "lib/OpenTelemetry/Attributes.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Baggage" : { "file" : "lib/OpenTelemetry/Baggage.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Constants" : { "file" : "lib/OpenTelemetry/Constants.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Context" : { "file" : "lib/OpenTelemetry/Context.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Exporter" : { "file" : "lib/OpenTelemetry/Exporter.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Integration" : { "file" : "lib/OpenTelemetry/Integration.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Integration::DBI" : { "file" : "lib/OpenTelemetry/Integration/DBI.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Integration::HTTP::Tiny" : { "file" : "lib/OpenTelemetry/Integration/HTTP/Tiny.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Integration::LWP::UserAgent" : { "file" : "lib/OpenTelemetry/Integration/LWP/UserAgent.pm", - "version" : "0.023" + "version" : "0.024" + }, + "OpenTelemetry::Logs::LogRecord::Processor" : { + "file" : "lib/OpenTelemetry/Logs/LogRecord/Processor.pm", + "version" : "0.024" + }, + "OpenTelemetry::Logs::Logger" : { + "file" : "lib/OpenTelemetry/Logs/Logger.pm", + "version" : "0.024" + }, + "OpenTelemetry::Logs::LoggerProvider" : { + "file" : "lib/OpenTelemetry/Logs/LoggerProvider.pm", + "version" : "0.024" + }, + "OpenTelemetry::Processor" : { + "file" : "lib/OpenTelemetry/Processor.pm", + "version" : "0.024" }, "OpenTelemetry::Propagator" : { "file" : "lib/OpenTelemetry/Propagator.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Propagator::Baggage" : { "file" : "lib/OpenTelemetry/Propagator/Baggage.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Propagator::Composite" : { "file" : "lib/OpenTelemetry/Propagator/Composite.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Propagator::None" : { "file" : "lib/OpenTelemetry/Propagator/None.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Propagator::TextMap" : { "file" : "lib/OpenTelemetry/Propagator/TextMap.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Propagator::TraceContext" : { "file" : "lib/OpenTelemetry/Propagator/TraceContext.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Propagator::TraceContext::TraceFlags" : { "file" : "lib/OpenTelemetry/Propagator/TraceContext/TraceFlags.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Propagator::TraceContext::TraceParent" : { "file" : "lib/OpenTelemetry/Propagator/TraceContext/TraceParent.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Propagator::TraceContext::TraceState" : { "file" : "lib/OpenTelemetry/Propagator/TraceContext/TraceState.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Trace" : { "file" : "lib/OpenTelemetry/Trace.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Trace::Event" : { "file" : "lib/OpenTelemetry/Trace/Event.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Trace::Link" : { "file" : "lib/OpenTelemetry/Trace/Link.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Trace::Span" : { "file" : "lib/OpenTelemetry/Trace/Span.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Trace::Span::Processor" : { "file" : "lib/OpenTelemetry/Trace/Span/Processor.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Trace::Span::Status" : { "file" : "lib/OpenTelemetry/Trace/Span/Status.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Trace::SpanContext" : { "file" : "lib/OpenTelemetry/Trace/SpanContext.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Trace::Tracer" : { "file" : "lib/OpenTelemetry/Trace/Tracer.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::Trace::TracerProvider" : { "file" : "lib/OpenTelemetry/Trace/TracerProvider.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::X" : { "file" : "lib/OpenTelemetry/X.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::X::Invalid" : { "file" : "lib/OpenTelemetry/X/Invalid.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::X::Parsing" : { "file" : "lib/OpenTelemetry/X/Parsing.pm", - "version" : "0.023" + "version" : "0.024" }, "OpenTelemetry::X::Unsupported" : { "file" : "lib/OpenTelemetry/X/Unsupported.pm", - "version" : "0.023" + "version" : "0.024" } }, "release_status" : "stable", @@ -219,7 +239,7 @@ "web" : "https://github.com/jjatria/perl-opentelemetry" } }, - "version" : "0.023", + "version" : "0.024", "x_Dist_Zilla" : { "perl" : { "version" : "5.038002" @@ -450,7 +470,7 @@ "branch" : null, "changelog" : "Changes", "signed" : 0, - "tag" : "0.023", + "tag" : "0.024", "tag_format" : "%v", "tag_message" : "" }, diff --git a/lib/Log/Any/Adapter/OpenTelemetry.pm b/lib/Log/Any/Adapter/OpenTelemetry.pm new file mode 100644 index 0000000..5439c84 --- /dev/null +++ b/lib/Log/Any/Adapter/OpenTelemetry.pm @@ -0,0 +1,106 @@ +package Log::Any::Adapter::OpenTelemetry; + +use strict; +use warnings; +use experimental 'signatures'; + +our $VERSION = '0.024'; + +use Log::Any::Adapter::Util (); +use OpenTelemetry qw( otel_config otel_span_from_context otel_logger_provider ); +use Ref::Util 'is_hashref'; +use Time::HiRes 'time'; + +use OpenTelemetry::Constants qw( + LOG_LEVEL_TRACE + LOG_LEVEL_DEBUG + LOG_LEVEL_INFO + LOG_LEVEL_WARN + LOG_LEVEL_ERROR + LOG_LEVEL_FATAL +); + +use base 'Log::Any::Adapter::Base'; + +my %LOG2OTEL = ( + trace => LOG_LEVEL_TRACE, + debug => LOG_LEVEL_DEBUG, + info => LOG_LEVEL_INFO, + warn => LOG_LEVEL_WARN, + error => LOG_LEVEL_ERROR, + fatal => LOG_LEVEL_FATAL, +); + +my %OTEL2LOG = ( + trace => 8, + debug => 7, + info => 6, + warn => 4, + error => 3, + fatal => 2, +); + +sub init ( $self, @ ) { + # FIXME: It would be good to get a logger early and cache + # it for eventual calls. However, this suffers from the same + # issue with caching tracers that is documented in the POD + # for OpenTelemetry::Trace::Tracer: namely, that if we get + # the no-op logger before we've set up a real logger provider + # that can generate real loggers, we'll be stuck with a no-op. + # It might be that we need to revisit the proxy classes removed + # in d9e321bd1bf65d510b12ef34fe2b5a0c51da0bf2, although the + # rationale for why they were removed is still sound. We'd just + # have to come up with a way to make sure its delegate continues + # to point to the right place even if the tracer provider changes + # $self->{logger} = otel_logger_provider->logger; +} + +for my $method ( Log::Any::Adapter::Util::logging_methods() ) { + no strict 'refs'; + *$method = sub ( $self, @args) { + $self->structured( $method, $self->category, @args ); + }; +} + +for my $method ( Log::Any::Adapter::Util::detection_methods() ) { + my $numeric = Log::Any::Adapter::Util::numeric_level( $method =~ s/^is_//r ); + + no strict 'refs'; + *$method = sub { + $numeric <= $OTEL2LOG{ lc( otel_config('LOG_LEVEL') // 'info' ) }; + }; +} + +sub structured ( $self, $method, $category, @parts ) { + my $level = $method; + for ($level) { + s/(?:emergency|alert|critical)/fatal/; + s/notice/info/; + s/warning/warn/; + } + + # FIXME: This is a little finicky. The aim is for the first + # argument to be the body (even if it is structured), and + # anything else gets put into the attributes. If the log + # comes with structured data that is not a hash, we put it + # under a `payload` key. Maybe this can be simplified to + # always put the data under a given key, but then we add + # data to the arguably common operation of attaching a hash. + my %args = ( body => shift @parts ); + + $args{attributes} = @parts == 1 + ? is_hashref $parts[0] + ? $parts[0] : { payload => $parts[0] } + : @parts % 2 + ? { payload => \@parts } : { @parts } + if @parts; + + otel_logger_provider->logger->emit_record( + timestamp => time, + severity_text => $method, + severity_number => 0+$LOG2OTEL{$level}, + %args, + ); +} + +1; diff --git a/lib/OpenTelemetry.pm b/lib/OpenTelemetry.pm index f09885b..d5a0e6d 100644 --- a/lib/OpenTelemetry.pm +++ b/lib/OpenTelemetry.pm @@ -12,6 +12,7 @@ use OpenTelemetry::Common; use OpenTelemetry::Context; use OpenTelemetry::Propagator::None; use OpenTelemetry::Trace::TracerProvider; +use OpenTelemetry::Logs::LoggerProvider; use OpenTelemetry::X; use Scalar::Util 'refaddr'; use Ref::Util 'is_coderef'; @@ -26,6 +27,7 @@ use Exporter::Shiny qw( otel_error_handler otel_handle_error otel_logger + otel_logger_provider otel_propagator otel_span_from_context otel_tracer_provider @@ -55,6 +57,38 @@ sub _generate_otel_logger { \&logger } sub tracer_provider :lvalue { sentinel get => sub { $instance }, set => $set } } +# FIXME: The functions in this block mean that in all likelihood +# OpenTelemetry->logger will have to go away. It no longer fits, +# since we gain the concept of a logger, which is different from +# the logger we used to return (which is just a Log::Any logger). +# This is not such a big problem: it just means that users who +# want to use the same logger as the rest of the OpenTelemetry +# implementation need to do +# +# my $log = Log::Any->get_logger( category => 'OpenTelemetry' ); +# +# but you can argue that that's not so onerous, and if it is, that +# logging on that given category rather than the default one is +# pointless. +{ + my $lock = Mutex->new; + my $instance = OpenTelemetry::Logs::LoggerProvider->new; + + my $set = sub ( $new ) { + die OpenTelemetry::X->create( + Invalid => 'Global logger provider must be a subclass of OpenTelemetry::Logs::LoggerProvider, got instead ' . ( ref $new || 'a plain scalar' ), + ) unless $new isa OpenTelemetry::Logs::LoggerProvider; + + $lock->enter( sub { $instance = $new }); + }; + + sub _generate_otel_logger_provider { + my $x = sub :lvalue { sentinel get => sub { $instance }, set => $set }; + } + + sub logger_provider :lvalue { sentinel get => sub { $instance }, set => $set } +} + { my $lock = Mutex->new; my $instance = OpenTelemetry::Propagator::None->new; diff --git a/lib/OpenTelemetry.pod b/lib/OpenTelemetry.pod index 0324dc6..25034b6 100644 --- a/lib/OpenTelemetry.pod +++ b/lib/OpenTelemetry.pod @@ -55,15 +55,19 @@ consider using L, or other similar methods. =head1 CLASS METHODS -=head2 logger +=head2 logger_provider - $logger = OpenTelemetry->logger; + $logger_provider = OpenTelemetry->logger_provider; + OpenTelemetry->logger_provider = $new_logger_provider; -I: This method will be removed in an upcoming release. +Get the global logger provider. This function can be used as an lvalue to set +a new logger provider, but if so the new value must be a subclass of +L. Trying to set it to a class that does +not inherit from that class is an error. -Get a L logger that is configured to log in the "OpenTelemetry" -category. There is no reason to use this instead of obtaining your own logger -via the standard L interface. +If the logger provider is read before any is set, it will default to a +L, which returns a logger that does +nothing. Please refer to the documentation of that class for more details. =head2 tracer_provider @@ -119,8 +123,8 @@ An optional hash reference with additional contextual information. =back -The default error handler simply concatenates these and logs them using the -default L. If present, the details are passed as structured data to +The default error handler simply concatenates these and logs them using +L. If present, the details are passed as structured data to the logger. =head2 handle_error @@ -201,11 +205,13 @@ used as an lvalue. Behaves exactly as the L class method described above. -=head2 otel_logger +=head2 otel_logger_provider - $logger = otel_logger; + $logger_provider = otel_logger_provider; + otel_logger_provider = $new_logger_provider; -Behaves exactly as the L class method described above. +Behaves exactly as the L class method described above. Can +be used as an lvalue. =head2 otel_propagator diff --git a/lib/OpenTelemetry/Constants.pm b/lib/OpenTelemetry/Constants.pm index 885eaae..7625484 100644 --- a/lib/OpenTelemetry/Constants.pm +++ b/lib/OpenTelemetry/Constants.pm @@ -2,35 +2,101 @@ package OpenTelemetry::Constants; our $VERSION = '0.024'; +use Scalar::Util (); + use constant { - SPAN_STATUS_UNSET => 0, - SPAN_STATUS_OK => 1, - SPAN_STATUS_ERROR => 2, + SPAN_STATUS_UNSET => 0, + SPAN_STATUS_OK => 1, + SPAN_STATUS_ERROR => 2, + + SPAN_KIND_INTERNAL => 1, + SPAN_KIND_SERVER => 2, + SPAN_KIND_CLIENT => 3, + SPAN_KIND_PRODUCER => 4, + SPAN_KIND_CONSUMER => 5, - SPAN_KIND_INTERNAL => 1, - SPAN_KIND_SERVER => 2, - SPAN_KIND_CLIENT => 3, - SPAN_KIND_PRODUCER => 4, - SPAN_KIND_CONSUMER => 5, + # TODO: the dualvar is nice in theory, but it might be a little + # too clever. It does mean we need to jump through some hoops + # when exporting. But if we don't keep this mapping here, then + # where? More constants? A utility function, like in + # Log::Any::Util? And if so, where? OpenTelemetry::Common? + # OpenTelemetry::Logs::Logger? + LOG_LEVEL_TRACE => Scalar::Util::dualvar( 1, 'TRACE' ), + LOG_LEVEL_TRACE2 => Scalar::Util::dualvar( 2, 'TRACE2' ), + LOG_LEVEL_TRACE3 => Scalar::Util::dualvar( 3, 'TRACE3' ), + LOG_LEVEL_TRACE4 => Scalar::Util::dualvar( 4, 'TRACE4' ), + LOG_LEVEL_DEBUG => Scalar::Util::dualvar( 5, 'DEBUG' ), + LOG_LEVEL_DEBUG2 => Scalar::Util::dualvar( 6, 'DEBUG2' ), + LOG_LEVEL_DEBUG3 => Scalar::Util::dualvar( 7, 'DEBUG3' ), + LOG_LEVEL_DEBUG4 => Scalar::Util::dualvar( 8, 'DEBUG4' ), + LOG_LEVEL_INFO => Scalar::Util::dualvar( 9, 'INFO' ), + LOG_LEVEL_INFO2 => Scalar::Util::dualvar( 10, 'INFO2' ), + LOG_LEVEL_INFO3 => Scalar::Util::dualvar( 11, 'INFO3' ), + LOG_LEVEL_INFO4 => Scalar::Util::dualvar( 12, 'INFO4' ), + LOG_LEVEL_WARN => Scalar::Util::dualvar( 13, 'WARN' ), + LOG_LEVEL_WARN2 => Scalar::Util::dualvar( 14, 'WARN2' ), + LOG_LEVEL_WARN3 => Scalar::Util::dualvar( 15, 'WARN3' ), + LOG_LEVEL_WARN4 => Scalar::Util::dualvar( 16, 'WARN4' ), + LOG_LEVEL_ERROR => Scalar::Util::dualvar( 17, 'ERROR' ), + LOG_LEVEL_ERROR2 => Scalar::Util::dualvar( 18, 'ERROR2' ), + LOG_LEVEL_ERROR3 => Scalar::Util::dualvar( 19, 'ERROR3' ), + LOG_LEVEL_ERROR4 => Scalar::Util::dualvar( 20, 'ERROR4' ), + LOG_LEVEL_FATAL => Scalar::Util::dualvar( 21, 'FATAL' ), + LOG_LEVEL_FATAL2 => Scalar::Util::dualvar( 22, 'FATAL2' ), + LOG_LEVEL_FATAL3 => Scalar::Util::dualvar( 23, 'FATAL3' ), + LOG_LEVEL_FATAL4 => Scalar::Util::dualvar( 24, 'FATAL4' ), - TRACE_EXPORT_SUCCESS => 0, - TRACE_EXPORT_FAILURE => 1, - TRACE_EXPORT_TIMEOUT => 2, + # TODO: Since these are now used for exporting both logs and + # traces, we cannot really give them the TRACE_ prefix. This + # name is arguably better, but it might be unfamiliar to + # non-Perl OpenTelemetry users. + EXPORT_RESULT_SUCCESS => 0, + EXPORT_RESULT_FAILURE => 1, + EXPORT_RESULT_TIMEOUT => 2, - INVALID_TRACE_ID => "\0" x 16, - INVALID_SPAN_ID => "\0" x 8, + INVALID_TRACE_ID => "\0" x 16, + INVALID_SPAN_ID => "\0" x 8, }; use constant { - HEX_INVALID_TRACE_ID => unpack('H*', INVALID_TRACE_ID), - HEX_INVALID_SPAN_ID => unpack('H*', INVALID_SPAN_ID), + # TODO: Redefining these here for now so we don't break the + # code that still uses them. I think we can still break things + # but we should decide on a stability policy and come up with + # some deprecation strategy for the future. + TRACE_EXPORT_SUCCESS => EXPORT_RESULT_SUCCESS, + TRACE_EXPORT_FAILURE => EXPORT_RESULT_FAILURE, + TRACE_EXPORT_TIMEOUT => EXPORT_RESULT_TIMEOUT, + + HEX_INVALID_TRACE_ID => unpack('H*', INVALID_TRACE_ID), + HEX_INVALID_SPAN_ID => unpack('H*', INVALID_SPAN_ID), }; our %EXPORT_TAGS = ( - span_status => [qw( - SPAN_STATUS_UNSET - SPAN_STATUS_OK - SPAN_STATUS_ERROR + log => [qw( + LOG_LEVEL_TRACE + LOG_LEVEL_TRACE2 + LOG_LEVEL_TRACE3 + LOG_LEVEL_TRACE4 + LOG_LEVEL_DEBUG + LOG_LEVEL_DEBUG2 + LOG_LEVEL_DEBUG3 + LOG_LEVEL_DEBUG4 + LOG_LEVEL_INFO + LOG_LEVEL_INFO2 + LOG_LEVEL_INFO3 + LOG_LEVEL_INFO4 + LOG_LEVEL_WARN + LOG_LEVEL_WARN2 + LOG_LEVEL_WARN3 + LOG_LEVEL_WARN4 + LOG_LEVEL_ERROR + LOG_LEVEL_ERROR2 + LOG_LEVEL_ERROR3 + LOG_LEVEL_ERROR4 + LOG_LEVEL_FATAL + LOG_LEVEL_FATAL2 + LOG_LEVEL_FATAL3 + LOG_LEVEL_FATAL4 )], span_kind => [qw( SPAN_KIND_INTERNAL @@ -39,11 +105,21 @@ our %EXPORT_TAGS = ( SPAN_KIND_PRODUCER SPAN_KIND_CONSUMER )], + span_status => [qw( + SPAN_STATUS_UNSET + SPAN_STATUS_OK + SPAN_STATUS_ERROR + )], trace_export => [qw( TRACE_EXPORT_SUCCESS TRACE_EXPORT_FAILURE TRACE_EXPORT_TIMEOUT )], + export => [qw( + EXPORT_RESULT_SUCCESS + EXPORT_RESULT_FAILURE + EXPORT_RESULT_TIMEOUT + )], ); $EXPORT_TAGS{span} = [ @@ -64,6 +140,6 @@ $EXPORT_TAGS{trace} = [ use Exporter::Shiny; -our @EXPORT_OK = map @$_, @EXPORT_TAGS{qw( trace span )}; +our @EXPORT_OK = map @$_, @EXPORT_TAGS{qw( export trace span log )}; 1; diff --git a/lib/OpenTelemetry/Constants.pod b/lib/OpenTelemetry/Constants.pod index ff8bd54..02bc032 100644 --- a/lib/OpenTelemetry/Constants.pod +++ b/lib/OpenTelemetry/Constants.pod @@ -78,25 +78,160 @@ The span describes a consumer receiving a message from a broker. =back +=head2 Export Results + +These constants are used to distinguish the result of an export operation. +They can be imported individually, or with the C tag. + +=over + +=item EXPORT_RESULT_SUCCESS + +Marks an export operation as a success. + +=item EXPORT_RESULT_FAILURE + +Marks an export operation as a failure. + +=item EXPORT_RESULT_TIMEOUT + +Marks an export operation that was interrupted because it took too long. + +=back + =head2 Trace Export Results -These constants are used to distinguish the result of a trace export -operation. They can be imported individually, or with the C tag. -They are also imported when using the C tag. +These constants are the sames as those described in the section above, but +were defined when they were only expected to be used for the export of traces. +They are kept for backwards compatibility, and because these names continue to +be used by the OpenTelemetry specifiction. + +They can be imported individually, or with the C tag. They are +also imported when using the C tag. =over =item TRACE_EXPORT_SUCCESS -Marks an export operation as a success. +Marks an export operation as a success. A synomym for +L. =item TRACE_EXPORT_FAILURE -Marks an export operation as a failure. +Marks an export operation as a failure. A synomym for +L. =item TRACE_EXPORT_TIMEOUT Marks an export operation that was interrupted because it took too long. +A synomym for L. + +=back + +=head2 Log Severity Values + +These constants are used to identify the severity of different log record +objects. They can be imported individually, or with the C tag. They +are dualvar scalars, with a numeric value and a string value suitable to +be used when displaying to a human-readable location. + +=over + +=item LOG_LEVEL_TRACE + +Equivalent to 1 when used as a number, and C when used as a string. + +=item LOG_LEVEL_TRACE2 + +Equivalent to 2 when used as a number, and C when used as a string. + +=item LOG_LEVEL_TRACE3 + +Equivalent to 3 when used as a number, and C when used as a string. + +=item LOG_LEVEL_TRACE4 + +Equivalent to 4 when used as a number, and C when used as a string. + +=item LOG_LEVEL_DEBUG + +Equivalent to 5 when used as a number, and C when used as a string. + +=item LOG_LEVEL_DEBUG2 + +Equivalent to 6 when used as a number, and C when used as a string. + +=item LOG_LEVEL_DEBUG3 + +Equivalent to 7 when used as a number, and C when used as a string. + +=item LOG_LEVEL_DEBUG4 + +Equivalent to 8 when used as a number, and C when used as a string. + +=item LOG_LEVEL_INFO + +Equivalent to 9 when used as a number, and C when used as a string. + +=item LOG_LEVEL_INFO2 + +Equivalent to 10 when used as a number, and C when used as a string. + +=item LOG_LEVEL_INFO3 + +Equivalent to 11 when used as a number, and C when used as a string. + +=item LOG_LEVEL_INFO4 + +Equivalent to 12 when used as a number, and C when used as a string. + +=item LOG_LEVEL_WARN + +Equivalent to 13 when used as a number, and C when used as a string. + +=item LOG_LEVEL_WARN2 + +Equivalent to 14 when used as a number, and C when used as a string. + +=item LOG_LEVEL_WARN3 + +Equivalent to 15 when used as a number, and C when used as a string. + +=item LOG_LEVEL_WARN4 + +Equivalent to 16 when used as a number, and C when used as a string. + +=item LOG_LEVEL_ERROR + +Equivalent to 17 when used as a number, and C when used as a string. + +=item LOG_LEVEL_ERROR2 + +Equivalent to 18 when used as a number, and C when used as a string. + +=item LOG_LEVEL_ERROR3 + +Equivalent to 19 when used as a number, and C when used as a string. + +=item LOG_LEVEL_ERROR4 + +Equivalent to 20 when used as a number, and C when used as a string. + +=item LOG_LEVEL_FATAL + +Equivalent to 21 when used as a number, and C when used as a string. + +=item LOG_LEVEL_FATAL2 + +Equivalent to 22 when used as a number, and C when used as a string. + +=item LOG_LEVEL_FATAL3 + +Equivalent to 23 when used as a number, and C when used as a string. + +=item LOG_LEVEL_FATAL4 + +Equivalent to 24 when used as a number, and C when used as a string. =back diff --git a/lib/OpenTelemetry/Exporter.pod b/lib/OpenTelemetry/Exporter.pod index a19c96b..dc735ad 100644 --- a/lib/OpenTelemetry/Exporter.pod +++ b/lib/OpenTelemetry/Exporter.pod @@ -11,28 +11,29 @@ OpenTelemetry::Exporter - Abstract interface of an OpenTelemetry exporter use Future::AsyncAwait; class My::Exporter :does(OpenTelemetry::Exporter) { - method export ( $spans, $timeout // undef ) { ... } + method export ( $elements, $timeout // undef ) { ... } async method shutdown ( $timeout // undef ) { ... } async method force_flush ( $timeout // undef ) { ... } } - # Use it with a span processor - my $processor = OpenTelemetry::SDK::Trace::Span::Processor::Batch->new( - exporter => My::Exporter->new, - ); + # The exporter interface is the same for exporters regardless of what + # they export. - # Register it with the OpenTelemetry tracer provider - OpenTelemetry->tracer_provider->add_span_processor($processor); + # Attach it to a processor + my $processor = Some::Processor->new( exporter => My::Exporter->new ); + + # Register the processor with a provider + $provider->add_span_processor($processor); =head1 DESCRIPTION This module provides an abstract role that can be used by classes implementing -OpenTelemetry exporters. Exporters are objects that can take telemetry data -generated by traces, and send it to whoever needs to process that data. +OpenTelemetry exporters. Exporters are objects that can take telemetry data, +and send it to some external target. -Exporters receive the data they export from a span processor, which must -implement the interface defined in L. +Exporters receive the data they export from a processor, which must +implement the interface defined in L. Although this cannot be enforced in the code, the methods described in this role are all expected to return one of the values from @@ -42,10 +43,11 @@ L. =head2 export - $result = $exporter->export( \@spans, $timeout // undef ); + $result = $exporter->export( \@elements, $timeout // undef ); -Takes an array reference with readable spans (such as those provided by -L) and an optional timeout value +Takes an array reference with exportable elements (such as readable spans +like L or log records like +L) and an optional timeout value, and returns the outcome of exporting the span data. The return value will be one of the @@ -84,12 +86,12 @@ L. =item L -=item L - -=item L +=item L =item L +=item L + =back =head1 COPYRIGHT AND LICENSE diff --git a/lib/OpenTelemetry/Logs/LogRecord/Processor.pm b/lib/OpenTelemetry/Logs/LogRecord/Processor.pm new file mode 100644 index 0000000..ebbf816 --- /dev/null +++ b/lib/OpenTelemetry/Logs/LogRecord/Processor.pm @@ -0,0 +1,10 @@ +use Object::Pad; +# ABSTRACT: The abstract interface for OpenTelemetry log record processors + +package OpenTelemetry::Logs::LogRecord::Processor; + +our $VERSION = '0.015'; + +role OpenTelemetry::Logs::LogRecord::Processor :does(OpenTelemetry::Processor) { + method on_emit; +} diff --git a/lib/OpenTelemetry/Logs/LogRecord/Processor.pod b/lib/OpenTelemetry/Logs/LogRecord/Processor.pod new file mode 100644 index 0000000..3f2380e --- /dev/null +++ b/lib/OpenTelemetry/Logs/LogRecord/Processor.pod @@ -0,0 +1,110 @@ +=encoding UTF-8 + +=head1 NAME + +OpenTelemetry::Logs::LogRecord::Processor - Abstract interface of an OpenTelemetry log record processor + +=head1 SYNOPSIS + + use Object::Pad; + use Future::AsyncAwait; + + class My::Processor :does(OpenTelemetry::Logs::LogRecord::Processor) { + method on_emit ( $log_record ) { ... } + } + + # Create it + my $processor = My::Processor->new( ... ); + + # Register it with the OpenTelemetry tracer provider + OpenTelemetry->logger_provider->add_log_record_processor($processor); + +=head1 DESCRIPTION + +This module provides an abstract role that can be used by classes implementing +OpenTelemetry log record processors. Log record processors are objects that are +registered with a L (or, more accurately, +with one of its subclasses such as those provided by an OpenTelemetry SDK) to +perform additional operations on log records when they are emitted. + +The processor is considered to be the start of a pipeline, which must be +allowed to end with a L to export the log records +to some collector. To this end, processors are expected to accept an optional +C parameter to their constructor. But processors can be used for +other ends. + +=head1 METHODS + +=head2 new + + $processor = Class::Implementing::This::Role->new( + exporter => ..., # optional + ... + ); + +Should take an optional exporter set to an instance of a class implementing +the L role. Processor classes are free to accept any +other parameters they choose. + +=head2 on_emit + + $processor->on_emit( $log_record ); + +Called when the log record is emitted. It takes the emitted log record as its +only parameter. + +This method is called synchronously when the log record is emitted, so it +should not block or die. + +The return value of this method is ignored. + +=head2 shutdown + + $result = await $processor->shutdown( $timeout // undef ); + +Takes an optional timeout value and returns a L that will be done +when this log record processor has completed shutting down. The shutdown +process must include the effects of L, described below. After +shutting down, the processor is not expected to do any further work, and +should ignore any subsequent calls. + +The value of the future will be one of the +L. + +=head2 force_flush + + $result = await $processor->force_flush( $timeout // undef ); + +Takes an optional timeout value and returns a L that will be done +when this log record processor has finished flushing. Flushing signals to the +processor that it should process the data for any unprocessed elements as soon +as possible. This could be due to an imminent shutdown, but does not have to +be. + +The value of the future will be one of the +L. + +=head1 SEE ALSO + +=over + +=item L + +=item L + +=item L + +=item L + +=item L + +=item L + +=back + +=head1 COPYRIGHT AND LICENSE + +This software is copyright (c) 2024 by José Joaquín Atria. + +This is free software; you can redistribute it and/or modify it under the same +terms as the Perl 5 programming language system itself. diff --git a/lib/OpenTelemetry/Logs/Logger.pm b/lib/OpenTelemetry/Logs/Logger.pm new file mode 100644 index 0000000..03b850e --- /dev/null +++ b/lib/OpenTelemetry/Logs/Logger.pm @@ -0,0 +1,16 @@ +use Object::Pad; +# ABSTRACT: A log factory for OpenTelemetry + +package OpenTelemetry::Logs::Logger; + +our $VERSION = '0.015'; + +# TODO: Should this implement an interface like that of Mojo::Log +# or Log::Any? It would mean that writing adapters like +# Log::Any::Adapter::OpenTelemetry for other loggers (eg. Log::ger, +# Dancer2::Logger) would be simpler, since the high-level logging +# interface would already exist. I don't think this goes against +# the standard. +class OpenTelemetry::Logs::Logger { + method emit_record ( %args ) { } +} diff --git a/lib/OpenTelemetry/Logs/Logger.pod b/lib/OpenTelemetry/Logs/Logger.pod new file mode 100644 index 0000000..57002cf --- /dev/null +++ b/lib/OpenTelemetry/Logs/Logger.pod @@ -0,0 +1,101 @@ +=encoding UTF-8 + +=head1 NAME + +OpenTelemetry::Logs::Logger - A log record factory for OpenTelemetry + +=head1 SYNOPSIS + + use OpenTelemetry; + + my $provider = OpenTelemetry->logger_provider; + my $logger = $provider->logger; + + # Emit a log record + $logger->emit_record(%args); + +=head1 DESCRIPTION + +A logger is responsible for emitting log records. The class provided by this +module does nothing, but is suitable to be sub-classed to emit specific kinds +of logs. See L for one such example which +emits L objects. + +=head1 METHODS + +=head2 emit_record + + $logger->emit_record( + attributes => %attributes // {}, + body => $body, + context => $context // undef, + observed_timestamp => $timestamp // time, + severity_number => $number // undef, + severity_text => $text // undef, + timestamp => $timestamp // undef, + ) + +Emits a log record. + +Takes a list of key / value pairs. Of these, the only one that callers are +I expected to provide is the log record body: not doing so may result +in a warning being logged. All other keys are optional and can be used to +further specify the record. The value of the C parameter is not required +to be a string, in order to support structured loggers. + +If provided, the value of the C parameter should be an instance of +L. Otherwise, the current context will be used. In +either case, this will be used to access the span context this log record +should be associated to, if any. + +The value of the C parameter represents the time at which the +event the log record is associated with took place. This can be left unset +if no such time is known. This is different from the C, +which is the time at which the record was seen by the collection platform. +If this is not provided, it will be given a default value by the platform. + +The value of the C parameter should be set to the name of +the level of the logged event (eg. "error", "warning", etc) as known at the +source. The value of the C, on the other hand, should be +a number between 1 and 24, with numbers mapping to the following levels: + +=over + +=item TRACE + +Levels 1-4. A fine-grained debugging event. Typically disabled in default +configurations. + +=item DEBUG + +Levels 5-8. A debugging event. + +=item INFO + +Levels 9-12. An informational event. Indicates that an event happened. + +=item WARN + +Levels 13-16. A warning event. Not an error but is likely more important +than an informational event. + +=item ERROR + +Levels 17-20. An error event. Something went wrong. + +=item FATAL + +Levels 21-24. A fatal error such as application or system crash. + +=back + +Values in L are suitable to +be used as values for C. They will be evaluated in numeric +context internally. + +=head1 COPYRIGHT AND LICENSE + +This software is copyright (c) 2024 by José Joaquín Atria. + +This is free software; you can redistribute it and/or modify it under the same +terms as the Perl 5 programming language system itself. diff --git a/lib/OpenTelemetry/Logs/LoggerProvider.pm b/lib/OpenTelemetry/Logs/LoggerProvider.pm new file mode 100644 index 0000000..ea43a96 --- /dev/null +++ b/lib/OpenTelemetry/Logs/LoggerProvider.pm @@ -0,0 +1,16 @@ +use Object::Pad; +# ABSTRACT: Provides access to OpenTelemetry Loggers + +package OpenTelemetry::Logs::LoggerProvider; + +our $VERSION = '0.015'; + +class OpenTelemetry::Logs::LoggerProvider { + use OpenTelemetry::Logs::Logger; + + field $logger; + + method logger ( %args ) { + $logger //= OpenTelemetry::Logs::Logger->new; + } +} diff --git a/lib/OpenTelemetry/Logs/LoggerProvider.pod b/lib/OpenTelemetry/Logs/LoggerProvider.pod new file mode 100644 index 0000000..3e18a30 --- /dev/null +++ b/lib/OpenTelemetry/Logs/LoggerProvider.pod @@ -0,0 +1,98 @@ +=encoding UTF-8 + +=head1 NAME + +OpenTelemetry::Logs::LoggerProvider - Provides access to OpenTelemetry Loggers + +=head1 SYNOPSIS + + use OpenTelemetry; + + # Read the globally set provider + my $provider = OpenTelemetry->logger_provider; + my $logger = $provider->logger; + $logger->emit_record( body => 'Reticulating splines' ); + + # Set a global logger provider + OpenTelemetry->logger_provider = $another_provider; + +=head1 DESCRIPTION + +As implied by its name, the logger provider is responsible for providing +access to a usable instance of L, which can in +turn be used to emit log records. + +The provider implemented in this package returns an instance of +L which is cached internally. This behaviour +can be modified by inheriting from this class and providing a different +implementation of the L method described below. See +L for a way to set this modified version as a +globally available logger provider. + +=head1 METHODS + +=head2 new + + $provider = OpenTelemetry::Logs::LoggerProvider->new + +Creates a new instance of the logger provider. + +=head2 logger + + $logger = $logger_provider->logger( %args ) + +Takes a set of named parameters, and returns a logger that can be used to +emit log records via L. Accepted +parameters are: + +=over + +=item C + +A name that uniquely identifies an +L. This can +be the instrumentation library, a package name, etc. This value I be +set to a non-empty string. + +=item C + +Specifies the version of the +L, if one is +available. + +=item C + +A hash reference with a set of attributes for this +L. + +=item C + +The schema URL to be recorded in the emitted data. + +=back + +The code implemented in this package ignores all arguments and returns a +L, but subclasses (most notably +L) are free to modify this. + +Callers are free to cache this logger, which logger providers must ensure +can continue to work. In the event that the configuration of the logger +provider has changed, it is the responsibility of the provider to +propagate these changes to existing loggers, or to ensure that existing +loggers remain usable. + +That said, callers should be aware that logger providers I change, +even in limited scopes, and while the logger provider is responsible for +looking after the loggers it has generated, they are not required (and may +not be capable) to alter the functioning of loggers that have been created +by other providers. + +If creating the logger is expensive, then it's the logger provider's +responsibility to cache it. + +=head1 COPYRIGHT AND LICENSE + +This software is copyright (c) 2024 by José Joaquín Atria. + +This is free software; you can redistribute it and/or modify it under the same +terms as the Perl 5 programming language system itself. diff --git a/lib/OpenTelemetry/Processor.pm b/lib/OpenTelemetry/Processor.pm new file mode 100644 index 0000000..e42989b --- /dev/null +++ b/lib/OpenTelemetry/Processor.pm @@ -0,0 +1,22 @@ +use Object::Pad; +# ABSTRACT: The abstract interface for OpenTelemetry processors + +package OpenTelemetry::Processor; + +our $VERSION = '0.015'; + +# NOTE: Moving this here creates a nice symmetry where we have +# OpenTelemetry::{Propagator,Processor,Exporter} at the top-level +# and allow for specific implementations to live under them. +# We should decide where we expect implementations that are +# specific to Traces / Logs / Metrics should live, though. +# For now, this ends up giving us +# * OpenTelemetry::Trace::Span::Processor +# * OpenTelemetry::Logs::LogRecord::Processor +# * OpenTelemetry::Metrics::Instrument::Processor (hypothetical) +# and the SDK implementations with `::SDK` in there somewhere. +role OpenTelemetry::Processor { + method process; + method shutdown; + method force_flush; +} diff --git a/lib/OpenTelemetry/Processor.pod b/lib/OpenTelemetry/Processor.pod new file mode 100644 index 0000000..1db1a67 --- /dev/null +++ b/lib/OpenTelemetry/Processor.pod @@ -0,0 +1,76 @@ +=encoding UTF-8 + +=head1 NAME + +OpenTelemetry::Processor - An abstract interface for OpenTelemetry processors + +=head1 SYNOPSIS + + use Object::Pad; + use Future::AsyncAwait; + + class My::Processor :does(OpenTelemetry::Processor) { + method process { ... } + async method shutdown { ... } + async method force_flush { ... } + + # Any additional methods in your processor + ... + } + + my $processor = My::Processor->new; + +=head1 DESCRIPTION + +This package provides an L role that defines the interface +that OpenTelemetry processor classes should implement. Processors are +objects that represent the start of a pipeline that starts with a provider +and will, in most cases, end with a class implementing the +L role. + +=head1 METHODS + +Although there is unfortunately no way to currently enforce it, this document +describes the way the methods of a class implementing this role are expected +to behave. + +=head2 process + + $processor->process(@items) + +Takes a list of elements to process, and calls +L<"export"|OpenTelemetry::Exporter/export> on the configured exporter on those +elements. Returns nothing. + +=head2 shutdown + + $result = await $processor->shutdown( ... ) + +Calls L<"shutdown"|OpenTelemetry::Exporter/shutdown> on the configured +exporter and returns a L that will hold the result of that operation. + +=head2 force_flush + + $result = await $processor->force_flush( ... ) + +Calls L<"force_flush"|OpenTelemetry::Exporter/force_flush> on the configured +exporter and returns a L that will hold the result of that operation. + +=head1 SEE ALSO + +=over + +=item L + +=item L + +=item L + +=back + +=head1 COPYRIGHT AND LICENSE + +This software is copyright (c) 2024 by José Joaquín Atria. + +This is free software; you can redistribute it and/or modify it under the same +terms as the Perl 5 programming language system itself. diff --git a/lib/OpenTelemetry/Propagator.pod b/lib/OpenTelemetry/Propagator.pod index f29f354..c0a41c8 100644 --- a/lib/OpenTelemetry/Propagator.pod +++ b/lib/OpenTelemetry/Propagator.pod @@ -40,8 +40,8 @@ to define those formats. =head1 METHODS Although there is unfortunately no way to currently enforce it, this document -describes the behaviour of the methods that a class implementing this role -is expected to follow. +describes the way the methods of a class implementing this role are expected +to behave. =head2 inject diff --git a/lib/OpenTelemetry/Trace/Span/Processor.pm b/lib/OpenTelemetry/Trace/Span/Processor.pm index d41df44..ba97e3a 100644 --- a/lib/OpenTelemetry/Trace/Span/Processor.pm +++ b/lib/OpenTelemetry/Trace/Span/Processor.pm @@ -5,10 +5,7 @@ package OpenTelemetry::Trace::Span::Processor; our $VERSION = '0.024'; -role OpenTelemetry::Trace::Span::Processor { +role OpenTelemetry::Trace::Span::Processor :does(OpenTelemetry::Processor) { method on_start; method on_end; - - method shutdown; - method force_flush; } diff --git a/lib/OpenTelemetry/Trace/Tracer.pod b/lib/OpenTelemetry/Trace/Tracer.pod index c6cd733..95ab26f 100644 --- a/lib/OpenTelemetry/Trace/Tracer.pod +++ b/lib/OpenTelemetry/Trace/Tracer.pod @@ -28,11 +28,11 @@ A tracer is responsible for creating L objects. =head2 create_span $span = $tracer->create_span( - name => $name, // 'empty', - parent => $context // undef, - kind => $span_kind // $internal, attributes => $attributes // {}, + kind => $span_kind // $internal, links => $links // [], + name => $name, // 'empty', + parent => $context // undef, start => $timestamp // time, ) diff --git a/t/OpenTelemetry/Constants.t b/t/OpenTelemetry/Constants.t index 23d31e0..8730283 100644 --- a/t/OpenTelemetry/Constants.t +++ b/t/OpenTelemetry/Constants.t @@ -3,6 +3,32 @@ use Test2::V0 -target => 'OpenTelemetry::Constants'; is \%OpenTelemetry::Constants::EXPORT_TAGS, { + log => [qw( + LOG_LEVEL_TRACE + LOG_LEVEL_TRACE2 + LOG_LEVEL_TRACE3 + LOG_LEVEL_TRACE4 + LOG_LEVEL_DEBUG + LOG_LEVEL_DEBUG2 + LOG_LEVEL_DEBUG3 + LOG_LEVEL_DEBUG4 + LOG_LEVEL_INFO + LOG_LEVEL_INFO2 + LOG_LEVEL_INFO3 + LOG_LEVEL_INFO4 + LOG_LEVEL_WARN + LOG_LEVEL_WARN2 + LOG_LEVEL_WARN3 + LOG_LEVEL_WARN4 + LOG_LEVEL_ERROR + LOG_LEVEL_ERROR2 + LOG_LEVEL_ERROR3 + LOG_LEVEL_ERROR4 + LOG_LEVEL_FATAL + LOG_LEVEL_FATAL2 + LOG_LEVEL_FATAL3 + LOG_LEVEL_FATAL4 + )], span => [qw( INVALID_SPAN_ID HEX_INVALID_SPAN_ID @@ -39,9 +65,17 @@ is \%OpenTelemetry::Constants::EXPORT_TAGS, { TRACE_EXPORT_FAILURE TRACE_EXPORT_TIMEOUT )], + export => [qw( + EXPORT_RESULT_SUCCESS + EXPORT_RESULT_FAILURE + EXPORT_RESULT_TIMEOUT + )], }, 'Export tags are correct'; is \@OpenTelemetry::Constants::EXPORT_OK, [qw( + EXPORT_RESULT_SUCCESS + EXPORT_RESULT_FAILURE + EXPORT_RESULT_TIMEOUT INVALID_TRACE_ID HEX_INVALID_TRACE_ID TRACE_EXPORT_SUCCESS @@ -57,6 +91,30 @@ is \@OpenTelemetry::Constants::EXPORT_OK, [qw( SPAN_KIND_CLIENT SPAN_KIND_PRODUCER SPAN_KIND_CONSUMER + LOG_LEVEL_TRACE + LOG_LEVEL_TRACE2 + LOG_LEVEL_TRACE3 + LOG_LEVEL_TRACE4 + LOG_LEVEL_DEBUG + LOG_LEVEL_DEBUG2 + LOG_LEVEL_DEBUG3 + LOG_LEVEL_DEBUG4 + LOG_LEVEL_INFO + LOG_LEVEL_INFO2 + LOG_LEVEL_INFO3 + LOG_LEVEL_INFO4 + LOG_LEVEL_WARN + LOG_LEVEL_WARN2 + LOG_LEVEL_WARN3 + LOG_LEVEL_WARN4 + LOG_LEVEL_ERROR + LOG_LEVEL_ERROR2 + LOG_LEVEL_ERROR3 + LOG_LEVEL_ERROR4 + LOG_LEVEL_FATAL + LOG_LEVEL_FATAL2 + LOG_LEVEL_FATAL3 + LOG_LEVEL_FATAL4 )], 'Exportable functions are correct'; is \@OpenTelemetry::EXPORT, [], 'Exports nothing by default';