Skip to content

Commit

Permalink
feat: Add log record attribute limits (#1696)
Browse files Browse the repository at this point in the history
* feat: Add log record attribute limits

Similar to SpanLimits, add a LogRecordLimits class that handles
configuration of attribute count and value length values.

* Update logs_sdk/test/opentelemetry/sdk/logs/log_record_test.rb

* Update logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb

* Update logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb

---------

Co-authored-by: Matthew Wear <matthew.wear@gmail.com>
  • Loading branch information
kaylareopelle and mwear authored Oct 11, 2024
1 parent 7aa9c11 commit c469bb5
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 6 deletions.
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
# 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

0 comments on commit c469bb5

Please sign in to comment.