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

feat: Add log record attribute limits #1696

Merged
merged 6 commits into from
Oct 11, 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
1 change: 1 addition & 0 deletions logs_sdk/lib/opentelemetry/sdk/logs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
require_relative 'logs/log_record_data'
require_relative 'logs/log_record_processor'
require_relative 'logs/export'
require_relative 'logs/log_record_limits'

module OpenTelemetry
module SDK
Expand Down
57 changes: 56 additions & 1 deletion logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ module SDK
module Logs
# Implementation of OpenTelemetry::Logs::LogRecord that records log events.
class LogRecord < OpenTelemetry::Logs::LogRecord
EMPTY_ATTRIBUTES = {}.freeze

private_constant :EMPTY_ATTRIBUTES

attr_accessor :timestamp,
:observed_timestamp,
:severity_text,
Expand Down Expand Up @@ -49,6 +53,8 @@ class LogRecord < OpenTelemetry::Logs::LogRecord
# source of the log, desrived from the LoggerProvider.
# @param [optional OpenTelemetry::SDK::InstrumentationScope] instrumentation_scope
# The instrumentation scope, derived from the emitting Logger
# @param [optional] OpenTelemetry::SDK::LogRecordLimits] log_record_limits
# Attribute limits
#
#
# @return [LogRecord]
Expand All @@ -63,7 +69,8 @@ def initialize(
span_id: nil,
trace_flags: nil,
resource: nil,
instrumentation_scope: nil
instrumentation_scope: nil,
log_record_limits: nil
)
@timestamp = timestamp
@observed_timestamp = observed_timestamp || timestamp || Time.now
Expand All @@ -76,7 +83,10 @@ def initialize(
@trace_flags = trace_flags
@resource = resource
@instrumentation_scope = instrumentation_scope
@log_record_limits = log_record_limits || LogRecordLimits::DEFAULT
@total_recorded_attributes = @attributes&.size || 0

trim_attributes(@attributes)
end

def to_log_record_data
Expand All @@ -103,6 +113,51 @@ def to_integer_nanoseconds(timestamp)

(timestamp.to_r * 10**9).to_i
end

def trim_attributes(attributes)
return if attributes.nil?

# truncate total attributes
truncate_attributes(attributes, @log_record_limits.attribute_count_limit)

# truncate attribute values
truncate_attribute_values(attributes, @log_record_limits.attribute_length_limit)

# validate attributes
validate_attributes(attributes)

nil
end

def truncate_attributes(attributes, attribute_limit)
excess = attributes.size - attribute_limit
excess.times { attributes.shift } if excess.positive?
end

def validate_attributes(attrs)
# Similar to Internal.valid_attributes?, but with different messages
kaylareopelle marked this conversation as resolved.
Show resolved Hide resolved
kaylareopelle marked this conversation as resolved.
Show resolved Hide resolved
# Future refactor opportunity: https://github.com/open-telemetry/opentelemetry-ruby/issues/1739
attrs.keep_if do |k, v|
if !Internal.valid_key?(k)
OpenTelemetry.handle_error(message: "invalid log record attribute key type #{k.class} on record: '#{body}'")
return false
elsif !Internal.valid_value?(v)
OpenTelemetry.handle_error(message: "invalid log record attribute value type #{v.class} for key '#{k}' on record: '#{body}'")
return false
end

true
end
end

def truncate_attribute_values(attributes, attribute_length_limit)
return EMPTY_ATTRIBUTES if attributes.nil?
return attributes if attribute_length_limit.nil?

attributes.transform_values! { |value| OpenTelemetry::Common::Utilities.truncate_attribute_value(value, attribute_length_limit) }

attributes
end
end
end
end
Expand Down
43 changes: 43 additions & 0 deletions logs_sdk/lib/opentelemetry/sdk/logs/log_record_limits.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0

module OpenTelemetry
module SDK
module Logs
# Class that holds log record attribute limit parameters.
class LogRecordLimits
# The global default max number of attributes per {LogRecord}.
attr_reader :attribute_count_limit

# The global default max length of attribute value per {LogRecord}.
attr_reader :attribute_length_limit

# Returns a {LogRecordLimits} with the desired values.
#
# @return [LogRecordLimits] with the desired values.
# @raise [ArgumentError] if any of the max numbers are not positive.
def initialize(attribute_count_limit: Integer(OpenTelemetry::Common::Utilities.config_opt(
'OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT',
'OTEL_ATTRIBUTE_COUNT_LIMIT',
default: 128
)),
attribute_length_limit: OpenTelemetry::Common::Utilities.config_opt(
'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT',
'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT'
))
raise ArgumentError, 'attribute_count_limit must be positive' unless attribute_count_limit.positive?
raise ArgumentError, 'attribute_length_limit must not be less than 32' unless attribute_length_limit.nil? || Integer(attribute_length_limit) >= 32

@attribute_count_limit = attribute_count_limit
@attribute_length_limit = attribute_length_limit.nil? ? nil : Integer(attribute_length_limit)
end

# The default {LogRecordLimits}.
DEFAULT = new
end
end
end
end
8 changes: 6 additions & 2 deletions logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ class LoggerProvider < OpenTelemetry::Logs::LoggerProvider
#
# @param [optional Resource] resource The resource to associate with
# new LogRecords created by {Logger}s created by this LoggerProvider.
# @param [optional LogRecordLimits] log_record_limits The limits for
# attributes count and attribute length for LogRecords.
#
# @return [OpenTelemetry::SDK::Logs::LoggerProvider]
def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create)
def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create, log_record_limits: LogRecordLimits::DEFAULT)
@log_record_processors = []
@log_record_limits = log_record_limits
@mutex = Mutex.new
@resource = resource
@stopped = false
Expand Down Expand Up @@ -142,7 +145,8 @@ def on_emit(timestamp: nil,
span_id: span_id,
trace_flags: trace_flags,
resource: @resource,
instrumentation_scope: instrumentation_scope)
instrumentation_scope: instrumentation_scope,
log_record_limits: @log_record_limits)

