Skip to content

[SDK] LogRecord attribute limits enforcement#4157

Merged
marcalff merged 19 commits into
open-telemetry:mainfrom
thc1006:feat/log-record-limits-4126
Jul 3, 2026
Merged

[SDK] LogRecord attribute limits enforcement#4157
marcalff merged 19 commits into
open-telemetry:mainfrom
thc1006:feat/log-record-limits-4126

Conversation

@thc1006

@thc1006 thc1006 commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #4126.

Implements the LogRecord attribute count and value length limits described
by the logs SDK spec
(https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecord-limits).
Limits flow from LoggerProvider through LoggerContext to each LogRecord
created by Logger::CreateLogRecord. The infrastructure existed in the
declarative configuration tier (LogRecordLimitsConfiguration) but was
never wired into the runtime pipeline.

What changed

New header sdk/include/opentelemetry/sdk/logs/log_record_limits.h:

  • LogRecordLimits struct with spec defaults
    attribute_count_limit = 128 and
    attribute_value_length_limit = SIZE_MAX (unlimited).

SDK Recordable hierarchy:

  • Recordable gains a non-pure virtual SetLogRecordLimits with a no-op
    default body. Existing implementations that do not enforce limits
    inherit the no-op and compile unchanged. The virtual is appended at
    the end of the vtable to keep the change additive.
  • ReadableLogRecord gains a non-pure virtual GetDroppedAttributesCount
    returning zero by default.
  • ReadWriteLogRecord overrides both. SetAttribute checks the count
    limit before inserting and truncates string / array-of-string values
    whose length exceeds the configured limit. Truncation is byte level,
    mirroring the existing Span attribute behavior.
  • MultiRecordable propagates SetLogRecordLimits to every wrapped
    recordable.

OTLP exporter:

  • OtlpLogRecordable overrides SetLogRecordLimits. SetAttribute
    drops attributes beyond the count limit and increments the proto
    LogRecord.dropped_attributes_count field. Strings and string-array
    values are truncated after OtlpPopulateAttributeUtils::PopulateAttribute
    populates the proto AnyValue.

Wiring:

  • LoggerContext owns a LogRecordLimits value and exposes
    GetLogRecordLimits(). A new fourth parameter is appended to the
    existing constructor with a default-constructed default, so existing
    call sites compile unchanged.
  • Logger::CreateLogRecord (both ABI v1 and ABI v2 variants) calls
    recordable->SetLogRecordLimits(context_->GetLogRecordLimits())
    immediately after MakeRecordable, before any user attribute write.
  • A new LoggerProviderFactory::Create overload accepts
    LogRecordLimits and builds a LoggerContext with those limits
    internally.

Declarative configuration:

  • SdkBuilder::CreateLoggerProvider now maps
    LogRecordLimitsConfiguration from the parsed
    LoggerProviderConfiguration to the runtime LogRecordLimits and
    passes them to the new factory overload. The existing FIXME-SDK
    comment about wiring limits is removed.

Tests

sdk/test/logs/log_record_limits_test.cc (new):

  • Default behavior: 200 attributes accepted, zero dropped when no
    limits object is supplied.
  • Count enforcement: attributes beyond the limit are dropped and
    counted. Replacing an existing key while at the limit must not drop.
  • Length truncation for strings and string arrays.
  • Type selectivity: only string and array-of-string are truncated;
    int / double / bool pass through.
  • Combined count + length case.
  • Logger-level wiring test: a TrackingRecordable produced by a
    TrackingProcessor confirms that the limits configured on
    LoggerProvider reach the recordable returned by
    Logger::CreateLogRecord.

exporters/otlp/test/otlp_log_recordable_test.cc augmented with four
cases that verify the proto dropped_attributes_count field and the
string / array truncation logic.

Verification

Built and tested in the otelcpp-dev container:

  • Release (gcc, abiv1): log_record_limits_test 9/9,
    otlp_log_recordable_test 23/23, logger_sdk_test 9/9,
    logger_provider_sdk_test 9/9, simple_log_record_processor_test
    10/10, batch_log_record_processor_test 13/13. No regression.
  • ABI v2 (clang): log_record_limits_test 9/9.
  • -fno-rtti (Bazel nortti CI mirror): log_record_limits_test 9/9,
    otlp_log_recordable_test 23/23.
  • clang-format fixed point verified for all touched files.
  • IWYU (abiv1-preview): include-list reconciled with diagnostics
    from a local clang-22 run.
  • git diff --check whitespace: clean.

