Conversation
Greptile SummaryThis PR extends the OTLP usage sink from a single optional backend to an arbitrary number of backends, enabling fan-out of usage data (tokens, costs, latency) to multiple OpenTelemetry-compatible destinations simultaneously (e.g. Grafana Cloud + Datadog). Key changes:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant S as server.rs startup
participant C as UsageConfig otlp list
participant O as OtlpSink new
participant CS as CompositeSink
participant BW as Buffer Worker
S->>C: iterate otlp configs
loop for each enabled OtlpConfig
S->>O: new(otlp_config, tracing_config)
O-->>S: Ok(OtlpSink with name+logger)
S->>CS: sinks.push(Arc::new(otlp_sink))
end
S->>CS: CompositeSink::new(sinks)
S->>BW: buffer.start_worker(composite_sink)
Note over BW,CS: On each flush, CompositeSink fans out write_batch to all OtlpSinks
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/config/observability.rs
Line: 782
Comment:
**Breaking config change — no migration path**
Changing `otlp` from `Option<UsageOtlpConfig>` to `Vec<UsageOtlpConfig>` alters the expected TOML syntax. Any existing deployment that has:
```toml
[observability.usage.otlp]
enabled = true
endpoint = "http://localhost:4317"
```
will now fail to deserialize at startup with a type mismatch, because a TOML inline table (`[section]`) cannot be coerced into a `Vec` — the new syntax requires:
```toml
[[observability.usage.otlp]]
enabled = true
endpoint = "http://localhost:4317"
```
Since `serde` will return an error rather than silently ignoring the old key, this is a hard startup failure for anyone who already has OTLP configured. Consider adding a note in the changelog/PR description, and ideally a custom deserializer or a serde `alias` / migration note so operators aren't surprised.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/cli/server.rs
Line: 300-301
Comment:
**Trait import placed mid-function**
Placing a `use` statement inside the body of a function (rather than at the top of the module or at the start of the enclosing block) is unusual in Rust and can be surprising to readers. The trait import is needed so that `otlp_sink.name()` resolves, but a cleaner place would be at the top of the module alongside the other `#[cfg(feature = "otlp")]` imports (e.g. near where `crate::config::UsageOtlpConfig` and `TracingConfig` are imported in `usage_sink.rs`), or at the very start of the `if let Some(buffer)` block.
```suggestion
#[cfg(feature = "otlp")]
{
use usage_sink::UsageSink as _;
for otlp_config in &config.observability.usage.otlp {
```
Alternatively, expose `name()` as an inherent method on `OtlpSink` (in addition to the trait impl) so no trait import is needed at the call site.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: "Review fix" |
| /// ``` | ||
| #[serde(default)] | ||
| pub otlp: Option<UsageOtlpConfig>, | ||
| pub otlp: Vec<UsageOtlpConfig>, |
There was a problem hiding this comment.
Breaking config change — no migration path
Changing otlp from Option<UsageOtlpConfig> to Vec<UsageOtlpConfig> alters the expected TOML syntax. Any existing deployment that has:
[observability.usage.otlp]
enabled = true
endpoint = "http://localhost:4317"will now fail to deserialize at startup with a type mismatch, because a TOML inline table ([section]) cannot be coerced into a Vec — the new syntax requires:
[[observability.usage.otlp]]
enabled = true
endpoint = "http://localhost:4317"Since serde will return an error rather than silently ignoring the old key, this is a hard startup failure for anyone who already has OTLP configured. Consider adding a note in the changelog/PR description, and ideally a custom deserializer or a serde alias / migration note so operators aren't surprised.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/observability.rs
Line: 782
Comment:
**Breaking config change — no migration path**
Changing `otlp` from `Option<UsageOtlpConfig>` to `Vec<UsageOtlpConfig>` alters the expected TOML syntax. Any existing deployment that has:
```toml
[observability.usage.otlp]
enabled = true
endpoint = "http://localhost:4317"
```
will now fail to deserialize at startup with a type mismatch, because a TOML inline table (`[section]`) cannot be coerced into a `Vec` — the new syntax requires:
```toml
[[observability.usage.otlp]]
enabled = true
endpoint = "http://localhost:4317"
```
Since `serde` will return an error rather than silently ignoring the old key, this is a hard startup failure for anyone who already has OTLP configured. Consider adding a note in the changelog/PR description, and ideally a custom deserializer or a serde `alias` / migration note so operators aren't surprised.
How can I resolve this? If you propose a fix, please make it concise.| #[cfg(feature = "otlp")] | ||
| if let Some(otlp_config) = &config.observability.usage.otlp | ||
| && otlp_config.enabled | ||
| { | ||
| use usage_sink::UsageSink as _; |
There was a problem hiding this comment.
Trait import placed mid-function
Placing a use statement inside the body of a function (rather than at the top of the module or at the start of the enclosing block) is unusual in Rust and can be surprising to readers. The trait import is needed so that otlp_sink.name() resolves, but a cleaner place would be at the top of the module alongside the other #[cfg(feature = "otlp")] imports (e.g. near where crate::config::UsageOtlpConfig and TracingConfig are imported in usage_sink.rs), or at the very start of the if let Some(buffer) block.
| #[cfg(feature = "otlp")] | |
| if let Some(otlp_config) = &config.observability.usage.otlp | |
| && otlp_config.enabled | |
| { | |
| use usage_sink::UsageSink as _; | |
| #[cfg(feature = "otlp")] | |
| { | |
| use usage_sink::UsageSink as _; | |
| for otlp_config in &config.observability.usage.otlp { |
Alternatively, expose name() as an inherent method on OtlpSink (in addition to the trait impl) so no trait import is needed at the call site.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cli/server.rs
Line: 300-301
Comment:
**Trait import placed mid-function**
Placing a `use` statement inside the body of a function (rather than at the top of the module or at the start of the enclosing block) is unusual in Rust and can be surprising to readers. The trait import is needed so that `otlp_sink.name()` resolves, but a cleaner place would be at the top of the module alongside the other `#[cfg(feature = "otlp")]` imports (e.g. near where `crate::config::UsageOtlpConfig` and `TracingConfig` are imported in `usage_sink.rs`), or at the very start of the `if let Some(buffer)` block.
```suggestion
#[cfg(feature = "otlp")]
{
use usage_sink::UsageSink as _;
for otlp_config in &config.observability.usage.otlp {
```
Alternatively, expose `name()` as an inherent method on `OtlpSink` (in addition to the trait impl) so no trait import is needed at the call site.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
No description provided.