Skip to content

Commit cdda888

Browse files
committed
OpenTelemetry logs improvement
This commit updates our OTel logs payload to be configurable by the end user. We allow in a manner similar to metrics for the contexts to be capped, attributes per level of the message format to be set and seprately from the context consideration we allow for generation and constraint of total trace-ids. REF SMPTNG-659 Signed-off-by: Brian L. Troutwine <brian.troutwine@datadoghq.com>
1 parent 17c18e5 commit cdda888

File tree

15 files changed

+1053
-211
lines changed

15 files changed

+1053
-211
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
## Added
9+
- Added configuration surface area to the OTel logs payload generator, in a
10+
manner similar to OTel metrics.
811

912
## [0.27.0]
1013
## Added

CLAUDE.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ When handling errors:
6060
This project enforces code style through automated tooling. Use `ci/validate` to
6161
check style compliance - it will run formatting and linting checks for you.
6262

63+
**Module organization**: Never use `mod.rs` files. Always name modules directly
64+
(e.g., use `foo.rs` instead of `foo/mod.rs`). This makes the codebase easier to
65+
navigate and grep.
66+
6367
We do not allow for warnings: all warnings are errors. Deprecation warnings MUST
6468
be treated as errors. Lading is written in a "naive" style where abstraction is
6569
not preferred if a duplicated pattern will satisfy. Our reasoning for this is it
@@ -183,3 +187,4 @@ When in doubt, implement rather than import.
183187
12. Generators must be deterministic - no randomness without explicit seeding
184188
13. Pre-compute in initialization, not in hot paths
185189
14. Think about how your code affects the measurement of the target
190+
15. NEVER use mod.rs files - always name modules directly (e.g., foo.rs not foo/mod.rs)

integration/sheepdog/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,8 @@ generator:
423423
method:
424424
post:
425425
maximum_prebuild_cache_size_bytes: "8 MiB"
426-
variant: "opentelemetry_logs"
426+
variant:
427+
opentelemetry_logs: {}
427428
headers:
428429
Content-Type: "application/x-protobuf"
429430
"#,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc c936baccbc78a4d1a4c4e3f22ba82ca3c8b3c8e74894e7d64167d8ec10c3eb7c # shrinks to seed = 0, total_contexts = 1, steps = 1
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc 48f21c68f6a47b5f04cbc25d9d9907b9db94c3c1352438d61a636a1e819f5aa1 # shrinks to seed = 0, total_contexts = 1, steps = 1
8+
cc 07dda2b662c8e5b8971ea35f997615743e8be3c2e6604e7fe3ad676ff62a9de4 # shrinks to seed = 6751211249958810060, steps = 2, budget = 1460

