Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
76 changes: 51 additions & 25 deletions templates/code/rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,26 @@ Keep the consuming crate's files outside that directory.

## Generated API

The root `mod.rs` carries `SCHEMA_URL` and declares the generated modules.
Each attribute namespace lives in a sibling `<namespace>attr.rs` file, alongside
the signal modules. Names use the first dotted
segment as their namespace; `vendor_prefixes` removes a leading vendor segment
before choosing that namespace.

| Module | API |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| `<namespace>attr` | Attribute newtypes and enum variants, `KEY` and conversions into `KeyValue` or log `AnyValue`. |
| `options` | Typed instrumentation options containing scope configuration. |
| `scope` | Scope attributes configuration; package identity is fixed. |
| `meter` | Schema-aware `Meter`, with `new`, `inner`, and a no-op `Default`. |
| `tracer` | Schema-aware `Tracer`, with `new`, `inner`, and a no-op `Default`. |
| `logger` | Schema-aware generic `Logger`, with `new`, `inner`, and a no-op `Default`. |
| `<namespace>metric` | Synchronous instruments and observable counters, up-down counters, and gauges. |
| `<namespace>span` | Span starters, typed names, start attributes, and attributes allowed after starting. |
| `<namespace>event` | Typed log event emitters and options for attributes, severity, and timestamp. |
| `<namespace>entity` | Typed entity values, attribute iterators, and resources carrying the registry schema URL. |
The root `mod.rs` carries `SCHEMA_URL` and declares the generated modules. The
layout mirrors the `opentelemetry-semantic-conventions` crate: one flat module
per signal, holding every namespace. Each generated type and function name
starts with its namespace, the first dotted segment of the registry name;
`vendor_prefixes` removes a leading vendor segment before choosing that
namespace. For example, `myapp.task.id` becomes `attribute::MyappTaskId`,
and the `myapp.task.run` span starter becomes `trace::start_myapp_task_run`.

| Module | API |
| ----------- | ---------------------------------------------------------------------------------------------- |
| `attribute` | Attribute newtypes and enum variants, `KEY` and conversions into `KeyValue` or log `AnyValue`. |
| `options` | Typed instrumentation options containing scope configuration. |
| `scope` | Scope attributes configuration; package identity is fixed. |
| `meter` | Schema-aware `Meter`, with `new`, `inner`, and a no-op `Default`. |
| `tracer` | Schema-aware `Tracer`, with `new`, `inner`, and a no-op `Default`. |
| `logger` | Schema-aware generic `Logger`, with `new`, `inner`, and a no-op `Default`. |
| `metric` | Synchronous instruments and observable counters, up-down counters, and gauges. |
| `trace` | Span starters, typed names, start attributes, and attributes allowed after starting. |
| `event` | Typed log event emitters and options for attributes, severity, and timestamp. |
| `resource` | Typed entity values, attribute iterators, and resources carrying the registry schema URL. |

Handle modules are emitted when the registry includes their corresponding
signals. Their `new(provider, options)` constructor takes the exact
Expand Down Expand Up @@ -83,16 +85,16 @@ Attribute keys, metric names, and event names retain their full registry names.
### Typed attributes

An attribute such as `myapp.task.id` becomes
`myappattr::TaskIdAttr::from("task-1")`. Convert the attribute into an OpenTelemetry `KeyValue` with
`attribute::MyappTaskId::from("task-1")`. Convert the attribute into an OpenTelemetry `KeyValue` with
`KeyValue::from(attribute)` or `attribute.into()`. String attributes implement
`From<String>`, `From<&'static str>`, and `From<opentelemetry::StringValue>`.
Static strings retain their static storage. Own a dynamic borrowed string
explicitly, such as `Attr::from(text.to_owned())`. Scalar and array attributes
explicitly, such as `MyappTaskId::from(text.to_owned())`. Scalar and array attributes
implement `From` for their backing types; string arrays accept `Vec<String>`
and native `Vec<opentelemetry::StringValue>`. Native storage moves into the SDK
without rebuilding the vector. Use `Attr::from(value)` or `value.into()` when
without rebuilding the vector. Use `MyappTaskId::from(value)` or `value.into()` when
the attribute type is known. Enums expose named variants,
such as `myappattr::TaskStateAttr::Running`, rather than arbitrary constructors.
such as `attribute::MyappTaskState::Running`, rather than arbitrary constructors.