Known limitations

  • ElasticSearchRecordable is not modified in this PR. It inherits
    the no-op default and does not enforce limits. A follow-up PR can
    add enforcement if desired.
  • Truncation is byte level. Strings whose configured limit falls inside
    a multibyte UTF-8 sequence will be truncated mid-sequence, matching
    the existing Span attribute behavior in OtlpRecordable.
  • The default branch in the YAML parser
    (ParseLogRecordLimitsConfiguration) keeps the existing
    attribute_value_length_limit = 4096 fallback for callers that
    opt into the limits: block without specifying a length. Aligning
    that fallback with the spec default (unlimited) is left to a follow-up.

@thc1006 thc1006 requested a review from a team as a code owner June 14, 2026 14:25
@thc1006 thc1006 force-pushed the feat/log-record-limits-4126 branch from 7bdc3c5 to 1769757 Compare June 14, 2026 14:26
Apply attribute count and value length limits described by the logs
SDK spec (https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecord-limits)
during attribute writes. Limits flow from LoggerProvider through
LoggerContext to each LogRecord created by Logger::CreateLogRecord.

* Add LogRecordLimits struct with spec defaults: attribute_count_limit
  = 128 and attribute_value_length_limit = SIZE_MAX (unlimited).
* Recordable gains a virtual SetLogRecordLimits with a no-op default
  so existing implementations need not change. ReadableLogRecord gains
  a virtual GetDroppedAttributesCount returning zero by default.
* ReadWriteLogRecord and OtlpLogRecordable enforce the limits in
  SetAttribute. An attribute beyond attribute_count_limit is dropped
  and counted as dropped; string and string-array values whose byte
  length exceeds attribute_value_length_limit are truncated.
  Truncation is byte-level, mirroring the existing Span attribute
  behavior. The OTLP path also populates dropped_attributes_count on
  the proto LogRecord.
* MultiRecordable propagates the limits to every wrapped recordable.
* LoggerContext owns a LogRecordLimits value; Logger calls
  SetLogRecordLimits on the recordable returned by MakeRecordable
  before any user attribute writes. A new
  LoggerProviderFactory::Create overload accepts LogRecordLimits.
* The declarative configuration path (SdkBuilder) wires
  LogRecordLimitsConfiguration to the runtime LogRecordLimits.

