Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e244122
[SDK] LogRecord attribute limits enforcement
thc1006 Jun 14, 2026
1612edc
[SDK] LogRecord limits: address review (store by value, OTLP UTF-8-aw…
thc1006 Jun 17, 2026
c4532cd
[SDK] LogRecord limits: address dbarker review (extract utilities, de…
thc1006 Jun 19, 2026
3144991
[SDK] LogRecord limits: fix noexcept correctness + restore Bazel link
thc1006 Jun 19, 2026
ccdd5e9
Merge branch 'main' into feat/log-record-limits-4126
dbarker Jun 20, 2026
8c965e7
[SDK] LogRecord limits: fix MSVC variable shadow + drop dead includes…
thc1006 Jun 21, 2026
5588f2c
Merge branch 'main' into feat/log-record-limits-4126
dbarker Jun 23, 2026
cd413f8
[SDK] LogRecord limits: address dbarker v3 review (truncate during co…
thc1006 Jun 23, 2026
6c68257
Merge branch 'main' into feat/log-record-limits-4126
ThomsonTan Jun 25, 2026
3e32fd7
[SDK] LogRecord limits: gate SetLogRecordLimits on processor capability
thc1006 Jun 25, 2026
0977ddd
[SDK] LogRecord limits: UTF-8-safe truncation in AttributeConverter
thc1006 Jun 25, 2026
3be2379
[SDK] LogRecord limits: satisfy the clang-tidy warning limit
thc1006 Jun 26, 2026
622d670
[SDK] LogRecord limits: default recordable to no limits, inject via p…
thc1006 Jun 26, 2026
9627722
Merge branch 'main' into feat/log-record-limits-4126
dbarker Jun 26, 2026
980ef8e
[SDK] LogRecord limits: address dbarker review (comments, UTF-8 scan …
thc1006 Jun 27, 2026
0ac2cf3
Merge branch 'main' into feat/log-record-limits-4126
dbarker Jun 29, 2026
378442f
[SDK] LogRecord limits: drop vtable implementation-detail comment
thc1006 Jun 30, 2026
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ Increment the:
* [CI] iwyu and clang-tidy: use install_thirdparty.sh for third-party
[#4136](https://github.com/open-telemetry/opentelemetry-cpp/pull/4136)

* [SDK] LogRecord attribute limits enforcement
[#4157](https://github.com/open-telemetry/opentelemetry-cpp/pull/4157)

* [CONFIGURATION] File configuration: declarative resource detection types
[#4148](https://github.com/open-telemetry/opentelemetry-cpp/pull/4148)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "opentelemetry/common/attribute_value.h"
#include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h"
#include "opentelemetry/sdk/logs/log_record_limits.h"
#include "opentelemetry/sdk/logs/recordable.h"
#include "opentelemetry/sdk/resource/resource.h"
#include "opentelemetry/version.h"
Expand Down Expand Up @@ -96,6 +97,14 @@ class OtlpLogRecordable final : public opentelemetry::sdk::logs::Recordable
void SetAttribute(nostd::string_view key,
const opentelemetry::common::AttributeValue &value) noexcept override;

/**
* Apply attribute count and value length limits. Must be called before any
* SetAttribute call to take effect. The referenced limits object must
* outlive this recordable.
*/
void SetLogRecordLimits(
const opentelemetry::sdk::logs::LogRecordLimits &limits) noexcept override;

/**
* Set Resource of this log
* @param Resource the resource to set
Expand All @@ -114,6 +123,7 @@ class OtlpLogRecordable final : public opentelemetry::sdk::logs::Recordable
const opentelemetry::sdk::resource::Resource *resource_ = nullptr;
const opentelemetry::sdk::instrumentationscope::InstrumentationScope *instrumentation_scope_ =
nullptr;
const opentelemetry::sdk::logs::LogRecordLimits *limits_ = nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid storing a pointer to the limits object here. In the normal logger path, CreateLogRecord() sets this from LoggerContext, but the returned LogRecord is an independent unique_ptr. User code can keep that record and call SetAttribute() after the logger/provider/context is gone, which would leave limits_ dangling.

Can we instead copy LogRecordLimits into the recordable instead of storing &limits? The struct is tiny, and it avoids adding a lifetime requirement to LogRecord.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Switched to by-value storage in ba8f9047. The default-constructed value carries the spec defaults (count=128, length=unlimited), so a fresh recordable now enforces the spec count cap from construction — happy to revisit that semantic if you'd prefer a no-op-when-unset sentinel instead.

};

} // namespace otlp
Expand Down
56 changes: 55 additions & 1 deletion exporters/otlp/src/otlp_log_recordable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
// SPDX-License-Identifier: Apache-2.0

#include "opentelemetry/exporters/otlp/otlp_log_recordable.h"
#include <cstddef>
#include <limits>
#include <string>
#include "opentelemetry/common/attribute_value.h"
#include "opentelemetry/common/timestamp.h"
#include "opentelemetry/exporters/otlp/otlp_populate_attribute_utils.h"
#include "opentelemetry/logs/severity.h"
#include "opentelemetry/nostd/span.h"
#include "opentelemetry/nostd/string_view.h"
#include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h"
#include "opentelemetry/sdk/logs/log_record_limits.h"
#include "opentelemetry/sdk/logs/readable_log_record.h"
#include "opentelemetry/sdk/resource/resource.h"
#include "opentelemetry/trace/span_id.h"
Expand All @@ -18,6 +22,7 @@

// clang-format off
#include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep
#include "opentelemetry/proto/common/v1/common.pb.h"
#include "opentelemetry/proto/logs/v1/logs.pb.h"
#include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" // IWYU pragma: keep
// clang-format on
Expand Down Expand Up @@ -233,10 +238,59 @@ void OtlpLogRecordable::SetTraceFlags(const opentelemetry::trace::TraceFlags &tr
proto_record_.set_flags(trace_flags.flags());
}

namespace
{

void TruncateProtoStringValue(proto::common::v1::AnyValue *value, std::size_t max_length) noexcept
{
if (value == nullptr)
{
return;
}
if (value->value_case() == proto::common::v1::AnyValue::kStringValue)
{
if (value->string_value().size() > max_length)
{
value->mutable_string_value()->resize(max_length);
Comment thread
thc1006 marked this conversation as resolved.
Outdated
}
return;
}
if (value->value_case() == proto::common::v1::AnyValue::kArrayValue)
{
auto *array = value->mutable_array_value();
for (int i = 0; i < array->values_size(); ++i)
{
TruncateProtoStringValue(array->mutable_values(i), max_length);
}
}
}

} // namespace

void OtlpLogRecordable::SetAttribute(opentelemetry::nostd::string_view key,
const opentelemetry::common::AttributeValue &value) noexcept
{
OtlpPopulateAttributeUtils::PopulateAttribute(proto_record_.add_attributes(), key, value, true);
if (limits_ != nullptr &&
static_cast<std::size_t>(proto_record_.attributes_size()) >= limits_->attribute_count_limit)
{
proto_record_.set_dropped_attributes_count(proto_record_.dropped_attributes_count() + 1);
return;
}

auto *kv = proto_record_.add_attributes();
OtlpPopulateAttributeUtils::PopulateAttribute(kv, key, value, true);

if (limits_ != nullptr &&
limits_->attribute_value_length_limit != (std::numeric_limits<std::size_t>::max)())
{
TruncateProtoStringValue(kv->mutable_value(), limits_->attribute_value_length_limit);
}
}

void OtlpLogRecordable::SetLogRecordLimits(
const opentelemetry::sdk::logs::LogRecordLimits &limits) noexcept
{
limits_ = &limits;
}

void OtlpLogRecordable::SetResource(const opentelemetry::sdk::resource::Resource &resource) noexcept
Expand Down
68 changes: 68 additions & 0 deletions exporters/otlp/test/otlp_log_recordable_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "opentelemetry/nostd/string_view.h"
#include "opentelemetry/nostd/variant.h"
#include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h"
#include "opentelemetry/sdk/logs/log_record_limits.h"
#include "opentelemetry/sdk/logs/readable_log_record.h"
#include "opentelemetry/sdk/logs/recordable.h"
#include "opentelemetry/sdk/resource/resource.h"
Expand Down Expand Up @@ -419,6 +420,73 @@ TEST(OtlpLogRecordable, PopulateRequestSameScope)
EXPECT_EQ(req.resource_logs(0).scope_logs(0).log_records_size(), 2);
EXPECT_EQ(req.resource_logs(0).scope_logs(0).scope().name(), "lib");
}

TEST(OtlpLogRecordable, AttributeCountLimitReportsDroppedCount)
{
opentelemetry::sdk::logs::LogRecordLimits limits;
limits.attribute_count_limit = 2;

OtlpLogRecordable rec;
rec.SetLogRecordLimits(limits);
rec.SetAttribute("a", static_cast<int64_t>(1));
rec.SetAttribute("b", static_cast<int64_t>(2));
rec.SetAttribute("c", static_cast<int64_t>(3));
rec.SetAttribute("d", static_cast<int64_t>(4));

EXPECT_EQ(rec.log_record().attributes_size(), 2);
EXPECT_EQ(rec.log_record().dropped_attributes_count(), 2u);
}

TEST(OtlpLogRecordable, AttributeValueLengthLimitTruncatesString)
{
opentelemetry::sdk::logs::LogRecordLimits limits;
limits.attribute_value_length_limit = 4;

OtlpLogRecordable rec;
rec.SetLogRecordLimits(limits);
rec.SetAttribute("k", nostd::string_view("abcdefghij"));

ASSERT_EQ(rec.log_record().attributes_size(), 1);
EXPECT_EQ(rec.log_record().attributes(0).value().string_value(), "abcd");
EXPECT_EQ(rec.log_record().dropped_attributes_count(), 0u);
}

TEST(OtlpLogRecordable, AttributeValueLengthLimitTruncatesArrayElements)
{
opentelemetry::sdk::logs::LogRecordLimits limits;
limits.attribute_value_length_limit = 3;

OtlpLogRecordable rec;
rec.SetLogRecordLimits(limits);
nostd::string_view values[] = {nostd::string_view("aaaaaa"), nostd::string_view("bb"),
nostd::string_view("cccc")};
rec.SetAttribute("k", nostd::span<const nostd::string_view>(values, 3));

ASSERT_EQ(rec.log_record().attributes_size(), 1);
const auto &array = rec.log_record().attributes(0).value().array_value();
ASSERT_EQ(array.values_size(), 3);
EXPECT_EQ(array.values(0).string_value(), "aaa");
EXPECT_EQ(array.values(1).string_value(), "bb");
EXPECT_EQ(array.values(2).string_value(), "ccc");
}

TEST(OtlpLogRecordable, AttributeValueLengthLimitLeavesNonStringTypesUnchanged)
{
opentelemetry::sdk::logs::LogRecordLimits limits;
limits.attribute_value_length_limit = 1;

OtlpLogRecordable rec;
rec.SetLogRecordLimits(limits);
rec.SetAttribute("i", static_cast<int64_t>(1234567890));
rec.SetAttribute("d", 3.14);
rec.SetAttribute("b", true);

ASSERT_EQ(rec.log_record().attributes_size(), 3);
EXPECT_EQ(rec.log_record().attributes(0).value().int_value(), 1234567890);
EXPECT_DOUBLE_EQ(rec.log_record().attributes(1).value().double_value(), 3.14);
EXPECT_EQ(rec.log_record().attributes(2).value().bool_value(), true);
}

} // namespace otlp
} // namespace exporter
OPENTELEMETRY_END_NAMESPACE
37 changes: 37 additions & 0 deletions sdk/include/opentelemetry/sdk/logs/log_record_limits.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include <cstddef>
#include <limits>

#include "opentelemetry/version.h"

OPENTELEMETRY_BEGIN_NAMESPACE
namespace sdk
{
namespace logs
{

/**
* LogRecordLimits carries the SDK-level attribute limits applied to every
* LogRecord emitted through a LoggerProvider. The defaults match the
* specification: at most 128 attributes per record, attribute value length
* unlimited (no truncation).
*
* See https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecord-limits.
*/
struct LogRecordLimits
{
static constexpr std::size_t kDefaultAttributeCountLimit = 128;
static constexpr std::size_t kDefaultAttributeValueLengthLimit =
(std::numeric_limits<std::size_t>::max)();

std::size_t attribute_count_limit = kDefaultAttributeCountLimit;
std::size_t attribute_value_length_limit = kDefaultAttributeValueLengthLimit;
};

} // namespace logs
} // namespace sdk
OPENTELEMETRY_END_NAMESPACE
12 changes: 11 additions & 1 deletion sdk/include/opentelemetry/sdk/logs/logger_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include "logger_config.h"
#include "opentelemetry/sdk/instrumentationscope/scope_configurator.h"
#include "opentelemetry/sdk/logs/log_record_limits.h"
#include "opentelemetry/sdk/logs/processor.h"
#include "opentelemetry/sdk/resource/resource.h"
#include "opentelemetry/version.h"
Expand Down Expand Up @@ -43,7 +44,8 @@ class LoggerContext
std::make_unique<instrumentationscope::ScopeConfigurator<LoggerConfig>>(
instrumentationscope::ScopeConfigurator<LoggerConfig>::Builder(
LoggerConfig::Default())
.Build())) noexcept;
.Build()),
LogRecordLimits log_record_limits = LogRecordLimits()) noexcept;

/**
* Attaches a log processor to list of configured processors to this logger context.
Expand Down Expand Up @@ -76,6 +78,12 @@ class LoggerContext
const instrumentationscope::ScopeConfigurator<LoggerConfig> &GetLoggerConfigurator()
const noexcept;

/**
* Obtain the LogRecord limits applied by this context.
* @return The LogRecordLimits for this logger context.
*/
const LogRecordLimits &GetLogRecordLimits() const noexcept;

/**
* Force all active LogProcessors to flush any buffered logs
* within the given timeout.
Expand All @@ -93,6 +101,8 @@ class LoggerContext
std::unique_ptr<LogRecordProcessor> processor_;

std::unique_ptr<instrumentationscope::ScopeConfigurator<LoggerConfig>> logger_configurator_;

LogRecordLimits log_record_limits_;
};
} // namespace logs
} // namespace sdk
Expand Down
10 changes: 10 additions & 0 deletions sdk/include/opentelemetry/sdk/logs/logger_provider_factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <memory>
#include <vector>

#include "opentelemetry/sdk/logs/log_record_limits.h"
#include "opentelemetry/sdk/logs/logger_context.h"
#include "opentelemetry/sdk/logs/logger_provider.h"
#include "opentelemetry/sdk/logs/processor.h"
Expand Down Expand Up @@ -66,6 +67,15 @@ class OPENTELEMETRY_EXPORT LoggerProviderFactory
const opentelemetry::sdk::resource::Resource &resource,
std::unique_ptr<instrumentationscope::ScopeConfigurator<LoggerConfig>> logger_configurator);

/**
* Create a LoggerProvider with explicit LogRecord limits.
*/
static std::unique_ptr<opentelemetry::sdk::logs::LoggerProvider> Create(
std::vector<std::unique_ptr<LogRecordProcessor>> &&processors,
const opentelemetry::sdk::resource::Resource &resource,
std::unique_ptr<instrumentationscope::ScopeConfigurator<LoggerConfig>> logger_configurator,
LogRecordLimits log_record_limits);

/**
* Create a LoggerProvider.
*/
Expand Down
7 changes: 7 additions & 0 deletions sdk/include/opentelemetry/sdk/logs/multi_recordable.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "opentelemetry/common/timestamp.h"
#include "opentelemetry/logs/log_record.h"
#include "opentelemetry/nostd/string_view.h"
#include "opentelemetry/sdk/logs/log_record_limits.h"
#include "opentelemetry/sdk/logs/processor.h"
#include "opentelemetry/sdk/logs/recordable.h"
#include "opentelemetry/sdk/resource/resource.h"
Expand Down Expand Up @@ -107,6 +108,12 @@ class MultiRecordable final : public Recordable
void SetInstrumentationScope(const opentelemetry::sdk::instrumentationscope::InstrumentationScope
&instrumentation_scope) noexcept override;

/**
* Propagate attribute limits to every wrapped recordable. Must be called
* before any SetAttribute call.
*/
void SetLogRecordLimits(const LogRecordLimits &limits) noexcept override;

private:
std::unordered_map<std::size_t, std::unique_ptr<Recordable>> recordables_;
};
Expand Down
17 changes: 17 additions & 0 deletions sdk/include/opentelemetry/sdk/logs/read_write_log_record.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "opentelemetry/logs/severity.h"
#include "opentelemetry/nostd/string_view.h"
#include "opentelemetry/sdk/common/attribute_utils.h"
#include "opentelemetry/sdk/logs/log_record_limits.h"
#include "opentelemetry/sdk/logs/readable_log_record.h"
#include "opentelemetry/trace/span_id.h"
#include "opentelemetry/trace/trace_flags.h"
Expand Down Expand Up @@ -153,13 +154,26 @@ class ReadWriteLogRecord final : public ReadableLogRecord
void SetAttribute(nostd::string_view key,
const opentelemetry::common::AttributeValue &value) noexcept override;

/**
* Apply attribute count and value length limits. Must be called before any
* SetAttribute call to take effect. The referenced limits object must
Comment thread
dbarker marked this conversation as resolved.
Outdated
* outlive this recordable.
*/
void SetLogRecordLimits(const LogRecordLimits &limits) noexcept override;

/**
* Get attributes of this log.
* @return the body field of this log
*/
const std::unordered_map<std::string, opentelemetry::sdk::common::OwnedAttributeValue> &
GetAttributes() const noexcept override;

/**
* Get the number of attributes dropped because the attribute count limit
* was reached. Truncated string values are not counted as dropped.
*/
uint32_t GetDroppedAttributesCount() const noexcept override;

/**
* Get resource of this log
* @return the resource of this log
Expand Down Expand Up @@ -200,6 +214,9 @@ class ReadWriteLogRecord final : public ReadableLogRecord
int64_t event_id_{0};
std::string event_name_;

const LogRecordLimits *limits_{nullptr};
Comment thread
thc1006 marked this conversation as resolved.
Outdated
uint32_t dropped_attributes_count_{0};

// We do not pay for trace state when not necessary
struct TraceState
{
Expand Down
8 changes: 8 additions & 0 deletions sdk/include/opentelemetry/sdk/logs/readable_log_record.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ class ReadableLogRecord : public Recordable
virtual const std::unordered_map<std::string, opentelemetry::sdk::common::OwnedAttributeValue> &
GetAttributes() const noexcept = 0;

/**
* Get the number of attributes dropped because the attribute count limit
* was reached. The default implementation reports zero so existing
* recordables that do not enforce limits compile without changes.
* @return the number of dropped attributes
*/
virtual uint32_t GetDroppedAttributesCount() const noexcept { return 0; }

/**
* Get resource of this log
* @return the resource of this log
Expand Down
Loading
Loading