@log_record_processors.each { |processor| processor.on_emit(log_record, context) }
end
Expand Down
79 changes: 79 additions & 0 deletions logs_sdk/test/opentelemetry/sdk/logs/log_record_limits_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0

require 'test_helper'

describe OpenTelemetry::SDK::Logs::LogRecordLimits do
let(:log_record_limits) { OpenTelemetry::SDK::Logs::LogRecordLimits.new }

describe '#initialize' do
it 'provides defaults' do
_(log_record_limits.attribute_count_limit).must_equal 128
_(log_record_limits.attribute_length_limit).must_be_nil
end

it 'prioritizes specific environment varibles for attribute value length limits' do
OpenTelemetry::TestHelpers.with_env('OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '35',
'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '33') do
_(log_record_limits.attribute_length_limit).must_equal 33
end
end

it 'uses general attribute value length limits in the absence of more specific ones' do
OpenTelemetry::TestHelpers.with_env('OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '35') do
_(log_record_limits.attribute_length_limit).must_equal 35
end
end

it 'reflects environment variables' do
OpenTelemetry::TestHelpers.with_env('OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT' => '1',
'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '32') do
_(log_record_limits.attribute_count_limit).must_equal 1
_(log_record_limits.attribute_length_limit).must_equal 32
end
end

it 'reflects explicit overrides' do
OpenTelemetry::TestHelpers.with_env('OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT' => '1',
'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '4') do
log_record_limits = OpenTelemetry::SDK::Logs::LogRecordLimits.new(attribute_count_limit: 10,
attribute_length_limit: 32)
_(log_record_limits.attribute_count_limit).must_equal 10
_(log_record_limits.attribute_length_limit).must_equal 32
end
end

it 'reflects generic attribute env vars' do
OpenTelemetry::TestHelpers.with_env('OTEL_ATTRIBUTE_COUNT_LIMIT' => '1',
'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '32') do
_(log_record_limits.attribute_count_limit).must_equal 1
_(log_record_limits.attribute_length_limit).must_equal 32
end
end

it 'prefers model-specific attribute env vars over generic attribute env vars' do
OpenTelemetry::TestHelpers.with_env('OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT' => '1',
'OTEL_ATTRIBUTE_COUNT_LIMIT' => '2',
'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '32',
'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '33') do
_(log_record_limits.attribute_count_limit).must_equal 1
_(log_record_limits.attribute_length_limit).must_equal 32
end
end

it 'raises if attribute_count_limit is not positive' do
assert_raises ArgumentError do
OpenTelemetry::SDK::Logs::LogRecordLimits.new(attribute_count_limit: -1)
end
end

it 'raises if attribute_length_limit is less than 32' do
assert_raises ArgumentError do
OpenTelemetry::SDK::Logs::LogRecordLimits.new(attribute_length_limit: 31)
end
end
end
end
80 changes: 80 additions & 0 deletions logs_sdk/test/opentelemetry/sdk/logs/log_record_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,85 @@
assert_equal(args[:instrumentation_scope], log_record_data.instrumentation_scope)
end
end

describe 'attribute limits' do
it 'uses the limits set by the logger provider via the logger' do
# Spy on the console output
captured_stdout = StringIO.new
original_stdout = $stdout
$stdout = captured_stdout

# Create the LoggerProvider with the console exporter and an attribute limit of 1
limits = Logs::LogRecordLimits.new(attribute_count_limit: 1)
logger_provider = Logs::LoggerProvider.new(log_record_limits: limits)
console_exporter = Logs::Export::SimpleLogRecordProcessor.new(Logs::Export::ConsoleLogRecordExporter.new)
logger_provider.add_log_record_processor(console_exporter)

# Create a logger that uses the given LoggerProvider
logger = Logs::Logger.new('', '', logger_provider)

# Emit a log from that logger, with attribute count exceeding the limit
logger.on_emit(attributes: { 'a' => 'a', 'b' => 'b' })

# Look at the captured output to see if the attributes have been truncated
assert_match(/attributes={"b"=>"b"}/, captured_stdout.string)
refute_match(/"a"=>"a"/, captured_stdout.string)

# Return STDOUT to its normal output
$stdout = original_stdout
end

it 'emits an error message if attribute key is invalid' do
OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
logger.on_emit(attributes: { a: 'a' })
assert_match(/invalid log record attribute key type Symbol/, log_stream.string)
end
end

it 'emits an error message if the attribute value is invalid' do
OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
logger.on_emit(attributes: { 'a' => Class.new })
assert_match(/invalid log record attribute value type Class/, log_stream.string)
end
end

it 'uses the default limits if none provided' do
log_record = Logs::LogRecord.new
default = Logs::LogRecordLimits::DEFAULT

assert_equal(default.attribute_count_limit, log_record.instance_variable_get(:@log_record_limits).attribute_count_limit)
# default length is nil
assert_nil(log_record.instance_variable_get(:@log_record_limits).attribute_length_limit)
end

it 'trims the oldest attributes' do
limits = Logs::LogRecordLimits.new(attribute_count_limit: 1)
attributes = { 'old' => 'old', 'new' => 'new' }
log_record = Logs::LogRecord.new(log_record_limits: limits, attributes: attributes)

assert_equal({ 'new' => 'new' }, log_record.attributes)
end
end

describe 'attribute value limit' do
it 'truncates the values that are too long' do
length_limit = 32
too_long = 'a' * (length_limit + 1)
just_right = 'a' * (length_limit - 3) # truncation removes 3 chars for the '...'
limits = Logs::LogRecordLimits.new(attribute_length_limit: length_limit)
log_record = Logs::LogRecord.new(log_record_limits: limits, attributes: { 'key' => too_long })

assert_equal({ 'key' => "#{just_right}..." }, log_record.attributes)
end

it 'does not alter values within the range' do
length_limit = 32
within_range = 'a' * length_limit
limits = Logs::LogRecordLimits.new(attribute_length_limit: length_limit)
log_record = Logs::LogRecord.new(log_record_limits: limits, attributes: { 'key' => within_range })

assert_equal({ 'key' => within_range }, log_record.attributes)
end
end
end
end
15 changes: 12 additions & 3 deletions logs_sdk/test/opentelemetry/sdk/logs/logger_provider_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@
end
end

describe '#initialize' do
it 'activates a default LogRecordLimits' do
assert_equal(
OpenTelemetry::SDK::Logs::LogRecordLimits::DEFAULT,
logger_provider.instance_variable_get(:@log_record_limits)
)
end
end

describe '#add_log_record_processor' do
it "adds the processor to the logger provider's processors" do
assert_equal(0, logger_provider.instance_variable_get(:@log_record_processors).length)
Expand Down Expand Up @@ -73,15 +82,15 @@
# :version is nil by default, but explicitly setting it here
# to make the test easier to read
logger = logger_provider.logger(name: 'name', version: nil)
assert_equal(logger.instance_variable_get(:@instrumentation_scope).version, '')
assert_equal('', logger.instance_variable_get(:@instrumentation_scope).version)
end

it 'creates a new logger with the passed-in name and version' do
name = 'name'
version = 'version'
logger = logger_provider.logger(name: name, version: version)
assert_equal(logger.instance_variable_get(:@instrumentation_scope).name, name)
assert_equal(logger.instance_variable_get(:@instrumentation_scope).version, version)
assert_equal(name, logger.instance_variable_get(:@instrumentation_scope).name)
assert_equal(version, logger.instance_variable_get(:@instrumentation_scope).version)
end
end

Expand Down
Loading