Tests cover ReadWriteLogRecord and OtlpLogRecordable: defaults, count
enforcement (including the "replace existing key while at limit must
not drop" case), length truncation of strings and string arrays, type
selectivity (only string and array-of-string are truncated), the
combined count plus length case, and a Logger-level wiring test that
verifies the limits configured on LoggerProvider reach the recordable
returned by Logger::CreateLogRecord.

Fixes open-telemetry#4126

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@thc1006 thc1006 force-pushed the feat/log-record-limits-4126 branch from 1769757 to e244122 Compare June 14, 2026 14:29
@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.28358% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.83%. Comparing base (8bc3640) to head (f78e67a).

Files with missing lines Patch % Lines
...xporters/otlp/src/otlp_populate_attribute_utils.cc 80.77% 5 Missing ⚠️
...include/opentelemetry/sdk/common/attribute_utils.h 97.50% 1 Missing ⚠️
sdk/include/opentelemetry/sdk/logs/exporter.h 0.00% 1 Missing ⚠️
...clude/opentelemetry/sdk/logs/readable_log_record.h 0.00% 1 Missing ⚠️
sdk/include/opentelemetry/sdk/logs/recordable.h 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4157      +/-   ##
==========================================
- Coverage   82.88%   82.83%   -0.05%     
==========================================
  Files         405      415      +10     
  Lines       17287    17423     +136     
==========================================
+ Hits        14327    14431     +104     
- Misses       2960     2992      +32     
Files with missing lines Coverage Δ
...ntelemetry/exporters/ostream/log_record_exporter.h 100.00% <100.00%> (ø)
...try/exporters/otlp/otlp_file_log_record_exporter.h 100.00% <100.00%> (ø)
...try/exporters/otlp/otlp_grpc_log_record_exporter.h 100.00% <100.00%> (ø)
...try/exporters/otlp/otlp_http_log_record_exporter.h 100.00% <100.00%> (ø)
...opentelemetry/exporters/otlp/otlp_log_recordable.h 100.00% <ø> (ø)
...try/exporters/otlp/otlp_populate_attribute_utils.h 100.00% <100.00%> (ø)
exporters/otlp/src/otlp_log_recordable.cc 38.16% <100.00%> (+2.11%) ⬆️
...pentelemetry/sdk/logs/batch_log_record_processor.h 100.00% <100.00%> (ø)
...include/opentelemetry/sdk/logs/log_record_limits.h 100.00% <100.00%> (ø)
sdk/include/opentelemetry/sdk/logs/processor.h 100.00% <100.00%> (ø)
... and 12 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@proost

proost commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

@thc1006
I'm working on the feature in here(#4132).
Do you want to implement this feature?

@thc1006

thc1006 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@proost Hey, thanks for the ping. I went through #4132 and the review thread before pushing #4157. There's a fair bit of substantive work in yours that mine doesn't have: the OTEL_LOGRECORD_* env var integration, the benchmark file, ElasticSearchRecordable enforcement, and the yaml count_limit plumbing.

The reason I went ahead with #4157 even after seeing yours is lalitb's comment from yesterday (#4132 (comment)), which was asking to move enforcement out of the base Recordable hot path and let each recordable handle it the way that fits. That's the shape #4157 takes. Recordable just gains a SetLogRecordLimits virtual with a no-op default body, and the actual enforcement lives in ReadWriteLogRecord and OtlpLogRecordable. Non-enforcing exporters inherit the no-op so they pay nothing.

If the maintainers think #4157 is the better base, I'd like to layer the env vars, benchmark, and ES enforcement on top in follow-ups with attribution to your work. If they prefer the #4132 redesign route, happy to close #4157 and review the new version there.

@marcalff @lalitb would appreciate your call on which to drive forward.

@proost

proost commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

@thc1006

Thanks for the explanation.

I'm always open to feedback, and if there were concerns about the direction of #4132, I would have been happy to discuss and adjust it. I do wish those concerns had been raised directly on the PR earlier, as it would have helped avoid duplicated work.

That said, I don't have a strong preference on whose implementation lands. If you would like to continue driving this work, I'm happy to step aside and close #4132 so we don't spend effort maintaining two competing PRs.

So would you like to continue driving this work?

@thc1006

thc1006 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@proost You're right and that one's on me. The honest reason I missed #4132: I was searching open issues by help wanted label and recent merged PRs for landscape, not by fixes #4126 against open PRs. #4132 has no labels and the title in the open-PR list truncates around "attribute count l...", so I read it as a narrower length-only fix and moved on. That was a diligence gap on my part, not a deliberate decision to bypass your work. Sorry about that.

If you're comfortable stepping aside, yes I'd like to continue with #4157.

After #4157 lands I'd open these as follow-ups:

  1. OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT and OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT env-var integration through LogRecordLimits and the SdkBuilder path, modeled on your feat: log record attribute value length limit & attribute count limit #4132 work and adding you as a Co-authored-by on the commit.
  2. The benchmark file from feat: log record attribute value length limit & attribute count limit #4132, ported onto the per-recordable enforcement so the numbers reflect the new architecture, attribution in the commit body.
  3. ElasticSearchRecordable enforcement following the same SetLogRecordLimits pattern.
  4. YAML attribute_count_limit parsing along the lines feat: log record attribute value length limit & attribute count limit #4132 did.

If you'd rather author any of those follow-ups yourself and have me review, that works equally well. Whatever keeps the work landing.

@proost

proost commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Thanks for the clarification and the apology.

I’m happy for you to continue driving this work, so I’ll close #4132 to avoid duplicate effort.

@thc1006

thc1006 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@proost Thanks for the clean handoff. The reviewer feedback from owent, lalitb, and ThomsonTan on #4132 still applies here, and I'll reference the specific discussions when each follow-up goes up. I'll ping you on each so you can shape the attribution however works for you.

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.

Comment thread sdk/include/opentelemetry/sdk/logs/read_write_log_record.h Outdated
Comment thread exporters/otlp/src/otlp_log_recordable.cc Outdated
thc1006 added a commit to thc1006/opentelemetry-cpp that referenced this pull request Jun 18, 2026
…runcate)

Address three review comments from @lalitb on PR open-telemetry#4157:

1. Store LogRecordLimits by value inside ReadWriteLogRecord and
   OtlpLogRecordable instead of a raw pointer to a LoggerContext-owned
   object. A LogRecord is handed out as a unique_ptr, so a record
   outliving the context that produced it would otherwise dereference a
   dangling pointer when the user calls SetAttribute() later. The
   default-constructed value already carries the spec defaults
   (count=128, length=unlimited), so a fresh recordable enforces the
   spec count cap from construction, matching the PR's "enforcement"
   contract.

2. Drop the now-redundant `limits_ != nullptr` short-circuit at every
   enforcement site (4 in total). This also closes the Codecov-reported
   uncovered branch in otlp_log_recordable.cc.

3. Truncate UTF-8 string attributes at a code-point boundary instead of
   a raw byte boundary, so an OTLP protobuf string_value produced by
   truncation stays valid UTF-8 when the input was. Malformed UTF-8 and
   trailing lead bytes degrade to plain byte truncation. Logic adapted
   from open-telemetry#4132 with attribution.

While in the same truncation paths, also apply the byte-length cap to
raw bytes attributes (`vector<uint8_t>` on the SDK side, AnyValue
`bytes_value` on the OTLP side). Both were previously passing through
any size, even though the spec applies `attribute_value_length_limit`
to bytes attributes as well.

