Skip to content

Commit c7e5c7a

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 b8687d0 commit c7e5c7a

File tree

15 files changed

+1049
-210
lines changed

15 files changed

+1049
-210
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.26.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
@@ -178,3 +182,4 @@ When in doubt, implement rather than import.
178182
12. Generators must be deterministic - no randomness without explicit seeding
179183
13. Pre-compute in initialization, not in hot paths
180184
14. Think about how your code affects the measurement of the target
185+
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::templates::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(crate) 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: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
1-
//! Tag generation for OpenTelemetry metric payloads
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;
6+
27
use std::{cmp, rc::Rc};
38

4-
use super::templates::GeneratorError;
5-
use crate::{Error, Generator, common::config::ConfRange, common::strings::Pool};
9+
use crate::{Error, Generator, SizedGenerator, common::config::ConfRange, common::strings::Pool};
610
use opentelemetry_proto::tonic::common::v1::{AnyValue, KeyValue, any_value};
711
use prost::Message;
812

13+
/// Errors that can occur during generation
14+
#[derive(thiserror::Error, Debug, Clone, Copy)]
15+
pub enum GeneratorError {
16+
#[error("Generator exhausted bytes budget prematurely")]
17+
SizeExhausted,
18+
#[error("Failed to generate string")]
19+
StringGenerate,
20+
}
21+
22+
/// Ratio of unique tags to use in tag generation
23+
pub(crate) const UNIQUE_TAG_RATIO: f32 = 0.75;
24+
25+
/// Smallest useful `KeyValue` protobuf, determined by experimentation and enforced in tests
26+
pub(crate) const SMALLEST_KV_PROTOBUF: usize = 10;
27+
28+
/// Tag generator for OpenTelemetry attributes
929
#[derive(Debug, Clone)]
1030
pub(crate) struct TagGenerator {
1131
inner: crate::common::tags::Generator,
1232
}
1333

14-
// smallest useful protobuf, determined by experimentation and enforced in
15-
// smallest_kv_protobuf test
16-
const SMALLEST_KV_PROTOBUF: usize = 10;
17-
1834
impl TagGenerator {
1935
/// Creates a new tag generator
2036
///
@@ -43,7 +59,8 @@ impl TagGenerator {
4359
}
4460
}
4561

46-
fn varint_len(v: usize) -> usize {
62+
/// Calculate the length of a varint encoding
63+
pub(crate) fn varint_len(v: usize) -> usize {
4764
let mut v = v;
4865
let mut n = 1;
4966
while v > 0x7f {
@@ -53,14 +70,15 @@ fn varint_len(v: usize) -> usize {
5370
n
5471
}
5572

56-
fn overhead(v: usize) -> usize {
73+
/// Calculate the overhead for a `KeyValue` in a repeated field
74+
pub(crate) fn overhead(v: usize) -> usize {
5775
// overhead in a repeated field is per-item, so:
5876
//
5977
// [tag-byte] [varint-length] [kv-bytes…]
6078
varint_len(v) + 1 + v
6179
}
6280

63-
impl<'a> crate::SizedGenerator<'a> for TagGenerator {
81+
impl<'a> SizedGenerator<'a> for TagGenerator {
6482
type Output = Vec<KeyValue>;
6583
type Error = GeneratorError;
6684

@@ -143,11 +161,11 @@ mod test {
143161
}),
144162
};
145163

146-
let encoded_size = overhead(kv.encoded_len());
164+
let sz = overhead(kv.encoded_len());
147165

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

0 commit comments

Comments
 (0)