lading_payload/src/block.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,15 @@ impl Cache {
332332
let _guard = span.enter();
333333
construct_block_cache_inner(rng, &mut pyld, maximum_block_bytes, total_bytes.get())?
334334
}
335-
crate::Config::OpentelemetryLogs => {
336-
let mut pyld = crate::OpentelemetryLogs::new(&mut rng);
335+
crate::Config::OpentelemetryLogs(config) => {
336+
match config.valid() {
337+
Ok(()) => (),
338+
Err(e) => {
339+
warn!("Invalid OpentelemetryLogs configuration: {}", e);
340+
return Err(Error::InvalidConfig(e));
341+
}
342+
}
343+
let mut pyld = crate::OpentelemetryLogs::new(*config, &mut rng)?;
337344
let span = span!(Level::INFO, "fixed", payload = "otel-logs");
338345
let _guard = span.enter();
339346
construct_block_cache_inner(rng, &mut pyld, maximum_block_bytes, total_bytes.get())?

lading_payload/src/lib.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,14 @@ pub enum Error {
9696
/// See [`prost::EncodeError`]
9797
#[error(transparent)]
9898
ProstEncode(#[from] prost::EncodeError),
99-
/// See [`opentelemetry_metric::templates::PoolError`]
99+
/// See [`opentelemetry::common::PoolError`]
100100
#[error("Unable to choose from pool: {0}")]
101-
Pool(#[from] opentelemetry::metric::templates::PoolError),
101+
Pool(
102+
#[from] opentelemetry::common::templates::PoolError<opentelemetry::common::GeneratorError>,
103+
),
104+
/// Validation error
105+
#[error("Validation error: {0}")]
106+
Validation(String),
102107
}
103108

104109
/// To serialize into bytes
@@ -172,7 +177,7 @@ pub enum Config {
172177
/// Generates OpenTelemetry traces
173178
OpentelemetryTraces,
174179
/// Generates OpenTelemetry logs
175-
OpentelemetryLogs,
180+
OpentelemetryLogs(crate::opentelemetry::log::Config),
176181
/// Generates OpenTelemetry metrics
177182
OpentelemetryMetrics(crate::opentelemetry::metric::Config),
178183
/// Generates `DogStatsD`

lading_payload/src/opentelemetry.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//!
33
//! This module contains payload generators for OpenTelemetry formats.
44
5+
pub mod common;
56
pub mod log;
67
pub mod metric;
78
pub mod trace;

lading_payload/src/opentelemetry/metric/tags.rs renamed to lading_payload/src/opentelemetry/common.rs

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
1-
//! Tag generation for OpenTelemetry metric payloads
2-
use std::{cmp, rc::Rc};
1+
//! Common utilities and types for OpenTelemetry payload generation
2+
//!
3+
//! This module contains shared code used by both metrics and logs implementations.
4+
5+
pub(crate) mod templates;
36

4-
use super::templates::GeneratorError;
5-
use crate::{Error, Generator, common::config::ConfRange, common::strings::Pool};
7+
use crate::{Error, Generator, SizedGenerator, common::config::ConfRange, common::strings::Pool};
68
use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue, any_value};
79
use prost::Message;
10+
use std::{cmp, rc::Rc};
11+
12+
/// Errors that can occur during generation
13+
#[derive(thiserror::Error, Debug, Clone, Copy)]
14+
pub enum GeneratorError {
15+
/// Generator exhausted bytes budget prematurely
16+
#[error("Generator exhausted bytes budget prematurely")]
17+
SizeExhausted,
18+
/// Failed to generate string
19+
#[error("Failed to generate string")]
20+
StringGenerate,
21+
}
22+
23+
/// Ratio of unique tags to use in tag generation
24+
pub(crate) const UNIQUE_TAG_RATIO: f32 = 0.75;
825

26+
/// Smallest useful `KeyValue` protobuf, determined by experimentation and enforced in tests
27+
pub(crate) const SMALLEST_KV_PROTOBUF: usize = 10;
28+
29+
/// Tag generator for OpenTelemetry attributes
930
#[derive(Debug, Clone)]
1031
pub(crate) struct TagGenerator {
1132
inner: crate::common::tags::Generator,
1233
}
1334

14-
// smallest useful protobuf, determined by experimentation and enforced in
15-
// smallest_kv_protobuf test
16-
const SMALLEST_KV_PROTOBUF: usize = 10;
17-
1835
impl TagGenerator {
1936
/// Creates a new tag generator
2037
///
@@ -43,7 +60,8 @@ impl TagGenerator {
4360
}
4461
}
4562

46-
fn varint_len(v: usize) -> usize {
63+
/// Calculate the length of a varint encoding
64+
pub(crate) fn varint_len(v: usize) -> usize {
4765
let mut v = v;
4866
let mut n = 1;
4967
while v > 0x7f {
@@ -53,14 +71,15 @@ fn varint_len(v: usize) -> usize {
5371
n
5472
}
5573

56-
fn overhead(v: usize) -> usize {
74+
/// Calculate the overhead for a `KeyValue` in a repeated field
75+
pub(crate) fn overhead(v: usize) -> usize {
5776
// overhead in a repeated field is per-item, so:
5877
//
5978
// [tag-byte] [varint-length] [kv-bytes…]
6079
varint_len(v) + 1 + v
6180
}
6281

63-
impl<'a> crate::SizedGenerator<'a> for TagGenerator {
82+
impl<'a> SizedGenerator<'a> for TagGenerator {
6483
type Output = Vec<KeyValue>;
6584
type Error = GeneratorError;
6685

@@ -143,11 +162,11 @@ mod test {
143162
}),
144163
};
145164

146-
let encoded_size = overhead(kv.encoded_len());
165+
let sz = overhead(kv.encoded_len());
147166

148-
assert!(
149-
encoded_size == SMALLEST_KV_PROTOBUF,
150-
"Minimal useful request size ({encoded_size}) should be == SMALLEST_KV_PROTOBUF ({SMALLEST_KV_PROTOBUF})"
167+
assert_eq!(
168+
sz, SMALLEST_KV_PROTOBUF,
169+
"Minimal useful key/value pair should have size {SMALLEST_KV_PROTOBUF}, was {sz}"
151170
);
152171
}
153172
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! Template utilities for OpenTelemetry payload generation
2+
3+
use prost::Message;
4+
use rand::{Rng, prelude::*, seq::IteratorRandom};
5+
use std::collections::BTreeMap;
6+
7+
/// Errors related to pool operations
8+
#[derive(thiserror::Error, Debug, Clone, Copy)]
9+
pub enum PoolError<E> {
10+
/// Choice could not be made on empty container
11+
#[error("Choice could not be made on empty container.")]
12+
EmptyChoice,
13+
/// Generation error
14+
#[error("Generation error: {0}")]
15+
Generator(E),
16+
}
17+
18+
/// A pool that stores pre-generated instances indexed by their encoded size
19+
#[derive(Debug, Clone)]
20+
pub(crate) struct Pool<T, G> {
21+
context_cap: u32,
22+
/// key: encoded size; val: templates with that size
23+
by_size: BTreeMap<usize, Vec<T>>,
24+
generator: G,
25+
len: u32,
26+
}
27+
28+
impl<T, G> Pool<T, G>
29+
where
30+
T: Message,
31+
{
32+
/// Build an empty pool that can hold at most `context_cap` templates.
33+
pub(crate) fn new(context_cap: u32, generator: G) -> Self {
34+
Self {
35+
context_cap,
36+
by_size: BTreeMap::new(),
37+
generator,
38+
len: 0,
39+
}
40+
}
41+
42+
/// Return a reference to an item from the pool.
43+
///
44+
/// Instances returned by this function are guaranteed to be of an encoded
45+
/// size no greater than budget. No greater than `context_cap` instances
46+
/// will ever be stored in this structure.
47+
pub(crate) fn fetch<'a, R>(
48+
&'a mut self,
49+
rng: &mut R,
50+
budget: &mut usize,
51+
) -> Result<&'a T, PoolError<G::Error>>
52+
where
53+
R: Rng + ?Sized,
54+
G: crate::SizedGenerator<'a, Output = T>,
55+
G::Error: 'a,
56+
{
57+
// If we are at context cap, search by_size for templates <= budget and
58+
// return a random choice. If we are not at context cap, call
59+
// generator with the budget and then store the result
60+
// for future use in `by_size`.
61+
//
62+
// Size search is in the interval (0, budget].
63+
64+
let upper = *budget;
65+
66+
// Generate new instances until either context_cap is hit or the
67+
// remaining space drops below our lookup interval.
68+
if self.len < self.context_cap {
69+
let mut limit = *budget;
70+
match self.generator.generate(rng, &mut limit) {
71+
Ok(item) => {
72+
let sz = item.encoded_len();
73+
self.by_size.entry(sz).or_default().push(item);
74+
self.len += 1;
75+
}
76+
Err(e) => return Err(PoolError::Generator(e)),
77+
}
78+
}
79+
80+
let (choice_sz, choices) = self
81+
.by_size
82+
.range(..=upper)
83+
.choose(rng)
84+
.ok_or(PoolError::EmptyChoice)?;
85+
86+
let choice = choices.choose(rng).ok_or(PoolError::EmptyChoice)?;
87+
*budget = budget.saturating_sub(*choice_sz);
88+
89+
Ok(choice)
90+
}
91+
}

0 commit comments

Comments
 (0)