Test changes:
- Rename DefaultsPassThroughWithoutLimitsObject to
  DefaultRecordEnforcesSpecCountCap (200 attrs in, 128 stored, 72
  dropped) to reflect the new spec-correct default behavior.
- Add 3 UTF-8 regression tests on the SDK side (split prevention,
  exact fit at sequence boundary, malformed input falls back) plus a
  bytes-truncation test.
- Add 3 mirror tests on the OTLP side, a default-cap test, and a
  bytes-truncation test.

Refs: open-telemetry#4126

Co-authored-by: Hyeonho Kim <proost@apache.org>
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Comment thread sdk/src/logs/read_write_log_record.cc Outdated
…are truncate)

Address three review comments from @lalitb on PR open-telemetry#4157, plus a
follow-up from @owent (r3434178971).

1. Store LogRecordLimits by value inside ReadWriteLogRecord and
   OtlpLogRecordable instead of a raw pointer to a LoggerContext-owned
   object. A LogRecord is handed out as a unique_ptr, so a record
   outliving the context that produced it would otherwise dereference a
   dangling pointer when the user calls SetAttribute() later. The
   default-constructed value already carries the spec defaults
   (count=128, length=unlimited), so a fresh recordable enforces the
   spec count cap from construction, matching the PR's "enforcement"
   contract.

2. Drop the now-redundant `limits_ != nullptr` short-circuit at every
   enforcement site (4 in total). This also closes the Codecov-reported
   uncovered branch in otlp_log_recordable.cc.

3. Truncate OTLP string attributes at a UTF-8 code-point boundary
   instead of a raw byte boundary, so the protobuf string_value produced
   by truncation stays valid UTF-8 when the input was. Malformed UTF-8
   and trailing lead bytes degrade to plain byte truncation. Logic
   adapted from open-telemetry#4132 with attribution.

   The SDK-side ReadWriteLogRecord truncation stays as plain byte cut.
   The in-memory `OwnedAttributeValue::std::string` variant may
   legitimately carry raw bytes when constructed from a non-UTF-8
   source, so forcing UTF-8 boundary semantics there would over-truncate
   that case (per @owent's r3434178971, echoing the same point on open-telemetry#4132
   r3409677314). Each recordable's truncation strategy now matches its
   own consumer's wire-format requirement: SDK in-memory has no wire
   requirement, OTLP protobuf requires valid UTF-8.

While in the same truncation paths, also apply the byte-length cap to
raw bytes attributes (`vector<uint8_t>` on the SDK side, AnyValue
`bytes_value` on the OTLP side). Both were previously passing through
any size, even though the spec applies `attribute_value_length_limit`
to bytes attributes as well.

Test changes:
- Rename DefaultsPassThroughWithoutLimitsObject to
  DefaultRecordEnforcesSpecCountCap (200 attrs in, 128 stored, 72
  dropped) to reflect the new spec-correct default behavior.
- Add bytes-truncation tests on both SDK and OTLP sides.
- Add 3 UTF-8 regression tests on the OTLP side only (split prevention,
  exact fit at sequence boundary; malformed-fallback omitted since the
  algorithm's seq=1 fallback for invalid continuations is implementation
  detail rather than wire contract).
- Add a default-cap test on the OTLP side.

Refs: open-telemetry#4126

Co-authored-by: Hyeonho Kim <proost@apache.org>
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@thc1006 thc1006 force-pushed the feat/log-record-limits-4126 branch from ba8f904 to 1612edc Compare June 18, 2026 09:08

@dbarker dbarker left a comment

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.

Thanks for the PR. Please see comments below.

Comment thread exporters/otlp/src/otlp_log_recordable.cc Outdated
Comment thread sdk/src/logs/read_write_log_record.cc Outdated
Comment thread sdk/src/logs/read_write_log_record.cc Outdated
…dupe SetAttribute lookup)

Address @dbarker's three review comments on PR open-telemetry#4157.

* r3442854800: Extract Utf8SafePrefixLength and TruncateProtoAttributeValue
  from the anonymous namespace in otlp_log_recordable.cc into
  OtlpPopulateAttributeUtils as static methods, with new direct unit tests
  in otlp_populate_attribute_utils_test.cc. The upcoming SpanLimits PR
  will reuse these from the OTLP trace recordable. The OTLP helper that
  was previously TruncateProtoStringValue is renamed
  TruncateProtoAttributeValue to reflect that it covers string_value,
  bytes_value, and array_value branches.

