Skip to content

Commit aa6ecce

Browse files
feat: OpenTelemetry.logger_provider API, ProxyLoggers, Configuration, and Instrument Registry (#1725)
* WIP: Log SDK configuration * feat: Add configuration patch for logs SDK * style: Update spacing * test: Add tests for logs api * feat: Update inheritance to get tests to pass * feat: Add Instrument Registry to LoggerProvider Create a registry for loggers to make sure a logger with an identical name and version is created only once and reused * feat: Rescue NameError for OTLP logs exporter When OTLP logs exporter not installed, rescue the error, emit a message and set the exporter to nil. * Remove skip instrumenting stuff * style: Rubocop * test: Add skip for intermittent failure * refactor: Remove delegate, mutex from ProxyLogger * fix: Do not emit logs if stopped Previously, a no-op Logger was returned when LoggerProvider#logger was called after the provider was stopped. Now, when the provider is stopped, the on_emit method will return early and not emit any log records. This more closely follows the behavior in the TracerProvider.
1 parent be01344 commit aa6ecce

File tree

10 files changed

+347
-9
lines changed

10 files changed

+347
-9
lines changed

logs_api/lib/opentelemetry-logs-api.rb

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,37 @@
55
# SPDX-License-Identifier: Apache-2.0
66

77
require 'opentelemetry'
8-
require_relative 'opentelemetry/logs'
9-
require_relative 'opentelemetry/logs/version'
8+
require 'opentelemetry/logs'
9+
require 'opentelemetry/logs/version'
10+
require 'opentelemetry/internal/proxy_logger_provider'
11+
require 'opentelemetry/internal/proxy_logger'
12+
13+
# OpenTelemetry is an open source observability framework, providing a
14+
# general-purpose API, SDK, and related tools required for the instrumentation
15+
# of cloud-native software, frameworks, and libraries.
16+
#
17+
# The OpenTelemetry module in the Logs API gem provides global accessors
18+
# for logs-related objects.
19+
module OpenTelemetry
20+
@logger_provider = Internal::ProxyLoggerProvider.new
21+
22+
# Register the global logger provider.
23+
#
24+
# @param [LoggerProvider] provider A logger provider to register as the
25+
# global instance.
26+
def logger_provider=(provider)
27+
@mutex.synchronize do
28+
if @logger_provider.instance_of? Internal::ProxyLoggerProvider
29+
logger.debug("Upgrading default proxy logger provider to #{provider.class}")
30+
@logger_provider.delegate = provider
31+
end
32+
@logger_provider = provider
33+
end
34+
end
35+
36+
# @return [Object, Logs::LoggerProvider] registered logger provider or a
37+
# default no-op implementation of the logger provider.
38+
def logger_provider
39+
@mutex.synchronize { @logger_provider }
40+
end
41+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
module OpenTelemetry
8+
module Internal
9+
# @api private
10+
#
11+
# {ProxyLogger} is an implementation of {OpenTelemetry::Logs::Logger}. It is returned from
12+
# the ProxyLoggerProvider until a delegate logger provider is installed. After the delegate
13+
# logger provider is installed, the ProxyLogger will delegate to the corresponding "real"
14+
# logger.
15+
class ProxyLogger < Logs::Logger
16+
attr_writer :delegate
17+
18+
# Returns a new {ProxyLogger} instance.
19+
#
20+
# @return [ProxyLogger]
21+
def initialize
22+
@delegate = nil
23+
end
24+
25+
def on_emit(
26+
timestamp: nil,
27+
observed_timestamp: nil,
28+
severity_number: nil,
29+
severity_text: nil,
30+
body: nil,
31+
trace_id: nil,
32+
span_id: nil,
33+
trace_flags: nil,
34+
attributes: nil,
35+
context: nil
36+
)
37+
unless @delegate.nil?
38+
return @delegate.on_emit(
39+
timestamp: nil,
40+
observed_timestamp: nil,
41+
severity_number: nil,
42+
severity_text: nil,
43+
body: nil,
44+
trace_id: nil,
45+
span_id: nil,
46+
trace_flags: nil,
47+
attributes: nil,
48+
context: nil
49+
)
50+
end
51+
52+
super
53+
end
54+
end
55+
end
56+
end
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
module OpenTelemetry
8+
module Internal
9+
# @api private
10+
#
11+
# {ProxyLoggerProvider} is an implementation of {OpenTelemetry::Logs::LoggerProvider}.
12+
# It is the default global logger provider returned by OpenTelemetry.logger_provider.
13+
# It delegates to a "real" LoggerProvider after the global logger provider is registered.
14+
# It returns {ProxyLogger} instances until the delegate is installed.
15+
class ProxyLoggerProvider < Logs::LoggerProvider
16+
Key = Struct.new(:name, :version)
17+
private_constant(:Key)
18+
# Returns a new {ProxyLoggerProvider} instance.
19+
#
20+
# @return [ProxyLoggerProvider]
21+
def initialize
22+
super
23+
24+
@mutex = Mutex.new
25+
@registry = {}
26+
@delegate = nil
27+
end
28+
29+
# Set the delegate logger provider. If this is called more than once, a warning will
30+
# be logged and superfluous calls will be ignored.
31+
#
32+
# @param [LoggerProvider] provider The logger provider to delegate to
33+
def delegate=(provider)
34+
unless @delegate.nil?
35+
OpenTelemetry.logger.warn 'Attempt to reset delegate in ProxyLoggerProvider ignored.'
36+
return
37+
end
38+
39+
@mutex.synchronize do
40+
@delegate = provider
41+
@registry.each { |key, logger| logger.delegate = provider.logger(key.name, key.version) }
42+
end
43+
end
44+
45+
# Returns a {Logger} instance.
46+
#
47+
# @param [optional String] name Instrumentation package name
48+
# @param [optional String] version Instrumentation package version
49+
#
50+
# @return [Logger]
51+
def logger(name = nil, version = nil)
52+
@mutex.synchronize do
53+
return @delegate.logger(name, version) unless @delegate.nil?
54+
55+
@registry[Key.new(name, version)] ||= ProxyLogger.new
56+
end
57+
end
58+
end
59+
end
60+
end

logs_api/lib/opentelemetry/logs.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,6 @@
44
#
55
# SPDX-License-Identifier: Apache-2.0
66

7-
require_relative 'logs/log_record'
8-
require_relative 'logs/logger'
9-
require_relative 'logs/logger_provider'
10-
require_relative 'logs/severity_number'
11-
127
module OpenTelemetry
138
# The Logs API records a timestamped record with metadata.
149
# In OpenTelemetry, any data that is not part of a distributed trace or a
@@ -20,3 +15,8 @@ module OpenTelemetry
2015
module Logs
2116
end
2217
end
18+
19+
require 'opentelemetry/logs/log_record'
20+
require 'opentelemetry/logs/logger'
21+
require 'opentelemetry/logs/logger_provider'
22+
require 'opentelemetry/logs/severity_number'
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
require 'test_helper'
8+
9+
describe OpenTelemetry do
10+
class CustomLogRecord < OpenTelemetry::Logs::LogRecord
11+
end
12+
13+
class CustomLogger < OpenTelemetry::Logs::Logger
14+
def on_emit(*)
15+
CustomLogRecord.new
16+
end
17+
end
18+
19+
class CustomLoggerProvider < OpenTelemetry::Logs::LoggerProvider
20+
def logger(name = nil, version = nil)
21+
CustomLogger.new
22+
end
23+
end
24+
25+
describe '.logger_provider' do
26+
after do
27+
# Ensure we don't leak custom logger factories and loggers to other tests
28+
OpenTelemetry.logger_provider = OpenTelemetry::Internal::ProxyLoggerProvider.new
29+
end
30+
31+
it 'returns a Logs::LoggerProvider by default' do
32+
logger_provider = OpenTelemetry.logger_provider
33+
_(logger_provider).must_be_kind_of(OpenTelemetry::Logs::LoggerProvider)
34+
end
35+
36+
it 'returns the same instance when accessed multiple times' do
37+
_(OpenTelemetry.logger_provider).must_equal(OpenTelemetry.logger_provider)
38+
end
39+
40+
it 'returns user-specified logger provider' do
41+
custom_logger_provider = CustomLoggerProvider.new
42+
OpenTelemetry.logger_provider = custom_logger_provider
43+
_(OpenTelemetry.logger_provider).must_equal(custom_logger_provider)
44+
end
45+
end
46+
47+
describe '.logger_provider=' do
48+
after do
49+
# Ensure we don't leak custom logger factories and loggers to other tests
50+
OpenTelemetry.logger_provider = OpenTelemetry::Internal::ProxyLoggerProvider.new
51+
end
52+
53+
it 'has a default proxy logger' do
54+
refute_nil OpenTelemetry.logger_provider.logger
55+
end
56+
57+
it 'upgrades default loggers to *real* loggers' do
58+
# proxy loggers do not emit any log records, nor does the API logger
59+
# the on_emit method is empty
60+
default_logger = OpenTelemetry.logger_provider.logger
61+
_(default_logger.on_emit(body: 'test')).must_be_instance_of(NilClass)
62+
OpenTelemetry.logger_provider = CustomLoggerProvider.new
63+
_(default_logger.on_emit(body: 'test')).must_be_instance_of(CustomLogRecord)
64+
end
65+
66+
it 'upgrades the default logger provider to a *real* logger provider' do
67+
default_logger_provider = OpenTelemetry.logger_provider
68+
OpenTelemetry.logger_provider = CustomLoggerProvider.new
69+
_(default_logger_provider.logger).must_be_instance_of(CustomLogger)
70+
end
71+
end
72+
end

logs_sdk/lib/opentelemetry/sdk/logs.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# SPDX-License-Identifier: Apache-2.0
66

77
require_relative 'logs/version'
8+
require_relative 'logs/configuration_patch'
89
require_relative 'logs/logger'
910
require_relative 'logs/logger_provider'
1011
require_relative 'logs/log_record'
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
require 'opentelemetry/sdk/configurator'
8+
9+
module OpenTelemetry
10+
module SDK
11+
module Logs
12+
# The ConfiguratorPatch implements a hook to configure the logs portion
13+
# of the SDK.
14+
module ConfiguratorPatch
15+
def add_log_record_processor(log_record_processor)
16+
@log_record_processors << log_record_processor
17+
end
18+
19+
private
20+
21+
def initialize
22+
super
23+
@log_record_processors = []
24+
end
25+
26+
# The logs_configuration_hook method is where we define the setup
27+
# process for logs SDK.
28+
def logs_configuration_hook
29+
OpenTelemetry.logger_provider = Logs::LoggerProvider.new(resource: @resource)
30+
configure_log_record_processors
31+
end
32+
33+
def configure_log_record_processors
34+
processors = @log_record_processors.empty? ? wrapped_log_exporters_from_env.compact : @log_record_processors
35+
processors.each { |p| OpenTelemetry.logger_provider.add_log_record_processor(p) }
36+
end
37+
38+
def wrapped_log_exporters_from_env
39+
# TODO: set default to OTLP to match traces, default is console until other exporters merged
40+
exporters = ENV.fetch('OTEL_LOGS_EXPORTER', 'console')
41+
42+
exporters.split(',').map do |exporter|
43+
case exporter.strip
44+
when 'none' then nil
45+
when 'console' then Logs::Export::SimpleLogRecordProcessor.new(Logs::Export::ConsoleLogRecordExporter.new)
46+
when 'otlp'
47+
otlp_protocol = ENV['OTEL_EXPORTER_OTLP_LOGS_PROTOCOL'] || ENV['OTEL_EXPORTER_OTLP_PROTOCOL'] || 'http/protobuf'
48+
49+
if otlp_protocol != 'http/protobuf'
50+
OpenTelemetry.logger.warn "The #{otlp_protocol} transport protocol is not supported by the OTLP exporter, log_records will not be exported."
51+
nil
52+
else
53+
begin
54+
Logs::Export::BatchLogRecordProcessor.new(OpenTelemetry::Exporter::OTLP::LogsExporter.new)
55+
rescue NameError
56+
OpenTelemetry.logger.warn 'The otlp logs exporter cannot be configured - please add opentelemetry-exporter-otlp-logs to your Gemfile. Logs will not be exported'
57+
nil
58+
end
59+
end
60+
else
61+
OpenTelemetry.logger.warn "The #{exporter} exporter is unknown and cannot be configured, log records will not be exported"
62+
nil
63+
end
64+
end
65+
end
66+
end
67+
end
68+
end
69+
end
70+
71+
OpenTelemetry::SDK::Configurator.prepend(OpenTelemetry::SDK::Logs::ConfiguratorPatch)

logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ module SDK
99
module Logs
1010
# The SDK implementation of OpenTelemetry::Logs::LoggerProvider.
1111
class LoggerProvider < OpenTelemetry::Logs::LoggerProvider
12+
Key = Struct.new(:name, :version)
13+
private_constant(:Key)
14+
1215
UNEXPECTED_ERROR_MESSAGE = 'unexpected error in ' \
1316
'OpenTelemetry::SDK::Logs::LoggerProvider#%s'
1417

@@ -28,6 +31,8 @@ def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create, log_rec
2831
@mutex = Mutex.new
2932
@resource = resource
3033
@stopped = false
34+
@registry = {}
35+
@registry_mutex = Mutex.new
3136
end
3237

3338
# Returns an {OpenTelemetry::SDK::Logs::Logger} instance.
@@ -44,7 +49,9 @@ def logger(name:, version: nil)
4449
"invalid name. Name provided: #{name.inspect}")
4550
end
4651

47-
Logger.new(name, version, self)
52+
@registry_mutex.synchronize do
53+
@registry[Key.new(name, version)] ||= Logger.new(name, version, self)
54+
end
4855
end
4956

5057
# Adds a new log record processor to this LoggerProvider's
@@ -135,6 +142,8 @@ def on_emit(timestamp: nil,
135142
instrumentation_scope: nil,
136143
context: nil)
137144

145+
return if @stopped
146+
138147
log_record = LogRecord.new(timestamp: timestamp,
139148
observed_timestamp: observed_timestamp,
140149
severity_text: severity_text,

0 commit comments

Comments
 (0)