Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
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,10 @@ class OtlpLogRecordable final : public opentelemetry::sdk::logs::Recordable
const opentelemetry::sdk::resource::Resource *resource_ = nullptr;
const opentelemetry::sdk::instrumentationscope::InstrumentationScope *instrumentation_scope_ =
nullptr;
// Stored by value so the recordable does not depend on the limits object
// outliving the LoggerContext that supplied it. The default-constructed
// value carries the spec defaults (count=128, length=unlimited).
opentelemetry::sdk::logs::LogRecordLimits limits_{};
};

} // namespace otlp
Expand Down
110 changes: 109 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,113 @@ void OtlpLogRecordable::SetTraceFlags(const opentelemetry::trace::TraceFlags &tr
proto_record_.set_flags(trace_flags.flags());
}

namespace
{

// Byte length of the longest prefix of `value` that fits within `max_bytes`
// without splitting a well-formed UTF-8 multi-byte sequence. A lead byte's
// declared length is only honored when its continuation bytes are present and
// in range (0x80-0xBF); otherwise the lead is treated as a one-byte unit, so
// malformed input degrades to plain byte truncation. This keeps the resulting
// protobuf `string_value` valid UTF-8 when the input was valid UTF-8.
std::size_t Utf8SafePrefixLength(const std::string &value, std::size_t max_bytes) noexcept
Comment thread
dbarker marked this conversation as resolved.
Outdated
{
std::size_t i = 0;
while (i < value.size())
{
const auto lead = static_cast<unsigned char>(value[i]);
std::size_t seq = (lead < 0x80) ? 1
: (lead < 0xC0) ? 1
: (lead < 0xE0) ? 2
: (lead < 0xF0) ? 3
: (lead < 0xF8) ? 4
: 1;
if (seq > 1)
{
if (i + seq > value.size())
{
seq = 1;
}
else
{
for (std::size_t k = 1; k < seq; ++k)
{
const auto continuation = static_cast<unsigned char>(value[i + k]);
if (continuation < 0x80 || continuation > 0xBF)
{
seq = 1;
break;
}
}
}
}
if (i + seq > max_bytes)
{
break;
}
i += seq;
}
return i;
}

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(
Utf8SafePrefixLength(value->string_value(), max_length));
}
return;
}
if (value->value_case() == proto::common::v1::AnyValue::kBytesValue)
{
// Raw bytes are not UTF-8 text, so the byte-length cap applies verbatim.
if (value->bytes_value().size() > max_length)
{
value->mutable_bytes_value()->resize(max_length);
}
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 (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_.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
130 changes: 130 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,135 @@ 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);
}

TEST(OtlpLogRecordable, AttributeValueLengthLimitPreservesUtf8MultiByte)
{
// "h\xC3\xA9llo" is "héllo" in UTF-8; 'é' occupies bytes 1-2. With a 2-byte
// budget the truncator keeps 'h' alone and drops the partial 'é', so the
// resulting protobuf string_value stays valid UTF-8.
opentelemetry::sdk::logs::LogRecordLimits limits;
limits.attribute_value_length_limit = 2;

OtlpLogRecordable rec;
rec.SetLogRecordLimits(limits);
rec.SetAttribute("k", nostd::string_view("h\xC3\xA9llo"));

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

TEST(OtlpLogRecordable, AttributeValueLengthLimitKeepsCompletedUtf8Sequence)
{
// Same input as above, with a 3-byte budget that exactly fits 'h' + 'é'.
opentelemetry::sdk::logs::LogRecordLimits limits;
limits.attribute_value_length_limit = 3;

OtlpLogRecordable rec;
rec.SetLogRecordLimits(limits);
rec.SetAttribute("k", nostd::string_view("h\xC3\xA9llo"));

ASSERT_EQ(rec.log_record().attributes_size(), 1);
EXPECT_EQ(rec.log_record().attributes(0).value().string_value(), "h\xC3\xA9");
}

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

OtlpLogRecordable rec;
rec.SetLogRecordLimits(limits);
const uint8_t bytes_in[] = {0x01, 0x02, 0x03, 0x04, 0x05};
rec.SetAttribute("k", nostd::span<const uint8_t>(bytes_in, 5));

ASSERT_EQ(rec.log_record().attributes_size(), 1);
const auto &b = rec.log_record().attributes(0).value().bytes_value();
ASSERT_EQ(b.size(), 3u);
EXPECT_EQ(static_cast<uint8_t>(b[0]), 0x01);
EXPECT_EQ(static_cast<uint8_t>(b[1]), 0x02);
EXPECT_EQ(static_cast<uint8_t>(b[2]), 0x03);
}

TEST(OtlpLogRecordable, DefaultRecordEnforcesSpecCountCap)
{
// No SetLogRecordLimits() call: the default-constructed LogRecordLimits{}
// carries the spec defaults (count=128, length=unlimited), so the 129th
// distinct key is dropped instead of stored.
OtlpLogRecordable rec;
for (int i = 0; i < 200; ++i)
{
rec.SetAttribute("attr_" + std::to_string(i), static_cast<int64_t>(i));
}
EXPECT_EQ(rec.log_record().attributes_size(), 128);
EXPECT_EQ(rec.log_record().dropped_attributes_count(), 200u - 128u);
}

} // 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
Loading
Loading