* r3443005793: Extract the SDK byte-length truncation helper into
  sdk::common::TruncateAttributeValueByteLength (inline, declared in the
  existing sdk/common/attribute_utils.h), with new direct unit tests in
  attribute_utils_test.cc. The upcoming SpanLimits PR will reuse this
  from ReadWriteSpanData. The new name reflects that the helper covers
  string, string-array, AND bytes variants rather than only strings.

* r3443034336: Rewrite ReadWriteLogRecord::SetAttribute to use a single
  unordered_map lookup. The previous code did .find() to gate the count
  cap, then operator[] to fetch-or-insert; the new code does .find()
  followed by conditional .emplace(), so existing-key replacement and
  new-key insertion each cost one hash lookup.

Refs: open-telemetry#4126
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Comment thread sdk/include/opentelemetry/sdk/common/attribute_utils.h Outdated
Two small fixes on top of c4532cd:

* Address @dbarker's r3444034234: rewrite
  sdk::common::TruncateAttributeValueByteLength to dispatch on the
  variant via nostd::get_if (returns nullptr if the alternative does
  not hold) instead of nostd::holds_alternative + nostd::get. The
  helper is declared noexcept; nostd::get throws when the alternative
  does not match, which would invoke std::terminate even though the
  preceding holds_alternative check makes that path unreachable in
  practice. The get_if rewrite removes the throwing call entirely so
  the noexcept contract is statically honored.

* Restore the Bazel link of the new otlp_populate_attribute_utils_test
  target by adding //sdk/src/metrics to its deps, matching the
  existing otlp_log_recordable_test target. The new test target links
  against :otlp_recordable, which transitively references
  sdk::metrics::AdaptingCircularBufferCounter symbols; Bazel's strict
  layering requires the dep to be declared at the cc_test level. CMake
  did not catch this because its default link aggregates the whole
  library.

Refs: open-telemetry#4126
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@thc1006 thc1006 requested review from dbarker and lalitb June 19, 2026 19:41
dbarker and others added 2 commits June 20, 2026 10:55
… + clang-tidy NOLINT

Three CI failure classes surfaced after the branch-update merge of main
into the PR branch (ccdd5e9). Address each:

* MSVC C2220 (warning-as-error) on attribute_utils.h:93 — the
  `for (auto &s : *vec)` loop inside TruncateAttributeValueByteLength
  shadowed the `if (auto *s = get_if<std::string>(...))` on line 84.
  GCC/Clang accept if-init scoping for the two `s`, MSVC /W4 /WX
  treats C4456 (declaration hides previous local) as an error. Rename
  the loop variable to `element` to remove the shadow.

* IWYU drop four includes that the v3+v4 utility-extraction refactor
  made redundant:
  - sdk/src/logs/read_write_log_record.cc: <vector>
  - exporters/otlp/src/otlp_log_recordable.cc: <string>
  - exporters/otlp/test/otlp_populate_attribute_utils_test.cc:
    <cstddef> and <limits>

* clang-tidy abiv2-preview misc-no-recursion on
  OtlpPopulateAttributeUtils::TruncateProtoAttributeValue — the recursive
  descent into AnyValue::kArrayValue children is intentional and bounded
  by the SDK-side AttributeValue variant depth. Suppress with a
  NOLINTBEGIN/END(misc-no-recursion) block, matching the codebase
  precedent in sdk/src/configuration/configuration_parser.cc.

The remaining clang-tidy bugprone-exception-escape warnings in the same
config (read_write_log_record.cc:68 SetBody, :158 SetAttribute and
otlp_populate_attribute_utils.cc:62/220 PopulateAnyValue) trace through
nostd::visit / nostd::get into bad_alloc paths that pre-date this PR;
they are out of scope for the LogRecord limits change.

Refs: open-telemetry#4126
Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@thc1006

thc1006 commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

@dbarker the only failing CI on 8c965e7e is DocFX check and it's a transient infrastructure failure unrelated to this PR — the GitHub Releases download for docfx.zip returned HTTP 500:

Attempt to get headers for https://github.com/dotnet/docfx/releases/download/v2.58.5/docfx.zip failed.
The remote server returned an error: (500) Internal Server Error.
Chocolatey installed 0/1 packages. 1 packages failed.
docfx (exited 404)

The job failed in 40 seconds during the docfx download step before any of the PR's content was even read. Would you mind re-running just this job (I don't have admin rights to do it myself)? All other 31 jobs on this head are SUCCESS / 2 SKIPPED, no other failures.

@dbarker dbarker left a comment

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.

Thanks for the cleanup. Please see comments below. The main question is: Can the truncation be applied on creation of the owned type instead of after in order to prevent allocating the overflowing value?