Supported backing types are `StringValue`, `i64`, `f64`, `bool`, and their `Vec`
forms. Enums can have string, integer, fractional, or boolean values.
Expand All @@ -103,14 +105,14 @@ formatting. Boolean, integer, floating-point and enum attributes are `Copy`.
Deprecation metadata becomes Rust deprecation annotations.

Attributes also convert directly into `opentelemetry::logs::AnyValue` through
`From`. Generated event emitters pass `Attr::KEY` and the typed attribute to
`From`. Generated event emitters pass the attribute type's `KEY` and the typed attribute to
the SDK log record.

### Metrics

Each metric becomes a typed instrument with a `new(&meter)` constructor and
an `add` or `record` method. Required attributes are typed arguments. Other
attributes are variants of the instrument's `Attr` enum, passed as an iterator.
attributes are variants of the metric's `Attr` enum, such as `metric::MyappTaskDurationAttr`, passed as an iterator.
Observable constructors register a callback whose typed observer has the same
required arguments and optional attribute contract.

Expand Down Expand Up @@ -203,12 +205,36 @@ params:
error: upstream
```

That mapping refers to `upstream::errorattr` in the consuming crate. Both
That mapping refers to `upstream::attribute` in the consuming crate, so
`error.type` becomes `upstream::attribute::ErrorType`. Both
registries must use compatible namespace and vendor-prefix settings.
Plain imported strings use `opentelemetry::StringValue`, and string arrays use
`Vec<opentelemetry::StringValue>`, preserving their SDK storage. Convert a static
string with `.into()` or `StringValue::from` at the call site.

### Signal metadata

Signal types carry their registry metadata as associated constants, usable in
`const` contexts. The generated code reads these constants, so each value
appears once.

| Type | Constants |
| ----------------------------------- | ---------------------------------------------------------------- |
| `metric::MyappTaskDuration` | `NAME`, `UNIT`, `DESCRIPTION` |
| `metric::MyappQueueDepthObservable` | `NAME`, `UNIT`, `DESCRIPTION`, defined as the synchronous type's |
| `trace::MyappTaskRun` | `NAME`, the span type |
| `event::MyappTaskFinished` | `NAME`, the event name |
| `resource::MyappHost` | `NAME`, the entity type |

`UNIT` and `DESCRIPTION` are empty strings when the registry leaves them unset.
Refinements report the type they refine. Each event has a unit struct that
holds its `NAME`; emitting still goes through its `emit_` function.

```rust
const DURATION: &str = metric::MyappTaskDuration::NAME;
assert_eq!(metric::MyappTaskDuration::UNIT, "s");
```

## Parameters

| Parameter | Default | Purpose |
Expand Down
4 changes: 3 additions & 1 deletion templates/code/rust/entities.rs.j2
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// Code generated by weaver. DO NOT EDIT.
#![allow(deprecated)]
{% for def in ctx.entities %}
{%- set name = s.exported(def.short_name) ~ "Entity" %}
{%- set name = s.exported(def.qualified) %}
{% if def.optional %}
#[derive(Clone, Debug)]
pub enum {{ name }}Attr {
Expand Down Expand Up @@ -46,6 +46,8 @@ pub struct {{ name }} {
}

impl {{ name }} {
pub const NAME: &'static str = {{ s.literal(def.type) }};

pub fn new({% for req in def.required %}{{ s.ident(req.attr_param) }}: {{ s.attr_type(req) }}, {% endfor %}{% if def.optional %}options: impl IntoIterator<Item = {{ name }}Attr>{% endif %}) -> Self {
Self {
{% for req in def.required %}
Expand Down
16 changes: 12 additions & 4 deletions templates/code/rust/events.rs.j2
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ use ::opentelemetry::trace::TraceContextExt;

{% import "signal_helpers.j2" as h %}
{% for def in ctx.events %}
{% set name = h.exported(def.short_name) or 'Event' %}
{% set name = h.exported(def.qualified) %}
{{ (h.docs(def) ~ "\n" ~ h.deprecated(def)) | trim }}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct {{ name }};

impl {{ name }} {
pub const NAME: &'static str = {{ h.literal(def.name) }};
}

#[derive(Clone, Debug)]
pub enum {{ name }}Option {
Severity(::opentelemetry::logs::Severity),
Expand All @@ -18,7 +26,7 @@ pub enum {{ name }}Option {
{% endfor %}
}
{{ (h.docs(def) ~ "\n" ~ h.deprecated(def)) | trim }}
pub fn {{ h.signal_fn('emit', def.short_name) }}<L: ::opentelemetry::logs::Logger>(context: &::opentelemetry::Context, logger: &super::logger::Logger<L>{% for req in def.required %}, {{ h.ident(req.attr_param) }}: {{ h.attr_type(req) }}{% endfor %}, options: impl IntoIterator<Item = {{ name }}Option>) {
pub fn {{ h.signal_fn('emit', def.qualified) }}<L: ::opentelemetry::logs::Logger>(context: &::opentelemetry::Context, logger: &super::logger::Logger<L>{% for req in def.required %}, {{ h.ident(req.attr_param) }}: {{ h.attr_type(req) }}{% endfor %}, options: impl IntoIterator<Item = {{ name }}Option>) {
let mut severity = ::opentelemetry::logs::Severity::Info;
let mut timestamp = None;
{% if def.optional %}
Expand All @@ -33,10 +41,10 @@ pub fn {{ h.signal_fn('emit', def.short_name) }}<L: ::opentelemetry::logs::Logge
{% endif %}
}
}
if !logger.inner().event_enabled(severity, "", Some({{ h.literal(def.name) }})) { return; }
if !logger.inner().event_enabled(severity, "", Some({{ name }}::NAME)) { return; }
let _context_guard = context.clone().attach();
let mut record = logger.inner().create_log_record();
record.set_event_name({{ h.literal(def.name) }});
record.set_event_name({{ name }}::NAME);
record.set_severity_number(severity);
record.set_observed_timestamp(::std::time::SystemTime::now());
if let Some(timestamp) = timestamp { record.set_timestamp(timestamp); }
Expand Down
24 changes: 16 additions & 8 deletions templates/code/rust/metrics.rs.j2
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{% import "signal_helpers.j2" as h %}
{% for def in ctx.metrics %}
{% set suffix = def.instrument | map_text("metric_instrument") %}
{% set name = h.exported(def.short_name) ~ suffix %}
{% set name = h.exported(def.qualified) %}
{% set method = 'add' if def.instrument in ['counter','updowncounter'] else 'record' %}
{% set value_type = 'f64' if def.annotations.code_generation.metric_value_type == 'double' else ('i64' if def.instrument in ['updowncounter','gauge'] else 'u64') %}
{% set builder = value_type ~ '_' ~ ('up_down_counter' if def.instrument == 'updowncounter' else def.instrument) %}
Expand Down Expand Up @@ -35,10 +35,14 @@ pub struct {{ name }} {
}

impl {{ name }} {
pub const NAME: &'static str = {{ h.literal(def.instrument_name) }};
pub const UNIT: &'static str = {{ h.literal(def.unit or '') }};
pub const DESCRIPTION: &'static str = {{ h.literal((def.description or '') | trim) }};

pub fn new(meter: &super::meter::Meter) -> Self {
let instrument = meter.inner().{{ builder }}({{ h.literal(def.instrument_name) }})
.with_description({{ h.literal(def.description | trim) }})
.with_unit({{ h.literal(def.unit) }})
let instrument = meter.inner().{{ builder }}(Self::NAME)
.with_description(Self::DESCRIPTION)
.with_unit(Self::UNIT)
{% if def.bucket_boundaries %}
.with_boundaries(vec![{% for bound in def.bucket_boundaries %}{{ bound }}_f64{% if not loop.last %}, {% endif %}{% endfor %}])
{% endif %}
Expand All @@ -63,7 +67,7 @@ impl Default for {{ name }} {
fn default() -> Self { Self::new(&super::meter::Meter::default()) }
}
{% if def.instrument != 'histogram' %}
{% set observable = h.exported(def.short_name) ~ 'Observable' ~ suffix %}
{% set observable = name ~ 'Observable' %}
pub struct {{ name }}Observer<'a> {
observer: &'a dyn ::opentelemetry::metrics::AsyncInstrument<{{ value_type }}>,
}
Expand All @@ -89,10 +93,14 @@ pub struct {{ observable }} {
}

impl {{ observable }} {
pub const NAME: &'static str = {{ name }}::NAME;
pub const UNIT: &'static str = {{ name }}::UNIT;
pub const DESCRIPTION: &'static str = {{ name }}::DESCRIPTION;

pub fn new(meter: &super::meter::Meter, callback: impl Fn({{ name }}Observer<'_>) + Send + Sync + 'static) -> Self {
let instrument = meter.inner().{{ value_type }}_observable_{{ 'up_down_counter' if def.instrument == 'updowncounter' else def.instrument }}({{ h.literal(def.instrument_name) }})
.with_description({{ h.literal(def.description | trim) }})
.with_unit({{ h.literal(def.unit) }})
let instrument = meter.inner().{{ value_type }}_observable_{{ 'up_down_counter' if def.instrument == 'updowncounter' else def.instrument }}(Self::NAME)
.with_description(Self::DESCRIPTION)
.with_unit(Self::UNIT)
.with_callback(move |observer| callback({{ name }}Observer { observer }))
.build();
Self { _instrument: instrument }
Expand Down
6 changes: 3 additions & 3 deletions templates/code/rust/mod.rs.j2
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
// Regenerate: {{ params.regenerate_command | replace("\n", " ") | replace("\r", " ") }}
{% endif %}
pub const SCHEMA_URL: &str = {{ ctx.schema_literal }};
{% for namespace in ctx.namespaces %}
pub mod {{ namespace }};
{% endfor %}
{% if ctx.has_attributes %}
pub mod attribute;
{% endif %}

{% if ctx.has_metrics or ctx.has_spans or ctx.has_events %}
pub mod scope;
Expand Down
2 changes: 1 addition & 1 deletion templates/code/rust/signal_helpers.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{% macro signal_fn(prefix, short_name) -%}{{ ident(prefix ~ ('.' ~ short_name if short_name else '')) }}{%- endmacro %}
{% macro attr_type(a) -%}
{%- if a.imported -%}{{ "::opentelemetry::StringValue" if a.attr_type == "string" else ("Vec<::opentelemetry::StringValue>" if a.attr_type == "string[]" else a.attr_type | map_text("rust_type")) }}
{%- else -%}{{ a.attr_import_path if a.attr_import_path else 'super::' ~ a.attr_namespace ~ 'attr' }}::{{ exported(a.attr_short_name) }}Attr
{%- else -%}{{ a.attr_import_path if a.attr_import_path else 'super::attribute' }}::{{ exported(a.attr_param) }}
{%- endif -%}
{%- endmacro %}
{% macro attr_kv(a, value) -%}
Expand Down
18 changes: 11 additions & 7 deletions templates/code/rust/spans.rs.j2
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{%- filter trim %}
#![allow(deprecated)]
use ::opentelemetry::trace::{TraceContextExt, Tracer};
use ::opentelemetry::trace::{TraceContextExt as _, Tracer as _};
{% macro needs_name_formatting(definitions) -%}
{%- for definition in definitions -%}
{%- for part in definition.name_parts or [] -%}
Expand All @@ -9,7 +9,7 @@ use ::opentelemetry::trace::{TraceContextExt, Tracer};
{%- endfor -%}
{%- endmacro %}
{% if needs_name_formatting(ctx.spans) %}
use ::std::fmt::Write;
use ::std::fmt::Write as _;
{% endif %}

{% import "signal_helpers.j2" as h %}
Expand All @@ -32,8 +32,8 @@ use ::std::fmt::Write;
{%- endif -%}
{%- endmacro %}
{% for def in ctx.spans %}
{% set name = h.exported(def.short_name) %}
{% set span = name ~ 'Span' %}
{% set name = h.exported(def.qualified) %}
{% set span = name %}
{% if def.optional %}
#[derive(Clone, Debug)]
pub enum {{ name }}StartAttr {
Expand Down Expand Up @@ -112,13 +112,17 @@ impl<S: ::opentelemetry::trace::Span> {{ span }}<S> {
{% endif %}
}

impl {{ span }} {
pub const NAME: &'static str = {{ h.literal(def.type) }};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Span name constant not generic

Medium Severity

NAME is declared on impl {{ span }}, but the span type is generic with a default of NoopSpan. That inherent impl applies only to the no-op instantiation, so NAME is missing from the start_* return type once a real tracer span is substituted.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4f79d65. Configure here.


impl Default for {{ span }} {
fn default() -> Self {
Self { span: super::tracer::Tracer::default().inner().start({{ h.literal(def.type) }}) }
Self { span: super::tracer::Tracer::default().inner().start(Self::NAME) }
}
}
{{ h.deprecated(def) }}
pub fn {{ h.signal_fn('start', def.short_name) }}<T: ::opentelemetry::trace::Tracer>(context: &::opentelemetry::Context, tracer: &super::tracer::Tracer<T>{% if not def.name_parts %}, name: {{ name }}Name{% endif %}{% for req in def.required %}, {{ h.ident(req.attr_param) }}: {{ h.attr_type(req) }}{% endfor %}{% if def.optional %}, options: impl IntoIterator<Item = {{ name }}StartAttr>{% endif %}) -> {{ span }}<T::Span> {
pub fn {{ h.signal_fn('start', def.qualified) }}<T: ::opentelemetry::trace::Tracer>(context: &::opentelemetry::Context, tracer: &super::tracer::Tracer<T>{% if not def.name_parts %}, name: {{ name }}Name{% endif %}{% for req in def.required %}, {{ h.ident(req.attr_param) }}: {{ h.attr_type(req) }}{% endfor %}{% if def.optional %}, options: impl IntoIterator<Item = {{ name }}StartAttr>{% endif %}) -> {{ span }}<T::Span> {
{% if def.name_parts %}
let mut name = String::with_capacity({% for part in def.name_parts %}{% if not loop.first %} + {% endif %}{% if part.attr %}{{ name_capacity(part.attr, h.ident(part.attr.attr_param)) }}{% else %}{{ h.literal(part.text) }}.len(){% endif %}{% endfor %});
{% for part in def.name_parts %}
Expand All @@ -142,7 +146,7 @@ pub fn {{ h.signal_fn('start', def.short_name) }}<T: ::opentelemetry::trace::Tra
{% endif %}
{% endfor %}
{% else %}
let name = if name.0.is_empty() { ::std::borrow::Cow::Borrowed({{ h.literal(def.type) }}) } else { name.0 };
let name = if name.0.is_empty() { ::std::borrow::Cow::Borrowed({{ span }}::NAME) } else { name.0 };
{% endif %}
let attributes: [::opentelemetry::KeyValue; {{ def.required | length }}] = [{% for req in def.required %}{{ h.attr_kv(req, h.ident(req.attr_param)) }}{% if not loop.last %}, {% endif %}{% endfor %}];
let span = tracer.inner().span_builder(name)
Expand Down
Loading
Loading