Comment thread sdk/src/logs/read_write_log_record.cc Outdated
Comment thread sdk/src/logs/read_write_log_record.cc Outdated
Comment thread exporters/otlp/src/otlp_populate_attribute_utils.cc Outdated
Comment thread exporters/otlp/src/otlp_populate_attribute_utils.cc Outdated
Comment thread sdk/include/opentelemetry/sdk/common/attribute_utils.h Outdated
@ThomsonTan

Copy link
Copy Markdown
Contributor

The introduced limit changes the current default behavior, a default-constructed ReadWriteLogRecord / OtlpLogRecordable now enforces the 128 attribute cap even when no limits were configured and the logger wiring path was never used. Suggest iither make "no explicit SetLogRecordLimits = unlimited" the recordable default and let the wiring layer inject 128, or keep the spec default but call this out in CHANGELOG as a breaking change.

ThomsonTan and others added 3 commits June 25, 2026 10:06
CreateLogRecord() called SetLogRecordLimits() on every record
unconditionally, paying a virtual dispatch even for recordables that do
not enforce limits. Add a RecordableEnforcesLogRecordLimits() capability
query on LogRecordExporter and LogRecordProcessor (default false; OTLP
and ostream exporters return true; Simple and Batch processors delegate
to their exporter; Multi returns true if any child does). LoggerContext
caches the result at construction and refreshes it in AddProcessor, so
the Logger hot path reads a plain bool and only calls SetLogRecordLimits()
when a processor actually enforces limits.

The new virtuals are appended at the end of their vtables to keep the
change additive.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The in-memory AttributeConverter cut string values at a raw byte
boundary, which can split a multi-byte UTF-8 sequence and leave an
invalid std::string. Move Utf8SafePrefixLength from the OTLP recordable
into sdk::common so the converter can share it, and truncate the
std::string, string_view, const char*, and string-array alternatives at a
UTF-8 code-point boundary. Raw bytes (span<const uint8_t>) keep the exact
byte cut since they carry no encoding. The OTLP helper now forwards to the
shared implementation, leaving its public surface and tests unchanged.

Also fold in review nits: delegate the const char* overload to the
string_view overload to avoid copying an oversized C string before
truncating, drop a redundant pointer cast in the bytes overload, and use
std::strlen.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@thc1006

thc1006 commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

BTW the v+num is Number of fixed commit (revisions)

A recent merge of main lowered the abiv1-preview and abiv2-preview
clang-tidy ceilings to 331 and 341. Two warnings need addressing to stay
within them:

- Initialize LoggerContext::recordable_enforces_limits_ in the member
  initializer list instead of the constructor body
  (cppcoreguidelines-prefer-member-initializer).
- Move the rvalue reference parameter in the test TrackingProcessor
  OnEmit stub (cppcoreguidelines-rvalue-reference-param-not-moved).

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@thc1006

thc1006 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Good catch, you're right. A default-constructed ReadWriteLogRecord / OtlpLogRecordable starts with limits_{} = {128, unlimited}, so it caps at 128 even when no provider wired it up. That goes against the intent that these limits are provider-level.

I'll go with option 1: the recordable defaults to no limits, and the wiring injects the configured ones. This lines up with the RecordableEnforcesLogRecordLimits gating in the latest revision: LoggerContext keeps the spec default ({128, unlimited}) and the Logger injects it through SetLogRecordLimits only when a processor enforces. The result:

  • a bare recordable, or one behind a non-enforcing processor: no limits, matching the pre-PR behavior.
  • a recordable created through the standard provider path: the spec default of 128 (or whatever the provider was configured with) applies.

Concretely I'll default limits_ to unbounded in both recordables, keep the LoggerContext default at the spec values, and update the DefaultRecordEnforcesSpecCountCap test to match. The CHANGELOG will describe the provider-path enforcement as the new behavior.

Happy to do option 2 instead (keep the spec default on the recordable and just document it) if you'd rather, but option 1 looks closer to the provider-level intent.

thc1006 and others added 2 commits June 26, 2026 12:29
…rovider

A default-constructed ReadWriteLogRecord / OtlpLogRecordable previously
carried the spec-default LogRecordLimits (count 128), so a recordable used
without any LoggerProvider wiring enforced the 128 cap. That changed
behavior for code that constructs a recordable directly. Default the
recordable to no limits and let the LoggerProvider wiring inject the
configured limits through SetLogRecordLimits, which it already does only
when a processor enforces them. LoggerContext keeps the spec default, so a
record created through the standard provider path still gets the 128 cap
(or the configured value), while a bare recordable no longer caps on its
own.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

@dbarker dbarker left a comment

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.

Thanks for the revision. Please see feedback below. Given the changes please review the code comments and update if needed.

Comment thread sdk/test/logs/log_record_limits_test.cc
Comment thread sdk/include/opentelemetry/sdk/logs/recordable.h Outdated
Comment thread sdk/include/opentelemetry/sdk/logs/read_write_log_record.h Outdated
Comment thread sdk/include/opentelemetry/sdk/logs/exporter.h Outdated
Comment thread exporters/otlp/src/otlp_populate_attribute_utils.cc
thc1006 and others added 2 commits June 27, 2026 12:33
…shortcut, test)

- Correct the SetLogRecordLimits comments on the base Recordable,
  ReadWriteLogRecord, and OtlpLogRecordable: the limits are copied, so the
  caller does not need to keep the supplied object alive (the previous
  "must outlive" wording was stale).
- Drop the vtable-append implementation-detail paragraph from the
  RecordableEnforcesLogRecordLimits comments on LogRecordExporter and
  LogRecordProcessor.
- Short-circuit Utf8SafePrefixLength when the whole value fits within the
  budget (max_bytes >= size, including the unbounded default), so the
  common path skips the per-byte scan.
- Add a const char* attribute value truncation test.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>

@dbarker dbarker left a comment

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.

Looks good to me. Thanks. Approved, but we need to hold on merging until benchmarks are added and compared with main.

Please run the otlp_recordable_benchmark to verify changes to the attribute utils did not introduce a regression for the otlp span recording path. There is no benchmark for log recordables now. At least the oltp log recordable benchmark needs to be added in a separate PR.

Comment thread sdk/include/opentelemetry/sdk/logs/recordable.h Outdated
@dbarker dbarker added the pr:do-not-merge This PR is not ready to be merged. label Jun 30, 2026
Remove the second paragraph of the Recordable::SetLogRecordLimits doc
comment (the vtable-append note) per review. It is an implementation
detail and matches the paragraphs already dropped from the exporter and
processor headers.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@thc1006 thc1006 requested a review from dbarker June 30, 2026 07:54
@thc1006

thc1006 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Heads up: the red W3C Distributed Tracing Validation V1 check is a flaky harness timeout, not the change. The job log shows w3c_tracecontext_http_test_server left as an orphan process and force-terminated at the 5-minute mark with no test output, while V2 (the same validation, newer spec) passed in the same run. The diff in the latest commit is a comment-only removal in recordable.h, which cannot affect trace-context propagation; the prior commit 0ac2cf3f was 68/0 green. A re-run should clear it whenever you next look at this.

@owent owent left a comment

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.

LGTM on my end. Given the scope, let's get one more reviewer to double-check before merging.

@lalitb lalitb left a comment

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.

LGTM from my side. I agree with @dbarker we should at least compare the existing otlp_recordable_benchmark against main for this PR, since the shared attribute utils changed. A dedicated otlp_log_recordable_benchmark can be a follow-up PR.

@thc1006

thc1006 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Ran otlp_recordable_benchmark against main before merge, per your note.

Built it from both main (a56f0e2) and this PR in Release with identical flags. The benchmark source is byte-identical on both refs, so the only variable is the attribute-utils change. I confirmed the two binaries differ, then ran them interleaved and order-balanced across 20 paired rounds, reporting the paired per-round delta with a 95% CI (pairing cancels per-round environment drift):

benchmark PR vs main (95% CI)
Nominal (6 attrs) -0.8% ± 2.0%
MaxAttributes (128 attrs) +0.7% ± 2.2%
PopulateRequest, 512 spans -0.2% ± 1.9%

Every interval spans zero, so no measurable regression on the span recording path. MaxAttributes, which exercises the attribute conversion hardest, is flat. This matches the code: OtlpRecordable::SetAttribute uses the default unbounded max_length, so Utf8SafePrefixLength takes its if (max_bytes >= size) return size; early-out and inlines away (no call sites in the binary), leaving one size comparison per attribute.

I'll add the dedicated otlp_log_recordable_benchmark you mentioned as a separate follow-up PR.

(Local Release build in the otelcpp-dev image.)

@dbarker dbarker removed the pr:do-not-merge This PR is not ready to be merged. label Jul 1, 2026
Copilot AI review requested due to automatic review settings July 1, 2026 18:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Implements OpenTelemetry Logs SDK LogRecord attribute limits (count + value length) end-to-end: introduces a runtime LogRecordLimits type, wires limits from LoggerProvider/LoggerContext into each newly created recordable, and enforces limits in both in-memory (ReadWriteLogRecord) and OTLP (OtlpLogRecordable) recordables. Also adds UTF-8-safe truncation utilities in the common attribute conversion layer and expands test coverage across SDK and OTLP exporter paths.

Changes:

  • Add LogRecordLimits (spec defaults) and wire it through LoggerProviderFactoryLoggerContextLogger::CreateLogRecord()Recordable::SetLogRecordLimits().
  • Enforce attribute count dropping and value-length truncation in ReadWriteLogRecord and OtlpLogRecordable, including proto dropped_attributes_count.
  • Add/extend unit tests and register new tests in both CMake and Bazel builds.

Reviewed changes

Copilot reviewed 37 out of 37 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
sdk/test/logs/log_record_limits_test.cc New SDK tests for count/length enforcement and logger wiring.
sdk/test/logs/CMakeLists.txt Registers new log_record_limits_test.
sdk/test/logs/BUILD Adds Bazel target for log_record_limits_test.
sdk/test/common/attribute_utils_test.cc Adds tests for UTF-8-safe truncation behavior in AttributeConverter.
sdk/src/logs/read_write_log_record.cc Enforces count/length limits in in-memory log record attributes.
sdk/src/logs/multi_recordable.cc Propagates SetLogRecordLimits to wrapped recordables.
sdk/src/logs/multi_log_record_processor.cc Aggregates “enforces limits” capability across processors.
sdk/src/logs/logger.cc Pushes limits onto recordables on creation when needed.
sdk/src/logs/logger_provider_factory.cc Adds Create(..., LogRecordLimits) overload.
sdk/src/logs/logger_context.cc Stores limits and caches whether recordables enforce them.
sdk/src/configuration/sdk_builder.cc Maps declarative LogRecordLimitsConfiguration to runtime limits.
sdk/include/opentelemetry/sdk/logs/simple_log_record_processor.h Delegates “enforces limits” capability to exporter.
sdk/include/opentelemetry/sdk/logs/recordable.h Adds no-op default SetLogRecordLimits hook.
sdk/include/opentelemetry/sdk/logs/readable_log_record.h Adds default GetDroppedAttributesCount() API.
sdk/include/opentelemetry/sdk/logs/read_write_log_record.h Declares limits/drop-count support for ReadWriteLogRecord.
sdk/include/opentelemetry/sdk/logs/processor.h Adds RecordableEnforcesLogRecordLimits() capability flag.
sdk/include/opentelemetry/sdk/logs/multi_recordable.h Declares SetLogRecordLimits propagation API.
sdk/include/opentelemetry/sdk/logs/multi_log_record_processor.h Declares aggregated capability method.
sdk/include/opentelemetry/sdk/logs/logger_provider_factory.h Declares factory overload taking LogRecordLimits.
sdk/include/opentelemetry/sdk/logs/logger_context.h Adds limits storage and capability accessor APIs.
sdk/include/opentelemetry/sdk/logs/log_record_limits.h New header defining LogRecordLimits defaults + NoLimits().
sdk/include/opentelemetry/sdk/logs/exporter.h Adds exporter capability method for limit enforcement.
sdk/include/opentelemetry/sdk/logs/batch_log_record_processor.h Delegates “enforces limits” capability to exporter.
sdk/include/opentelemetry/sdk/common/attribute_utils.h Adds UTF-8-safe prefix helper and truncating AttributeConverter.
exporters/otlp/test/otlp_populate_attribute_utils_test.cc New tests for UTF-8-safe prefix behavior in OTLP utils.
exporters/otlp/test/otlp_log_recordable_test.cc Adds OTLP log recordable tests for count/length enforcement.
exporters/otlp/src/otlp_populate_attribute_utils.cc Adds max-length truncation support to OTLP AnyValue population.
exporters/otlp/src/otlp_log_recordable.cc Enforces limits in OTLP log recordable and sets dropped count.
exporters/otlp/include/opentelemetry/exporters/otlp/otlp_populate_attribute_utils.h Extends APIs with max-length and documents truncation semantics.
exporters/otlp/include/opentelemetry/exporters/otlp/otlp_log_recordable.h Adds SetLogRecordLimits override + stored limits member.
exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_log_record_exporter.h Marks OTLP HTTP exporter recordable as enforcing limits.
exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_log_record_exporter.h Marks OTLP gRPC exporter recordable as enforcing limits.
exporters/otlp/include/opentelemetry/exporters/otlp/otlp_file_log_record_exporter.h Marks OTLP file exporter recordable as enforcing limits.
exporters/otlp/CMakeLists.txt Adds otlp_populate_attribute_utils_test executable.
exporters/otlp/BUILD Adds Bazel target for otlp_populate_attribute_utils_test.
exporters/ostream/include/opentelemetry/exporters/ostream/log_record_exporter.h Marks ostream exporter recordable as enforcing limits.
CHANGELOG.md Documents the new LogRecord limits enforcement feature.

Comment thread sdk/test/logs/log_record_limits_test.cc
Comment thread exporters/otlp/src/otlp_log_recordable.cc
@marcalff marcalff merged commit 1accbc0 into open-telemetry:main Jul 3, 2026
122 of 123 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement LogRecord limits

8 participants