From 1e7e8dfd484d13ae8464f581e775f60d4a58462e Mon Sep 17 00:00:00 2001 From: John Coffey Date: Tue, 27 Oct 2026 15:17:10 -0700 Subject: [PATCH] Vendor upstream's opentelemetry-rust fork at the pinned commit --- Cargo.lock | 6 - Cargo.toml | 13 +- crates/common/Cargo.toml | 9 +- vendor/opentelemetry-rust/LICENSE | 201 + vendor/opentelemetry-rust/VENDORED.md | 32 + .../opentelemetry-http/CHANGELOG.md | 184 + .../opentelemetry-http/Cargo.toml | 132 + .../opentelemetry-http/README.md | 81 + .../allowed-external-types.toml | 21 + .../opentelemetry-http/src/lib.rs | 842 ++ .../opentelemetry-otlp/CHANGELOG.md | 687 + .../opentelemetry-otlp/Cargo.toml | 271 + .../opentelemetry-otlp/README.md | 85 + .../allowed-external-types.toml | 22 + .../src/exporter/http/logs.rs | 111 + .../src/exporter/http/metrics.rs | 105 + .../src/exporter/http/mod.rs | 2166 +++ .../src/exporter/http/trace.rs | 112 + .../opentelemetry-otlp/src/exporter/mod.rs | 1083 ++ .../src/exporter/tonic/logs.rs | 150 + .../src/exporter/tonic/metrics.rs | 135 + .../src/exporter/tonic/mod.rs | 1780 +++ .../src/exporter/tonic/trace.rs | 155 + .../opentelemetry-otlp/src/lib.rs | 884 ++ .../opentelemetry-otlp/src/logs.rs | 221 + .../opentelemetry-otlp/src/metric.rs | 409 + .../opentelemetry-otlp/src/retry.rs | 767 ++ .../src/retry_classification.rs | 608 + .../opentelemetry-otlp/src/retry_policy.rs | 70 + .../opentelemetry-otlp/src/span.rs | 397 + .../opentelemetry-otlp/tests/smoke.rs | 243 + .../opentelemetry-proto/CHANGELOG.md | 162 + .../opentelemetry-proto/Cargo.toml | 164 + .../opentelemetry-proto/README.md | 70 + .../opentelemetry-proto/src/lib.rs | 38 + .../opentelemetry-proto/src/proto.rs | 516 + .../opentelemetry.proto.collector.logs.v1.rs | 374 + ...pentelemetry.proto.collector.metrics.v1.rs | 374 + ....proto.collector.profiles.v1development.rs | 377 + .../opentelemetry.proto.collector.trace.v1.rs | 374 + .../tonic/opentelemetry.proto.common.v1.rs | 187 + .../tonic/opentelemetry.proto.logs.v1.rs | 338 + .../tonic/opentelemetry.proto.metrics.v1.rs | 1005 ++ ...ntelemetry.proto.profiles.v1development.rs | 463 + .../tonic/opentelemetry.proto.resource.v1.rs | 26 + .../tonic/opentelemetry.proto.trace.v1.rs | 529 + .../tonic/opentelemetry.proto.tracez.v1.rs | 90 + .../src/proto/tracez.proto | 59 + .../src/transform/common.rs | 212 + .../opentelemetry-proto/src/transform/logs.rs | 525 + .../src/transform/metrics.rs | 334 + .../opentelemetry-proto/src/transform/mod.rs | 16 + .../src/transform/profiles.rs | 1 + .../src/transform/trace.rs | 524 + .../src/transform/tracez.rs | 72 + .../opentelemetry-proto/tests/grpc_build.rs | 265 + .../opentelemetry-proto/tests/json_serde.rs | 2243 +++ .../.gitignore | 1 + .../CHANGELOG.md | 179 + .../Cargo.toml | 75 + .../README.md | 55 + .../scripts/generate-consts-from-spec.sh | 79 + .../templates/registry/rust/attribute.rs.j2 | 26 + .../scripts/templates/registry/rust/lib.rs.j2 | 25 + .../scripts/templates/registry/rust/macros.j2 | 13 + .../templates/registry/rust/metric.rs.j2 | 79 + .../templates/registry/rust/resource.rs.j2 | 36 + .../templates/registry/rust/trace.rs.j2 | 41 + .../templates/registry/rust/weaver.yaml | 46 + .../src/attribute.rs | 11372 ++++++++++++++++ .../src/lib.rs | 25 + .../src/metric.rs | 10734 +++++++++++++++ .../src/resource.rs | 631 + .../src/trace.rs | 755 + .../opentelemetry/CHANGELOG.md | 952 ++ .../opentelemetry/Cargo.toml | 172 + .../opentelemetry/README.md | 144 + .../opentelemetry/allowed-external-types.toml | 9 + .../opentelemetry/benches/anyvalue.rs | 51 + .../opentelemetry/benches/attributes.rs | 128 + .../opentelemetry/benches/baggage.rs | 104 + .../opentelemetry/benches/context_attach.rs | 122 + .../benches/context_suppression.rs | 66 + .../opentelemetry/benches/logs.rs | 85 + .../opentelemetry/benches/metrics.rs | 113 + .../opentelemetry/benches/trace.rs | 184 + .../opentelemetry/src/baggage.rs | 677 + .../opentelemetry/src/common.rs | 761 ++ .../opentelemetry/src/context.rs | 1662 +++ .../opentelemetry/src/context/future_ext.rs | 108 + .../src/global/internal_logging.rs | 227 + .../opentelemetry/src/global/metrics.rs | 82 + .../opentelemetry/src/global/mod.rs | 149 + .../opentelemetry/src/global/propagation.rs | 43 + .../opentelemetry/src/global/trace.rs | 440 + .../opentelemetry/src/lib.rs | 305 + .../opentelemetry/src/logs/logger.rs | 62 + .../opentelemetry/src/logs/mod.rs | 11 + .../opentelemetry/src/logs/noop.rs | 85 + .../opentelemetry/src/logs/record.rs | 230 + .../src/metrics/instruments/counter.rs | 101 + .../src/metrics/instruments/gauge.rs | 104 + .../src/metrics/instruments/histogram.rs | 78 + .../src/metrics/instruments/mod.rs | 372 + .../metrics/instruments/up_down_counter.rs | 110 + .../opentelemetry/src/metrics/meter.rs | 481 + .../opentelemetry/src/metrics/mod.rs | 132 + .../opentelemetry/src/metrics/noop.rs | 92 + .../src/propagation/composite.rs | 286 + .../opentelemetry/src/propagation/mod.rs | 178 + .../src/propagation/text_map_propagator.rs | 77 + .../opentelemetry/src/testing/mod.rs | 3 + .../opentelemetry/src/testing/trace.rs | 36 + .../opentelemetry/src/trace/context.rs | 391 + .../opentelemetry/src/trace/mod.rs | 274 + .../opentelemetry/src/trace/noop.rs | 214 + .../opentelemetry/src/trace/span.rs | 329 + .../opentelemetry/src/trace/span_context.rs | 586 + .../opentelemetry/src/trace/tracer.rs | 445 + .../src/trace/tracer_provider.rs | 58 + .../opentelemetry/src/trace_context.rs | 257 + .../opentelemetry_sdk/CHANGELOG.md | 1165 ++ .../opentelemetry_sdk/Cargo.toml | 348 + .../opentelemetry_sdk/README.md | 115 + .../allowed-external-types.toml | 18 + .../benches/batch_span_processor.rs | 91 + .../benches/bound_instruments.rs | 321 + .../opentelemetry_sdk/benches/context.rs | 193 + .../opentelemetry_sdk/benches/log.rs | 340 + .../opentelemetry_sdk/benches/log_enabled.rs | 83 + .../opentelemetry_sdk/benches/log_exporter.rs | 184 + .../benches/log_processor.rs | 180 + .../opentelemetry_sdk/benches/metric.rs | 356 + .../benches/metrics_counter.rs | 259 + .../benches/metrics_gauge.rs | 88 + .../benches/metrics_histogram.rs | 163 + .../opentelemetry_sdk/benches/span.rs | 218 + .../opentelemetry_sdk/benches/span_builder.rs | 100 + .../opentelemetry_sdk/benches/trace.rs | 109 + .../benches/tracer_creation.rs | 104 + .../opentelemetry_sdk/src/error.rs | 82 + .../opentelemetry_sdk/src/growable_array.rs | 369 + .../opentelemetry_sdk/src/lib.rs | 194 + .../src/logs/batch_log_processor.rs | 1654 +++ .../opentelemetry_sdk/src/logs/export.rs | 162 + .../src/logs/in_memory_exporter.rs | 226 + .../src/logs/log_processor.rs | 240 + .../logs/log_processor_with_async_runtime.rs | 863 ++ .../opentelemetry_sdk/src/logs/logger.rs | 164 + .../src/logs/logger_provider.rs | 1010 ++ .../opentelemetry_sdk/src/logs/mod.rs | 297 + .../opentelemetry_sdk/src/logs/record.rs | 380 + .../src/logs/simple_log_processor.rs | 688 + .../src/metrics/aggregation.rs | 230 + .../opentelemetry_sdk/src/metrics/data/mod.rs | 799 ++ .../opentelemetry_sdk/src/metrics/error.rs | 28 + .../opentelemetry_sdk/src/metrics/exporter.rs | 40 + .../src/metrics/in_memory_exporter.rs | 263 + .../src/metrics/instrument.rs | 804 ++ .../src/metrics/internal/aggregate.rs | 494 + .../metrics/internal/exponential_histogram.rs | 1699 +++ .../src/metrics/internal/histogram.rs | 406 + .../src/metrics/internal/last_value.rs | 330 + .../src/metrics/internal/mod.rs | 922 ++ .../src/metrics/internal/precomputed_sum.rs | 393 + .../src/metrics/internal/sum.rs | 288 + .../src/metrics/manual_reader.rs | 163 + .../opentelemetry_sdk/src/metrics/meter.rs | 803 ++ .../src/metrics/meter_provider.rs | 781 ++ .../opentelemetry_sdk/src/metrics/mod.rs | 6745 +++++++++ .../opentelemetry_sdk/src/metrics/noop.rs | 57 + .../src/metrics/periodic_reader.rs | 1006 ++ .../periodic_reader_with_async_runtime.rs | 538 + .../opentelemetry_sdk/src/metrics/pipeline.rs | 774 ++ .../opentelemetry_sdk/src/metrics/reader.rs | 67 + .../opentelemetry_sdk/src/metrics/test.md | 24 + .../opentelemetry_sdk/src/metrics/view.rs | 54 + .../src/propagation/baggage.rs | 396 + .../opentelemetry_sdk/src/propagation/mod.rs | 6 + .../src/propagation/trace_context.rs | 300 + .../src/resource/attributes.rs | 32 + .../opentelemetry_sdk/src/resource/env.rs | 209 + .../opentelemetry_sdk/src/resource/mod.rs | 557 + .../src/resource/telemetry.rs | 27 + .../opentelemetry_sdk/src/runtime.rs | 275 + .../src/testing/metrics/metric_reader.rs | 57 + .../src/testing/metrics/mod.rs | 4 + .../opentelemetry_sdk/src/testing/mod.rs | 8 + .../src/testing/trace/mod.rs | 3 + .../src/testing/trace/span_exporters.rs | 117 + .../opentelemetry_sdk/src/trace/config.rs | 137 + .../opentelemetry_sdk/src/trace/events.rs | 37 + .../opentelemetry_sdk/src/trace/export.rs | 108 + .../src/trace/id_generator/mod.rs | 36 + .../src/trace/in_memory_exporter.rs | 179 + .../opentelemetry_sdk/src/trace/links.rs | 37 + .../opentelemetry_sdk/src/trace/mod.rs | 608 + .../opentelemetry_sdk/src/trace/provider.rs | 954 ++ .../src/trace/runtime_tests.rs | 158 + .../opentelemetry_sdk/src/trace/sampler.rs | 469 + .../src/trace/sampler/jaeger_remote/mod.rs | 10 + .../trace/sampler/jaeger_remote/rate_limit.rs | 111 + .../src/trace/sampler/jaeger_remote/remote.rs | 86 + .../trace/sampler/jaeger_remote/sampler.rs | 313 + .../jaeger_remote/sampling_strategy.rs | 232 + .../opentelemetry_sdk/src/trace/span.rs | 768 ++ .../opentelemetry_sdk/src/trace/span_limit.rs | 46 + .../src/trace/span_processor.rs | 2361 ++++ .../span_processor_with_async_runtime.rs | 706 + .../opentelemetry_sdk/src/trace/tracer.rs | 620 + .../opentelemetry_sdk/src/util.rs | 9 + .../opentelemetry_sdk/tests/miri_smoke.rs | 8 + .../tests/otel_default_constants.rs | 86 + 213 files changed, 95597 insertions(+), 13 deletions(-) create mode 100644 vendor/opentelemetry-rust/LICENSE create mode 100644 vendor/opentelemetry-rust/VENDORED.md create mode 100644 vendor/opentelemetry-rust/opentelemetry-http/CHANGELOG.md create mode 100644 vendor/opentelemetry-rust/opentelemetry-http/Cargo.toml create mode 100644 vendor/opentelemetry-rust/opentelemetry-http/README.md create mode 100644 vendor/opentelemetry-rust/opentelemetry-http/allowed-external-types.toml create mode 100644 vendor/opentelemetry-rust/opentelemetry-http/src/lib.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/CHANGELOG.md create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/Cargo.toml create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/README.md create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/allowed-external-types.toml create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/logs.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/metrics.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/trace.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/logs.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/metrics.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/trace.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/lib.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/logs.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/metric.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/retry.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/retry_classification.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/retry_policy.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/src/span.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-otlp/tests/smoke.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/CHANGELOG.md create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/Cargo.toml create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/README.md create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/lib.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.logs.v1.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.metrics.v1.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.profiles.v1development.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.trace.v1.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.common.v1.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.logs.v1.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.profiles.v1development.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.resource.v1.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.trace.v1.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.tracez.v1.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tracez.proto create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/transform/common.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/transform/logs.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/transform/metrics.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/transform/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/transform/profiles.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/transform/trace.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/src/transform/tracez.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/tests/grpc_build.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-proto/tests/json_serde.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/.gitignore create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/CHANGELOG.md create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/Cargo.toml create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/README.md create mode 100755 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/generate-consts-from-spec.sh create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/attribute.rs.j2 create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/lib.rs.j2 create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/macros.j2 create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/metric.rs.j2 create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/resource.rs.j2 create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/trace.rs.j2 create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/weaver.yaml create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/attribute.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/lib.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/metric.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/resource.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/trace.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/CHANGELOG.md create mode 100644 vendor/opentelemetry-rust/opentelemetry/Cargo.toml create mode 100644 vendor/opentelemetry-rust/opentelemetry/README.md create mode 100644 vendor/opentelemetry-rust/opentelemetry/allowed-external-types.toml create mode 100644 vendor/opentelemetry-rust/opentelemetry/benches/anyvalue.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/benches/attributes.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/benches/baggage.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/benches/context_attach.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/benches/context_suppression.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/benches/logs.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/benches/metrics.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/benches/trace.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/baggage.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/common.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/context.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/context/future_ext.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/global/internal_logging.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/global/metrics.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/global/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/global/propagation.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/global/trace.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/lib.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/logs/logger.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/logs/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/logs/noop.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/logs/record.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/counter.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/gauge.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/histogram.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/up_down_counter.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/metrics/meter.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/metrics/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/metrics/noop.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/propagation/composite.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/propagation/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/propagation/text_map_propagator.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/testing/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/testing/trace.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/trace/context.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/trace/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/trace/noop.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/trace/span.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/trace/span_context.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/trace/tracer.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/trace/tracer_provider.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry/src/trace_context.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/CHANGELOG.md create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/Cargo.toml create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/README.md create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/allowed-external-types.toml create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/batch_span_processor.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/bound_instruments.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/context.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/log.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_enabled.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_exporter.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_processor.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/metric.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_counter.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_gauge.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_histogram.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/span.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/span_builder.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/trace.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/benches/tracer_creation.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/error.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/growable_array.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/lib.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/batch_log_processor.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/export.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/in_memory_exporter.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/log_processor.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/log_processor_with_async_runtime.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/logger.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/logger_provider.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/record.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/simple_log_processor.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/aggregation.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/data/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/error.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/exporter.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/in_memory_exporter.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/instrument.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/aggregate.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/exponential_histogram.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/histogram.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/last_value.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/precomputed_sum.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/sum.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/manual_reader.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/meter.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/meter_provider.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/noop.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/periodic_reader.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/periodic_reader_with_async_runtime.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/pipeline.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/reader.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/test.md create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/view.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/baggage.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/trace_context.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/attributes.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/env.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/telemetry.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/runtime.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/metrics/metric_reader.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/metrics/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/trace/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/trace/span_exporters.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/config.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/events.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/export.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/id_generator/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/in_memory_exporter.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/links.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/provider.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/runtime_tests.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/mod.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/rate_limit.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/remote.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/sampler.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/sampling_strategy.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_limit.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_processor.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_processor_with_async_runtime.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/tracer.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/src/util.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/tests/miri_smoke.rs create mode 100644 vendor/opentelemetry-rust/opentelemetry_sdk/tests/otel_default_constants.rs diff --git a/Cargo.lock b/Cargo.lock index 00facb9b..c7403b09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5508,7 +5508,6 @@ dependencies = [ [[package]] name = "opentelemetry" version = "0.33.0" -source = "git+https://github.com/stalwartlabs/opentelemetry-rust#ae66e97b140f70e477ab710686aafce665cc2f8b" dependencies = [ "futures-core", "futures-sink", @@ -5521,7 +5520,6 @@ dependencies = [ [[package]] name = "opentelemetry-http" version = "0.33.0" -source = "git+https://github.com/stalwartlabs/opentelemetry-rust#ae66e97b140f70e477ab710686aafce665cc2f8b" dependencies = [ "async-trait", "bytes", @@ -5533,7 +5531,6 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" version = "0.33.0" -source = "git+https://github.com/stalwartlabs/opentelemetry-rust#ae66e97b140f70e477ab710686aafce665cc2f8b" dependencies = [ "http 1.5.0", "httpdate", @@ -5552,7 +5549,6 @@ dependencies = [ [[package]] name = "opentelemetry-proto" version = "0.33.0" -source = "git+https://github.com/stalwartlabs/opentelemetry-rust#ae66e97b140f70e477ab710686aafce665cc2f8b" dependencies = [ "opentelemetry", "opentelemetry_sdk", @@ -5564,12 +5560,10 @@ dependencies = [ [[package]] name = "opentelemetry-semantic-conventions" version = "0.33.0" -source = "git+https://github.com/stalwartlabs/opentelemetry-rust#ae66e97b140f70e477ab710686aafce665cc2f8b" [[package]] name = "opentelemetry_sdk" version = "0.33.0" -source = "git+https://github.com/stalwartlabs/opentelemetry-rust#ae66e97b140f70e477ab710686aafce665cc2f8b" dependencies = [ "futures-channel", "futures-executor", diff --git a/Cargo.toml b/Cargo.toml index ce4b3730..99524cdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,11 +86,18 @@ rpath = false # vendor/sieve-rs is the published 0.7.3 with those renamed; see its # VENDORED.md. Re-vendor when the version in Cargo.lock moves. # -# inbuxa: hickory is upstream's patched branch (DNSSEC fixes), vendored at -# the commit upstream pins so builds never fetch from upstream's -# repositories; see vendor/hickory-dns/VENDORED.md. +# inbuxa: hickory and opentelemetry are upstream's patched branches, vendored +# at the commits upstream pins so builds never fetch from upstream's +# repositories; see vendor/hickory-dns/VENDORED.md and +# vendor/opentelemetry-rust/VENDORED.md. [patch.crates-io] sieve-rs = { path = "vendor/sieve-rs" } hickory-proto = { path = "vendor/hickory-dns/hickory-proto" } hickory-net = { path = "vendor/hickory-dns/hickory-net" } hickory-resolver = { path = "vendor/hickory-dns/hickory-resolver" } +opentelemetry = { path = "vendor/opentelemetry-rust/opentelemetry" } +opentelemetry_sdk = { path = "vendor/opentelemetry-rust/opentelemetry_sdk" } +opentelemetry-otlp = { path = "vendor/opentelemetry-rust/opentelemetry-otlp" } +opentelemetry-proto = { path = "vendor/opentelemetry-rust/opentelemetry-proto" } +opentelemetry-http = { path = "vendor/opentelemetry-rust/opentelemetry-http" } +opentelemetry-semantic-conventions = { path = "vendor/opentelemetry-rust/opentelemetry-semantic-conventions" } diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index e411eb2a..39351a2b 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -43,10 +43,11 @@ x509-parser = "0.18" pem = "4.0" chrono = { version = "0.4", features = ["serde"] } hyper = { version = "1.11.1", features = ["server", "http1", "http2"] } -opentelemetry = { git = "https://github.com/stalwartlabs/opentelemetry-rust" } -opentelemetry_sdk = { git = "https://github.com/stalwartlabs/opentelemetry-rust" } -opentelemetry-otlp = { git = "https://github.com/stalwartlabs/opentelemetry-rust", default-features = false, features = ["reqwest-client", "http-proto", "trace", "metrics", "logs", "internal-logs", "grpc-tonic", "tls-aws-lc", "tls-roots", "reqwest-rustls"] } -opentelemetry-semantic-conventions = { git = "https://github.com/stalwartlabs/opentelemetry-rust" } +# inbuxa: the opentelemetry crates come from vendor/ (root [patch.crates-io]) +opentelemetry = { version = "0.33" } +opentelemetry_sdk = { version = "0.33" } +opentelemetry-otlp = { version = "0.33", default-features = false, features = ["reqwest-client", "http-proto", "trace", "metrics", "logs", "internal-logs", "grpc-tonic", "tls-aws-lc", "tls-roots", "reqwest-rustls"] } +opentelemetry-semantic-conventions = { version = "0.33" } prometheus = { version = "0.14", default-features = false } imagesize = "0.15" sha1 = "0.11" diff --git a/vendor/opentelemetry-rust/LICENSE b/vendor/opentelemetry-rust/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/vendor/opentelemetry-rust/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/opentelemetry-rust/VENDORED.md b/vendor/opentelemetry-rust/VENDORED.md new file mode 100644 index 00000000..a53fc9ae --- /dev/null +++ b/vendor/opentelemetry-rust/VENDORED.md @@ -0,0 +1,32 @@ +# opentelemetry-rust, vendored + +`opentelemetry`, `opentelemetry_sdk`, `opentelemetry-otlp`, +`opentelemetry-proto`, `opentelemetry-http` and +`opentelemetry-semantic-conventions` 0.33.0 as upstream builds them: from its +fork of [opentelemetry-rust](https://github.com/open-telemetry/opentelemetry-rust) +at commit `ae66e97b140f70e477ab710686aafce665cc2f8b` (2026-09-26), under +Apache-2.0 (`LICENSE` here is the project's). The root `Cargo.toml` patches +them in with `[patch.crates-io]`, so builds never fetch from upstream's +repositories. + +That commit is opentelemetry-rust's own `main` at `b7852a8c` (2026-09-24, +after the 0.33.0 release candidate) plus one change: public constructors for +the metrics data types (`ResourceMetrics`, `ScopeMetrics`, `Metric`, the +gauge, sum and histogram types and their data points) and a public +`SdkLogRecord::new()` with a `Default`. Nothing else differs. + +## Changes + +None. Each directory is `cargo package --no-verify` of that commit (the +manifest normalized, so it builds outside the project's workspace), without +`Cargo.lock`, `Cargo.toml.orig` and `.cargo_vcs_info.json`. + +## Updating + +When upstream moves its pin, or opentelemetry-rust releases the +constructors, re-vendor: clone the repository at the new commit, run `cargo +package --no-verify` for the six crates, unpack each `.crate` here, and drop +`Cargo.lock` and `Cargo.toml.orig`. If the version changes, Cargo stops +using these copies and only warns that the patch went unused, so check +`Cargo.lock` has no `opentelemetry*` entry with a `source` line. Update the +commit above. diff --git a/vendor/opentelemetry-rust/opentelemetry-http/CHANGELOG.md b/vendor/opentelemetry-rust/opentelemetry-http/CHANGELOG.md new file mode 100644 index 00000000..7ed4763c --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-http/CHANGELOG.md @@ -0,0 +1,184 @@ +# Changelog + +## vNext + +## 0.33.0 + +Released 2026-Sep-18 + +- Apply `HyperClient`'s configured timeout to the complete response body, not + only request dispatch and response headers. + +- **Breaking** Sealed the `ResponseExt` trait so it can no longer be implemented by + downstream crates. The trait provides a blanket implementation for all + `http::Response` types, so calling code is unaffected -- only + `impl ResponseExt for MyType` will stop compiling. If you have a custom + implementation, remove it and rely on the blanket impl instead. +- **Breaking** Removed the deprecated `HttpClient::send` method, which accepted + `Request>`. Implement and call `HttpClient::send_bytes` instead, + converting existing requests with `request.map(Bytes::from)` when needed. + +- **Breaking:** Remove `opentelemetry_http::hyper::Body`, which is no longer used + by any public constructor. Use `http_body_util::Full` for custom Hyper + client request bodies. +- Limit HTTP response body reads to 4 MiB in built-in HTTP clients (`reqwest` async/blocking and `hyper`). Reads exceeding the limit are aborted to prevent unbounded memory allocation and return the new opaque `ResponseBodyTooLarge` error. Custom HTTP clients can construct this error with `ResponseBodyTooLarge::new()` or `ResponseBodyTooLarge::default()`. + +- **Breaking** Built-in reqwest and hyper clients now return HTTP 4xx and 5xx + responses as `Ok(Response)` instead of `Err(HttpError)`. Here, `Ok` + means that the transport completed the request and received an HTTP response; + it does not imply a successful HTTP status. This preserves the response status + and headers for exporter retry classification. Transport failures and timeouts + continue to return `Err`. + If your code relied on `send_bytes` returning `Err` for non-success statuses, + call `ResponseExt::error_for_status()` on the response instead. +- **Breaking** Removed `reqwest-rustls-webpki-roots` feature. The `webpki-roots` cargo feature was + removed from `reqwest` in v0.13.0. Use `reqwest-rustls` instead, which now correctly enables + `reqwest/rustls` (platform native trust roots). To use Mozilla's embedded CA bundle, construct a + custom `reqwest::Client` with a `rustls::ClientConfig` containing + `rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned())`, then supply it + to the exporter with `with_http_client()`. + +## 0.32.0 + +Released 2026-May-08 + +- `reqwest`'s crypto backend has changed from `ring` to `aws-lc-sys`. + +## 0.31.0 + +Released 2025-Sep-25 + +- Implementation of `Extractor::get_all` for `HeaderExtractor` +- Support `HttpClient` implementation for `HyperClient` with custom connectors beyond `HttpConnector`, enabling Unix Domain Socket connections and other custom transports +- Add `reqwest` and `reqwest-blocking` features to enable async and blocking + reqwest HTTP clients +- Updated `opentelemetry` dependency to version 0.31.0. + +## 0.30.0 + +Released 2025-May-23 + +- Updated `opentelemetry` dependency to version 0.30.0. + +## 0.29.0 + +Released 2025-Mar-21 + +- Update `opentelemetry` dependency version to 0.29. + +## 0.28.0 + +Released 2025-Feb-10 + +- Update `opentelemetry` dependency version to 0.28. +- Bump msrv to 1.75.0. +- Add "internal-logs" feature flag (enabled by default), and emit internal logs via `tracing` crate. +- Add `HttpClient::send_bytes` with `bytes::Bytes` request payload and deprecate old `HttpClient::send` function. + +## 0.27.0 + +Released 2024-Nov-08 + +- Update `opentelemetry` dependency version to 0.27 + +- Bump MSRV to 1.70 [#2179](https://github.com/open-telemetry/opentelemetry-rust/pull/2179) + +## v0.26.0 +Released 2024-Sep-30 + +- Update `opentelemetry` dependency version to 0.26 + +## v0.25.0 + +- Update `opentelemetry` dependency version to 0.25 +- Starting with this version, this crate will align with `opentelemetry` crate + on major,minor versions. + +## v0.13.0 + +- **Breaking** Correct the misspelling of "webkpi" to "webpki" in features [#1842](https://github.com/open-telemetry/opentelemetry-rust/pull/1842) +- **Breaking** Remove support for the `isahc` HTTP client [#1924](https://github.com/open-telemetry/opentelemetry-rust/pull/1924) +- Update to `http` v1 [#1674](https://github.com/open-telemetry/opentelemetry-rust/pull/1674) +- Update `opentelemetry` dependency version to 0.24 + +## v0.12.0 + +- Add `reqwest-rustls-webpki-roots` feature flag to configure [`reqwest`](https://docs.rs/reqwest/0.11.27/reqwest/index.html#optional-features) to use embedded `webpki-roots`. +- Update `opentelemetry` dependency version to 0.23 + +## v0.11.1 + +- Add feature flag enabling users to configure `reqwest` usage to use rustls via + `reqwest/rustls-tls` feature flag + [1638](https://github.com/open-telemetry/opentelemetry-rust/pull/1638). + +## v0.11.0 + +### Changed + +- **Breaking** Remove built-in support for surf HTTP client [#1537](https://github.com/open-telemetry/opentelemetry-rust/pull/1537) +- **Breaking** Surface non-2xx status codes as errors; change `ResponseExt` trait to return `HttpError` instead of `TraceError`[#1484](https://github.com/open-telemetry/opentelemetry-rust/pull/1484) + +## v0.10.0 + +### Changed + +- Bump MSRV to 1.65 [#1318](https://github.com/open-telemetry/opentelemetry-rust/pull/1318) +- Bump MSRV to 1.64 [#1203](https://github.com/open-telemetry/opentelemetry-rust/pull/1203) + +## v0.9.0 + +### Changed + +- Update to opentelemetry-api v0.20.0 + +## v0.8.0 + +### Changed +- Add response headers in response for `HttpClient` implementations [#918](https://github.com/open-telemetry/opentelemetry-rust/pull/918). +- Bump MSRV to 1.57 [#953](https://github.com/open-telemetry/opentelemetry-rust/pull/953). +- Update dependencies and bump MSRV to 1.60 [#969](https://github.com/open-telemetry/opentelemetry-rust/pull/969). + +## v0.7.0 + +### Changed + +- Update to opentelemetry v0.18.0 +- Export `byte` and `http` types #798 +- Implementation of collector http client with pure hyper #853 + +## v0.6.0 + +### Changed + +- Update to opentelemetry v0.17.0 + +## v0.5.0 + +### Changed + +- Update to opentelemetry v0.16.0 + +## v0.4.0 + +### Changed + +- Update to opentelemetry v0.15.0 + +## v0.3.0 + +### Changed + +- Return response from `HttpClient` #511 +- Update to opentelemetry v0.14.0 + +## v0.2.0 + +### Changed +- Update to opentelemetry v0.13.0 + +## v0.1.0 + +### Added + +- Opentelemetry integration with http #415 diff --git a/vendor/opentelemetry-rust/opentelemetry-http/Cargo.toml b/vendor/opentelemetry-rust/opentelemetry-http/Cargo.toml new file mode 100644 index 00000000..98404690 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-http/Cargo.toml @@ -0,0 +1,132 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.75.0" +name = "opentelemetry-http" +version = "0.33.0" +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Helper implementations for sending HTTP requests. Uses include propagating and extracting context over http, exporting telemetry, requesting sampling strategies." +homepage = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-http" +readme = "README.md" +keywords = [ + "opentelemetry", + "tracing", + "context", + "propagation", +] +license = "Apache-2.0" +repository = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-http" + +[features] +default = ["internal-logs"] +hyper = [ + "dep:http-body-util", + "dep:hyper", + "dep:hyper-util", + "dep:tokio", +] +internal-logs = ["opentelemetry/internal-logs"] +reqwest = ["dep:reqwest"] +reqwest-blocking = [ + "dep:reqwest", + "reqwest/blocking", +] +reqwest-rustls = [ + "dep:reqwest", + "reqwest/rustls", +] + +[lib] +name = "opentelemetry_http" +path = "src/lib.rs" +bench = false + +[dependencies.async-trait] +version = "0.1" + +[dependencies.bytes] +version = "1" + +[dependencies.http] +version = "1.1" +features = ["std"] +default-features = false + +[dependencies.http-body-util] +version = "0.1" +optional = true + +[dependencies.hyper] +version = "1.3" +optional = true +default-features = false + +[dependencies.hyper-util] +version = "0.1" +features = [ + "client-legacy", + "http1", + "http2", +] +optional = true + +[dependencies.opentelemetry] +version = "0.33" +features = ["trace"] +default-features = false + +[dependencies.reqwest] +version = "0.13.1" +optional = true +default-features = false + +[dependencies.tokio] +version = "1" +features = ["time"] +optional = true +default-features = false + +[dev-dependencies.futures-executor] +version = "0.3" + +[dev-dependencies.tokio] +version = "1" +features = [ + "rt-multi-thread", + "macros", + "net", + "io-util", + "time", +] +default-features = false + +[dev-dependencies.tower-service] +version = "0.3" + +[lints.clippy.all] +level = "warn" +priority = 1 + +[lints.rust] +edition_2024_expr_fragment_specifier = "allow" +if_let_rescope = "allow" +tail_expr_drop_order = "allow" + +[lints.rust.rust_2024_compatibility] +level = "warn" +priority = -1 diff --git a/vendor/opentelemetry-rust/opentelemetry-http/README.md b/vendor/opentelemetry-rust/opentelemetry-http/README.md new file mode 100644 index 00000000..922f1951 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-http/README.md @@ -0,0 +1,81 @@ +# OpenTelemetry HTTP + +![OpenTelemetry — An observability framework for cloud-native software.][splash] + +[splash]: https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo-text.png + +This crate contains helper implementations for sending HTTP requests, used +by [OpenTelemetry](https://opentelemetry.io/) components. Common uses include +propagating and extracting context over HTTP, exporting telemetry data over +HTTP, and requesting sampling strategies from remote endpoints. + +[![Crates.io: opentelemetry-http](https://img.shields.io/crates/v/opentelemetry-http.svg)](https://crates.io/crates/opentelemetry-http) +[![Documentation](https://docs.rs/opentelemetry-http/badge.svg)](https://docs.rs/opentelemetry-http) +[![LICENSE](https://img.shields.io/crates/l/opentelemetry-http)](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-http/LICENSE) +[![GitHub Actions CI](https://github.com/open-telemetry/opentelemetry-rust/workflows/CI/badge.svg)](https://github.com/open-telemetry/opentelemetry-rust/actions?query=workflow%3ACI+branch%3Amain) +[![Slack](https://img.shields.io/badge/slack-@cncf/otel/rust-brightgreen.svg?logo=slack)](https://cloud-native.slack.com/archives/C03GDP0H023) + +## Overview + +OpenTelemetry is an Observability framework and toolkit designed to create and +manage telemetry data such as traces, metrics, and logs. OpenTelemetry is +vendor- and tool-agnostic, meaning that it can be used with a broad variety of +Observability backends, including open source tools like [Jaeger] and +[Prometheus], as well as commercial offerings. + +OpenTelemetry is *not* an observability backend like Jaeger, Prometheus, or other +commercial vendors. OpenTelemetry is focused on the generation, collection, +management, and export of telemetry. A major goal of OpenTelemetry is that you +can easily instrument your applications or systems, no matter their language, +infrastructure, or runtime environment. Crucially, the storage and visualization +of telemetry is intentionally left to other tools. + +*[Supported Rust Versions](#supported-rust-versions)* + +[Prometheus]: https://prometheus.io +[Jaeger]: https://www.jaegertracing.io + +### What does this crate contain? + +This crate contains shared HTTP building blocks used across the +OpenTelemetry Rust ecosystem, including: + +- An `HttpClient` trait and adapters for popular HTTP clients such as + `reqwest` and `hyper`. +- Helpers for propagating and extracting OpenTelemetry context (for example + W3C TraceContext) over HTTP headers. +- Utilities used by exporters to send telemetry over HTTP. + +This crate is primarily used as a dependency by other OpenTelemetry crates +(such as [`opentelemetry-otlp`](https://crates.io/crates/opentelemetry-otlp)) +rather than directly by end users. + +### Related crates + +- **[opentelemetry](https://crates.io/crates/opentelemetry):** The core + OpenTelemetry API. +- **[opentelemetry-sdk](https://crates.io/crates/opentelemetry-sdk):** The + OpenTelemetry SDK implementation. +- **[opentelemetry-otlp](https://crates.io/crates/opentelemetry-otlp):** OTLP + exporter that uses this crate for HTTP transport. + +## Getting started + +See [docs](https://docs.rs/opentelemetry-http). + +## Release Notes + +You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-http/CHANGELOG.md). + +## Supported Rust Versions + +OpenTelemetry is built against the latest stable release. The minimum supported +version is 1.75.0. The current OpenTelemetry version is NOT guaranteed to build +on Rust versions earlier than the minimum supported version. + +The current stable Rust compiler and the three most recent minor versions +before it will always be supported. For example, if the current stable compiler +version is 1.49, the minimum supported version will not be increased past 1.46, +three minor versions prior. Increasing the minimum supported compiler version +is not considered a semver breaking change as long as doing so complies with +this policy. diff --git a/vendor/opentelemetry-rust/opentelemetry-http/allowed-external-types.toml b/vendor/opentelemetry-rust/opentelemetry-http/allowed-external-types.toml new file mode 100644 index 00000000..963df55d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-http/allowed-external-types.toml @@ -0,0 +1,21 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# This is used with cargo-check-external-types to reduce the surface area of downstream crates from +# the public API. Ideally this can have as few exceptions as possible. +allowed_external_types = [ + # ResponseBodyTooLarge's constructor exposes no external types. + # Public HTTP client contract and propagation adapters. + "bytes::bytes::Bytes", + "http::header::map::HeaderMap", + "http::header::value::HeaderValue", + "http::request::Request", + "http::response::Response", + + # HyperClient's public default connector and generic connector bound. + "hyper_util::client::legacy::connect::http::HttpConnector", + "hyper_util::client::legacy::connect::sealed::Connect", + + # Traits implemented by the public propagation adapters. + "opentelemetry::propagation::Extractor", + "opentelemetry::propagation::Injector", +] diff --git a/vendor/opentelemetry-rust/opentelemetry-http/src/lib.rs b/vendor/opentelemetry-rust/opentelemetry-http/src/lib.rs new file mode 100644 index 00000000..4ad379b1 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-http/src/lib.rs @@ -0,0 +1,842 @@ +//! HTTP types and client adapters shared by OpenTelemetry components. +//! +//! [`HeaderInjector`] and [`HeaderExtractor`] adapt an [`http::HeaderMap`] to +//! OpenTelemetry's text-map propagation interfaces. [`HttpClient`] is the +//! transport abstraction used by exporters and other components that issue +//! HTTP requests. +//! +//! # HTTP clients +//! +//! This crate does not enable a concrete HTTP client by default. Select one of +//! these features when an OpenTelemetry component does not provide one: +//! +//! - `reqwest` implements [`HttpClient`] for the asynchronous +//! `reqwest::Client`. +//! - `reqwest-blocking` additionally implements [`HttpClient`] for +//! `reqwest::blocking::Client`. +//! - `reqwest-rustls` enables reqwest with its Rustls TLS backend. +//! - `hyper` provides `hyper::HyperClient`, including support for custom +//! connectors. +//! +//! The reqwest implementations use the timeout configured on the supplied +//! reqwest client. `hyper::HyperClient` requires a Tokio runtime and uses +//! `hyper_util::rt::TokioExecutor` internally. +//! +//! # Implementing a client +//! +//! A custom client controls connection management, redirects, and timeouts. +//! `send_bytes` returns HTTP responses regardless of their status code; +//! transport failures and timeouts are returned as errors. Use +//! [`ResponseExt::error_for_status`] when non-success status codes should be +//! converted into errors. +//! +//! ``` +//! use async_trait::async_trait; +//! use opentelemetry_http::{Bytes, HttpClient, HttpError, Request, Response}; +//! +//! #[derive(Debug)] +//! struct ExampleClient; +//! +//! #[async_trait] +//! impl HttpClient for ExampleClient { +//! async fn send_bytes( +//! &self, +//! request: Request, +//! ) -> Result, HttpError> { +//! // A real implementation would send the request and enforce its +//! // configured timeout while collecting the complete response body. +//! Ok(Response::new(request.into_body())) +//! } +//! } +//! +//! let request = Request::post("http://collector.example/v1/traces") +//! .body(Bytes::from_static(b"encoded telemetry"))?; +//! let response = futures_executor::block_on(ExampleClient.send_bytes(request))?; +//! assert_eq!(response.body(), &Bytes::from_static(b"encoded telemetry")); +//! # Ok::<(), HttpError>(()) +//! ``` +//! +//! # Response size limit +//! +//! The built-in reqwest and Hyper implementations collect response bodies into +//! memory and reject bodies larger than 4 MiB with [`ResponseBodyTooLarge`]. + +use async_trait::async_trait; +use std::fmt::Debug; + +#[doc(no_inline)] +pub use bytes::Bytes; +#[doc(no_inline)] +pub use http::{Request, Response}; +use opentelemetry::propagation::{Extractor, Injector}; + +/// Helper for injecting headers into HTTP Requests. This is used for OpenTelemetry context +/// propagation over HTTP. +/// See [this](https://github.com/open-telemetry/opentelemetry-rust/blob/main/examples/tracing-http-propagator/README.md) +/// for example usage. +pub struct HeaderInjector<'a>(pub &'a mut http::HeaderMap); + +impl Injector for HeaderInjector<'_> { + /// Set a key and value in the HeaderMap. Does nothing if the key or value are not valid inputs. + fn set(&mut self, key: &str, value: String) { + if let Ok(name) = http::header::HeaderName::from_bytes(key.as_bytes()) { + if let Ok(val) = http::header::HeaderValue::from_str(&value) { + self.0.insert(name, val); + } + } + } + + /// Reserves capacity for at least `additional` more entries to be inserted. + fn reserve(&mut self, additional: usize) { + self.0.reserve(additional); + } +} + +/// Helper for extracting headers from HTTP Requests. This is used for OpenTelemetry context +/// propagation over HTTP. +/// See [this](https://github.com/open-telemetry/opentelemetry-rust/blob/main/examples/tracing-http-propagator/README.md) +/// for example usage. +pub struct HeaderExtractor<'a>(pub &'a http::HeaderMap); + +impl Extractor for HeaderExtractor<'_> { + /// Get a value for a key from the HeaderMap. If the value is not valid ASCII, returns None. + fn get(&self, key: &str) -> Option<&str> { + self.0.get(key).and_then(|value| value.to_str().ok()) + } + + /// Collect all the keys from the HeaderMap. + fn keys(&self) -> Vec<&str> { + self.0 + .keys() + .map(|value| value.as_str()) + .collect::>() + } + + /// Get all the values for a key from the HeaderMap + fn get_all(&self, key: &str) -> Option> { + let all_iter = self.0.get_all(key).iter(); + if let (0, Some(0)) = all_iter.size_hint() { + return None; + } + + Some(all_iter.filter_map(|value| value.to_str().ok()).collect()) + } +} + +/// Error returned when an HTTP request cannot be completed. +pub type HttpError = Box; + +/// A minimal interface for sending byte-oriented HTTP requests. +/// +/// This is primarily used for exporting telemetry and for fetching remote +/// sampling strategies. Implementations are responsible for enforcing any +/// required request timeout, including while reading the response body. +/// +/// HTTP clients may depend on a particular async runtime. This trait allows +/// users to supply an implementation suitable for their runtime. +#[async_trait] +pub trait HttpClient: Debug + Send + Sync { + /// Send the specified HTTP request with `Bytes` payload. + /// + /// Returns the complete HTTP response, including non-success status codes. + /// + /// Returns an error if the request cannot be completed, for example because + /// of a connection failure, timeout, redirect failure, or response body + /// larger than 4 MiB in a built-in client. + async fn send_bytes(&self, request: Request) -> Result, HttpError>; +} + +#[cfg(any(feature = "reqwest", feature = "hyper"))] +const MAX_RESPONSE_BODY_BYTES: usize = 4 * 1024 * 1024; + +/// Error returned when an HTTP response body exceeds the configured size limit. +/// +/// Construct this error with [`Self::new`] or [`Default::default`]. Its fields +/// are private to allow future diagnostic details without changing construction. +#[derive(Debug, Default)] +pub struct ResponseBodyTooLarge { + _private: (), +} + +impl ResponseBodyTooLarge { + /// Creates an error indicating that the response body exceeded the size limit. + pub fn new() -> Self { + Self::default() + } +} + +impl std::fmt::Display for ResponseBodyTooLarge { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "response body exceeded maximum allowed 4 MiB limit") + } +} + +impl std::error::Error for ResponseBodyTooLarge {} + +#[cfg(feature = "reqwest")] +mod reqwest { + use opentelemetry::otel_debug; + + use crate::ResponseBodyTooLarge; + + use super::{ + async_trait, Bytes, HttpClient, HttpError, Request, Response, MAX_RESPONSE_BODY_BYTES, + }; + + #[async_trait] + impl HttpClient for reqwest::Client { + async fn send_bytes(&self, request: Request) -> Result, HttpError> { + otel_debug!(name: "ReqwestClient.Send"); + let request = request.try_into()?; + let mut response = self.execute(request).await?; + let capacity = response + .content_length() + .unwrap_or(0) + .min(MAX_RESPONSE_BODY_BYTES as u64) as usize; + + let mut body_bytes = bytes::BytesMut::with_capacity(capacity); + + let status = response.status(); + let headers = std::mem::take(response.headers_mut()); + while let Some(chunk) = response.chunk().await? { + if body_bytes.len() + chunk.len() > MAX_RESPONSE_BODY_BYTES { + return Err(Box::new(ResponseBodyTooLarge::new())); + } + body_bytes.extend_from_slice(&chunk); + } + let mut http_response = Response::builder() + .status(status) + .body(body_bytes.freeze())?; + + *http_response.headers_mut() = headers; + Ok(http_response) + } + } + + #[cfg(not(target_arch = "wasm32"))] + #[cfg(feature = "reqwest-blocking")] + #[async_trait] + impl HttpClient for reqwest::blocking::Client { + async fn send_bytes(&self, request: Request) -> Result, HttpError> { + use std::io::Read; + otel_debug!(name: "ReqwestBlockingClient.Send"); + let request = request.try_into()?; + let mut response = self.execute(request)?; + let capacity = response + .content_length() + .unwrap_or(0) + .min(MAX_RESPONSE_BODY_BYTES as u64) as usize; + let status = response.status(); + let headers = std::mem::take(response.headers_mut()); + let mut body_bytes = Vec::with_capacity(capacity); + response + .take(MAX_RESPONSE_BODY_BYTES as u64 + 1) + .read_to_end(&mut body_bytes)?; + if body_bytes.len() > MAX_RESPONSE_BODY_BYTES { + return Err(Box::new(ResponseBodyTooLarge::new())); + } + let mut http_response = Response::builder() + .status(status) + .body(Bytes::from(body_bytes))?; + *http_response.headers_mut() = headers; + Ok(http_response) + } + } +} + +#[cfg(feature = "hyper")] +pub mod hyper { + use super::{ + async_trait, Bytes, HttpClient, HttpError, Request, Response, MAX_RESPONSE_BODY_BYTES, + }; + use crate::ResponseBodyTooLarge; + use http::HeaderValue; + use http_body_util::{BodyExt, Full}; + use hyper::body::Body as _; + use hyper_util::client::legacy::{ + connect::{Connect, HttpConnector}, + Client, + }; + use opentelemetry::otel_debug; + use std::fmt::Debug; + use std::time::Duration; + use tokio::time; + + /// An [`HttpClient`] backed by Hyper. + /// + /// This client requires a Tokio runtime and uses + /// [`hyper_util::rt::TokioExecutor`] to drive connections. Responses larger + /// than 4 MiB are rejected with [`ResponseBodyTooLarge`]. + #[derive(Debug, Clone)] + pub struct HyperClient + where + C: Connect + Clone + Send + Sync + 'static, + { + inner: Client>, + timeout: Duration, + authorization: Option, + } + + impl HyperClient + where + C: Connect + Clone + Send + Sync + 'static, + { + /// Creates a client with a custom Hyper connector. + /// + /// The connector must satisfy Hyper's [`Connect`] bounds. `timeout` + /// configures the request deadline. When `authorization` is provided, + /// its value replaces any `Authorization` header already present on + /// each request. + pub fn new(connector: C, timeout: Duration, authorization: Option) -> Self { + // TODO - support custom executor + let inner = Client::builder(hyper_util::rt::TokioExecutor::new()).build(connector); + Self { + inner, + timeout, + authorization, + } + } + } + + impl HyperClient { + /// Creates a client with Hyper's default [`HttpConnector`]. + /// + /// `timeout` configures the request deadline. When `authorization` is + /// provided, its value replaces any `Authorization` header already + /// present on each request. + pub fn with_default_connector( + timeout: Duration, + authorization: Option, + ) -> Self { + Self::new(HttpConnector::new(), timeout, authorization) + } + } + + #[async_trait] + impl HttpClient for HyperClient + where + C: Connect + Clone + Send + Sync + 'static, + HyperClient: Debug, + { + async fn send_bytes(&self, request: Request) -> Result, HttpError> { + otel_debug!(name: "HyperClient.Send"); + let (parts, body) = request.into_parts(); + let mut request = Request::from_parts(parts, Full::from(body)); + if let Some(ref authorization) = self.authorization { + request + .headers_mut() + .insert(http::header::AUTHORIZATION, authorization.clone()); + } + time::timeout(self.timeout, async { + let mut response = self.inner.request(request).await?; + let capacity = response + .body() + .size_hint() + .upper() + .unwrap_or(0) + .min(MAX_RESPONSE_BODY_BYTES as u64) as usize; + let mut body_bytes = bytes::BytesMut::with_capacity(capacity); + let status = response.status(); + let headers = std::mem::take(response.headers_mut()); + let mut body = response.into_body(); + while let Some(frame) = body.frame().await { + let frame = frame?; + if let Ok(chunk) = frame.into_data() { + if body_bytes.len() + chunk.len() > MAX_RESPONSE_BODY_BYTES { + return Err(Box::new(ResponseBodyTooLarge::new()) as HttpError); + } + body_bytes.extend_from_slice(&chunk); + } + } + let mut http_response = Response::builder() + .status(status) + .body(body_bytes.freeze())?; + *http_response.headers_mut() = headers; + Ok(http_response) + }) + .await? + } + } +} + +mod private { + pub trait Sealed {} + impl Sealed for http::Response {} +} + +/// Methods to make working with responses from the [`HttpClient`] trait easier. +/// +/// This trait is sealed and cannot be implemented outside of this crate. +pub trait ResponseExt: private::Sealed + Sized { + /// Turn a response into an error if the HTTP status does not indicate success (200 - 299). + fn error_for_status(self) -> Result; +} + +impl ResponseExt for Response { + fn error_for_status(self) -> Result { + if self.status().is_success() { + Ok(self) + } else { + Err(format!("request failed with status {}", self.status()).into()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use http::HeaderValue; + + #[test] + fn response_body_too_large_construction() { + for error in [ResponseBodyTooLarge::new(), ResponseBodyTooLarge::default()] { + let error: HttpError = Box::new(error); + assert!(error.downcast_ref::().is_some()); + assert_eq!( + error.to_string(), + "response body exceeded maximum allowed 4 MiB limit" + ); + } + } + + #[cfg(all( + any(feature = "hyper", feature = "reqwest", feature = "reqwest-blocking"), + not(target_arch = "wasm32") + ))] + use std::io::{Read, Write}; + + #[cfg(all( + any(feature = "hyper", feature = "reqwest", feature = "reqwest-blocking"), + not(target_arch = "wasm32") + ))] + use std::net::{SocketAddr, TcpListener}; + + #[cfg(all( + any(feature = "hyper", feature = "reqwest", feature = "reqwest-blocking"), + not(target_arch = "wasm32") + ))] + use std::thread::JoinHandle; + + #[test] + fn http_headers_get() { + let mut carrier = http::HeaderMap::new(); + HeaderInjector(&mut carrier).set("headerName", "value".to_string()); + + assert_eq!( + HeaderExtractor(&carrier).get("HEADERNAME"), + Some("value"), + "case insensitive extraction" + ) + } + #[test] + fn http_headers_get_all() { + let mut carrier = http::HeaderMap::new(); + carrier.append("headerName", HeaderValue::from_static("value")); + carrier.append("headerName", HeaderValue::from_static("value2")); + carrier.append("headerName", HeaderValue::from_static("value3")); + + assert_eq!( + HeaderExtractor(&carrier).get_all("HEADERNAME"), + Some(vec!["value", "value2", "value3"]), + "all values from a key extraction" + ) + } + + #[test] + fn http_headers_get_all_missing_key() { + let mut carrier = http::HeaderMap::new(); + carrier.append("headerName", HeaderValue::from_static("value")); + + assert_eq!( + HeaderExtractor(&carrier).get_all("not_existing"), + None, + "all values from a missing key extraction" + ) + } + + #[test] + fn http_headers_keys() { + let mut carrier = http::HeaderMap::new(); + HeaderInjector(&mut carrier).set("headerName1", "value1".to_string()); + HeaderInjector(&mut carrier).set("headerName2", "value2".to_string()); + + let extractor = HeaderExtractor(&carrier); + let got = extractor.keys(); + assert_eq!(got.len(), 2); + assert!(got.contains(&"headername1")); + assert!(got.contains(&"headername2")); + } + + #[test] + fn http_headers_reserve() { + let mut carrier = http::HeaderMap::new(); + + // Test that reserve doesn't panic and works correctly + { + let mut injector = HeaderInjector(&mut carrier); + injector.reserve(10); + + // Verify the HeaderMap still works after reserve + injector.set("test-header", "test-value".to_string()); + } + assert_eq!( + HeaderExtractor(&carrier).get("test-header"), + Some("test-value") + ); + + // Test reserve with zero capacity + { + let mut injector = HeaderInjector(&mut carrier); + injector.reserve(0); + injector.set("another-header", "another-value".to_string()); + } + assert_eq!( + HeaderExtractor(&carrier).get("another-header"), + Some("another-value") + ); + + // Test that capacity is actually reserved (at least the requested amount) + let mut new_carrier = http::HeaderMap::new(); + { + let mut new_injector = HeaderInjector(&mut new_carrier); + new_injector.reserve(5); + } + let initial_capacity = new_carrier.capacity(); + + // Add some headers and verify capacity doesn't decrease + { + let mut new_injector = HeaderInjector(&mut new_carrier); + for i in 0..3 { + new_injector.set(&format!("header-{}", i), format!("value-{}", i)); + } + } + + assert!(new_carrier.capacity() >= initial_capacity); + assert!(new_carrier.capacity() >= 5); + } + + #[test] + fn error_for_status_matches_http_status_class() { + for status in [http::StatusCode::OK, http::StatusCode::NO_CONTENT] { + let response = Response::builder().status(status).body(()).unwrap(); + assert!(response.error_for_status().is_ok()); + } + + for status in [ + http::StatusCode::MOVED_PERMANENTLY, + http::StatusCode::BAD_REQUEST, + http::StatusCode::TOO_MANY_REQUESTS, + http::StatusCode::INTERNAL_SERVER_ERROR, + ] { + let response = Response::builder().status(status).body(()).unwrap(); + assert!(response.error_for_status().is_err()); + } + } + + #[cfg(all( + any(feature = "hyper", feature = "reqwest", feature = "reqwest-blocking"), + not(target_arch = "wasm32") + ))] + fn spawn_error_response_server() -> (SocketAddr, JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let server = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut request = [0; 1024]; + let _ = stream.read(&mut request).unwrap(); + stream + .write_all( + b"HTTP/1.1 429 Too Many Requests\r\n\ +Retry-After: 7\r\n\ +Content-Length: 0\r\n\ +Connection: close\r\n\r\n", + ) + .unwrap(); + }); + (address, server) + } + + #[cfg(all(feature = "reqwest-blocking", not(target_arch = "wasm32")))] + #[test] + fn reqwest_blocking_preserves_error_response_status_and_headers() { + let (address, server) = spawn_error_response_server(); + let client = ::reqwest::blocking::Client::new(); + let request = Request::post(format!("http://{address}/v1/traces")) + .body(Bytes::new()) + .unwrap(); + let response = futures_executor::block_on(client.send_bytes(request)).unwrap(); + + server.join().unwrap(); + assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS); + assert_eq!(response.headers().get("retry-after").unwrap(), "7"); + } + + #[cfg(all(feature = "reqwest", not(target_arch = "wasm32")))] + #[test] + fn reqwest_async_preserves_error_response_status_and_headers() { + let (address, server) = spawn_error_response_server(); + let client = ::reqwest::Client::new(); + let request = Request::post(format!("http://{address}/v1/traces")) + .body(Bytes::new()) + .unwrap(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let response = runtime.block_on(client.send_bytes(request)).unwrap(); + + server.join().unwrap(); + assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS); + assert_eq!(response.headers().get("retry-after").unwrap(), "7"); + } + + #[cfg(all(feature = "hyper", not(target_arch = "wasm32")))] + #[test] + fn hyper_preserves_error_response_status_and_headers() { + let (address, server) = spawn_error_response_server(); + let client = crate::hyper::HyperClient::with_default_connector( + std::time::Duration::from_secs(2), + None, + ); + let request = Request::post(format!("http://{address}/v1/traces")) + .body(Bytes::new()) + .unwrap(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let response = runtime.block_on(client.send_bytes(request)).unwrap(); + + server.join().unwrap(); + assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS); + assert_eq!(response.headers().get("retry-after").unwrap(), "7"); + } + + #[cfg(all( + test, + any(feature = "reqwest", feature = "reqwest-blocking", feature = "hyper") + ))] + mod body_limit_tests { + use super::MAX_RESPONSE_BODY_BYTES; + use crate::HttpClient; + use bytes::Bytes; + use http::Request; + #[cfg(feature = "hyper")] + use std::future::Future; + use std::net::SocketAddr; + #[cfg(feature = "hyper")] + use std::pin::Pin; + #[cfg(feature = "hyper")] + use std::task::{Context, Poll}; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpListener; + + #[cfg(feature = "hyper")] + #[derive(Clone, Debug)] + struct LocalConnector(SocketAddr); + + #[cfg(feature = "hyper")] + impl tower_service::Service for LocalConnector { + type Response = hyper_util::rt::TokioIo; + type Error = std::io::Error; + type Future = + Pin> + Send + 'static>>; + + fn poll_ready(&mut self, _context: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _uri: http::Uri) -> Self::Future { + let address = self.0; + Box::pin(async move { + tokio::net::TcpStream::connect(address) + .await + .map(hyper_util::rt::TokioIo::new) + }) + } + } + + async fn start_server(body_size: usize) -> SocketAddr { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tokio::spawn(async move { + if let Ok((mut socket, _)) = listener.accept().await { + let mut buf = [0u8; 1024]; + let _ = socket.read(&mut buf).await; + let body = vec![b'a'; body_size]; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + let _ = socket.write_all(response.as_bytes()).await; + let _ = socket.write_all(&body).await; + let _ = socket.shutdown().await; + } + }); + addr + } + + #[cfg(feature = "hyper")] + async fn start_stalled_body_server() -> SocketAddr { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tokio::spawn(async move { + if let Ok((mut socket, _)) = listener.accept().await { + let mut buf = [0u8; 1024]; + let _ = socket.read(&mut buf).await; + let _ = socket + .write_all( + b"HTTP/1.1 200 OK\r\nContent-Length: 1\r\nConnection: close\r\n\r\n", + ) + .await; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + }); + addr + } + + async fn assert_body_size(client: &dyn HttpClient, addr: SocketAddr, expected_size: usize) { + let request = Request::builder() + .method("POST") + .uri(format!("http://{}/", addr)) + .body(Bytes::new()) + .unwrap(); + let response = client.send_bytes(request).await.unwrap(); + assert_eq!(response.body().len(), expected_size); + } + + async fn assert_exceeds_limit(client: &dyn HttpClient, addr: SocketAddr) { + let request = Request::builder() + .method("POST") + .uri(format!("http://{}/", addr)) + .body(Bytes::new()) + .unwrap(); + let error = client.send_bytes(request).await.unwrap_err(); + assert!(error + .downcast_ref::() + .is_some()); + } + + #[cfg(feature = "reqwest-blocking")] + fn start_blocking_server(body_size: usize) -> SocketAddr { + use std::io::{Read, Write}; + use std::net::TcpListener; + + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + + std::thread::spawn(move || { + if let Ok((mut socket, _)) = listener.accept() { + let mut buf = [0u8; 1024]; + let _ = socket.read(&mut buf); + + let body = vec![b'a'; body_size]; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + + let _ = socket.write_all(response.as_bytes()); + let _ = socket.write_all(&body); + } + }); + + addr + } + + #[cfg(feature = "reqwest")] + #[tokio::test] + async fn reqwest_body_within_limit() { + let addr = start_server(MAX_RESPONSE_BODY_BYTES).await; + assert_body_size(&reqwest::Client::new(), addr, MAX_RESPONSE_BODY_BYTES).await; + } + + #[cfg(feature = "reqwest")] + #[tokio::test] + async fn reqwest_body_exceeds_limit() { + let addr = start_server(MAX_RESPONSE_BODY_BYTES + 1).await; + assert_exceeds_limit(&reqwest::Client::new(), addr).await; + } + + #[cfg(feature = "reqwest-blocking")] + #[test] + fn reqwest_blocking_body_within_limit() { + let addr = start_blocking_server(MAX_RESPONSE_BODY_BYTES); + + futures_executor::block_on(assert_body_size( + &reqwest::blocking::Client::new(), + addr, + MAX_RESPONSE_BODY_BYTES, + )); + } + + #[cfg(feature = "reqwest-blocking")] + #[test] + fn reqwest_blocking_body_exceeds_limit() { + let addr = start_blocking_server(MAX_RESPONSE_BODY_BYTES + 1); + + futures_executor::block_on(assert_exceeds_limit( + &reqwest::blocking::Client::new(), + addr, + )); + } + + #[cfg(feature = "hyper")] + #[tokio::test] + async fn hyper_body_within_limit() { + let addr = start_server(MAX_RESPONSE_BODY_BYTES).await; + let client = crate::hyper::HyperClient::with_default_connector( + std::time::Duration::from_secs(5), + None, + ); + assert_body_size(&client, addr, MAX_RESPONSE_BODY_BYTES).await; + } + + #[cfg(feature = "hyper")] + #[tokio::test] + async fn hyper_client_new_accepts_custom_connector() { + let addr = start_server(100).await; + let client = crate::hyper::HyperClient::new( + LocalConnector(addr), + std::time::Duration::from_secs(5), + None, + ); + assert_body_size(&client, addr, 100).await; + } + #[cfg(feature = "hyper")] + #[tokio::test] + async fn hyper_body_exceeds_limit() { + let addr = start_server(MAX_RESPONSE_BODY_BYTES + 1).await; + let client = crate::hyper::HyperClient::with_default_connector( + std::time::Duration::from_secs(5), + None, + ); + assert_exceeds_limit(&client, addr).await; + } + + #[cfg(feature = "hyper")] + #[tokio::test] + async fn hyper_timeout_covers_response_body() { + let addr = start_stalled_body_server().await; + let client = crate::hyper::HyperClient::with_default_connector( + std::time::Duration::from_millis(25), + None, + ); + let request = Request::post(format!("http://{addr}/")) + .body(Bytes::new()) + .unwrap(); + + let error = tokio::time::timeout( + std::time::Duration::from_millis(200), + client.send_bytes(request), + ) + .await + .expect("HyperClient must enforce its configured timeout") + .expect_err("stalled response body must time out"); + + assert!(error + .downcast_ref::() + .is_some()); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/CHANGELOG.md b/vendor/opentelemetry-rust/opentelemetry-otlp/CHANGELOG.md new file mode 100644 index 00000000..a1a5547d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/CHANGELOG.md @@ -0,0 +1,687 @@ +# Changelog + +## vNext + +## 0.33.0 + +Released 2026-Sep-18 + +- Exporter builder usage and environment configuration are unchanged. + **Breaking for callers parsing compression strings:** `Compression::from_str` + (including `.parse::()`) now returns the opaque `ParseConfigError` + instead of `ExporterBuildError`. Update explicit result types and error handling + that expects `ExporterBuildError::UnsupportedCompressionAlgorithm`. The new error + implements `Display` and `std::error::Error`; its message is for diagnostics. + Accepted strings and parsing behavior are unchanged. + + ```rust + // Before: + let result: Result = value.parse(); + + // After: + let result: Result = value.parse(); + if let Err(error) = result { + eprintln!("invalid compression configuration: {error}"); + } + ``` + +- Interpret protocol, compression, and metrics temporality environment values + case-insensitively. Treat empty values as unset, and warn and ignore invalid, + non-Unicode, or feature-unavailable enum values so resolution can continue + to the next environment variable or default. Compression `none` explicitly + disables compression, including when a generic compression value is set. + Programmatic configuration remains strict. + +### Retry + +- Retries are now enabled by default for OTLP/HTTP and OTLP/gRPC. The default + policy uses exponential backoff and jitter with up to 3 retries (4 attempts + total). Use `.with_retry_policy(RetryPolicy::disabled())` to disable retries, + or provide a custom `RetryPolicy` to change the behavior. +- **Migration for users of the experimental retry features:** If your + `Cargo.toml` enables `experimental-grpc-retry` or + `experimental-http-retry`, remove those feature flags. No migration action is + required for users who did not enable them. + [#3621](https://github.com/open-telemetry/opentelemetry-rust/pull/3621) +- **Breaking** Make the `retry` and `retry_classification` modules crate-private, + removing their retry engine, error type, and protocol classifiers from the + public API. `RetryPolicy` remains available from the crate root with private + fields and fluent configuration methods. Replace imports from + `opentelemetry_otlp::retry` with `opentelemetry_otlp::RetryPolicy`, and replace + struct literals with its `with_*` methods. + [#3672](https://github.com/open-telemetry/opentelemetry-rust/pull/3672) + +#### Retry fixes + +The following fixes apply to retry behavior that was experimental before this +release: + +- Retry only HTTP status codes 429, 502, 503, and 504, as required by the OTLP + specification. The exporter now also honors `Retry-After` on 503 responses. +- Honor positive gRPC `RetryInfo` delays returned with `Unavailable` responses. +- Continue exponential backoff from server-provided `RetryInfo` and + `Retry-After` delays when subsequent export attempts fail. + +### Other changes + +- Exporter compression configuration and behavior are unchanged; users of + `.with_compression(...)` need no changes. **Breaking only for direct conversion + callers:** removed `TryFrom` for + `tonic::codec::CompressionEncoding`. Code explicitly converting between these + enums must map the variants itself. + +- Return an exporter build error when construction of a built-in reqwest HTTP + client fails instead of silently falling back to a client without the + exporter-configured timeout. Failure to spawn the blocking client's setup + thread, or a panic in that thread, is also returned instead of panicking. + +- **Breaking** Removed `Default` from the `TonicExporterBuilderSet` and + `HttpExporterBuilderSet` typestate markers. This also removes `Default` from + the transport-selected exporter builders (e.g. + `SpanExporterBuilder`). Use the intended builder + flow instead: + ```rust + // Before (no longer compiles): + let exporter = SpanExporterBuilder::::default().build()?; + + // After (use the builder entry point): + let exporter = SpanExporter::builder().with_tonic().build()?; + ``` + Also removed the unused `#[doc(hidden)]` `NoExporterConfig` type. +- **Breaking** Mark `Protocol` and `Compression` as non-exhaustive so new OTLP + protocols, encodings, and compression algorithms can be added without + breaking downstream users. External exhaustive matches must add a wildcard + arm. Constructing existing variants and passing them to exporter builders is + unchanged. + ```rust + let protocol_name = match protocol { + Protocol::Grpc => "grpc", + Protocol::HttpBinary => "http/protobuf", + Protocol::HttpJson => "http/json", + _ => "unknown", // Required because Protocol is non-exhaustive. + }; + ``` +- **Breaking** Make `Protocol::from_env()` crate-private. Exporter builders + already resolve `OTEL_EXPORTER_OTLP_PROTOCOL` when built; applications that + need to inspect the raw environment setting should read the variable + directly. +- **Breaking** Remove `OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT`, which always held + the HTTP default (`http://localhost:4318`) despite gRPC using + `http://localhost:4317`. Omit `.with_endpoint(...)` to let the selected + transport use its correct default, or provide the appropriate URL explicitly. + [#3690](https://github.com/open-telemetry/opentelemetry-rust/issues/3690) +- **Breaking** Restrict `MetricExporterBuilder::with_http()` and `with_tonic()` + to builders where no transport has been selected, matching the span and log + exporter builders. Select a transport once; `with_temporality()` remains + available before or after transport selection. +- **Breaking** Remove the public `HttpExporterBuilder` and + `TonicExporterBuilder` transport-first APIs. Configure transports through the + signal builders instead: + - Replace `HttpExporterBuilder::default()` with the corresponding signal + exporter builder followed by `.with_http()`, then replace + `.build_span_exporter()` or `.build_log_exporter()` with `.build()`. + - Replace `.build_metrics_exporter(temporality)` with + `.with_temporality(temporality).build()`. + - Replace `TonicExporterBuilder::default()` with the corresponding signal + exporter builder followed by `.with_tonic()`. + Transport-specific configuration methods remain available after + `.with_http()` or `.with_tonic()`. +- **Breaking** Removed the deprecated `tls` feature alias. Replace `tls` with + `tls-ring`, or select `tls-aws-lc` or `tls-provider-agnostic` explicitly. +- Exporter builder usage is unchanged. **Breaking for code matching or constructing + removed error variants:** Simplified `ExporterBuildError` to the exhaustive + `InvalidConfiguration(String)` and `InternalFailure(String)` variants. + The enum is no longer marked `#[non_exhaustive]`. + Configuration errors such as invalid endpoints, missing HTTP clients, + transport/protocol mismatches, and missing compression features now use + `InvalidConfiguration`. Replace implementation-specific, non-exhaustive + matches such as: + ```rust + match error { + ExporterBuildError::InvalidUri(_, _) + | ExporterBuildError::InvalidConfig { .. } + | ExporterBuildError::NoHttpClient => { + eprintln!("fix the exporter configuration"); + } + ExporterBuildError::InternalFailure(message) => { + eprintln!("exporter initialization failed: {message}"); + } + _ => {} + } + ``` + with an exhaustive match over the two stable categories: + ```rust + match error { + ExporterBuildError::InvalidConfiguration(message) => { + eprintln!("fix the exporter configuration: {message}"); + } + ExporterBuildError::InternalFailure(message) => { + eprintln!("exporter initialization failed: {message}"); + } + } + ``` + Code that propagates build errors with `?` without inspecting their variants + needs no changes. + Tonic endpoint errors identify the originating environment variable when + validating the URI or reporting endpoint-related TLS setup failures. + [#3691](https://github.com/open-telemetry/opentelemetry-rust/issues/3691) +- Return an exporter build error for invalid OTLP/HTTP endpoint environment + variables instead of silently falling back to another endpoint or localhost. + Empty endpoint environment variables are now treated as unset. +- Return an exporter build error for invalid OTLP/gRPC endpoint environment + variables instead of silently falling back to another endpoint or localhost. + Empty endpoint environment variables are now treated as unset. +- Add `WithHttpConfig::with_max_request_body_size` to configure the HTTP request + body limit. OTLP/HTTP request bodies are now limited to 64 MiB by default, before and + after compression; oversized requests are discarded without being sent or + retried. +- **Breaking** Seal `WithExportConfig`, `WithHttpConfig`, and + `WithTonicConfig`. These traits remain public for calling configuration + methods on OTLP builders, but can no longer be implemented for external + types. +- Add support for INSECURE environment variables for gRPC (env-var-only, no builder method, per spec): + `OTEL_EXPORTER_OTLP_INSECURE` (generic), `OTEL_EXPORTER_OTLP_TRACES_INSECURE`, + `OTEL_EXPORTER_OTLP_METRICS_INSECURE`, `OTEL_EXPORTER_OTLP_LOGS_INSECURE`. + Per the spec, these only apply to gRPC connections. When an endpoint has no explicit scheme, + `INSECURE=true` uses `http://`, `INSECURE=false` (default) uses `https://` with auto-TLS. + **Breaking:** Schemeless endpoints (e.g., `collector.example.com:4317`) now default to `https://` + instead of being passed as-is. Set `OTEL_EXPORTER_OTLP_INSECURE=true` for plaintext connections. + Endpoints with an explicit scheme (e.g., `http://`, `https://`, `unix://`) are unaffected. + [#774](https://github.com/open-telemetry/opentelemetry-rust/issues/774) + [#984](https://github.com/open-telemetry/opentelemetry-rust/issues/984) +- **Breaking** Removed the `serialize` feature flag and its implicit `serde` + dependency. This feature gated `Serialize`/`Deserialize` derives on + `Protocol` and `Compression`, but the derived representations were incorrect + (Rust variant names instead of spec values) and the feature only covered + these two enums. The equivalent feature was removed from the core + `opentelemetry` crate in 2022. + **Migration**: Remove `serialize` (and `serde`, if listed) from your feature + list. If these values are part of serialisable app config, define a local + config enum or wrapper and convert it to `Protocol` or `Compression` when + building the exporter. + [#3711](https://github.com/open-telemetry/opentelemetry-rust/pull/3711) +- **Breaking** Removed `reqwest-rustls-webpki-roots` feature. The `webpki-roots` cargo feature was + removed from `reqwest` in v0.13.0, making this feature broken for anyone resolving `reqwest >= 0.13.0`. + **Migration**: Use `reqwest-rustls` instead (now correctly uses `reqwest/rustls` with platform native + trust roots). If you specifically need Mozilla's embedded CA bundle, construct a custom client: + ```rust + let root_store = rustls::RootCertStore::from_iter( + webpki_roots::TLS_SERVER_ROOTS.iter().cloned(), + ); + let tls_config = rustls::ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + let client = reqwest::Client::builder() + .tls_backend_preconfigured(tls_config) + .build()?; + exporter_builder.with_http_client(client) + ``` +- Allow to provide http client wrapped in Arc when configuring HTTP exporter. [3468](https://github.com/open-telemetry/opentelemetry-rust/pull/3468) + +## 0.32.0 + +Released 2026-May-08 + +- Add `tls-provider-agnostic` feature flag for environments that require a custom crypto backend (e.g., OpenSSL for FIPS compliance). Enables TLS code paths without bundling `ring` or `aws-lc-rs`. +- Add `build()` directly on `SpanExporterBuilder`, `MetricExporterBuilder`, and `LogExporterBuilder` + (before selecting a transport), which auto-selects the transport based on the + `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable or enabled features. + [#3394](https://github.com/open-telemetry/opentelemetry-rust/pull/3394) +- **Breaking** Removed `ExportConfig`, `HasExportConfig`, `with_export_config()`, `HasTonicConfig`, `HasHttpConfig`, `TonicConfig`, and `HttpConfig` from public API. + Use the public `WithExportConfig`, `WithTonicConfig`, and `WithHttpConfig` trait methods instead, which remain unchanged. +- The gRPC/tonic OTLP exporter's build method now returns an error for all signals (traces, metrics, logs) when + an `https://` endpoint is configured but no TLS feature (`tls-ring` or `tls-aws-lc`) is enabled, instead of + silently sending unencrypted traffic. When a TLS feature is enabled and an `https://` endpoint is used without + an explicit `.with_tls_config()`, a default `ClientTlsConfig` is automatically applied. + [#3182](https://github.com/open-telemetry/opentelemetry-rust/issues/3182) +- Prevent auth tokens from leaking in export error messages. gRPC and HTTP + exporter errors no longer include potentially sensitive server responses + (e.g., authentication tokens echoed back). Error messages returned to SDK + processors contain only the gRPC status code or HTTP status code. Full + details are logged at DEBUG level only. + [#3021](https://github.com/open-telemetry/opentelemetry-rust/issues/3021) +- Surface pre-flight transport error details at ERROR level when `grpc-tonic` + OTLP export fails due to a local misconfiguration. When the returned + `tonic::Status` wraps a local transport error (invalid URL, connect failure, + DNS), its source chain (e.g., `"transport error: invalid URI"`) is appended + to the returned error so SDK processors surface it at ERROR without + requiring DEBUG logging. Server-returned gRPC status messages remain + DEBUG-only to preserve the auth-token leak safeguards from + [#3021](https://github.com/open-telemetry/opentelemetry-rust/issues/3021). + [#3331](https://github.com/open-telemetry/opentelemetry-rust/issues/3331) +- Add support for per-signal protocol environment variables: + `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL`, `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL`, + `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL`. These allow configuring different transport protocols + per signal type. Signal-specific vars take precedence over generic `OTEL_EXPORTER_OTLP_PROTOCOL`. + The auto-select `build()` method on each exporter builder now respects the full priority chain: + signal-specific env var > generic env var > feature-based default. +- Transport/protocol mismatch validation: HTTP transport returns `InvalidConfig` when gRPC protocol + is requested; gRPC transport returns `InvalidConfig` when an HTTP protocol is requested. +- **Breaking**: `Protocol::default()` no longer consults the `OTEL_EXPORTER_OTLP_PROTOCOL` + environment variable. It now returns only the feature-based default (http-json > http-proto > + grpc-tonic). Protocol resolution from environment variables is handled internally by the + exporter builders. Users who relied on `Protocol::default()` to read env vars should use + `Protocol::from_env()` instead. +- Add support for `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` environment variable + to configure metrics temporality. Accepted values: `cumulative` (default), `delta`, + `lowmemory` (case-insensitive). Programmatic `.with_temporality()` overrides the env var. +- Fix `NoHttpClient` error when multiple HTTP client features are enabled by using priority-based selection (`reqwest-client` > `hyper-client` > `reqwest-blocking-client`). [#2994](https://github.com/open-telemetry/opentelemetry-rust/issues/2994) +- Add partial success response handling for OTLP exporters (traces, metrics, logs) per OTLP spec. Exporters now log warnings when the server returns partial success responses with rejected items and error messages. [#865](https://github.com/open-telemetry/opentelemetry-rust/issues/865) +- Refactor `internal-logs` feature in `opentelemetry-otlp` to reduce unnecessary dependencies[3191](https://github.com/open-telemetry/opentelemetry-rust/pull/3192) +- Fixed [#2777](https://github.com/open-telemetry/opentelemetry rust/issues/2777) to properly handle `shutdown_with_timeout()` when using `grpc-tonic`. +- Deprecate `tls` feature in favor of explicit `tls-ring` and `tls-aws-lc` features. + **Migration**: Replace `tls` with `tls-ring` (or `tls-aws-lc`). Users of `tls-roots` or `tls-webpki-roots` must now also enable one of these. +- Prevent logging of header values in OTLP tonic exporter [#3465](https://github.com/open-telemetry/opentelemetry-rust/pull/3465) + +## 0.31.0 + +Released 2025-Sep-25 + +- Update `opentelemetry-proto` and `opentelemetry-http` dependency version to 0.31.0 +- Add HTTP compression support with `gzip-http` and `zstd-http` feature flags +- Add retry with exponential backoff and throttling support for HTTP and gRPC exporters + This behaviour is opt in via the `experimental-grpc-retry` and `experimental-http-retry` flags on this crate. You can customize the retry policy using the `with_retry_policy` on the exporter builders. + +## 0.30.0 + +Released 2025-May-23 + +- Update `opentelemetry` dependency version to 0.30 +- Update `opentelemetry_sdk` dependency version to 0.30 +- Update `opentelemetry-http` dependency version to 0.30 +- Update `opentelemetry-proto` dependency version to 0.30 +- Update `tonic` dependency version to 0.13 +- Re-export `tonic` types under `tonic_types` + [2898](https://github.com/open-telemetry/opentelemetry-rust/pull/2898) +- Publicly re-exported `MetricExporterBuilder`, `SpanExporterBuilder`, and + `LogExporterBuilder` types, enabling users to directly reference and use these + builder types for metrics, traces, and logs exporters. + [2966](https://github.com/open-telemetry/opentelemetry-rust/pull/2966) + +## 0.29.0 + +Released 2025-Mar-21 + +- Update `opentelemetry` dependency version to 0.29 +- Update `opentelemetry_sdk` dependency version to 0.29 +- Update `opentelemetry-http` dependency version to 0.29 +- Update `opentelemetry-proto` dependency version to 0.29 + +- The `OTEL_EXPORTER_OTLP_TIMEOUT`, `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT`, `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` and `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` are changed from seconds to milliseconds. +- Fixed `.with_headers()` in `HttpExporterBuilder` to correctly support multiple key/value pairs. [#2699](https://github.com/open-telemetry/opentelemetry-rust/pull/2699) +- Fixed + [#2770](https://github.com/open-telemetry/opentelemetry-rust/issues/2770) + partially to properly handle `shutdown()` when using `http`. (`tonic` still + does not do proper shutdown) +- *Breaking* + ExporterBuilder's build() method now Result with `ExporterBuildError` being the + Error variant. Previously it returned signal specific errors like `LogError` + from the `opentelemetry_sdk`, which are no longer part of the sdk. No changes + required if you were using unwrap/expect. If you were matching on the returning + Error enum, replace with the enum `ExporterBuildError`. Unlike the previous + `Error` which contained many variants unrelated to building an exporter, the + new one returns specific variants applicable to building an exporter. Some + variants might be applicable only on select features. + Also, now unused `Error` enum is removed. +- **Breaking** `ExportConfig`'s `timeout` field is now optional(`Option`) +- **Breaking** Export configuration done via code is final. ENV variables cannot be used to override the code config. + Do not use code based config, if there is desire to control the settings via ENV variables. + List of ENV variables and corresponding setting being affected by this change. + - `OTEL_EXPORTER_OTLP_ENDPOINT` -> `ExportConfig.endpoint` + - `OTEL_EXPORTER_OTLP_TIMEOUT` -> `ExportConfig.timeout` + +## 0.28.0 + +Released 2025-Feb-10 + +- Update `opentelemetry` dependency version to 0.28. +- Update `opentelemetry_sdk` dependency version to 0.28. +- Update `opentelemetry-http` dependency version to 0.28. +- Update `opentelemetry-proto` dependency version to 0.28. +- Bump msrv to 1.75.0. +- Feature flag "populate-logs-event-name" is removed as no longer relevant. + LogRecord's `event_name()` is now automatically populated on the newly added + "event_name" field in LogRecord proto definition. +- Remove "grpc-tonic" feature from default, and instead add "http-proto" and + "reqwest-blocking-client" features as default, to align with the + specification. + [2516](https://github.com/open-telemetry/opentelemetry-rust/pull/2516) +- Remove unnecessarily public trait `opentelemetry_otlp::metrics::MetricsClient` + and `MetricExporter::new(..)` method. Use + `MetricExporter::builder()...build()` to obtain `MetricExporter`. +- The HTTP clients (reqwest, reqwest-blocking, hyper) now support the + export timeout interval configured in below order + - Signal specific env variable `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT`, + `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` or `OTEL_EXPORTER_OTLP_TIMEOUT`. + - `OTEL_EXPORTER_OTLP_TIMEOUT` env variable. + - `with_http().with_timeout()` API method of +`LogExporterBuilder` and `SpanExporterBuilder` and `MetricsExporterBuilder`. + - The default interval of 10 seconds is used if none is configured. + +## 0.27.0 + +Released 2024-Nov-11 + +- Update `opentelemetry` dependency version to 0.27 +- Update `opentelemetry_sdk` dependency version to 0.27 +- Update `opentelemetry-http` dependency version to 0.27 +- Update `opentelemetry-proto` dependency version to 0.27 + +- **BREAKING**: + - ([#2217](https://github.com/open-telemetry/opentelemetry-rust/pull/2217)) **Replaced**: The `MetricsExporterBuilder` interface is modified from `with_temporality_selector` to `with_temporality` example can be seen below: + Previous Signature: + ```rust + MetricsExporterBuilder::default().with_temporality_selector(DeltaTemporalitySelector::new()) + ``` + Updated Signature: + ```rust + MetricsExporterBuilder::default().with_temporality(opentelemetry_sdk::metrics::Temporality::Delta) + ``` + - ([#2221](https://github.com/open-telemetry/opentelemetry-rust/pull/2221)) **Replaced**: + - The `opentelemetry_otlp::new_pipeline().{trace,logging,metrics}()` interface is now replaced with `{TracerProvider,SdkMeterProvider,LoggerProvider}::builder()`. + - The `opentelemetry_otlp::new_exporter()` interface is now replaced with `{SpanExporter,MetricsExporter,LogExporter}::builder()`. + + Pull request [#2221](https://github.com/open-telemetry/opentelemetry-rust/pull/2221) has a detailed migration guide in the description. See example below, + and [basic-otlp](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp/src/main.rs) for more details: + + Previous Signature: + ```rust + let logger_provider: LoggerProvider = opentelemetry_otlp::new_pipeline() + .logging() + .with_resource(RESOURCE.clone()) + .with_exporter( + opentelemetry_otlp::new_exporter() + .tonic() + .with_endpoint("http://localhost:4317") + ) + .install_batch(runtime::Tokio)?; + ``` + Updated Signature: + ```rust + let exporter = LogExporter::builder() + .with_tonic() + .with_endpoint("http://localhost:4317") + .build()?; + + Ok(LoggerProvider::builder() + .with_resource(RESOURCE.clone()) + .with_batch_exporter(exporter, runtime::Tokio) + .build()) + ``` + - **Renamed** + - ([#2255](https://github.com/open-telemetry/opentelemetry-rust/pull/2255)): de-pluralize Metric types. + - `MetricsExporter` -> `MetricExporter` + - `MetricsExporterBuilder` -> `MetricExporterBuilder` + + - [#2263](https://github.com/open-telemetry/opentelemetry-rust/pull/2263) + Support `hyper` client for opentelemetry-otlp. This can be enabled using flag `hyper-client`. + Refer example: https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-otlp/examples/basic-otlp-http + +## v0.26.0 +Released 2024-Sep-30 + +- Update `opentelemetry` dependency version to 0.26 +- Update `opentelemetry_sdk` dependency version to 0.26 +- Update `opentelemetry-http` dependency version to 0.26 +- Update `opentelemetry-proto` dependency version to 0.26 +- Bump MSRV to 1.71.1 [2140](https://github.com/open-telemetry/opentelemetry-rust/pull/2140) + +## v0.25.0 + +- Update `opentelemetry` dependency version to 0.25 +- Update `opentelemetry_sdk` dependency version to 0.25 +- Update `opentelemetry-http` dependency version to 0.25 +- Update `opentelemetry-proto` dependency version to 0.25 +- Starting with this version, this crate will align with `opentelemetry` crate + on major,minor versions. +- **Breaking** +The logrecord event-name is added as an attribute only if the feature flag +`populate-logs-event-name` is enabled. The name of the attribute is changed from +"name" to "event.name". +[1994](https://github.com/open-telemetry/opentelemetry-rust/pull/1994), +[2050](https://github.com/open-telemetry/opentelemetry-rust/pull/2050) + +## v0.17.0 + +- Add "metrics", "logs" to default features. With this, default feature list is + "trace", "metrics" and "logs". +- **Breaking** `OtlpMetricPipeline.build()` no longer invoke the + `global::set_meter_provider`. User who setup the pipeline must do it + themselves using `global::set_meter_provider(meter_provider.clone());`. +- Add `with_resource` on `OtlpLogPipeline`, replacing the `with_config` method. +Instead of using +`.with_config(Config::default().with_resource(RESOURCE::default()))` users must +now use `.with_resource(RESOURCE::default())` to configure Resource when using +`OtlpLogPipeline`. +- **Breaking** The methods `OtlpTracePipeline::install_simple()` and `OtlpTracePipeline::install_batch()` would now return `TracerProvider` instead of `Tracer`. + These methods would also no longer set the global tracer provider. It would now be the responsibility of users to set it by calling `global::set_tracer_provider(tracer_provider.clone());`. Refer to the [basic-otlp](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp/src/main.rs) and [basic-otlp-http](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp-http/src/main.rs) examples on how to initialize OTLP Trace Exporter. +- **Breaking** Correct the misspelling of "webkpi" to "webpki" in features [#1842](https://github.com/open-telemetry/opentelemetry-rust/pull/1842) +- Bump MSRV to 1.70 [#1840](https://github.com/open-telemetry/opentelemetry-rust/pull/1840) +- Fixing the OTLP HTTP/JSON exporter. [#1882](https://github.com/open-telemetry/opentelemetry-rust/pull/1882) - The exporter was broken in the + previous release. +- **Breaking** [1869](https://github.com/open-telemetry/opentelemetry-rust/pull/1869) The OTLP logs exporter now overrides the [InstrumentationScope::name](https://github.com/open-telemetry/opentelemetry-proto/blob/b3060d2104df364136d75a35779e6bd48bac449a/opentelemetry/proto/common/v1/common.proto#L73) field with the `target` from `LogRecord`, if target is populated. +- Groups batch of `LogRecord` and `Span` by their resource and instrumentation scope before exporting, for better efficiency [#1873](https://github.com/open-telemetry/opentelemetry-rust/pull/1873). +- **Breaking** Update to `http` v1 and `tonic` v0.12 [#1674](https://github.com/open-telemetry/opentelemetry-rust/pull/1674) +- Update `opentelemetry` dependency version to 0.24 +- Update `opentelemetry_sdk` dependency version to 0.24 +- Update `opentelemetry-http` dependency version to 0.13 +- Update `opentelemetry-proto` dependency version to 0.7 + +## v0.16.0 + +### Fixed + +- URL encoded values in `OTEL_EXPORTER_OTLP_HEADERS` are now correctly decoded. [#1578](https://github.com/open-telemetry/opentelemetry-rust/pull/1578) +- OTLP exporter will not change the URL added through `ExportConfig` [#1706](https://github.com/open-telemetry/opentelemetry-rust/pull/1706) +- Default grpc endpoint will not have path based on signal(e.g `/v1/traces`) [#1706](https://github.com/open-telemetry/opentelemetry-rust/pull/1706) +- Fix feature flags for `OTEL_EXPORTER_OTLP_PROTOCOL_DEFAULT` [#1746](https://github.com/open-telemetry/opentelemetry-rust/pull/1746) + +### Added + +- Added `DeltaTemporalitySelector` ([#1568]) +- Add `webpki-roots` features to `reqwest` and `tonic` backends + +[#1568]: https://github.com/open-telemetry/opentelemetry-rust/pull/1568 + +### Changed + - **Breaking** Remove global provider for Logs [#1691](https://github.com/open-telemetry/opentelemetry-rust/pull/1691/) + - The method OtlpLogPipeline::install_simple() and OtlpLogPipeline::install_batch() now return `LoggerProvider` instead of + `Logger`. Refer to the [basic-otlp](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp/src/main.rs) and [basic-otlp-http](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp-http/src/main.rs) examples for how to initialize OTLP Log Exporter to use with OpenTelemetryLogBridge and OpenTelemetryTracingBridge respectively. +- Update `opentelemetry` dependency version to 0.23 +- Update `opentelemetry_sdk` dependency version to 0.23 +- Update `opentelemetry-http` dependency version to 0.12 +- Update `opentelemetry-proto` dependency version to 0.6 + +## v0.15.0 + +### Added + +- Support custom channels in topic exporters [#1335](https://github.com/open-telemetry/opentelemetry-rust/pull/1335) +- Allow specifying OTLP Tonic metadata from env variable [#1377](https://github.com/open-telemetry/opentelemetry-rust/pull/1377) + +### Changed +- Update to tonic 0.11 and prost 0.12 [#1536](https://github.com/open-telemetry/opentelemetry-rust/pull/1536) + +### Fixed +- Fix `tonic()` to the use correct port. [#1556](https://github.com/open-telemetry/opentelemetry-rust/pull/1556) + +### Removed +- **Breaking** Remove support for surf HTTP client [#1537](https://github.com/open-telemetry/opentelemetry-rust/pull/1537) +- **Breaking** Remove support for grpcio transport [#1534](https://github.com/open-telemetry/opentelemetry-rust/pull/1534) + +## v0.14.0 + +### Added + +- Add `build_{signal}_exporter` methods to client builders [#1187](https://github.com/open-telemetry/opentelemetry-rust/pull/1187) +- Add `grpcio` metrics exporter [#1202](https://github.com/open-telemetry/opentelemetry-rust/pull/1202) +- Allow specifying OTLP HTTP headers from env variable [#1290](https://github.com/open-telemetry/opentelemetry-rust/pull/1290) + +### Changed + +- Bump MSRV to 1.65 [#1318](https://github.com/open-telemetry/opentelemetry-rust/pull/1318) +- Bump MSRV to 1.64 [#1203](https://github.com/open-telemetry/opentelemetry-rust/pull/1203) +- Changed dependency from `opentelemetry_api` to `opentelemetry` as the latter + is now the API crate. [#1226](https://github.com/open-telemetry/opentelemetry-rust/pull/1226) +- Make `NoExporterBuilder` a compiling time error [#1272](https://github.com/open-telemetry/opentelemetry-rust/pull/1272) + +## v0.13.0 + +### Added + +- Add OTLP HTTP Metrics Exporter [#1020](https://github.com/open-telemetry/opentelemetry-rust/pull/1020). +- Add tonic compression support [#1165](https://github.com/open-telemetry/opentelemetry-rust/pull/1165). + +### Changed + +- make the tonic/transport feature optional #985 +- update to opentelemetry-api v0.20.0 + +### Fixed + +- Fix a missing import when http-proto is enabled without grpc-sys #1081 + +## v0.12.0 + +### Added + +- Add batch config for otlp pipeline [#979](https://github.com/open-telemetry/opentelemetry-rust/pull/979). +- Add tonic interceptor [#901](https://github.com/open-telemetry/opentelemetry-rust/pull/901). + +### Changed + +- Update `opentelemetry` to 0.19. +- Update `opentelemetry-semantic-conventions` to 0.11. +- Update `opentelemetry-http` to 0.8. +- Bump MSRV to 1.57 [#953](https://github.com/open-telemetry/opentelemetry-rust/pull/953). +- Add `User-Agent` header on all exporters [#896](https://github.com/open-telemetry/opentelemetry-rust/pull/896). +- Improve OTLP exporter environment variable handling [#912](https://github.com/open-telemetry/opentelemetry-rust/pull/912). +- Fix the issue where tonic exporter builder ignored provided metadata [#937](https://github.com/open-telemetry/opentelemetry-rust/pull/937). +- Export `MetricsExporterBuilder` [#943](https://github.com/open-telemetry/opentelemetry-rust/pull/943). +- Report OTLP http export errors [#945](https://github.com/open-telemetry/opentelemetry-rust/pull/945). +- Change to export using v0.19.0 protobuf definitions. [#989](https://github.com/open-telemetry/opentelemetry-rust/pull/989). +- Update dependencies and bump MSRV to 1.60 [#969](https://github.com/open-telemetry/opentelemetry-rust/pull/969). + + +## v0.11.0 + +### Changed + +- reduce `tokio` feature requirements #750 +- Update to opentelemetry v0.18.0 +- Update to opentelemetry-http v0.7.0 +- Update `tonic` to 0.7 #783 +- Automatically add traces / metrics paths #806 + +## v0.10.0 + +### Changed + +- Update to opentelemetry v0.17.0 +- Update to opentelemetry-http v0.6.0 +- Update `tonic` to 0.6 #660 + +## v0.9.0 + +### Changed + +- Merge metrics and tracing pipeline #585 +- Update to opentelemetry v0.16.0 + +### Fixed + +- `MetricsExporterBuilder` drops `exporter_pipeline` #590 +- Improve error messages #603 +- Upgrade `tonic` to `0.5.x` #597 + +## v0.8.0 + +### Changed + +- Update grpcio version and add the coverage badge #556 +- Update to opentelemetry v0.15.0 + +## v0.7.0 + +### Added + +- adding otlp http transport, using proto binary #516 + +### Fixed + +- docs cannot compile #507 +- exporter cannot merge IntSum correctly. #518 +- update metrics proto and metric transformation #535 + +### Changed + +- Allow users to bring their own tonic channel #515 +- Remove default surf features #546 +- Update to opentelemetry v0.14.0 + +### v0.6.0 + +### Added +- Examples on how to connect to an external otlp using tonic, tls and tokio #449 +- Examples on how to connect to an external otlp using grpcio and tls #450 +- `with_env` method for `OtlpPipelineBuilder` to use environment variables to config otlp pipeline #451 +- Update `tracing-grpc` example to include extractors and injectors #464 +- Mentioned `service.name` resource in README #476 + +### Changed +- Update to opentelemetry v0.13.0 +- Update `tonic-build` dependency to 0.4 #463 +- Update the opentelemetry pipeline to use API to choose grpc layer instead of feature #467 +- Rename trace config with_default_sampler to with_sampler #482 + +### Removed +- Removed `from_env` and use environment variables to initialize the configurations by default #459 +- Removed support for running tonic without tokio runtime #483 + +## v0.5.0 + +### Added + +- Otlp metric exporter #402 +- Otlp exporter integration test #424 + +### Changed + +- Update to opentelemetry v0.12.0 +- Update tokio to v1 #421 + +## v0.4.0 + +### Added + +- Tonic support #352 +- Add openssl feature flags for grpcio #367 + +### Changed + +- Update to opentelemetry v0.11.0 +- Update default otlp port to `4317` #388 + +### Fixed + +- Propagate `Resource` information #366 +- Propagate `Resource` in tonic as well #390 + +## v0.3.0 + +### Changed + +- Update to opentelemetry v0.10.0 + +## v0.2.0 + +### Changed + +- Update to opentelemetry v0.9.0 +- Add exporter pipeline #210 + +## v0.1.0 + +### Added + +- Initial Alpha implementation diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/Cargo.toml b/vendor/opentelemetry-rust/opentelemetry-otlp/Cargo.toml new file mode 100644 index 00000000..85bb0d6c --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/Cargo.toml @@ -0,0 +1,271 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.75.0" +name = "opentelemetry-otlp" +version = "0.33.0" +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Exporter for the OpenTelemetry Collector" +homepage = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-otlp" +readme = "README.md" +keywords = [ + "opentelemetry", + "otlp", + "logging", + "tracing", + "metrics", +] +categories = [ + "development-tools::debugging", + "development-tools::profiling", + "asynchronous", +] +license = "Apache-2.0" +repository = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-otlp" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[features] +default = [ + "http-proto", + "reqwest-blocking-client", + "trace", + "metrics", + "logs", + "internal-logs", +] +grpc-tonic = [ + "tonic", + "tonic-types", + "prost", + "http", + "tokio", + "opentelemetry-proto/gen-tonic", +] +gzip-http = ["flate2"] +gzip-tonic = ["tonic/gzip"] +http-json = [ + "serde_json", + "prost", + "opentelemetry-http", + "opentelemetry-proto/gen-tonic-messages", + "opentelemetry-proto/with-serde", + "http", + "httpdate", + "trace", + "metrics", +] +http-proto = [ + "prost", + "opentelemetry-http", + "opentelemetry-proto/gen-tonic-messages", + "http", + "httpdate", + "trace", + "metrics", +] +hyper-client = [ + "opentelemetry-http/hyper", + "tokio", +] +integration-testing = [ + "tonic", + "prost", + "tokio/full", + "trace", + "logs", +] +internal-logs = [ + "opentelemetry_sdk/internal-logs", + "opentelemetry/internal-logs", +] +logs = [ + "opentelemetry/logs", + "opentelemetry_sdk/logs", + "opentelemetry-proto/logs", +] +metrics = [ + "opentelemetry/metrics", + "opentelemetry_sdk/metrics", + "opentelemetry-proto/metrics", +] +reqwest-blocking-client = [ + "reqwest/blocking", + "opentelemetry-http/reqwest-blocking", +] +reqwest-client = [ + "reqwest", + "opentelemetry-http/reqwest", + "tokio", +] +reqwest-rustls = [ + "reqwest", + "opentelemetry-http/reqwest-rustls", +] +tls-aws-lc = ["tonic/tls-aws-lc"] +tls-provider-agnostic = ["tonic/_tls-any"] +tls-ring = ["tonic/tls-ring"] +tls-roots = ["tonic/tls-native-roots"] +tls-webpki-roots = ["tonic/tls-webpki-roots"] +trace = [ + "opentelemetry/trace", + "opentelemetry_sdk/trace", + "opentelemetry-proto/trace", +] +zstd-http = ["zstd"] +zstd-tonic = ["tonic/zstd"] + +[lib] +name = "opentelemetry_otlp" +path = "src/lib.rs" +bench = false + +[[test]] +name = "smoke" +path = "tests/smoke.rs" +required-features = ["integration-testing"] + +[dependencies.flate2] +version = "1.1.2" +optional = true + +[dependencies.http] +version = "1.1" +features = ["std"] +optional = true +default-features = false + +[dependencies.httpdate] +version = "1.0.3" +optional = true + +[dependencies.opentelemetry] +version = "0.33" +default-features = false + +[dependencies.opentelemetry-http] +version = "0.33" +optional = true +default-features = false + +[dependencies.opentelemetry-proto] +version = "0.33" +default-features = false + +[dependencies.opentelemetry_sdk] +version = "0.33" +default-features = false + +[dependencies.prost] +version = "0.14" +optional = true + +[dependencies.reqwest] +version = "0.13.1" +optional = true +default-features = false + +[dependencies.serde_json] +version = "1.0" +optional = true + +[dependencies.thiserror] +version = "2" +default-features = false + +[dependencies.tokio] +version = "1" +features = [ + "sync", + "rt", + "time", +] +optional = true +default-features = false + +[dependencies.tonic] +version = "0.14.1" +optional = true +default-features = false + +[dependencies.tonic-types] +version = "0.14.1" +optional = true + +[dependencies.zstd] +version = "0.13" +optional = true + +[dev-dependencies.async-trait] +version = "0.1" + +[dev-dependencies.futures-executor] +version = "0.3" + +[dev-dependencies.futures-util] +version = "0.3" +default-features = false + +[dev-dependencies.opentelemetry_sdk] +version = "0.33" +features = [ + "trace", + "testing", +] +default-features = false + +[dev-dependencies.temp-env] +version = "0.3.6" + +[dev-dependencies.tokio] +version = "1" +features = [ + "macros", + "rt-multi-thread", +] +default-features = false + +[dev-dependencies.tokio-stream] +version = "0.1" +features = ["net"] + +[dev-dependencies.tonic] +version = "0.14.1" +features = [ + "router", + "server", +] +default-features = false + +[lints.clippy.all] +level = "warn" +priority = 1 + +[lints.rust] +edition_2024_expr_fragment_specifier = "allow" +if_let_rescope = "allow" +tail_expr_drop_order = "allow" + +[lints.rust.rust_2024_compatibility] +level = "warn" +priority = -1 diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/README.md b/vendor/opentelemetry-rust/opentelemetry-otlp/README.md new file mode 100644 index 00000000..2c8df5a4 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/README.md @@ -0,0 +1,85 @@ +# OpenTelemetry OTLP Exporter + +![OpenTelemetry — An observability framework for cloud-native software.][splash] + +[splash]: https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo-text.png + +This crate contains the [OpenTelemetry](https://opentelemetry.io/) Exporter +implementation for +[OTLP](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md). + +[![Crates.io: opentelemetry-otlp](https://img.shields.io/crates/v/opentelemetry-otlp.svg)](https://crates.io/crates/opentelemetry-otlp) +[![Documentation](https://docs.rs/opentelemetry-otlp/badge.svg)](https://docs.rs/opentelemetry-otlp) +[![LICENSE](https://img.shields.io/crates/l/opentelemetry-otlp)](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/LICENSE) +[![GitHub Actions CI](https://github.com/open-telemetry/opentelemetry-rust/workflows/CI/badge.svg)](https://github.com/open-telemetry/opentelemetry-rust/actions?query=workflow%3ACI+branch%3Amain) +[![codecov](https://codecov.io/gh/open-telemetry/opentelemetry-rust/branch/main/graph/badge.svg)](https://codecov.io/gh/open-telemetry/opentelemetry-rust) +[![Slack](https://img.shields.io/badge/slack-@cncf/otel/rust-brightgreen.svg?logo=slack)](https://cloud-native.slack.com/archives/C03GDP0H023) + +## Overview + +OpenTelemetry is an Observability framework and toolkit designed to create and +manage telemetry data such as traces, metrics, and logs. OpenTelemetry is +vendor- and tool-agnostic, meaning that it can be used with a broad variety of +Observability backends, including open source tools like [Jaeger] and +[Prometheus], as well as commercial offerings. + +OpenTelemetry is *not* an observability backend like Jaeger, Prometheus, or other +commercial vendors. OpenTelemetry is focused on the generation, collection, +management, and export of telemetry. A major goal of OpenTelemetry is that you +can easily instrument your applications or systems, no matter their language, +infrastructure, or runtime environment. Crucially, the storage and visualization +of telemetry is intentionally left to other tools. + +*[Supported Rust Versions](#supported-rust-versions)* + +[Prometheus]: https://prometheus.io +[Jaeger]: https://www.jaegertracing.io + +### What does this crate contain? + +This crate provides an [OTLP +(OpenTelemetry +Protocol)](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/README.md) +exporter that sends telemetry to any OTLP-compatible backend such as the +[OpenTelemetry Collector](https://github.com/open-telemetry/opentelemetry-collector) +or vendor endpoints. Exporters are provided for all three signals - Logs, +Metrics and Traces - and support both `grpc` and `http` (binary protobuf or +JSON) transports. + +OTLP is the recommended way to export telemetry from OpenTelemetry, as it is +vendor-neutral and supported by most observability backends. + +### Related crates + +This crate exports telemetry produced via the OpenTelemetry SDK. It is +commonly used alongside: + +- **[opentelemetry](https://crates.io/crates/opentelemetry):** The core + OpenTelemetry API. +- **[opentelemetry-sdk](https://crates.io/crates/opentelemetry-sdk):** The + OpenTelemetry SDK implementation that this exporter plugs into. +- **[opentelemetry-http](https://crates.io/crates/opentelemetry-http):** HTTP + client abstractions used by the HTTP transport in this crate. +- **[opentelemetry-proto](https://crates.io/crates/opentelemetry-proto):** + Generated protobuf types used on the wire. + +## Getting started + +See [docs](https://docs.rs/opentelemetry-otlp). + +## Release Notes + +You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/CHANGELOG.md). + +## Supported Rust Versions + +OpenTelemetry is built against the latest stable release. The minimum supported +version is 1.75.0. The current OpenTelemetry version is NOT guaranteed to build +on Rust versions earlier than the minimum supported version. + +The current stable Rust compiler and the three most recent minor versions +before it will always be supported. For example, if the current stable compiler +version is 1.49, the minimum supported version will not be increased past 1.46, +three minor versions prior. Increasing the minimum supported compiler version +is not considered a semver breaking change as long as doing so complies with +this policy. diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/allowed-external-types.toml b/vendor/opentelemetry-rust/opentelemetry-otlp/allowed-external-types.toml new file mode 100644 index 00000000..e3eb8f1a --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/allowed-external-types.toml @@ -0,0 +1,22 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# This is used with cargo-check-external-types to reduce the surface area of downstream crates from +# the public API. Ideally this can have a few exceptions as possible. +allowed_external_types = [ + # opentelemetry-http + "opentelemetry_http::HttpClient", + + # opentelemetry-sdk + "opentelemetry_sdk::logs::export::LogExporter", + "opentelemetry_sdk::metrics::exporter::PushMetricExporter", + "opentelemetry_sdk::metrics::Temporality", + "opentelemetry_sdk::trace::export::SpanExporter", + + # tonic (pre-1.0 crate) + "tonic::metadata::map::MetadataMap", + "tonic::service::interceptor::Interceptor", + "tonic::transport::channel::Channel", + "tonic::transport::channel::tls::ClientTlsConfig", + "tonic::transport::tls::Certificate", + "tonic::transport::tls::Identity", +] diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/logs.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/logs.rs new file mode 100644 index 00000000..75a0484c --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/logs.rs @@ -0,0 +1,111 @@ +use super::OtlpHttpClient; +use crate::Protocol; +use opentelemetry::{otel_debug, otel_warn}; +use opentelemetry_sdk::error::{OTelSdkError, OTelSdkResult}; +use opentelemetry_sdk::logs::{LogBatch, LogExporter}; +#[cfg(feature = "http-proto")] +use prost::Message; +use std::time; + +impl LogExporter for OtlpHttpClient { + async fn export(&self, batch: LogBatch<'_>) -> OTelSdkResult { + let response_body = self + .export_http_with_retry( + batch, + OtlpHttpClient::build_logs_export_body, + "HttpLogsClient.Export", + ) + .await?; + + handle_partial_success(&response_body, self.protocol); + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: time::Duration) -> OTelSdkResult { + let mut client_guard = self.client.lock().map_err(|e| { + OTelSdkError::InternalFailure(format!("Failed to acquire client lock: {e}")) + })?; + + if client_guard.take().is_none() { + return Err(OTelSdkError::AlreadyShutdown); + } + + Ok(()) + } + + fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) { + self.resource = resource.into(); + } +} + +/// Handles partial success returned by OTLP endpoints. We log the rejected log records, +/// as well as the error message returned. +fn handle_partial_success(response_body: &[u8], protocol: Protocol) { + use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceResponse; + + let response: ExportLogsServiceResponse = match protocol { + #[cfg(feature = "http-json")] + Protocol::HttpJson => match serde_json::from_slice(response_body) { + Ok(r) => r, + Err(e) => { + otel_debug!(name: "HttpLogsClient.ResponseParseError", error = e.to_string()); + return; + } + }, + #[cfg(feature = "http-proto")] + Protocol::HttpBinary => match Message::decode(response_body) { + Ok(r) => r, + Err(e) => { + otel_debug!(name: "HttpLogsClient.ResponseParseError", error = e.to_string()); + return; + } + }, + #[cfg(feature = "grpc-tonic")] + Protocol::Grpc => { + unreachable!("HTTP client should not receive Grpc protocol") + } + }; + + if let Some(partial_success) = response.partial_success { + if partial_success.rejected_log_records > 0 || !partial_success.error_message.is_empty() { + otel_warn!( + name: "HttpLogsClient.PartialSuccess", + rejected_log_records = partial_success.rejected_log_records, + error_message = partial_success.error_message.as_str(), + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "http-proto")] + #[test] + fn test_handle_invalid_protobuf() { + // Corrupted/invalid protobuf data + let invalid = vec![0xFF, 0xFF, 0xFF, 0xFF]; + + // Should not panic - logs debug and returns early + handle_partial_success(&invalid, Protocol::HttpBinary); + } + + #[cfg(feature = "http-proto")] + #[test] + fn test_handle_empty_response() { + let empty = vec![]; + + // Should not panic + handle_partial_success(&empty, Protocol::HttpBinary); + } + + #[cfg(feature = "http-json")] + #[test] + fn test_handle_invalid_json() { + let invalid_json = b"{not valid json}"; + + // Should not panic - logs debug and returns + handle_partial_success(invalid_json, Protocol::HttpJson); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/metrics.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/metrics.rs new file mode 100644 index 00000000..467ec6e6 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/metrics.rs @@ -0,0 +1,105 @@ +use crate::metric::MetricsClient; +use crate::Protocol; +use opentelemetry::{otel_debug, otel_warn}; +use opentelemetry_sdk::error::{OTelSdkError, OTelSdkResult}; +use opentelemetry_sdk::metrics::data::ResourceMetrics; +#[cfg(feature = "http-proto")] +use prost::Message; + +use super::OtlpHttpClient; + +impl MetricsClient for OtlpHttpClient { + async fn export(&self, metrics: &ResourceMetrics) -> OTelSdkResult { + let response_body = self + .export_http_with_retry( + metrics, + OtlpHttpClient::build_metrics_export_body, + "HttpMetricsClient.Export", + ) + .await?; + + handle_partial_success(&response_body, self.protocol); + Ok(()) + } + + fn shutdown(&self) -> OTelSdkResult { + self.client + .lock() + .map_err(|e| OTelSdkError::InternalFailure(format!("Failed to acquire lock: {e}")))? + .take(); + + Ok(()) + } +} + +/// Handles partial success returned by OTLP endpoints. We log the rejected data points, +/// as well as the error message returned. +fn handle_partial_success(response_body: &[u8], protocol: Protocol) { + use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceResponse; + + let response: ExportMetricsServiceResponse = match protocol { + #[cfg(feature = "http-json")] + Protocol::HttpJson => match serde_json::from_slice(response_body) { + Ok(r) => r, + Err(e) => { + otel_debug!(name: "HttpMetricsClient.ResponseParseError", error = e.to_string()); + return; + } + }, + #[cfg(feature = "http-proto")] + Protocol::HttpBinary => match Message::decode(response_body) { + Ok(r) => r, + Err(e) => { + otel_debug!(name: "HttpMetricsClient.ResponseParseError", error = e.to_string()); + return; + } + }, + #[cfg(feature = "grpc-tonic")] + Protocol::Grpc => { + unreachable!("HTTP client should not receive Grpc protocol") + } + }; + + if let Some(partial_success) = response.partial_success { + if partial_success.rejected_data_points > 0 || !partial_success.error_message.is_empty() { + otel_warn!( + name: "HttpMetricsClient.PartialSuccess", + rejected_data_points = partial_success.rejected_data_points, + error_message = partial_success.error_message.as_str(), + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "http-proto")] + #[test] + fn test_handle_invalid_protobuf() { + // Corrupted/invalid protobuf data + let invalid = vec![0xFF, 0xFF, 0xFF, 0xFF]; + + // Should not panic - logs debug and returns early + handle_partial_success(&invalid, Protocol::HttpBinary); + } + + #[cfg(feature = "http-proto")] + #[test] + fn test_handle_empty_response() { + let empty = vec![]; + + // Should not panic + handle_partial_success(&empty, Protocol::HttpBinary); + } + + #[cfg(feature = "http-json")] + #[test] + fn test_handle_invalid_json() { + let invalid_json = b"{not valid json}"; + + // Should not panic - logs debug and returns + handle_partial_success(invalid_json, Protocol::HttpJson); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/mod.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/mod.rs new file mode 100644 index 00000000..014c55c6 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/mod.rs @@ -0,0 +1,2166 @@ +use super::{ + default_headers, parse_header_string, read_env_var, resolve_timeout, ExporterBuildError, + OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT, +}; +use crate::{ + exporter::ExportConfig, Protocol, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, +}; +use http::{HeaderName, HeaderValue, Uri}; +use opentelemetry::otel_debug; +use opentelemetry_http::{Bytes, HttpClient, ResponseBodyTooLarge}; +use opentelemetry_proto::transform::common::tonic::ResourceAttributesWithSchema; +#[cfg(feature = "logs")] +use opentelemetry_proto::transform::logs::tonic::group_logs_by_resource_and_scope; +#[cfg(feature = "trace")] +use opentelemetry_proto::transform::trace::tonic::group_spans_by_resource_and_scope; +#[cfg(feature = "logs")] +use opentelemetry_sdk::logs::LogBatch; +#[cfg(feature = "trace")] +use opentelemetry_sdk::trace::SpanData; +#[cfg(feature = "http-proto")] +use prost::Message; +use std::collections::HashMap; +use std::env; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use crate::retry::RetryErrorType; +use crate::retry_classification::http::classify_http_error; +use crate::RetryPolicy; + +// Recommended by the OTLP/HTTP specification: +// https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md#otlphttp-request +const DEFAULT_MAX_REQUEST_BODY_SIZE: usize = 64 * 1024 * 1024; + +// Shared HTTP retry functionality +/// HTTP-specific error wrapper for retry classification +#[derive(Debug)] +pub(crate) enum HttpExportError { + /// An error with an HTTP status code and optional Retry-After header + HttpStatus { + status_code: u16, + retry_after: Option, + message: String, + }, + /// An HTTP response body exceeded the client's configured limit + ResponseBodyTooLarge { message: String }, +} + +impl HttpExportError { + /// Create a new HttpExportError without retry-after header + pub(crate) fn new(status_code: u16, message: String) -> Self { + Self::HttpStatus { + status_code, + retry_after: None, + message, + } + } + + /// Create a new HttpExportError with retry-after header + pub(crate) fn with_retry_after(status_code: u16, retry_after: String, message: String) -> Self { + Self::HttpStatus { + status_code, + retry_after: Some(retry_after), + message, + } + } + + /// Create an error for an oversized HTTP response body + fn response_body_too_large(message: String) -> Self { + Self::ResponseBodyTooLarge { message } + } +} + +/// Classify HTTP export errors for retry decisions +pub(crate) fn classify_http_export_error(error: &HttpExportError) -> RetryErrorType { + match error { + HttpExportError::HttpStatus { + status_code, + retry_after, + .. + } => classify_http_error(*status_code, retry_after.as_deref()), + HttpExportError::ResponseBodyTooLarge { .. } => RetryErrorType::NonRetryable, + } +} + +/// Shared HTTP request data for retry attempts - optimizes Arc usage by bundling all data +/// we need to pass into the retry handler +#[derive(Debug)] +pub(crate) struct HttpRetryData { + pub body: Vec, + pub headers: Arc>, + pub endpoint: String, +} + +#[cfg(feature = "metrics")] +mod metrics; + +#[cfg(feature = "metrics")] +use opentelemetry_sdk::metrics::data::ResourceMetrics; + +#[cfg(feature = "logs")] +pub(crate) mod logs; + +#[cfg(feature = "trace")] +mod trace; + +#[cfg(all( + not(feature = "reqwest-client"), + not(feature = "reqwest-blocking-client"), + feature = "hyper-client" +))] +use opentelemetry_http::hyper::HyperClient; + +/// Configuration of the http transport +#[derive(Debug, Default)] +pub(crate) struct HttpConfig { + /// Select the HTTP client + client: Option>, + + /// Additional headers to send to the OTLP endpoint. + headers: Option>, + + /// The compression algorithm to use when communicating with the OTLP endpoint. + compression: Option, + + /// The retry policy to use for HTTP requests. + retry_policy: Option, + + /// Maximum HTTP request body size, before and after compression. + max_request_body_size: Option, +} + +/// Configuration for the OTLP HTTP exporter. +/// +/// ## Examples +/// +/// ```no_run +/// # #[cfg(feature="metrics")] +/// use opentelemetry_sdk::metrics::Temporality; +/// +/// # fn main() -> Result<(), Box> { +/// // Create a span exporter you can use when configuring tracer providers +/// # #[cfg(feature="trace")] +/// let span_exporter = opentelemetry_otlp::SpanExporter::builder().with_http().build()?; +/// +/// // Create a metrics exporter you can use when configuring meter providers +/// # #[cfg(feature="metrics")] +/// let metrics_exporter = opentelemetry_otlp::MetricExporter::builder() +/// .with_http() +/// .with_temporality(Temporality::default()) +/// .build()?; +/// +/// // Create a log exporter you can use when configuring logger providers +/// # #[cfg(feature="logs")] +/// let log_exporter = opentelemetry_otlp::LogExporter::builder().with_http().build()?; +/// # Ok(()) +/// # } +/// ``` +/// +#[derive(Debug)] +pub(crate) struct HttpExporterBuilder { + pub(crate) exporter_config: ExportConfig, + pub(crate) http_config: HttpConfig, +} + +impl Default for HttpExporterBuilder { + fn default() -> Self { + HttpExporterBuilder { + exporter_config: ExportConfig::default(), + http_config: HttpConfig { + headers: Some(default_headers()), + ..HttpConfig::default() + }, + } + } +} + +impl HttpExporterBuilder { + fn build_client( + &mut self, + signal_endpoint_var: &str, + signal_endpoint_path: &str, + signal_timeout_var: &str, + signal_http_headers_var: &str, + signal_compression_var: &str, + signal_protocol_var: &str, + ) -> Result { + let protocol = super::resolve_protocol(signal_protocol_var, self.exporter_config.protocol); + + // Validate protocol is compatible with HTTP transport + #[cfg(feature = "grpc-tonic")] + if matches!(protocol, Protocol::Grpc) { + return Err(ExporterBuildError::invalid_configuration( + "protocol", + "gRPC protocol is not compatible with HTTP transport; use `.with_tonic()` instead", + )); + } + + let endpoint = resolve_http_endpoint( + signal_endpoint_var, + signal_endpoint_path, + self.exporter_config.endpoint.as_deref(), + )?; + + let compression = self.resolve_compression(signal_compression_var)?; + + let timeout = resolve_timeout(signal_timeout_var, self.exporter_config.timeout.as_ref()); + + #[allow(unused_mut)] // TODO - clippy thinks mut is not needed, but it is + let mut http_client = self.http_config.client.take(); + + // When multiple HTTP client features are enabled, we use a priority order + // to select the client. This follows Rust's feature unification principle + // where features should be additive. Priority (highest to lowest): + // 1. reqwest-client (async) + // 2. hyper-client + // 3. reqwest-blocking-client (default) + if http_client.is_none() { + #[cfg(feature = "reqwest-client")] + { + let client = reqwest::Client::builder() + .timeout(timeout) + .build() + .map_err(|error| { + ExporterBuildError::internal_failure(format!( + "failed to build the reqwest HTTP client: {error}" + )) + })?; + http_client = Some(Arc::new(client) as Arc); + } + #[cfg(all(not(feature = "reqwest-client"), feature = "hyper-client"))] + { + // TODO - support configuring custom connector and executor + http_client = Some(Arc::new(HyperClient::with_default_connector(timeout, None)) + as Arc); + } + #[cfg(all( + not(feature = "reqwest-client"), + not(feature = "hyper-client"), + feature = "reqwest-blocking-client" + ))] + { + let timeout_clone = timeout; + let client = std::thread::Builder::new() + .spawn(move || { + reqwest::blocking::Client::builder() + .timeout(timeout_clone) + .build() + }) + .map_err(|error| { + ExporterBuildError::internal_failure(format!( + "failed to spawn thread for the blocking HTTP client: {error}" + )) + })? + .join() + .map_err(|_| { + ExporterBuildError::internal_failure( + "thread creating the blocking HTTP client panicked", + ) + })? + .map_err(|error| { + ExporterBuildError::internal_failure(format!( + "failed to build the blocking reqwest HTTP client: {error}" + )) + })?; + http_client = Some(Arc::new(client) as Arc); + } + } + + let http_client = http_client.ok_or_else(|| { + ExporterBuildError::invalid_configuration( + "http_client", + "no HTTP client is configured; enable an HTTP client feature or provide one with `.with_http_client()`", + ) + })?; + + #[allow(clippy::mutable_key_type)] // http headers are not mutated + let mut headers: HashMap = self + .http_config + .headers + .take() + .unwrap_or_default() + .into_iter() + .filter_map(|(k, v)| { + Some(( + HeaderName::from_str(&k).ok()?, + HeaderValue::from_str(&v).ok()?, + )) + }) + .collect(); + + // read headers from env var - signal specific env var is preferred over general + if let Ok(input) = + env::var(signal_http_headers_var).or_else(|_| env::var(OTEL_EXPORTER_OTLP_HEADERS)) + { + add_header_from_string(&input, &mut headers); + } + + let mut client = OtlpHttpClient::new( + http_client, + endpoint, + headers, + protocol, + timeout, + compression, + self.http_config.retry_policy.take(), + ); + if let Some(max_request_body_size) = self.http_config.max_request_body_size { + client.max_request_body_size = max_request_body_size; + } + Ok(client) + } + + fn resolve_compression( + &self, + env_override: &str, + ) -> Result, super::ExporterBuildError> { + super::resolve_compression_from_env( + self.http_config.compression, + env_override, + |compression| match compression { + crate::Compression::Gzip if !cfg!(feature = "gzip-http") => { + Err(ExporterBuildError::invalid_configuration( + "compression", + "gzip compression requested but gzip-http feature not enabled", + )) + } + crate::Compression::Zstd if !cfg!(feature = "zstd-http") => { + Err(ExporterBuildError::invalid_configuration( + "compression", + "zstd compression requested but zstd-http feature not enabled", + )) + } + _ => Ok(compression), + }, + ) + } + + /// Create a span exporter with the current configuration + #[cfg(feature = "trace")] + pub(crate) fn build_span_exporter(mut self) -> Result { + use crate::{ + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + }; + + let client = self.build_client( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + "/v1/traces", + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + )?; + + Ok(crate::SpanExporter::from_http(client)) + } + + /// Create a log exporter with the current configuration + #[cfg(feature = "logs")] + pub(crate) fn build_log_exporter(mut self) -> Result { + use crate::{ + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + }; + + let client = self.build_client( + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + "/v1/logs", + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, + )?; + + Ok(crate::LogExporter::from_http(client)) + } + + /// Create a metrics exporter with the current configuration + #[cfg(feature = "metrics")] + pub(crate) fn build_metrics_exporter( + mut self, + temporality: opentelemetry_sdk::metrics::Temporality, + ) -> Result { + use crate::{ + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + OTEL_EXPORTER_OTLP_METRICS_HEADERS, OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + }; + + let client = self.build_client( + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + "/v1/metrics", + OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + OTEL_EXPORTER_OTLP_METRICS_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, + )?; + + Ok(crate::MetricExporter::from_http(client, temporality)) + } +} + +#[derive(Debug)] +pub(crate) struct OtlpHttpClient { + client: Mutex>>, + collector_endpoint: Uri, + headers: Arc>, + protocol: Protocol, + timeout: Duration, + compression: Option, + retry_policy: RetryPolicy, + max_request_body_size: usize, + #[allow(dead_code)] + // would be removed once we support set_resource for metrics and traces. + resource: opentelemetry_proto::transform::common::tonic::ResourceAttributesWithSchema, +} + +impl OtlpHttpClient { + /// Shared HTTP export logic used by all exporters with retry support. + /// + /// Uses the configured retry policy with the exporter timeout as the deadline. + /// Delays between retries adapt to the calling context: cooperative + /// `tokio::time::sleep` inside a Tokio runtime, or `std::thread::sleep` + /// on bare OS threads (the SDK's default batch processors). + async fn export_http_with_retry( + &self, + data: T, + build_body_fn: F, + operation_name: &'static str, + ) -> Result + where + F: Fn(&Self, T) -> Result<(Vec, &'static str, Option<&'static str>), String>, + { + use crate::retry::retry_with_backoff; + + // Build request body once before retry loop + let (body, content_type, content_encoding) = build_body_fn(self, data) + .map_err(opentelemetry_sdk::error::OTelSdkError::InternalFailure)?; + + let retry_data = Arc::new(HttpRetryData { + body, + headers: self.headers.clone(), + endpoint: self.collector_endpoint.to_string(), + }); + + let response_body = retry_with_backoff( + &self.retry_policy, + self.timeout, + classify_http_export_error, + operation_name, + || async { + self.export_http_once(&retry_data, content_type, content_encoding, operation_name) + .await + }, + ) + .await + .map_err(|e| { + let message = match e { + HttpExportError::HttpStatus { message, .. } + | HttpExportError::ResponseBodyTooLarge { message } => message, + }; + opentelemetry_sdk::error::OTelSdkError::InternalFailure(message) + })?; + + Ok(response_body) + } + + /// Single HTTP export attempt - shared between retry and no-retry paths + async fn export_http_once( + &self, + retry_data: &HttpRetryData, + content_type: &'static str, + content_encoding: Option<&'static str>, + _operation_name: &'static str, + ) -> Result { + // Get client + let client = self + .client + .lock() + .map_err(|e| HttpExportError::new(500, format!("Mutex lock failed: {e}")))? + .as_ref() + .ok_or_else(|| HttpExportError::new(500, "Exporter already shutdown".to_string()))? + .clone(); + + // Build HTTP request + let mut request_builder = http::Request::builder() + .method(http::Method::POST) + .uri(&retry_data.endpoint) + .header(http::header::CONTENT_TYPE, content_type); + + if let Some(encoding) = content_encoding { + request_builder = request_builder.header("Content-Encoding", encoding); + } + + let mut request = request_builder + .body(retry_data.body.clone().into()) + .map_err(|e| HttpExportError::new(400, format!("Failed to build HTTP request: {e}")))?; + + for (k, v) in retry_data.headers.iter() { + request.headers_mut().insert(k.clone(), v.clone()); + } + + let request_uri = request.uri().to_string(); + otel_debug!(name: "HttpClient.ExportStarted"); + + // Send request + let response = client.send_bytes(request).await.map_err(|e| { + if e.downcast_ref::().is_some() { + let message = e.to_string(); + otel_debug!( + name: "HttpClient.ResponseBodyTooLarge", + url = request_uri.as_str(), + error = message.as_str() + ); + return HttpExportError::response_body_too_large(message); + } + + // Connection errors (e.g., "Connection refused", DNS failures) typically + // indicate user-side misconfigurations and don't contain sensitive data. + // We don't log at WARN here because SDK processors (BatchLogProcessor, + // BatchSpanProcessor, PeriodicReader) already log the returned error + // via otel_error!. + otel_debug!( + name: "HttpClient.NetworkError", + url = request_uri.as_str(), + error = format!("{e}") + ); + HttpExportError::new(0, "HTTP export failed: network error".to_string()) + })?; + + let status_code = response.status().as_u16(); + let retry_after = response + .headers() + .get("retry-after") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()); + + if !response.status().is_success() { + // We don't log at WARN here because SDK processors (BatchLogProcessor, + // BatchSpanProcessor, PeriodicReader) already log the returned error + // via otel_error!. Response body may contain sensitive information + // (e.g., auth tokens echoed back by the server), so log it at DEBUG + // level only. + otel_debug!( + name: "HttpClient.StatusError", + status_code = status_code, + url = request_uri.as_str(), + response_body = format!("{:?}", response.body()) + ); + let message = format!("HTTP export failed with status code: {status_code}"); + return Err(match retry_after { + Some(retry_after) => { + HttpExportError::with_retry_after(status_code, retry_after, message) + } + None => HttpExportError::new(status_code, message), + }); + } + + otel_debug!(name: "HttpClient.ExportSucceeded"); + + // Return the response, consuming the body to save a copy + Ok(response.into_body()) + } + + /// Compress data using gzip or zstd if the user has requested it and the relevant feature + /// has been enabled. If the user has requested it but the feature has not been enabled, + /// we should catch this at exporter build time and never get here. + fn process_body(&self, body: Vec) -> Result<(Vec, Option<&'static str>), String> { + self.validate_request_body_size(&body, "uncompressed")?; + + let (processed_body, content_encoding) = match self.compression { + #[cfg(feature = "gzip-http")] + Some(crate::Compression::Gzip) => { + use flate2::{write::GzEncoder, Compression}; + use std::io::Write; + + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&body).map_err(|e| e.to_string())?; + let compressed = encoder.finish().map_err(|e| e.to_string())?; + (compressed, Some("gzip")) + } + #[cfg(not(feature = "gzip-http"))] + Some(crate::Compression::Gzip) => { + return Err( + "gzip compression requested but gzip-http feature not enabled".to_string(), + ); + } + #[cfg(feature = "zstd-http")] + Some(crate::Compression::Zstd) => { + let compressed = zstd::bulk::compress(&body, 0).map_err(|e| e.to_string())?; + (compressed, Some("zstd")) + } + #[cfg(not(feature = "zstd-http"))] + Some(crate::Compression::Zstd) => { + return Err( + "zstd compression requested but zstd-http feature not enabled".to_string(), + ); + } + None => (body, None), + }; + + if content_encoding.is_some() { + self.validate_request_body_size(&processed_body, "compressed")?; + } + + Ok((processed_body, content_encoding)) + } + + fn validate_request_body_size( + &self, + body: &[u8], + representation: &'static str, + ) -> Result<(), String> { + if body.len() > self.max_request_body_size { + return Err(format!( + "OTLP HTTP {representation} request body is {} bytes, exceeding the configured limit of {} bytes; request will not be sent and telemetry data will be discarded; reduce the batch size, telemetry item size, or configure a larger limit", + body.len(), self.max_request_body_size + )); + } + Ok(()) + } + + #[allow(clippy::mutable_key_type)] // http headers are not mutated + fn new( + client: Arc, + collector_endpoint: Uri, + headers: HashMap, + protocol: Protocol, + timeout: Duration, + compression: Option, + retry_policy: Option, + ) -> Self { + OtlpHttpClient { + client: Mutex::new(Some(client)), + collector_endpoint, + headers: Arc::new(headers), + protocol, + timeout, + compression, + retry_policy: retry_policy.unwrap_or_default(), + max_request_body_size: DEFAULT_MAX_REQUEST_BODY_SIZE, + resource: ResourceAttributesWithSchema::default(), + } + } + + #[cfg(feature = "trace")] + fn build_trace_export_body( + &self, + spans: Vec, + ) -> Result<(Vec, &'static str, Option<&'static str>), String> { + use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest; + let resource_spans = group_spans_by_resource_and_scope(spans, &self.resource); + + let req = ExportTraceServiceRequest { resource_spans }; + let (body, content_type) = match self.protocol { + #[cfg(feature = "http-json")] + Protocol::HttpJson => match serde_json::to_string_pretty(&req) { + Ok(json) => (json.into_bytes(), "application/json"), + Err(e) => { + return Err(format!("failed to serialize traces to OTLP/HTTP JSON: {e}")); + } + }, + #[cfg(feature = "http-proto")] + Protocol::HttpBinary => (req.encode_to_vec(), "application/x-protobuf"), + #[cfg(feature = "grpc-tonic")] + Protocol::Grpc => { + unreachable!("HTTP client should not receive Grpc protocol") + } + }; + + let (processed_body, content_encoding) = self.process_body(body)?; + Ok((processed_body, content_type, content_encoding)) + } + + #[cfg(feature = "logs")] + fn build_logs_export_body( + &self, + logs: LogBatch<'_>, + ) -> Result<(Vec, &'static str, Option<&'static str>), String> { + use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest; + let resource_logs = group_logs_by_resource_and_scope(&logs, &self.resource); + let req = ExportLogsServiceRequest { resource_logs }; + + let (body, content_type) = match self.protocol { + #[cfg(feature = "http-json")] + Protocol::HttpJson => match serde_json::to_string_pretty(&req) { + Ok(json) => (json.into_bytes(), "application/json"), + Err(e) => { + return Err(format!("failed to serialize logs to OTLP/HTTP JSON: {e}")); + } + }, + #[cfg(feature = "http-proto")] + Protocol::HttpBinary => (req.encode_to_vec(), "application/x-protobuf"), + #[cfg(feature = "grpc-tonic")] + Protocol::Grpc => { + unreachable!("HTTP client should not receive Grpc protocol") + } + }; + + let (processed_body, content_encoding) = self.process_body(body)?; + Ok((processed_body, content_type, content_encoding)) + } + + #[cfg(feature = "metrics")] + fn build_metrics_export_body( + &self, + metrics: &ResourceMetrics, + ) -> Result<(Vec, &'static str, Option<&'static str>), String> { + use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceRequest; + + let req: ExportMetricsServiceRequest = metrics.into(); + + let (body, content_type) = match self.protocol { + #[cfg(feature = "http-json")] + Protocol::HttpJson => match serde_json::to_string_pretty(&req) { + Ok(json) => (json.into_bytes(), "application/json"), + Err(e) => { + return Err(format!( + "failed to serialize metrics to OTLP/HTTP JSON: {e}" + )); + } + }, + #[cfg(feature = "http-proto")] + Protocol::HttpBinary => (req.encode_to_vec(), "application/x-protobuf"), + #[cfg(feature = "grpc-tonic")] + Protocol::Grpc => { + unreachable!("HTTP client should not receive Grpc protocol") + } + }; + + let (processed_body, content_encoding) = self.process_body(body)?; + Ok((processed_body, content_type, content_encoding)) + } +} + +fn build_endpoint_uri(endpoint: &str, path: &str) -> Result { + let path = if endpoint.ends_with('/') && path.starts_with('/') { + path.strip_prefix('/').unwrap() + } else { + path + }; + let endpoint = format!("{endpoint}{path}"); + endpoint.parse() +} + +fn invalid_endpoint_env( + variable: &str, + value: &str, + error: http::uri::InvalidUri, +) -> ExporterBuildError { + ExporterBuildError::invalid_configuration( + variable, + format!("invalid endpoint '{value}': {error}"), + ) +} + +// see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#endpoint-urls-for-otlphttp +fn resolve_http_endpoint( + signal_endpoint_var: &str, + signal_endpoint_path: &str, + provided_endpoint: Option<&str>, +) -> Result { + // programmatic configuration overrides any value set via environment variables + if let Some(provider_endpoint) = provided_endpoint.filter(|s| !s.is_empty()) { + provider_endpoint.parse().map_err(|error| { + ExporterBuildError::invalid_configuration( + "endpoint", + format!("invalid endpoint '{provider_endpoint}': {error}"), + ) + }) + } else if let Some(endpoint) = read_env_var(signal_endpoint_var)? { + // per signal env var is not modified + endpoint + .parse() + .map_err(|error| invalid_endpoint_env(signal_endpoint_var, &endpoint, error)) + } else if let Some(endpoint) = read_env_var(OTEL_EXPORTER_OTLP_ENDPOINT)? { + // if signal env var is not set, then we check if the OTEL_EXPORTER_OTLP_ENDPOINT env var is set + build_endpoint_uri(&endpoint, signal_endpoint_path) + .map_err(|error| invalid_endpoint_env(OTEL_EXPORTER_OTLP_ENDPOINT, &endpoint, error)) + } else { + build_endpoint_uri( + OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT, + signal_endpoint_path, + ) + .map_err(|error| { + ExporterBuildError::internal_failure(format!( + "the default HTTP endpoint is invalid: {error}" + )) + }) + } +} + +#[allow(clippy::mutable_key_type)] // http headers are not mutated +fn add_header_from_string(input: &str, headers: &mut HashMap) { + headers.extend(parse_header_string(input).filter_map(|(key, value)| { + Some(( + HeaderName::from_str(key).ok()?, + HeaderValue::from_str(&value).ok()?, + )) + })); +} + +/// Expose interface for modifying builder config. +pub(crate) trait HasHttpConfig { + /// Return a mutable reference to the config within the exporter builders. + fn http_client_config(&mut self) -> &mut HttpConfig; +} + +/// Expose interface for modifying builder config. +impl HasHttpConfig for HttpExporterBuilder { + fn http_client_config(&mut self) -> &mut HttpConfig { + &mut self.http_config + } +} + +/// Expose methods to override HTTP-specific configuration. +/// +/// ## Examples +/// ``` +/// # #[cfg(all(feature = "trace", feature = "grpc-tonic"))] +/// # { +/// use crate::opentelemetry_otlp::WithHttpConfig; +/// let exporter_builder = opentelemetry_otlp::SpanExporter::builder() +/// .with_http() +/// .with_headers(std::collections::HashMap::new()); +/// # } +/// ``` +/// +/// This trait is sealed and cannot be implemented for types outside this crate. +pub trait WithHttpConfig: super::sealed::WithHttpConfig { + /// Assign client implementation + fn with_http_client(self, client: T) -> Self; + + /// Assign client implementation wrapped into shared pointer + /// + /// Prefer this method if you'd like to re-use http client across multiple exporters in your application + fn with_shared_http_client(self, client: std::sync::Arc) -> Self; + + /// Set additional headers to send to the collector. + fn with_headers(self, headers: HashMap) -> Self; + + /// Set the compression algorithm to use when communicating with the collector. + fn with_compression(self, compression: crate::Compression) -> Self; + + /// Set the retry policy for HTTP requests. + fn with_retry_policy(self, policy: RetryPolicy) -> Self; + + /// Set the maximum HTTP request body size in bytes. + /// + /// The limit is enforced both before and after compression. Requests that + /// exceed it are discarded without being sent or retried. Reduce the batch + /// size or telemetry item size if requests exceed the configured limit. + /// The default is 64 MiB. + fn with_max_request_body_size(self, max_size: usize) -> Self; +} + +impl super::sealed::WithHttpConfig for B {} + +impl WithHttpConfig for B { + fn with_http_client(self, client: T) -> Self { + self.with_shared_http_client(std::sync::Arc::new(client)) + } + + fn with_shared_http_client(mut self, client: std::sync::Arc) -> Self { + self.http_client_config().client = Some(client); + self + } + + fn with_headers(mut self, headers: HashMap) -> Self { + // headers will be wrapped, so we must do some logic to unwrap first. + let http_client_headers = self + .http_client_config() + .headers + .get_or_insert(HashMap::new()); + headers.into_iter().for_each(|(key, value)| { + http_client_headers.insert(key, super::url_decode(&value).unwrap_or(value)); + }); + self + } + + fn with_compression(mut self, compression: crate::Compression) -> Self { + self.http_client_config().compression = Some(compression); + self + } + + fn with_retry_policy(mut self, policy: RetryPolicy) -> Self { + self.http_client_config().retry_policy = Some(policy); + self + } + + fn with_max_request_body_size(mut self, max_size: usize) -> Self { + self.http_client_config().max_request_body_size = Some(max_size); + self + } +} + +#[cfg(test)] +mod tests { + use crate::exporter::http::HttpConfig; + use crate::exporter::tests::run_env_test; + use crate::{ + WithExportConfig, WithHttpConfig, OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + }; + + use super::{build_endpoint_uri, resolve_http_endpoint, HttpExporterBuilder}; + + #[test] + fn test_append_signal_path_to_generic_env() { + run_env_test( + vec![(OTEL_EXPORTER_OTLP_ENDPOINT, "http://example.com")], + || { + let endpoint = + resolve_http_endpoint(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "/v1/traces", None) + .unwrap(); + assert_eq!(endpoint, "http://example.com/v1/traces"); + }, + ) + } + + #[test] + fn test_not_append_signal_path_to_signal_env() { + run_env_test( + vec![(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "http://example.com")], + || { + let endpoint = + resolve_http_endpoint(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "/v1/traces", None) + .unwrap(); + assert_eq!(endpoint, "http://example.com"); + }, + ) + } + + #[test] + fn test_priority_of_signal_env_over_generic_env() { + run_env_test( + vec![ + (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "http://example.com"), + (OTEL_EXPORTER_OTLP_ENDPOINT, "http://wrong.com"), + ], + || { + let endpoint = super::resolve_http_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + "/v1/traces", + None, + ) + .unwrap(); + assert_eq!(endpoint, "http://example.com"); + }, + ); + } + + #[test] + fn test_priority_of_code_based_config_over_envs() { + run_env_test( + vec![ + (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "http://example.com"), + (OTEL_EXPORTER_OTLP_ENDPOINT, "http://wrong.com"), + ], + || { + let endpoint = super::resolve_http_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + "/v1/traces", + Some("http://localhost:4317"), + ) + .unwrap(); + assert_eq!(endpoint, "http://localhost:4317"); + }, + ); + } + + #[test] + fn test_use_default_when_empty_string_for_option() { + run_env_test(vec![], || { + let endpoint = + super::resolve_http_endpoint("non_existent_var", "/v1/traces", Some("")).unwrap(); + assert_eq!(endpoint, "http://localhost:4318/v1/traces"); + }); + } + + #[test] + fn test_use_default_when_others_missing() { + run_env_test(vec![], || { + let endpoint = + super::resolve_http_endpoint("NON_EXISTENT_VAR", "/v1/traces", None).unwrap(); + assert_eq!(endpoint, "http://localhost:4318/v1/traces"); + }); + } + + #[test] + fn test_build_endpoint_uri() { + let uri = build_endpoint_uri("https://example.com", "/v1/traces").unwrap(); + assert_eq!(uri, "https://example.com/v1/traces"); + + // Should be no duplicate slahes: + let uri = build_endpoint_uri("https://example.com/", "/v1/traces").unwrap(); + assert_eq!(uri, "https://example.com/v1/traces"); + + // Append paths properly: + let uri = build_endpoint_uri("https://example.com/additional/path/", "/v1/traces").unwrap(); + assert_eq!(uri, "https://example.com/additional/path/v1/traces"); + } + + #[test] + fn test_invalid_uri_in_signal_env_returns_error() { + run_env_test( + vec![ + ( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + "-*/*-/*-//-/-/invalid-uri", + ), + (OTEL_EXPORTER_OTLP_ENDPOINT, "http://example.com"), + ], + || { + let endpoint = super::resolve_http_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + "/v1/traces", + None, + ); + assert!(matches!( + endpoint, + Err(crate::exporter::ExporterBuildError::InvalidConfiguration(message)) + if message.contains(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) + && message.matches("-*/*-/*-//-/-/invalid-uri").count() == 1 + )); + }, + ); + } + + #[test] + fn test_invalid_uri_in_generic_env_returns_error() { + run_env_test( + vec![(OTEL_EXPORTER_OTLP_ENDPOINT, "-*/*-/*-//-/-/invalid-uri")], + || { + let endpoint = super::resolve_http_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + "/v1/traces", + None, + ); + assert!(matches!( + endpoint, + Err(crate::exporter::ExporterBuildError::InvalidConfiguration(message)) + if message.contains(OTEL_EXPORTER_OTLP_ENDPOINT) + && message.matches("-*/*-/*-//-/-/invalid-uri").count() == 1 + )); + }, + ); + } + + #[test] + fn test_empty_endpoint_envs_are_treated_as_unset() { + run_env_test( + vec![ + (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, ""), + (OTEL_EXPORTER_OTLP_ENDPOINT, ""), + ], + || { + let endpoint = super::resolve_http_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + "/v1/traces", + None, + ) + .unwrap(); + assert_eq!(endpoint, "http://localhost:4318/v1/traces"); + }, + ); + } + + #[cfg(unix)] + #[test] + fn test_non_unicode_endpoint_env_returns_error() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + temp_env::with_var( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + Some(OsStr::from_bytes(b"http://example.com/\x80")), + || { + let endpoint = super::resolve_http_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + "/v1/traces", + None, + ); + assert!(matches!( + endpoint, + Err(crate::exporter::ExporterBuildError::InvalidConfiguration(message)) + if message.contains(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) + && message.contains("not valid Unicode") + )); + }, + ); + } + + #[test] + fn test_invalid_programmatic_endpoint_returns_error() { + run_env_test(vec![], || { + let result = super::resolve_http_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + "/v1/traces", + Some("-*/*-/*-//-/-/yet-another-invalid-uri"), + ); + assert!(result.is_err()); + }); + } + + #[test] + fn test_add_header_from_string() { + use http::{HeaderName, HeaderValue}; + use std::collections::HashMap; + let test_cases = vec![ + // Format: (input_str, expected_headers) + ("k1=v1", vec![("k1", "v1")]), + ("k1=v1,k2=v2", vec![("k1", "v1"), ("k2", "v2")]), + ("k1=v1=10,k2,k3", vec![("k1", "v1=10")]), + ("k1=v1,,,k2,k3=10", vec![("k1", "v1"), ("k3", "10")]), + ]; + + for (input_str, expected_headers) in test_cases { + #[allow(clippy::mutable_key_type)] // http headers are not mutated + let mut headers: HashMap = HashMap::new(); + super::add_header_from_string(input_str, &mut headers); + + assert_eq!( + headers.len(), + expected_headers.len(), + "Failed on input: {input_str}" + ); + + for (expected_key, expected_value) in expected_headers { + assert_eq!( + headers.get(&HeaderName::from_static(expected_key)), + Some(&HeaderValue::from_static(expected_value)), + "Failed on key: {expected_key} with input: {input_str}" + ); + } + } + } + + #[test] + fn test_merge_header_from_string() { + use http::{HeaderName, HeaderValue}; + use std::collections::HashMap; + #[allow(clippy::mutable_key_type)] // http headers are not mutated + let mut headers: HashMap = std::collections::HashMap::new(); + headers.insert( + HeaderName::from_static("k1"), + HeaderValue::from_static("v1"), + ); + headers.insert( + HeaderName::from_static("k2"), + HeaderValue::from_static("v2"), + ); + let test_cases = vec![ + // Format: (input_str, expected_headers) + ("k1=v1_new", vec![("k1", "v1_new"), ("k2", "v2")]), + ( + "k3=val=10,22,34,k4=,k5=10", + vec![ + ("k1", "v1_new"), + ("k2", "v2"), + ("k3", "val=10"), + ("k5", "10"), + ], + ), + ]; + + for (input_str, expected_headers) in test_cases { + super::add_header_from_string(input_str, &mut headers); + + assert_eq!( + headers.len(), + expected_headers.len(), + "Failed on input: {input_str}" + ); + + for (expected_key, expected_value) in expected_headers { + assert_eq!( + headers.get(&HeaderName::from_static(expected_key)), + Some(&HeaderValue::from_static(expected_value)), + "Failed on key: {expected_key} with input: {input_str}" + ); + } + } + } + + #[test] + fn test_http_exporter_builder_with_headers() { + use std::collections::HashMap; + // Arrange + let initial_headers = HashMap::from([("k1".to_string(), "v1".to_string())]); + let extra_headers = HashMap::from([ + ("k2".to_string(), "v2".to_string()), + ("k3".to_string(), "v3".to_string()), + ]); + let expected_headers = initial_headers.iter().chain(extra_headers.iter()).fold( + HashMap::new(), + |mut acc, (k, v)| { + acc.insert(k.clone(), v.clone()); + acc + }, + ); + let builder = HttpExporterBuilder { + http_config: HttpConfig { + client: None, + headers: Some(initial_headers), + compression: None, + retry_policy: None, + max_request_body_size: None, + }, + exporter_config: crate::exporter::ExportConfig::default(), + }; + + // Act + let builder = builder.with_headers(extra_headers); + + // Assert + assert_eq!( + builder + .http_config + .headers + .clone() + .expect("headers should always be Some"), + expected_headers, + ); + } + + #[test] + fn test_http_exporter_endpoint() { + // default endpoint should add signal path + run_env_test(vec![], || { + let exporter = HttpExporterBuilder::default(); + + let url = resolve_http_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + "/v1/traces", + exporter.exporter_config.endpoint.as_deref(), + ) + .unwrap(); + + assert_eq!(url, "http://localhost:4318/v1/traces"); + }); + + // if builder endpoint is set, it should not add signal path + run_env_test(vec![], || { + let exporter = HttpExporterBuilder::default() + .with_endpoint("http://localhost:4318/v1/tracesbutnotreally"); + + let url = resolve_http_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + "/v1/traces", + exporter.exporter_config.endpoint.as_deref(), + ) + .unwrap(); + + assert_eq!(url, "http://localhost:4318/v1/tracesbutnotreally"); + }); + } + + #[test] + fn should_ensure_http_client_can_be_assigned_raw_or_arc() { + use super::{HttpExporterBuilder, WithHttpConfig}; + use crate::exporter::http::HttpConfig; + use crate::exporter::ExportConfig; + use export_body_tests::MockHttpClient; + + let _ = HttpExporterBuilder { + exporter_config: ExportConfig::default(), + http_config: HttpConfig::default(), + } + .with_http_client(MockHttpClient); + + let _ = HttpExporterBuilder { + exporter_config: ExportConfig::default(), + http_config: HttpConfig::default(), + } + .with_shared_http_client(std::sync::Arc::new(MockHttpClient)); + } + + #[cfg(feature = "gzip-http")] + mod compression_tests { + use super::super::OtlpHttpClient; + use flate2::read::GzDecoder; + use opentelemetry_http::{Bytes, HttpClient}; + use std::io::Read; + + #[cfg(feature = "http-proto")] + #[test] + fn test_gzip_compression_and_decompression() { + let client = OtlpHttpClient::new( + std::sync::Arc::new(MockHttpClient), + "http://localhost:4318".parse().unwrap(), + std::collections::HashMap::new(), + crate::Protocol::HttpBinary, + std::time::Duration::from_secs(10), + Some(crate::Compression::Gzip), + None, + ); + + // Test with some sample data + let test_data = b"Hello, world! This is test data for compression."; + let result = client.process_body(test_data.to_vec()).unwrap(); + let (compressed_body, content_encoding) = result; + + // Verify encoding header is set + assert_eq!(content_encoding, Some("gzip")); + + // Verify we can decompress the body + let mut decoder = GzDecoder::new(&compressed_body[..]); + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed).unwrap(); + + // Verify decompressed data matches original + assert_eq!(decompressed, test_data); + // Verify compression actually happened (compressed should be different) + assert_ne!(compressed_body, test_data.to_vec()); + } + + #[cfg(all(feature = "http-proto", feature = "gzip-http"))] + #[test] + fn request_body_limit_applies_before_and_after_compression() { + let mut client = OtlpHttpClient::new( + std::sync::Arc::new(MockHttpClient), + "http://localhost:4318".parse().unwrap(), + std::collections::HashMap::new(), + crate::Protocol::HttpBinary, + std::time::Duration::from_secs(10), + Some(crate::Compression::Gzip), + None, + ); + client.max_request_body_size = 1; + + let uncompressed_error = client.process_body(vec![0; 2]).unwrap_err(); + assert!(uncompressed_error.contains("uncompressed request body is 2 bytes")); + + let compressed_error = client.process_body(vec![0]).unwrap_err(); + assert!(compressed_error.contains("compressed request body is")); + } + + #[cfg(all(feature = "http-proto", feature = "zstd-http"))] + #[test] + fn test_zstd_compression_and_decompression() { + let client = OtlpHttpClient::new( + std::sync::Arc::new(MockHttpClient), + "http://localhost:4318".parse().unwrap(), + std::collections::HashMap::new(), + crate::Protocol::HttpBinary, + std::time::Duration::from_secs(10), + Some(crate::Compression::Zstd), + None, + ); + + // Test with some sample data + let test_data = b"Hello, world! This is test data for zstd compression."; + let result = client.process_body(test_data.to_vec()).unwrap(); + let (compressed_body, content_encoding) = result; + + // Verify encoding header is set + assert_eq!(content_encoding, Some("zstd")); + + // Verify we can decompress the body + let decompressed = zstd::bulk::decompress(&compressed_body, test_data.len()).unwrap(); + + // Verify decompressed data matches original + assert_eq!(decompressed, test_data); + // Verify compression actually happened (compressed should be different) + assert_ne!(compressed_body, test_data.to_vec()); + } + + #[cfg(feature = "http-proto")] + #[test] + fn test_no_compression_when_disabled() { + let client = OtlpHttpClient::new( + std::sync::Arc::new(MockHttpClient), + "http://localhost:4318".parse().unwrap(), + std::collections::HashMap::new(), + crate::Protocol::HttpBinary, + std::time::Duration::from_secs(10), + None, // No compression + None, + ); + + let body = vec![1, 2, 3, 4]; + let result = client.process_body(body.clone()).unwrap(); + let (result_body, content_encoding) = result; + + // Body should be unchanged and no encoding header + assert_eq!(result_body, body); + assert_eq!(content_encoding, None); + } + + #[cfg(all(feature = "http-proto", not(feature = "gzip-http")))] + #[test] + fn test_gzip_error_when_feature_disabled() { + let client = OtlpHttpClient::new( + std::sync::Arc::new(MockHttpClient), + "http://localhost:4318".parse().unwrap(), + std::collections::HashMap::new(), + crate::Protocol::HttpBinary, + std::time::Duration::from_secs(10), + Some(crate::Compression::Gzip), + None, + ); + + let body = vec![1, 2, 3, 4]; + let result = client.process_body(body); + + // Should return error when gzip requested but feature not enabled + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("gzip-http feature not enabled")); + } + + #[cfg(all(feature = "http-proto", not(feature = "zstd-http")))] + #[test] + fn test_zstd_error_when_feature_disabled() { + let client = OtlpHttpClient::new( + std::sync::Arc::new(MockHttpClient), + "http://localhost:4318".parse().unwrap(), + std::collections::HashMap::new(), + crate::Protocol::HttpBinary, + std::time::Duration::from_secs(10), + Some(crate::Compression::Zstd), + None, + ); + + let body = vec![1, 2, 3, 4]; + let result = client.process_body(body); + + // Should return error when zstd requested but feature not enabled + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("zstd-http feature not enabled")); + } + + // Mock HTTP client for testing + #[derive(Debug)] + struct MockHttpClient; + + #[async_trait::async_trait] + impl HttpClient for MockHttpClient { + async fn send_bytes( + &self, + _request: http::Request, + ) -> Result, opentelemetry_http::HttpError> { + Ok(http::Response::builder() + .status(200) + .body(Bytes::new()) + .unwrap()) + } + } + } + + mod export_body_tests { + use super::super::OtlpHttpClient; + use opentelemetry_http::{Bytes, HttpClient}; + use std::collections::HashMap; + use std::time::Duration; + + #[derive(Debug)] + pub(crate) struct MockHttpClient; + + #[async_trait::async_trait] + impl HttpClient for MockHttpClient { + async fn send_bytes( + &self, + _request: http::Request, + ) -> Result, opentelemetry_http::HttpError> { + Ok(http::Response::builder() + .status(200) + .body(Bytes::new()) + .unwrap()) + } + } + + fn create_test_client( + protocol: crate::Protocol, + compression: Option, + ) -> OtlpHttpClient { + OtlpHttpClient::new( + std::sync::Arc::new(MockHttpClient), + "http://localhost:4318".parse().unwrap(), + HashMap::new(), + protocol, + std::time::Duration::from_secs(10), + compression, + None, + ) + } + + #[cfg(feature = "http-proto")] + #[test] + fn request_body_at_configured_limit_is_accepted() { + let mut client = create_test_client(crate::Protocol::HttpBinary, None); + client.max_request_body_size = 4; + + let (body, content_encoding) = client.process_body(vec![0; 4]).unwrap(); + + assert_eq!(body.len(), 4); + assert_eq!(content_encoding, None); + } + + #[cfg(feature = "http-proto")] + #[test] + fn request_body_over_configured_limit_is_rejected() { + let mut client = create_test_client(crate::Protocol::HttpBinary, None); + client.max_request_body_size = 4; + + let error = client.process_body(vec![0; 5]).unwrap_err(); + + assert!(error.contains("uncompressed request body is 5 bytes")); + assert!(error.contains("configured limit of 4 bytes")); + assert!(error.contains("request will not be sent")); + } + + #[cfg(feature = "http-proto")] + #[test] + fn default_request_body_limit_is_64_mib() { + let client = create_test_client(crate::Protocol::HttpBinary, None); + + assert_eq!( + client.max_request_body_size, + super::super::DEFAULT_MAX_REQUEST_BODY_SIZE + ); + } + + fn create_test_span_data() -> opentelemetry_sdk::trace::SpanData { + use opentelemetry::trace::Status; + use opentelemetry::trace::{ + SpanContext, SpanId, SpanKind, TraceFlags, TraceId, TraceState, + }; + use opentelemetry_sdk::trace::{SpanData, SpanEvents, SpanLinks}; + use std::borrow::Cow; + use std::time::{Duration, SystemTime}; + + let span_context = SpanContext::new( + TraceId::from(123), + SpanId::from(456), + TraceFlags::default(), + false, + TraceState::default(), + ); + SpanData { + span_context, + parent_span_id: SpanId::from(0), + parent_span_is_remote: false, + span_kind: SpanKind::Internal, + name: Cow::Borrowed("test_span"), + start_time: SystemTime::UNIX_EPOCH, + end_time: SystemTime::UNIX_EPOCH + Duration::from_secs(1), + attributes: vec![], + dropped_attributes_count: 0, + events: SpanEvents::default(), + links: SpanLinks::default(), + status: Status::Unset, + instrumentation_scope: opentelemetry::InstrumentationScope::default(), + } + } + + #[cfg(all(feature = "trace", feature = "http-proto"))] + #[test] + fn test_build_trace_export_body_binary_protocol() { + let client = create_test_client(crate::Protocol::HttpBinary, None); + let span_data = create_test_span_data(); + + let result = client.build_trace_export_body(vec![span_data]).unwrap(); + let (_body, content_type, content_encoding) = result; + + assert_eq!(content_type, "application/x-protobuf"); + assert_eq!(content_encoding, None); + } + + #[cfg(all(feature = "trace", feature = "http-json"))] + #[test] + fn test_build_trace_export_body_json_protocol() { + let client = create_test_client(crate::Protocol::HttpJson, None); + let span_data = create_test_span_data(); + + let result = client.build_trace_export_body(vec![span_data]).unwrap(); + let (_body, content_type, content_encoding) = result; + + assert_eq!(content_type, "application/json"); + assert_eq!(content_encoding, None); + } + + #[cfg(all(feature = "http-proto", feature = "trace", feature = "gzip-http"))] + #[test] + fn test_build_trace_export_body_with_compression() { + let client = + create_test_client(crate::Protocol::HttpBinary, Some(crate::Compression::Gzip)); + let span_data = create_test_span_data(); + + let result = client.build_trace_export_body(vec![span_data]).unwrap(); + let (_body, content_type, content_encoding) = result; + + assert_eq!(content_type, "application/x-protobuf"); + assert_eq!(content_encoding, Some("gzip")); + } + + #[cfg(feature = "logs")] + fn create_test_log_batch() -> opentelemetry_sdk::logs::LogBatch<'static> { + use opentelemetry_sdk::logs::LogBatch; + + // Use empty batch for simplicity - the method should still handle protocol/compression correctly + LogBatch::new(&[]) + } + + #[cfg(all(feature = "http-proto", feature = "logs"))] + #[test] + fn test_build_logs_export_body_binary_protocol() { + let client = create_test_client(crate::Protocol::HttpBinary, None); + let batch = create_test_log_batch(); + + let result = client.build_logs_export_body(batch).unwrap(); + let (_body, content_type, content_encoding) = result; + + assert_eq!(content_type, "application/x-protobuf"); + assert_eq!(content_encoding, None); + } + + #[cfg(all(feature = "logs", feature = "http-json"))] + #[test] + fn test_build_logs_export_body_json_protocol() { + let client = create_test_client(crate::Protocol::HttpJson, None); + let batch = create_test_log_batch(); + + let result = client.build_logs_export_body(batch).unwrap(); + let (_body, content_type, content_encoding) = result; + + assert_eq!(content_type, "application/json"); + assert_eq!(content_encoding, None); + } + + #[cfg(all(feature = "http-proto", feature = "logs", feature = "gzip-http"))] + #[test] + fn test_build_logs_export_body_with_compression() { + let client = + create_test_client(crate::Protocol::HttpBinary, Some(crate::Compression::Gzip)); + let batch = create_test_log_batch(); + + let result = client.build_logs_export_body(batch).unwrap(); + let (_body, content_type, content_encoding) = result; + + assert_eq!(content_type, "application/x-protobuf"); + assert_eq!(content_encoding, Some("gzip")); + } + + #[cfg(all(feature = "http-proto", feature = "metrics"))] + #[test] + fn test_build_metrics_export_body_binary_protocol() { + use opentelemetry_sdk::metrics::data::ResourceMetrics; + + let client = create_test_client(crate::Protocol::HttpBinary, None); + let metrics = ResourceMetrics::default(); + + let result = client.build_metrics_export_body(&metrics).unwrap(); + let (_body, content_type, content_encoding) = result; + + assert_eq!(content_type, "application/x-protobuf"); + assert_eq!(content_encoding, None); + } + + #[cfg(all(feature = "metrics", feature = "http-json"))] + #[test] + fn test_build_metrics_export_body_json_protocol() { + use opentelemetry_sdk::metrics::data::ResourceMetrics; + + let client = create_test_client(crate::Protocol::HttpJson, None); + let metrics = ResourceMetrics::default(); + + let result = client.build_metrics_export_body(&metrics).unwrap(); + let (_body, content_type, content_encoding) = result; + + assert_eq!(content_type, "application/json"); + assert_eq!(content_encoding, None); + } + + #[cfg(all(feature = "http-proto", feature = "metrics", feature = "gzip-http"))] + #[test] + fn test_build_metrics_export_body_with_compression() { + use opentelemetry_sdk::metrics::data::ResourceMetrics; + + let client = + create_test_client(crate::Protocol::HttpBinary, Some(crate::Compression::Gzip)); + let metrics = ResourceMetrics::default(); + + let result = client.build_metrics_export_body(&metrics).unwrap(); + let (_body, content_type, content_encoding) = result; + + assert_eq!(content_type, "application/x-protobuf"); + assert_eq!(content_encoding, Some("gzip")); + } + + #[cfg(all( + feature = "http-proto", + feature = "metrics", + not(feature = "gzip-http") + ))] + #[test] + fn test_build_metrics_export_body_returns_compression_error() { + use opentelemetry_sdk::metrics::data::ResourceMetrics; + + let client = + create_test_client(crate::Protocol::HttpBinary, Some(crate::Compression::Gzip)); + let metrics = ResourceMetrics::default(); + + let error = client.build_metrics_export_body(&metrics).unwrap_err(); + assert!(error.contains("gzip-http feature not enabled")); + } + + #[test] + fn test_resolve_compression_uses_generic_env_fallback() { + use super::super::HttpExporterBuilder; + use crate::exporter::tests::run_env_test; + + // Test that generic OTEL_EXPORTER_OTLP_COMPRESSION is used when signal-specific env var is not set + run_env_test( + vec![(crate::OTEL_EXPORTER_OTLP_COMPRESSION, "gzip")], + || { + let builder = HttpExporterBuilder::default(); + let result = builder + .resolve_compression("NONEXISTENT_SIGNAL_COMPRESSION") + .unwrap(); + #[cfg(feature = "gzip-http")] + assert_eq!(result, Some(crate::Compression::Gzip)); + #[cfg(not(feature = "gzip-http"))] + assert_eq!(result, None); + }, + ); + } + + #[cfg(all(feature = "trace", not(feature = "gzip-http")))] + #[test] + fn test_build_span_exporter_with_gzip_without_feature() { + use super::super::HttpExporterBuilder; + use crate::{ExporterBuildError, WithHttpConfig}; + + let builder = HttpExporterBuilder::default().with_compression(crate::Compression::Gzip); + + let result = builder.build_span_exporter(); + assert!(matches!( + result, + Err(ExporterBuildError::InvalidConfiguration(message)) + if message.contains("gzip-http") + )); + } + + #[cfg(all(feature = "trace", not(feature = "zstd-http")))] + #[test] + fn test_build_span_exporter_with_zstd_without_feature() { + use super::super::HttpExporterBuilder; + use crate::{ExporterBuildError, WithHttpConfig}; + + let builder = HttpExporterBuilder::default().with_compression(crate::Compression::Zstd); + + let result = builder.build_span_exporter(); + assert!(matches!( + result, + Err(ExporterBuildError::InvalidConfiguration(message)) + if message.contains("zstd-http") + )); + } + + #[test] + fn test_with_max_request_body_size() { + use super::super::HttpExporterBuilder; + use crate::WithHttpConfig; + + let builder = HttpExporterBuilder::default().with_max_request_body_size(1024); + + assert_eq!(builder.http_config.max_request_body_size, Some(1024)); + } + + #[test] + fn test_with_retry_policy() { + use super::super::HttpExporterBuilder; + use crate::RetryPolicy; + use crate::WithHttpConfig; + + let custom_policy = RetryPolicy::default() + .with_max_retries(5) + .with_initial_delay(Duration::from_millis(200)) + .with_max_delay(Duration::from_millis(3200)) + .with_max_jitter(Duration::from_millis(50)); + + let builder = HttpExporterBuilder::default().with_retry_policy(custom_policy); + + // Verify the retry policy was set + let retry_policy = builder.http_config.retry_policy.as_ref().unwrap(); + assert_eq!(retry_policy.max_retries, 5); + assert_eq!(retry_policy.initial_delay, Duration::from_millis(200)); + assert_eq!(retry_policy.max_delay, Duration::from_millis(3200)); + assert_eq!(retry_policy.max_jitter, Duration::from_millis(50)); + } + + #[cfg(feature = "http-proto")] + #[test] + fn test_default_retry_policy_when_none_configured() { + let client = create_test_client(crate::Protocol::HttpBinary, None); + + // Verify the recommended default values are used. + assert_eq!(client.retry_policy.max_retries, 3); + assert_eq!( + client.retry_policy.initial_delay, + Duration::from_millis(100) + ); + assert_eq!(client.retry_policy.max_delay, Duration::from_millis(1600)); + assert_eq!(client.retry_policy.max_jitter, Duration::from_millis(100)); + } + + #[cfg(feature = "http-proto")] + #[test] + fn test_custom_retry_policy_used() { + use crate::RetryPolicy; + + let custom_policy = RetryPolicy::default() + .with_max_retries(7) + .with_initial_delay(Duration::from_millis(500)) + .with_max_delay(Duration::from_millis(5000)) + .with_max_jitter(Duration::from_millis(200)); + + let client = OtlpHttpClient::new( + std::sync::Arc::new(MockHttpClient), + "http://localhost:4318".parse().unwrap(), + HashMap::new(), + crate::Protocol::HttpBinary, + std::time::Duration::from_secs(10), + None, + Some(custom_policy), + ); + + // Verify custom values are used + assert_eq!(client.retry_policy.max_retries, 7); + assert_eq!( + client.retry_policy.initial_delay, + Duration::from_millis(500) + ); + assert_eq!(client.retry_policy.max_delay, Duration::from_millis(5000)); + assert_eq!(client.retry_policy.max_jitter, Duration::from_millis(200)); + } + } + + /// Integration tests verifying retry behavior end-to-end through the HTTP exporter. + /// These test the full path: HttpClient response -> HttpExportError -> classification -> retry loop. + #[cfg(feature = "http-proto")] + mod retry_integration_tests { + use super::super::OtlpHttpClient; + use crate::RetryPolicy; + use opentelemetry_http::{Bytes, HttpClient}; + use std::collections::HashMap; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::Arc; + use std::time::Duration; + + /// Mock HTTP client that returns a sequence of responses controlled by an attempt counter. + #[derive(Debug)] + struct SequencedMockClient { + attempts: AtomicUsize, + responses: Vec>, + } + + impl SequencedMockClient { + fn new(responses: Vec>) -> Self { + assert!( + !responses.is_empty(), + "SequencedMockClient requires at least one response", + ); + Self { + attempts: AtomicUsize::new(0), + responses, + } + } + + fn attempt_count(&self) -> usize { + self.attempts.load(Ordering::SeqCst) + } + } + + #[async_trait::async_trait] + impl HttpClient for SequencedMockClient { + async fn send_bytes( + &self, + _request: http::Request, + ) -> Result, opentelemetry_http::HttpError> { + let attempt = self.attempts.fetch_add(1, Ordering::SeqCst); + let idx = attempt.min(self.responses.len() - 1); + let resp = &self.responses[idx]; + // Clone the response + let mut builder = http::Response::builder().status(resp.status()); + for (k, v) in resp.headers() { + builder = builder.header(k, v); + } + Ok(builder.body(resp.body().clone()).unwrap()) + } + } + + /// Mock that returns Err (simulating network failure) for the first N attempts, + /// then Ok(200). + #[derive(Debug)] + struct NetworkFailureMockClient { + attempts: AtomicUsize, + fail_count: usize, + } + + impl NetworkFailureMockClient { + fn new(fail_count: usize) -> Self { + Self { + attempts: AtomicUsize::new(0), + fail_count, + } + } + + fn attempt_count(&self) -> usize { + self.attempts.load(Ordering::SeqCst) + } + } + + #[async_trait::async_trait] + impl HttpClient for NetworkFailureMockClient { + async fn send_bytes( + &self, + _request: http::Request, + ) -> Result, opentelemetry_http::HttpError> { + let attempt = self.attempts.fetch_add(1, Ordering::SeqCst); + if attempt < self.fail_count { + Err("connection refused".into()) + } else { + Ok(http::Response::builder() + .status(200) + .body(Bytes::new()) + .unwrap()) + } + } + } + + #[derive(Debug, Default)] + struct OversizedResponseMockClient { + attempts: AtomicUsize, + } + + impl OversizedResponseMockClient { + fn attempt_count(&self) -> usize { + self.attempts.load(Ordering::SeqCst) + } + } + + #[async_trait::async_trait] + impl HttpClient for OversizedResponseMockClient { + async fn send_bytes( + &self, + _request: http::Request, + ) -> Result, opentelemetry_http::HttpError> { + self.attempts.fetch_add(1, Ordering::SeqCst); + Err(Box::new(opentelemetry_http::ResponseBodyTooLarge::new())) + } + } + + fn build_test_body( + _client: &OtlpHttpClient, + _data: (), + ) -> Result<(Vec, &'static str, Option<&'static str>), String> { + Ok((vec![1, 2, 3], "application/x-protobuf", None)) + } + + fn build_processed_test_body( + client: &OtlpHttpClient, + _data: (), + ) -> Result<(Vec, &'static str, Option<&'static str>), String> { + let (body, content_encoding) = client.process_body(vec![1, 2, 3])?; + Ok((body, "application/x-protobuf", content_encoding)) + } + + fn make_client(mock: Arc, retry_policy: RetryPolicy) -> OtlpHttpClient { + OtlpHttpClient::new( + mock, + "http://localhost:4318/v1/traces".parse().unwrap(), + HashMap::new(), + crate::Protocol::HttpBinary, + std::time::Duration::from_secs(5), + None, + Some(retry_policy), + ) + } + + fn retry_policy() -> RetryPolicy { + RetryPolicy::default() + .with_max_retries(3) + .with_initial_delay(Duration::from_millis(1)) + .with_max_delay(Duration::from_millis(10)) + .with_max_jitter(Duration::ZERO) + } + + #[test] + fn oversized_request_is_not_sent_or_retried() { + let mock = Arc::new(SequencedMockClient::new(vec![http::Response::builder() + .status(200) + .body(Bytes::new()) + .unwrap()])); + let mut client = make_client(mock.clone(), retry_policy()); + client.max_request_body_size = 2; + + let result = futures_executor::block_on(client.export_http_with_retry( + (), + build_processed_test_body, + "test", + )); + + let error = result.unwrap_err().to_string(); + assert!(error.contains("request will not be sent")); + assert_eq!(mock.attempt_count(), 0); + } + + #[test] + fn retries_on_503_then_succeeds() { + let mock = Arc::new(SequencedMockClient::new(vec![ + http::Response::builder() + .status(503) + .body(Bytes::new()) + .unwrap(), + http::Response::builder() + .status(200) + .body(Bytes::new()) + .unwrap(), + ])); + let client = make_client(mock.clone(), retry_policy()); + + let result = futures_executor::block_on(client.export_http_with_retry( + (), + build_test_body, + "test", + )); + + assert!(result.is_ok()); + assert_eq!(mock.attempt_count(), 2); + } + + #[test] + fn does_not_retry_on_400() { + let mock = Arc::new(SequencedMockClient::new(vec![http::Response::builder() + .status(400) + .body(Bytes::new()) + .unwrap()])); + let client = make_client(mock.clone(), retry_policy()); + + let result = futures_executor::block_on(client.export_http_with_retry( + (), + build_test_body, + "test", + )); + + assert!(result.is_err()); + assert_eq!(mock.attempt_count(), 1); + } + + #[test] + fn does_not_retry_when_response_body_is_too_large() { + let mock = Arc::new(OversizedResponseMockClient::default()); + let client = make_client(mock.clone(), retry_policy()); + + let error = futures_executor::block_on(client.export_http_with_retry( + (), + build_test_body, + "test", + )) + .unwrap_err(); + + assert_eq!(mock.attempt_count(), 1); + assert!(error + .to_string() + .contains("response body exceeded maximum allowed 4 MiB limit")); + } + + #[test] + fn does_not_retry_on_unlisted_server_error() { + let mock = Arc::new(SequencedMockClient::new(vec![http::Response::builder() + .status(500) + .body(Bytes::new()) + .unwrap()])); + let client = make_client(mock.clone(), retry_policy()); + + let result = futures_executor::block_on(client.export_http_with_retry( + (), + build_test_body, + "test", + )); + + assert!(result.is_err()); + assert_eq!(mock.attempt_count(), 1); + } + + #[test] + fn retries_on_429_with_retry_after() { + let mock = Arc::new(SequencedMockClient::new(vec![ + http::Response::builder() + .status(429) + .header("retry-after", "1") + .body(Bytes::new()) + .unwrap(), + http::Response::builder() + .status(200) + .body(Bytes::new()) + .unwrap(), + ])); + let client = make_client(mock.clone(), retry_policy()); + + let start = std::time::Instant::now(); + let result = futures_executor::block_on(client.export_http_with_retry( + (), + build_test_body, + "test", + )); + + assert!(result.is_ok()); + assert_eq!(mock.attempt_count(), 2); + // Should have honored the 1s Retry-After + assert!(start.elapsed() >= std::time::Duration::from_millis(900)); + } + + #[test] + fn retries_on_503_with_retry_after() { + let mock = Arc::new(SequencedMockClient::new(vec![ + http::Response::builder() + .status(503) + .header("retry-after", "1") + .body(Bytes::new()) + .unwrap(), + http::Response::builder() + .status(200) + .body(Bytes::new()) + .unwrap(), + ])); + let client = make_client(mock.clone(), retry_policy()); + + let start = std::time::Instant::now(); + let result = futures_executor::block_on(client.export_http_with_retry( + (), + build_test_body, + "test", + )); + + assert!(result.is_ok()); + assert_eq!(mock.attempt_count(), 2); + assert!(start.elapsed() >= std::time::Duration::from_millis(900)); + } + + #[test] + fn retries_on_network_error() { + let mock = Arc::new(NetworkFailureMockClient::new(2)); + let client = make_client(mock.clone(), retry_policy()); + + let result = futures_executor::block_on(client.export_http_with_retry( + (), + build_test_body, + "test", + )); + + assert!(result.is_ok()); + assert_eq!(mock.attempt_count(), 3); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/trace.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/trace.rs new file mode 100644 index 00000000..78690bd4 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/http/trace.rs @@ -0,0 +1,112 @@ +use super::OtlpHttpClient; +use crate::Protocol; +use opentelemetry::{otel_debug, otel_warn}; +use opentelemetry_sdk::{ + error::{OTelSdkError, OTelSdkResult}, + trace::{SpanData, SpanExporter}, +}; +#[cfg(feature = "http-proto")] +use prost::Message; + +impl SpanExporter for OtlpHttpClient { + async fn export(&self, batch: Vec) -> OTelSdkResult { + let response_body = self + .export_http_with_retry( + batch, + OtlpHttpClient::build_trace_export_body, + "HttpTracesClient.Export", + ) + .await?; + + handle_partial_success(&response_body, self.protocol); + Ok(()) + } + + fn shutdown(&self) -> OTelSdkResult { + let mut client_guard = self.client.lock().map_err(|e| { + OTelSdkError::InternalFailure(format!("Failed to acquire client lock: {e}")) + })?; + + if client_guard.take().is_none() { + return Err(OTelSdkError::AlreadyShutdown); + } + + Ok(()) + } + + fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) { + self.resource = resource.into(); + } +} + +/// Handles partial success returned by OTLP endpoints. We log the rejected spans, +/// as well as the error message returned. +fn handle_partial_success(response_body: &[u8], protocol: Protocol) { + use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceResponse; + + let response: ExportTraceServiceResponse = match protocol { + #[cfg(feature = "http-json")] + Protocol::HttpJson => match serde_json::from_slice(response_body) { + Ok(r) => r, + Err(e) => { + otel_debug!(name: "HttpTraceClient.ResponseParseError", error = e.to_string()); + return; + } + }, + #[cfg(feature = "http-proto")] + Protocol::HttpBinary => match Message::decode(response_body) { + Ok(r) => r, + Err(e) => { + otel_debug!(name: "HttpTraceClient.ResponseParseError", error = e.to_string()); + return; + } + }, + #[cfg(feature = "grpc-tonic")] + Protocol::Grpc => { + unreachable!("HTTP client should not receive Grpc protocol") + } + }; + + if let Some(partial_success) = response.partial_success { + if partial_success.rejected_spans > 0 || !partial_success.error_message.is_empty() { + otel_warn!( + name: "HttpTraceClient.PartialSuccess", + rejected_spans = partial_success.rejected_spans, + error_message = partial_success.error_message.as_str(), + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "http-proto")] + #[test] + fn test_handle_invalid_protobuf() { + // Corrupted/invalid protobuf data + let invalid = vec![0xFF, 0xFF, 0xFF, 0xFF]; + + // Should not panic - logs debug and returns early + handle_partial_success(&invalid, Protocol::HttpBinary); + } + + #[cfg(feature = "http-proto")] + #[test] + fn test_handle_empty_response() { + let empty = vec![]; + + // Should not panic + handle_partial_success(&empty, Protocol::HttpBinary); + } + + #[cfg(feature = "http-json")] + #[test] + fn test_handle_invalid_json() { + let invalid_json = b"{not valid json}"; + + // Should not panic - logs debug and returns + handle_partial_success(invalid_json, Protocol::HttpJson); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/mod.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/mod.rs new file mode 100644 index 00000000..3014085e --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/mod.rs @@ -0,0 +1,1083 @@ +//! OTLP exporter builder and configurations. +//! +//! OTLP supports sending data via different protocols and formats. + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +use crate::exporter::http::HttpExporterBuilder; +#[cfg(feature = "grpc-tonic")] +use crate::exporter::tonic::TonicExporterBuilder; +use crate::Protocol; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use std::time::Duration; +use thiserror::Error; + +/// Target to which the exporter sends signals. +/// +/// When unset, the exporter uses the transport-specific default: `http://localhost:4317` +/// for gRPC or `http://localhost:4318` for HTTP. Learn more about endpoint handling at +/// . +pub const OTEL_EXPORTER_OTLP_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT"; +/// Key-value pairs to be used as headers associated with gRPC or HTTP requests +/// Example: `k1=v1,k2=v2` +/// Note: as of now, this is only supported for HTTP requests. +pub const OTEL_EXPORTER_OTLP_HEADERS: &str = "OTEL_EXPORTER_OTLP_HEADERS"; +/// Protocol the exporter will use. Either `http/protobuf` or `grpc`. +pub const OTEL_EXPORTER_OTLP_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_PROTOCOL"; +/// Compression algorithm to use, defaults to none. +pub const OTEL_EXPORTER_OTLP_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_COMPRESSION"; + +/// Protocol value for HTTP with protobuf encoding +pub const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF: &str = "http/protobuf"; +/// Protocol value for gRPC +pub const OTEL_EXPORTER_OTLP_PROTOCOL_GRPC: &str = "grpc"; +/// Protocol value for HTTP with JSON encoding +pub const OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON: &str = "http/json"; + +/// Max waiting time for the backend to process each signal batch, defaults to 10 seconds. +pub const OTEL_EXPORTER_OTLP_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_TIMEOUT"; + +/// Whether to disable TLS for the exporter's gRPC connection. +/// Per the [OTLP exporter spec](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options), +/// this only applies to gRPC endpoints that have no explicit scheme; an endpoint +/// with an explicit scheme is used as-is. HTTP security is determined by the +/// endpoint URL scheme. +/// There is intentionally no programmatic builder method — this is env-var-only. +/// Default: `false` (TLS is used). +pub const OTEL_EXPORTER_OTLP_INSECURE: &str = "OTEL_EXPORTER_OTLP_INSECURE"; +/// Default max waiting time for the backend to process each signal batch. +pub const OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT: Duration = Duration::from_millis(10000); + +// Endpoints per protocol https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md +#[cfg(feature = "grpc-tonic")] +const OTEL_EXPORTER_OTLP_GRPC_ENDPOINT_DEFAULT: &str = "http://localhost:4317"; +#[cfg(any(feature = "http-proto", feature = "http-json"))] +const OTEL_EXPORTER_OTLP_HTTP_ENDPOINT_DEFAULT: &str = "http://localhost:4318"; + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +pub(crate) mod http; +#[cfg(feature = "grpc-tonic")] +pub(crate) mod tonic; + +mod sealed { + pub trait WithExportConfig {} + + #[cfg(any(feature = "http-proto", feature = "http-json"))] + pub trait WithHttpConfig {} + + #[cfg(feature = "grpc-tonic")] + pub trait WithTonicConfig {} +} + +/// Configuration for the OTLP exporter. +#[derive(Debug, Default)] +pub(crate) struct ExportConfig { + /// The address of the OTLP collector. + /// Default address will be used based on the protocol. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + pub endpoint: Option, + + /// The protocol to use when communicating with the collector. + /// `None` means the protocol will be resolved from environment variables + /// or feature defaults at build time. + pub protocol: Option, + + /// The timeout to the collector. + /// The default value is 10 seconds. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + pub timeout: Option, +} + +/// Resolve protocol with priority: +/// 1. Programmatic configuration (provided value) +/// 2. Signal-specific environment variable +/// 3. Generic OTEL_EXPORTER_OTLP_PROTOCOL environment variable +/// 4. Feature-based default +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +pub(crate) fn resolve_protocol( + signal_protocol_var: &str, + provided_protocol: Option, +) -> Protocol { + if let Some(protocol) = provided_protocol { + return protocol; + } + if let Some(protocol) = Protocol::parse_from_env_var(signal_protocol_var) { + return protocol; + } + if let Some(protocol) = Protocol::from_env() { + return protocol; + } + Protocol::feature_default() +} + +#[derive(Error, Debug)] +/// Errors that can occur while building an exporter. +pub enum ExporterBuildError { + /// Exporter configuration is invalid and should be corrected by the user. + /// + /// The error message identifies the invalid setting and is intended for + /// diagnostics. It should not be used for programmatic decisions. + #[error("invalid exporter configuration: {0}")] + InvalidConfiguration(String), + + /// Failed due to an internal error. + /// + /// The error message is intended for logging purposes only and should not + /// be used to make programmatic decisions. It is implementation-specific + /// and subject to change without notice. Consumers of this error should not + /// rely on its content beyond logging. + #[error("exporter initialization failed: {0}")] + InternalFailure(String), +} + +impl ExporterBuildError { + #[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] + pub(crate) fn invalid_configuration( + name: &str, + reason: impl std::fmt::Display, + ) -> ExporterBuildError { + ExporterBuildError::InvalidConfiguration(format!("{name}: {reason}")) + } + + #[cfg(any(feature = "http-proto", feature = "http-json"))] + pub(crate) fn internal_failure(reason: impl std::fmt::Display) -> ExporterBuildError { + ExporterBuildError::InternalFailure(reason.to_string()) + } +} + +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +pub(crate) fn read_env_var(name: &str) -> Result, ExporterBuildError> { + match std::env::var(name) { + Ok(value) if value.is_empty() => Ok(None), + Ok(value) => Ok(Some(value)), + Err(std::env::VarError::NotPresent) => Ok(None), + Err(std::env::VarError::NotUnicode(_)) => Err(ExporterBuildError::invalid_configuration( + name, + "environment variable value is not valid Unicode", + )), + } +} + +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +pub(crate) fn read_enum_env_var(name: &str) -> Option { + match std::env::var(name) { + Ok(value) if value.is_empty() => None, + Ok(value) => Some(value), + Err(std::env::VarError::NotPresent) => None, + Err(std::env::VarError::NotUnicode(_)) => { + warn_ignored_enum_env_var(name, "", "value is not valid Unicode"); + None + } + } +} + +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +pub(crate) fn warn_ignored_enum_env_var( + environment_variable: &str, + value: &str, + reason: impl std::fmt::Display, +) { + let reason = reason.to_string(); + let message = format!("Ignoring value '{value}' for {environment_variable}: {reason}"); + opentelemetry::otel_warn!( + name: "Exporter.Config.InvalidEnvironmentVariable", + message = message.as_str(), + environment_variable = environment_variable, + value = value, + reason = reason.as_str() + ); +} + +#[cfg(all( + any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"), + not(all(feature = "grpc-tonic", feature = "http-proto", feature = "http-json")) +))] +pub(crate) fn warn_missing_protocol_feature( + environment_variable: &str, + value: &str, + feature: &str, +) { + warn_ignored_enum_env_var( + environment_variable, + value, + format!("feature '{feature}' is not enabled"), + ); +} + +/// The compression algorithm to use when sending data. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Compression { + /// Compresses data using gzip. + Gzip, + /// Compresses data using zstd. + Zstd, +} + +impl Display for Compression { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Compression::Gzip => write!(f, "gzip"), + Compression::Zstd => write!(f, "zstd"), + } + } +} + +/// An error parsing an OTLP configuration value. +/// +/// The error message provides diagnostic context. Its wording is not a stable +/// interface and should not be used for programmatic decisions. +#[derive(Debug, Error)] +#[error("{message}")] +pub struct ParseConfigError { + message: String, +} + +impl FromStr for Compression { + type Err = ParseConfigError; + + fn from_str(s: &str) -> Result { + match s { + "gzip" => Ok(Compression::Gzip), + "zstd" => Ok(Compression::Zstd), + _ => Err(ParseConfigError { + message: format!("unsupported compression algorithm '{s}'"), + }), + } + } +} + +/// Resolve compression from environment variables with priority: +/// 1. Provided config value +/// 2. Signal-specific environment variable +/// 3. Generic OTEL_EXPORTER_OTLP_COMPRESSION +/// 4. None (default) +#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] +fn resolve_compression_from_env( + config_compression: Option, + signal_env_var: &str, + convert: impl Fn(Compression) -> Result, +) -> Result, ExporterBuildError> { + if let Some(compression) = config_compression { + return convert(compression).map(Some); + } + for name in [signal_env_var, OTEL_EXPORTER_OTLP_COMPRESSION] { + let Some(value) = read_enum_env_var(name) else { + continue; + }; + if value.eq_ignore_ascii_case("none") { + return Ok(None); + } + let compression = match value.to_ascii_lowercase().parse::() { + Ok(compression) => compression, + Err(error) => { + warn_ignored_enum_env_var(name, &value, error); + continue; + } + }; + match convert(compression) { + Ok(compression) => return Ok(Some(compression)), + Err(error) => warn_ignored_enum_env_var(name, &value, error), + } + } + Ok(None) +} + +/// Resolve whether the connection should be insecure (no TLS). +/// +/// Priority: +/// 1. Signal-specific env var (e.g., `OTEL_EXPORTER_OTLP_TRACES_INSECURE`) +/// 2. Generic `OTEL_EXPORTER_OTLP_INSECURE` +/// 3. Default: `false` (secure/TLS) +/// +/// Values: `"true"` (case-insensitive) = insecure, everything else = secure. +/// Per the spec, this only applies to gRPC connections. +#[cfg(feature = "grpc-tonic")] +pub(crate) fn resolve_insecure(signal_insecure_var: &str) -> bool { + let value = std::env::var(signal_insecure_var) + .ok() + .or_else(|| std::env::var(OTEL_EXPORTER_OTLP_INSECURE).ok()); + match value { + Some(val) => val.eq_ignore_ascii_case("true"), + None => false, + } +} + +/// Returns the default protocol based on enabled features. +/// +/// Note: This does not consult environment variables. Protocol resolution +/// from environment variables is handled internally by the exporter builders. +/// +/// Priority order (first available wins): +/// 1. http-json (if enabled) +/// 2. http-proto (if enabled) +/// 3. grpc-tonic (if enabled) +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +impl Default for Protocol { + fn default() -> Self { + Protocol::feature_default() + } +} + +/// default user-agent headers +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +fn default_headers() -> std::collections::HashMap { + let mut headers = std::collections::HashMap::new(); + headers.insert( + "User-Agent".to_string(), + format!("OTel-OTLP-Exporter-Rust/{}", env!("CARGO_PKG_VERSION")), + ); + headers +} + +/// Provide access to the [ExportConfig] field within the exporter builders. +pub(crate) trait HasExportConfig { + /// Return a mutable reference to the [ExportConfig] within the exporter builders. + fn export_config(&mut self) -> &mut ExportConfig; +} + +/// Provide [ExportConfig] access to the [TonicExporterBuilder]. +#[cfg(feature = "grpc-tonic")] +impl HasExportConfig for TonicExporterBuilder { + fn export_config(&mut self) -> &mut ExportConfig { + &mut self.exporter_config + } +} + +/// Provide [ExportConfig] access to the [HttpExporterBuilder]. +#[cfg(any(feature = "http-proto", feature = "http-json"))] +impl HasExportConfig for HttpExporterBuilder { + fn export_config(&mut self) -> &mut ExportConfig { + &mut self.exporter_config + } +} + +/// Expose methods to set export configuration on exporter builders. +/// +/// ## Examples +/// ``` +/// # #[cfg(all(feature = "trace", feature = "grpc-tonic"))] +/// # { +/// use crate::opentelemetry_otlp::WithExportConfig; +/// let exporter_builder = opentelemetry_otlp::SpanExporter::builder() +/// .with_tonic() +/// .with_endpoint("http://localhost:7201"); +/// # } +/// ``` +/// +/// This trait is sealed and cannot be implemented for types outside this crate. +pub trait WithExportConfig: sealed::WithExportConfig { + /// Set the address of the OTLP collector. If not set or set to empty string, the default address is used. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + fn with_endpoint>(self, endpoint: T) -> Self; + /// Set the transport protocol to use when communicating with the collector. + /// + /// This is mainly useful on the HTTP transport to choose between + /// [`Protocol::HttpBinary`] (protobuf) and [`Protocol::HttpJson`]. + /// Setting a protocol that conflicts with the chosen transport + /// (e.g. [`Protocol::Grpc`] on an HTTP builder) will cause `build()` + /// to return an error. + /// + /// Note that `with_protocol()` is only available after a transport has + /// been selected via `.with_http()` or `.with_tonic()`. If you call + /// `.builder().build()` directly without selecting a transport, the + /// transport is chosen automatically from the + /// `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable and enabled + /// cargo features - `with_protocol()` is not involved in that path. + /// + /// Note: Programmatically setting this will override any value set via the + /// `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable. + fn with_protocol(self, protocol: Protocol) -> Self; + /// Set the timeout to the collector. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + fn with_timeout(self, timeout: Duration) -> Self; +} + +impl sealed::WithExportConfig for B {} + +impl WithExportConfig for B { + fn with_endpoint>(mut self, endpoint: T) -> Self { + self.export_config().endpoint = Some(endpoint.into()); + self + } + + fn with_protocol(mut self, protocol: Protocol) -> Self { + self.export_config().protocol = Some(protocol); + self + } + + fn with_timeout(mut self, timeout: Duration) -> Self { + self.export_config().timeout = Some(timeout); + self + } +} + +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +fn resolve_timeout(signal_timeout_var: &str, provided_timeout: Option<&Duration>) -> Duration { + // programmatic configuration overrides any value set via environment variables + if let Some(timeout) = provided_timeout { + *timeout + } else if let Some(timeout) = std::env::var(signal_timeout_var) + .ok() + .and_then(|s| s.parse().ok()) + { + // per signal env var is not modified + Duration::from_millis(timeout) + } else if let Some(timeout) = std::env::var(OTEL_EXPORTER_OTLP_TIMEOUT) + .ok() + .and_then(|s| s.parse().ok()) + { + // if signal env var is not set, then we check if the OTEL_EXPORTER_OTLP_TIMEOUT env var is set + Duration::from_millis(timeout) + } else { + OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT + } +} + +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +fn parse_header_string(value: &str) -> impl Iterator { + value + .split_terminator(',') + .map(str::trim) + .filter_map(parse_header_key_value_string) +} + +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +fn url_decode(value: &str) -> Option { + let mut result = String::with_capacity(value.len()); + let mut chars_to_decode = Vec::::new(); + let mut all_chars = value.chars(); + + loop { + let ch = all_chars.next(); + + if ch.is_some() && ch.unwrap() == '%' { + chars_to_decode.push( + u8::from_str_radix(&format!("{}{}", all_chars.next()?, all_chars.next()?), 16) + .ok()?, + ); + continue; + } + + if !chars_to_decode.is_empty() { + result.push_str(std::str::from_utf8(&chars_to_decode).ok()?); + chars_to_decode.clear(); + } + + if let Some(c) = ch { + result.push(c); + } else { + return Some(result); + } + } +} + +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +fn parse_header_key_value_string(key_value_string: &str) -> Option<(&str, String)> { + key_value_string + .split_once('=') + .map(|(key, value)| { + ( + key.trim(), + url_decode(value.trim()).unwrap_or(value.to_string()), + ) + }) + .filter(|(key, value)| !key.is_empty() && !value.is_empty()) +} + +#[cfg(test)] +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +mod tests { + use super::{ + resolve_compression_from_env, Compression, ExporterBuildError, + OTEL_EXPORTER_OTLP_COMPRESSION, + }; + + pub(crate) fn run_env_test(env_vars: T, f: F) + where + F: FnOnce(), + T: Into>, + { + temp_env::with_vars( + env_vars + .into() + .iter() + .map(|&(k, v)| (k, Some(v))) + .collect::)>>(), + f, + ) + } + + #[cfg(any(feature = "http-proto", feature = "http-json"))] + #[test] + fn test_default_http_endpoint() { + let exporter_builder = crate::exporter::http::HttpExporterBuilder::default(); + + assert_eq!(exporter_builder.exporter_config.endpoint, None); + } + + #[cfg(feature = "logs")] + #[cfg(any(feature = "http-proto", feature = "http-json"))] + #[test] + fn export_builder_error_invalid_http_endpoint() { + use crate::{LogExporter, WithExportConfig}; + + let exporter_result = LogExporter::builder() + .with_http() + .with_endpoint("invalid_uri/something") + .with_timeout(std::time::Duration::from_secs(10)) + .build(); + + assert!( + matches!( + exporter_result, + Err(crate::exporter::ExporterBuildError::InvalidConfiguration(ref message)) + if message.contains("endpoint") + ), + "expected InvalidConfiguration error, but got {exporter_result:?}" + ); + } + + #[cfg(all(feature = "grpc-tonic", feature = "logs"))] + #[tokio::test] + async fn export_builder_error_invalid_grpc_endpoint() { + use crate::{LogExporter, WithExportConfig}; + + // Use a URI with an explicit scheme but malformed host to ensure it + // fails URI parsing regardless of INSECURE scheme-prepending logic + let exporter_result = LogExporter::builder() + .with_tonic() + .with_endpoint("http://[invalid") + .with_timeout(std::time::Duration::from_secs(10)) + .build(); + + assert!(matches!( + exporter_result, + Err(crate::exporter::ExporterBuildError::InvalidConfiguration(message)) + if message.contains("endpoint") + )); + } + + #[test] + fn exporter_build_error_categories_are_exhaustive() { + fn category(error: ExporterBuildError) -> &'static str { + match error { + ExporterBuildError::InvalidConfiguration(_) => "configuration", + ExporterBuildError::InternalFailure(_) => "internal", + } + } + + assert_eq!( + category(ExporterBuildError::InvalidConfiguration( + "endpoint: invalid URI".to_string() + )), + "configuration" + ); + assert_eq!( + category(ExporterBuildError::InternalFailure( + "thread panicked".to_string() + )), + "internal" + ); + } + + #[cfg(feature = "grpc-tonic")] + #[test] + fn test_default_tonic_endpoint() { + let exporter_builder = crate::exporter::tonic::TonicExporterBuilder::default(); + + assert_eq!(exporter_builder.exporter_config.endpoint, None); + } + + #[test] + fn test_default_protocol() { + #[cfg(all( + feature = "http-json", + not(any(feature = "grpc-tonic", feature = "http-proto")) + ))] + { + assert_eq!(crate::Protocol::default(), crate::Protocol::HttpJson); + } + + #[cfg(all( + feature = "http-proto", + not(any(feature = "grpc-tonic", feature = "http-json")) + ))] + { + assert_eq!(crate::Protocol::default(), crate::Protocol::HttpBinary); + } + + #[cfg(all( + feature = "grpc-tonic", + not(any(feature = "http-proto", feature = "http-json")) + ))] + { + assert_eq!(crate::Protocol::default(), crate::Protocol::Grpc); + } + } + + #[test] + fn test_protocol_from_env() { + use crate::{Protocol, OTEL_EXPORTER_OTLP_PROTOCOL}; + + // Test with no env var set - should return None + temp_env::with_var_unset(OTEL_EXPORTER_OTLP_PROTOCOL, || { + assert_eq!(Protocol::from_env(), None); + }); + + // Test with grpc protocol + #[cfg(feature = "grpc-tonic")] + run_env_test(vec![(OTEL_EXPORTER_OTLP_PROTOCOL, "grpc")], || { + assert_eq!(Protocol::from_env(), Some(Protocol::Grpc)); + }); + + // Test with http/protobuf protocol + #[cfg(feature = "http-proto")] + run_env_test(vec![(OTEL_EXPORTER_OTLP_PROTOCOL, "http/protobuf")], || { + assert_eq!(Protocol::from_env(), Some(Protocol::HttpBinary)); + }); + + // Test with http/json protocol + #[cfg(feature = "http-json")] + run_env_test(vec![(OTEL_EXPORTER_OTLP_PROTOCOL, "http/json")], || { + assert_eq!(Protocol::from_env(), Some(Protocol::HttpJson)); + }); + + // Test with invalid protocol - should return None + run_env_test(vec![(OTEL_EXPORTER_OTLP_PROTOCOL, "invalid")], || { + assert_eq!(Protocol::from_env(), None); + }); + } + + #[test] + fn test_default_protocol_ignores_env() { + // Protocol::default() should always return the feature-based default, + // NOT consult environment variables. Env var resolution is handled + // by resolve_protocol(). + + // Without any env vars, default() equals feature_default() + run_env_test(vec![], || { + assert_eq!( + crate::Protocol::default(), + crate::Protocol::feature_default() + ); + }); + + // Even with a valid env var set, default() still equals feature_default() + #[cfg(all(feature = "grpc-tonic", feature = "http-json"))] + run_env_test(vec![(crate::OTEL_EXPORTER_OTLP_PROTOCOL, "grpc")], || { + assert_eq!( + crate::Protocol::default(), + crate::Protocol::feature_default() + ); + }); + } + + #[test] + fn test_url_decode() { + let test_cases = vec![ + // Format: (encoded, expected_decoded) + ("v%201", Some("v 1")), + ("v 1", Some("v 1")), + ("%C3%B6%C3%A0%C2%A7%C3%96abcd%C3%84", Some("öà§ÖabcdÄ")), + ("v%XX1", None), + ]; + + for (encoded, expected_decoded) in test_cases { + assert_eq!( + super::url_decode(encoded), + expected_decoded.map(|v| v.to_string()), + ) + } + } + + #[test] + fn test_parse_header_string() { + let test_cases = vec![ + // Format: (input_str, expected_headers) + ("k1=v1", vec![("k1", "v1")]), + ("k1=v1,k2=v2", vec![("k1", "v1"), ("k2", "v2")]), + ("k1=v1=10,k2,k3", vec![("k1", "v1=10")]), + ("k1=v1,,,k2,k3=10", vec![("k1", "v1"), ("k3", "10")]), + ]; + + for (input_str, expected_headers) in test_cases { + assert_eq!( + super::parse_header_string(input_str).collect::>(), + expected_headers + .into_iter() + .map(|(k, v)| (k, v.to_string())) + .collect::>(), + ) + } + } + + #[test] + fn test_parse_header_key_value_string() { + let test_cases = vec![ + // Format: (input_str, expected_header) + ("k1=v1", Some(("k1", "v1"))), + ( + "Authentication=Basic AAA", + Some(("Authentication", "Basic AAA")), + ), + ( + "Authentication=Basic%20AAA", + Some(("Authentication", "Basic AAA")), + ), + ("k1=%XX", Some(("k1", "%XX"))), + ("", None), + ("=v1", None), + ("k1=", None), + ]; + + for (input_str, expected_headers) in test_cases { + assert_eq!( + super::parse_header_key_value_string(input_str), + expected_headers.map(|(k, v)| (k, v.to_string())), + ) + } + } + + #[test] + fn test_priority_of_signal_env_over_generic_env_for_timeout() { + run_env_test( + vec![ + (crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, "3000"), + (super::OTEL_EXPORTER_OTLP_TIMEOUT, "2000"), + ], + || { + let timeout = + super::resolve_timeout(crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, None); + assert_eq!(timeout.as_millis(), 3000); + }, + ); + } + + #[test] + fn test_priority_of_code_based_config_over_envs_for_timeout() { + run_env_test( + vec![ + (crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, "3000"), + (super::OTEL_EXPORTER_OTLP_TIMEOUT, "2000"), + ], + || { + let timeout = super::resolve_timeout( + crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + Some(&std::time::Duration::from_millis(1000)), + ); + assert_eq!(timeout.as_millis(), 1000); + }, + ); + } + + #[test] + fn test_use_default_when_others_missing_for_timeout() { + run_env_test(vec![], || { + let timeout = super::resolve_timeout(crate::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, None); + assert_eq!(timeout.as_millis(), 10_000); + }); + } + + #[test] + fn test_protocol_parse_from_env_var() { + use crate::Protocol; + + // Test with custom env var name + temp_env::with_var_unset("MY_CUSTOM_PROTOCOL_VAR", || { + assert_eq!(Protocol::parse_from_env_var("MY_CUSTOM_PROTOCOL_VAR"), None); + }); + + #[cfg(feature = "http-proto")] + run_env_test(vec![("MY_CUSTOM_PROTOCOL_VAR", "HTTP/PROTOBUF")], || { + assert_eq!( + Protocol::parse_from_env_var("MY_CUSTOM_PROTOCOL_VAR"), + Some(Protocol::HttpBinary) + ); + }); + + #[cfg(feature = "grpc-tonic")] + run_env_test(vec![("MY_CUSTOM_PROTOCOL_VAR", "GRPC")], || { + assert_eq!( + Protocol::parse_from_env_var("MY_CUSTOM_PROTOCOL_VAR"), + Some(Protocol::Grpc) + ); + }); + + run_env_test(vec![("MY_CUSTOM_PROTOCOL_VAR", "invalid")], || { + assert_eq!(Protocol::parse_from_env_var("MY_CUSTOM_PROTOCOL_VAR"), None); + }); + } + + #[cfg(all(feature = "grpc-tonic", feature = "http-proto"))] + #[test] + fn test_invalid_signal_protocol_falls_back_to_generic() { + run_env_test( + vec![ + (crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, "invalid"), + (crate::OTEL_EXPORTER_OTLP_PROTOCOL, "GRPC"), + ], + || { + assert_eq!( + super::resolve_protocol(crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, None), + crate::Protocol::Grpc + ); + }, + ); + } + + #[cfg(not(feature = "grpc-tonic"))] + #[test] + fn test_protocol_requires_enabled_feature() { + use crate::Protocol; + + run_env_test(vec![("MY_CUSTOM_PROTOCOL_VAR", "grpc")], || { + assert_eq!(Protocol::parse_from_env_var("MY_CUSTOM_PROTOCOL_VAR"), None); + }); + } + + #[cfg(feature = "http-proto")] + #[test] + fn test_resolve_protocol_signal_env_overrides_generic() { + use crate::Protocol; + + run_env_test( + vec![ + (crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, "http/protobuf"), + (crate::OTEL_EXPORTER_OTLP_PROTOCOL, "grpc"), + ], + || { + let protocol = + super::resolve_protocol(crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, None); + assert_eq!(protocol, Protocol::HttpBinary); + }, + ); + } + + #[cfg(all(feature = "grpc-tonic", feature = "trace"))] + #[test] + fn test_resolve_insecure_signal_overrides_generic() { + run_env_test( + vec![ + (crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "true"), + (super::OTEL_EXPORTER_OTLP_INSECURE, "false"), + ], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(insecure); + }, + ); + } + + #[cfg(feature = "http-proto")] + #[test] + fn test_resolve_protocol_code_overrides_all_envs() { + use crate::Protocol; + + run_env_test( + vec![ + (crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, "grpc"), + (crate::OTEL_EXPORTER_OTLP_PROTOCOL, "grpc"), + ], + || { + let protocol = super::resolve_protocol( + crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + Some(Protocol::HttpBinary), + ); + assert_eq!(protocol, Protocol::HttpBinary); + }, + ); + } + + #[cfg(all(feature = "grpc-tonic", feature = "trace"))] + #[test] + fn test_resolve_insecure_default_is_false() { + temp_env::with_vars_unset( + [ + crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, + super::OTEL_EXPORTER_OTLP_INSECURE, + ], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(!insecure); + }, + ); + } + + #[cfg(all(feature = "grpc-tonic", feature = "http-proto"))] + #[test] + fn test_resolve_protocol_falls_back_to_generic_env() { + use crate::Protocol; + + run_env_test(vec![(crate::OTEL_EXPORTER_OTLP_PROTOCOL, "grpc")], || { + let protocol = super::resolve_protocol(crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, None); + assert_eq!(protocol, Protocol::Grpc); + }); + } + + #[test] + fn test_resolve_protocol_falls_back_to_feature_default() { + use crate::Protocol; + + run_env_test(vec![], || { + let protocol = super::resolve_protocol(crate::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, None); + assert_eq!(protocol, Protocol::feature_default()); + }); + } + + #[test] + fn compression_env_precedence() { + for (signal, generic, expected) in [ + ("", "gzip", Some(Compression::Gzip)), + ("invalid", "GZIP", Some(Compression::Gzip)), + ("ZSTD", "gzip", Some(Compression::Zstd)), + ("NoNe", "gzip", None), + ("invalid", "none", None), + ("invalid", "invalid", None), + ] { + run_env_test( + vec![ + ("MY_CUSTOM_COMPRESSION_VAR", signal), + (OTEL_EXPORTER_OTLP_COMPRESSION, generic), + ], + || { + assert_eq!( + resolve_compression_from_env(None, "MY_CUSTOM_COMPRESSION_VAR", Ok) + .unwrap(), + expected, + "signal={signal}, generic={generic}" + ) + }, + ); + } + } + + #[test] + fn unavailable_compression_env_falls_back_but_programmatic_config_errors() { + run_env_test( + vec![ + ("MY_CUSTOM_COMPRESSION_VAR", "gzip"), + (OTEL_EXPORTER_OTLP_COMPRESSION, "zstd"), + ], + || { + let convert = |compression| match compression { + Compression::Gzip => Err(super::ExporterBuildError::invalid_configuration( + "compression", + "gzip is unavailable", + )), + Compression::Zstd => Ok(compression), + }; + assert_eq!( + resolve_compression_from_env(None, "MY_CUSTOM_COMPRESSION_VAR", convert) + .unwrap(), + Some(Compression::Zstd) + ); + assert!(resolve_compression_from_env( + Some(Compression::Gzip), + "MY_CUSTOM_COMPRESSION_VAR", + convert + ) + .is_err()); + }, + ); + } + + #[cfg(unix)] + #[test] + fn test_non_unicode_compression_env_falls_back_to_generic() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + temp_env::with_vars( + [ + ( + "MY_CUSTOM_COMPRESSION_VAR", + Some(OsStr::from_bytes(b"\x80")), + ), + (OTEL_EXPORTER_OTLP_COMPRESSION, Some(OsStr::new("zstd"))), + ], + || { + assert_eq!( + resolve_compression_from_env(None, "MY_CUSTOM_COMPRESSION_VAR", Ok).unwrap(), + Some(Compression::Zstd) + ); + }, + ); + } + + #[cfg(all(feature = "grpc-tonic", feature = "trace"))] + #[test] + fn test_resolve_insecure_case_insensitive() { + run_env_test( + vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "True")], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(insecure); + }, + ); + run_env_test( + vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "TRUE")], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(insecure); + }, + ); + } + + #[cfg(all(feature = "grpc-tonic", feature = "trace"))] + #[test] + fn test_resolve_insecure_falls_back_to_generic() { + temp_env::with_var_unset(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, || { + run_env_test(vec![(super::OTEL_EXPORTER_OTLP_INSECURE, "true")], || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(insecure); + }); + }); + } + + #[cfg(all(feature = "grpc-tonic", feature = "trace"))] + #[test] + fn test_resolve_insecure_non_true_is_false() { + run_env_test( + vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "false")], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(!insecure); + }, + ); + run_env_test( + vec![(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, "invalid")], + || { + let insecure = super::resolve_insecure(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE); + assert!(!insecure); + }, + ); + } +} + +#[cfg(test)] +mod config_parsing_tests { + use crate::{Compression, ParseConfigError}; + + #[test] + fn compression_parsing_uses_config_error() { + for (input, expected) in [("gzip", Compression::Gzip), ("zstd", Compression::Zstd)] { + let result: Result = input.parse(); + assert_eq!(result.unwrap(), expected); + } + + // Public parsing remains strict; environment normalization is separate. + for input in ["br", "GZIP", "", "none", " gzip "] { + let result: Result = input.parse(); + let error = result.unwrap_err(); + let error: &(dyn std::error::Error + Send + Sync) = &error; + let message = error.to_string(); + assert!(message.contains("compression")); + assert!(message.contains(input)); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/logs.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/logs.rs new file mode 100644 index 00000000..6ebdc939 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/logs.rs @@ -0,0 +1,150 @@ +use core::fmt; +use opentelemetry::{otel_debug, otel_warn}; +use opentelemetry_proto::tonic::collector::logs::v1::{ + logs_service_client::LogsServiceClient, ExportLogsServiceRequest, +}; +use opentelemetry_sdk::error::{OTelSdkError, OTelSdkResult}; +use opentelemetry_sdk::logs::{LogBatch, LogExporter}; +use std::sync::{Arc, Mutex}; +use std::time; +use tonic::{codegen::CompressionEncoding, service::Interceptor, transport::Channel, Request}; + +use opentelemetry_proto::transform::logs::tonic::group_logs_by_resource_and_scope; + +use super::BoxInterceptor; + +use crate::RetryPolicy; + +pub(crate) struct TonicLogsClient { + inner: Mutex>, + retry_policy: RetryPolicy, + timeout: std::time::Duration, + #[allow(dead_code)] + // would be removed once we support set_resource for metrics. + resource: opentelemetry_proto::transform::common::tonic::ResourceAttributesWithSchema, +} + +struct ClientInner { + client: LogsServiceClient, + interceptor: BoxInterceptor, +} + +impl fmt::Debug for TonicLogsClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("TonicLogsClient") + } +} + +impl TonicLogsClient { + pub(super) fn new( + channel: Channel, + interceptor: BoxInterceptor, + compression: Option, + retry_policy: Option, + timeout: std::time::Duration, + ) -> Self { + let mut client = LogsServiceClient::new(channel); + if let Some(compression) = compression { + client = client + .send_compressed(compression) + .accept_compressed(compression); + } + + otel_debug!(name: "TonicsLogsClientBuilt"); + + TonicLogsClient { + inner: Mutex::new(Some(ClientInner { + client, + interceptor, + })), + retry_policy: retry_policy.unwrap_or_default(), + timeout, + resource: Default::default(), + } + } +} + +impl LogExporter for TonicLogsClient { + async fn export(&self, batch: LogBatch<'_>) -> OTelSdkResult { + let batch = Arc::new(batch); + + match super::tonic_retry_with_backoff( + &self.retry_policy, + self.timeout, + crate::retry_classification::grpc::classify_tonic_status, + "TonicLogsClient.Export", + || async { + let batch_clone = Arc::clone(&batch); + + // Execute the export operation + let (mut client, metadata, extensions) = self + .inner + .lock() + .map_err(|e| tonic::Status::internal(format!("Failed to acquire lock: {e:?}"))) + .and_then(|mut inner| match &mut *inner { + Some(inner) => { + let (m, e, _) = inner + .interceptor + .call(Request::new(())) + .map_err(|e| { + super::handle_interceptor_error!("TonicLogsClient", e) + })? + .into_parts(); + Ok((inner.client.clone(), m, e)) + } + None => Err(tonic::Status::failed_precondition( + "log exporter is already shut down", + )), + })?; + + let resource_logs = group_logs_by_resource_and_scope(&batch_clone, &self.resource); + + otel_debug!(name: "TonicLogsClient.ExportStarted"); + + client + .export(Request::from_parts( + metadata, + extensions, + ExportLogsServiceRequest { resource_logs }, + )) + .await + .map(|response| { + otel_debug!(name: "TonicLogsClient.ExportSucceeded"); + + // Handle partial success. As per spec, we log and _do not_ retry. + if let Some(partial_success) = response.into_inner().partial_success { + if partial_success.rejected_log_records > 0 + || !partial_success.error_message.is_empty() + { + otel_warn!( + name: "TonicLogsClient.PartialSuccess", + rejected_log_records = partial_success.rejected_log_records, + error_message = partial_success.error_message.as_str(), + ); + } + } + }) + }, + ) + .await + { + Ok(_) => Ok(()), + Err(tonic_status) => { + super::handle_tonic_export_error!("TonicLogsClient", tonic_status) + } + } + } + + fn shutdown_with_timeout(&self, _timeout: time::Duration) -> OTelSdkResult { + self.inner + .lock() + .map_err(|e| OTelSdkError::InternalFailure(format!("Failed to acquire lock: {e}")))? + .take(); + + Ok(()) + } + + fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) { + self.resource = resource.into(); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/metrics.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/metrics.rs new file mode 100644 index 00000000..7853181d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/metrics.rs @@ -0,0 +1,135 @@ +use core::fmt; +use std::sync::Mutex; + +use opentelemetry::{otel_debug, otel_warn}; +use opentelemetry_proto::tonic::collector::metrics::v1::{ + metrics_service_client::MetricsServiceClient, ExportMetricsServiceRequest, +}; +use opentelemetry_sdk::error::{OTelSdkError, OTelSdkResult}; +use opentelemetry_sdk::metrics::data::ResourceMetrics; +use tonic::{codegen::CompressionEncoding, service::Interceptor, transport::Channel, Request}; + +use super::BoxInterceptor; +use crate::metric::MetricsClient; + +use crate::RetryPolicy; + +pub(crate) struct TonicMetricsClient { + inner: Mutex>, + retry_policy: RetryPolicy, + timeout: std::time::Duration, +} + +struct ClientInner { + client: MetricsServiceClient, + interceptor: BoxInterceptor, +} + +impl fmt::Debug for TonicMetricsClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("TonicMetricsClient") + } +} + +impl TonicMetricsClient { + pub(super) fn new( + channel: Channel, + interceptor: BoxInterceptor, + compression: Option, + retry_policy: Option, + timeout: std::time::Duration, + ) -> Self { + let mut client = MetricsServiceClient::new(channel); + if let Some(compression) = compression { + client = client + .send_compressed(compression) + .accept_compressed(compression); + } + + otel_debug!(name: "TonicsMetricsClientBuilt"); + + TonicMetricsClient { + inner: Mutex::new(Some(ClientInner { + client, + interceptor, + })), + retry_policy: retry_policy.unwrap_or_default(), + timeout, + } + } +} + +impl MetricsClient for TonicMetricsClient { + async fn export(&self, metrics: &ResourceMetrics) -> OTelSdkResult { + match super::tonic_retry_with_backoff( + &self.retry_policy, + self.timeout, + crate::retry_classification::grpc::classify_tonic_status, + "TonicMetricsClient.Export", + || async { + // Execute the export operation + let (mut client, metadata, extensions) = self + .inner + .lock() + .map_err(|e| tonic::Status::internal(format!("Failed to acquire lock: {e:?}"))) + .and_then(|mut inner| match &mut *inner { + Some(inner) => { + let (m, e, _) = inner + .interceptor + .call(Request::new(())) + .map_err(|e| { + super::handle_interceptor_error!("TonicMetricsClient", e) + })? + .into_parts(); + Ok((inner.client.clone(), m, e)) + } + None => Err(tonic::Status::failed_precondition( + "metrics exporter is already shut down", + )), + })?; + + otel_debug!(name: "TonicMetricsClient.ExportStarted"); + + client + .export(Request::from_parts( + metadata, + extensions, + ExportMetricsServiceRequest::from(metrics), + )) + .await + .map(|response| { + otel_debug!(name: "TonicMetricsClient.ExportSucceeded"); + + // Handle partial success. As per spec, we log and _do not_ retry. + if let Some(partial_success) = response.into_inner().partial_success { + if partial_success.rejected_data_points > 0 + || !partial_success.error_message.is_empty() + { + otel_warn!( + name: "TonicMetricsClient.PartialSuccess", + rejected_data_points = partial_success.rejected_data_points, + error_message = partial_success.error_message.as_str(), + ); + } + } + }) + }, + ) + .await + { + Ok(_) => Ok(()), + Err(tonic_status) => { + super::handle_tonic_export_error!("TonicMetricsClient", tonic_status) + } + } + } + + fn shutdown(&self) -> OTelSdkResult { + self.inner + .lock() + .map_err(|e| OTelSdkError::InternalFailure(format!("Failed to acquire lock: {e}")))? + .take(); + + Ok(()) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/mod.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/mod.rs new file mode 100644 index 00000000..85db6601 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/mod.rs @@ -0,0 +1,1780 @@ +use std::env; +use std::fmt::{Debug, Formatter}; +use std::str::FromStr; + +use http::{HeaderMap, HeaderName, HeaderValue}; +use opentelemetry::otel_debug; +use tonic::codec::CompressionEncoding; +use tonic::metadata::{KeyAndValueRef, MetadataMap}; +use tonic::service::Interceptor; +use tonic::transport::Channel; +#[cfg(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" +))] +use tonic::transport::ClientTlsConfig; + +use super::{default_headers, parse_header_string, OTEL_EXPORTER_OTLP_GRPC_ENDPOINT_DEFAULT}; +use super::{resolve_timeout, ExporterBuildError}; +use crate::exporter::Compression; +use crate::{exporter::ExportConfig, OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS}; + +#[cfg(all( + feature = "grpc-tonic", + any(feature = "trace", feature = "metrics", feature = "logs") +))] +use crate::retry::retry_with_backoff; +use crate::RetryPolicy; +#[cfg(all( + feature = "grpc-tonic", + any(feature = "trace", feature = "metrics", feature = "logs") +))] +use std::future::Future; + +#[cfg(feature = "logs")] +pub(crate) mod logs; + +#[cfg(feature = "metrics")] +pub(crate) mod metrics; + +#[cfg(feature = "trace")] +pub(crate) mod trace; + +/// Configuration for [tonic] +/// +/// [tonic]: https://github.com/hyperium/tonic +#[derive(Debug, Default)] +#[non_exhaustive] +pub(crate) struct TonicConfig { + /// Custom metadata entries to send to the collector. + pub(crate) metadata: Option, + /// TLS settings for the collector endpoint. + #[cfg(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + ))] + pub(crate) tls_config: Option, + /// The compression algorithm to use when communicating with the collector. + pub(crate) compression: Option, + pub(crate) channel: Option, + pub(crate) interceptor: Option, + /// The retry policy to use for gRPC requests. + pub(crate) retry_policy: Option, +} + +fn to_tonic_compression( + compression: Compression, +) -> Result { + match compression { + #[cfg(feature = "gzip-tonic")] + Compression::Gzip => Ok(tonic::codec::CompressionEncoding::Gzip), + #[cfg(not(feature = "gzip-tonic"))] + Compression::Gzip => Err(ExporterBuildError::invalid_configuration( + "compression", + "feature 'gzip-tonic' is required to use the compression algorithm 'gzip'", + )), + #[cfg(feature = "zstd-tonic")] + Compression::Zstd => Ok(tonic::codec::CompressionEncoding::Zstd), + #[cfg(not(feature = "zstd-tonic"))] + Compression::Zstd => Err(ExporterBuildError::invalid_configuration( + "compression", + "feature 'zstd-tonic' is required to use the compression algorithm 'zstd'", + )), + } +} + +/// Configuration for the [tonic] OTLP GRPC exporter. +/// +/// It allows you to +/// - add additional metadata +/// - set tls config (via the `tls-ring`, `tls-aws-lc`, or `tls-provider-agnostic` features) +/// - specify custom [channel]s +/// +/// [tonic]: +/// [channel]: tonic::transport::Channel +/// +/// ## Examples +/// +/// ```no_run +/// # #[cfg(feature="metrics")] +/// use opentelemetry_sdk::metrics::Temporality; +/// +/// # fn main() -> Result<(), Box> { +/// // Create a span exporter you can use to when configuring tracer providers +/// # #[cfg(feature="trace")] +/// let span_exporter = opentelemetry_otlp::SpanExporter::builder().with_tonic().build()?; +/// +/// // Create a metric exporter you can use when configuring meter providers +/// # #[cfg(feature="metrics")] +/// let metric_exporter = opentelemetry_otlp::MetricExporter::builder() +/// .with_tonic() +/// .with_temporality(Temporality::default()) +/// .build()?; +/// +/// // Create a log exporter you can use when configuring logger providers +/// # #[cfg(feature="logs")] +/// let log_exporter = opentelemetry_otlp::LogExporter::builder().with_tonic().build()?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug)] +pub(crate) struct TonicExporterBuilder { + pub(crate) tonic_config: TonicConfig, + pub(crate) exporter_config: ExportConfig, +} + +pub(crate) struct BoxInterceptor(Box); +impl tonic::service::Interceptor for BoxInterceptor { + fn call(&mut self, request: tonic::Request<()>) -> Result, tonic::Status> { + self.0.call(request) + } +} + +impl Debug for BoxInterceptor { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "BoxInterceptor(..)") + } +} + +impl Default for TonicExporterBuilder { + fn default() -> Self { + TonicExporterBuilder { + tonic_config: TonicConfig { + metadata: Some(MetadataMap::from_headers( + (&default_headers()) + .try_into() + .expect("Invalid tonic headers"), + )), + #[cfg(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + ))] + tls_config: None, + compression: None, + channel: Option::default(), + interceptor: Option::default(), + retry_policy: None, + }, + exporter_config: ExportConfig { + protocol: Some(crate::Protocol::Grpc), + ..Default::default() + }, + } + } +} + +impl TonicExporterBuilder { + // This is for clippy to work with only the grpc-tonic feature enabled + #[allow(unused, clippy::type_complexity)] + fn build_channel( + self, + signal_endpoint_var: &str, + signal_timeout_var: &str, + signal_compression_var: &str, + signal_headers_var: &str, + signal_protocol_var: &str, + signal_insecure_var: &str, + ) -> Result< + ( + Channel, + BoxInterceptor, + Option, + Option, + std::time::Duration, + ), + ExporterBuildError, + > { + // Resolve protocol and validate compatibility with gRPC transport. + // Note: TonicExporterBuilder defaults protocol to Some(Grpc), so for the + // typical `.with_tonic().build()` path, resolve_protocol returns Grpc + // immediately (env vars are skipped because programmatic config takes + // precedence). This validation primarily catches programmatic misuse like + // `.with_tonic().with_protocol(Protocol::HttpBinary).build()`. + // Env var-based transport selection is handled at the auto-select layer + // (e.g., SpanExporter::builder().build()), which routes to the correct + // transport before reaching this code. + #[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] + { + let protocol = + super::resolve_protocol(signal_protocol_var, self.exporter_config.protocol); + + let is_http_protocol = false; + #[cfg(feature = "http-proto")] + let is_http_protocol = + is_http_protocol || matches!(protocol, crate::Protocol::HttpBinary); + #[cfg(feature = "http-json")] + let is_http_protocol = + is_http_protocol || matches!(protocol, crate::Protocol::HttpJson); + if is_http_protocol { + return Err(ExporterBuildError::invalid_configuration( + "protocol", + "HTTP protocol is not compatible with gRPC transport; use `.with_http()` instead", + )); + } + } + + let compression = self.resolve_compression(signal_compression_var)?; + + let (headers_from_env, headers_for_logging) = parse_headers_from_env(signal_headers_var); + + let metadata = merge_metadata_with_headers_from_env( + self.tonic_config.metadata.unwrap_or_default(), + headers_from_env, + ); + + let add_metadata = move |mut req: tonic::Request<()>| { + for key_and_value in metadata.iter() { + match key_and_value { + KeyAndValueRef::Ascii(key, value) => { + req.metadata_mut().append(key, value.to_owned()) + } + KeyAndValueRef::Binary(key, value) => { + req.metadata_mut().append_bin(key, value.to_owned()) + } + }; + } + + Ok(req) + }; + + let interceptor = match self.tonic_config.interceptor { + Some(mut interceptor) => { + BoxInterceptor(Box::new(move |req| interceptor.call(add_metadata(req)?))) + } + None => BoxInterceptor(Box::new(add_metadata)), + }; + + // Get retry policy before consuming self + let retry_policy = self.tonic_config.retry_policy.clone(); + + // Resolve timeout early so it's available for both custom-channel and built-channel paths + let timeout = resolve_timeout(signal_timeout_var, self.exporter_config.timeout.as_ref()); + + // If a custom channel was provided, use that channel instead of creating one + if let Some(channel) = self.tonic_config.channel { + return Ok((channel, interceptor, compression, retry_policy, timeout)); + } + + let config = self.exporter_config; + + let (endpoint_str, endpoint_source) = + Self::resolve_endpoint(signal_endpoint_var, config.endpoint)?; + let endpoint_str = + apply_insecure_scheme(endpoint_str, super::resolve_insecure(signal_insecure_var)); + + // Used for logging the endpoint + let endpoint_clone = endpoint_str.clone(); + + let endpoint = tonic::transport::Endpoint::from_shared(endpoint_str).map_err(|error| { + ExporterBuildError::invalid_configuration( + endpoint_source, + format!( + "invalid endpoint '{endpoint_clone}': {}", + render_source_chain(&error) + ), + ) + })?; + + let is_https = endpoint + .uri() + .scheme() + .is_some_and(|s| *s == http::uri::Scheme::HTTPS); + + #[cfg(not(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + )))] + if is_https { + return Err(ExporterBuildError::invalid_configuration( + endpoint_source, + format!( + "endpoint '{}' uses HTTPS but no TLS feature is enabled; \ + enable one of the `tls-ring`, `tls-aws-lc`, or `tls-provider-agnostic` features on `opentelemetry-otlp`", + endpoint_clone + ), + )); + } + #[cfg(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + ))] + let channel = match self.tonic_config.tls_config { + Some(tls_config) => endpoint.tls_config(tls_config).map_err(|error| { + ExporterBuildError::invalid_configuration( + "tls_config", + render_source_chain(&error), + ) + })?, + None if is_https => endpoint + .tls_config(ClientTlsConfig::new()) + .map_err(|error| { + ExporterBuildError::invalid_configuration( + endpoint_source, + format!( + "failed to configure default TLS for HTTPS endpoint '{endpoint_clone}': {}; \ + ensure an appropriate TLS provider feature is enabled" + , + render_source_chain(&error) + ), + ) + })?, + None => endpoint, + } + .timeout(timeout) + .connect_lazy(); + + #[cfg(not(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + )))] + let channel = endpoint.timeout(timeout).connect_lazy(); + + otel_debug!(name: "TonicChannelBuilt", endpoint = endpoint_clone, timeout_in_millisecs = timeout.as_millis(), compression = format!("{:?}", compression), headers = format!("{:?}", headers_for_logging)); + Ok((channel, interceptor, compression, retry_policy, timeout)) + } + + fn resolve_endpoint( + signal_endpoint_var: &str, + provided_endpoint: Option, + ) -> Result<(String, &str), ExporterBuildError> { + // resolving endpoint string + // grpc doesn't have a "path" like http(See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md) + // the path of grpc calls are based on the protobuf service definition + // so we won't append one for default grpc endpoints + // If users for some reason want to use a custom path, they can use env var or builder to pass it + // + // programmatic configuration overrides any value set via environment variables + if let Some(endpoint) = provided_endpoint.filter(|s| !s.is_empty()) { + Ok((endpoint, "endpoint")) + } else if let Some(endpoint) = endpoint_from_env(signal_endpoint_var)? { + Ok((endpoint, signal_endpoint_var)) + } else if let Some(endpoint) = endpoint_from_env(OTEL_EXPORTER_OTLP_ENDPOINT)? { + Ok((endpoint, OTEL_EXPORTER_OTLP_ENDPOINT)) + } else { + Ok(( + OTEL_EXPORTER_OTLP_GRPC_ENDPOINT_DEFAULT.to_string(), + "endpoint", + )) + } + } + + fn resolve_compression( + &self, + env_override: &str, + ) -> Result, ExporterBuildError> { + super::resolve_compression_from_env( + self.tonic_config.compression, + env_override, + to_tonic_compression, + ) + } + + /// Build a new tonic log exporter + #[cfg(feature = "logs")] + pub(crate) fn build_log_exporter(self) -> Result { + use crate::exporter::tonic::logs::TonicLogsClient; + + otel_debug!(name: "LogsTonicChannelBuilding"); + + let (channel, interceptor, compression, retry_policy, timeout) = self.build_channel( + crate::logs::OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_HEADERS, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, + crate::logs::OTEL_EXPORTER_OTLP_LOGS_INSECURE, + )?; + + let client = TonicLogsClient::new(channel, interceptor, compression, retry_policy, timeout); + + Ok(crate::logs::LogExporter::from_tonic(client)) + } + + /// Build a new tonic metrics exporter + #[cfg(feature = "metrics")] + pub(crate) fn build_metrics_exporter( + self, + temporality: opentelemetry_sdk::metrics::Temporality, + ) -> Result { + use crate::MetricExporter; + use metrics::TonicMetricsClient; + + otel_debug!(name: "MetricsTonicChannelBuilding"); + + let (channel, interceptor, compression, retry_policy, timeout) = self.build_channel( + crate::metric::OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_HEADERS, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, + crate::metric::OTEL_EXPORTER_OTLP_METRICS_INSECURE, + )?; + + let client = + TonicMetricsClient::new(channel, interceptor, compression, retry_policy, timeout); + + Ok(MetricExporter::from_tonic(client, temporality)) + } + + /// Build a new tonic span exporter + #[cfg(feature = "trace")] + pub(crate) fn build_span_exporter(self) -> Result { + use crate::exporter::tonic::trace::TonicTracesClient; + + otel_debug!(name: "TracesTonicChannelBuilding"); + + let (channel, interceptor, compression, retry_policy, timeout) = self.build_channel( + crate::span::OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + crate::span::OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, + crate::span::OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + crate::span::OTEL_EXPORTER_OTLP_TRACES_HEADERS, + crate::span::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + crate::span::OTEL_EXPORTER_OTLP_TRACES_INSECURE, + )?; + + let client = + TonicTracesClient::new(channel, interceptor, compression, retry_policy, timeout); + + Ok(crate::SpanExporter::from_tonic(client)) + } +} + +/// Retries a tonic export operation with exponential backoff. +/// +/// Delays between retries adapt to the calling context: cooperative +/// `tokio::time::sleep` inside a Tokio runtime, or `std::thread::sleep` +/// on bare OS threads. +#[cfg(all( + feature = "grpc-tonic", + any(feature = "trace", feature = "metrics", feature = "logs") +))] +async fn tonic_retry_with_backoff( + policy: &RetryPolicy, + timeout: std::time::Duration, + classify_fn: fn(&tonic::Status) -> crate::retry::RetryErrorType, + operation_name: &'static str, + operation: F, +) -> Result +where + F: Fn() -> Fut, + Fut: Future>, +{ + retry_with_backoff(policy, timeout, classify_fn, operation_name, operation).await +} + +#[cfg(any(feature = "trace", feature = "metrics", feature = "logs"))] +/// Log and convert a `tonic::Status` from a failed export into an `OTelSdkError`. +/// +/// The gRPC code, message, and details are logged at DEBUG level only, since +/// the message may contain sensitive information such as authentication tokens +/// echoed back by the server. +/// +/// The returned `OTelSdkError` never contains the gRPC message, only the code. +/// When `status.source()` is `Some`, the status was locally generated by +/// tonic's transport stack (connect failure, invalid URL, DNS, etc.); its +/// source chain is library-deterministic text and is appended to the returned +/// error so misconfigurations surface at ERROR via the SDK processors. +/// We don't log at WARN here because SDK processors (BatchLogProcessor, +/// BatchSpanProcessor, PeriodicReader) already log the returned error via +/// `otel_error!`. +/// +/// `$client_name` must be a string literal so that `concat!` can produce +/// compile-time event names consistent with the codebase naming convention. +macro_rules! handle_tonic_export_error { + ($client_name:literal, $tonic_status:expr) => {{ + let status = &$tonic_status; + let code = status.code(); + otel_debug!( + name: concat!($client_name, ".ExportFailed"), + grpc_code = format!("{:?}", code), + grpc_message = status.message(), + grpc_details = format!("{:?}", status.details()) + ); + let mut err_msg = format!( + concat!($client_name, " export failed with gRPC code: {:?}"), + code + ); + if let Some(src) = std::error::Error::source(status) { + err_msg.push_str(": "); + err_msg.push_str(&$crate::exporter::tonic::render_source_chain(src)); + } + Err(opentelemetry_sdk::error::OTelSdkError::InternalFailure( + err_msg, + )) + }}; +} + +#[cfg(any(feature = "trace", feature = "metrics", feature = "logs"))] +/// Log and convert a `tonic::Status` from a failed interceptor into a new +/// `tonic::Status` suitable for retry classification. +/// +/// Interceptor errors are always treated as potentially sensitive since +/// interceptors are the primary mechanism for adding auth tokens. Only the +/// gRPC code is included in the returned status message; the original message +/// and details are logged at DEBUG level only. +macro_rules! handle_interceptor_error { + ($client_name:literal, $e:expr) => {{ + let status = &$e; + otel_debug!( + name: concat!($client_name, ".InterceptorFailed"), + grpc_code = format!("{:?}", status.code()), + grpc_message = status.message(), + grpc_details = format!("{:?}", status.details()) + ); + tonic::Status::internal(format!( + concat!( + $client_name, + " export failed in interceptor with gRPC code: {:?}" + ), + status.code() + )) + }}; +} + +// Make macros available to submodules (logs, trace, metrics). +#[cfg(any(feature = "trace", feature = "metrics", feature = "logs"))] +pub(crate) use handle_interceptor_error; +#[cfg(any(feature = "trace", feature = "metrics", feature = "logs"))] +pub(crate) use handle_tonic_export_error; + +/// Render an `std::error::Error` and its `source()` chain into a single +/// colon-separated string (e.g. `"transport error: invalid URL, scheme is missing"`). +pub(crate) fn render_source_chain(err: &(dyn std::error::Error + 'static)) -> String { + use std::fmt::Write; + let mut out = err.to_string(); + let mut next = err.source(); + while let Some(cur) = next { + let _ = write!(&mut out, ": {cur}"); + next = cur.source(); + } + out +} + +fn endpoint_from_env(variable: &str) -> Result, ExporterBuildError> { + super::read_env_var(variable) +} + +/// Apply the OTLP `INSECURE` rule to a (possibly schemeless) gRPC endpoint. +/// +/// Per the [OTLP exporter spec], `INSECURE` only governs endpoints that have no +/// explicit scheme. An endpoint that already carries a scheme — `http://`, +/// `https://`, `unix://`, etc. — is returned unchanged so its transport security +/// is decided by that scheme. A schemeless endpoint (e.g. +/// `collector.example.com:4317`) is prefixed with `http://` when `insecure` is +/// `true` and `https://` otherwise. +/// +/// An endpoint carries an explicit scheme only when `://` precedes any +/// path/query/fragment delimiter, so `unix:///tmp/x` is treated as schemed +/// while `collector:4317/p://q` is not. +/// +/// [OTLP exporter spec]: https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options +fn apply_insecure_scheme(endpoint: String, insecure: bool) -> String { + let has_scheme = endpoint + .split_once("://") + .is_some_and(|(scheme, _)| !scheme.contains(['/', '?', '#'])); + + if has_scheme { + endpoint + } else if insecure { + format!("http://{endpoint}") + } else { + format!("https://{endpoint}") + } +} + +fn merge_metadata_with_headers_from_env( + metadata: MetadataMap, + headers_from_env: HeaderMap, +) -> MetadataMap { + if headers_from_env.is_empty() { + metadata + } else { + let mut existing_headers: HeaderMap = metadata.into_headers(); + existing_headers.extend(headers_from_env); + + MetadataMap::from_headers(existing_headers) + } +} + +fn parse_headers_from_env(signal_headers_var: &str) -> (HeaderMap, Vec) { + let mut headers = Vec::new(); + + ( + env::var(signal_headers_var) + .or_else(|_| env::var(OTEL_EXPORTER_OTLP_HEADERS)) + .map(|input| { + parse_header_string(&input) + .filter_map(|(key, value)| { + headers.push(key.to_owned()); + Some(( + HeaderName::from_str(key).ok()?, + HeaderValue::from_str(&value).ok()?, + )) + }) + .collect::() + }) + .unwrap_or_default(), + headers, + ) +} + +/// Expose interface for modifying [TonicConfig] fields within the exporter builders. +pub(crate) trait HasTonicConfig { + /// Return a mutable reference to the export config within the exporter builders. + fn tonic_config(&mut self) -> &mut TonicConfig; +} + +/// Expose interface for modifying [TonicConfig] fields within the [TonicExporterBuilder]. +impl HasTonicConfig for TonicExporterBuilder { + fn tonic_config(&mut self) -> &mut TonicConfig { + &mut self.tonic_config + } +} + +/// Expose methods to override tonic-specific configuration. +/// +/// ## Examples +/// ``` +/// # #[cfg(all(feature = "trace", feature = "grpc-tonic"))] +/// # { +/// use opentelemetry_otlp::{WithExportConfig, WithTonicConfig}; +/// let exporter_builder = opentelemetry_otlp::SpanExporter::builder() +/// .with_tonic() +/// .with_compression(opentelemetry_otlp::Compression::Gzip); +/// # } +/// ``` +/// +/// This trait is sealed and cannot be implemented for types outside this crate. +pub trait WithTonicConfig: super::sealed::WithTonicConfig { + /// Set the TLS settings for the collector endpoint. + #[cfg(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + ))] + fn with_tls_config(self, tls_config: ClientTlsConfig) -> Self; + + /// Set custom metadata entries to send to the collector. + /// + /// **Note**: This method is additive - calling it multiple times will merge + /// the metadata entries. If the same key is provided in multiple calls, + /// the last value will override previous ones. + /// + /// # Example + /// ```no_run + /// # #[cfg(feature = "grpc-tonic")] + /// # { + /// use tonic::metadata::MetadataMap; + /// use opentelemetry_otlp::WithTonicConfig; + /// + /// let mut metadata1 = MetadataMap::new(); + /// metadata1.insert("key1", "value1".parse().unwrap()); + /// + /// let mut metadata2 = MetadataMap::new(); + /// metadata2.insert("key2", "value2".parse().unwrap()); + /// + /// let exporter = opentelemetry_otlp::SpanExporter::builder() + /// .with_tonic() + /// .with_metadata(metadata1) // Adds key1=value1 + /// .with_metadata(metadata2) // Adds key2=value2 (both are present) + /// .build()?; + /// # } + /// # Ok::<(), Box>(()) + /// ``` + fn with_metadata(self, metadata: MetadataMap) -> Self; + + /// Set the compression algorithm to use when communicating with the collector. + fn with_compression(self, compression: Compression) -> Self; + + /// Use `channel` as tonic's transport channel. + /// this will override tls config and should only be used + /// when working with non-HTTP transports. + /// + /// Users MUST make sure the timeout is + /// the same as the channel's timeout. + fn with_channel(self, channel: tonic::transport::Channel) -> Self; + + /// Use a custom `interceptor` to modify each outbound request. + /// This can be used to modify the gRPC metadata, for example + /// to inject auth tokens. + /// + /// **Note**: Calling this method multiple times will replace the previous + /// interceptor. If you need multiple interceptors, chain them together + /// before passing to this method. + /// + /// # Examples + /// + /// ## Single interceptor + /// ```no_run + /// # #[cfg(feature = "grpc-tonic")] + /// # { + /// use tonic::{Request, Status}; + /// use opentelemetry_otlp::WithTonicConfig; + /// + /// fn auth_interceptor(mut req: Request<()>) -> Result, Status> { + /// req.metadata_mut().insert("authorization", "Bearer token".parse().unwrap()); + /// Ok(req) + /// } + /// + /// let exporter = opentelemetry_otlp::SpanExporter::builder() + /// .with_tonic() + /// .with_interceptor(auth_interceptor) + /// .build()?; + /// # } + /// # Ok::<(), Box>(()) + /// ``` + /// + /// ## Multiple interceptors (chaining) + /// ```no_run + /// # #[cfg(feature = "grpc-tonic")] + /// # { + /// use tonic::{Request, Status}; + /// use opentelemetry_otlp::WithTonicConfig; + /// + /// fn auth_interceptor(mut req: Request<()>) -> Result, Status> { + /// req.metadata_mut().insert("authorization", "Bearer token".parse().unwrap()); + /// Ok(req) + /// } + /// + /// fn logging_interceptor(req: Request<()>) -> Result, Status> { + /// println!("Sending gRPC request with metadata: {:?}", req.metadata()); + /// Ok(req) + /// } + /// + /// // Chain interceptors by wrapping them + /// fn combined_interceptor(req: Request<()>) -> Result, Status> { + /// let req = logging_interceptor(req)?; + /// auth_interceptor(req) + /// } + /// + /// let exporter = opentelemetry_otlp::SpanExporter::builder() + /// .with_tonic() + /// .with_interceptor(combined_interceptor) + /// .build()?; + /// # } + /// # Ok::<(), Box>(()) + /// ``` + fn with_interceptor(self, interceptor: I) -> Self + where + I: tonic::service::Interceptor + Clone + Send + Sync + 'static; + + /// Set the retry policy for gRPC requests. + fn with_retry_policy(self, policy: RetryPolicy) -> Self; +} + +impl super::sealed::WithTonicConfig for B {} + +impl WithTonicConfig for B { + #[cfg(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + ))] + fn with_tls_config(mut self, tls_config: ClientTlsConfig) -> Self { + self.tonic_config().tls_config = Some(tls_config); + self + } + + /// Set custom metadata entries to send to the collector. + fn with_metadata(mut self, metadata: MetadataMap) -> Self { + // extending metadata maps is harder than just casting back/forth + let mut existing_headers = self + .tonic_config() + .metadata + .clone() + .unwrap_or_default() + .into_headers(); + existing_headers.extend(metadata.into_headers()); + + self.tonic_config().metadata = Some(MetadataMap::from_headers(existing_headers)); + self + } + + fn with_compression(mut self, compression: Compression) -> Self { + self.tonic_config().compression = Some(compression); + self + } + + fn with_channel(mut self, channel: tonic::transport::Channel) -> Self { + self.tonic_config().channel = Some(channel); + self + } + + fn with_interceptor(mut self, interceptor: I) -> Self + where + I: tonic::service::Interceptor + Clone + Send + Sync + 'static, + { + self.tonic_config().interceptor = Some(BoxInterceptor(Box::new(interceptor))); + self + } + + fn with_retry_policy(mut self, policy: RetryPolicy) -> Self { + self.tonic_config().retry_policy = Some(policy); + self + } +} + +#[cfg(test)] +mod tests { + use crate::exporter::tests::run_env_test; + use crate::exporter::tonic::WithTonicConfig; + #[cfg(feature = "grpc-tonic")] + use crate::exporter::Compression; + use crate::OTEL_EXPORTER_OTLP_TRACES_ENDPOINT; + + use super::TonicExporterBuilder; + use crate::{OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TRACES_HEADERS}; + use http::{HeaderMap, HeaderName, HeaderValue}; + use tonic::metadata::{MetadataMap, MetadataValue}; + + // Scheme-resolution unit tests. These are intentionally free of env vars, a + // tokio runtime, and TLS features so they run under every feature + // combination (including CI's `--all-features`), unlike the TLS-gated + // integration tests below which compile out when a TLS feature is enabled. + #[test] + fn apply_insecure_scheme_defaults_to_https() { + assert_eq!( + super::apply_insecure_scheme("collector.example.com:4317".to_string(), false), + "https://collector.example.com:4317" + ); + } + + #[test] + fn apply_insecure_scheme_uses_http_when_insecure() { + assert_eq!( + super::apply_insecure_scheme("collector.example.com:4317".to_string(), true), + "http://collector.example.com:4317" + ); + } + + #[test] + fn apply_insecure_scheme_preserves_explicit_scheme() { + // An explicit scheme always wins over INSECURE, regardless of its value. + // The `unix://` case is the regression guard for an endpoint with a + // non-http(s) scheme being rewritten to `https://unix://...`. + for endpoint in [ + "http://collector.example.com:4317", + "https://collector.example.com:4317", + "unix:///tmp/test", + ] { + assert_eq!( + super::apply_insecure_scheme(endpoint.to_string(), true), + endpoint + ); + assert_eq!( + super::apply_insecure_scheme(endpoint.to_string(), false), + endpoint + ); + } + } + + #[test] + fn apply_insecure_scheme_ignores_delimited_scheme_separator() { + // A `://` that appears after a path/query/fragment delimiter is not a + // scheme, so the (schemeless) endpoint must still be prefixed rather than + // passed through and left to fail URI parsing. + assert_eq!( + super::apply_insecure_scheme("collector:4317/path://foo".to_string(), false), + "https://collector:4317/path://foo" + ); + assert_eq!( + super::apply_insecure_scheme("collector:4317/path://foo".to_string(), true), + "http://collector:4317/path://foo" + ); + } + + #[test] + fn test_with_metadata() { + // metadata should merge with the current one with priority instead of just replacing it + let mut metadata = MetadataMap::new(); + metadata.insert("foo", "bar".parse().unwrap()); + let builder = TonicExporterBuilder::default().with_metadata(metadata); + let result = builder.tonic_config.metadata.unwrap(); + let foo = result + .get("foo") + .expect("there to always be an entry for foo"); + assert_eq!(foo, &MetadataValue::try_from("bar").unwrap()); + assert!(result.get("User-Agent").is_some()); + + // metadata should override entries with the same key in the default one + let mut metadata = MetadataMap::new(); + metadata.insert("user-agent", "baz".parse().unwrap()); + let builder = TonicExporterBuilder::default().with_metadata(metadata); + let result = builder.tonic_config.metadata.unwrap(); + assert_eq!( + result.get("User-Agent").unwrap(), + &MetadataValue::try_from("baz").unwrap() + ); + assert_eq!( + result.len(), + TonicExporterBuilder::default() + .tonic_config + .metadata + .unwrap() + .len() + ); + } + + #[test] + #[cfg(feature = "gzip-tonic")] + fn test_with_gzip_compression() { + let builder = TonicExporterBuilder::default().with_compression(Compression::Gzip); + assert_eq!(builder.tonic_config.compression.unwrap(), Compression::Gzip); + } + + #[test] + #[cfg(feature = "zstd-tonic")] + fn test_with_zstd_compression() { + let builder = TonicExporterBuilder::default().with_compression(Compression::Zstd); + assert_eq!(builder.tonic_config.compression.unwrap(), Compression::Zstd); + } + + #[test] + fn test_convert_compression() { + #[cfg(feature = "gzip-tonic")] + assert!(super::to_tonic_compression(Compression::Gzip).is_ok()); + #[cfg(not(feature = "gzip-tonic"))] + assert!(super::to_tonic_compression(Compression::Gzip).is_err()); + #[cfg(feature = "zstd-tonic")] + assert!(super::to_tonic_compression(Compression::Zstd).is_ok()); + #[cfg(not(feature = "zstd-tonic"))] + assert!(super::to_tonic_compression(Compression::Zstd).is_err()); + } + + #[cfg(feature = "zstd-tonic")] + #[test] + fn test_priority_of_signal_env_over_generic_env_for_compression() { + run_env_test( + vec![ + (crate::OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, "zstd"), + (crate::OTEL_EXPORTER_OTLP_COMPRESSION, "gzip"), + ], + || { + let builder = TonicExporterBuilder::default(); + + let compression = builder + .resolve_compression(crate::OTEL_EXPORTER_OTLP_TRACES_COMPRESSION) + .unwrap(); + assert_eq!(compression, Some(tonic::codec::CompressionEncoding::Zstd)); + }, + ); + } + + #[cfg(feature = "zstd-tonic")] + #[test] + fn test_priority_of_code_based_config_over_envs_for_compression() { + run_env_test( + vec![ + (crate::OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, "gzip"), + (crate::OTEL_EXPORTER_OTLP_COMPRESSION, "gzip"), + ], + || { + let builder = TonicExporterBuilder::default().with_compression(Compression::Zstd); + + let compression = builder + .resolve_compression(crate::OTEL_EXPORTER_OTLP_TRACES_COMPRESSION) + .unwrap(); + assert_eq!(compression, Some(tonic::codec::CompressionEncoding::Zstd)); + }, + ); + } + + #[test] + fn test_use_default_when_others_missing_for_compression() { + run_env_test(vec![], || { + let builder = TonicExporterBuilder::default(); + + let compression = builder + .resolve_compression(crate::OTEL_EXPORTER_OTLP_TRACES_COMPRESSION) + .unwrap(); + assert!(compression.is_none()); + }); + } + + #[test] + fn test_parse_headers_from_env() { + run_env_test( + vec![ + (OTEL_EXPORTER_OTLP_TRACES_HEADERS, "k1=v1,k2=v2"), + (OTEL_EXPORTER_OTLP_HEADERS, "k3=v3"), + ], + || { + assert_eq!( + super::parse_headers_from_env(OTEL_EXPORTER_OTLP_TRACES_HEADERS).0, + HeaderMap::from_iter([ + ( + HeaderName::from_static("k1"), + HeaderValue::from_static("v1") + ), + ( + HeaderName::from_static("k2"), + HeaderValue::from_static("v2") + ), + ]) + ); + + assert_eq!( + super::parse_headers_from_env("EMPTY_ENV").0, + HeaderMap::from_iter([( + HeaderName::from_static("k3"), + HeaderValue::from_static("v3") + )]) + ); + }, + ) + } + + #[test] + fn test_merge_metadata_with_headers_from_env() { + run_env_test( + vec![(OTEL_EXPORTER_OTLP_TRACES_HEADERS, "k1=v1,k2=v2")], + || { + let headers_from_env = + super::parse_headers_from_env(OTEL_EXPORTER_OTLP_TRACES_HEADERS); + + let mut metadata = MetadataMap::new(); + metadata.insert("foo", "bar".parse().unwrap()); + metadata.insert("k1", "v0".parse().unwrap()); + + let result = + super::merge_metadata_with_headers_from_env(metadata, headers_from_env.0); + + assert_eq!( + result.get("foo").unwrap(), + MetadataValue::from_static("bar") + ); + assert_eq!(result.get("k1").unwrap(), MetadataValue::from_static("v1")); + assert_eq!(result.get("k2").unwrap(), MetadataValue::from_static("v2")); + }, + ); + } + + #[test] + fn test_priority_of_signal_env_over_generic_env_for_endpoint() { + run_env_test( + vec![ + (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "http://localhost:1234"), + (super::OTEL_EXPORTER_OTLP_ENDPOINT, "http://localhost:2345"), + ], + || { + let url = TonicExporterBuilder::resolve_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + None, + ) + .unwrap(); + assert_eq!( + url, + ( + "http://localhost:1234".to_string(), + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT + ) + ); + }, + ); + } + + #[test] + fn test_priority_of_code_based_config_over_envs_for_endpoint() { + run_env_test( + vec![ + (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "http://localhost:1234"), + (super::OTEL_EXPORTER_OTLP_ENDPOINT, "http://localhost:2345"), + ], + || { + let url = TonicExporterBuilder::resolve_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + Some("http://localhost:3456".to_string()), + ) + .unwrap(); + assert_eq!(url, ("http://localhost:3456".to_string(), "endpoint")); + }, + ); + } + + #[test] + fn test_use_default_when_others_missing_for_endpoint() { + run_env_test(vec![], || { + let url = + TonicExporterBuilder::resolve_endpoint(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, None) + .unwrap(); + assert_eq!(url, ("http://localhost:4317".to_string(), "endpoint")); + }); + } + + #[test] + fn test_use_default_when_empty_string_for_option() { + run_env_test(vec![], || { + let url = TonicExporterBuilder::resolve_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + Some(String::new()), + ) + .unwrap(); + assert_eq!(url, ("http://localhost:4317".to_string(), "endpoint")); + }); + } + + #[test] + fn test_empty_endpoint_envs_are_treated_as_unset() { + run_env_test( + vec![ + (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, ""), + (super::OTEL_EXPORTER_OTLP_ENDPOINT, ""), + ], + || { + let url = TonicExporterBuilder::resolve_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + None, + ) + .unwrap(); + assert_eq!(url, ("http://localhost:4317".to_string(), "endpoint")); + }, + ); + } + + #[test] + fn test_empty_signal_env_falls_through_to_generic_env() { + run_env_test( + vec![ + (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, ""), + (super::OTEL_EXPORTER_OTLP_ENDPOINT, "http://collector:4317"), + ], + || { + let url = TonicExporterBuilder::resolve_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + None, + ) + .unwrap(); + assert_eq!( + url, + ( + "http://collector:4317".to_string(), + super::OTEL_EXPORTER_OTLP_ENDPOINT + ) + ); + }, + ); + } + + #[cfg(unix)] + #[test] + fn test_non_unicode_endpoint_env_returns_error() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + temp_env::with_var( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + Some(OsStr::from_bytes(b"http://example.com/\x80")), + || { + let result = TonicExporterBuilder::resolve_endpoint( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + None, + ); + assert!(matches!( + result, + Err(crate::exporter::ExporterBuildError::InvalidConfiguration(message)) + if message.contains(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) + && message.contains("not valid Unicode") + )); + }, + ); + } + + #[cfg(feature = "trace")] + #[test] + fn invalid_endpoint_error_identifies_environment_source() { + use crate::{ExporterBuildError, Protocol, SpanExporter, WithExportConfig}; + + for (signal_endpoint, expected_source) in [ + (Some("http://[invalid"), OTEL_EXPORTER_OTLP_TRACES_ENDPOINT), + (None, super::OTEL_EXPORTER_OTLP_ENDPOINT), + ] { + temp_env::with_vars( + [ + (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, signal_endpoint), + (super::OTEL_EXPORTER_OTLP_ENDPOINT, Some("http://[invalid")), + ], + || { + let error = SpanExporter::builder() + .with_tonic() + .with_protocol(Protocol::Grpc) + .build() + .unwrap_err(); + assert!( + matches!(error, ExporterBuildError::InvalidConfiguration(ref message) + if message.starts_with(&format!("{expected_source}:")) + && message.contains("invalid endpoint")), + "expected endpoint source {expected_source}, got {error}" + ); + }, + ); + } + } + + #[cfg(all( + feature = "trace", + not(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + )) + ))] + #[test] + fn missing_tls_error_identifies_environment_source() { + use crate::{ExporterBuildError, Protocol, SpanExporter, WithExportConfig}; + + for (signal_endpoint, expected_source) in [ + ( + Some("https://collector.example.com:4317"), + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + ), + (None, super::OTEL_EXPORTER_OTLP_ENDPOINT), + ] { + temp_env::with_vars( + [ + (OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, signal_endpoint), + ( + super::OTEL_EXPORTER_OTLP_ENDPOINT, + Some("https://collector.example.com:4317"), + ), + ], + || { + let error = SpanExporter::builder() + .with_tonic() + .with_protocol(Protocol::Grpc) + .build() + .unwrap_err(); + assert!( + matches!(error, ExporterBuildError::InvalidConfiguration(ref message) + if message.starts_with(&format!("{expected_source}:")) + && message.contains("no TLS feature")), + "expected endpoint source {expected_source}, got {error}" + ); + }, + ); + } + } + + #[test] + fn test_with_retry_policy() { + use crate::RetryPolicy; + use crate::WithTonicConfig; + + let custom_policy = RetryPolicy::default() + .with_max_retries(5) + .with_initial_delay(std::time::Duration::from_millis(200)) + .with_max_delay(std::time::Duration::from_millis(3200)) + .with_max_jitter(std::time::Duration::from_millis(50)); + + let builder = TonicExporterBuilder::default().with_retry_policy(custom_policy); + + // Verify the retry policy was set + let retry_policy = builder.tonic_config.retry_policy.as_ref().unwrap(); + assert_eq!(retry_policy.max_retries, 5); + assert_eq!( + retry_policy.initial_delay, + std::time::Duration::from_millis(200) + ); + assert_eq!( + retry_policy.max_delay, + std::time::Duration::from_millis(3200) + ); + assert_eq!( + retry_policy.max_jitter, + std::time::Duration::from_millis(50) + ); + } + + #[test] + fn test_default_retry_policy_when_none_configured() { + // This test requires us to create a tonic client, but we can't easily do that without + // a channel in a unit test. The default behavior is tested implicitly in integration tests. + let builder = TonicExporterBuilder::default(); + assert!(builder.tonic_config.retry_policy.is_none()); + } + + #[tokio::test] + #[cfg(all( + feature = "trace", + not(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + )) + ))] + async fn test_schemeless_endpoint_insecure_false_errors_without_tls() { + use crate::exporter::tests::run_env_test; + use crate::exporter::ExporterBuildError; + use crate::SpanExporter; + use crate::WithExportConfig; + + // INSECURE=false (default) + schemeless endpoint → https:// prepended → error (no TLS) + // Unset signal-specific var to ensure generic takes effect + temp_env::with_var_unset(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, || { + run_env_test(vec![(crate::OTEL_EXPORTER_OTLP_INSECURE, "false")], || { + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("collector.example.com:4317") + .build(); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, ExporterBuildError::InvalidConfiguration(_)), + "expected InvalidConfiguration error for schemeless+secure without TLS, got: {err:?}" + ); + let message = err.to_string(); + assert!( + message.contains("collector.example.com:4317") + && message.contains("HTTPS") + && message.contains("TLS"), + "error should identify the endpoint and TLS requirement, got: {message}" + ); + }); + }); + } + + #[tokio::test] + #[cfg(all( + feature = "trace", + not(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + )) + ))] + async fn test_schemeless_endpoint_insecure_true_succeeds_without_tls() { + use crate::exporter::tests::run_env_test; + use crate::SpanExporter; + use crate::WithExportConfig; + + // INSECURE=true + schemeless endpoint → http:// prepended → no TLS needed + // Unset signal-specific var to ensure generic takes effect + temp_env::with_var_unset(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, || { + run_env_test(vec![(crate::OTEL_EXPORTER_OTLP_INSECURE, "true")], || { + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("collector.example.com:4317") + .build(); + + assert!( + result.is_ok(), + "schemeless + INSECURE=true should succeed without TLS, got: {:?}", + result.unwrap_err() + ); + }); + }); + } + + #[tokio::test] + #[cfg(all( + feature = "trace", + not(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + )) + ))] + async fn test_schemeless_endpoint_defaults_to_https() { + use crate::exporter::ExporterBuildError; + use crate::SpanExporter; + use crate::WithExportConfig; + + // No INSECURE env var set → defaults to secure (https://) → error without TLS + // This verifies the behavioral change: schemeless endpoints now get https:// prepended + temp_env::with_vars_unset( + [ + crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, + crate::OTEL_EXPORTER_OTLP_INSECURE, + ], + || { + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("collector.example.com:4317") + .build(); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, ExporterBuildError::InvalidConfiguration(_)), + "schemeless endpoint should default to https:// and fail without TLS, got: {err:?}" + ); + let message = err.to_string(); + assert!( + message.contains("collector.example.com:4317") + && message.contains("HTTPS") + && message.contains("TLS"), + "error should identify the endpoint and TLS requirement, got: {message}" + ); + }, + ); + } + + #[tokio::test] + #[cfg(feature = "trace")] + async fn test_explicit_http_scheme_ignores_insecure_env() { + use crate::exporter::tests::run_env_test; + use crate::SpanExporter; + use crate::WithExportConfig; + + // Explicit http:// scheme should succeed regardless of INSECURE value + // Unset signal-specific var to ensure generic takes effect + temp_env::with_var_unset(crate::OTEL_EXPORTER_OTLP_TRACES_INSECURE, || { + run_env_test(vec![(crate::OTEL_EXPORTER_OTLP_INSECURE, "false")], || { + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("http://collector.example.com:4317") + .build(); + + assert!( + result.is_ok(), + "explicit http:// should succeed even with INSECURE=false, got: {:?}", + result.unwrap_err() + ); + }); + }); + } + + #[test] + #[cfg(not(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + )))] + fn test_https_endpoint_errors_without_tls_feature() { + use crate::exporter::ExporterBuildError; + use crate::SpanExporter; + use crate::WithExportConfig; + + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("https://example.com") + .build(); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, ExporterBuildError::InvalidConfiguration(_)), + "expected InvalidConfiguration error, got: {err:?}" + ); + let msg = err.to_string(); + assert!( + msg.contains("HTTPS") && msg.contains("TLS"), + "error message should mention HTTPS and TLS, got: {msg}" + ); + } + + #[tokio::test] + #[cfg(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + ))] + async fn test_https_endpoint_succeeds_with_tls_feature() { + use crate::SpanExporter; + use crate::WithExportConfig; + + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("https://example.com") + .build(); + + assert!( + result.is_ok(), + "https endpoint should succeed when TLS feature is enabled, got: {:?}", + result.unwrap_err() + ); + } + + #[tokio::test] + async fn test_http_endpoint_succeeds_without_tls_feature() { + use crate::SpanExporter; + use crate::WithExportConfig; + + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("http://localhost:4317") + .build(); + + assert!( + result.is_ok(), + "http endpoint should always succeed, got: {:?}", + result.unwrap_err() + ); + } + + #[test] + #[cfg(not(feature = "gzip-tonic"))] + fn test_gzip_compression_errors_without_feature() { + use crate::exporter::ExporterBuildError; + use crate::SpanExporter; + use crate::WithTonicConfig; + + let result = SpanExporter::builder() + .with_tonic() + .with_compression(crate::Compression::Gzip) + .build(); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, ExporterBuildError::InvalidConfiguration(_)), + "expected InvalidConfiguration error, got: {err:?}" + ); + let msg = err.to_string(); + assert!( + msg.contains("gzip-tonic"), + "error message should mention 'gzip-tonic' feature, got: {msg}" + ); + } + + #[tokio::test] + #[cfg(feature = "gzip-tonic")] + async fn test_gzip_compression_succeeds_with_feature() { + use crate::SpanExporter; + use crate::WithTonicConfig; + + let result = SpanExporter::builder() + .with_tonic() + .with_compression(crate::Compression::Gzip) + .build(); + + assert!( + result.is_ok(), + "gzip compression should succeed when gzip-tonic feature is enabled, got: {:?}", + result.unwrap_err() + ); + } + + #[test] + #[cfg(not(feature = "zstd-tonic"))] + fn test_zstd_compression_errors_without_feature() { + use crate::exporter::ExporterBuildError; + use crate::SpanExporter; + use crate::WithTonicConfig; + + let result = SpanExporter::builder() + .with_tonic() + .with_compression(crate::Compression::Zstd) + .build(); + + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, ExporterBuildError::InvalidConfiguration(_)), + "expected InvalidConfiguration error, got: {err:?}" + ); + let msg = err.to_string(); + assert!( + msg.contains("zstd-tonic"), + "error message should mention 'zstd-tonic' feature, got: {msg}" + ); + } + + #[tokio::test] + #[cfg(feature = "zstd-tonic")] + async fn test_zstd_compression_succeeds_with_feature() { + use crate::SpanExporter; + use crate::WithTonicConfig; + + let result = SpanExporter::builder() + .with_tonic() + .with_compression(crate::Compression::Zstd) + .build(); + + assert!( + result.is_ok(), + "zstd compression should succeed when zstd-tonic feature is enabled, got: {:?}", + result.unwrap_err() + ); + } + + #[tokio::test] + async fn test_unix_socket_endpoint_succeeds() { + use crate::SpanExporter; + use crate::WithExportConfig; + + let result = SpanExporter::builder() + .with_tonic() + .with_endpoint("unix:///tmp/test") + .build(); + + assert!( + result.is_ok(), + "unix socket endpoint should succeed, got: {:?}", + result.unwrap_err() + ); + } + + #[cfg(any(feature = "trace", feature = "metrics", feature = "logs"))] + mod error_handling_tests { + use opentelemetry::otel_debug; + use opentelemetry_sdk::error::OTelSdkError; + + #[test] + fn export_error_includes_grpc_code_but_not_sensitive_message() { + let status = tonic::Status::unauthenticated("Bearer secret-token-123"); + let result: Result<(), OTelSdkError> = + super::super::handle_tonic_export_error!("TestExporter", status); + let msg = format!("{}", result.unwrap_err()); + + assert!( + msg.contains("Unauthenticated"), + "Error should contain the gRPC code, got: {msg}" + ); + assert!( + !msg.contains("secret-token-123"), + "Error must not contain the sensitive token, got: {msg}" + ); + } + + #[test] + fn export_error_includes_exporter_name() { + let status = tonic::Status::unavailable("connection refused"); + let result: Result<(), OTelSdkError> = + super::super::handle_tonic_export_error!("TonicLogsClient", status); + let msg = format!("{}", result.unwrap_err()); + + assert!( + msg.contains("TonicLogsClient"), + "Error should identify the exporter, got: {msg}" + ); + } + + #[test] + fn export_error_never_includes_grpc_message() { + // Neither connection nor sensitive codes should leak the message + // into the returned error (messages are only logged, not returned) + let statuses = [ + tonic::Status::unavailable("safe connection info"), + tonic::Status::unknown("safe connection info"), + tonic::Status::deadline_exceeded("safe connection info"), + tonic::Status::resource_exhausted("safe connection info"), + tonic::Status::aborted("safe connection info"), + tonic::Status::cancelled("safe connection info"), + tonic::Status::unauthenticated("Bearer my-secret-token"), + tonic::Status::permission_denied("Bearer my-secret-token"), + tonic::Status::internal("Bearer my-secret-token"), + ]; + for status in &statuses { + let result: Result<(), OTelSdkError> = + super::super::handle_tonic_export_error!("TestExporter", status); + let msg = format!("{}", result.unwrap_err()); + assert!( + msg.contains("TestExporter export failed with gRPC code"), + "Expected structured error message, got: {msg}" + ); + assert!( + !msg.contains("safe connection info") && !msg.contains("my-secret-token"), + "Error message should not include the gRPC message, got: {msg}" + ); + } + } + + #[test] + fn interceptor_error_returns_internal_status_without_sensitive_data() { + let original = tonic::Status::unauthenticated("Bearer secret"); + let result = super::super::handle_interceptor_error!("TestExporter", original); + + assert_eq!(result.code(), tonic::Code::Internal); + assert!( + result.message().contains("Unauthenticated"), + "Interceptor error should contain original gRPC code, got: {}", + result.message() + ); + assert!( + !result.message().contains("secret"), + "Interceptor error must not leak sensitive data, got: {}", + result.message() + ); + } + + #[test] + fn interceptor_error_includes_exporter_name() { + let original = tonic::Status::internal("some error"); + let result = super::super::handle_interceptor_error!("TonicTracesClient", original); + + assert!( + result.message().contains("TonicTracesClient"), + "Interceptor error should identify the exporter, got: {}", + result.message() + ); + } + + #[test] + fn export_error_surfaces_transport_source_chain() { + // `Status::from_error` is how tonic wraps local transport failures + // (connect errors, URL parse errors, DNS). These sources are + // library-deterministic text and safe to surface at ERROR level. + let transport_err = std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "invalid URL, scheme is missing", + ); + let status = tonic::Status::from_error(Box::new(transport_err)); + let result: Result<(), OTelSdkError> = + super::super::handle_tonic_export_error!("TonicTracesClient", status); + let msg = format!("{}", result.unwrap_err()); + + assert!( + msg.contains("TonicTracesClient export failed with gRPC code"), + "Expected structured error message, got: {msg}" + ); + assert!( + msg.contains("invalid URL, scheme is missing"), + "Pre-flight transport error text should be surfaced, got: {msg}" + ); + } + + #[test] + fn server_returned_status_does_not_surface_message_even_when_similar_to_transport() { + // Server-returned statuses have no `source()`. Even if the server's + // message resembles a transport error, it must not leak. + let status = tonic::Status::unknown("invalid URL, scheme is missing"); + let result: Result<(), OTelSdkError> = + super::super::handle_tonic_export_error!("TestExporter", status); + let msg = format!("{}", result.unwrap_err()); + + assert!( + !msg.contains("invalid URL, scheme is missing"), + "Server-returned status message must not leak, got: {msg}" + ); + } + + #[test] + fn render_source_chain_joins_nested_sources() { + #[derive(Debug)] + struct Wrap { + msg: &'static str, + src: Box, + } + impl std::fmt::Display for Wrap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.msg) + } + } + impl std::error::Error for Wrap { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(self.src.as_ref()) + } + } + + let inner = std::io::Error::other("invalid URL"); + let middle = Wrap { + msg: "connect error", + src: Box::new(inner), + }; + let outer = Wrap { + msg: "transport error", + src: Box::new(middle), + }; + + let rendered = super::super::render_source_chain(&outer); + assert_eq!(rendered, "transport error: connect error: invalid URL"); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/trace.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/trace.rs new file mode 100644 index 00000000..35f1ccaf --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/exporter/tonic/trace.rs @@ -0,0 +1,155 @@ +use core::fmt; +use std::sync::{Arc, Mutex}; + +use opentelemetry::{otel_debug, otel_warn}; +use opentelemetry_proto::tonic::collector::trace::v1::{ + trace_service_client::TraceServiceClient, ExportTraceServiceRequest, +}; +use opentelemetry_proto::transform::trace::tonic::group_spans_by_resource_and_scope; +use opentelemetry_sdk::error::OTelSdkError; +use opentelemetry_sdk::{ + error::OTelSdkResult, + trace::{SpanData, SpanExporter}, +}; +use tonic::{codegen::CompressionEncoding, service::Interceptor, transport::Channel, Request}; + +use super::BoxInterceptor; + +use crate::RetryPolicy; + +pub(crate) struct TonicTracesClient { + inner: Mutex>, + retry_policy: RetryPolicy, + timeout: std::time::Duration, + #[allow(dead_code)] + // would be removed once we support set_resource for metrics. + resource: opentelemetry_proto::transform::common::tonic::ResourceAttributesWithSchema, +} + +struct ClientInner { + client: TraceServiceClient, + interceptor: BoxInterceptor, +} + +impl fmt::Debug for TonicTracesClient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("TonicTracesClient") + } +} + +impl TonicTracesClient { + pub(super) fn new( + channel: Channel, + interceptor: BoxInterceptor, + compression: Option, + retry_policy: Option, + timeout: std::time::Duration, + ) -> Self { + let mut client = TraceServiceClient::new(channel); + if let Some(compression) = compression { + client = client + .send_compressed(compression) + .accept_compressed(compression); + } + + otel_debug!(name: "TonicsTracesClientBuilt"); + + TonicTracesClient { + inner: Mutex::new(Some(ClientInner { + client, + interceptor, + })), + retry_policy: retry_policy.unwrap_or_default(), + timeout, + resource: Default::default(), + } + } +} + +impl SpanExporter for TonicTracesClient { + async fn export(&self, batch: Vec) -> OTelSdkResult { + let batch = Arc::new(batch); + + match super::tonic_retry_with_backoff( + &self.retry_policy, + self.timeout, + crate::retry_classification::grpc::classify_tonic_status, + "TonicTracesClient.Export", + || async { + let batch_clone = Arc::clone(&batch); + + // Execute the export operation + let (mut client, metadata, extensions) = self + .inner + .lock() + .map_err(|e| tonic::Status::internal(format!("failed to acquire lock: {e}"))) + .and_then(|mut inner| match &mut *inner { + Some(inner) => { + let (m, e, _) = inner + .interceptor + .call(Request::new(())) + .map_err(|e| { + super::handle_interceptor_error!("TonicTracesClient", e) + })? + .into_parts(); + Ok((inner.client.clone(), m, e)) + } + None => Err(tonic::Status::failed_precondition( + "exporter already shutdown", + )), + })?; + + let resource_spans = + group_spans_by_resource_and_scope((*batch_clone).clone(), &self.resource); + + otel_debug!(name: "TonicTracesClient.ExportStarted"); + + client + .export(Request::from_parts( + metadata, + extensions, + ExportTraceServiceRequest { resource_spans }, + )) + .await + .map(|response| { + otel_debug!(name: "TonicTracesClient.ExportSucceeded"); + + // Handle partial success. As per spec, we log and _do not_ retry. + if let Some(partial_success) = response.into_inner().partial_success { + if partial_success.rejected_spans > 0 + || !partial_success.error_message.is_empty() + { + otel_warn!( + name: "TonicTracesClient.PartialSuccess", + rejected_spans = partial_success.rejected_spans, + error_message = partial_success.error_message.as_str(), + ); + } + } + }) + }, + ) + .await + { + Ok(_) => Ok(()), + Err(tonic_status) => { + super::handle_tonic_export_error!("TonicTracesClient", tonic_status) + } + } + } + + fn shutdown(&self) -> OTelSdkResult { + let mut inner_guard = self + .inner + .lock() + .map_err(|e| OTelSdkError::InternalFailure(format!("Failed to acquire lock: {e}")))?; + match inner_guard.take() { + Some(_) => Ok(()), // Successfully took `inner`, indicating a successful shutdown. + None => Err(OTelSdkError::AlreadyShutdown), // `inner` was already `None`, meaning it's already shut down. + } + } + + fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) { + self.resource = resource.into(); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/lib.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/lib.rs new file mode 100644 index 00000000..c3fcd858 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/lib.rs @@ -0,0 +1,884 @@ +//! # OpenTelemetry OTLP Exporter +//! +//! The OTLP Exporter enables exporting telemetry data (logs, metrics, and traces) in the +//! OpenTelemetry Protocol (OTLP) format to compatible backends. These backends include: +//! +//! - OpenTelemetry Collector +//! - Open-source observability tools (Prometheus, Jaeger, etc.) +//! - Vendor-specific monitoring platforms +//! +//! This crate supports sending OTLP data via: +//! - gRPC +//! - HTTP (binary protobuf or JSON) +//! +//! ## Quickstart with OpenTelemetry Collector +//! +//! The examples below show traces, but the same pattern applies to metrics +//! ([`MetricExporter`]) and logs ([`LogExporter`]) — just swap the exporter +//! builder and the corresponding SDK provider. +//! +//! ### HTTP Transport (Port 4318) +//! +//! Run the OpenTelemetry Collector: +//! +//! ```shell +//! $ docker run -p 4318:4318 otel/opentelemetry-collector:latest +//! ``` +//! +//! Configure your application to export traces via HTTP: +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "http-proto"))] +//! # { +//! use opentelemetry::global; +//! use opentelemetry::trace::Tracer; +//! use opentelemetry_otlp::Protocol; +//! use opentelemetry_otlp::WithExportConfig; +//! +//! fn main() -> Result<(), Box> { +//! // Initialize OTLP exporter using HTTP binary protocol +//! let otlp_exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_http() +//! .with_protocol(Protocol::HttpBinary) +//! .build()?; +//! +//! // Create a tracer provider with the exporter +//! let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() +//! .with_batch_exporter(otlp_exporter) +//! .build(); +//! +//! // Set it as the global provider +//! global::set_tracer_provider(tracer_provider); +//! +//! // Get a tracer and create spans +//! let tracer = global::tracer("my_tracer"); +//! tracer.in_span("doing_work", |_cx| { +//! // Your application logic here... +//! }); +//! +//! Ok(()) +//! # } +//! } +//! ``` +//! +//! ### gRPC Transport (Port 4317) +//! +//! Run the OpenTelemetry Collector: +//! +//! ```shell +//! $ docker run -p 4317:4317 otel/opentelemetry-collector:latest +//! ``` +//! +//! Configure your application to export traces via gRPC (the tonic client requires a Tokio runtime): +//! +//! - With `[tokio::main]` +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "grpc-tonic"))] +//! # { +//! use opentelemetry::{global, trace::Tracer}; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! // Initialize OTLP exporter using gRPC (Tonic) +//! let otlp_exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_tonic() +//! .build()?; +//! +//! // Create a tracer provider with the exporter +//! let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() +//! .with_batch_exporter(otlp_exporter) +//! .build(); +//! +//! // Set it as the global provider +//! global::set_tracer_provider(tracer_provider); +//! +//! // Get a tracer and create spans +//! let tracer = global::tracer("my_tracer"); +//! tracer.in_span("doing_work", |_cx| { +//! // Your application logic here... +//! }); +//! +//! Ok(()) +//! # } +//! } +//! ``` +//! +//! - Without `[tokio::main]` +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "grpc-tonic"))] +//! # { +//! use opentelemetry::{global, trace::Tracer}; +//! +//! fn main() -> Result<(), Box> { +//! // Initialize OTLP exporter using gRPC (Tonic) +//! let rt = tokio::runtime::Runtime::new()?; +//! let tracer_provider = rt.block_on(async { +//! let exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_tonic() +//! .build() +//! .expect("Failed to create span exporter"); +//! opentelemetry_sdk::trace::SdkTracerProvider::builder() +//! .with_batch_exporter(exporter) +//! .build() +//! }); +//! +//! // Set it as the global provider +//! global::set_tracer_provider(tracer_provider); +//! +//! // Get a tracer and create spans +//! let tracer = global::tracer("my_tracer"); +//! tracer.in_span("doing_work", |_cx| { +//! // Your application logic here... +//! }); +//! +//! // Ensure the runtime (`rt`) remains active until the program ends +//! Ok(()) +//! # } +//! } +//! ``` +//! +//! ## Using with Jaeger +//! +//! Jaeger natively supports the OTLP protocol, making it easy to send traces directly: +//! +//! ```shell +//! $ docker run -p 16686:16686 -p 4317:4317 jaegertracing/jaeger:latest +//! ``` +//! +//! After running your application configured with the OTLP exporter, view traces at: +//! `http://localhost:16686` +//! +//! ## Using with Prometheus +//! +//! Prometheus natively supports accepting metrics via the OTLP protocol +//! (HTTP/protobuf). You can [run +//! Prometheus](https://prometheus.io/docs/prometheus/latest/installation/) with +//! the following command: +//! +//! ```shell +//! docker run -p 9090:9090 -v ./prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus --config.file=/etc/prometheus/prometheus.yml --web.enable-otlp-receiver +//! ``` +//! +//! (An empty prometheus.yml file is sufficient for this example.) +//! +//! Modify your application to export metrics via OTLP: +//! +//! ```no_run +//! # #[cfg(all(feature = "metrics", feature = "http-proto"))] +//! # { +//! use opentelemetry::global; +//! use opentelemetry::metrics::Meter; +//! use opentelemetry::KeyValue; +//! use opentelemetry_otlp::Protocol; +//! use opentelemetry_otlp::WithExportConfig; +//! +//! fn main() -> Result<(), Box> { +//! // Initialize OTLP exporter using HTTP binary protocol +//! let exporter = opentelemetry_otlp::MetricExporter::builder() +//! .with_http() +//! .with_protocol(Protocol::HttpBinary) +//! .with_endpoint("http://localhost:9090/api/v1/otlp/v1/metrics") +//! .build()?; +//! +//! // Create a meter provider with the OTLP Metric exporter +//! let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder() +//! .with_periodic_exporter(exporter) +//! .build(); +//! global::set_meter_provider(meter_provider.clone()); +//! +//! // Get a meter +//! let meter = global::meter("my_meter"); +//! +//! // Create a metric +//! let counter = meter.u64_counter("my_counter").build(); +//! counter.add(1, &[KeyValue::new("key", "value")]); +//! +//! // Shutdown the meter provider. This will trigger an export of all metrics. +//! meter_provider.shutdown()?; +//! +//! Ok(()) +//! # } +//! } +//! ``` +//! +//! After running your application configured with the OTLP exporter, view metrics at: +//! `http://localhost:9090` +//! +//! # Environment Variables +//! +//! The OTLP exporter respects the following environment variables, as defined by the +//! [OpenTelemetry specification]. Programmatic configuration via builder methods +//! takes precedence over environment variables. Signal-specific variables take +//! precedence over the generic `OTEL_EXPORTER_OTLP_*` variables. +//! +//! [OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/protocol/exporter/ +//! +//! ## General (all signals) +//! +//! | Variable | Description | Default | +//! |---|---|---| +//! | `OTEL_EXPORTER_OTLP_ENDPOINT` | Target URL for the exporter. For HTTP, signal paths (`/v1/traces`, `/v1/metrics`, `/v1/logs`) are appended automatically. | `http://localhost:4318` (HTTP), `http://localhost:4317` (gRPC) | +//! | `OTEL_EXPORTER_OTLP_PROTOCOL` | Transport protocol. Valid values: `grpc`, `http/protobuf`, `http/json`. Requires the corresponding crate feature. | Feature-dependent | +//! | `OTEL_EXPORTER_OTLP_TIMEOUT` | Maximum wait time (in milliseconds) for the backend to process each batch. | `10000` | +//! | `OTEL_EXPORTER_OTLP_HEADERS` | Key-value pairs for request headers. Format: `key1=value1,key2=value2`. Values are URL-decoded. | (none) | +//! | `OTEL_EXPORTER_OTLP_COMPRESSION` | Compression algorithm. Valid values: `gzip`, `zstd`, `none`. | `none` | +//! | `OTEL_EXPORTER_OTLP_INSECURE` | Whether to disable TLS for gRPC connections. Only applies to gRPC; HTTP security is determined by URL scheme. Valid values: `true`, `false` (case-insensitive). | `false` | +//! +//! ## Traces +//! +//! | Variable | Description | +//! |---|---| +//! | `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Signal-specific endpoint for trace exports. | +//! | `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` | Signal-specific protocol for trace exports. Valid values: `grpc`, `http/protobuf`, `http/json`. | +//! | `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT` | Signal-specific timeout (in milliseconds) for trace exports. | +//! | `OTEL_EXPORTER_OTLP_TRACES_HEADERS` | Signal-specific headers for trace exports. | +//! | `OTEL_EXPORTER_OTLP_TRACES_COMPRESSION` | Signal-specific compression for trace exports. | +//! | `OTEL_EXPORTER_OTLP_TRACES_INSECURE` | Signal-specific insecure flag for gRPC trace exports. | +//! +//! ## Metrics +//! +//! | Variable | Description | +//! |---|---| +//! | `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | Signal-specific endpoint for metrics exports. | +//! | `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` | Signal-specific protocol for metrics exports. Valid values: `grpc`, `http/protobuf`, `http/json`. | +//! | `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` | Signal-specific timeout (in milliseconds) for metrics exports. | +//! | `OTEL_EXPORTER_OTLP_METRICS_HEADERS` | Signal-specific headers for metrics exports. | +//! | `OTEL_EXPORTER_OTLP_METRICS_COMPRESSION` | Signal-specific compression for metrics exports. | +//! | `OTEL_EXPORTER_OTLP_METRICS_INSECURE` | Signal-specific insecure flag for gRPC metrics exports. | +//! | `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` | Temporality preference for metrics. Valid values: `cumulative`, `delta`, `lowmemory` (case-insensitive). | `cumulative` | +//! +//! ## Logs +//! +//! | Variable | Description | +//! |---|---| +//! | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Signal-specific endpoint for log exports. | +//! | `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` | Signal-specific protocol for log exports. Valid values: `grpc`, `http/protobuf`, `http/json`. | +//! | `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` | Signal-specific timeout (in milliseconds) for log exports. | +//! | `OTEL_EXPORTER_OTLP_LOGS_HEADERS` | Signal-specific headers for log exports. | +//! | `OTEL_EXPORTER_OTLP_LOGS_COMPRESSION` | Signal-specific compression for log exports. | +//! | `OTEL_EXPORTER_OTLP_LOGS_INSECURE` | Signal-specific insecure flag for gRPC log exports. | +//! +//! # Feature Flags +//! The following feature flags can enable exporters for different telemetry signals: +//! +//! * `trace`: Includes the trace exporters. +//! * `metrics`: Includes the metrics exporters. +//! * `logs`: Includes the logs exporters. +//! +//! The following feature flags offer additional configurations on gRPC: +//! +//! For users using `tonic` as grpc layer: +//! * `grpc-tonic`: Use `tonic` as grpc layer. +//! * `gzip-tonic`: Use gzip compression for `tonic` grpc layer. +//! * `zstd-tonic`: Use zstd compression for `tonic` grpc layer. +//! * `tls-ring`: Enable rustls TLS support using ring for `tonic`. +//! * `tls-aws-lc`: Enable rustls TLS support using aws-lc for `tonic`. +//! * `tls-provider-agnostic`: Provider-agnostic TLS — enables TLS code paths without bundling a specific +//! crypto provider. Use this when you install a `CryptoProvider` globally +//! (e.g., via `rustls-openssl` for FIPS/OpenSSL environments). +//! * `tls-roots`: Adds system trust roots to rustls-based gRPC clients using the rustls-native-certs crate (use with `tls-ring` or `tls-aws-lc`). +//! * `tls-webpki-roots`: Embeds Mozilla's trust roots to rustls-based gRPC clients using the webpki-roots crate (use with `tls-ring` or `tls-aws-lc`). +//! +//! The following feature flags offer additional configurations on http: +//! +//! * `http-proto`: Use http as transport layer, protobuf as body format. This feature is enabled by default. +//! * `gzip-http`: Use gzip compression for HTTP transport. +//! * `zstd-http`: Use zstd compression for HTTP transport. +//! * `reqwest-blocking-client`: Use reqwest blocking http client. This feature is enabled by default. +//! * `reqwest-client`: Use reqwest async http client. +//! * `hyper-client`: Use hyper async http client. +//! * `reqwest-rustls`: Use reqwest with TLS. Uses `rustls` with the platform's native trust roots by default. +//! If you need Mozilla's embedded CA bundle (webpki-roots), build a custom `reqwest::Client` with +//! a `rustls::ClientConfig` containing +//! `rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned())`, then pass it +//! via `with_http_client()`. +//! +//! Retry with exponential backoff is always enabled for both gRPC and HTTP exports. +//! Failed exports are automatically retried according to the configured retry policy, +//! with proper classification of retryable vs non-retryable errors and support for +//! server-provided throttling hints (HTTP Retry-After, gRPC RetryInfo). +//! +//! # Full Configuration Reference +//! +//! +//! There are two layers of configuration for the OTLP exporter: +//! +//! 1. **Exporter configuration** – controls how telemetry is sent (endpoint, transport, headers, TLS, compression, timeout). +//! Built via the signal-specific builder: [`SpanExporter::builder()`], [`MetricExporter::builder()`], [`LogExporter::builder()`]. +//! 2. **Provider/SDK configuration** – controls how telemetry is collected and batched (sampling, batch size, resource, etc.). +//! Built via [`opentelemetry_sdk::trace::SdkTracerProvider::builder()`], +//! [`opentelemetry_sdk::metrics::SdkMeterProvider::builder()`], or +//! [`opentelemetry_sdk::logs::SdkLoggerProvider::builder()`]. +//! +//! **Configuration precedence** (highest wins): +//! 1. Programmatic configuration via builder methods — always wins when set +//! 2. Signal-specific environment variables (e.g. `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) — used when no programmatic value is set +//! 3. Generic environment variables (e.g. `OTEL_EXPORTER_OTLP_ENDPOINT`) — used when neither programmatic nor signal-specific env var is set +//! 4. Built-in defaults — used when nothing else is configured +//! +//! ## gRPC (tonic) — all configuration options +//! +//! Requires the `grpc-tonic` feature. The methods below come from two traits: +//! - [`WithExportConfig`]: `with_endpoint`, `with_timeout` (shared with HTTP) +//! - [`WithTonicConfig`]: `with_metadata`, `with_compression`, `with_tls_config`, `with_channel`, `with_interceptor`, `with_retry_policy` +//! +//! The examples here use [`SpanExporter`], but the same builder methods are +//! available on [`MetricExporter`] and [`LogExporter`]. +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "grpc-tonic"))] +//! # { +//! use opentelemetry_otlp::{WithExportConfig, WithTonicConfig, Compression}; +//! use std::time::Duration; +//! use tonic::metadata::MetadataMap; +//! +//! // ── gRPC metadata (custom request headers) ─────────────────────────────── +//! // MetadataMap carries per-call key/value pairs sent as HTTP/2 headers. +//! // with_metadata() is additive: calling it multiple times merges entries. +//! let mut metadata = MetadataMap::with_capacity(3); +//! metadata.insert("x-host", "example.com".parse().unwrap()); +//! metadata.insert("x-api-key", "secret".parse().unwrap()); +//! metadata.insert_bin("trace-proto-bin", tonic::metadata::MetadataValue::from_bytes(b"[bin]")); +//! +//! let exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_tonic() +//! // Target gRPC endpoint. Defaults to http://localhost:4317. +//! // Env var: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT (or OTEL_EXPORTER_OTLP_ENDPOINT). +//! .with_endpoint("http://my-collector:4317") +//! // Per-export timeout. Defaults to 10 s. +//! // Env var: OTEL_EXPORTER_OTLP_TRACES_TIMEOUT (or OTEL_EXPORTER_OTLP_TIMEOUT). +//! .with_timeout(Duration::from_secs(5)) +//! // Custom gRPC metadata (auth tokens, routing headers, …). +//! // Env var: OTEL_EXPORTER_OTLP_TRACES_HEADERS (or OTEL_EXPORTER_OTLP_HEADERS). +//! .with_metadata(metadata) +//! // Compression. Requires the `gzip-tonic` or `zstd-tonic` feature. +//! // Env var: OTEL_EXPORTER_OTLP_TRACES_COMPRESSION (or OTEL_EXPORTER_OTLP_COMPRESSION). +//! .with_compression(Compression::Gzip) +//! .build() +//! .expect("Failed to build SpanExporter"); +//! # } +//! ``` +//! +//! ### TLS (grpc-tonic) +//! +//! Requires the `tls-ring` or `tls-aws-lc` feature (plus optionally `tls-roots` or `tls-webpki-roots` +//! to load CA roots automatically). +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "grpc-tonic", any(feature = "tls-ring", feature = "tls-aws-lc")))] +//! # { +//! use opentelemetry_otlp::{WithExportConfig, WithTonicConfig}; +//! use opentelemetry_otlp::tonic_types::transport::ClientTlsConfig; +//! +//! let tls = ClientTlsConfig::new() +//! .domain_name("my-collector.example.com") +//! // Optionally verify the server with a CA certificate: +//! // .ca_certificate(tonic::transport::Certificate::from_pem(CA_PEM)) +//! // Or present a client identity for mutual TLS (mTLS): +//! // .identity(tonic::transport::Identity::from_pem(CERT_PEM, KEY_PEM)) +//! ; +//! +//! let exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_tonic() +//! .with_endpoint("https://my-collector.example.com:4317") +//! .with_tls_config(tls) +//! .build() +//! .expect("Failed to build SpanExporter"); +//! # } +//! ``` +//! +//! ### Pre-built tonic channel +//! +//! Use `with_channel` when you need full control over the transport (e.g. Unix sockets, +//! custom load-balancing). **Note:** `with_channel` overrides any TLS config set via +//! `with_tls_config`, and you are responsible for matching the channel timeout to +//! the exporter timeout. +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "grpc-tonic"))] +//! # { +//! use opentelemetry_otlp::{WithExportConfig, WithTonicConfig}; +//! use std::time::Duration; +//! +//! let channel = tonic::transport::Channel::from_static("http://localhost:4317") +//! .timeout(Duration::from_secs(5)) +//! .connect_lazy(); +//! +//! let exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_tonic() +//! .with_channel(channel) +//! .with_timeout(Duration::from_secs(5)) // keep in sync with channel timeout above +//! .build() +//! .expect("Failed to build SpanExporter"); +//! # } +//! ``` +//! +//! ### gRPC interceptors +//! +//! Use `with_interceptor` to modify every outbound gRPC request — useful for injecting +//! auth tokens or dynamic metadata. Only one interceptor can be set; chain multiple together +//! before passing them in. +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "grpc-tonic"))] +//! # { +//! use opentelemetry_otlp::WithTonicConfig; +//! use tonic::{Request, Status}; +//! +//! fn auth_interceptor(mut req: Request<()>) -> Result, Status> { +//! req.metadata_mut().insert("authorization", "Bearer my-token".parse().unwrap()); +//! Ok(req) +//! } +//! +//! let exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_tonic() +//! .with_interceptor(auth_interceptor) +//! .build() +//! .expect("Failed to build SpanExporter"); +//! # } +//! ``` +//! +//! ### gRPC retry policy +//! +//! Failed exports are retried with exponential backoff and jitter. The retry policy +//! can be customized: +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "grpc-tonic"))] +//! # { +//! use opentelemetry_otlp::{WithTonicConfig, RetryPolicy}; +//! use std::time::Duration; +//! +//! let exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_tonic() +//! .with_retry_policy( +//! RetryPolicy::default() +//! .with_max_retries(5) +//! .with_initial_delay(Duration::from_millis(500)) +//! .with_max_delay(Duration::from_secs(30)) +//! .with_max_jitter(Duration::from_millis(100)), +//! ) +//! .build() +//! .expect("Failed to build SpanExporter"); +//! # } +//! ``` +//! +//! ## HTTP — all configuration options +//! +//! Requires the `http-proto` (default) or `http-json` feature. The methods below come from: +//! - [`WithExportConfig`]: `with_endpoint`, `with_timeout`, `with_protocol` +//! - [`WithHttpConfig`]: `with_headers`, `with_compression`, `with_http_client`, +//! `with_retry_policy`, `with_max_request_body_size` +//! +//! The examples here use [`SpanExporter`], but the same builder methods are +//! available on [`MetricExporter`] and [`LogExporter`]. +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "http-proto"))] +//! # { +//! use opentelemetry_otlp::{WithExportConfig, WithHttpConfig, Protocol, Compression}; +//! use std::time::Duration; +//! use std::collections::HashMap; +//! +//! let exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_http() +//! // Target URL, used verbatim: include the signal path, e.g. /v1/traces. +//! // OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is also used verbatim. +//! // OTEL_EXPORTER_OTLP_ENDPOINT is a base URL and appends /v1/traces automatically. +//! // Defaults to http://localhost:4318/v1/traces. +//! .with_endpoint("http://my-collector:4318/v1/traces") +//! // Per-export timeout. Defaults to 10 s. +//! // Env var: OTEL_EXPORTER_OTLP_TRACES_TIMEOUT (or OTEL_EXPORTER_OTLP_TIMEOUT). +//! .with_timeout(Duration::from_secs(5)) +//! // Transport encoding. HttpBinary (protobuf) is the default. +//! // HttpJson requires the `http-json` feature. +//! // Env var: OTEL_EXPORTER_OTLP_PROTOCOL. +//! .with_protocol(Protocol::HttpBinary) +//! // Custom HTTP headers (auth tokens, routing headers, …). +//! // Values are URL-decoded when read from environment variables. +//! // Env var: OTEL_EXPORTER_OTLP_TRACES_HEADERS (or OTEL_EXPORTER_OTLP_HEADERS). +//! .with_headers(HashMap::from([ +//! ("x-api-key".to_string(), "secret".to_string()), +//! ])) +//! // Compression. Requires the `gzip-http` or `zstd-http` feature. +//! // Env var: OTEL_EXPORTER_OTLP_TRACES_COMPRESSION (or OTEL_EXPORTER_OTLP_COMPRESSION). +//! .with_compression(Compression::Gzip) +//! // Maximum serialized HTTP request size before and after compression. +//! // Defaults to the OTLP-recommended 64 MiB. +//! .with_max_request_body_size(64 * 1024 * 1024) +//! .build() +//! .expect("Failed to build SpanExporter"); +//! # } +//! ``` +//! +//! ### Custom HTTP client +//! +//! By default the exporter uses a `reqwest` blocking client (`reqwest-blocking-client` feature, +//! enabled by default). Supply your own client to control TLS, proxies, connection pooling, etc. +//! The client must implement the [`opentelemetry_http::HttpClient`] trait. +//! +//! The standard SDK path uses the default `BatchSpanProcessor`, +//! `BatchLogProcessor`, and `PeriodicReader`. These components run exports on +//! SDK-owned background threads, so OTLP/HTTP should use the default +//! `reqwest-blocking-client` with them. +//! +//! HTTP client selection is based on enabled crate features and is not aware of +//! the SDK processor or metric reader that will drive the exporter. If you enable +//! async HTTP clients such as `reqwest-client` or `hyper-client`, do not use them +//! with the default batch processors or periodic reader. +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "http-proto", feature = "reqwest-client"))] +//! # { +//! use opentelemetry_otlp::WithHttpConfig; +//! +//! // reqwest async client (requires the `reqwest-client` feature) +//! let http_client = reqwest::Client::builder() +//! .timeout(std::time::Duration::from_secs(5)) +//! // .danger_accept_invalid_certs(true) // for testing only +//! .build() +//! .expect("Failed to build reqwest client"); +//! +//! let exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_http() +//! .with_http_client(http_client) +//! .build() +//! .expect("Failed to build SpanExporter"); +//! # } +//! ``` +//! +//! ### HTTP retry policy +//! +//! Failed exports are retried with exponential backoff and jitter. The retry policy +//! can be customized: +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "http-proto"))] +//! # { +//! use opentelemetry_otlp::{WithHttpConfig, RetryPolicy}; +//! use std::time::Duration; +//! +//! let exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_http() +//! .with_retry_policy( +//! RetryPolicy::default() +//! .with_max_retries(5) +//! .with_initial_delay(Duration::from_millis(500)) +//! .with_max_delay(Duration::from_secs(30)) +//! .with_max_jitter(Duration::from_millis(100)), +//! ) +//! .build() +//! .expect("Failed to build SpanExporter"); +//! # } +//! ``` +//! +//! ## All three signals (Traces, Metrics, Logs) +//! +//! The same exporter configuration options apply to all three signals. The only differences are: +//! - The builder entry point: [`SpanExporter::builder()`], [`MetricExporter::builder()`], [`LogExporter::builder()`] +//! - The signal-specific environment variables (e.g. `OTEL_EXPORTER_OTLP_TRACES_*` vs `OTEL_EXPORTER_OTLP_METRICS_*`) +//! - Metrics has an additional `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE` variable +//! +//! ```no_run +//! # #[cfg(all(feature = "trace", feature = "metrics", feature = "logs", feature = "grpc-tonic"))] +//! # { +//! use opentelemetry_otlp::{WithExportConfig, WithTonicConfig}; +//! use opentelemetry_sdk::{ +//! trace::SdkTracerProvider, +//! metrics::SdkMeterProvider, +//! logs::SdkLoggerProvider, +//! Resource, +//! }; +//! use std::time::Duration; +//! +//! let resource = Resource::builder() +//! .with_service_name("my-service") +//! .build(); +//! +//! // Traces +//! let span_exporter = opentelemetry_otlp::SpanExporter::builder() +//! .with_tonic() +//! .with_endpoint("http://my-collector:4317") +//! .with_timeout(Duration::from_secs(5)) +//! .build() +//! .expect("Failed to build SpanExporter"); +//! let tracer_provider = SdkTracerProvider::builder() +//! .with_resource(resource.clone()) +//! .with_batch_exporter(span_exporter) +//! .build(); +//! +//! // Metrics +//! let metric_exporter = opentelemetry_otlp::MetricExporter::builder() +//! .with_tonic() +//! .with_endpoint("http://my-collector:4317") +//! .with_timeout(Duration::from_secs(5)) +//! .build() +//! .expect("Failed to build MetricExporter"); +//! let meter_provider = SdkMeterProvider::builder() +//! .with_resource(resource.clone()) +//! .with_periodic_exporter(metric_exporter) +//! .build(); +//! +//! // Logs +//! let log_exporter = opentelemetry_otlp::LogExporter::builder() +//! .with_tonic() +//! .with_endpoint("http://my-collector:4317") +//! .with_timeout(Duration::from_secs(5)) +//! .build() +//! .expect("Failed to build LogExporter"); +//! let logger_provider = SdkLoggerProvider::builder() +//! .with_resource(resource) +//! .with_batch_exporter(log_exporter) +//! .build(); +//! # } +//! ``` +#![warn( + future_incompatible, + missing_debug_implementations, + missing_docs, + nonstandard_style, + rust_2018_idioms, + unreachable_pub, + unused +)] +#![allow(elided_lifetimes_in_paths)] +#![cfg_attr(docsrs, feature(doc_cfg), deny(rustdoc::broken_intra_doc_links))] +#![cfg_attr(test, deny(warnings))] + +mod exporter; +#[cfg(feature = "logs")] +#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] +mod logs; +#[cfg(feature = "metrics")] +#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] +mod metric; +#[cfg(feature = "trace")] +#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] +mod span; + +#[cfg(any( + feature = "http-proto", + feature = "http-json", + all( + feature = "grpc-tonic", + any(feature = "trace", feature = "metrics", feature = "logs") + ) +))] +mod retry_classification; + +#[cfg(any( + feature = "http-proto", + feature = "http-json", + all( + feature = "grpc-tonic", + any(feature = "trace", feature = "metrics", feature = "logs") + ) +))] +mod retry; + +// RetryPolicy configures transport builders even when no signal is enabled, so +// it is available under a broader feature gate than the execution modules above. +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +mod retry_policy; + +pub use crate::exporter::Compression; +pub use crate::exporter::ExporterBuildError; +pub use crate::exporter::ParseConfigError; +#[cfg(feature = "trace")] +#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] +pub use crate::span::{ + SpanExporter, SpanExporterBuilder, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_HEADERS, + OTEL_EXPORTER_OTLP_TRACES_INSECURE, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, +}; + +#[cfg(feature = "metrics")] +#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] +pub use crate::metric::{ + MetricExporter, MetricExporterBuilder, OTEL_EXPORTER_OTLP_METRICS_COMPRESSION, + OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_HEADERS, + OTEL_EXPORTER_OTLP_METRICS_INSECURE, OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, OTEL_EXPORTER_OTLP_METRICS_TIMEOUT, +}; + +#[cfg(feature = "logs")] +#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] +pub use crate::logs::{ + LogExporter, LogExporterBuilder, OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_INSECURE, OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, +}; + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +use crate::exporter::http::HttpExporterBuilder; +#[cfg(any(feature = "http-proto", feature = "http-json"))] +pub use crate::exporter::http::WithHttpConfig; + +#[cfg(feature = "grpc-tonic")] +use crate::exporter::tonic::TonicExporterBuilder; +#[cfg(feature = "grpc-tonic")] +pub use crate::exporter::tonic::WithTonicConfig; + +pub use crate::exporter::{ + WithExportConfig, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_ENDPOINT, + OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_INSECURE, OTEL_EXPORTER_OTLP_PROTOCOL, + OTEL_EXPORTER_OTLP_PROTOCOL_GRPC, OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON, + OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF, OTEL_EXPORTER_OTLP_TIMEOUT, + OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT, +}; + +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +pub use retry_policy::RetryPolicy; + +/// Type to indicate the builder does not have a client set. +#[derive(Debug, Default, Clone)] +pub struct NoExporterBuilderSet; + +/// Type indicating that the tonic transport has been selected. +#[cfg(feature = "grpc-tonic")] +// This is for clippy to work with only the grpc-tonic feature enabled +#[allow(unused)] +#[derive(Debug)] +pub struct TonicExporterBuilderSet(TonicExporterBuilder); + +/// Type indicating that the HTTP transport has been selected. +#[cfg(any(feature = "http-proto", feature = "http-json"))] +#[derive(Debug)] +pub struct HttpExporterBuilderSet(HttpExporterBuilder); + +/// The communication protocol to use when exporting data. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Protocol { + /// GRPC protocol + #[cfg(feature = "grpc-tonic")] + Grpc, + /// HTTP protocol with binary protobuf + #[cfg(feature = "http-proto")] + HttpBinary, + /// HTTP protocol with JSON payload + #[cfg(feature = "http-json")] + HttpJson, +} + +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +impl Protocol { + /// Attempts to parse a protocol from the `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable. + /// + /// Returns `None` if: + /// - The environment variable is not set + /// - The value doesn't match a known protocol + /// - The specified protocol's feature is not enabled + pub(crate) fn from_env() -> Option { + Self::parse_from_env_var(OTEL_EXPORTER_OTLP_PROTOCOL) + } + + /// Attempts to parse a protocol from the given environment variable. + pub(crate) fn parse_from_env_var(env_var: &str) -> Option { + use crate::exporter::{ + OTEL_EXPORTER_OTLP_PROTOCOL_GRPC, OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON, + OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF, + }; + + let protocol = crate::exporter::read_enum_env_var(env_var)?; + let normalized = protocol.to_ascii_lowercase(); + + match normalized.as_str() { + OTEL_EXPORTER_OTLP_PROTOCOL_GRPC => { + #[cfg(feature = "grpc-tonic")] + { + Some(Protocol::Grpc) + } + #[cfg(not(feature = "grpc-tonic"))] + { + crate::exporter::warn_missing_protocol_feature( + env_var, + &protocol, + "grpc-tonic", + ); + None + } + } + OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_PROTOBUF => { + #[cfg(feature = "http-proto")] + { + Some(Protocol::HttpBinary) + } + #[cfg(not(feature = "http-proto"))] + { + crate::exporter::warn_missing_protocol_feature( + env_var, + &protocol, + "http-proto", + ); + None + } + } + OTEL_EXPORTER_OTLP_PROTOCOL_HTTP_JSON => { + #[cfg(feature = "http-json")] + { + Some(Protocol::HttpJson) + } + #[cfg(not(feature = "http-json"))] + { + crate::exporter::warn_missing_protocol_feature(env_var, &protocol, "http-json"); + None + } + } + _ => { + crate::exporter::warn_ignored_enum_env_var( + env_var, + &protocol, + "expected 'grpc', 'http/protobuf', or 'http/json'", + ); + None + } + } + } + + /// Returns the default protocol based on enabled features, without + /// consulting environment variables. + /// + /// Priority order (first available wins): + /// 1. http-json (if enabled) + /// 2. http-proto (if enabled) + /// 3. grpc-tonic (if enabled) + pub(crate) fn feature_default() -> Self { + #[cfg(feature = "http-json")] + return Protocol::HttpJson; + + #[cfg(all(feature = "http-proto", not(feature = "http-json")))] + return Protocol::HttpBinary; + + #[cfg(all( + feature = "grpc-tonic", + not(any(feature = "http-proto", feature = "http-json")) + ))] + return Protocol::Grpc; + } +} + +/// Re-exported types from the `tonic` crate. +#[cfg(feature = "grpc-tonic")] +pub mod tonic_types { + /// Re-exported types from `tonic::metadata`. + pub mod metadata { + #[doc(no_inline)] + pub use tonic::metadata::MetadataMap; + } + + /// Re-exported types from `tonic::transport`. + #[cfg(any( + feature = "tls-ring", + feature = "tls-aws-lc", + feature = "tls-provider-agnostic" + ))] + pub mod transport { + #[doc(no_inline)] + pub use tonic::transport::{Certificate, ClientTlsConfig, Identity}; + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/logs.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/logs.rs new file mode 100644 index 00000000..23a85fcc --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/logs.rs @@ -0,0 +1,221 @@ +//! OTLP - Log Exporter +//! +//! Defines a [LogExporter] to send logs via the OpenTelemetry Protocol (OTLP) + +#[cfg(feature = "grpc-tonic")] +use opentelemetry::otel_debug; +use opentelemetry_sdk::{error::OTelSdkResult, logs::LogBatch}; +use std::fmt::Debug; +use std::time; + +use crate::{exporter::HasExportConfig, ExporterBuildError, NoExporterBuilderSet}; + +#[cfg(feature = "grpc-tonic")] +use crate::{ + exporter::tonic::{HasTonicConfig, TonicExporterBuilder}, + TonicExporterBuilderSet, +}; + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +use crate::{ + exporter::http::{HasHttpConfig, HttpExporterBuilder}, + HttpExporterBuilderSet, +}; + +/// Compression algorithm to use, defaults to none. +pub const OTEL_EXPORTER_OTLP_LOGS_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_LOGS_COMPRESSION"; + +/// Target to which the exporter is going to send logs +pub const OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"; + +/// Maximum time the OTLP exporter will wait for each batch logs export. +pub const OTEL_EXPORTER_OTLP_LOGS_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT"; + +/// Key-value pairs to be used as headers associated with gRPC or HTTP requests +/// for sending logs. +/// Example: `k1=v1,k2=v2` +/// Note: this is only supported for HTTP. +pub const OTEL_EXPORTER_OTLP_LOGS_HEADERS: &str = "OTEL_EXPORTER_OTLP_LOGS_HEADERS"; +/// Protocol to use for log exports. Valid values: `grpc`, `http/protobuf`, `http/json`. +pub const OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"; +/// Whether to disable TLS for gRPC log exports. +/// Only applies to gRPC; HTTP security is determined by URL scheme. +pub const OTEL_EXPORTER_OTLP_LOGS_INSECURE: &str = "OTEL_EXPORTER_OTLP_LOGS_INSECURE"; + +/// Builder for creating a new [LogExporter]. +#[derive(Debug, Default, Clone)] +pub struct LogExporterBuilder { + client: C, +} + +impl LogExporterBuilder { + /// Create a new [LogExporterBuilder] with default settings. + pub fn new() -> Self { + LogExporterBuilder::default() + } + + /// With the gRPC Tonic transport. + #[cfg(feature = "grpc-tonic")] + pub fn with_tonic(self) -> LogExporterBuilder { + LogExporterBuilder { + client: TonicExporterBuilderSet(TonicExporterBuilder::default()), + } + } + + /// With the HTTP transport. + #[cfg(any(feature = "http-proto", feature = "http-json"))] + pub fn with_http(self) -> LogExporterBuilder { + LogExporterBuilder { + client: HttpExporterBuilderSet(HttpExporterBuilder::default()), + } + } + + /// Build the [LogExporter] with the default transport selected by environment + /// variable or feature flags. + /// + /// The transport is chosen based on: + /// 1. `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` environment variable + /// 2. `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable + /// 3. Enabled features, with priority: `http-json` > `http-proto` > `grpc-tonic` + /// + /// Use [`with_tonic`](Self::with_tonic) or [`with_http`](Self::with_http) to + /// explicitly select a transport and access transport-specific configuration. + #[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] + pub fn build(self) -> Result { + use crate::WithExportConfig; + + let protocol = crate::exporter::resolve_protocol(OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, None); + match protocol { + #[cfg(feature = "grpc-tonic")] + crate::Protocol::Grpc => self.with_tonic().with_protocol(protocol).build(), + #[cfg(feature = "http-proto")] + crate::Protocol::HttpBinary => self.with_http().with_protocol(protocol).build(), + #[cfg(feature = "http-json")] + crate::Protocol::HttpJson => self.with_http().with_protocol(protocol).build(), + } + } +} + +#[cfg(feature = "grpc-tonic")] +impl LogExporterBuilder { + /// Build the [LogExporter] with the gRPC Tonic transport. + pub fn build(self) -> Result { + let result = self.client.0.build_log_exporter(); + otel_debug!(name: "LogExporterBuilt", result = format!("{:?}", &result)); + result + } +} + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +impl LogExporterBuilder { + /// Build the [LogExporter] with the HTTP transport. + pub fn build(self) -> Result { + self.client.0.build_log_exporter() + } +} + +#[cfg(feature = "grpc-tonic")] +impl HasExportConfig for LogExporterBuilder { + fn export_config(&mut self) -> &mut crate::exporter::ExportConfig { + &mut self.client.0.exporter_config + } +} + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +impl HasExportConfig for LogExporterBuilder { + fn export_config(&mut self) -> &mut crate::exporter::ExportConfig { + &mut self.client.0.exporter_config + } +} + +#[cfg(feature = "grpc-tonic")] +impl HasTonicConfig for LogExporterBuilder { + fn tonic_config(&mut self) -> &mut crate::exporter::tonic::TonicConfig { + &mut self.client.0.tonic_config + } +} + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +impl HasHttpConfig for LogExporterBuilder { + fn http_client_config(&mut self) -> &mut crate::exporter::http::HttpConfig { + &mut self.client.0.http_config + } +} + +/// OTLP exporter that sends log data +#[derive(Debug)] +pub struct LogExporter { + client: SupportedTransportClient, +} + +#[derive(Debug)] +enum SupportedTransportClient { + #[cfg(feature = "grpc-tonic")] + Tonic(crate::exporter::tonic::logs::TonicLogsClient), + #[cfg(any(feature = "http-proto", feature = "http-json"))] + Http(crate::exporter::http::OtlpHttpClient), +} + +impl LogExporter { + /// Obtain a builder to configure a [LogExporter]. + pub fn builder() -> LogExporterBuilder { + LogExporterBuilder::default() + } + + #[cfg(any(feature = "http-proto", feature = "http-json"))] + pub(crate) fn from_http(client: crate::exporter::http::OtlpHttpClient) -> Self { + LogExporter { + client: SupportedTransportClient::Http(client), + } + } + + #[cfg(feature = "grpc-tonic")] + pub(crate) fn from_tonic(client: crate::exporter::tonic::logs::TonicLogsClient) -> Self { + LogExporter { + client: SupportedTransportClient::Tonic(client), + } + } +} + +impl opentelemetry_sdk::logs::LogExporter for LogExporter { + async fn export(&self, batch: LogBatch<'_>) -> OTelSdkResult { + match &self.client { + #[cfg(feature = "grpc-tonic")] + SupportedTransportClient::Tonic(client) => client.export(batch).await, + #[cfg(any(feature = "http-proto", feature = "http-json"))] + SupportedTransportClient::Http(client) => client.export(batch).await, + } + } + + fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) { + match &mut self.client { + #[cfg(feature = "grpc-tonic")] + SupportedTransportClient::Tonic(client) => client.set_resource(resource), + #[cfg(any(feature = "http-proto", feature = "http-json"))] + SupportedTransportClient::Http(client) => client.set_resource(resource), + } + } + + fn shutdown_with_timeout(&self, _timeout: time::Duration) -> OTelSdkResult { + match &self.client { + #[cfg(feature = "grpc-tonic")] + SupportedTransportClient::Tonic(client) => client.shutdown(), + #[cfg(any(feature = "http-proto", feature = "http-json"))] + SupportedTransportClient::Http(client) => client.shutdown(), + } + } +} + +#[cfg(test)] +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +mod tests { + use crate::LogExporter; + + // Uses a tokio runtime because, under a gRPC-only build, the auto-selected + // tonic transport needs an active reactor to construct its channel. + #[tokio::test] + async fn build_with_default_transport() { + let result = LogExporter::builder().build(); + assert!(result.is_ok(), "build() should succeed: {:?}", result.err()); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/metric.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/metric.rs new file mode 100644 index 00000000..c75f05d7 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/metric.rs @@ -0,0 +1,409 @@ +//! OTEL metric exporter +//! +//! Defines a [MetricExporter] to send metric data to backend via OTLP protocol. +//! + +#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] +use crate::exporter::HasExportConfig; + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +use crate::{ + exporter::http::{HasHttpConfig, HttpExporterBuilder}, + HttpExporterBuilderSet, +}; + +#[cfg(feature = "grpc-tonic")] +use crate::{ + exporter::tonic::{HasTonicConfig, TonicExporterBuilder}, + TonicExporterBuilderSet, +}; + +use crate::{ExporterBuildError, NoExporterBuilderSet}; + +use core::fmt; +use opentelemetry_sdk::error::OTelSdkResult; + +use opentelemetry_sdk::metrics::{ + data::ResourceMetrics, exporter::PushMetricExporter, Temporality, +}; +use std::fmt::{Debug, Formatter}; +use std::time::Duration; + +/// Target to which the exporter is going to send metrics, defaults to https://localhost:4317/v1/metrics. +/// Learn about the relationship between this constant and default/spans/logs at +/// +pub const OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"; +/// Max waiting time for the backend to process each metrics batch, defaults to 10s. +pub const OTEL_EXPORTER_OTLP_METRICS_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT"; +/// Compression algorithm to use, defaults to none. +pub const OTEL_EXPORTER_OTLP_METRICS_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION"; +/// Key-value pairs to be used as headers associated with gRPC or HTTP requests +/// for sending metrics. +/// Example: `k1=v1,k2=v2` +/// Note: this is only supported for HTTP. +pub const OTEL_EXPORTER_OTLP_METRICS_HEADERS: &str = "OTEL_EXPORTER_OTLP_METRICS_HEADERS"; +/// Protocol to use for metrics exports. Valid values: `grpc`, `http/protobuf`, `http/json`. +pub const OTEL_EXPORTER_OTLP_METRICS_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"; +/// Whether to disable TLS for gRPC metrics exports. +/// Only applies to gRPC; HTTP security is determined by URL scheme. +pub const OTEL_EXPORTER_OTLP_METRICS_INSECURE: &str = "OTEL_EXPORTER_OTLP_METRICS_INSECURE"; +/// Temporality preference for metrics, defaults to cumulative. +pub const OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE: &str = + "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"; + +/// A builder for creating a new [MetricExporter]. +#[derive(Debug, Default, Clone)] +pub struct MetricExporterBuilder { + client: C, + temporality: Option, +} + +impl MetricExporterBuilder { + /// Create a new [MetricExporterBuilder] with default settings. + pub fn new() -> Self { + MetricExporterBuilder::default() + } + + /// Build the [MetricExporter] with the default transport selected by environment + /// variable or feature flags. + /// + /// The transport is chosen based on: + /// 1. `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL` environment variable + /// 2. `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable + /// 3. Enabled features, with priority: `http-json` > `http-proto` > `grpc-tonic` + /// + /// Use [`with_tonic`](Self::with_tonic) or [`with_http`](Self::with_http) to + /// explicitly select a transport and access transport-specific configuration. + #[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] + pub fn build(self) -> Result { + use crate::WithExportConfig; + + let protocol = crate::exporter::resolve_protocol(OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, None); + match protocol { + #[cfg(feature = "grpc-tonic")] + crate::Protocol::Grpc => self.with_tonic().with_protocol(protocol).build(), + #[cfg(feature = "http-proto")] + crate::Protocol::HttpBinary => self.with_http().with_protocol(protocol).build(), + #[cfg(feature = "http-json")] + crate::Protocol::HttpJson => self.with_http().with_protocol(protocol).build(), + } + } + + /// With the gRPC Tonic transport. + #[cfg(feature = "grpc-tonic")] + pub fn with_tonic(self) -> MetricExporterBuilder { + MetricExporterBuilder { + client: TonicExporterBuilderSet(TonicExporterBuilder::default()), + temporality: self.temporality, + } + } + + /// With the HTTP transport. + #[cfg(any(feature = "http-proto", feature = "http-json"))] + pub fn with_http(self) -> MetricExporterBuilder { + MetricExporterBuilder { + client: HttpExporterBuilderSet(HttpExporterBuilder::default()), + temporality: self.temporality, + } + } +} + +impl MetricExporterBuilder { + /// Set the temporality for the metrics. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + pub fn with_temporality(self, temporality: Temporality) -> MetricExporterBuilder { + MetricExporterBuilder { + client: self.client, + temporality: Some(temporality), + } + } +} + +/// Resolve temporality with priority: +/// 1. Provided config value +/// 2. OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE environment variable +/// 3. Default (Cumulative) +#[cfg(any(feature = "http-proto", feature = "http-json", feature = "grpc-tonic"))] +fn resolve_temporality(provided: Option) -> Temporality { + if let Some(temporality) = provided { + return temporality; + } + + if let Some(value) = + crate::exporter::read_enum_env_var(OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE) + { + return value.parse::().unwrap_or_else(|_| { + crate::exporter::warn_ignored_enum_env_var( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + &value, + "expected 'cumulative', 'delta', or 'lowmemory'", + ); + Temporality::default() + }); + } + + Temporality::default() +} + +#[cfg(feature = "grpc-tonic")] +impl MetricExporterBuilder { + /// Build the [MetricExporter] with the gRPC Tonic transport. + pub fn build(self) -> Result { + let temporality = resolve_temporality(self.temporality); + let exporter = self.client.0.build_metrics_exporter(temporality)?; + opentelemetry::otel_debug!(name: "MetricExporterBuilt"); + Ok(exporter) + } +} + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +impl MetricExporterBuilder { + /// Build the [MetricExporter] with the HTTP transport. + pub fn build(self) -> Result { + let temporality = resolve_temporality(self.temporality); + let exporter = self.client.0.build_metrics_exporter(temporality)?; + Ok(exporter) + } +} + +#[cfg(feature = "grpc-tonic")] +impl HasExportConfig for MetricExporterBuilder { + fn export_config(&mut self) -> &mut crate::exporter::ExportConfig { + &mut self.client.0.exporter_config + } +} + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +impl HasExportConfig for MetricExporterBuilder { + fn export_config(&mut self) -> &mut crate::exporter::ExportConfig { + &mut self.client.0.exporter_config + } +} + +#[cfg(feature = "grpc-tonic")] +impl HasTonicConfig for MetricExporterBuilder { + fn tonic_config(&mut self) -> &mut crate::exporter::tonic::TonicConfig { + &mut self.client.0.tonic_config + } +} + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +impl HasHttpConfig for MetricExporterBuilder { + fn http_client_config(&mut self) -> &mut crate::exporter::http::HttpConfig { + &mut self.client.0.http_config + } +} + +/// An interface for OTLP metrics clients +pub(crate) trait MetricsClient: fmt::Debug + Send + Sync + 'static { + fn export( + &self, + metrics: &ResourceMetrics, + ) -> impl std::future::Future + Send; + fn shutdown(&self) -> OTelSdkResult; +} + +/// Export metrics in OTEL format. +pub struct MetricExporter { + client: SupportedTransportClient, + temporality: Temporality, +} + +#[derive(Debug)] +enum SupportedTransportClient { + #[cfg(feature = "grpc-tonic")] + Tonic(crate::exporter::tonic::metrics::TonicMetricsClient), + #[cfg(any(feature = "http-proto", feature = "http-json"))] + Http(crate::exporter::http::OtlpHttpClient), +} + +impl Debug for MetricExporter { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MetricExporter").finish() + } +} + +impl PushMetricExporter for MetricExporter { + async fn export(&self, metrics: &ResourceMetrics) -> OTelSdkResult { + match &self.client { + #[cfg(feature = "grpc-tonic")] + SupportedTransportClient::Tonic(client) => client.export(metrics).await, + #[cfg(any(feature = "http-proto", feature = "http-json"))] + SupportedTransportClient::Http(client) => client.export(metrics).await, + } + } + + fn force_flush(&self) -> OTelSdkResult { + // this component is stateless + Ok(()) + } + + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + match &self.client { + #[cfg(feature = "grpc-tonic")] + SupportedTransportClient::Tonic(client) => client.shutdown(), + #[cfg(any(feature = "http-proto", feature = "http-json"))] + SupportedTransportClient::Http(client) => client.shutdown(), + } + } + + fn temporality(&self) -> Temporality { + self.temporality + } +} + +impl MetricExporter { + /// Obtain a builder to configure a [MetricExporter]. + pub fn builder() -> MetricExporterBuilder { + MetricExporterBuilder::default() + } + + #[cfg(feature = "grpc-tonic")] + pub(crate) fn from_tonic( + client: crate::exporter::tonic::metrics::TonicMetricsClient, + temporality: Temporality, + ) -> Self { + Self { + client: SupportedTransportClient::Tonic(client), + temporality, + } + } + + #[cfg(any(feature = "http-proto", feature = "http-json"))] + pub(crate) fn from_http( + client: crate::exporter::http::OtlpHttpClient, + temporality: Temporality, + ) -> Self { + Self { + client: SupportedTransportClient::Http(client), + temporality, + } + } +} + +#[cfg(test)] +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +mod build_tests { + use crate::MetricExporter; + + // Uses a tokio runtime because, under a gRPC-only build, the auto-selected + // tonic transport needs an active reactor to construct its channel. + #[tokio::test] + async fn build_with_default_transport() { + // Unset the temporality env var so parallel tests (e.g. + // invalid_env_var_returns_error) that set it to invalid values + // don't cause this build to fail. + temp_env::with_var_unset( + super::OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + || { + let result = MetricExporter::builder().build(); + assert!(result.is_ok(), "build() should succeed: {:?}", result.err()); + }, + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn run_env_test(env_vars: T, f: F) + where + F: FnOnce(), + T: Into>, + { + temp_env::with_vars( + env_vars + .into() + .iter() + .map(|&(k, v)| (k, Some(v))) + .collect::)>>(), + f, + ) + } + + #[test] + fn code_config_overrides_env_var() { + run_env_test( + vec![( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + "cumulative", + )], + || { + let result = resolve_temporality(Some(Temporality::Delta)); + assert_eq!(result, Temporality::Delta); + }, + ); + } + + #[test] + fn env_var_sets_delta() { + run_env_test( + vec![(OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, "delta")], + || { + let result = resolve_temporality(None); + assert_eq!(result, Temporality::Delta); + }, + ); + } + + #[test] + fn env_var_sets_lowmemory() { + run_env_test( + vec![( + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, + "lowmemory", + )], + || { + let result = resolve_temporality(None); + assert_eq!(result, Temporality::LowMemory); + }, + ); + } + + #[test] + fn env_var_case_insensitive() { + run_env_test( + vec![(OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, "Delta")], + || { + let result = resolve_temporality(None); + assert_eq!(result, Temporality::Delta); + }, + ); + } + + #[test] + fn invalid_env_var_uses_default() { + run_env_test( + vec![(OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, "invalid")], + || { + let result = resolve_temporality(None); + assert_eq!(result, Temporality::Cumulative); + }, + ); + } + + #[test] + fn empty_env_var_uses_default() { + run_env_test( + vec![(OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, "")], + || { + let result = resolve_temporality(None); + assert_eq!(result, Temporality::Cumulative); + }, + ); + } + + #[test] + fn test_use_default_when_nothing_set() { + temp_env::with_var_unset(OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, || { + let result = resolve_temporality(None); + assert_eq!(result, Temporality::Cumulative); + }); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/retry.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/retry.rs new file mode 100644 index 00000000..87f71329 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/retry.rs @@ -0,0 +1,767 @@ +//! Internal retry execution with exponential backoff, jitter, and throttling hints. + +use crate::retry_policy::RetryPolicy; +use opentelemetry::{otel_debug, otel_info, otel_warn}; +use std::collections::hash_map::DefaultHasher; +use std::future::Future; +use std::hash::Hasher; +use std::time::{Duration, Instant, SystemTime}; + +/// Sleeps for the given duration. +/// +/// Uses `tokio::time::sleep` when the `tokio` feature is enabled and a Tokio +/// runtime is present (cooperative, won't block the reactor); otherwise falls +/// back to `std::thread::sleep`. +/// +/// Note: `tokio` is not a default feature, so default builds +/// (`reqwest-blocking-client`) always use `std::thread::sleep`. It is pulled in +/// by `grpc-tonic`, `reqwest-client`, and `hyper-client`. +async fn sleep_for(duration: Duration) { + #[cfg(feature = "tokio")] + { + if tokio::runtime::Handle::try_current().is_ok() { + tokio::time::sleep(duration).await; + return; + } + } + std::thread::sleep(duration); +} + +/// Classification of errors for retry purposes. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum RetryErrorType { + /// Error is not retryable (e.g., authentication failure, bad request). + NonRetryable, + /// Error is retryable with exponential backoff (e.g., server error, network timeout). + Retryable, + /// Error indicates throttling - wait for at least the specified duration before retrying. + /// The server delay seeds exponential backoff: subsequent retries grow from it. + Throttled(Duration), +} + +/// Maximum duration a server-provided throttle delay (Retry-After / RetryInfo) is allowed +/// to block the export thread. Any server-provided delay exceeding this cap is truncated. +/// This prevents a misbehaving server from stalling the export pipeline indefinitely. +const MAX_THROTTLE_DURATION: Duration = Duration::from_secs(30); + +// Generates a random jitter value up to max_jitter at millisecond resolution. +fn generate_jitter(max_jitter: Duration) -> Duration { + let nanos = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .subsec_nanos(); + + let mut hasher = DefaultHasher::default(); + hasher.write_u32(nanos); + let max_jitter_ms = max_jitter.as_millis().min(u64::MAX as u128) as u64; + let jitter_ms = if max_jitter_ms == u64::MAX { + hasher.finish() + } else { + hasher.finish() % (max_jitter_ms + 1) + }; + Duration::from_millis(jitter_ms) +} + +/// Retries the given operation with exponential backoff, jitter, and error classification. +/// +/// This function provides retry behavior by classifying errors and honoring server-provided +/// throttling hints (e.g., HTTP Retry-After, gRPC RetryInfo). +/// +/// When a throttle delay is received, it seeds the exponential backoff: the delay is set +/// to `max(current_delay, server_delay)` and subsequent retries double from there. If the +/// server delay exceeds the configured `max_delay_ms`, the backoff cap is raised to +/// `MAX_THROTTLE_DURATION` (30 seconds) so that repeated failures produce increasing +/// delays rather than getting stuck at the server value. +/// +/// Delays between retries adapt to the calling context: when running inside a Tokio +/// runtime (gRPC/async HTTP exporters), `tokio::time::sleep` is used cooperatively; +/// on a bare OS thread (the SDK's default batch processors), `std::thread::sleep` +/// is used instead. +/// +/// A time budget (deadline) bounds the total retry duration. If the budget is exhausted, +/// the function returns the last error without further retries. +/// +/// **Note:** The deadline governs whether to start another retry and caps inter-retry +/// sleep durations, but it does not cancel an in-flight export operation. On dedicated +/// threads (the SDK's default batch processors), individual export calls do not have a +/// timeout today, so a single slow export can exceed the deadline. This is an existing +/// limitation. +/// +/// # Arguments +/// +/// * `policy` - The retry policy configuration. +/// * `deadline` - Maximum total time allowed for all retry attempts combined. +/// * `error_classifier` - Function to classify errors for retry decisions. +/// * `operation_name` - The name of the operation being retried. +/// * `operation` - The operation to be retried. +/// +/// # Returns +/// +/// A `Result` containing the operation's result or an error if max retries are reached +/// or a non-retryable error occurs. +pub(crate) async fn retry_with_backoff( + policy: &RetryPolicy, + deadline: Duration, + error_classifier: C, + operation_name: &str, + mut operation: F, +) -> Result +where + F: FnMut() -> Fut, + E: std::fmt::Debug, + Fut: Future>, + C: Fn(&E) -> RetryErrorType, +{ + let start = Instant::now(); + let mut attempt = 0; + let mut delay = policy.initial_delay; + // The delay cap may be raised beyond the configured max if a server-provided + // throttle delay exceeds it, since retrying sooner than the server requested + // would defeat the purpose of backpressure signalling. The overall exporter + // timeout remains the final bound. + let mut delay_cap = policy.max_delay; + + loop { + match operation().await { + Ok(result) => return Ok(result), + Err(err) => { + // Check time budget before deciding to retry + let elapsed = start.elapsed(); + if elapsed >= deadline { + otel_warn!(name: "Export.Failed.DeadlineExceeded", + operation = operation_name, + retries = attempt, + elapsed_ms = elapsed.as_millis(), + message = "OTLP export deadline exceeded - telemetry data will be lost" + ); + return Err(err); + } + + let error_type = error_classifier(&err); + + match error_type { + RetryErrorType::NonRetryable => { + otel_warn!(name: "Export.Failed.NonRetryable", + operation = operation_name, + message = "OTLP export failed with non-retryable error - telemetry data will be lost" + ); + return Err(err); + } + RetryErrorType::Retryable if attempt < policy.max_retries => { + attempt += 1; + let jitter = generate_jitter(policy.max_jitter); + let sleep_duration = delay.saturating_add(jitter).min(delay_cap); + + // If the backoff delay would consume all remaining budget, fail now + let remaining = deadline.saturating_sub(start.elapsed()); + if sleep_duration >= remaining { + otel_warn!(name: "Export.Failed.DeadlineExceeded", + operation = operation_name, + retries = attempt, + message = "OTLP export deadline exceeded during backoff - telemetry data will be lost" + ); + return Err(err); + } + + otel_debug!(name: "Export.InProgress.Retrying", + operation = operation_name, + attempt = attempt, + delay_ms = sleep_duration.as_millis(), + message = "OTLP export failed with retryable error - retrying" + ); + sleep_for(sleep_duration).await; + delay = delay.saturating_mul(2).min(delay_cap); + } + RetryErrorType::Throttled(server_delay) if attempt < policy.max_retries => { + attempt += 1; + let capped_server_delay = server_delay.min(MAX_THROTTLE_DURATION); + + // RetryInfo is the base for subsequent exponential backoff. A later + // RetryInfo may extend, but must not shorten, the current backoff. + // When the server delay exceeds the configured max, allow backoff + // to grow up to MAX_THROTTLE_DURATION so repeated failures + // produce increasing delays rather than getting stuck. + if capped_server_delay > delay_cap { + delay_cap = MAX_THROTTLE_DURATION; + } + if !capped_server_delay.is_zero() { + delay = delay.max(capped_server_delay); + } + + let jitter = generate_jitter(policy.max_jitter); + let sleep_duration = delay.saturating_add(jitter).min(delay_cap); + + // If the throttle delay would consume all remaining budget, fail now + let remaining = deadline.saturating_sub(start.elapsed()); + if sleep_duration >= remaining { + otel_warn!(name: "Export.Failed.DeadlineExceeded", + operation = operation_name, + retries = attempt, + message = "OTLP export deadline exceeded during throttle - telemetry data will be lost" + ); + return Err(err); + } + + otel_info!(name: "Export.InProgress.Throttled", + operation = operation_name, + attempt = attempt, + delay_ms = sleep_duration.as_millis(), + server_requested_ms = capped_server_delay.as_millis(), + message = "OTLP export throttled by OTLP endpoint - delaying and retrying" + ); + sleep_for(sleep_duration).await; + delay = delay.saturating_mul(2).min(delay_cap); + } + _ => { + // Max retries reached + otel_warn!(name: "Export.Failed.Exhausted", + operation = operation_name, + retries = attempt, + message = "OTLP export exhausted retries - telemetry data will be lost" + ); + return Err(err); + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + fn policy( + max_retries: usize, + initial_delay_ms: u64, + max_delay_ms: u64, + max_jitter_ms: u64, + ) -> RetryPolicy { + RetryPolicy::default() + .with_max_retries(max_retries) + .with_initial_delay(Duration::from_millis(initial_delay_ms)) + .with_max_delay(Duration::from_millis(max_delay_ms)) + .with_max_jitter(Duration::from_millis(max_jitter_ms)) + } + + #[test] + fn test_generate_jitter() { + let max_jitter = Duration::from_millis(100); + let jitter = generate_jitter(max_jitter); + assert!(jitter <= max_jitter); + } + + #[test] + fn test_generate_jitter_handles_max_duration() { + assert!(generate_jitter(Duration::MAX) <= Duration::from_millis(u64::MAX)); + } + + #[test] + fn test_default_retry_policy() { + let policy = RetryPolicy::default(); + assert_eq!(policy.max_retries, 3); + assert_eq!(policy.initial_delay, Duration::from_millis(100)); + assert_eq!(policy.max_delay, Duration::from_millis(1600)); + assert_eq!(policy.max_jitter, Duration::from_millis(100)); + } + + #[test] + fn test_recommended_retry_policy() { + let policy = RetryPolicy::recommended(); + assert_eq!(policy.max_retries, 3); + assert_eq!(policy.initial_delay, Duration::from_millis(100)); + assert_eq!(policy.max_delay, Duration::from_millis(1600)); + assert_eq!(policy.max_jitter, Duration::from_millis(100)); + } + + #[test] + fn test_disabled_retry_policy() { + let policy = RetryPolicy::disabled(); + assert_eq!(policy.max_retries, 0); + assert_eq!(policy.initial_delay, Duration::ZERO); + assert_eq!(policy.max_delay, Duration::ZERO); + assert_eq!(policy.max_jitter, Duration::ZERO); + } + + #[tokio::test] + async fn test_disabled_retry_policy_performs_single_attempt() { + for classification in [ + RetryErrorType::Retryable, + RetryErrorType::Throttled(Duration::from_secs(5)), + ] { + let attempts = AtomicUsize::new(0); + let result = retry_with_backoff( + &RetryPolicy::disabled(), + Duration::from_secs(10), + |_| classification.clone(), + "test_operation", + || { + attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async { Err::<(), _>(()) }) + }, + ) + .await; + + assert_eq!(result, Err(())); + assert_eq!(attempts.load(Ordering::SeqCst), 1); + } + } + + #[test] + fn test_retry_error_type_equality() { + assert_eq!(RetryErrorType::NonRetryable, RetryErrorType::NonRetryable); + assert_eq!(RetryErrorType::Retryable, RetryErrorType::Retryable); + assert_eq!( + RetryErrorType::Throttled(Duration::from_secs(30)), + RetryErrorType::Throttled(Duration::from_secs(30)) + ); + assert_ne!(RetryErrorType::Retryable, RetryErrorType::NonRetryable); + } + + #[tokio::test] + async fn test_retry_success_on_first_attempt() { + let policy = RetryPolicy::default(); + let deadline = Duration::from_secs(10); + + let result = retry_with_backoff( + &policy, + deadline, + |_: &()| RetryErrorType::Retryable, + "test_operation", + || Box::pin(async { Ok::<_, ()>("success") }), + ) + .await; + + assert_eq!(result, Ok("success")); + } + + #[tokio::test] + async fn test_retry_succeeds_after_retries() { + let policy = policy(3, 10, 100, 5); + let deadline = Duration::from_secs(10); + let attempts = AtomicUsize::new(0); + + let result = retry_with_backoff( + &policy, + deadline, + |_: &&str| RetryErrorType::Retryable, + "test_operation", + || { + let attempt = attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async move { + if attempt < 2 { + Err("error") + } else { + Ok("success") + } + }) + }, + ) + .await; + + assert_eq!(result, Ok("success")); + assert_eq!(attempts.load(Ordering::SeqCst), 3); + } + + #[tokio::test] + async fn test_retry_fails_after_max_retries() { + let policy = policy(3, 10, 100, 5); + let deadline = Duration::from_secs(10); + let attempts = AtomicUsize::new(0); + + let result = retry_with_backoff( + &policy, + deadline, + |_: &&str| RetryErrorType::Retryable, + "test_operation", + || { + attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async { Err::<(), _>("error") }) + }, + ) + .await; + + assert_eq!(result, Err("error")); + assert_eq!(attempts.load(Ordering::SeqCst), 4); // initial + 3 retries + } + + #[tokio::test] + async fn test_retry_non_retryable_stops_immediately() { + let policy = RetryPolicy::default(); + let deadline = Duration::from_secs(10); + let attempts = AtomicUsize::new(0); + + let result = retry_with_backoff( + &policy, + deadline, + |_: &()| RetryErrorType::NonRetryable, + "test_operation", + || { + attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async { Err::<(), _>(()) }) + }, + ) + .await; + + assert!(result.is_err()); + assert_eq!(attempts.load(Ordering::SeqCst), 1); + } + + // -- Throttle/backoff interaction tests -- + // + // These tests form a progression: server delay within max_delay_ms (the + // easy case where the configured cap doesn't constrain growth), then + // server delay exceeding max_delay_ms (the case where the cap must be + // lifted to MAX_THROTTLE_DURATION to allow continued growth). + + #[tokio::test] + async fn test_throttle_seeds_backoff_within_max() { + // Server delay (50ms) is below max_delay_ms (5000ms), so the + // configured cap never constrains growth. Verifies the basic + // mechanism: server delay raises the backoff base from initial. + let policy = policy(2, 10, 5000, 0); + let deadline = Duration::from_secs(10); + let attempts = AtomicUsize::new(0); + + let start = Instant::now(); + let result = retry_with_backoff( + &policy, + deadline, + |_: &()| RetryErrorType::Throttled(Duration::from_millis(50)), + "test_operation", + || { + let attempt = attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async move { + if attempt < 1 { + Err(()) + } else { + Ok("success") + } + }) + }, + ) + .await; + + let elapsed = start.elapsed(); + assert_eq!(result, Ok("success")); + assert_eq!(attempts.load(Ordering::SeqCst), 2); + assert!(elapsed >= Duration::from_millis(50)); + assert!(elapsed < Duration::from_millis(500)); + } + + #[tokio::test] + async fn test_repeated_throttle_doubles_within_max() { + // Repeated throttles (20ms each) with max_delay_ms = 100ms. Server + // delay is within the cap, so doubling works without needing to lift + // the cap: 20ms -> 40ms. Total >= 55ms. + let policy = policy(2, 10, 100, 0); + let attempts = AtomicUsize::new(0); + let start = Instant::now(); + + let result = retry_with_backoff( + &policy, + Duration::from_secs(2), + |_: &usize| RetryErrorType::Throttled(Duration::from_millis(20)), + "test_operation", + || { + let attempt = attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async move { + if attempt < 2 { + Err(attempt) + } else { + Ok("success") + } + }) + }, + ) + .await; + + assert_eq!(result, Ok("success")); + assert_eq!(attempts.load(Ordering::SeqCst), 3); + assert!(start.elapsed() >= Duration::from_millis(55)); + } + + #[tokio::test] + async fn test_retryable_after_throttle_continues_backoff_within_max() { + // Throttle (20ms) then a retryable error, with max_delay_ms = 100ms. + // Server delay is within the cap, so backoff doubles normally: + // 20ms (throttled) -> 40ms (retryable, doubled from 20ms base). + let policy = policy(2, 10, 100, 0); + let attempts = AtomicUsize::new(0); + let start = Instant::now(); + + let result = retry_with_backoff( + &policy, + Duration::from_secs(2), + |attempt: &usize| { + if *attempt == 0 { + RetryErrorType::Throttled(Duration::from_millis(20)) + } else { + RetryErrorType::Retryable + } + }, + "test_operation", + || { + let attempt = attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async move { + if attempt < 2 { + Err(attempt) + } else { + Ok("success") + } + }) + }, + ) + .await; + + assert_eq!(result, Ok("success")); + assert_eq!(attempts.load(Ordering::SeqCst), 3); + // First: throttled 20ms, second: retryable 40ms (doubled from 20ms base) + assert!(start.elapsed() >= Duration::from_millis(55)); + } + + #[tokio::test] + async fn test_retryable_after_throttle_continues_backoff_exceeding_max() { + // Same shape as the test above, but the server delay (100ms) exceeds + // max_delay_ms (50ms). This is the critical case: the cap must be + // lifted to MAX_THROTTLE_DURATION so that doubling actually grows + // the delay instead of clamping it back to the server value. + let policy = policy(3, 10, 50, 0); // max delay intentionally below server delay + let attempts = AtomicUsize::new(0); + let start = Instant::now(); + + let result = retry_with_backoff( + &policy, + Duration::from_secs(10), + |attempt: &usize| { + if *attempt == 0 { + // Server asks for 100ms, which exceeds max_delay_ms (50ms) + RetryErrorType::Throttled(Duration::from_millis(100)) + } else { + RetryErrorType::Retryable + } + }, + "test_operation", + || { + let attempt = attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async move { + if attempt < 3 { + Err(attempt) + } else { + Ok("success") + } + }) + }, + ) + .await; + + assert_eq!(result, Ok("success")); + assert_eq!(attempts.load(Ordering::SeqCst), 4); + // Attempt 0 fails Throttled(100ms): delay = max(10ms, 100ms) = 100ms, + // sleep 100ms, delay -> 200ms (doubled, NOT capped back to 100ms) + // Attempt 1 fails Retryable: sleep 200ms, delay -> 400ms + // Attempt 2 fails Retryable: sleep 400ms, delay -> 800ms + // Attempt 3 succeeds + // Total: 100 + 200 + 400 = 700ms + let elapsed = start.elapsed(); + assert!( + elapsed >= Duration::from_millis(680), + "Expected >= 680ms (backoff should grow beyond server delay), got {:?}", + elapsed + ); + assert!( + elapsed < Duration::from_millis(1500), + "Expected < 1500ms, got {:?}", + elapsed + ); + } + + #[tokio::test] + async fn test_throttle_after_normal_backoff_does_not_reduce_delay() { + let policy = policy(3, 50, 500, 0); + let attempts = AtomicUsize::new(0); + let start = Instant::now(); + + let result = retry_with_backoff( + &policy, + Duration::from_secs(5), + |attempt: &usize| { + if *attempt == 1 { + // Server asks for 20ms, but backoff has already grown to 100ms + RetryErrorType::Throttled(Duration::from_millis(20)) + } else { + RetryErrorType::Retryable + } + }, + "test_operation", + || { + let attempt = attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async move { + if attempt < 3 { + Err(attempt) + } else { + Ok("success") + } + }) + }, + ) + .await; + + assert_eq!(result, Ok("success")); + assert_eq!(attempts.load(Ordering::SeqCst), 4); + // Attempt 0 fails Retryable: sleep 50ms, delay -> 100ms + // Attempt 1 fails Throttled(20ms): delay = max(100ms, 20ms) = 100ms, sleep 100ms, delay -> 200ms + // Attempt 2 fails Retryable: sleep 200ms, delay -> 400ms + // Attempt 3 succeeds + // Total: 50 + 100 + 200 = 350ms + assert!(start.elapsed() >= Duration::from_millis(340)); + } + + #[tokio::test] + async fn test_retry_deadline_exceeded() { + let policy = policy(100, 50, 200, 0); // many retries allowed + // Very short deadline + let deadline = Duration::from_millis(120); + let attempts = AtomicUsize::new(0); + + let start = Instant::now(); + let result = retry_with_backoff( + &policy, + deadline, + |_: &()| RetryErrorType::Retryable, + "test_operation", + || { + attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async { Err::<(), _>(()) }) + }, + ) + .await; + + let elapsed = start.elapsed(); + assert!(result.is_err()); + // Should have stopped before exhausting 100 retries due to deadline + assert!(attempts.load(Ordering::SeqCst) < 10); + // Shouldn't have taken much longer than the deadline + assert!(elapsed < Duration::from_millis(500)); + } + + #[tokio::test] + async fn test_retry_throttle_capped_at_max() { + let policy = policy(2, 10, 100, 0); + let attempts = AtomicUsize::new(0); + + let start = Instant::now(); + // Server requests 120s delay (exceeds MAX_THROTTLE_DURATION of 30s) + // But deadline is short so we don't wait 30s in tests + let short_deadline = Duration::from_millis(200); + let result = retry_with_backoff( + &policy, + short_deadline, + |_: &()| RetryErrorType::Throttled(Duration::from_secs(120)), + "test_operation", + || { + attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async { Err::<(), _>(()) }) + }, + ) + .await; + + let elapsed = start.elapsed(); + assert!(result.is_err()); + // Should be capped by the deadline, not sleep for 120s or even 30s + assert!(elapsed < Duration::from_millis(500)); + } + + // Tests exercising the dedicated-thread (std::thread::sleep) path. + // These use futures_executor::block_on with no Tokio runtime present. + + #[test] + fn test_retry_succeeds_after_retries_no_tokio() { + let policy = policy(3, 10, 100, 5); + let deadline = Duration::from_secs(10); + let attempts = AtomicUsize::new(0); + + let start = Instant::now(); + let result = futures_executor::block_on(retry_with_backoff( + &policy, + deadline, + |_: &&str| RetryErrorType::Retryable, + "test_operation", + || { + let attempt = attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async move { + if attempt < 2 { + Err("error") + } else { + Ok("success") + } + }) + }, + )); + + let elapsed = start.elapsed(); + assert_eq!(result, Ok("success")); + assert_eq!(attempts.load(Ordering::SeqCst), 3); + // Should have slept at least ~10ms for each of 2 retries + assert!(elapsed >= Duration::from_millis(20)); + } + + #[test] + fn test_retry_deadline_exceeded_no_tokio() { + let policy = policy(100, 50, 200, 0); + let deadline = Duration::from_millis(120); + let attempts = AtomicUsize::new(0); + + let start = Instant::now(); + let result = futures_executor::block_on(retry_with_backoff( + &policy, + deadline, + |_: &()| RetryErrorType::Retryable, + "test_operation", + || { + attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async { Err::<(), _>(()) }) + }, + )); + + let elapsed = start.elapsed(); + assert!(result.is_err()); + // Should have stopped before exhausting 100 retries due to deadline + assert!(attempts.load(Ordering::SeqCst) < 10); + // Shouldn't have taken much longer than the deadline + assert!(elapsed < Duration::from_millis(500)); + } + + #[test] + fn test_retry_throttled_uses_server_delay_no_tokio() { + let policy = policy(2, 10, 5000, 0); + let deadline = Duration::from_secs(10); + let attempts = AtomicUsize::new(0); + + let start = Instant::now(); + let result = futures_executor::block_on(retry_with_backoff( + &policy, + deadline, + |_: &()| RetryErrorType::Throttled(Duration::from_millis(50)), + "test_operation", + || { + let attempt = attempts.fetch_add(1, Ordering::SeqCst); + Box::pin(async move { + if attempt < 1 { + Err(()) + } else { + Ok("success") + } + }) + }, + )); + + let elapsed = start.elapsed(); + assert_eq!(result, Ok("success")); + assert_eq!(attempts.load(Ordering::SeqCst), 2); + // Should have waited ~50ms (the server delay raises it from the 10ms default) + assert!(elapsed >= Duration::from_millis(50)); + assert!(elapsed < Duration::from_millis(500)); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/retry_classification.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/retry_classification.rs new file mode 100644 index 00000000..83cf874f --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/retry_classification.rs @@ -0,0 +1,608 @@ +//! Error classification for OTLP exporters with protocol-specific throttling support. +//! +//! This module provides error classification functions for HTTP and gRPC protocols, +//! supporting server-provided throttling hints like HTTP Retry-After headers and +//! gRPC RetryInfo metadata. + +use crate::retry::RetryErrorType; + +#[cfg(feature = "grpc-tonic")] +use tonic_types::StatusExt; + +/// HTTP-specific error classification with Retry-After header support. +#[cfg(any(feature = "http-proto", feature = "http-json"))] +pub(crate) mod http { + use super::*; + use std::time::Duration; + + /// Classifies HTTP errors based on status code and headers. + /// + /// # Arguments + /// * `status_code` - HTTP status code + /// * `retry_after_header` - Value of the Retry-After header, if present + /// + /// # Retry-After Header Formats + /// * Seconds: "120" + /// * HTTP Date: "Fri, 31 Dec 1999 23:59:59 GMT" + pub(crate) fn classify_http_error( + status_code: u16, + retry_after_header: Option<&str>, + ) -> RetryErrorType { + match status_code { + // OTLP/HTTP throttling responses may provide an explicit retry delay. + 429 | 503 => { + if let Some(retry_after) = retry_after_header { + if let Some(duration) = parse_retry_after(retry_after) { + return RetryErrorType::Throttled(duration); + } + } + RetryErrorType::Retryable + } + // Other retryable response codes defined by OTLP/HTTP. + 502 | 504 => RetryErrorType::Retryable, + // The HTTP exporter uses status 0 for failures without a response. + 0 => RetryErrorType::Retryable, + // All other HTTP response status codes must not be retried. + _ => RetryErrorType::NonRetryable, + } + } + + /// Parses the Retry-After header value. + /// + /// Supports both formats: + /// - Delay seconds: "120" + /// - HTTP date: "Fri, 31 Dec 1999 23:59:59 GMT" + /// + /// Returns None if parsing fails or delay is unreasonable. + fn parse_retry_after(retry_after: &str) -> Option { + // Try parsing as seconds first + if let Ok(seconds) = retry_after.trim().parse::() { + // Cap at 10 minutes. TODO - what's sensible here? + let capped_seconds = seconds.min(600); + return Some(Duration::from_secs(capped_seconds)); + } + + // Try parsing as HTTP date (requires httpdate crate) + #[cfg(feature = "httpdate")] + if let Ok(delay_seconds) = parse_http_date_to_delay(retry_after) { + // Cap at 10 minutes. TODO - what's sensible here? + let capped_seconds = delay_seconds.min(600); + return Some(Duration::from_secs(capped_seconds)); + } + + None + } + + /// Parses HTTP date format and returns delay in seconds from now. + #[cfg(feature = "httpdate")] + fn parse_http_date_to_delay(date_str: &str) -> Result { + use std::time::SystemTime; + + // Try parse the date; if we fail, propagate an () error up to the caller. + let target_time = httpdate::parse_http_date(date_str).map_err(|_| ())?; + + let now = SystemTime::now(); + let delay = target_time + .duration_since(now) + .unwrap_or(std::time::Duration::ZERO); + Ok(delay.as_secs()) + } +} + +/// gRPC-specific error classification with RetryInfo support. +#[cfg(feature = "grpc-tonic")] +pub(crate) mod grpc { + use super::*; + + /// Classifies a tonic::Status error + #[cfg(feature = "grpc-tonic")] + pub(crate) fn classify_tonic_status(status: &tonic::Status) -> RetryErrorType { + // Use tonic-types to extract RetryInfo - this is the proper way! + let retry_delay = status + .get_details_retry_info() + .and_then(|retry_info| retry_info.retry_delay); + + classify_grpc_error(status.code(), retry_delay) + } + + /// Classifies gRPC errors based on status code and metadata. + /// + /// Implements the OpenTelemetry OTLP specification for error handling: + /// https://opentelemetry.io/docs/specs/otlp/ + /// https://github.com/open-telemetry/opentelemetry-proto/blob/main/docs/specification.md#failures + /// + /// # Arguments + /// * `grpc_code` - gRPC status code as tonic::Code enum + /// * `retry_delay` - Parsed retry delay from RetryInfo metadata, if present + fn classify_grpc_error( + grpc_code: tonic::Code, + retry_delay: Option, + ) -> RetryErrorType { + match grpc_code { + // RESOURCE_EXHAUSTED: Special case per OTLP spec + // Retryable only if server provides RetryInfo indicating recovery is possible + tonic::Code::ResourceExhausted => { + if let Some(delay) = retry_delay { + // Server signals recovery is possible - use throttled retry + // Cap at 10 minutes. TODO - what's sensible here? + return RetryErrorType::Throttled( + delay.min(std::time::Duration::from_secs(600)), + ); + } + // No RetryInfo - treat as non-retryable per OTLP spec + RetryErrorType::NonRetryable + } + + // UNAVAILABLE may include RetryInfo to signal an explicit throttle delay. + tonic::Code::Unavailable => match retry_delay.filter(|delay| !delay.is_zero()) { + Some(delay) => { + RetryErrorType::Throttled(delay.min(std::time::Duration::from_secs(600))) + } + None => RetryErrorType::Retryable, + }, + + // Retryable errors per OTLP specification + tonic::Code::Cancelled + | tonic::Code::DeadlineExceeded + | tonic::Code::Aborted + | tonic::Code::OutOfRange + | tonic::Code::DataLoss => RetryErrorType::Retryable, + + // Non-retryable errors per OTLP specification + tonic::Code::Unknown + | tonic::Code::InvalidArgument + | tonic::Code::NotFound + | tonic::Code::AlreadyExists + | tonic::Code::PermissionDenied + | tonic::Code::FailedPrecondition + | tonic::Code::Unimplemented + | tonic::Code::Internal + | tonic::Code::Unauthenticated => RetryErrorType::NonRetryable, + + // OK should never reach here in error scenarios, but handle gracefully + tonic::Code::Ok => RetryErrorType::NonRetryable, + } + } +} + +#[cfg(test)] +mod tests { + // Tests for HTTP error classification + + #[cfg(any(feature = "http-proto", feature = "http-json"))] + mod http_tests { + use crate::retry::RetryErrorType; + use crate::retry_classification::http::*; + use std::time::Duration; + + #[test] + fn test_http_throttling_responses_with_retry_after_seconds() { + for status_code in [429, 503] { + let result = classify_http_error(status_code, Some("30")); + assert_eq!(result, RetryErrorType::Throttled(Duration::from_secs(30))); + } + } + + #[test] + fn test_http_throttling_responses_with_large_retry_after_capped() { + for status_code in [429, 503] { + let result = classify_http_error(status_code, Some("900")); // 15 minutes + assert_eq!( + result, + RetryErrorType::Throttled(std::time::Duration::from_secs(600)) + ); // Capped at 10 minutes + } + } + + #[test] + fn test_http_throttling_responses_with_invalid_retry_after() { + for status_code in [429, 503] { + let result = classify_http_error(status_code, Some("invalid")); + assert_eq!(result, RetryErrorType::Retryable); // Fallback + } + } + + #[test] + fn test_http_throttling_responses_without_retry_after() { + for status_code in [429, 503] { + let result = classify_http_error(status_code, None); + assert_eq!(result, RetryErrorType::Retryable); // Fallback + } + } + + #[test] + fn test_http_retryable_response_codes() { + assert_eq!(classify_http_error(502, None), RetryErrorType::Retryable); + assert_eq!(classify_http_error(503, None), RetryErrorType::Retryable); + assert_eq!(classify_http_error(504, None), RetryErrorType::Retryable); + } + + #[test] + fn test_http_non_retryable_response_codes() { + for status_code in [400, 401, 403, 404, 408, 499, 500, 501, 505, 599] { + assert_eq!( + classify_http_error(status_code, None), + RetryErrorType::NonRetryable + ); + } + } + + #[test] + fn test_http_network_error_is_retryable() { + assert_eq!(classify_http_error(0, None), RetryErrorType::Retryable); + } + + #[test] + #[cfg(feature = "httpdate")] + fn test_http_throttling_responses_with_retry_after_valid_date() { + use std::time::SystemTime; + + // Create a time 30 seconds in the future + let future_time = SystemTime::now() + Duration::from_secs(30); + let date_str = httpdate::fmt_http_date(future_time); + for status_code in [429, 503] { + let result = classify_http_error(status_code, Some(&date_str)); + match result { + RetryErrorType::Throttled(duration) => { + let secs = duration.as_secs(); + assert!( + (29..=30).contains(&secs), + "Expected ~30 seconds, got {}", + secs + ); + } + _ => panic!("Expected Throttled, got {:?}", result), + } + } + } + + #[test] + #[cfg(feature = "httpdate")] + fn test_http_throttling_responses_with_retry_after_invalid_date() { + for status_code in [429, 503] { + let result = classify_http_error(status_code, Some("Not a valid date")); + assert_eq!(result, RetryErrorType::Retryable); // Falls back to retryable + } + } + + #[test] + #[cfg(feature = "httpdate")] + fn test_http_throttling_responses_with_retry_after_malformed_date() { + for status_code in [429, 503] { + let result = + classify_http_error(status_code, Some("Sun, 99 Nov 9999 99:99:99 GMT")); + assert_eq!(result, RetryErrorType::Retryable); // Falls back to retryable + } + } + } + + // Tests for gRPC error classification using public interface + #[cfg(feature = "grpc-tonic")] + mod grpc_tests { + use crate::retry::RetryErrorType; + use crate::retry_classification::grpc::classify_tonic_status; + use tonic_types::{ErrorDetails, StatusExt}; + + #[test] + fn test_grpc_resource_exhausted_with_retry_info() { + let error_details = + ErrorDetails::with_retry_info(Some(std::time::Duration::from_secs(45))); + let status = tonic::Status::with_error_details( + tonic::Code::ResourceExhausted, + "rate limited", + error_details, + ); + let result = classify_tonic_status(&status); + assert_eq!( + result, + RetryErrorType::Throttled(std::time::Duration::from_secs(45)) + ); + } + + #[test] + fn test_grpc_resource_exhausted_with_large_retry_info_capped() { + let error_details = + ErrorDetails::with_retry_info(Some(std::time::Duration::from_secs(900))); // 15 minutes + let status = tonic::Status::with_error_details( + tonic::Code::ResourceExhausted, + "rate limited", + error_details, + ); + let result = classify_tonic_status(&status); + assert_eq!( + result, + RetryErrorType::Throttled(std::time::Duration::from_secs(600)) + ); // Capped at 10 minutes + } + + #[test] + fn test_grpc_resource_exhausted_without_retry_info() { + let status = tonic::Status::new(tonic::Code::ResourceExhausted, "rate limited"); + let result = classify_tonic_status(&status); + // Per OTLP spec: RESOURCE_EXHAUSTED without RetryInfo is non-retryable + assert_eq!(result, RetryErrorType::NonRetryable); + } + + #[test] + fn test_grpc_unavailable_with_retry_info() { + let error_details = + ErrorDetails::with_retry_info(Some(std::time::Duration::from_secs(45))); + let status = tonic::Status::with_error_details( + tonic::Code::Unavailable, + "service unavailable", + error_details, + ); + + assert_eq!( + classify_tonic_status(&status), + RetryErrorType::Throttled(std::time::Duration::from_secs(45)) + ); + } + + #[test] + fn test_grpc_unavailable_with_fractional_retry_info() { + let error_details = + ErrorDetails::with_retry_info(Some(std::time::Duration::from_millis(500))); + let status = tonic::Status::with_error_details( + tonic::Code::Unavailable, + "service unavailable", + error_details, + ); + + assert_eq!( + classify_tonic_status(&status), + RetryErrorType::Throttled(std::time::Duration::from_millis(500)) + ); + } + + #[test] + fn test_grpc_unavailable_with_large_retry_info_capped() { + let error_details = + ErrorDetails::with_retry_info(Some(std::time::Duration::from_secs(900))); + let status = tonic::Status::with_error_details( + tonic::Code::Unavailable, + "service unavailable", + error_details, + ); + + assert_eq!( + classify_tonic_status(&status), + RetryErrorType::Throttled(std::time::Duration::from_secs(600)) + ); + } + + #[test] + fn test_grpc_unavailable_without_positive_retry_info() { + let without_retry_info = + tonic::Status::new(tonic::Code::Unavailable, "service unavailable"); + assert_eq!( + classify_tonic_status(&without_retry_info), + RetryErrorType::Retryable + ); + + let error_details = ErrorDetails::with_retry_info(Some(std::time::Duration::ZERO)); + let zero_retry_info = tonic::Status::with_error_details( + tonic::Code::Unavailable, + "service unavailable", + error_details, + ); + assert_eq!( + classify_tonic_status(&zero_retry_info), + RetryErrorType::Retryable + ); + } + + #[test] + fn test_grpc_retryable_errors() { + // Test all retryable errors per OTLP specification + let cancelled = tonic::Status::new(tonic::Code::Cancelled, "cancelled"); + assert_eq!(classify_tonic_status(&cancelled), RetryErrorType::Retryable); + + let deadline_exceeded = + tonic::Status::new(tonic::Code::DeadlineExceeded, "deadline exceeded"); + assert_eq!( + classify_tonic_status(&deadline_exceeded), + RetryErrorType::Retryable + ); + + let aborted = tonic::Status::new(tonic::Code::Aborted, "aborted"); + assert_eq!(classify_tonic_status(&aborted), RetryErrorType::Retryable); + + let out_of_range = tonic::Status::new(tonic::Code::OutOfRange, "out of range"); + assert_eq!( + classify_tonic_status(&out_of_range), + RetryErrorType::Retryable + ); + + let unavailable = tonic::Status::new(tonic::Code::Unavailable, "unavailable"); + assert_eq!( + classify_tonic_status(&unavailable), + RetryErrorType::Retryable + ); + + let data_loss = tonic::Status::new(tonic::Code::DataLoss, "data loss"); + assert_eq!(classify_tonic_status(&data_loss), RetryErrorType::Retryable); + } + + #[test] + fn test_grpc_non_retryable_errors() { + // Test all non-retryable errors per OTLP specification + let unknown = tonic::Status::new(tonic::Code::Unknown, "unknown"); + assert_eq!( + classify_tonic_status(&unknown), + RetryErrorType::NonRetryable + ); + + let invalid_argument = + tonic::Status::new(tonic::Code::InvalidArgument, "invalid argument"); + assert_eq!( + classify_tonic_status(&invalid_argument), + RetryErrorType::NonRetryable + ); + + let not_found = tonic::Status::new(tonic::Code::NotFound, "not found"); + assert_eq!( + classify_tonic_status(¬_found), + RetryErrorType::NonRetryable + ); + + let already_exists = tonic::Status::new(tonic::Code::AlreadyExists, "already exists"); + assert_eq!( + classify_tonic_status(&already_exists), + RetryErrorType::NonRetryable + ); + + let permission_denied = + tonic::Status::new(tonic::Code::PermissionDenied, "permission denied"); + assert_eq!( + classify_tonic_status(&permission_denied), + RetryErrorType::NonRetryable + ); + + let failed_precondition = + tonic::Status::new(tonic::Code::FailedPrecondition, "failed precondition"); + assert_eq!( + classify_tonic_status(&failed_precondition), + RetryErrorType::NonRetryable + ); + + let unimplemented = tonic::Status::new(tonic::Code::Unimplemented, "unimplemented"); + assert_eq!( + classify_tonic_status(&unimplemented), + RetryErrorType::NonRetryable + ); + + let internal = tonic::Status::new(tonic::Code::Internal, "internal error"); + assert_eq!( + classify_tonic_status(&internal), + RetryErrorType::NonRetryable + ); + + let unauthenticated = + tonic::Status::new(tonic::Code::Unauthenticated, "unauthenticated"); + assert_eq!( + classify_tonic_status(&unauthenticated), + RetryErrorType::NonRetryable + ); + } + + #[test] + fn test_grpc_ok_code_handled() { + // OK status should be handled gracefully (though unlikely in error scenarios) + let ok = tonic::Status::new(tonic::Code::Ok, "success"); + assert_eq!(classify_tonic_status(&ok), RetryErrorType::NonRetryable); + } + + // Tests for tonic-types RetryInfo integration + #[cfg(feature = "grpc-tonic")] + mod retry_info_tests { + use super::*; + use crate::retry_classification::grpc::classify_tonic_status; + use tonic_types::{ErrorDetails, StatusExt}; + + #[test] + fn test_classify_status_with_retry_info() { + // Create a tonic::Status with RetryInfo using proper StatusExt API + let error_details = + ErrorDetails::with_retry_info(Some(std::time::Duration::from_secs(30))); + let status = tonic::Status::with_error_details( + tonic::Code::ResourceExhausted, + "rate limited", + error_details, + ); + + // Test classification + let result = classify_tonic_status(&status); + assert_eq!( + result, + RetryErrorType::Throttled(std::time::Duration::from_secs(30)) + ); + } + + #[test] + fn test_classify_status_with_fractional_retry_info() { + // Create a tonic::Status with fractional seconds RetryInfo + let error_details = + ErrorDetails::with_retry_info(Some(std::time::Duration::from_millis(500))); + let status = tonic::Status::with_error_details( + tonic::Code::ResourceExhausted, + "rate limited", + error_details, + ); + + // Fractional seconds are preserved. + let result = classify_tonic_status(&status); + assert_eq!( + result, + RetryErrorType::Throttled(std::time::Duration::from_millis(500)) + ); + } + + #[test] + fn test_classify_status_without_retry_info() { + // Status with resource_exhausted but no RetryInfo + let status = tonic::Status::new(tonic::Code::ResourceExhausted, "rate limited"); + + // Per OTLP spec: should be non-retryable without RetryInfo + let result = classify_tonic_status(&status); + assert_eq!(result, RetryErrorType::NonRetryable); + } + + #[test] + fn test_classify_status_non_retryable_error() { + // Status with non-retryable error code + let status = tonic::Status::new(tonic::Code::InvalidArgument, "bad request"); + + let result = classify_tonic_status(&status); + assert_eq!(result, RetryErrorType::NonRetryable); + } + + #[test] + fn test_classify_status_retryable_error() { + // Status with retryable error code + let status = tonic::Status::new(tonic::Code::Unavailable, "service unavailable"); + + let result = classify_tonic_status(&status); + assert_eq!(result, RetryErrorType::Retryable); + } + + #[test] + fn test_classify_status_large_retry_delay() { + // Test with large retry delay - should be capped at 10 minutes + let error_details = + ErrorDetails::with_retry_info(Some(std::time::Duration::from_secs(3600))); // 1 hour + let status = tonic::Status::with_error_details( + tonic::Code::ResourceExhausted, + "rate limited", + error_details, + ); + + let result = classify_tonic_status(&status); + // Should be capped at 10 minutes (600 seconds) + assert_eq!( + result, + RetryErrorType::Throttled(std::time::Duration::from_secs(600)) + ); + } + + #[test] + fn test_status_ext_get_details() { + // Test that StatusExt works correctly + let error_details = + ErrorDetails::with_retry_info(Some(std::time::Duration::from_secs(45))); + let status = tonic::Status::with_error_details( + tonic::Code::ResourceExhausted, + "rate limited", + error_details, + ); + + // Direct extraction should work + let extracted = status.get_details_retry_info(); + assert!(extracted.is_some()); + + let retry_delay = extracted.unwrap().retry_delay; + assert_eq!(retry_delay, Some(std::time::Duration::from_secs(45))); + } + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/retry_policy.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/retry_policy.rs new file mode 100644 index 00000000..60dafe26 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/retry_policy.rs @@ -0,0 +1,70 @@ +use std::time::Duration; + +/// Configuration for retry policy. +/// +/// The default is [`RetryPolicy::recommended()`]. Use +/// [`RetryPolicy::disabled()`] to perform a single export attempt without +/// retrying, or customize the recommended behavior with the `with_*` methods. +#[derive(Debug, Clone)] +pub struct RetryPolicy { + pub(crate) max_retries: usize, + pub(crate) initial_delay: Duration, + pub(crate) max_delay: Duration, + pub(crate) max_jitter: Duration, +} + +impl Default for RetryPolicy { + /// Returns the recommended OTLP retry policy. + fn default() -> Self { + Self::recommended() + } +} + +impl RetryPolicy { + /// Returns a retry policy that performs a single export attempt with no retries. + pub fn disabled() -> Self { + Self { + max_retries: 0, + initial_delay: Duration::ZERO, + max_delay: Duration::ZERO, + max_jitter: Duration::ZERO, + } + } + + /// Returns the recommended policy: 3 retries with exponential backoff + /// (100ms initial, 1600ms maximum, and up to 100ms jitter). + pub fn recommended() -> Self { + Self { + max_retries: 3, + initial_delay: Duration::from_millis(100), + max_delay: Duration::from_millis(1600), + max_jitter: Duration::from_millis(100), + } + } + + /// Sets the maximum number of retry attempts after the initial export attempt. + pub fn with_max_retries(mut self, max_retries: usize) -> Self { + self.max_retries = max_retries; + self + } + + /// Sets the delay before the first retry attempt. + pub fn with_initial_delay(mut self, initial_delay: Duration) -> Self { + self.initial_delay = initial_delay; + self + } + + /// Sets the maximum delay between retry attempts. + /// + /// A server-provided throttling delay may raise this limit. + pub fn with_max_delay(mut self, max_delay: Duration) -> Self { + self.max_delay = max_delay; + self + } + + /// Sets the maximum random jitter added to each retry delay. + pub fn with_max_jitter(mut self, max_jitter: Duration) -> Self { + self.max_jitter = max_jitter; + self + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/src/span.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/src/span.rs new file mode 100644 index 00000000..2cb72461 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/src/span.rs @@ -0,0 +1,397 @@ +//! # OTLP - Span Exporter +//! +//! Defines a [SpanExporter] to send trace data via the OpenTelemetry Protocol (OTLP) + +use std::fmt::Debug; + +use opentelemetry_sdk::error::OTelSdkResult; +use opentelemetry_sdk::trace::SpanData; + +use crate::ExporterBuildError; +#[cfg(feature = "grpc-tonic")] +use crate::{ + exporter::tonic::{HasTonicConfig, TonicExporterBuilder}, + TonicExporterBuilderSet, +}; + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +use crate::{ + exporter::http::{HasHttpConfig, HttpExporterBuilder}, + HttpExporterBuilderSet, +}; + +use crate::{exporter::HasExportConfig, NoExporterBuilderSet}; + +/// Target to which the exporter is going to send spans, defaults to https://localhost:4317/v1/traces. +/// Learn about the relationship between this constant and default/metrics/logs at +/// +pub const OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"; +/// Max waiting time for the backend to process each spans batch, defaults to 10s. +pub const OTEL_EXPORTER_OTLP_TRACES_TIMEOUT: &str = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT"; +/// Compression algorithm to use, defaults to none. +pub const OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: &str = "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION"; +/// Key-value pairs to be used as headers associated with gRPC or HTTP requests +/// for sending spans. +/// Example: `k1=v1,k2=v2` +/// Note: this is only supported for HTTP. +pub const OTEL_EXPORTER_OTLP_TRACES_HEADERS: &str = "OTEL_EXPORTER_OTLP_TRACES_HEADERS"; +/// Protocol to use for trace exports. Valid values: `grpc`, `http/protobuf`, `http/json`. +pub const OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"; +/// Whether to disable TLS for gRPC trace exports. +/// Only applies to gRPC; HTTP security is determined by URL scheme. +pub const OTEL_EXPORTER_OTLP_TRACES_INSECURE: &str = "OTEL_EXPORTER_OTLP_TRACES_INSECURE"; + +/// OTLP span exporter builder +#[derive(Debug, Default, Clone)] +pub struct SpanExporterBuilder { + client: C, +} + +impl SpanExporterBuilder { + /// Create a new [SpanExporterBuilder] with default settings. + pub fn new() -> Self { + SpanExporterBuilder::default() + } + + /// With the gRPC Tonic transport. + #[cfg(feature = "grpc-tonic")] + pub fn with_tonic(self) -> SpanExporterBuilder { + SpanExporterBuilder { + client: TonicExporterBuilderSet(TonicExporterBuilder::default()), + } + } + + /// With the HTTP transport. + #[cfg(any(feature = "http-proto", feature = "http-json"))] + pub fn with_http(self) -> SpanExporterBuilder { + SpanExporterBuilder { + client: HttpExporterBuilderSet(HttpExporterBuilder::default()), + } + } + + /// Build the [SpanExporter] with the default transport selected by environment + /// variable or feature flags. + /// + /// The transport is chosen based on: + /// 1. `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` environment variable + /// 2. `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable + /// 3. Enabled features, with priority: `http-json` > `http-proto` > `grpc-tonic` + /// + /// Use [`with_tonic`](Self::with_tonic) or [`with_http`](Self::with_http) to + /// explicitly select a transport and access transport-specific configuration. + #[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] + pub fn build(self) -> Result { + use crate::WithExportConfig; + + let protocol = crate::exporter::resolve_protocol(OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, None); + match protocol { + #[cfg(feature = "grpc-tonic")] + crate::Protocol::Grpc => self.with_tonic().with_protocol(protocol).build(), + #[cfg(feature = "http-proto")] + crate::Protocol::HttpBinary => self.with_http().with_protocol(protocol).build(), + #[cfg(feature = "http-json")] + crate::Protocol::HttpJson => self.with_http().with_protocol(protocol).build(), + } + } +} + +#[cfg(feature = "grpc-tonic")] +impl SpanExporterBuilder { + /// Build the [SpanExporter] with the gRPC Tonic transport. + pub fn build(self) -> Result { + let span_exporter = self.client.0.build_span_exporter()?; + opentelemetry::otel_debug!(name: "SpanExporterBuilt"); + Ok(span_exporter) + } +} + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +impl SpanExporterBuilder { + /// Build the [SpanExporter] with the HTTP transport. + pub fn build(self) -> Result { + let span_exporter = self.client.0.build_span_exporter()?; + Ok(span_exporter) + } +} + +#[cfg(feature = "grpc-tonic")] +impl HasExportConfig for SpanExporterBuilder { + fn export_config(&mut self) -> &mut crate::exporter::ExportConfig { + &mut self.client.0.exporter_config + } +} + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +impl HasExportConfig for SpanExporterBuilder { + fn export_config(&mut self) -> &mut crate::exporter::ExportConfig { + &mut self.client.0.exporter_config + } +} + +#[cfg(feature = "grpc-tonic")] +impl HasTonicConfig for SpanExporterBuilder { + fn tonic_config(&mut self) -> &mut crate::exporter::tonic::TonicConfig { + &mut self.client.0.tonic_config + } +} + +#[cfg(any(feature = "http-proto", feature = "http-json"))] +impl HasHttpConfig for SpanExporterBuilder { + fn http_client_config(&mut self) -> &mut crate::exporter::http::HttpConfig { + &mut self.client.0.http_config + } +} + +/// OTLP exporter that sends tracing data +#[derive(Debug)] +pub struct SpanExporter { + client: SupportedTransportClient, +} + +#[derive(Debug)] +enum SupportedTransportClient { + #[cfg(feature = "grpc-tonic")] + Tonic(crate::exporter::tonic::trace::TonicTracesClient), + #[cfg(any(feature = "http-proto", feature = "http-json"))] + Http(crate::exporter::http::OtlpHttpClient), +} + +impl SpanExporter { + /// Obtain a builder to configure a [SpanExporter]. + pub fn builder() -> SpanExporterBuilder { + SpanExporterBuilder::default() + } + + #[cfg(any(feature = "http-proto", feature = "http-json"))] + pub(crate) fn from_http(client: crate::exporter::http::OtlpHttpClient) -> Self { + SpanExporter { + client: SupportedTransportClient::Http(client), + } + } + + #[cfg(feature = "grpc-tonic")] + pub(crate) fn from_tonic(client: crate::exporter::tonic::trace::TonicTracesClient) -> Self { + SpanExporter { + client: SupportedTransportClient::Tonic(client), + } + } +} + +impl opentelemetry_sdk::trace::SpanExporter for SpanExporter { + async fn export(&self, batch: Vec) -> OTelSdkResult { + match &self.client { + #[cfg(feature = "grpc-tonic")] + SupportedTransportClient::Tonic(client) => client.export(batch).await, + #[cfg(any(feature = "http-proto", feature = "http-json"))] + SupportedTransportClient::Http(client) => client.export(batch).await, + } + } + + fn set_resource(&mut self, resource: &opentelemetry_sdk::Resource) { + match &mut self.client { + #[cfg(feature = "grpc-tonic")] + SupportedTransportClient::Tonic(client) => client.set_resource(resource), + #[cfg(any(feature = "http-proto", feature = "http-json"))] + SupportedTransportClient::Http(client) => client.set_resource(resource), + } + } +} + +#[cfg(test)] +#[cfg(any(feature = "grpc-tonic", feature = "http-proto", feature = "http-json"))] +mod tests { + use crate::SpanExporter; + + // Uses a tokio runtime because, under a gRPC-only build, the auto-selected + // tonic transport needs an active reactor to construct its channel. + #[tokio::test] + async fn build_with_default_transport() { + let result = SpanExporter::builder().build(); + assert!(result.is_ok(), "build() should succeed: {:?}", result.err()); + } + + /// Verifies that `SpanExporter::builder().build()` respects the + /// signal-specific `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` env var + /// when selecting the transport. + #[cfg(all(feature = "grpc-tonic", feature = "http-proto"))] + #[test] + fn build_auto_select_respects_signal_protocol_env() { + // Tokio runtime is required because tonic's channel setup needs an active reactor. + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + // With signal-specific env var set to grpc, build() should pick tonic + temp_env::with_vars( + vec![ + (super::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, Some("grpc")), + (crate::OTEL_EXPORTER_OTLP_PROTOCOL, None::<&str>), + ], + || { + let result = SpanExporter::builder().build(); + assert!( + result.is_ok(), + "build() with TRACES_PROTOCOL=grpc should succeed: {:?}", + result.err() + ); + }, + ); + + // With signal-specific env var set to http/protobuf, build() should pick HTTP + temp_env::with_vars( + vec![ + ( + super::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + Some("http/protobuf"), + ), + (crate::OTEL_EXPORTER_OTLP_PROTOCOL, None::<&str>), + ], + || { + let result = SpanExporter::builder().build(); + assert!( + result.is_ok(), + "build() with TRACES_PROTOCOL=http/protobuf should succeed: {:?}", + result.err() + ); + }, + ); + }); + } + + /// Verifies that signal-specific protocol env var takes precedence + /// over the generic `OTEL_EXPORTER_OTLP_PROTOCOL` for transport selection. + #[cfg(all(feature = "grpc-tonic", feature = "http-proto"))] + #[test] + fn build_auto_select_signal_env_overrides_generic() { + // Tokio runtime is required because tonic's channel setup needs an active reactor. + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + temp_env::with_vars( + vec![ + (super::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, Some("grpc")), + (crate::OTEL_EXPORTER_OTLP_PROTOCOL, Some("http/protobuf")), + ], + || { + let result = SpanExporter::builder().build(); + assert!( + result.is_ok(), + "signal-specific protocol should override generic: {:?}", + result.err() + ); + }, + ); + }); + } + + /// Invalid enum values are warned about and ignored, so resolution + /// continues to the next source in the precedence chain. + #[cfg(all(feature = "grpc-tonic", feature = "http-proto"))] + #[test] + fn build_auto_select_ignores_invalid_protocol_env_values() { + let rt = tokio::runtime::Runtime::new().unwrap(); + let _guard = rt.enter(); + for (signal, generic, expect_grpc) in [ + ("not-a-protocol", "GRPC", true), + ("HTTP/PROTOBUF", "grpc", false), + ("not-a-protocol", "also-not-a-protocol", false), + ] { + temp_env::with_vars( + [ + (super::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, Some(signal)), + (crate::OTEL_EXPORTER_OTLP_PROTOCOL, Some(generic)), + ], + || { + let exporter = SpanExporter::builder().build().unwrap(); + assert_eq!( + matches!(exporter.client, super::SupportedTransportClient::Tonic(_)), + expect_grpc, + "signal={signal}, generic={generic}" + ); + }, + ); + } + } + + #[cfg(all(feature = "http-proto", not(feature = "grpc-tonic")))] + #[test] + fn build_auto_select_ignores_protocol_with_disabled_feature() { + temp_env::with_vars( + [ + (super::OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, None::<&str>), + (crate::OTEL_EXPORTER_OTLP_PROTOCOL, Some("grpc")), + ], + || { + let exporter = SpanExporter::builder().build().unwrap(); + assert!(matches!( + exporter.client, + super::SupportedTransportClient::Http(_) + )); + }, + ); + } + + #[cfg(all( + any(feature = "http-proto", feature = "http-json"), + feature = "gzip-http" + ))] + #[test] + fn build_http_succeeds_with_invalid_compression_env_value() { + temp_env::with_vars( + [ + ( + super::OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + Some("not-compression"), + ), + (crate::OTEL_EXPORTER_OTLP_COMPRESSION, Some("GZIP")), + ], + || { + let result = SpanExporter::builder().with_http().build(); + assert!( + result.is_ok(), + "invalid signal compression should fall back to generic: {:?}", + result.err() + ); + }, + ); + } + + /// Verifies that explicitly selecting tonic transport with an HTTP protocol + /// via `.with_protocol()` returns an error. + #[cfg(all(feature = "grpc-tonic", feature = "http-proto"))] + #[test] + fn build_tonic_with_http_protocol_returns_error() { + use crate::exporter::WithExportConfig; + + // Tokio runtime is required because tonic's channel setup needs an active reactor. + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let result = SpanExporter::builder() + .with_tonic() + .with_protocol(crate::Protocol::HttpBinary) + .build(); + assert!(result.is_err(), "tonic with HTTP protocol should fail"); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("not compatible with gRPC transport"), + "error should mention transport incompatibility, got: {err}" + ); + }); + } + + /// Verifies that explicitly selecting HTTP transport with a gRPC protocol + /// via `.with_protocol()` returns an error. + #[cfg(all(feature = "grpc-tonic", feature = "http-proto"))] + #[test] + fn build_http_with_grpc_protocol_returns_error() { + use crate::exporter::WithExportConfig; + + let result = SpanExporter::builder() + .with_http() + .with_protocol(crate::Protocol::Grpc) + .build(); + assert!(result.is_err(), "HTTP with gRPC protocol should fail"); + let err = result.unwrap_err().to_string(); + assert!( + err.contains("not compatible with HTTP transport"), + "error should mention transport incompatibility, got: {err}" + ); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-otlp/tests/smoke.rs b/vendor/opentelemetry-rust/opentelemetry-otlp/tests/smoke.rs new file mode 100644 index 00000000..466e1317 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-otlp/tests/smoke.rs @@ -0,0 +1,243 @@ +use futures_util::StreamExt; +use opentelemetry::global; +use opentelemetry::trace::{Span, SpanKind, Tracer}; +use opentelemetry_otlp::{WithExportConfig, WithTonicConfig}; +use opentelemetry_proto::tonic::collector::trace::v1::{ + trace_service_server::{TraceService, TraceServiceServer}, + ExportTraceServiceRequest, ExportTraceServiceResponse, +}; +use std::{net::SocketAddr, sync::Mutex}; +use tokio::sync::mpsc; +use tokio_stream::wrappers::TcpListenerStream; +#[cfg(feature = "gzip-tonic")] +use tonic::codec::CompressionEncoding; + +struct MockServer { + tx: Mutex>, +} + +impl MockServer { + pub fn new(tx: mpsc::Sender) -> Self { + Self { tx: Mutex::new(tx) } + } +} + +#[tonic::async_trait] +impl TraceService for MockServer { + async fn export( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + println!("Sending request into channel..."); + // assert we have required metadata key + assert_eq!( + request.metadata().get("x-header-key"), + Some(&("header-value".parse().unwrap())) + ); + self.tx + .lock() + .unwrap() + .try_send(request.into_inner()) + .expect("Channel full"); + Ok(tonic::Response::new(ExportTraceServiceResponse { + partial_success: None, + })) + } +} + +async fn setup() -> (SocketAddr, mpsc::Receiver) { + let addr: SocketAddr = "[::1]:0".parse().unwrap(); + let listener = tokio::net::TcpListener::bind(addr) + .await + .expect("failed to bind"); + let addr = listener.local_addr().unwrap(); + let stream = TcpListenerStream::new(listener).map(|s| { + if let Ok(ref s) = s { + println!("Got new conn at {}", s.peer_addr().unwrap()); + } + s + }); + + let (req_tx, req_rx) = mpsc::channel(10); + #[cfg(feature = "gzip-tonic")] + let service = TraceServiceServer::new(MockServer::new(req_tx)) + .accept_compressed(CompressionEncoding::Gzip); + #[cfg(not(feature = "gzip-tonic"))] + let service = TraceServiceServer::new(MockServer::new(req_tx)); + tokio::task::spawn(async move { + tonic::transport::Server::builder() + .add_service(service) + .serve_with_incoming(stream) + .await + .expect("Server failed"); + }); + (addr, req_rx) +} + +#[tokio::test(flavor = "multi_thread")] +async fn smoke_tracer() { + println!("Starting server setup..."); + let (addr, mut req_rx) = setup().await; + + { + println!("Installing tracer provider..."); + let mut metadata = tonic::metadata::MetadataMap::new(); + metadata.insert("x-header-key", "header-value".parse().unwrap()); + let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() + .with_batch_exporter( + #[cfg(feature = "gzip-tonic")] + opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_compression(opentelemetry_otlp::Compression::Gzip) + .with_endpoint(format!("http://{addr}")) + .with_metadata(metadata) + .build() + .expect("gzip-tonic SpanExporter failed to build"), + #[cfg(not(feature = "gzip-tonic"))] + opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint(format!("http://{}", addr)) + .with_metadata(metadata) + .build() + .expect("NON gzip-tonic SpanExporter failed to build"), + ) + .build(); + + global::set_tracer_provider(tracer_provider.clone()); + + let tracer = global::tracer("smoke"); + + println!("Sending span..."); + let mut span = tracer + .span_builder("my-test-span") + .with_kind(SpanKind::Server) + .start(&tracer); + span.add_event("my-test-event", vec![]); + span.end(); + + tracer_provider + .shutdown() + .expect("tracer_provider should shutdown successfully"); + } + + println!("Waiting for request..."); + let req = req_rx.recv().await.expect("missing export request"); + let first_span = req + .resource_spans + .first() + .unwrap() + .scope_spans + .first() + .unwrap() + .spans + .first() + .unwrap(); + assert_eq!("my-test-span", first_span.name); + let first_event = first_span.events.first().unwrap(); + assert_eq!("my-test-event", first_event.name); +} + +// Smoke test for partial success handling. We set everything up and push some +// partial success messages through with the expectation that it doesn't all fall apart. +#[tokio::test(flavor = "multi_thread")] +async fn partial_success_handling() { + use opentelemetry_proto::tonic::collector::trace::v1::ExportTracePartialSuccess; + + struct PartialSuccessServer { + tx: Mutex>, + } + + impl PartialSuccessServer { + pub fn new(tx: mpsc::Sender) -> Self { + Self { tx: Mutex::new(tx) } + } + } + + #[tonic::async_trait] + impl TraceService for PartialSuccessServer { + async fn export( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + self.tx + .lock() + .unwrap() + .try_send(request.into_inner()) + .expect("Channel full"); + + // Return partial success response + Ok(tonic::Response::new(ExportTraceServiceResponse { + partial_success: Some(ExportTracePartialSuccess { + rejected_spans: 5, + error_message: "Some spans were rejected due to invalid data".to_string(), + }), + })) + } + } + + println!("Starting partial success test..."); + let addr: SocketAddr = "[::1]:0".parse().unwrap(); + let listener = tokio::net::TcpListener::bind(addr) + .await + .expect("failed to bind"); + let addr = listener.local_addr().unwrap(); + let stream = TcpListenerStream::new(listener).map(|s| { + if let Ok(ref s) = s { + println!("Got new conn at {}", s.peer_addr().unwrap()); + } + s + }); + + let (req_tx, mut req_rx) = mpsc::channel(10); + let service = TraceServiceServer::new(PartialSuccessServer::new(req_tx)); + tokio::task::spawn(async move { + tonic::transport::Server::builder() + .add_service(service) + .serve_with_incoming(stream) + .await + .expect("Server failed"); + }); + + { + println!("Installing tracer provider for partial success test..."); + let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() + .with_batch_exporter( + opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint(format!("http://{}", addr)) + .build() + .expect("SpanExporter failed to build"), + ) + .build(); + + global::set_tracer_provider(tracer_provider.clone()); + let tracer = global::tracer("partial-success-test"); + + println!("Sending span..."); + let mut span = tracer + .span_builder("test-span-partial-success") + .with_kind(SpanKind::Server) + .start(&tracer); + span.end(); + + // Export should succeed even with partial success response + tracer_provider + .shutdown() + .expect("tracer_provider should shutdown successfully despite partial success"); + } + + println!("Waiting for request..."); + let req = req_rx.recv().await.expect("missing export request"); + let first_span = req + .resource_spans + .first() + .unwrap() + .scope_spans + .first() + .unwrap() + .spans + .first() + .unwrap(); + assert_eq!("test-span-partial-success", first_span.name); + println!("Partial success test completed successfully"); +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/CHANGELOG.md b/vendor/opentelemetry-rust/opentelemetry-proto/CHANGELOG.md new file mode 100644 index 00000000..f2657024 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/CHANGELOG.md @@ -0,0 +1,162 @@ +# Changelog + +## vNext + +## 0.33.0 + +Released 2026-Sep-18 + +- **Bug fix**: Keep OTLP logs with the same target but different scope versions, + attributes, or schema URLs in separate groups. Target still overrides the scope + name. Export each scope's schema URL instead of the resource's schema URL. + +- **Bug fix**: Accept empty `AnyValue` objects in OTLP/JSON payloads instead of rejecting the entire request. +- **Bug fix**: Accept omitted resource fields in empty OTLP/JSON collector requests. +- **Bug fix**: Accept `null` fields in OTLP/JSON `AnyValue` objects as unset. +- **Bug fix**: Accept OTLP/JSON partial-success responses when protobuf default fields are omitted. +- **Performance**: `group_spans_by_resource_and_scope` no longer clones every `SpanData`. The batch is owned by the function, so spans are now moved into the proto conversion instead of being grouped by reference and cloned. This cuts the transform time of a 512-span batch roughly in half. ([#3652](https://github.com/open-telemetry/opentelemetry-rust/pull/3652)) + +## 0.32.0 + +Released 2026-May-08 + +- Update proto definitions to v1.10.0. +- Updated `schemars` dependency to version 1.0.0. +- **Bug fix**: `InstrumentationScope` version and attributes are now preserved when logs have a target set. Previously, setting a log target would discard the scope's version and attributes. ([#3276](https://github.com/open-telemetry/opentelemetry-rust/issues/3276)) + +## 0.31.0 + +Released 2025-Sep-25 + +- **Feature**: Add span flags support for `isRemote` property in OTLP trace transformation ([#3153](https://github.com/open-telemetry/opentelemetry-rust/pull/3153)) +- Updated span and link transformations to properly set flags field (0x100 for local, 0x300 for remote) +- Update proto definitions to v1.8.0. +- Added Rust generated protos for profiles collector. [#3077](https://github.com/open-telemetry/opentelemetry-rust/pull/3077) +- **Breaking change**: package opentelemetry_proto::tonic::profiles::v1 renamed to opentelemetry_proto::tonic::profiles::v1development. [#3077](https://github.com/open-telemetry/opentelemetry-rust/pull/3077) +- Update `tonic` dependency version to 0.14.1 +- Update `prost` dependency version to 0.14 +- Update `opentelemetry` dependency version to 0.31.0 +- Updated `opentelemetry_sdk` dependency to version 0.31.0. + +## 0.30.0 + +Released 2025-May-23 + +- Update `opentelemetry` dependency version to 0.30 +- Updated `opentelemetry_sdk` dependency to version 0.30.0. +- **Feature**: Added Rust code generation for profiles protos. [#2979](https://github.com/open-telemetry/opentelemetry-rust/pull/2979) +- Update `tonic` dependency version to 0.13 +- Update proto definitions to v1.6.0. + +## 0.29.0 + +Released 2025-Mar-21 + +- Update `opentelemetry` dependency version to 0.29 +- Update `opentelemetry_sdk` dependency version to 0.29 + +## 0.28.0 + +Released 2025-Feb-10 + +- Update `opentelemetry` dependency version to 0.28. +- Update `opentelemetry_sdk` dependency version to 0.28. +- Bump msrv to 1.75.0. +- Update proto definitions to v1.5.0 [#2439](https://github.com/open-telemetry/opentelemetry-rust/pull/2439) +- Feature flag "populate-logs-event-name" is removed as no longer relevant. + LogRecord's `event_name()` is now automatically populated on the newly added + "event_name" field in LogRecord proto definition. + +## 0.27.0 + +Released 2024-Nov-11 + +- Update `opentelemetry` dependency version to 0.27 +- Update `opentelemetry_sdk` dependency version to 0.27 + +## v0.26.1 + +- Require tonic 0.12.3 to match generated gRPC code [#2168](https://github.com/open-telemetry/opentelemetry-rust/pull/2168) + +## v0.26.0 +Released 2024-Sep-30 + +- Update `opentelemetry` dependency version to 0.26 +- Update `opentelemetry_sdk` dependency version to 0.26 +- Fix JSON serialization of `metrics::Exemplar` and `trace::span::Link` [#2069](https://github.com/open-telemetry/opentelemetry-rust/pull/2069) +- Bump MSRV to 1.71.1 [2140](https://github.com/open-telemetry/opentelemetry-rust/pull/2140) + +## v0.25.0 +- Update `opentelemetry` dependency version to 0.25 +- Update `opentelemetry_sdk` dependency version to 0.25 +- Starting with this version, this crate will align with `opentelemetry` crate + on major,minor versions. +- Update protobuf definitions to v1.3.2 [#1945](https://github.com/open-telemetry/opentelemetry-rust/pull/1945) + +## v0.7.0 + +- Bump MSRV to 1.70 [1864](https://github.com/open-telemetry/opentelemetry-rust/pull/1874) +- Group log and Span batch by their resource and instrumentation scope before exporting [#1873](https://github.com/open-telemetry/opentelemetry-rust/pull/1873). + - Introduced `group_logs_by_resource_and_scope()` and `group_spans_by_resource_and_scope()` methods to group logs and spans by the resource and scope respectively. +- Update `opentelemetry` dependency version to 0.24 +- Update `opentelemetry_sdk` dependency version to 0.24 + +## v0.6.0 + +- Update protobuf definitions to v1.3.1 [#1721](https://github.com/open-telemetry/opentelemetry-rust/pull/1721) +- Fix the feature flag condition of `opentelemetry-proto/src/transform/logs.rs` [#1746](https://github.com/open-telemetry/opentelemetry-rust/pull/1746) +- Update `opentelemetry` dependency version to 0.23 +- Update `opentelemetry_sdk` dependency version to 0.23 + +## v0.5.0 + +### Changed + +- Update to tonic 0.11 and prost 0.12 [#1536](https://github.com/open-telemetry/opentelemetry-rust/pull/1536) +- **Breaking** Remove support for grpcio transport [#1534](https://github.com/open-telemetry/opentelemetry-rust/pull/1534) + +### Added + +- Add `schemars::JsonSchema` trait support with `with-schemars` feature [#1419](https://github.com/open-telemetry/opentelemetry-rust/pull/1419) +- Update protobuf definitions to v1.1.0 [#1482](https://github.com/open-telemetry/opentelemetry-rust/pull/1482) + +## v0.4.0 + +### Added + +- Implement tonic metrics proto transformations [#1184](https://github.com/open-telemetry/opentelemetry-rust/pull/1184) +- Move proto for zPage to tonic [#1214](https://github.com/open-telemetry/opentelemetry-rust/pull/1214) +- Support exponential histograms [#1267](https://github.com/open-telemetry/opentelemetry-rust/pull/1267) + +### Changed + +- Bump MSRV to 1.65 [#1318](https://github.com/open-telemetry/opentelemetry-rust/pull/1318) +- Bump MSRV to 1.64 [#1203](https://github.com/open-telemetry/opentelemetry-rust/pull/1203) + +### Fixed + +- Rename `traces` feature to the more standard `trace` [#1183](https://github.com/open-telemetry/opentelemetry-rust/pull/1183) + +### Changed + +- Switch to `prost` for `grpcio` protos. [#1202](https://github.com/open-telemetry/opentelemetry-rust/pull/1202) + The `gen-protoc` feature is accordingly renamed to `gen-grpcio`. + +## v0.3.0 + +### Updated + +- Update protobuf definitions to v1.0.0 #1154 +- Update to opentelemetry-api v0.20.0 + +## v0.2.0 +### Changed +- Update to opentelemetry v0.19.0. +- Remove build script and generate files using unit tests [#881](https://github.com/open-telemetry/opentelemetry-rust/pull/881). +- Bump MSRV to 1.57 [#953](https://github.com/open-telemetry/opentelemetry-rust/pull/953). +- Update dependencies and bump MSRV to 1.60 [#969](https://github.com/open-telemetry/opentelemetry-rust/pull/969). +- Bump to use the v0.19.0 protobuf definitions. [#989](https://github.com/open-telemetry/opentelemetry-rust/pull/989). + +## v0.1.0 + +Initial crate release. diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/Cargo.toml b/vendor/opentelemetry-rust/opentelemetry-proto/Cargo.toml new file mode 100644 index 00000000..418f981e --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/Cargo.toml @@ -0,0 +1,164 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.75.0" +name = "opentelemetry-proto" +version = "0.33.0" +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Protobuf generated files and transformations." +homepage = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-proto" +readme = "README.md" +keywords = [ + "opentelemetry", + "otlp", + "logging", + "tracing", + "metrics", +] +categories = [ + "development-tools::debugging", + "development-tools::profiling", + "asynchronous", +] +license = "Apache-2.0" +repository = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-proto" + +[features] +default = ["full"] +full = [ + "gen-tonic", + "trace", + "logs", + "metrics", + "zpages", + "with-serde", + "internal-logs", +] +gen-tonic = [ + "gen-tonic-messages", + "tonic", + "tonic-prost", + "tonic/channel", +] +gen-tonic-messages = ["prost"] +internal-logs = ["opentelemetry/internal-logs"] +logs = [ + "opentelemetry/logs", + "opentelemetry_sdk/logs", +] +metrics = [ + "opentelemetry/metrics", + "opentelemetry_sdk/metrics", +] +profiles = [] +testing = ["opentelemetry/testing"] +trace = [ + "opentelemetry/trace", + "opentelemetry_sdk/trace", +] +with-schemars = ["schemars"] +with-serde = [ + "serde", + "const-hex", + "base64", +] +zpages = ["trace"] + +[lib] +name = "opentelemetry_proto" +path = "src/lib.rs" +doctest = false +bench = false + +[[test]] +name = "grpc_build" +path = "tests/grpc_build.rs" + +[[test]] +name = "json_serde" +path = "tests/json_serde.rs" + +[dependencies.base64] +version = "0.22.1" +optional = true + +[dependencies.const-hex] +version = "1.14.1" +optional = true + +[dependencies.opentelemetry] +version = "0.33" +default-features = false + +[dependencies.opentelemetry_sdk] +version = "0.33" +default-features = false + +[dependencies.prost] +version = "0.14" +optional = true + +[dependencies.schemars] +version = "1.0" +optional = true + +[dependencies.serde] +version = "1.0" +features = [ + "serde_derive", + "std", +] +optional = true +default-features = false + +[dependencies.tonic] +version = "0.14.1" +features = ["codegen"] +optional = true +default-features = false + +[dependencies.tonic-prost] +version = "0.14.1" +optional = true + +[dev-dependencies.opentelemetry] +version = "0.33" +features = ["testing"] +default-features = false + +[dev-dependencies.serde_json] +version = "1.0" + +[dev-dependencies.tempfile] +version = "3.3.0" + +[dev-dependencies.tonic-prost-build] +version = "0.14.1" + +[lints.clippy.all] +level = "warn" +priority = 1 + +[lints.rust] +edition_2024_expr_fragment_specifier = "allow" +if_let_rescope = "allow" +tail_expr_drop_order = "allow" + +[lints.rust.rust_2024_compatibility] +level = "warn" +priority = -1 diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/README.md b/vendor/opentelemetry-rust/opentelemetry-proto/README.md new file mode 100644 index 00000000..1e083dc6 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/README.md @@ -0,0 +1,70 @@ +# OpenTelemetry Proto + +![OpenTelemetry — An observability framework for cloud-native software.][splash] + +[splash]: https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo-text.png + +This crate contains generated files from +[opentelemetry-proto](https://github.com/open-telemetry/opentelemetry-proto) +repository and transformations between types from generated files and types +defined in +[opentelemetry](https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry). + +[![Crates.io: opentelemetry-proto](https://img.shields.io/crates/v/opentelemetry-proto.svg)](https://crates.io/crates/opentelemetry-proto) +[![Documentation](https://docs.rs/opentelemetry-proto/badge.svg)](https://docs.rs/opentelemetry-proto) +[![LICENSE](https://img.shields.io/crates/l/opentelemetry-proto)](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-proto/LICENSE) +[![GitHub Actions CI](https://github.com/open-telemetry/opentelemetry-rust/workflows/CI/badge.svg)](https://github.com/open-telemetry/opentelemetry-rust/actions?query=workflow%3ACI+branch%3Amain) +[![codecov](https://codecov.io/gh/open-telemetry/opentelemetry-rust/branch/main/graph/badge.svg)](https://codecov.io/gh/open-telemetry/opentelemetry-rust) +[![Slack](https://img.shields.io/badge/slack-@cncf/otel/rust-brightgreen.svg?logo=slack)](https://cloud-native.slack.com/archives/C03GDP0H023) + +## Overview + +OpenTelemetry is an Observability framework and toolkit designed to create and +manage telemetry data such as traces, metrics, and logs. OpenTelemetry is +vendor- and tool-agnostic, meaning that it can be used with a broad variety of +Observability backends, including open source tools like [Jaeger] and +[Prometheus], as well as commercial offerings. + +OpenTelemetry is *not* an observability backend like Jaeger, Prometheus, or other +commercial vendors. OpenTelemetry is focused on the generation, collection, +management, and export of telemetry. A major goal of OpenTelemetry is that you +can easily instrument your applications or systems, no matter their language, +infrastructure, or runtime environment. Crucially, the storage and visualization +of telemetry is intentionally left to other tools. + +*[Supported Rust Versions](#supported-rust-versions)* + +[Prometheus]: https://prometheus.io +[Jaeger]: https://www.jaegertracing.io + +### What does this crate contain? + +This crate provides auto-generated Protobuf types from the [OpenTelemetry +protocol specification](https://github.com/open-telemetry/opentelemetry-proto), +along with conversion implementations between these generated types and the +types defined in the +[opentelemetry](https://crates.io/crates/opentelemetry) crate. It is used +internally by exporters such as +[opentelemetry-otlp](https://crates.io/crates/opentelemetry-otlp) to serialize +and deserialize telemetry data. + +## Getting started + +See [docs](https://docs.rs/opentelemetry-proto). + +## Release Notes + +You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-proto/CHANGELOG.md). + +## Supported Rust Versions + +OpenTelemetry is built against the latest stable release. The minimum supported +version is 1.75.0. The current OpenTelemetry version is NOT guaranteed to build +on Rust versions earlier than the minimum supported version. + +The current stable Rust compiler and the three most recent minor versions +before it will always be supported. For example, if the current stable compiler +version is 1.49, the minimum supported version will not be increased past 1.46, +three minor versions prior. Increasing the minimum supported compiler version +is not considered a semver breaking change as long as doing so complies with +this policy. diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/lib.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/lib.rs new file mode 100644 index 00000000..bd6ef1ec --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/lib.rs @@ -0,0 +1,38 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +//! This crate contains generated files from [opentelemetry-proto](https://github.com/open-telemetry/opentelemetry-proto) +//! repository and transformation between types from generated files and types defined in [opentelemetry](https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry) +//! +//! Based on the build tool needed, users can choose to generate files using [tonic](https://github.com/hyperium/tonic) +//! or [grpcio](https://github.com/tikv/grpc-rs). +//! +//! +//! # Feature flags +//! `Opentelemetry-proto` includes a set of feature flags to avoid pull in unnecessary dependencies. +//! The following is the full list of currently supported features: +//! +//! ## Signals +//! - `trace`: generate types that used in traces. Currently supports `gen-tonic`. +//! - `metrics`: generate types that used in metrics. Currently supports `gen-tonic`. +//! - `logs`: generate types that used in logs. Currently supports `gen-tonic`. +//! - `zpages`: generate types that used in zPages. Currently only tracez related types will be generated. Currently supports `gen-tonic`. +//! +//! ## Creates used to generate files +//! - `gen-tonic-messages`: generate OTLP message types using [prost](https://github.com/tokio-rs/prost). +//! - `gen-tonic`: add tonic gRPC client/server transport support on top of `gen-tonic-messages`. +//! +//! ## Misc +//! - `full`: enabled all features above. +//! +//! By default, no feature is enabled. + +// proto mod contains file generated by protobuf or other build tools. +// we shouldn't manually change it. Thus skip format and lint check. +#[rustfmt::skip] +#[allow(warnings)] +#[doc(hidden)] +mod proto; + +#[cfg(feature = "gen-tonic-messages")] +pub use proto::tonic; + +pub mod transform; diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto.rs new file mode 100644 index 00000000..f3980ed8 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto.rs @@ -0,0 +1,516 @@ +/// provide serde support for proto traceIds and spanIds. +/// Those are hex encoded strings in the jsons but they are byte arrays in the proto. +/// See https://opentelemetry.io/docs/specs/otlp/#json-protobuf-encoding for more details +#[cfg(all(feature = "with-serde", feature = "gen-tonic-messages"))] +pub(crate) mod serializers { + use crate::tonic::common::v1::any_value::{self, Value}; + use crate::tonic::common::v1::{AnyValue, ArrayValue, KeyValueList}; + use serde::de::{self, MapAccess, Visitor}; + use serde::ser::{SerializeMap, SerializeSeq, SerializeStruct}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::fmt; + + pub fn is_default(value: &T) -> bool + where + T: Default + PartialEq, + { + value == &T::default() + } + + // hex string <-> bytes conversion + + pub fn serialize_to_hex_string(bytes: &[u8], serializer: S) -> Result + where + S: Serializer, + { + let hex_string = const_hex::encode(bytes); + serializer.serialize_str(&hex_string) + } + + pub fn deserialize_from_hex_string<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + struct BytesVisitor; + + impl<'de> Visitor<'de> for BytesVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string representing hex-encoded bytes") + } + + fn visit_str(self, value: &str) -> Result, E> + where + E: de::Error, + { + const_hex::decode(value).map_err(E::custom) + } + } + + deserializer.deserialize_str(BytesVisitor) + } + + // AnyValue <-> KeyValue conversion + pub fn serialize_to_value(value: &Option, serializer: S) -> Result + where + S: Serializer, + { + match &value { + Some(Value::IntValue(i)) => { + // Attempt to serialize the intValue field + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("intValue", &i.to_string()); + map.end() + } + Some(Value::BytesValue(b)) => { + let mut map = serializer.serialize_map(Some(1))?; + map.serialize_entry("bytesValue", &base64::encode(b)); + map.end() + } + Some(value) => value.serialize(serializer), + None => serializer.serialize_none(), + } + } + + pub fn deserialize_from_value<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + struct ValueVisitor; + + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrInt { + Int(i64), + String(String), + } + + impl StringOrInt { + fn get_int<'de, V>(&self) -> Result + where + V: de::MapAccess<'de>, + { + match self { + Self::Int(val) => Ok(*val), + Self::String(val) => Ok(val.parse::().map_err(de::Error::custom)?), + } + } + } + + impl<'de> de::Visitor<'de> for ValueVisitor { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a JSON object for AnyValue") + } + + fn visit_map(self, mut map: V) -> Result, V::Error> + where + V: de::MapAccess<'de>, + { + let mut value: Option = None; + + while let Some(key) = map.next_key::()? { + let key_str = key.as_str(); + match key_str { + "stringValue" => { + if let Some(s) = map.next_value::>()? { + value = Some(any_value::Value::StringValue(s)); + } + } + "boolValue" => { + if let Some(b) = map.next_value::>()? { + value = Some(any_value::Value::BoolValue(b)); + } + } + "intValue" => { + if let Some(int_value) = map.next_value::>()? { + value = Some(any_value::Value::IntValue(int_value.get_int::()?)); + } + } + "doubleValue" => { + if let Some(d) = map.next_value::>()? { + value = Some(any_value::Value::DoubleValue(d)); + } + } + "arrayValue" => { + if let Some(a) = map.next_value::>()? { + value = Some(any_value::Value::ArrayValue(a)); + } + } + "kvlistValue" => { + if let Some(kv) = map.next_value::>()? { + value = Some(any_value::Value::KvlistValue(kv)); + } + } + "bytesValue" => { + if let Some(base64) = map.next_value::>()? { + let decoded = base64::decode(base64.as_bytes()) + .map_err(|e| de::Error::custom(e))?; + value = Some(any_value::Value::BytesValue(decoded)); + } + } + _ => { + //skip unknown keys, and handle error later. + continue; + } + } + } + + // An AnyValue with no selected field is valid and considered empty. + // Unknown fields are ignored according to the OTLP/JSON specification, + // so an empty or unknown-only object has the same representation. + Ok(value) + } + } + + let value = deserializer.deserialize_map(ValueVisitor)?; + Ok(value) + } + + pub fn serialize_u64_to_string(value: &u64, serializer: S) -> Result + where + S: Serializer, + { + let s = value.to_string(); + serializer.serialize_str(&s) + } + + pub fn deserialize_string_to_u64<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct U64Visitor; + + impl<'de> de::Visitor<'de> for U64Visitor { + type Value = u64; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a u64 integer or a string containing a u64 integer") + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + Ok(value) + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + u64::try_from(value) + .map_err(|_| E::custom(format!("i64 value {} is out of range for u64", value))) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + value.parse::().map_err(de::Error::custom) + } + } + + deserializer.deserialize_any(U64Visitor) + } + + pub fn serialize_vec_u64_to_string(value: &[u64], serializer: S) -> Result + where + S: Serializer, + { + let s = value.iter().map(|v| v.to_string()).collect::>(); + let mut sq = serializer.serialize_seq(Some(s.len()))?; + for v in value { + sq.serialize_element(&v.to_string())?; + } + sq.end() + } + + pub fn deserialize_vec_string_to_vec_u64<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + struct U64ElemVisitor; + + impl<'de> de::Visitor<'de> for U64ElemVisitor { + type Value = u64; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a u64 integer or a string containing a u64 integer") + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + Ok(value) + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + u64::try_from(value) + .map_err(|_| E::custom(format!("i64 value {} is out of range for u64", value))) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + value.parse::().map_err(de::Error::custom) + } + } + + struct VecU64Visitor; + + impl<'de> de::Visitor<'de> for VecU64Visitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a sequence of u64 integers or strings containing u64 integers") + } + + fn visit_seq(self, mut seq: A) -> Result, A::Error> + where + A: de::SeqAccess<'de>, + { + let mut values = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(value) = seq.next_element_seed(U64ElemSeed)? { + values.push(value); + } + Ok(values) + } + } + + struct U64ElemSeed; + + impl<'de> de::DeserializeSeed<'de> for U64ElemSeed { + type Value = u64; + + fn deserialize(self, deserializer: D2) -> Result + where + D2: Deserializer<'de>, + { + deserializer.deserialize_any(U64ElemVisitor) + } + } + + deserializer.deserialize_seq(VecU64Visitor) + } + + pub fn serialize_i64_to_string(value: &i64, serializer: S) -> Result + where + S: Serializer, + { + let s = value.to_string(); + serializer.serialize_str(&s) + } + + pub fn deserialize_string_to_i64<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct I64Visitor; + + impl<'de> de::Visitor<'de> for I64Visitor { + type Value = i64; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an i64 integer or a string containing an i64 integer") + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + Ok(value) + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + i64::try_from(value) + .map_err(|_| E::custom(format!("u64 value {} is out of range for i64", value))) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + value.parse::().map_err(de::Error::custom) + } + } + + deserializer.deserialize_any(I64Visitor) + } + + // Special serializer and deserializer for NaN, Infinity, and -Infinity + pub fn serialize_f64_special(value: &f64, serializer: S) -> Result + where + S: Serializer, + { + if value.is_nan() { + serializer.serialize_str("NaN") + } else if value.is_infinite() { + if value.is_sign_positive() { + serializer.serialize_str("Infinity") + } else { + serializer.serialize_str("-Infinity") + } + } else { + serializer.serialize_f64(*value) + } + } + + pub fn deserialize_f64_special<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct F64Visitor; + + impl<'de> de::Visitor<'de> for F64Visitor { + type Value = f64; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a float or a string representing NaN, Infinity, or -Infinity") + } + + fn visit_f64(self, value: f64) -> Result + where + E: de::Error, + { + Ok(value) + } + + fn visit_u64(self, value: u64) -> Result + where + E: de::Error, + { + Ok(value as f64) + } + + fn visit_i64(self, value: i64) -> Result + where + E: de::Error, + { + Ok(value as f64) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value { + "NaN" => Ok(f64::NAN), + "Infinity" => Ok(f64::INFINITY), + "-Infinity" => Ok(f64::NEG_INFINITY), + _ => value.parse::().map_err(|_| { + E::custom(format!( + "invalid string for f64: expected a number, NaN, Infinity, or -Infinity but got '{}'", + value + )) + }), + } + } + } + + deserializer.deserialize_any(F64Visitor) + } +} + +#[cfg(feature = "gen-tonic-messages")] +#[path = "proto/tonic"] +/// Generated files using [`tonic`](https://docs.rs/crate/tonic) and [`prost`](https://docs.rs/crate/prost) +pub mod tonic { + /// Service stub and clients + #[path = ""] + pub mod collector { + #[cfg(feature = "logs")] + #[path = ""] + pub mod logs { + #[path = "opentelemetry.proto.collector.logs.v1.rs"] + pub mod v1; + } + + #[cfg(feature = "metrics")] + #[path = ""] + pub mod metrics { + #[path = "opentelemetry.proto.collector.metrics.v1.rs"] + pub mod v1; + } + + #[cfg(feature = "trace")] + #[path = ""] + pub mod trace { + #[path = "opentelemetry.proto.collector.trace.v1.rs"] + pub mod v1; + } + + #[cfg(feature = "profiles")] + #[path = ""] + pub mod profiles { + #[path = "opentelemetry.proto.collector.profiles.v1development.rs"] + pub mod v1development; + } + } + + /// Common types used across all signals + #[path = ""] + pub mod common { + #[path = "opentelemetry.proto.common.v1.rs"] + pub mod v1; + } + + /// Generated types used in logging. + #[cfg(feature = "logs")] + #[path = ""] + pub mod logs { + #[path = "opentelemetry.proto.logs.v1.rs"] + pub mod v1; + } + + /// Generated types used in metrics. + #[cfg(feature = "metrics")] + #[path = ""] + pub mod metrics { + #[path = "opentelemetry.proto.metrics.v1.rs"] + pub mod v1; + } + + /// Generated types used in resources. + #[path = ""] + pub mod resource { + #[path = "opentelemetry.proto.resource.v1.rs"] + pub mod v1; + } + + /// Generated types used in traces. + #[cfg(feature = "trace")] + #[path = ""] + pub mod trace { + #[path = "opentelemetry.proto.trace.v1.rs"] + pub mod v1; + } + + /// Generated types used in zpages. + #[cfg(feature = "zpages")] + #[path = ""] + pub mod tracez { + #[path = "opentelemetry.proto.tracez.v1.rs"] + pub mod v1; + } + + /// Generated types used in zpages. + #[cfg(feature = "profiles")] + #[path = ""] + pub mod profiles { + #[path = "opentelemetry.proto.profiles.v1development.rs"] + pub mod v1development; + } + + pub use crate::transform::common::tonic::Attributes; +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.logs.v1.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.logs.v1.rs new file mode 100644 index 00000000..7fc56f93 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.logs.v1.rs @@ -0,0 +1,374 @@ +// This file is @generated by prost-build. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExportLogsServiceRequest { + /// An array of ResourceLogs. + /// For data coming from a single resource this array will typically contain one + /// element. Intermediary nodes (such as OpenTelemetry Collector) that receive + /// data from multiple origins typically batch the data before forwarding further and + /// in that case this array will contain multiple elements. + #[prost(message, repeated, tag = "1")] + pub resource_logs: ::prost::alloc::vec::Vec< + super::super::super::logs::v1::ResourceLogs, + >, +} +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExportLogsServiceResponse { + /// The details of a partially successful export request. + /// + /// If the request is only partially accepted + /// (i.e. when the server accepts only parts of the data and rejects the rest) + /// the server MUST initialize the `partial_success` field and MUST + /// set the `rejected_` with the number of items it rejected. + /// + /// Servers MAY also make use of the `partial_success` field to convey + /// warnings/suggestions to senders even when the request was fully accepted. + /// In such cases, the `rejected_` MUST have a value of `0` and + /// the `error_message` MUST be non-empty. + /// + /// A `partial_success` message with an empty value (rejected\_ = 0 and + /// `error_message` = "") is equivalent to it not being set/present. Senders + /// SHOULD interpret it the same way as in the full success case. + #[prost(message, optional, tag = "1")] + pub partial_success: ::core::option::Option, +} +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExportLogsPartialSuccess { + /// The number of rejected log records. + /// + /// A `rejected_` field holding a `0` value indicates that the + /// request was fully accepted. + #[prost(int64, tag = "1")] + pub rejected_log_records: i64, + /// A developer-facing human-readable message in English. It should be used + /// either to explain why the server rejected parts of the data during a partial + /// success or to convey warnings/suggestions during a full success. The message + /// should offer guidance on how users can address such issues. + /// + /// error_message is an optional field. An error_message with an empty value + /// is equivalent to it not being set. + #[prost(string, tag = "2")] + pub error_message: ::prost::alloc::string::String, +} +/// Generated client implementations. +#[cfg(feature = "gen-tonic")] +pub mod logs_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// Service that can be used to push logs between one Application instrumented with + /// OpenTelemetry and an collector, or between an collector and a central collector (in this + /// case logs are sent/received to/from multiple Applications). + #[derive(Debug, Clone)] + pub struct LogsServiceClient { + inner: tonic::client::Grpc, + } + impl LogsServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl LogsServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> LogsServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + LogsServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn export( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/opentelemetry.proto.collector.logs.v1.LogsService/Export", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "opentelemetry.proto.collector.logs.v1.LogsService", + "Export", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +#[cfg(feature = "gen-tonic")] +pub mod logs_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with LogsServiceServer. + #[async_trait] + pub trait LogsService: std::marker::Send + std::marker::Sync + 'static { + async fn export( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// Service that can be used to push logs between one Application instrumented with + /// OpenTelemetry and an collector, or between an collector and a central collector (in this + /// case logs are sent/received to/from multiple Applications). + #[derive(Debug)] + pub struct LogsServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl LogsServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for LogsServiceServer + where + T: LogsService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/opentelemetry.proto.collector.logs.v1.LogsService/Export" => { + #[allow(non_camel_case_types)] + struct ExportSvc(pub Arc); + impl< + T: LogsService, + > tonic::server::UnaryService + for ExportSvc { + type Response = super::ExportLogsServiceResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::export(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ExportSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for LogsServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "opentelemetry.proto.collector.logs.v1.LogsService"; + impl tonic::server::NamedService for LogsServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.metrics.v1.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.metrics.v1.rs new file mode 100644 index 00000000..0b06bd16 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.metrics.v1.rs @@ -0,0 +1,374 @@ +// This file is @generated by prost-build. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExportMetricsServiceRequest { + /// An array of ResourceMetrics. + /// For data coming from a single resource this array will typically contain one + /// element. Intermediary nodes (such as OpenTelemetry Collector) that receive + /// data from multiple origins typically batch the data before forwarding further and + /// in that case this array will contain multiple elements. + #[prost(message, repeated, tag = "1")] + pub resource_metrics: ::prost::alloc::vec::Vec< + super::super::super::metrics::v1::ResourceMetrics, + >, +} +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExportMetricsServiceResponse { + /// The details of a partially successful export request. + /// + /// If the request is only partially accepted + /// (i.e. when the server accepts only parts of the data and rejects the rest) + /// the server MUST initialize the `partial_success` field and MUST + /// set the `rejected_` with the number of items it rejected. + /// + /// Servers MAY also make use of the `partial_success` field to convey + /// warnings/suggestions to senders even when the request was fully accepted. + /// In such cases, the `rejected_` MUST have a value of `0` and + /// the `error_message` MUST be non-empty. + /// + /// A `partial_success` message with an empty value (rejected\_ = 0 and + /// `error_message` = "") is equivalent to it not being set/present. Senders + /// SHOULD interpret it the same way as in the full success case. + #[prost(message, optional, tag = "1")] + pub partial_success: ::core::option::Option, +} +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExportMetricsPartialSuccess { + /// The number of rejected data points. + /// + /// A `rejected_` field holding a `0` value indicates that the + /// request was fully accepted. + #[prost(int64, tag = "1")] + pub rejected_data_points: i64, + /// A developer-facing human-readable message in English. It should be used + /// either to explain why the server rejected parts of the data during a partial + /// success or to convey warnings/suggestions during a full success. The message + /// should offer guidance on how users can address such issues. + /// + /// error_message is an optional field. An error_message with an empty value + /// is equivalent to it not being set. + #[prost(string, tag = "2")] + pub error_message: ::prost::alloc::string::String, +} +/// Generated client implementations. +#[cfg(feature = "gen-tonic")] +pub mod metrics_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// Service that can be used to push metrics between one Application + /// instrumented with OpenTelemetry and a collector, or between a collector and a + /// central collector. + #[derive(Debug, Clone)] + pub struct MetricsServiceClient { + inner: tonic::client::Grpc, + } + impl MetricsServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl MetricsServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> MetricsServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + MetricsServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn export( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/opentelemetry.proto.collector.metrics.v1.MetricsService/Export", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "opentelemetry.proto.collector.metrics.v1.MetricsService", + "Export", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +#[cfg(feature = "gen-tonic")] +pub mod metrics_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with MetricsServiceServer. + #[async_trait] + pub trait MetricsService: std::marker::Send + std::marker::Sync + 'static { + async fn export( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// Service that can be used to push metrics between one Application + /// instrumented with OpenTelemetry and a collector, or between a collector and a + /// central collector. + #[derive(Debug)] + pub struct MetricsServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl MetricsServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for MetricsServiceServer + where + T: MetricsService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/opentelemetry.proto.collector.metrics.v1.MetricsService/Export" => { + #[allow(non_camel_case_types)] + struct ExportSvc(pub Arc); + impl< + T: MetricsService, + > tonic::server::UnaryService + for ExportSvc { + type Response = super::ExportMetricsServiceResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::export(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ExportSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for MetricsServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "opentelemetry.proto.collector.metrics.v1.MetricsService"; + impl tonic::server::NamedService for MetricsServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.profiles.v1development.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.profiles.v1development.rs new file mode 100644 index 00000000..647c1e42 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.profiles.v1development.rs @@ -0,0 +1,377 @@ +// This file is @generated by prost-build. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExportProfilesServiceRequest { + /// An array of ResourceProfiles. + /// For data coming from a single resource this array will typically contain one + /// element. Intermediary nodes (such as OpenTelemetry Collector) that receive + /// data from multiple origins typically batch the data before forwarding further and + /// in that case this array will contain multiple elements. + #[prost(message, repeated, tag = "1")] + pub resource_profiles: ::prost::alloc::vec::Vec< + super::super::super::profiles::v1development::ResourceProfiles, + >, + /// The reference table containing all data shared by profiles across the message being sent. + #[prost(message, optional, tag = "2")] + pub dictionary: ::core::option::Option< + super::super::super::profiles::v1development::ProfilesDictionary, + >, +} +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExportProfilesServiceResponse { + /// The details of a partially successful export request. + /// + /// If the request is only partially accepted + /// (i.e. when the server accepts only parts of the data and rejects the rest) + /// the server MUST initialize the `partial_success` field and MUST + /// set the `rejected_` with the number of items it rejected. + /// + /// Servers MAY also make use of the `partial_success` field to convey + /// warnings/suggestions to senders even when the request was fully accepted. + /// In such cases, the `rejected_` MUST have a value of `0` and + /// the `error_message` MUST be non-empty. + /// + /// A `partial_success` message with an empty value (rejected\_ = 0 and + /// `error_message` = "") is equivalent to it not being set/present. Senders + /// SHOULD interpret it the same way as in the full success case. + #[prost(message, optional, tag = "1")] + pub partial_success: ::core::option::Option, +} +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExportProfilesPartialSuccess { + /// The number of rejected profiles. + /// + /// A `rejected_` field holding a `0` value indicates that the + /// request was fully accepted. + #[prost(int64, tag = "1")] + pub rejected_profiles: i64, + /// A developer-facing human-readable message in English. It should be used + /// either to explain why the server rejected parts of the data during a partial + /// success or to convey warnings/suggestions during a full success. The message + /// should offer guidance on how users can address such issues. + /// + /// error_message is an optional field. An error_message with an empty value + /// is equivalent to it not being set. + #[prost(string, tag = "2")] + pub error_message: ::prost::alloc::string::String, +} +/// Generated client implementations. +#[cfg(feature = "gen-tonic")] +pub mod profiles_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// Service that can be used to push profiles between one Application instrumented with + /// OpenTelemetry and a collector, or between a collector and a central collector. + #[derive(Debug, Clone)] + pub struct ProfilesServiceClient { + inner: tonic::client::Grpc, + } + impl ProfilesServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ProfilesServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> ProfilesServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + ProfilesServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn export( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/opentelemetry.proto.collector.profiles.v1development.ProfilesService/Export", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "opentelemetry.proto.collector.profiles.v1development.ProfilesService", + "Export", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +#[cfg(feature = "gen-tonic")] +pub mod profiles_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with ProfilesServiceServer. + #[async_trait] + pub trait ProfilesService: std::marker::Send + std::marker::Sync + 'static { + async fn export( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// Service that can be used to push profiles between one Application instrumented with + /// OpenTelemetry and a collector, or between a collector and a central collector. + #[derive(Debug)] + pub struct ProfilesServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl ProfilesServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for ProfilesServiceServer + where + T: ProfilesService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/opentelemetry.proto.collector.profiles.v1development.ProfilesService/Export" => { + #[allow(non_camel_case_types)] + struct ExportSvc(pub Arc); + impl< + T: ProfilesService, + > tonic::server::UnaryService + for ExportSvc { + type Response = super::ExportProfilesServiceResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::export(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ExportSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for ProfilesServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "opentelemetry.proto.collector.profiles.v1development.ProfilesService"; + impl tonic::server::NamedService for ProfilesServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.trace.v1.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.trace.v1.rs new file mode 100644 index 00000000..44fb545a --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.collector.trace.v1.rs @@ -0,0 +1,374 @@ +// This file is @generated by prost-build. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExportTraceServiceRequest { + /// An array of ResourceSpans. + /// For data coming from a single resource this array will typically contain one + /// element. Intermediary nodes (such as OpenTelemetry Collector) that receive + /// data from multiple origins typically batch the data before forwarding further and + /// in that case this array will contain multiple elements. + #[prost(message, repeated, tag = "1")] + pub resource_spans: ::prost::alloc::vec::Vec< + super::super::super::trace::v1::ResourceSpans, + >, +} +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExportTraceServiceResponse { + /// The details of a partially successful export request. + /// + /// If the request is only partially accepted + /// (i.e. when the server accepts only parts of the data and rejects the rest) + /// the server MUST initialize the `partial_success` field and MUST + /// set the `rejected_` with the number of items it rejected. + /// + /// Servers MAY also make use of the `partial_success` field to convey + /// warnings/suggestions to senders even when the request was fully accepted. + /// In such cases, the `rejected_` MUST have a value of `0` and + /// the `error_message` MUST be non-empty. + /// + /// A `partial_success` message with an empty value (rejected\_ = 0 and + /// `error_message` = "") is equivalent to it not being set/present. Senders + /// SHOULD interpret it the same way as in the full success case. + #[prost(message, optional, tag = "1")] + pub partial_success: ::core::option::Option, +} +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ExportTracePartialSuccess { + /// The number of rejected spans. + /// + /// A `rejected_` field holding a `0` value indicates that the + /// request was fully accepted. + #[prost(int64, tag = "1")] + pub rejected_spans: i64, + /// A developer-facing human-readable message in English. It should be used + /// either to explain why the server rejected parts of the data during a partial + /// success or to convey warnings/suggestions during a full success. The message + /// should offer guidance on how users can address such issues. + /// + /// error_message is an optional field. An error_message with an empty value + /// is equivalent to it not being set. + #[prost(string, tag = "2")] + pub error_message: ::prost::alloc::string::String, +} +/// Generated client implementations. +#[cfg(feature = "gen-tonic")] +pub mod trace_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// Service that can be used to push spans between one Application instrumented with + /// OpenTelemetry and a collector, or between a collector and a central collector (in this + /// case spans are sent/received to/from multiple Applications). + #[derive(Debug, Clone)] + pub struct TraceServiceClient { + inner: tonic::client::Grpc, + } + impl TraceServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl TraceServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> TraceServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + TraceServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn export( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/opentelemetry.proto.collector.trace.v1.TraceService/Export", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "opentelemetry.proto.collector.trace.v1.TraceService", + "Export", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +#[cfg(feature = "gen-tonic")] +pub mod trace_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with TraceServiceServer. + #[async_trait] + pub trait TraceService: std::marker::Send + std::marker::Sync + 'static { + async fn export( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// Service that can be used to push spans between one Application instrumented with + /// OpenTelemetry and a collector, or between a collector and a central collector (in this + /// case spans are sent/received to/from multiple Applications). + #[derive(Debug)] + pub struct TraceServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl TraceServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for TraceServiceServer + where + T: TraceService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/opentelemetry.proto.collector.trace.v1.TraceService/Export" => { + #[allow(non_camel_case_types)] + struct ExportSvc(pub Arc); + impl< + T: TraceService, + > tonic::server::UnaryService + for ExportSvc { + type Response = super::ExportTraceServiceResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::export(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ExportSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for TraceServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "opentelemetry.proto.collector.trace.v1.TraceService"; + impl tonic::server::NamedService for TraceServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.common.v1.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.common.v1.rs new file mode 100644 index 00000000..7297e411 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.common.v1.rs @@ -0,0 +1,187 @@ +// This file is @generated by prost-build. +/// Represents any type of attribute value. AnyValue may contain a +/// primitive value such as a string or integer or it may contain an arbitrary nested +/// object containing arrays, key-value lists and primitives. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct AnyValue { + /// The value is one of the listed fields. It is valid for all values to be unspecified + /// in which case this AnyValue is considered to be "empty". + #[prost(oneof = "any_value::Value", tags = "1, 2, 3, 4, 5, 6, 7, 8")] + #[cfg_attr( + feature = "with-serde", + serde( + flatten, + serialize_with = "crate::proto::serializers::serialize_to_value", + deserialize_with = "crate::proto::serializers::deserialize_from_value" + ) + )] + pub value: ::core::option::Option, +} +/// Nested message and enum types in `AnyValue`. +pub mod any_value { + /// The value is one of the listed fields. It is valid for all values to be unspecified + /// in which case this AnyValue is considered to be "empty". + #[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] + #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Value { + #[prost(string, tag = "1")] + StringValue(::prost::alloc::string::String), + #[prost(bool, tag = "2")] + BoolValue(bool), + #[prost(int64, tag = "3")] + IntValue(i64), + #[prost(double, tag = "4")] + DoubleValue(f64), + #[prost(message, tag = "5")] + ArrayValue(super::ArrayValue), + #[prost(message, tag = "6")] + KvlistValue(super::KeyValueList), + #[prost(bytes, tag = "7")] + BytesValue(::prost::alloc::vec::Vec), + /// Reference to the string value in ProfilesDictionary.string_table. + /// + /// Note: This is currently used exclusively in the Profiling signal. + /// Implementers of OTLP receivers for signals other than Profiling should + /// treat the presence of this value as a non-fatal issue. + /// Log an error or warning indicating an unexpected field intended for the + /// Profiling signal and process the data as if this value were absent or + /// empty, ignoring its semantic content for the non-Profiling signal. + /// + /// Status: \[Development\] + #[prost(int32, tag = "8")] + StringValueStrindex(i32), + } +} +/// ArrayValue is a list of AnyValue messages. We need ArrayValue as a message +/// since oneof in AnyValue does not allow repeated fields. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ArrayValue { + /// Array of values. The array may be empty (contain 0 elements). + #[prost(message, repeated, tag = "1")] + pub values: ::prost::alloc::vec::Vec, +} +/// KeyValueList is a list of KeyValue messages. We need KeyValueList as a message +/// since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need +/// a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to +/// avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches +/// are semantically equivalent. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct KeyValueList { + /// A collection of key/value pairs of key-value pairs. The list may be empty (may + /// contain 0 elements). + /// + /// The keys MUST be unique (it is not allowed to have more than one + /// value with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "1")] + pub values: ::prost::alloc::vec::Vec, +} +/// Represents a key-value pair that is used to store Span attributes, Link +/// attributes, etc. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct KeyValue { + /// The key name of the pair. + /// key_ref MUST NOT be set if key is used. + #[prost(string, tag = "1")] + pub key: ::prost::alloc::string::String, + /// The value of the pair. + #[prost(message, optional, tag = "2")] + pub value: ::core::option::Option, + /// Reference to the string key in ProfilesDictionary.string_table. + /// key MUST NOT be set if key_strindex is used. + /// + /// Note: This is currently used exclusively in the Profiling signal. + /// Implementers of OTLP receivers for signals other than Profiling should + /// treat the presence of this key as a non-fatal issue. + /// Log an error or warning indicating an unexpected field intended for the + /// Profiling signal and process the data as if this value were absent or + /// empty, ignoring its semantic content for the non-Profiling signal. + /// + /// Status: \[Development\] + #[prost(int32, tag = "3")] + #[cfg_attr( + feature = "with-serde", + serde(default, skip_serializing_if = "crate::proto::serializers::is_default") + )] + pub key_strindex: i32, +} +/// InstrumentationScope is a message representing the instrumentation scope information +/// such as the fully qualified name and version. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct InstrumentationScope { + /// A name denoting the Instrumentation scope. + /// An empty instrumentation scope name means the name is unknown. + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + /// Defines the version of the instrumentation scope. + /// An empty instrumentation scope version means the version is unknown. + #[prost(string, tag = "2")] + pub version: ::prost::alloc::string::String, + /// Additional attributes that describe the scope. \[Optional\]. + /// Attribute keys MUST be unique (it is not allowed to have more than one + /// attribute with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "3")] + pub attributes: ::prost::alloc::vec::Vec, + /// The number of attributes that were discarded. Attributes + /// can be discarded because their keys are too long or because there are too many + /// attributes. If this value is 0, then no attributes were dropped. + #[prost(uint32, tag = "4")] + pub dropped_attributes_count: u32, +} +/// A reference to an Entity. +/// Entity represents an object of interest associated with produced telemetry: e.g spans, metrics, profiles, or logs. +/// +/// Status: \[Development\] +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct EntityRef { + /// The Schema URL, if known. This is the identifier of the Schema that the entity data + /// is recorded in. To learn more about Schema URL see + /// + /// + /// This schema_url applies to the data in this message and to the Resource attributes + /// referenced by id_keys and description_keys. + /// TODO: discuss if we are happy with this somewhat complicated definition of what + /// the schema_url applies to. + /// + /// This field obsoletes the schema_url field in ResourceMetrics/ResourceSpans/ResourceLogs. + #[prost(string, tag = "1")] + pub schema_url: ::prost::alloc::string::String, + /// Defines the type of the entity. MUST not change during the lifetime of the entity. + /// For example: "service" or "host". This field is required and MUST not be empty + /// for valid entities. + #[prost(string, tag = "2")] + pub r#type: ::prost::alloc::string::String, + /// Attribute Keys that identify the entity. + /// MUST not change during the lifetime of the entity. The Id must contain at least one attribute. + /// These keys MUST exist in the containing {message}.attributes. + #[prost(string, repeated, tag = "3")] + pub id_keys: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + /// Descriptive (non-identifying) attribute keys of the entity. + /// MAY change over the lifetime of the entity. MAY be empty. + /// These attribute keys are not part of entity's identity. + /// These keys MUST exist in the containing {message}.attributes. + #[prost(string, repeated, tag = "4")] + pub description_keys: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.logs.v1.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.logs.v1.rs new file mode 100644 index 00000000..d326259a --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.logs.v1.rs @@ -0,0 +1,338 @@ +// This file is @generated by prost-build. +/// LogsData represents the logs data that can be stored in a persistent storage, +/// OR can be embedded by other protocols that transfer OTLP logs data but do not +/// implement the OTLP protocol. +/// +/// The main difference between this message and collector protocol is that +/// in this message there will not be any "control" or "metadata" specific to +/// OTLP protocol. +/// +/// When new fields are added into this message, the OTLP request MUST be updated +/// as well. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LogsData { + /// An array of ResourceLogs. + /// For data coming from a single resource this array will typically contain + /// one element. Intermediary nodes that receive data from multiple origins + /// typically batch the data before forwarding further and in that case this + /// array will contain multiple elements. + #[prost(message, repeated, tag = "1")] + pub resource_logs: ::prost::alloc::vec::Vec, +} +/// A collection of ScopeLogs from a Resource. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ResourceLogs { + /// The resource for the logs in this message. + /// If this field is not set then resource info is unknown. + #[prost(message, optional, tag = "1")] + pub resource: ::core::option::Option, + /// A list of ScopeLogs that originate from a resource. + #[prost(message, repeated, tag = "2")] + pub scope_logs: ::prost::alloc::vec::Vec, + /// The Schema URL, if known. This is the identifier of the Schema that the resource data + /// is recorded in. Notably, the last part of the URL path is the version number of the + /// schema: http\[s\]://server\[:port\]/path/. To learn more about Schema URL see + /// + /// This schema_url applies to the data in the "resource" field. It does not apply + /// to the data in the "scope_logs" field which have their own schema_url field. + #[prost(string, tag = "3")] + pub schema_url: ::prost::alloc::string::String, +} +/// A collection of Logs produced by a Scope. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ScopeLogs { + /// The instrumentation scope information for the logs in this message. + /// Semantically when InstrumentationScope isn't set, it is equivalent with + /// an empty instrumentation scope name (unknown). + #[prost(message, optional, tag = "1")] + pub scope: ::core::option::Option, + /// A list of log records. + #[prost(message, repeated, tag = "2")] + pub log_records: ::prost::alloc::vec::Vec, + /// The Schema URL, if known. This is the identifier of the Schema that the log data + /// is recorded in. Notably, the last part of the URL path is the version number of the + /// schema: http\[s\]://server\[:port\]/path/. To learn more about Schema URL see + /// + /// This schema_url applies to the data in the "scope" field and all logs in the + /// "log_records" field. + #[prost(string, tag = "3")] + pub schema_url: ::prost::alloc::string::String, +} +/// A log record according to OpenTelemetry Log Data Model: +/// +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LogRecord { + /// time_unix_nano is the time when the event occurred. + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + /// Value of 0 indicates unknown or missing timestamp. + #[prost(fixed64, tag = "1")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub time_unix_nano: u64, + /// Time when the event was observed by the collection system. + /// For events that originate in OpenTelemetry (e.g. using OpenTelemetry Logging SDK) + /// this timestamp is typically set at the generation time and is equal to Timestamp. + /// For events originating externally and collected by OpenTelemetry (e.g. using + /// Collector) this is the time when OpenTelemetry's code observed the event measured + /// by the clock of the OpenTelemetry code. This field MUST be set once the event is + /// observed by OpenTelemetry. + /// + /// For converting OpenTelemetry log data to formats that support only one timestamp or + /// when receiving OpenTelemetry log data by recipients that support only one timestamp + /// internally the following logic is recommended: + /// + /// * Use time_unix_nano if it is present, otherwise use observed_time_unix_nano. + /// + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + /// Value of 0 indicates unknown or missing timestamp. + #[prost(fixed64, tag = "11")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub observed_time_unix_nano: u64, + /// Numerical value of the severity, normalized to values described in Log Data Model. + /// \[Optional\]. + #[prost(enumeration = "SeverityNumber", tag = "2")] + pub severity_number: i32, + /// The severity text (also known as log level). The original string representation as + /// it is known at the source. \[Optional\]. + #[prost(string, tag = "3")] + pub severity_text: ::prost::alloc::string::String, + /// A value containing the body of the log record. Can be for example a human-readable + /// string message (including multi-line) describing the event in a free form or it can + /// be a structured data composed of arrays and maps of other values. \[Optional\]. + #[prost(message, optional, tag = "5")] + pub body: ::core::option::Option, + /// Additional attributes that describe the specific event occurrence. \[Optional\]. + /// Attribute keys MUST be unique (it is not allowed to have more than one + /// attribute with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "6")] + pub attributes: ::prost::alloc::vec::Vec, + #[prost(uint32, tag = "7")] + pub dropped_attributes_count: u32, + /// Flags, a bit field. 8 least significant bits are the trace flags as + /// defined in W3C Trace Context specification. 24 most significant bits are reserved + /// and must be set to 0. Readers must not assume that 24 most significant bits + /// will be zero and must correctly mask the bits when reading 8-bit trace flag (use + /// flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK). \[Optional\]. + #[prost(fixed32, tag = "8")] + pub flags: u32, + /// A unique identifier for a trace. All logs from the same trace share + /// the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR + /// of length other than 16 bytes is considered invalid (empty string in OTLP/JSON + /// is zero-length and thus is also invalid). + /// + /// This field is optional. + /// + /// The receivers SHOULD assume that the log record is not associated with a + /// trace if any of the following is true: + /// + /// * the field is not present, + /// * the field contains an invalid value. + #[prost(bytes = "vec", tag = "9")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_to_hex_string", + deserialize_with = "crate::proto::serializers::deserialize_from_hex_string" + ) + )] + pub trace_id: ::prost::alloc::vec::Vec, + /// A unique identifier for a span within a trace, assigned when the span + /// is created. The ID is an 8-byte array. An ID with all zeroes OR of length + /// other than 8 bytes is considered invalid (empty string in OTLP/JSON + /// is zero-length and thus is also invalid). + /// + /// This field is optional. If the sender specifies a valid span_id then it SHOULD also + /// specify a valid trace_id. + /// + /// The receivers SHOULD assume that the log record is not associated with a + /// span if any of the following is true: + /// + /// * the field is not present, + /// * the field contains an invalid value. + #[prost(bytes = "vec", tag = "10")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_to_hex_string", + deserialize_with = "crate::proto::serializers::deserialize_from_hex_string" + ) + )] + pub span_id: ::prost::alloc::vec::Vec, + /// A unique identifier of event category/type. + /// All events with the same event_name are expected to conform to the same + /// schema for both their attributes and their body. + /// + /// Recommended to be fully qualified and short (no longer than 256 characters). + /// + /// Presence of event_name on the log record identifies this record + /// as an event. + /// + /// \[Optional\]. + #[prost(string, tag = "12")] + pub event_name: ::prost::alloc::string::String, +} +/// Possible values for LogRecord.SeverityNumber. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum SeverityNumber { + Unspecified = 0, + Trace = 1, + Trace2 = 2, + Trace3 = 3, + Trace4 = 4, + Debug = 5, + Debug2 = 6, + Debug3 = 7, + Debug4 = 8, + Info = 9, + Info2 = 10, + Info3 = 11, + Info4 = 12, + Warn = 13, + Warn2 = 14, + Warn3 = 15, + Warn4 = 16, + Error = 17, + Error2 = 18, + Error3 = 19, + Error4 = 20, + Fatal = 21, + Fatal2 = 22, + Fatal3 = 23, + Fatal4 = 24, +} +impl SeverityNumber { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "SEVERITY_NUMBER_UNSPECIFIED", + Self::Trace => "SEVERITY_NUMBER_TRACE", + Self::Trace2 => "SEVERITY_NUMBER_TRACE2", + Self::Trace3 => "SEVERITY_NUMBER_TRACE3", + Self::Trace4 => "SEVERITY_NUMBER_TRACE4", + Self::Debug => "SEVERITY_NUMBER_DEBUG", + Self::Debug2 => "SEVERITY_NUMBER_DEBUG2", + Self::Debug3 => "SEVERITY_NUMBER_DEBUG3", + Self::Debug4 => "SEVERITY_NUMBER_DEBUG4", + Self::Info => "SEVERITY_NUMBER_INFO", + Self::Info2 => "SEVERITY_NUMBER_INFO2", + Self::Info3 => "SEVERITY_NUMBER_INFO3", + Self::Info4 => "SEVERITY_NUMBER_INFO4", + Self::Warn => "SEVERITY_NUMBER_WARN", + Self::Warn2 => "SEVERITY_NUMBER_WARN2", + Self::Warn3 => "SEVERITY_NUMBER_WARN3", + Self::Warn4 => "SEVERITY_NUMBER_WARN4", + Self::Error => "SEVERITY_NUMBER_ERROR", + Self::Error2 => "SEVERITY_NUMBER_ERROR2", + Self::Error3 => "SEVERITY_NUMBER_ERROR3", + Self::Error4 => "SEVERITY_NUMBER_ERROR4", + Self::Fatal => "SEVERITY_NUMBER_FATAL", + Self::Fatal2 => "SEVERITY_NUMBER_FATAL2", + Self::Fatal3 => "SEVERITY_NUMBER_FATAL3", + Self::Fatal4 => "SEVERITY_NUMBER_FATAL4", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "SEVERITY_NUMBER_UNSPECIFIED" => Some(Self::Unspecified), + "SEVERITY_NUMBER_TRACE" => Some(Self::Trace), + "SEVERITY_NUMBER_TRACE2" => Some(Self::Trace2), + "SEVERITY_NUMBER_TRACE3" => Some(Self::Trace3), + "SEVERITY_NUMBER_TRACE4" => Some(Self::Trace4), + "SEVERITY_NUMBER_DEBUG" => Some(Self::Debug), + "SEVERITY_NUMBER_DEBUG2" => Some(Self::Debug2), + "SEVERITY_NUMBER_DEBUG3" => Some(Self::Debug3), + "SEVERITY_NUMBER_DEBUG4" => Some(Self::Debug4), + "SEVERITY_NUMBER_INFO" => Some(Self::Info), + "SEVERITY_NUMBER_INFO2" => Some(Self::Info2), + "SEVERITY_NUMBER_INFO3" => Some(Self::Info3), + "SEVERITY_NUMBER_INFO4" => Some(Self::Info4), + "SEVERITY_NUMBER_WARN" => Some(Self::Warn), + "SEVERITY_NUMBER_WARN2" => Some(Self::Warn2), + "SEVERITY_NUMBER_WARN3" => Some(Self::Warn3), + "SEVERITY_NUMBER_WARN4" => Some(Self::Warn4), + "SEVERITY_NUMBER_ERROR" => Some(Self::Error), + "SEVERITY_NUMBER_ERROR2" => Some(Self::Error2), + "SEVERITY_NUMBER_ERROR3" => Some(Self::Error3), + "SEVERITY_NUMBER_ERROR4" => Some(Self::Error4), + "SEVERITY_NUMBER_FATAL" => Some(Self::Fatal), + "SEVERITY_NUMBER_FATAL2" => Some(Self::Fatal2), + "SEVERITY_NUMBER_FATAL3" => Some(Self::Fatal3), + "SEVERITY_NUMBER_FATAL4" => Some(Self::Fatal4), + _ => None, + } + } +} +/// LogRecordFlags represents constants used to interpret the +/// LogRecord.flags field, which is protobuf 'fixed32' type and is to +/// be used as bit-fields. Each non-zero value defined in this enum is +/// a bit-mask. To extract the bit-field, for example, use an +/// expression like: +/// +/// (logRecord.flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK) +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum LogRecordFlags { + /// The zero value for the enum. Should not be used for comparisons. + /// Instead use bitwise "and" with the appropriate mask as shown above. + DoNotUse = 0, + /// Bits 0-7 are used for trace flags. + TraceFlagsMask = 255, +} +impl LogRecordFlags { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::DoNotUse => "LOG_RECORD_FLAGS_DO_NOT_USE", + Self::TraceFlagsMask => "LOG_RECORD_FLAGS_TRACE_FLAGS_MASK", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "LOG_RECORD_FLAGS_DO_NOT_USE" => Some(Self::DoNotUse), + "LOG_RECORD_FLAGS_TRACE_FLAGS_MASK" => Some(Self::TraceFlagsMask), + _ => None, + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs new file mode 100644 index 00000000..aeb68f94 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.metrics.v1.rs @@ -0,0 +1,1005 @@ +// This file is @generated by prost-build. +/// MetricsData represents the metrics data that can be stored in a persistent +/// storage, OR can be embedded by other protocols that transfer OTLP metrics +/// data but do not implement the OTLP protocol. +/// +/// MetricsData +/// └─── ResourceMetrics +/// ├── Resource +/// ├── SchemaURL +/// └── ScopeMetrics +/// ├── Scope +/// ├── SchemaURL +/// └── Metric +/// ├── Name +/// ├── Description +/// ├── Unit +/// └── data +/// ├── Gauge +/// ├── Sum +/// ├── Histogram +/// ├── ExponentialHistogram +/// └── Summary +/// +/// The main difference between this message and collector protocol is that +/// in this message there will not be any "control" or "metadata" specific to +/// OTLP protocol. +/// +/// When new fields are added into this message, the OTLP request MUST be updated +/// as well. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MetricsData { + /// An array of ResourceMetrics. + /// For data coming from a single resource this array will typically contain + /// one element. Intermediary nodes that receive data from multiple origins + /// typically batch the data before forwarding further and in that case this + /// array will contain multiple elements. + #[prost(message, repeated, tag = "1")] + pub resource_metrics: ::prost::alloc::vec::Vec, +} +/// A collection of ScopeMetrics from a Resource. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ResourceMetrics { + /// The resource for the metrics in this message. + /// If this field is not set then no resource info is known. + #[prost(message, optional, tag = "1")] + pub resource: ::core::option::Option, + /// A list of metrics that originate from a resource. + #[prost(message, repeated, tag = "2")] + pub scope_metrics: ::prost::alloc::vec::Vec, + /// The Schema URL, if known. This is the identifier of the Schema that the resource data + /// is recorded in. Notably, the last part of the URL path is the version number of the + /// schema: http\[s\]://server\[:port\]/path/. To learn more about Schema URL see + /// + /// This schema_url applies to the data in the "resource" field. It does not apply + /// to the data in the "scope_metrics" field which have their own schema_url field. + #[prost(string, tag = "3")] + pub schema_url: ::prost::alloc::string::String, +} +/// A collection of Metrics produced by an Scope. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ScopeMetrics { + /// The instrumentation scope information for the metrics in this message. + /// Semantically when InstrumentationScope isn't set, it is equivalent with + /// an empty instrumentation scope name (unknown). + #[prost(message, optional, tag = "1")] + pub scope: ::core::option::Option, + /// A list of metrics that originate from an instrumentation library. + #[prost(message, repeated, tag = "2")] + pub metrics: ::prost::alloc::vec::Vec, + /// The Schema URL, if known. This is the identifier of the Schema that the metric data + /// is recorded in. Notably, the last part of the URL path is the version number of the + /// schema: http\[s\]://server\[:port\]/path/. To learn more about Schema URL see + /// + /// This schema_url applies to the data in the "scope" field and all metrics in the + /// "metrics" field. + #[prost(string, tag = "3")] + pub schema_url: ::prost::alloc::string::String, +} +/// Defines a Metric which has one or more timeseries. The following is a +/// brief summary of the Metric data model. For more details, see: +/// +/// +/// +/// The data model and relation between entities is shown in the +/// diagram below. Here, "DataPoint" is the term used to refer to any +/// one of the specific data point value types, and "points" is the term used +/// to refer to any one of the lists of points contained in the Metric. +/// +/// * Metric is composed of a metadata and data. +/// +/// * Metadata part contains a name, description, unit. +/// +/// * Data is one of the possible types (Sum, Gauge, Histogram, Summary). +/// +/// * DataPoint contains timestamps, attributes, and one of the possible value type +/// fields. +/// +/// Metric +/// +------------+ +/// \|name | +/// \|description | +/// \|unit | +------------------------------------+ +/// \|data |---> |Gauge, Sum, Histogram, Summary, ... | +/// +------------+ +------------------------------------+ +/// +/// Data \[One of Gauge, Sum, Histogram, Summary, ...\] +/// +-----------+ +/// \|... | // Metadata about the Data. +/// \|points |--+ +/// +-----------+ | +/// \| +---------------------------+ +/// \| |DataPoint 1 | +/// v |+------+------+ +------+ | +/// +-----+ ||label |label |...|label | | +/// \| 1 |-->||value1|value2|...|valueN| | +/// +-----+ |+------+------+ +------+ | +/// \| . | |+-----+ | +/// \| . | ||value| | +/// \| . | |+-----+ | +/// \| . | +---------------------------+ +/// \| . | . +/// \| . | . +/// \| . | . +/// \| . | +---------------------------+ +/// \| . | |DataPoint M | +/// +-----+ |+------+------+ +------+ | +/// \| M |-->||label |label |...|label | | +/// +-----+ ||value1|value2|...|valueN| | +/// \|+------+------+ +------+ | +/// \|+-----+ | +/// \||value| | +/// \|+-----+ | +/// +---------------------------+ +/// +/// Each distinct type of DataPoint represents the output of a specific +/// aggregation function, the result of applying the DataPoint's +/// associated function of to one or more measurements. +/// +/// All DataPoint types have three common fields: +/// +/// * Attributes includes key-value pairs associated with the data point +/// * TimeUnixNano is required, set to the end time of the aggregation +/// * StartTimeUnixNano is optional, but strongly encouraged for DataPoints +/// having an AggregationTemporality field, as discussed below. +/// +/// Both TimeUnixNano and StartTimeUnixNano values are expressed as +/// UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. +/// +/// # TimeUnixNano +/// +/// This field is required, having consistent interpretation across +/// DataPoint types. TimeUnixNano is the moment corresponding to when +/// the data point's aggregate value was captured. +/// +/// Data points with the 0 value for TimeUnixNano SHOULD be rejected +/// by consumers. +/// +/// # StartTimeUnixNano +/// +/// StartTimeUnixNano in general allows detecting when a sequence of +/// observations is unbroken. This field indicates to consumers the +/// start time for points with cumulative and delta +/// AggregationTemporality, and it should be included whenever possible +/// to support correct rate calculation. Although it may be omitted +/// when the start time is truly unknown, setting StartTimeUnixNano is +/// strongly encouraged. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Metric { + /// The name of the metric. + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + /// A description of the metric, which can be used in documentation. + #[prost(string, tag = "2")] + pub description: ::prost::alloc::string::String, + /// The unit in which the metric value is reported. Follows the format + /// described by + #[prost(string, tag = "3")] + pub unit: ::prost::alloc::string::String, + /// Additional metadata attributes that describe the metric. \[Optional\]. + /// Attributes are non-identifying. + /// Consumers SHOULD NOT need to be aware of these attributes. + /// These attributes MAY be used to encode information allowing + /// for lossless roundtrip translation to / from another data model. + /// Attribute keys MUST be unique (it is not allowed to have more than one + /// attribute with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "12")] + pub metadata: ::prost::alloc::vec::Vec, + /// Data determines the aggregation type (if any) of the metric, what is the + /// reported value type for the data points, as well as the relatationship to + /// the time interval over which they are reported. + #[prost(oneof = "metric::Data", tags = "5, 7, 9, 10, 11")] + #[cfg_attr(feature = "with-serde", serde(flatten))] + pub data: ::core::option::Option, +} +/// Nested message and enum types in `Metric`. +pub mod metric { + /// Data determines the aggregation type (if any) of the metric, what is the + /// reported value type for the data points, as well as the relatationship to + /// the time interval over which they are reported. + #[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] + #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Data { + #[prost(message, tag = "5")] + Gauge(super::Gauge), + #[prost(message, tag = "7")] + Sum(super::Sum), + #[prost(message, tag = "9")] + Histogram(super::Histogram), + #[prost(message, tag = "10")] + ExponentialHistogram(super::ExponentialHistogram), + #[prost(message, tag = "11")] + Summary(super::Summary), + } +} +/// Gauge represents the type of a scalar metric that always exports the +/// "current value" for every data point. It should be used for an "unknown" +/// aggregation. +/// +/// A Gauge does not support different aggregation temporalities. Given the +/// aggregation is unknown, points cannot be combined using the same +/// aggregation, regardless of aggregation temporalities. Therefore, +/// AggregationTemporality is not included. Consequently, this also means +/// "StartTimeUnixNano" is ignored for all data points. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Gauge { + /// The time series data points. + /// Note: Multiple time series may be included (same timestamp, different attributes). + #[prost(message, repeated, tag = "1")] + pub data_points: ::prost::alloc::vec::Vec, +} +/// Sum represents the type of a scalar metric that is calculated as a sum of all +/// reported measurements over a time interval. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Sum { + /// The time series data points. + /// Note: Multiple time series may be included (same timestamp, different attributes). + #[prost(message, repeated, tag = "1")] + pub data_points: ::prost::alloc::vec::Vec, + /// aggregation_temporality describes if the aggregator reports delta changes + /// since last report time, or cumulative changes since a fixed start time. + #[prost(enumeration = "AggregationTemporality", tag = "2")] + pub aggregation_temporality: i32, + /// Represents whether the sum is monotonic. + #[prost(bool, tag = "3")] + pub is_monotonic: bool, +} +/// Histogram represents the type of a metric that is calculated by aggregating +/// as a Histogram of all reported measurements over a time interval. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Histogram { + /// The time series data points. + /// Note: Multiple time series may be included (same timestamp, different attributes). + #[prost(message, repeated, tag = "1")] + pub data_points: ::prost::alloc::vec::Vec, + /// aggregation_temporality describes if the aggregator reports delta changes + /// since last report time, or cumulative changes since a fixed start time. + #[prost(enumeration = "AggregationTemporality", tag = "2")] + pub aggregation_temporality: i32, +} +/// ExponentialHistogram represents the type of a metric that is calculated by aggregating +/// as a ExponentialHistogram of all reported double measurements over a time interval. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExponentialHistogram { + /// The time series data points. + /// Note: Multiple time series may be included (same timestamp, different attributes). + #[prost(message, repeated, tag = "1")] + pub data_points: ::prost::alloc::vec::Vec, + /// aggregation_temporality describes if the aggregator reports delta changes + /// since last report time, or cumulative changes since a fixed start time. + #[prost(enumeration = "AggregationTemporality", tag = "2")] + pub aggregation_temporality: i32, +} +/// Summary metric data are used to convey quantile summaries, +/// a Prometheus (see: ) +/// and OpenMetrics (see: ) +/// data type. These data points cannot always be merged in a meaningful way. +/// While they can be useful in some applications, histogram data points are +/// recommended for new applications. +/// Summary metrics do not have an aggregation temporality field. This is +/// because the count and sum fields of a SummaryDataPoint are assumed to be +/// cumulative values. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Summary { + /// The time series data points. + /// Note: Multiple time series may be included (same timestamp, different attributes). + #[prost(message, repeated, tag = "1")] + pub data_points: ::prost::alloc::vec::Vec, +} +/// NumberDataPoint is a single data point in a timeseries that describes the +/// time-varying scalar value of a metric. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NumberDataPoint { + /// The set of key/value pairs that uniquely identify the timeseries from + /// where this point belongs. The list may be empty (may contain 0 elements). + /// Attribute keys MUST be unique (it is not allowed to have more than one + /// attribute with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "7")] + pub attributes: ::prost::alloc::vec::Vec, + /// StartTimeUnixNano is optional but strongly encouraged, see the + /// the detailed comments above Metric. + /// + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + /// 1970. + #[prost(fixed64, tag = "2")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub start_time_unix_nano: u64, + /// TimeUnixNano is required, see the detailed comments above Metric. + /// + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + /// 1970. + #[prost(fixed64, tag = "3")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub time_unix_nano: u64, + /// (Optional) List of exemplars collected from + /// measurements that were used to form the data point + #[prost(message, repeated, tag = "5")] + pub exemplars: ::prost::alloc::vec::Vec, + /// Flags that apply to this specific data point. See DataPointFlags + /// for the available flags and their meaning. + #[prost(uint32, tag = "8")] + pub flags: u32, + /// The value itself. A point is considered invalid when one of the recognized + /// value fields is not present inside this oneof. + #[prost(oneof = "number_data_point::Value", tags = "4, 6")] + #[cfg_attr(feature = "with-serde", serde(flatten))] + pub value: ::core::option::Option, +} +/// Nested message and enum types in `NumberDataPoint`. +pub mod number_data_point { + /// The value itself. A point is considered invalid when one of the recognized + /// value fields is not present inside this oneof. + #[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] + #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] + #[derive(Clone, Copy, PartialEq, ::prost::Oneof)] + pub enum Value { + #[prost(double, tag = "4")] + AsDouble(f64), + #[prost(sfixed64, tag = "6")] + AsInt(i64), + } +} +/// HistogramDataPoint is a single data point in a timeseries that describes the +/// time-varying values of a Histogram. A Histogram contains summary statistics +/// for a population of values, it may optionally contain the distribution of +/// those values across a set of buckets. +/// +/// If the histogram contains the distribution of values, then both +/// "explicit_bounds" and "bucket counts" fields must be defined. +/// If the histogram does not contain the distribution of values, then both +/// "explicit_bounds" and "bucket_counts" must be omitted and only "count" and +/// "sum" are known. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct HistogramDataPoint { + /// The set of key/value pairs that uniquely identify the timeseries from + /// where this point belongs. The list may be empty (may contain 0 elements). + /// Attribute keys MUST be unique (it is not allowed to have more than one + /// attribute with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "9")] + pub attributes: ::prost::alloc::vec::Vec, + /// StartTimeUnixNano is optional but strongly encouraged, see the + /// the detailed comments above Metric. + /// + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + /// 1970. + #[prost(fixed64, tag = "2")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub start_time_unix_nano: u64, + /// TimeUnixNano is required, see the detailed comments above Metric. + /// + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + /// 1970. + #[prost(fixed64, tag = "3")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub time_unix_nano: u64, + /// count is the number of values in the population. Must be non-negative. This + /// value must be equal to the sum of the "count" fields in buckets if a + /// histogram is provided. + #[prost(fixed64, tag = "4")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub count: u64, + /// sum of the values in the population. If count is zero then this field + /// must be zero. + /// + /// Note: Sum should only be filled out when measuring non-negative discrete + /// events, and is assumed to be monotonic over the values of these events. + /// Negative events *can* be recorded, but sum should not be filled out when + /// doing so. This is specifically to enforce compatibility w/ OpenMetrics, + /// see: + #[prost(double, optional, tag = "5")] + pub sum: ::core::option::Option, + /// bucket_counts is an optional field contains the count values of histogram + /// for each bucket. + /// + /// The sum of the bucket_counts must equal the value in the count field. + /// + /// The number of elements in bucket_counts array must be by one greater than + /// the number of elements in explicit_bounds array. The exception to this rule + /// is when the length of bucket_counts is 0, then the length of explicit_bounds + /// must also be 0. + #[prost(fixed64, repeated, tag = "6")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_vec_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_vec_string_to_vec_u64" + ) + )] + pub bucket_counts: ::prost::alloc::vec::Vec, + /// explicit_bounds specifies buckets with explicitly defined bounds for values. + /// + /// The boundaries for bucket at index i are: + /// + /// (-infinity, explicit_bounds\[i\]\] for i == 0 + /// (explicit_bounds\[i-1\], explicit_bounds\[i\]\] for 0 \< i \< size(explicit_bounds) + /// (explicit_bounds\[i-1\], +infinity) for i == size(explicit_bounds) + /// + /// The values in the explicit_bounds array must be strictly increasing. + /// + /// Histogram buckets are inclusive of their upper boundary, except the last + /// bucket where the boundary is at infinity. This format is intentionally + /// compatible with the OpenMetrics histogram definition. + /// + /// If bucket_counts length is 0 then explicit_bounds length must also be 0, + /// otherwise the data point is invalid. + #[prost(double, repeated, tag = "7")] + pub explicit_bounds: ::prost::alloc::vec::Vec, + /// (Optional) List of exemplars collected from + /// measurements that were used to form the data point + #[prost(message, repeated, tag = "8")] + pub exemplars: ::prost::alloc::vec::Vec, + /// Flags that apply to this specific data point. See DataPointFlags + /// for the available flags and their meaning. + #[prost(uint32, tag = "10")] + pub flags: u32, + /// min is the minimum value over (start_time, end_time\]. + #[prost(double, optional, tag = "11")] + pub min: ::core::option::Option, + /// max is the maximum value over (start_time, end_time\]. + #[prost(double, optional, tag = "12")] + pub max: ::core::option::Option, +} +/// ExponentialHistogramDataPoint is a single data point in a timeseries that describes the +/// time-varying values of a ExponentialHistogram of double values. A ExponentialHistogram contains +/// summary statistics for a population of values, it may optionally contain the +/// distribution of those values across a set of buckets. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ExponentialHistogramDataPoint { + /// The set of key/value pairs that uniquely identify the timeseries from + /// where this point belongs. The list may be empty (may contain 0 elements). + /// Attribute keys MUST be unique (it is not allowed to have more than one + /// attribute with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "1")] + pub attributes: ::prost::alloc::vec::Vec, + /// StartTimeUnixNano is optional but strongly encouraged, see the + /// the detailed comments above Metric. + /// + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + /// 1970. + #[prost(fixed64, tag = "2")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub start_time_unix_nano: u64, + /// TimeUnixNano is required, see the detailed comments above Metric. + /// + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + /// 1970. + #[prost(fixed64, tag = "3")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub time_unix_nano: u64, + /// The number of values in the population. Must be + /// non-negative. This value must be equal to the sum of the "bucket_counts" + /// values in the positive and negative Buckets plus the "zero_count" field. + #[prost(fixed64, tag = "4")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub count: u64, + /// The sum of the values in the population. If count is zero then this field + /// must be zero. + /// + /// Note: Sum should only be filled out when measuring non-negative discrete + /// events, and is assumed to be monotonic over the values of these events. + /// Negative events *can* be recorded, but sum should not be filled out when + /// doing so. This is specifically to enforce compatibility w/ OpenMetrics, + /// see: + #[prost(double, optional, tag = "5")] + pub sum: ::core::option::Option, + /// scale describes the resolution of the histogram. Boundaries are + /// located at powers of the base, where: + /// + /// base = (2^(2^-scale)) + /// + /// The histogram bucket identified by `index`, a signed integer, + /// contains values that are greater than (base^index) and + /// less than or equal to (base^(index+1)). + /// + /// The positive and negative ranges of the histogram are expressed + /// separately. Negative values are mapped by their absolute value + /// into the negative range using the same scale as the positive range. + /// + /// scale is not restricted by the protocol, as the permissible + /// values depend on the range of the data. + #[prost(sint32, tag = "6")] + pub scale: i32, + /// The count of values that are either exactly zero or + /// within the region considered zero by the instrumentation at the + /// tolerated degree of precision. This bucket stores values that + /// cannot be expressed using the standard exponential formula as + /// well as values that have been rounded to zero. + /// + /// Implementations MAY consider the zero bucket to have probability + /// mass equal to (zero_count / count). + #[prost(fixed64, tag = "7")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub zero_count: u64, + /// positive carries the positive range of exponential bucket counts. + #[prost(message, optional, tag = "8")] + pub positive: ::core::option::Option, + /// negative carries the negative range of exponential bucket counts. + #[prost(message, optional, tag = "9")] + pub negative: ::core::option::Option, + /// Flags that apply to this specific data point. See DataPointFlags + /// for the available flags and their meaning. + #[prost(uint32, tag = "10")] + pub flags: u32, + /// (Optional) List of exemplars collected from + /// measurements that were used to form the data point + #[prost(message, repeated, tag = "11")] + pub exemplars: ::prost::alloc::vec::Vec, + /// The minimum value over (start_time, end_time\]. + #[prost(double, optional, tag = "12")] + pub min: ::core::option::Option, + /// The maximum value over (start_time, end_time\]. + #[prost(double, optional, tag = "13")] + pub max: ::core::option::Option, + /// ZeroThreshold may be optionally set to convey the width of the zero + /// region. Where the zero region is defined as the closed interval + /// \[-ZeroThreshold, ZeroThreshold\]. + /// When ZeroThreshold is 0, zero count bucket stores values that cannot be + /// expressed using the standard exponential formula as well as values that + /// have been rounded to zero. + #[prost(double, tag = "14")] + pub zero_threshold: f64, +} +/// Nested message and enum types in `ExponentialHistogramDataPoint`. +pub mod exponential_histogram_data_point { + /// Buckets are a set of bucket counts, encoded in a contiguous array + /// of counts. + #[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] + #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] + pub struct Buckets { + /// The bucket index of the first entry in the bucket_counts array. + /// + /// Note: This uses a varint encoding as a simple form of compression. + #[prost(sint32, tag = "1")] + pub offset: i32, + /// An array of count values, where bucket_counts\[i\] carries + /// the count of the bucket at index (offset+i). bucket_counts\[i\] is the count + /// of values greater than base^(offset+i) and less than or equal to + /// base^(offset+i+1). + /// + /// Note: By contrast, the explicit HistogramDataPoint uses + /// fixed64. This field is expected to have many buckets, + /// especially zeros, so uint64 has been selected to ensure + /// varint encoding. + #[prost(uint64, repeated, tag = "2")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_vec_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_vec_string_to_vec_u64" + ) + )] + pub bucket_counts: ::prost::alloc::vec::Vec, + } +} +/// SummaryDataPoint is a single data point in a timeseries that describes the +/// time-varying values of a Summary metric. The count and sum fields represent +/// cumulative values. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SummaryDataPoint { + /// The set of key/value pairs that uniquely identify the timeseries from + /// where this point belongs. The list may be empty (may contain 0 elements). + /// Attribute keys MUST be unique (it is not allowed to have more than one + /// attribute with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "7")] + pub attributes: ::prost::alloc::vec::Vec, + /// StartTimeUnixNano is optional but strongly encouraged, see the + /// the detailed comments above Metric. + /// + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + /// 1970. + #[prost(fixed64, tag = "2")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub start_time_unix_nano: u64, + /// TimeUnixNano is required, see the detailed comments above Metric. + /// + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + /// 1970. + #[prost(fixed64, tag = "3")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub time_unix_nano: u64, + /// count is the number of values in the population. Must be non-negative. + #[prost(fixed64, tag = "4")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub count: u64, + /// sum of the values in the population. If count is zero then this field + /// must be zero. + /// + /// Note: Sum should only be filled out when measuring non-negative discrete + /// events, and is assumed to be monotonic over the values of these events. + /// Negative events *can* be recorded, but sum should not be filled out when + /// doing so. This is specifically to enforce compatibility w/ OpenMetrics, + /// see: + #[prost(double, tag = "5")] + pub sum: f64, + /// (Optional) list of values at different quantiles of the distribution calculated + /// from the current snapshot. The quantiles must be strictly increasing. + #[prost(message, repeated, tag = "6")] + pub quantile_values: ::prost::alloc::vec::Vec, + /// Flags that apply to this specific data point. See DataPointFlags + /// for the available flags and their meaning. + #[prost(uint32, tag = "8")] + pub flags: u32, +} +/// Nested message and enum types in `SummaryDataPoint`. +pub mod summary_data_point { + /// Represents the value at a given quantile of a distribution. + /// + /// To record Min and Max values following conventions are used: + /// + /// * The 1.0 quantile is equivalent to the maximum value observed. + /// * The 0.0 quantile is equivalent to the minimum value observed. + /// + /// See the following issue for more context: + /// + #[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] + #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] + #[derive(Clone, Copy, PartialEq, ::prost::Message)] + pub struct ValueAtQuantile { + /// The quantile of a distribution. Must be in the interval + /// \[0.0, 1.0\]. + #[prost(double, tag = "1")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_f64_special" + ) + )] + pub quantile: f64, + /// The value at the given quantile of a distribution. + /// + /// Quantile values must NOT be negative. + #[prost(double, tag = "2")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_f64_special", + deserialize_with = "crate::proto::serializers::deserialize_f64_special" + ) + )] + pub value: f64, + } +} +/// A representation of an exemplar, which is a sample input measurement. +/// Exemplars also hold information about the environment when the measurement +/// was recorded, for example the span and trace ID of the active span when the +/// exemplar was recorded. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Exemplar { + /// The set of key/value pairs that were filtered out by the aggregator, but + /// recorded alongside the original measurement. Only key/value pairs that were + /// filtered out by the aggregator should be included + #[prost(message, repeated, tag = "7")] + pub filtered_attributes: ::prost::alloc::vec::Vec< + super::super::common::v1::KeyValue, + >, + /// time_unix_nano is the exact time when this exemplar was recorded + /// + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + /// 1970. + #[prost(fixed64, tag = "2")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub time_unix_nano: u64, + /// (Optional) Span ID of the exemplar trace. + /// span_id may be missing if the measurement is not recorded inside a trace + /// or if the trace is not sampled. + #[prost(bytes = "vec", tag = "4")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_to_hex_string", + deserialize_with = "crate::proto::serializers::deserialize_from_hex_string" + ) + )] + pub span_id: ::prost::alloc::vec::Vec, + /// (Optional) Trace ID of the exemplar trace. + /// trace_id may be missing if the measurement is not recorded inside a trace + /// or if the trace is not sampled. + #[prost(bytes = "vec", tag = "5")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_to_hex_string", + deserialize_with = "crate::proto::serializers::deserialize_from_hex_string" + ) + )] + pub trace_id: ::prost::alloc::vec::Vec, + /// The value of the measurement that was recorded. An exemplar is + /// considered invalid when one of the recognized value fields is not present + /// inside this oneof. + #[prost(oneof = "exemplar::Value", tags = "3, 6")] + pub value: ::core::option::Option, +} +/// Nested message and enum types in `Exemplar`. +pub mod exemplar { + /// The value of the measurement that was recorded. An exemplar is + /// considered invalid when one of the recognized value fields is not present + /// inside this oneof. + #[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] + #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] + #[derive(Clone, Copy, PartialEq, ::prost::Oneof)] + pub enum Value { + #[prost(double, tag = "3")] + AsDouble(f64), + #[prost(sfixed64, tag = "6")] + AsInt(i64), + } +} +/// AggregationTemporality defines how a metric aggregator reports aggregated +/// values. It describes how those values relate to the time interval over +/// which they are aggregated. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum AggregationTemporality { + /// UNSPECIFIED is the default AggregationTemporality, it MUST not be used. + Unspecified = 0, + /// DELTA is an AggregationTemporality for a metric aggregator which reports + /// changes since last report time. Successive metrics contain aggregation of + /// values from continuous and non-overlapping intervals. + /// + /// The values for a DELTA metric are based only on the time interval + /// associated with one measurement cycle. There is no dependency on + /// previous measurements like is the case for CUMULATIVE metrics. + /// + /// For example, consider a system measuring the number of requests that + /// it receives and reports the sum of these requests every second as a + /// DELTA metric: + /// + /// 1. The system starts receiving at time=t_0. + /// 1. A request is received, the system measures 1 request. + /// 1. A request is received, the system measures 1 request. + /// 1. A request is received, the system measures 1 request. + /// 1. The 1 second collection cycle ends. A metric is exported for the + /// number of requests received over the interval of time t_0 to + /// t_0+1 with a value of 3. + /// 1. A request is received, the system measures 1 request. + /// 1. A request is received, the system measures 1 request. + /// 1. The 1 second collection cycle ends. A metric is exported for the + /// number of requests received over the interval of time t_0+1 to + /// t_0+2 with a value of 2. + Delta = 1, + /// CUMULATIVE is an AggregationTemporality for a metric aggregator which + /// reports changes since a fixed start time. This means that current values + /// of a CUMULATIVE metric depend on all previous measurements since the + /// start time. Because of this, the sender is required to retain this state + /// in some form. If this state is lost or invalidated, the CUMULATIVE metric + /// values MUST be reset and a new fixed start time following the last + /// reported measurement time sent MUST be used. + /// + /// For example, consider a system measuring the number of requests that + /// it receives and reports the sum of these requests every second as a + /// CUMULATIVE metric: + /// + /// 1. The system starts receiving at time=t_0. + /// 1. A request is received, the system measures 1 request. + /// 1. A request is received, the system measures 1 request. + /// 1. A request is received, the system measures 1 request. + /// 1. The 1 second collection cycle ends. A metric is exported for the + /// number of requests received over the interval of time t_0 to + /// t_0+1 with a value of 3. + /// 1. A request is received, the system measures 1 request. + /// 1. A request is received, the system measures 1 request. + /// 1. The 1 second collection cycle ends. A metric is exported for the + /// number of requests received over the interval of time t_0 to + /// t_0+2 with a value of 5. + /// 1. The system experiences a fault and loses state. + /// 1. The system recovers and resumes receiving at time=t_1. + /// 1. A request is received, the system measures 1 request. + /// 1. The 1 second collection cycle ends. A metric is exported for the + /// number of requests received over the interval of time t_1 to + /// t_0+1 with a value of 1. + /// + /// Note: Even though, when reporting changes since last report time, using + /// CUMULATIVE is valid, it is not recommended. This may cause problems for + /// systems that do not use start_time to determine when the aggregation + /// value was reset (e.g. Prometheus). + Cumulative = 2, +} +impl AggregationTemporality { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "AGGREGATION_TEMPORALITY_UNSPECIFIED", + Self::Delta => "AGGREGATION_TEMPORALITY_DELTA", + Self::Cumulative => "AGGREGATION_TEMPORALITY_CUMULATIVE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "AGGREGATION_TEMPORALITY_UNSPECIFIED" => Some(Self::Unspecified), + "AGGREGATION_TEMPORALITY_DELTA" => Some(Self::Delta), + "AGGREGATION_TEMPORALITY_CUMULATIVE" => Some(Self::Cumulative), + _ => None, + } + } +} +/// DataPointFlags is defined as a protobuf 'uint32' type and is to be used as a +/// bit-field representing 32 distinct boolean flags. Each flag defined in this +/// enum is a bit-mask. To test the presence of a single flag in the flags of +/// a data point, for example, use an expression like: +/// +/// (point.flags & DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK) == DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum DataPointFlags { + /// The zero value for the enum. Should not be used for comparisons. + /// Instead use bitwise "and" with the appropriate mask as shown above. + DoNotUse = 0, + /// This DataPoint is valid but has no recorded value. This value + /// SHOULD be used to reflect explicitly missing data in a series, as + /// for an equivalent to the Prometheus "staleness marker". + NoRecordedValueMask = 1, +} +impl DataPointFlags { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::DoNotUse => "DATA_POINT_FLAGS_DO_NOT_USE", + Self::NoRecordedValueMask => "DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "DATA_POINT_FLAGS_DO_NOT_USE" => Some(Self::DoNotUse), + "DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK" => Some(Self::NoRecordedValueMask), + _ => None, + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.profiles.v1development.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.profiles.v1development.rs new file mode 100644 index 00000000..addf7894 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.profiles.v1development.rs @@ -0,0 +1,463 @@ +// This file is @generated by prost-build. +/// ProfilesDictionary represents the profiles data shared across the +/// entire message being sent. The following applies to all fields in this +/// message: +/// +/// * A dictionary is an array of dictionary items. Users of the dictionary +/// compactly reference the items using the index within the array. +/// +/// * A dictionary MUST have a zero value encoded as the first element. This +/// allows for \_index fields pointing into the dictionary to use a 0 pointer +/// value to indicate 'null' / 'not set'. Unless otherwise defined, a 'zero +/// value' message value is one with all default field values, so as to +/// minimize wire encoded size. +/// +/// * There SHOULD NOT be dupes in a dictionary. The identity of dictionary +/// items is based on their value, recursively as needed. If a particular +/// implementation does emit duplicated items, it MUST NOT attempt to give them +/// meaning based on the index or order. A profile processor may remove +/// duplicate items and this MUST NOT have any observable effects for +/// consumers. +/// +/// * There SHOULD NOT be orphaned (unreferenced) items in a dictionary. A +/// profile processor may remove ("garbage-collect") orphaned items and this +/// MUST NOT have any observable effects for consumers. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProfilesDictionary { + /// Mappings from address ranges to the image/binary/library mapped + /// into that address range referenced by locations via Location.mapping_index. + /// + /// mapping_table\[0\] must always be zero value (Mapping{}) and present. + #[prost(message, repeated, tag = "1")] + pub mapping_table: ::prost::alloc::vec::Vec, + /// Locations referenced by samples via Stack.location_indices. + /// + /// location_table\[0\] must always be zero value (Location{}) and present. + #[prost(message, repeated, tag = "2")] + pub location_table: ::prost::alloc::vec::Vec, + /// Functions referenced by locations via Line.function_index. + /// + /// function_table\[0\] must always be zero value (Function{}) and present. + #[prost(message, repeated, tag = "3")] + pub function_table: ::prost::alloc::vec::Vec, + /// Links referenced by samples via Sample.link_index. + /// + /// link_table\[0\] must always be zero value (Link{}) and present. + #[prost(message, repeated, tag = "4")] + pub link_table: ::prost::alloc::vec::Vec, + /// A common table for strings referenced by various messages. + /// + /// string_table\[0\] must always be "" and present. + #[prost(string, repeated, tag = "5")] + pub string_table: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + /// A common table for attributes referenced by the Profile, Sample, Mapping + /// and Location messages below through attribute_indices field. Each entry is + /// a key/value pair with an optional unit. Since this is a dictionary table, + /// multiple entries with the same key may be present, unlike direct attribute + /// tables like Resource.attributes. The referencing attribute_indices fields, + /// though, do maintain the key uniqueness requirement. + /// + /// It's recommended to use attributes for variables with bounded cardinality, + /// such as categorical variables + /// (). Using an attribute of + /// a floating point type (e.g., CPU time) in a sample can quickly make every + /// attribute value unique, defeating the purpose of the dictionary and + /// impractically increasing the profile size. + /// + /// Examples of attributes: + /// "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" + /// "abc.com/myattribute": true + /// "allocation_size": 128 bytes + /// + /// attribute_table\[0\] must always be zero value (KeyValueAndUnit{}) and present. + #[prost(message, repeated, tag = "6")] + pub attribute_table: ::prost::alloc::vec::Vec, + /// Stacks referenced by samples via Sample.stack_index. + /// + /// stack_table\[0\] must always be zero value (Stack{}) and present. + #[prost(message, repeated, tag = "7")] + pub stack_table: ::prost::alloc::vec::Vec, +} +/// ProfilesData represents the profiles data that can be stored in persistent storage, +/// OR can be embedded by other protocols that transfer OTLP profiles data but do not +/// implement the OTLP protocol. +/// +/// The main difference between this message and collector protocol is that +/// in this message there will not be any "control" or "metadata" specific to +/// OTLP protocol. +/// +/// When new fields are added into this message, the OTLP request MUST be updated +/// as well. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ProfilesData { + /// An array of ResourceProfiles. + /// For data coming from an SDK profiler, this array will typically contain one + /// element. Host-level profilers will usually create one ResourceProfile per + /// container, as well as one additional ResourceProfile grouping all samples + /// from non-containerized processes. + /// Other resource groupings are possible as well and clarified via + /// Resource.attributes and semantic conventions. + /// Tools that visualize profiles should prefer displaying + /// resources_profiles\[0\].scope_profiles\[0\].profiles\[0\] by default. + #[prost(message, repeated, tag = "1")] + pub resource_profiles: ::prost::alloc::vec::Vec, + /// One instance of ProfilesDictionary + #[prost(message, optional, tag = "2")] + pub dictionary: ::core::option::Option, +} +/// A collection of ScopeProfiles from a Resource. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ResourceProfiles { + /// The resource for the profiles in this message. + /// If this field is not set then no resource info is known. + #[prost(message, optional, tag = "1")] + pub resource: ::core::option::Option, + /// A list of ScopeProfiles that originate from a resource. + #[prost(message, repeated, tag = "2")] + pub scope_profiles: ::prost::alloc::vec::Vec, + /// The Schema URL, if known. This is the identifier of the Schema that the resource data + /// is recorded in. Notably, the last part of the URL path is the version number of the + /// schema: http\[s\]://server\[:port\]/path/. To learn more about Schema URL see + /// + /// This schema_url applies to the data in the "resource" field. It does not apply + /// to the data in the "scope_profiles" field which have their own schema_url field. + #[prost(string, tag = "3")] + pub schema_url: ::prost::alloc::string::String, +} +/// A collection of Profiles produced by an InstrumentationScope. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ScopeProfiles { + /// The instrumentation scope information for the profiles in this message. + /// Semantically when InstrumentationScope isn't set, it is equivalent with + /// an empty instrumentation scope name (unknown). + #[prost(message, optional, tag = "1")] + pub scope: ::core::option::Option, + /// A list of Profiles that originate from an instrumentation scope. + #[prost(message, repeated, tag = "2")] + pub profiles: ::prost::alloc::vec::Vec, + /// The Schema URL, if known. This is the identifier of the Schema that the profile data + /// is recorded in. Notably, the last part of the URL path is the version number of the + /// schema: http\[s\]://server\[:port\]/path/. To learn more about Schema URL see + /// + /// This schema_url applies to the data in the "scope" field and all profiles in the + /// "profiles" field. + #[prost(string, tag = "3")] + pub schema_url: ::prost::alloc::string::String, +} +/// Represents a complete profile, including sample types, samples, mappings to +/// binaries, stacks, locations, functions, string table, and additional +/// metadata. It modifies and annotates pprof Profile with OpenTelemetry +/// specific fields. +/// +/// Note that whilst fields in this message retain the name and field id from pprof in most cases +/// for ease of understanding data migration, it is not intended that pprof:Profile and +/// OpenTelemetry:Profile encoding be wire compatible. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Profile { + /// The type and unit of all Sample.values in this profile. + /// For a cpu or off-cpu profile this might be: + /// \["cpu","nanoseconds"\] or \["off_cpu","nanoseconds"\] + /// For a heap profile, this might be: + /// \["allocated_objects","count"\] or \["allocated_space","bytes"\], + #[prost(message, optional, tag = "1")] + pub sample_type: ::core::option::Option, + /// The set of samples recorded in this profile. + #[prost(message, repeated, tag = "2")] + pub samples: ::prost::alloc::vec::Vec, + /// Time of collection. Value is UNIX Epoch time in nanoseconds since 00:00:00 + /// UTC on 1 January 1970. + #[prost(fixed64, tag = "3")] + pub time_unix_nano: u64, + /// Duration of the profile. For instant profiles like live heap snapshot, the + /// duration can be zero but it may be preferable to set time_unix_nano to the + /// process start time and duration_nano to the relative time when the profile + /// was gathered. This ensures Sample.timestamps_unix_nano values such as + /// allocation timestamp fall into the profile time range. + #[prost(uint64, tag = "4")] + pub duration_nano: u64, + /// The kind of events between sampled occurrences. + /// e.g \[ "cpu","cycles" \] or \[ "heap","bytes" \] + #[prost(message, optional, tag = "5")] + pub period_type: ::core::option::Option, + /// The number of events between sampled occurrences. + #[prost(int64, tag = "6")] + pub period: i64, + /// A globally unique identifier for a profile. The ID is a 16-byte array. An ID with + /// all zeroes is considered invalid. It may be used for deduplication and signal + /// correlation purposes. It is acceptable to treat two profiles with different values + /// in this field as not equal, even if they represented the same object at an earlier + /// time. + /// This field is optional; an ID may be assigned to an ID-less profile in a later step. + #[prost(bytes = "vec", tag = "7")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_to_hex_string", + deserialize_with = "crate::proto::serializers::deserialize_from_hex_string" + ) + )] + pub profile_id: ::prost::alloc::vec::Vec, + /// The number of attributes that were discarded. Attributes + /// can be discarded because their keys are too long or because there are too many + /// attributes. If this value is 0, then no attributes were dropped. + #[prost(uint32, tag = "8")] + pub dropped_attributes_count: u32, + /// The original payload format. See also original_payload. Optional, but the + /// format and the bytes must be set or unset together. + /// + /// The allowed values for the format string are defined by the OpenTelemetry + /// specification. Some examples are "jfr", "pprof", "linux_perf". + /// + /// The original payload may be optionally provided when the conversion to the + /// OLTP format was done from a different format with some loss of the fidelity + /// and the receiver may want to store the original payload to allow future + /// lossless export or reinterpretation. Some examples of the original format + /// are JFR (Java Flight Recorder), pprof, Linux perf. + /// + /// Even when the original payload is in a format that is semantically close to + /// OTLP, such as pprof, a conversion may still be lossy in some cases (e.g. if + /// the pprof file contains custom extensions or conventions). + /// + /// The original payload can be large in size, so including the original + /// payload should be configurable by the profiler or collector options. The + /// default behavior should be to not include the original payload. + #[prost(string, tag = "9")] + pub original_payload_format: ::prost::alloc::string::String, + /// The original payload bytes. See also original_payload_format. Optional, but + /// format and the bytes must be set or unset together. + #[prost(bytes = "vec", tag = "10")] + pub original_payload: ::prost::alloc::vec::Vec, + /// References to attributes in attribute_table. \[optional\] + #[prost(int32, repeated, tag = "11")] + pub attribute_indices: ::prost::alloc::vec::Vec, +} +/// A pointer from a profile Sample to a trace Span. +/// Connects a profile sample to a trace span, identified by unique trace and span IDs. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Link { + /// A unique identifier of a trace that this linked span is part of. The ID is a + /// 16-byte array. + #[prost(bytes = "vec", tag = "1")] + pub trace_id: ::prost::alloc::vec::Vec, + /// A unique identifier for the linked span. The ID is an 8-byte array. + #[prost(bytes = "vec", tag = "2")] + pub span_id: ::prost::alloc::vec::Vec, +} +/// ValueType describes the type and units of a value. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ValueType { + /// Index into ProfilesDictionary.string_table. + #[prost(int32, tag = "1")] + pub type_strindex: i32, + /// Index into ProfilesDictionary.string_table. + #[prost(int32, tag = "2")] + pub unit_strindex: i32, +} +/// Each Sample records values encountered in some program context. The program +/// context is typically a stack trace, perhaps augmented with auxiliary +/// information like the thread-id, some indicator of a higher level request +/// being handled etc. +/// +/// A Sample MUST have have at least one values or timestamps_unix_nano entry. If +/// both fields are populated, they MUST contain the same number of elements, and +/// the elements at the same index MUST refer to the same event. +/// +/// For the purposes of efficiently representing aggregated data observations, a Sample is regarded +/// as having a shared identity and an associated collection of per-observation data points. +/// Samples having the same identity SHOULD be combined by inserting timestamps and values to the data arrays. +/// +/// Examples of different ways ('shapes') of representing a sample with the total value of 10: +/// +/// Report of a stacktrace at 10 timestamps (consumers must assume the value is 1 for each point): +/// values: \[\] +/// timestamps_unix_nano: \[1, 2, 3, 4, 5, 6, 7, 8, 9, 10\] +/// +/// Report of a stacktrace with an aggregated value without timestamps: +/// values: \[10\] +/// timestamps_unix_nano: \[\] +/// +/// Report of a stacktrace at 4 timestamps where each point records a specific value: +/// values: \[2, 2, 3, 3\] +/// timestamps_unix_nano: \[1, 2, 3, 4\] +/// +/// All Samples for a Profile SHOULD have the same shape, i.e. all data observation series should consistently +/// adopt the same data recording style. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Sample { + /// Reference to stack in ProfilesDictionary.stack_table. + #[prost(int32, tag = "1")] + pub stack_index: i32, + /// References to attributes in ProfilesDictionary.attribute_table. \[optional\] + #[prost(int32, repeated, tag = "2")] + pub attribute_indices: ::prost::alloc::vec::Vec, + /// Reference to link in ProfilesDictionary.link_table. \[optional\] + /// It can be unset / set to 0 if no link exists, as link_table\[0\] is always a 'null' default value. + #[prost(int32, tag = "3")] + pub link_index: i32, + /// The type and unit of each value is defined by Profile.sample_type. + #[prost(int64, repeated, tag = "4")] + pub values: ::prost::alloc::vec::Vec, + /// Timestamps associated with Sample. Value is UNIX Epoch time in nanoseconds + /// since 00:00:00 UTC on 1 January 1970. The timestamps should fall within the + /// \[Profile.time_unix_nano, Profile.time_unix_nano + Profile.duration_nano) + /// time range. + #[prost(fixed64, repeated, tag = "5")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_vec_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_vec_string_to_vec_u64" + ) + )] + pub timestamps_unix_nano: ::prost::alloc::vec::Vec, +} +/// Describes the mapping of a binary in memory, including its address range, +/// file offset, and metadata like build ID +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Mapping { + /// Address at which the binary (or DLL) is loaded into memory. + #[prost(uint64, tag = "1")] + pub memory_start: u64, + /// The limit of the address range occupied by this mapping. + #[prost(uint64, tag = "2")] + pub memory_limit: u64, + /// Offset in the binary that corresponds to the first mapped address. + #[prost(uint64, tag = "3")] + pub file_offset: u64, + /// The object this entry is loaded from. This can be a filename on + /// disk for the main binary and shared libraries, or virtual + /// abstractions like "\[vdso\]". + /// + /// Index into ProfilesDictionary.string_table. + #[prost(int32, tag = "4")] + pub filename_strindex: i32, + /// References to attributes in ProfilesDictionary.attribute_table. \[optional\] + #[prost(int32, repeated, tag = "5")] + pub attribute_indices: ::prost::alloc::vec::Vec, +} +/// A Stack represents a stack trace as a list of locations. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Stack { + /// References to locations in ProfilesDictionary.location_table. + /// The first location is the leaf frame. + #[prost(int32, repeated, tag = "1")] + pub location_indices: ::prost::alloc::vec::Vec, +} +/// Describes function and line table debug information. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Location { + /// Reference to mapping in ProfilesDictionary.mapping_table. + /// It can be unset / set to 0 if the mapping is unknown or not applicable for + /// this profile type, as mapping_table\[0\] is always a 'null' default mapping. + #[prost(int32, tag = "1")] + pub mapping_index: i32, + /// The instruction address for this location, if available. It + /// should be within \[Mapping.memory_start...Mapping.memory_limit\] + /// for the corresponding mapping. A non-leaf address may be in the + /// middle of a call instruction. It is up to display tools to find + /// the beginning of the instruction if necessary. + #[prost(uint64, tag = "2")] + pub address: u64, + /// Multiple line indicates this location has inlined functions, + /// where the last entry represents the caller into which the + /// preceding entries were inlined. + /// + /// E.g., if memcpy() is inlined into printf: + /// lines\[0\].function_name == "memcpy" + /// lines\[1\].function_name == "printf" + #[prost(message, repeated, tag = "3")] + pub lines: ::prost::alloc::vec::Vec, + /// References to attributes in ProfilesDictionary.attribute_table. \[optional\] + #[prost(int32, repeated, tag = "4")] + pub attribute_indices: ::prost::alloc::vec::Vec, +} +/// Details a specific line in a source code, linked to a function. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Line { + /// Reference to function in ProfilesDictionary.function_table. + #[prost(int32, tag = "1")] + pub function_index: i32, + /// Line number in source code. 0 means unset. + #[prost(int64, tag = "2")] + pub line: i64, + /// Column number in source code. 0 means unset. + #[prost(int64, tag = "3")] + pub column: i64, +} +/// Describes a function, including its human-readable name, system name, +/// source file, and starting line number in the source. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Function { + /// The function name. Empty string if not available. + #[prost(int32, tag = "1")] + pub name_strindex: i32, + /// Function name, as identified by the system. For instance, + /// it can be a C++ mangled name. Empty string if not available. + #[prost(int32, tag = "2")] + pub system_name_strindex: i32, + /// Source file containing the function. Empty string if not available. + #[prost(int32, tag = "3")] + pub filename_strindex: i32, + /// Line number in source file. 0 means unset. + #[prost(int64, tag = "4")] + pub start_line: i64, +} +/// A custom 'dictionary native' style of encoding attributes which is more convenient +/// for profiles than opentelemetry.proto.common.v1.KeyValue +/// Specifically, uses the string table for keys and allows optional unit information. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct KeyValueAndUnit { + /// The index into the string table for the attribute's key. + #[prost(int32, tag = "1")] + pub key_strindex: i32, + /// The value of the attribute. + #[prost(message, optional, tag = "2")] + pub value: ::core::option::Option, + /// The index into the string table for the attribute's unit. + /// zero indicates implicit (by semconv) or non-defined unit. + #[prost(int32, tag = "3")] + pub unit_strindex: i32, +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.resource.v1.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.resource.v1.rs new file mode 100644 index 00000000..2b9aa448 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.resource.v1.rs @@ -0,0 +1,26 @@ +// This file is @generated by prost-build. +/// Resource information. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Resource { + /// Set of attributes that describe the resource. + /// Attribute keys MUST be unique (it is not allowed to have more than one + /// attribute with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "1")] + pub attributes: ::prost::alloc::vec::Vec, + /// The number of dropped attributes. If the value is 0, then + /// no attributes were dropped. + #[prost(uint32, tag = "2")] + pub dropped_attributes_count: u32, + /// Set of entities that participate in this Resource. + /// + /// Note: keys in the references MUST exist in attributes of this message. + /// + /// Status: \[Development\] + #[prost(message, repeated, tag = "3")] + pub entity_refs: ::prost::alloc::vec::Vec, +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.trace.v1.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.trace.v1.rs new file mode 100644 index 00000000..f10ecfb8 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.trace.v1.rs @@ -0,0 +1,529 @@ +// This file is @generated by prost-build. +/// TracesData represents the traces data that can be stored in a persistent storage, +/// OR can be embedded by other protocols that transfer OTLP traces data but do +/// not implement the OTLP protocol. +/// +/// The main difference between this message and collector protocol is that +/// in this message there will not be any "control" or "metadata" specific to +/// OTLP protocol. +/// +/// When new fields are added into this message, the OTLP request MUST be updated +/// as well. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TracesData { + /// An array of ResourceSpans. + /// For data coming from a single resource this array will typically contain + /// one element. Intermediary nodes that receive data from multiple origins + /// typically batch the data before forwarding further and in that case this + /// array will contain multiple elements. + #[prost(message, repeated, tag = "1")] + pub resource_spans: ::prost::alloc::vec::Vec, +} +/// A collection of ScopeSpans from a Resource. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ResourceSpans { + /// The resource for the spans in this message. + /// If this field is not set then no resource info is known. + #[prost(message, optional, tag = "1")] + pub resource: ::core::option::Option, + /// A list of ScopeSpans that originate from a resource. + #[prost(message, repeated, tag = "2")] + pub scope_spans: ::prost::alloc::vec::Vec, + /// The Schema URL, if known. This is the identifier of the Schema that the resource data + /// is recorded in. Notably, the last part of the URL path is the version number of the + /// schema: http\[s\]://server\[:port\]/path/. To learn more about Schema URL see + /// + /// This schema_url applies to the data in the "resource" field. It does not apply + /// to the data in the "scope_spans" field which have their own schema_url field. + #[prost(string, tag = "3")] + pub schema_url: ::prost::alloc::string::String, +} +/// A collection of Spans produced by an InstrumentationScope. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ScopeSpans { + /// The instrumentation scope information for the spans in this message. + /// Semantically when InstrumentationScope isn't set, it is equivalent with + /// an empty instrumentation scope name (unknown). + #[prost(message, optional, tag = "1")] + pub scope: ::core::option::Option, + /// A list of Spans that originate from an instrumentation scope. + #[prost(message, repeated, tag = "2")] + pub spans: ::prost::alloc::vec::Vec, + /// The Schema URL, if known. This is the identifier of the Schema that the span data + /// is recorded in. Notably, the last part of the URL path is the version number of the + /// schema: http\[s\]://server\[:port\]/path/. To learn more about Schema URL see + /// + /// This schema_url applies to the data in the "scope" field and all spans and span + /// events in the "spans" field. + #[prost(string, tag = "3")] + pub schema_url: ::prost::alloc::string::String, +} +/// A Span represents a single operation performed by a single component of the system. +/// +/// The next available field id is 17. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Span { + /// A unique identifier for a trace. All spans from the same trace share + /// the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR + /// of length other than 16 bytes is considered invalid (empty string in OTLP/JSON + /// is zero-length and thus is also invalid). + /// + /// This field is required. + #[prost(bytes = "vec", tag = "1")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_to_hex_string", + deserialize_with = "crate::proto::serializers::deserialize_from_hex_string" + ) + )] + pub trace_id: ::prost::alloc::vec::Vec, + /// A unique identifier for a span within a trace, assigned when the span + /// is created. The ID is an 8-byte array. An ID with all zeroes OR of length + /// other than 8 bytes is considered invalid (empty string in OTLP/JSON + /// is zero-length and thus is also invalid). + /// + /// This field is required. + #[prost(bytes = "vec", tag = "2")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_to_hex_string", + deserialize_with = "crate::proto::serializers::deserialize_from_hex_string" + ) + )] + pub span_id: ::prost::alloc::vec::Vec, + /// trace_state conveys information about request position in multiple distributed tracing graphs. + /// It is a trace_state in w3c-trace-context format: + /// See also for more details about this field. + #[prost(string, tag = "3")] + pub trace_state: ::prost::alloc::string::String, + /// The `span_id` of this span's parent span. If this is a root span, then this + /// field must be empty. The ID is an 8-byte array. + #[prost(bytes = "vec", tag = "4")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_to_hex_string", + deserialize_with = "crate::proto::serializers::deserialize_from_hex_string" + ) + )] + pub parent_span_id: ::prost::alloc::vec::Vec, + /// Flags, a bit field. + /// + /// Bits 0-7 (8 least significant bits) are the trace flags as defined in W3C Trace + /// Context specification. To read the 8-bit W3C trace flag, use + /// `flags & SPAN_FLAGS_TRACE_FLAGS_MASK`. + /// + /// See for the flag definitions. + /// + /// Bits 8 and 9 represent the 3 states of whether a span's parent + /// is remote. The states are (unknown, is not remote, is remote). + /// To read whether the value is known, use `(flags & SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) != 0`. + /// To read whether the span is remote, use `(flags & SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) != 0`. + /// + /// When creating span messages, if the message is logically forwarded from another source + /// with an equivalent flags fields (i.e., usually another OTLP span message), the field SHOULD + /// be copied as-is. If creating from a source that does not have an equivalent flags field + /// (such as a runtime representation of an OpenTelemetry span), the high 22 bits MUST + /// be set to zero. + /// Readers MUST NOT assume that bits 10-31 (22 most significant bits) will be zero. + /// + /// \[Optional\]. + #[prost(fixed32, tag = "16")] + pub flags: u32, + /// A description of the span's operation. + /// + /// For example, the name can be a qualified method name or a file name + /// and a line number where the operation is called. A best practice is to use + /// the same display name at the same call point in an application. + /// This makes it easier to correlate spans in different traces. + /// + /// This field is semantically required to be set to non-empty string. + /// Empty value is equivalent to an unknown span name. + /// + /// This field is required. + #[prost(string, tag = "5")] + pub name: ::prost::alloc::string::String, + /// Distinguishes between spans generated in a particular context. For example, + /// two spans with the same name may be distinguished using `CLIENT` (caller) + /// and `SERVER` (callee) to identify queueing latency associated with the span. + #[prost(enumeration = "span::SpanKind", tag = "6")] + pub kind: i32, + /// The start time of the span. On the client side, this is the time + /// kept by the local machine where the span execution starts. On the server side, this + /// is the time when the server's application handler starts running. + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + /// + /// This field is semantically required and it is expected that end_time >= start_time. + #[prost(fixed64, tag = "7")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub start_time_unix_nano: u64, + /// The end time of the span. On the client side, this is the time + /// kept by the local machine where the span execution ends. On the server side, this + /// is the time when the server application handler stops running. + /// Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + /// + /// This field is semantically required and it is expected that end_time >= start_time. + #[prost(fixed64, tag = "8")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub end_time_unix_nano: u64, + /// A collection of key/value pairs. Note, global attributes + /// like server name can be set using the resource API. Examples of attributes: + /// + /// ```text + /// "/http/user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36" + /// "/http/server_latency": 300 + /// "example.com/myattribute": true + /// "example.com/score": 10.239 + /// ``` + /// + /// Attribute keys MUST be unique (it is not allowed to have more than one + /// attribute with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "9")] + pub attributes: ::prost::alloc::vec::Vec, + /// The number of attributes that were discarded. Attributes + /// can be discarded because their keys are too long or because there are too many + /// attributes. If this value is 0, then no attributes were dropped. + #[prost(uint32, tag = "10")] + pub dropped_attributes_count: u32, + /// A collection of Event items. + #[prost(message, repeated, tag = "11")] + pub events: ::prost::alloc::vec::Vec, + /// The number of dropped events. If the value is 0, then no + /// events were dropped. + #[prost(uint32, tag = "12")] + pub dropped_events_count: u32, + /// A collection of Links, which are references from this span to a span + /// in the same or different trace. + #[prost(message, repeated, tag = "13")] + pub links: ::prost::alloc::vec::Vec, + /// The number of dropped links after the maximum size was + /// enforced. If this value is 0, then no links were dropped. + #[prost(uint32, tag = "14")] + pub dropped_links_count: u32, + /// An optional final status for this span. Semantically when Status isn't set, it means + /// span's status code is unset, i.e. assume STATUS_CODE_UNSET (code = 0). + #[prost(message, optional, tag = "15")] + pub status: ::core::option::Option, +} +/// Nested message and enum types in `Span`. +pub mod span { + /// Event is a time-stamped annotation of the span, consisting of user-supplied + /// text description and key-value pairs. + #[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] + #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] + #[cfg_attr(feature = "with-serde", serde(default))] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Event { + /// The time the event occurred. + #[prost(fixed64, tag = "1")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_u64_to_string", + deserialize_with = "crate::proto::serializers::deserialize_string_to_u64" + ) + )] + pub time_unix_nano: u64, + /// The name of the event. + /// This field is semantically required to be set to non-empty string. + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, + /// A collection of attribute key/value pairs on the event. + /// Attribute keys MUST be unique (it is not allowed to have more than one + /// attribute with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "3")] + pub attributes: ::prost::alloc::vec::Vec< + super::super::super::common::v1::KeyValue, + >, + /// The number of dropped attributes. If the value is 0, + /// then no attributes were dropped. + #[prost(uint32, tag = "4")] + pub dropped_attributes_count: u32, + } + /// A pointer from the current span to another span in the same trace or in a + /// different trace. For example, this can be used in batching operations, + /// where a single batch handler processes multiple requests from different + /// traces or when the handler receives a request from a different project. + #[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] + #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] + #[cfg_attr(feature = "with-serde", serde(default))] + #[derive(Clone, PartialEq, ::prost::Message)] + pub struct Link { + /// A unique identifier of a trace that this linked span is part of. The ID is a + /// 16-byte array. + #[prost(bytes = "vec", tag = "1")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_to_hex_string", + deserialize_with = "crate::proto::serializers::deserialize_from_hex_string" + ) + )] + pub trace_id: ::prost::alloc::vec::Vec, + /// A unique identifier for the linked span. The ID is an 8-byte array. + #[prost(bytes = "vec", tag = "2")] + #[cfg_attr( + feature = "with-serde", + serde( + serialize_with = "crate::proto::serializers::serialize_to_hex_string", + deserialize_with = "crate::proto::serializers::deserialize_from_hex_string" + ) + )] + pub span_id: ::prost::alloc::vec::Vec, + /// The trace_state associated with the link. + #[prost(string, tag = "3")] + pub trace_state: ::prost::alloc::string::String, + /// A collection of attribute key/value pairs on the link. + /// Attribute keys MUST be unique (it is not allowed to have more than one + /// attribute with the same key). + /// The behavior of software that receives duplicated keys can be unpredictable. + #[prost(message, repeated, tag = "4")] + pub attributes: ::prost::alloc::vec::Vec< + super::super::super::common::v1::KeyValue, + >, + /// The number of dropped attributes. If the value is 0, + /// then no attributes were dropped. + #[prost(uint32, tag = "5")] + pub dropped_attributes_count: u32, + /// Flags, a bit field. + /// + /// Bits 0-7 (8 least significant bits) are the trace flags as defined in W3C Trace + /// Context specification. To read the 8-bit W3C trace flag, use + /// `flags & SPAN_FLAGS_TRACE_FLAGS_MASK`. + /// + /// See for the flag definitions. + /// + /// Bits 8 and 9 represent the 3 states of whether the link is remote. + /// The states are (unknown, is not remote, is remote). + /// To read whether the value is known, use `(flags & SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) != 0`. + /// To read whether the link is remote, use `(flags & SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) != 0`. + /// + /// Readers MUST NOT assume that bits 10-31 (22 most significant bits) will be zero. + /// When creating new spans, bits 10-31 (most-significant 22-bits) MUST be zero. + /// + /// \[Optional\]. + #[prost(fixed32, tag = "6")] + pub flags: u32, + } + /// SpanKind is the type of span. Can be used to specify additional relationships between spans + /// in addition to a parent/child relationship. + #[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] + #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum SpanKind { + /// Unspecified. Do NOT use as default. + /// Implementations MAY assume SpanKind to be INTERNAL when receiving UNSPECIFIED. + Unspecified = 0, + /// Indicates that the span represents an internal operation within an application, + /// as opposed to an operation happening at the boundaries. Default value. + Internal = 1, + /// Indicates that the span covers server-side handling of an RPC or other + /// remote network request. + Server = 2, + /// Indicates that the span describes a request to some remote service. + Client = 3, + /// Indicates that the span describes a producer sending a message to a broker. + /// Unlike CLIENT and SERVER, there is often no direct critical path latency relationship + /// between producer and consumer spans. A PRODUCER span ends when the message was accepted + /// by the broker while the logical processing of the message might span a much longer time. + Producer = 4, + /// Indicates that the span describes consumer receiving a message from a broker. + /// Like the PRODUCER kind, there is often no direct critical path latency relationship + /// between producer and consumer spans. + Consumer = 5, + } + impl SpanKind { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unspecified => "SPAN_KIND_UNSPECIFIED", + Self::Internal => "SPAN_KIND_INTERNAL", + Self::Server => "SPAN_KIND_SERVER", + Self::Client => "SPAN_KIND_CLIENT", + Self::Producer => "SPAN_KIND_PRODUCER", + Self::Consumer => "SPAN_KIND_CONSUMER", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "SPAN_KIND_UNSPECIFIED" => Some(Self::Unspecified), + "SPAN_KIND_INTERNAL" => Some(Self::Internal), + "SPAN_KIND_SERVER" => Some(Self::Server), + "SPAN_KIND_CLIENT" => Some(Self::Client), + "SPAN_KIND_PRODUCER" => Some(Self::Producer), + "SPAN_KIND_CONSUMER" => Some(Self::Consumer), + _ => None, + } + } + } +} +/// The Status type defines a logical error model that is suitable for different +/// programming environments, including REST APIs and RPC APIs. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[cfg_attr(feature = "with-serde", serde(default))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Status { + /// A developer-facing human readable error message. + #[prost(string, tag = "2")] + pub message: ::prost::alloc::string::String, + /// The status code. + #[prost(enumeration = "status::StatusCode", tag = "3")] + pub code: i32, +} +/// Nested message and enum types in `Status`. +pub mod status { + /// For the semantics of status codes see + /// + #[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] + #[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] + #[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum StatusCode { + /// The default status. + Unset = 0, + /// The Span has been validated by an Application developer or Operator to + /// have completed successfully. + Ok = 1, + /// The Span contains an error. + Error = 2, + } + impl StatusCode { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::Unset => "STATUS_CODE_UNSET", + Self::Ok => "STATUS_CODE_OK", + Self::Error => "STATUS_CODE_ERROR", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "STATUS_CODE_UNSET" => Some(Self::Unset), + "STATUS_CODE_OK" => Some(Self::Ok), + "STATUS_CODE_ERROR" => Some(Self::Error), + _ => None, + } + } + } +} +/// SpanFlags represents constants used to interpret the +/// Span.flags field, which is protobuf 'fixed32' type and is to +/// be used as bit-fields. Each non-zero value defined in this enum is +/// a bit-mask. To extract the bit-field, for example, use an +/// expression like: +/// +/// (span.flags & SPAN_FLAGS_TRACE_FLAGS_MASK) +/// +/// See for the flag definitions. +/// +/// Note that Span flags were introduced in version 1.1 of the +/// OpenTelemetry protocol. Older Span producers do not set this +/// field, consequently consumers should not rely on the absence of a +/// particular flag bit to indicate the presence of a particular feature. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] +#[repr(i32)] +pub enum SpanFlags { + /// The zero value for the enum. Should not be used for comparisons. + /// Instead use bitwise "and" with the appropriate mask as shown above. + DoNotUse = 0, + /// Bits 0-7 are used for trace flags. + TraceFlagsMask = 255, + /// Bits 8 and 9 are used to indicate that the parent span or link span is remote. + /// Bit 8 (`HAS_IS_REMOTE`) indicates whether the value is known. + /// Bit 9 (`IS_REMOTE`) indicates whether the span or link is remote. + ContextHasIsRemoteMask = 256, + ContextIsRemoteMask = 512, +} +impl SpanFlags { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + Self::DoNotUse => "SPAN_FLAGS_DO_NOT_USE", + Self::TraceFlagsMask => "SPAN_FLAGS_TRACE_FLAGS_MASK", + Self::ContextHasIsRemoteMask => "SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK", + Self::ContextIsRemoteMask => "SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "SPAN_FLAGS_DO_NOT_USE" => Some(Self::DoNotUse), + "SPAN_FLAGS_TRACE_FLAGS_MASK" => Some(Self::TraceFlagsMask), + "SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK" => Some(Self::ContextHasIsRemoteMask), + "SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK" => Some(Self::ContextIsRemoteMask), + _ => None, + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.tracez.v1.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.tracez.v1.rs new file mode 100644 index 00000000..203b1e17 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tonic/opentelemetry.proto.tracez.v1.rs @@ -0,0 +1,90 @@ +// This file is @generated by prost-build. +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct TracezCounts { + #[prost(string, tag = "1")] + pub spanname: ::prost::alloc::string::String, + /// \[A\] + #[prost(uint32, repeated, tag = "2")] + pub latency: ::prost::alloc::vec::Vec, + #[prost(uint32, tag = "3")] + pub running: u32, + #[prost(uint32, tag = "4")] + pub error: u32, +} +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LatencyData { + #[prost(bytes = "vec", tag = "1")] + pub traceid: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "2")] + pub spanid: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "3")] + pub parentid: ::prost::alloc::vec::Vec, + #[prost(fixed64, tag = "4")] + pub starttime: u64, + #[prost(fixed64, tag = "5")] + pub endtime: u64, + /// \[1\] + #[prost(message, repeated, tag = "6")] + pub attributes: ::prost::alloc::vec::Vec, + /// \[2\] + #[prost(message, repeated, tag = "7")] + pub events: ::prost::alloc::vec::Vec, + /// \[3\] + #[prost(message, repeated, tag = "8")] + pub links: ::prost::alloc::vec::Vec, +} +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RunningData { + #[prost(bytes = "vec", tag = "1")] + pub traceid: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "2")] + pub spanid: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "3")] + pub parentid: ::prost::alloc::vec::Vec, + #[prost(fixed64, tag = "4")] + pub starttime: u64, + /// \[1\] + #[prost(message, repeated, tag = "5")] + pub attributes: ::prost::alloc::vec::Vec, + /// \[2\] + #[prost(message, repeated, tag = "6")] + pub events: ::prost::alloc::vec::Vec, + /// \[3\] + #[prost(message, repeated, tag = "7")] + pub links: ::prost::alloc::vec::Vec, +} +#[cfg_attr(feature = "with-schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "with-serde", serde(rename_all = "camelCase"))] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ErrorData { + #[prost(bytes = "vec", tag = "1")] + pub traceid: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "2")] + pub spanid: ::prost::alloc::vec::Vec, + #[prost(bytes = "vec", tag = "3")] + pub parentid: ::prost::alloc::vec::Vec, + #[prost(fixed64, tag = "4")] + pub starttime: u64, + /// \[1\] + #[prost(message, repeated, tag = "5")] + pub attributes: ::prost::alloc::vec::Vec, + /// \[2\] + #[prost(message, repeated, tag = "6")] + pub events: ::prost::alloc::vec::Vec, + /// \[3\] + #[prost(message, repeated, tag = "7")] + pub links: ::prost::alloc::vec::Vec, + /// \[4\] + #[prost(message, optional, tag = "8")] + pub status: ::core::option::Option, +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tracez.proto b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tracez.proto new file mode 100644 index 00000000..a3ee15b4 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/proto/tracez.proto @@ -0,0 +1,59 @@ +// Copyright 2019, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.tracez.v1; + +import "opentelemetry/proto/common/v1/common.proto"; +import "opentelemetry/proto/trace/v1/trace.proto"; + +message TracezCounts { + string spanname = 1; + repeated uint32 latency = 2; // [A] + uint32 running = 3; + uint32 error = 4; +} + +message LatencyData { + bytes traceid = 1; + bytes spanid = 2; + bytes parentid = 3; + fixed64 starttime = 4; + fixed64 endtime = 5; + repeated opentelemetry.proto.common.v1.KeyValue attributes = 6; // [1] + repeated opentelemetry.proto.trace.v1.Span.Event events = 7; // [2] + repeated opentelemetry.proto.trace.v1.Span.Link links = 8; // [3] +} + +message RunningData { + bytes traceid = 1; + bytes spanid = 2; + bytes parentid = 3; + fixed64 starttime = 4; + repeated opentelemetry.proto.common.v1.KeyValue attributes = 5; // [1] + repeated opentelemetry.proto.trace.v1.Span.Event events = 6; // [2] + repeated opentelemetry.proto.trace.v1.Span.Link links = 7; // [3] +} + +message ErrorData { + bytes traceid = 1; + bytes spanid = 2; + bytes parentid = 3; + fixed64 starttime = 4; + repeated opentelemetry.proto.common.v1.KeyValue attributes = 5; // [1] + repeated opentelemetry.proto.trace.v1.Span.Event events = 6; // [2] + repeated opentelemetry.proto.trace.v1.Span.Link links = 7; // [3] + opentelemetry.proto.trace.v1.Status status = 8; // [4] +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/common.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/common.rs new file mode 100644 index 00000000..d9f14e3b --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/common.rs @@ -0,0 +1,212 @@ +#[cfg(all( + feature = "gen-tonic-messages", + any(feature = "trace", feature = "metrics", feature = "logs") +))] +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +#[cfg(all( + feature = "gen-tonic-messages", + any(feature = "trace", feature = "metrics", feature = "logs") +))] +pub(crate) fn to_nanos(time: SystemTime) -> u64 { + time.duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| Duration::from_secs(0)) + .as_nanos() as u64 +} + +#[cfg(feature = "gen-tonic-messages")] +pub mod tonic { + use crate::proto::tonic::common::v1::{ + any_value, AnyValue, ArrayValue, InstrumentationScope, KeyValue, + }; + use opentelemetry::{Array, Value}; + use std::borrow::Cow; + + #[cfg(any(feature = "trace", feature = "logs"))] + #[derive(Debug, Default)] + pub struct ResourceAttributesWithSchema { + pub attributes: Attributes, + pub schema_url: Option, + } + + #[cfg(any(feature = "trace", feature = "logs"))] + impl From<&opentelemetry_sdk::Resource> for ResourceAttributesWithSchema { + fn from(resource: &opentelemetry_sdk::Resource) -> Self { + ResourceAttributesWithSchema { + attributes: resource_attributes(resource), + schema_url: resource.schema_url().map(ToString::to_string), + } + } + } + + #[cfg(any(feature = "trace", feature = "logs"))] + use opentelemetry_sdk::Resource; + + impl + From<( + opentelemetry::InstrumentationScope, + Option>, + )> for InstrumentationScope + { + fn from( + data: ( + opentelemetry::InstrumentationScope, + Option>, + ), + ) -> Self { + let (library, target) = data; + InstrumentationScope { + name: target.map_or_else(|| library.name().to_owned(), Cow::into_owned), + version: library.version().unwrap_or_default().to_owned(), + attributes: Attributes::from(library.attributes().cloned()).0, + ..Default::default() + } + } + } + + impl + From<( + &opentelemetry::InstrumentationScope, + Option>, + )> for InstrumentationScope + { + fn from( + data: ( + &opentelemetry::InstrumentationScope, + Option>, + ), + ) -> Self { + let (library, target) = data; + InstrumentationScope { + name: target.map_or_else(|| library.name().to_owned(), Cow::into_owned), + version: library.version().unwrap_or_default().to_owned(), + attributes: Attributes::from(library.attributes().cloned()).0, + ..Default::default() + } + } + } + + /// Wrapper type for Vec<`KeyValue`> + #[derive(Default, Debug)] + pub struct Attributes(pub ::std::vec::Vec); + + impl> From for Attributes { + fn from(kvs: I) -> Self { + Attributes( + kvs.into_iter() + .map(|api_kv| KeyValue { + key: api_kv.key.as_str().to_string(), + value: Some(api_kv.value.into()), + key_strindex: 0, + }) + .collect(), + ) + } + } + + #[cfg(feature = "logs")] + impl, V: Into> FromIterator<(K, V)> for Attributes { + fn from_iter>(iter: T) -> Self { + Attributes( + iter.into_iter() + .map(|(k, v)| KeyValue { + key: k.into(), + value: Some(v.into()), + key_strindex: 0, + }) + .collect(), + ) + } + } + + impl From for AnyValue { + fn from(value: Value) -> Self { + AnyValue { + value: match value { + Value::Bool(val) => Some(any_value::Value::BoolValue(val)), + Value::I64(val) => Some(any_value::Value::IntValue(val)), + Value::F64(val) => Some(any_value::Value::DoubleValue(val)), + Value::String(val) => Some(any_value::Value::StringValue(val.to_string())), + Value::Array(array) => Some(any_value::Value::ArrayValue(match array { + Array::Bool(vals) => array_into_proto(vals), + Array::I64(vals) => array_into_proto(vals), + Array::F64(vals) => array_into_proto(vals), + Array::String(vals) => array_into_proto(vals), + _ => unreachable!("Nonexistent array type"), // Needs to be updated when new array types are added + })), + _ => unreachable!("Nonexistent value type"), // Needs to be updated when new value types are added + }, + } + } + } + + fn array_into_proto(vals: Vec) -> ArrayValue + where + Value: From, + { + let values = vals + .into_iter() + .map(|val| AnyValue::from(Value::from(val))) + .collect(); + + ArrayValue { values } + } + + #[cfg(any(feature = "trace", feature = "logs"))] + pub(crate) fn resource_attributes(resource: &Resource) -> Attributes { + resource + .iter() + .map(|(k, v)| opentelemetry::KeyValue::new(k.clone(), v.clone())) + .collect::>() + .into() + } + + #[cfg(test)] + mod tests { + use super::*; + use opentelemetry::KeyValue; + use std::borrow::Cow; + + fn assert_scope_fields( + proto_scope: &InstrumentationScope, + expected_name: &str, + expected_version: &str, + expected_attr_key: &str, + ) { + assert_eq!(proto_scope.name, expected_name); + assert_eq!(proto_scope.version, expected_version); + assert_eq!(proto_scope.attributes.len(), 1); + assert_eq!(proto_scope.attributes[0].key, expected_attr_key); + } + + #[test] + fn instrumentation_scope_with_target_overrides_name_but_preserves_version_and_attributes() { + let scope = opentelemetry::InstrumentationScope::builder("my-lib") + .with_version("1.0.0") + .with_attributes([KeyValue::new("feature", "metrics")]) + .build(); + let target: Option> = Some(Cow::Borrowed("my_app::handlers")); + + let from_owned = InstrumentationScope::from((scope.clone(), target.clone())); + let from_ref = InstrumentationScope::from((&scope, target)); + + assert_scope_fields(&from_owned, "my_app::handlers", "1.0.0", "feature"); + assert_scope_fields(&from_ref, "my_app::handlers", "1.0.0", "feature"); + } + + #[test] + fn instrumentation_scope_without_target_preserves_all_fields() { + let scope = opentelemetry::InstrumentationScope::builder("my-lib") + .with_version("1.0.0") + .with_attributes([KeyValue::new("feature", "metrics")]) + .build(); + let target: Option> = None; + + let from_owned = InstrumentationScope::from((scope.clone(), target.clone())); + let from_ref = InstrumentationScope::from((&scope, target)); + + assert_scope_fields(&from_owned, "my-lib", "1.0.0", "feature"); + assert_scope_fields(&from_ref, "my-lib", "1.0.0", "feature"); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/logs.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/logs.rs new file mode 100644 index 00000000..1832eb56 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/logs.rs @@ -0,0 +1,525 @@ +#[cfg(feature = "gen-tonic-messages")] +pub mod tonic { + use crate::{ + tonic::{ + common::v1::{ + any_value::Value, AnyValue, ArrayValue, InstrumentationScope, KeyValue, + KeyValueList, + }, + logs::v1::{LogRecord, ResourceLogs, ScopeLogs, SeverityNumber}, + resource::v1::Resource, + }, + transform::common::{to_nanos, tonic::ResourceAttributesWithSchema}, + }; + use opentelemetry::logs::{AnyValue as LogsAnyValue, Severity}; + use opentelemetry_sdk::logs::LogBatch; + use std::borrow::Cow; + use std::collections::HashMap; + + impl From for AnyValue { + fn from(value: LogsAnyValue) -> Self { + AnyValue { + value: Some(value.into()), + } + } + } + + impl From for Value { + fn from(value: LogsAnyValue) -> Self { + match value { + LogsAnyValue::Double(f) => Value::DoubleValue(f), + LogsAnyValue::Int(i) => Value::IntValue(i), + LogsAnyValue::String(s) => Value::StringValue(s.into()), + LogsAnyValue::Boolean(b) => Value::BoolValue(b), + LogsAnyValue::ListAny(v) => Value::ArrayValue(ArrayValue { + values: v + .into_iter() + .map(|v| AnyValue { + value: Some(v.into()), + }) + .collect(), + }), + LogsAnyValue::Map(m) => Value::KvlistValue(KeyValueList { + values: m + .into_iter() + .map(|(key, value)| KeyValue { + key: key.into(), + value: Some(AnyValue { + value: Some(value.into()), + }), + key_strindex: 0, + }) + .collect(), + }), + LogsAnyValue::Bytes(v) => Value::BytesValue(*v), + _ => unreachable!("Nonexistent value type"), + } + } + } + + impl From<&opentelemetry_sdk::logs::SdkLogRecord> for LogRecord { + fn from(log_record: &opentelemetry_sdk::logs::SdkLogRecord) -> Self { + let trace_context = log_record.trace_context(); + let severity_number = match log_record.severity_number() { + Some(Severity::Trace) => SeverityNumber::Trace, + Some(Severity::Trace2) => SeverityNumber::Trace2, + Some(Severity::Trace3) => SeverityNumber::Trace3, + Some(Severity::Trace4) => SeverityNumber::Trace4, + Some(Severity::Debug) => SeverityNumber::Debug, + Some(Severity::Debug2) => SeverityNumber::Debug2, + Some(Severity::Debug3) => SeverityNumber::Debug3, + Some(Severity::Debug4) => SeverityNumber::Debug4, + Some(Severity::Info) => SeverityNumber::Info, + Some(Severity::Info2) => SeverityNumber::Info2, + Some(Severity::Info3) => SeverityNumber::Info3, + Some(Severity::Info4) => SeverityNumber::Info4, + Some(Severity::Warn) => SeverityNumber::Warn, + Some(Severity::Warn2) => SeverityNumber::Warn2, + Some(Severity::Warn3) => SeverityNumber::Warn3, + Some(Severity::Warn4) => SeverityNumber::Warn4, + Some(Severity::Error) => SeverityNumber::Error, + Some(Severity::Error2) => SeverityNumber::Error2, + Some(Severity::Error3) => SeverityNumber::Error3, + Some(Severity::Error4) => SeverityNumber::Error4, + Some(Severity::Fatal) => SeverityNumber::Fatal, + Some(Severity::Fatal2) => SeverityNumber::Fatal2, + Some(Severity::Fatal3) => SeverityNumber::Fatal3, + Some(Severity::Fatal4) => SeverityNumber::Fatal4, + None => SeverityNumber::Unspecified, + }; + + LogRecord { + time_unix_nano: log_record.timestamp().map(to_nanos).unwrap_or_default(), + observed_time_unix_nano: to_nanos(log_record.observed_timestamp().unwrap()), + attributes: { + log_record + .attributes_iter() + .map(|kv| KeyValue { + key: kv.0.to_string(), + value: Some(AnyValue { + value: Some(kv.1.clone().into()), + }), + key_strindex: 0, + }) + .collect() + }, + event_name: log_record.event_name().unwrap_or_default().into(), + severity_number: severity_number.into(), + severity_text: log_record + .severity_text() + .map(Into::into) + .unwrap_or_default(), + body: log_record.body().cloned().map(Into::into), + dropped_attributes_count: 0, + flags: trace_context + .map(|ctx| { + ctx.trace_flags + .map(|flags| flags.to_u8() as u32) + .unwrap_or_default() + }) + .unwrap_or_default(), + span_id: trace_context + .map(|ctx| ctx.span_id.to_bytes().to_vec()) + .unwrap_or_default(), + trace_id: trace_context + .map(|ctx| ctx.trace_id.to_bytes().to_vec()) + .unwrap_or_default(), + } + } + } + + impl + From<( + ( + &opentelemetry_sdk::logs::SdkLogRecord, + &opentelemetry::InstrumentationScope, + ), + &ResourceAttributesWithSchema, + )> for ResourceLogs + { + fn from( + data: ( + ( + &opentelemetry_sdk::logs::SdkLogRecord, + &opentelemetry::InstrumentationScope, + ), + &ResourceAttributesWithSchema, + ), + ) -> Self { + let ((log_record, instrumentation), resource) = data; + + ResourceLogs { + resource: Some(Resource { + attributes: resource.attributes.0.clone(), + dropped_attributes_count: 0, + entity_refs: vec![], + }), + schema_url: resource.schema_url.clone().unwrap_or_default(), + scope_logs: vec![ScopeLogs { + schema_url: instrumentation + .schema_url() + .map(ToOwned::to_owned) + .unwrap_or_default(), + scope: Some((instrumentation, log_record.target().cloned()).into()), + log_records: vec![log_record.into()], + }], + } + } + } + + pub fn group_logs_by_resource_and_scope<'a>( + logs: &'a LogBatch<'a>, + resource: &ResourceAttributesWithSchema, + ) -> Vec { + // Group by the exported scope: target overrides only the name. + let scope_map = logs.iter().fold( + HashMap::new(), + |mut scope_map: HashMap< + opentelemetry::InstrumentationScope, + Vec<( + &opentelemetry_sdk::logs::SdkLogRecord, + &opentelemetry::InstrumentationScope, + )>, + >, + (log_record, instrumentation)| { + let name = log_record + .target() + .cloned() + .unwrap_or_else(|| Cow::Owned(instrumentation.name().to_owned())); + let key = opentelemetry::InstrumentationScope::builder(name) + .with_version(instrumentation.version().unwrap_or_default().to_owned()) + .with_schema_url(instrumentation.schema_url().unwrap_or_default().to_owned()) + .with_attributes(instrumentation.attributes().cloned()) + .build(); + scope_map + .entry(key) + .or_default() + .push((log_record, instrumentation)); + scope_map + }, + ); + + let scope_logs = scope_map + .into_iter() + .map(|(key, log_data)| ScopeLogs { + scope: Some(InstrumentationScope::from((&key, None))), + schema_url: key.schema_url().unwrap_or_default().to_owned(), + log_records: log_data + .into_iter() + .map(|(log_record, _)| log_record.into()) + .collect(), + }) + .collect(); + + vec![ResourceLogs { + resource: Some(Resource { + attributes: resource.attributes.0.clone(), + dropped_attributes_count: 0, + entity_refs: vec![], + }), + scope_logs, + schema_url: resource.schema_url.clone().unwrap_or_default(), + }] + } +} + +#[cfg(test)] +mod tests { + use crate::transform::common::tonic::ResourceAttributesWithSchema; + use opentelemetry::logs::LogRecord as _; + use opentelemetry::logs::Logger; + use opentelemetry::logs::LoggerProvider; + use opentelemetry::time::now; + use opentelemetry::{InstrumentationScope, KeyValue}; + use opentelemetry_sdk::error::OTelSdkResult; + use opentelemetry_sdk::logs::LogProcessor; + use opentelemetry_sdk::logs::SdkLoggerProvider; + use opentelemetry_sdk::{logs::LogBatch, logs::SdkLogRecord, Resource}; + use std::borrow::Cow; + + #[derive(Debug)] + struct MockProcessor; + + impl LogProcessor for MockProcessor { + fn emit(&self, _record: &mut SdkLogRecord, _instrumentation: &InstrumentationScope) {} + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } + } + + fn create_test_log_data( + instrumentation_name: &str, + _message: &str, + ) -> (SdkLogRecord, InstrumentationScope) { + let processor = MockProcessor {}; + let logger = SdkLoggerProvider::builder() + .with_log_processor(processor) + .build() + .logger("test"); + let mut logrecord = logger.create_log_record(); + logrecord.set_timestamp(now()); + logrecord.set_observed_timestamp(now()); + let instrumentation = + InstrumentationScope::builder(instrumentation_name.to_string()).build(); + (logrecord, instrumentation) + } + + #[test] + fn test_group_logs_by_resource_and_scope_single_scope() { + let resource = Resource::builder().build(); + let (log_record1, instrum_lib1) = create_test_log_data("test-lib", "Log 1"); + let (log_record2, instrum_lib2) = create_test_log_data("test-lib", "Log 2"); + + let logs = [(&log_record1, &instrum_lib1), (&log_record2, &instrum_lib2)]; + let log_batch = LogBatch::new(&logs); + let resource: ResourceAttributesWithSchema = (&resource).into(); // Convert Resource to ResourceAttributesWithSchema + + let grouped_logs = + crate::transform::logs::tonic::group_logs_by_resource_and_scope(&log_batch, &resource); + + assert_eq!(grouped_logs.len(), 1); + let resource_logs = &grouped_logs[0]; + assert_eq!(resource_logs.scope_logs.len(), 1); + + let scope_logs = &resource_logs.scope_logs[0]; + assert_eq!(scope_logs.log_records.len(), 2); + } + + #[test] + fn test_group_logs_by_resource_and_scope_multiple_scopes() { + let resource = Resource::builder().build(); + let (log_record1, instrum_lib1) = create_test_log_data("lib1", "Log 1"); + let (log_record2, instrum_lib2) = create_test_log_data("lib2", "Log 2"); + + let logs = [(&log_record1, &instrum_lib1), (&log_record2, &instrum_lib2)]; + let log_batch = LogBatch::new(&logs); + let resource: ResourceAttributesWithSchema = (&resource).into(); // Convert Resource to ResourceAttributesWithSchema + let grouped_logs = + crate::transform::logs::tonic::group_logs_by_resource_and_scope(&log_batch, &resource); + + assert_eq!(grouped_logs.len(), 1); + let resource_logs = &grouped_logs[0]; + assert_eq!(resource_logs.scope_logs.len(), 2); + + let scope_logs_1 = &resource_logs + .scope_logs + .iter() + .find(|scope| scope.scope.as_ref().unwrap().name == "lib1") + .unwrap(); + let scope_logs_2 = &resource_logs + .scope_logs + .iter() + .find(|scope| scope.scope.as_ref().unwrap().name == "lib2") + .unwrap(); + + assert_eq!(scope_logs_1.log_records.len(), 1); + assert_eq!(scope_logs_2.log_records.len(), 1); + } + + #[test] + fn scope_grouping_same_target_preserves_distinct_versions() { + let (mut first, _) = create_test_log_data("bridge", "first"); + let (mut second, _) = create_test_log_data("bridge", "second"); + first.set_target(Cow::Borrowed("my_app::handlers")); + second.set_target(Cow::Borrowed("my_app::handlers")); + let first_scope = InstrumentationScope::builder("bridge") + .with_version("1.0") + .build(); + let second_scope = InstrumentationScope::builder("bridge") + .with_version("2.0") + .build(); + let logs = [(&first, &first_scope), (&second, &second_scope)]; + let batch = LogBatch::new(&logs); + let grouped = crate::transform::logs::tonic::group_logs_by_resource_and_scope( + &batch, + &ResourceAttributesWithSchema::default(), + ); + + // Target still supplies the exported scope name. Version distinguishes groups. + let mut actual: Vec<_> = grouped[0] + .scope_logs + .iter() + .map(|group| { + let scope = group.scope.as_ref().unwrap(); + ( + scope.name.as_str(), + scope.version.as_str(), + group.log_records.len(), + ) + }) + .collect(); + actual.sort_unstable(); + assert_eq!( + actual, + vec![ + ("my_app::handlers", "1.0", 1), + ("my_app::handlers", "2.0", 1), + ] + ); + } + + #[test] + fn scope_grouping_respects_effective_metadata() { + let cases = [ + ( + "different attributes", + InstrumentationScope::builder("bridge") + .with_attributes([KeyValue::new("source", "a")]) + .build(), + InstrumentationScope::builder("bridge") + .with_attributes([KeyValue::new("source", "b")]) + .build(), + 2, + ), + ( + "different schema URLs", + InstrumentationScope::builder("bridge") + .with_schema_url("https://scope.example/v1") + .build(), + InstrumentationScope::builder("bridge") + .with_schema_url("https://scope.example/v2") + .build(), + 2, + ), + ( + "attribute order is irrelevant", + InstrumentationScope::builder("bridge") + .with_attributes([KeyValue::new("a", "1"), KeyValue::new("b", "2")]) + .build(), + InstrumentationScope::builder("bridge") + .with_attributes([KeyValue::new("b", "2"), KeyValue::new("a", "1")]) + .build(), + 1, + ), + ( + "target overrides different original names", + InstrumentationScope::builder("bridge-a") + .with_version("1") + .build(), + InstrumentationScope::builder("bridge-b") + .with_version("1") + .build(), + 1, + ), + ]; + for (description, first_scope, second_scope, expected_groups) in cases { + let (mut first, _) = create_test_log_data("", "first"); + let (mut second, _) = create_test_log_data("", "second"); + first.set_target(Cow::Borrowed("shared-target")); + second.set_target(Cow::Borrowed("shared-target")); + first.set_body("first".into()); + second.set_body("second".into()); + let logs = [(&first, &first_scope), (&second, &second_scope)]; + let batch = LogBatch::new(&logs); + let resource = ResourceAttributesWithSchema::default(); + let grouped = + crate::transform::logs::tonic::group_logs_by_resource_and_scope(&batch, &resource); + assert_eq!( + grouped[0].scope_logs.len(), + expected_groups, + "{description}" + ); + // Every record must retain the same metadata as when exported alone. + for (record, scope) in logs { + let single = + crate::tonic::logs::v1::ResourceLogs::from(((record, scope), &resource)); + let expected = &single.scope_logs[0]; + let actual = grouped[0] + .scope_logs + .iter() + .find(|group| group.log_records.contains(&expected.log_records[0])) + .unwrap(); + let mut actual_scope = actual.scope.clone().unwrap(); + let mut expected_scope = expected.scope.clone().unwrap(); + actual_scope.attributes.sort_by(|a, b| a.key.cmp(&b.key)); + expected_scope.attributes.sort_by(|a, b| a.key.cmp(&b.key)); + assert_eq!(actual_scope, expected_scope, "{description}"); + assert_eq!(actual.schema_url, expected.schema_url, "{description}"); + } + } + } + + #[test] + fn scope_grouping_same_target_empty_metadata_stays_together() { + let (mut first, first_scope) = create_test_log_data("", "first"); + let (mut second, second_scope) = create_test_log_data("", "second"); + first.set_target(Cow::Borrowed("my_app::handlers")); + second.set_target(Cow::Borrowed("my_app::handlers")); + let logs = [(&first, &first_scope), (&second, &second_scope)]; + let batch = LogBatch::new(&logs); + let grouped = crate::transform::logs::tonic::group_logs_by_resource_and_scope( + &batch, + &ResourceAttributesWithSchema::default(), + ); + + // Typical appender logs with the same target should remain one group. + assert_eq!(grouped[0].scope_logs.len(), 1); + let group = &grouped[0].scope_logs[0]; + assert_eq!(group.scope.as_ref().unwrap().name, "my_app::handlers"); + assert_eq!(group.log_records.len(), 2); + } + + #[test] + fn scope_grouping_uses_scope_schema_instead_of_resource_schema() { + let (mut record, _) = create_test_log_data("bridge", "message"); + record.set_target(Cow::Borrowed("my_app::handlers")); + let scope = InstrumentationScope::builder("bridge") + .with_schema_url("https://scope.example/schema") + .build(); + let resource = ResourceAttributesWithSchema { + schema_url: Some("https://resource.example/schema".to_owned()), + ..Default::default() + }; + let logs = [(&record, &scope)]; + let batch = LogBatch::new(&logs); + let grouped = + crate::transform::logs::tonic::group_logs_by_resource_and_scope(&batch, &resource); + + assert_eq!(grouped[0].schema_url, "https://resource.example/schema"); + let group = &grouped[0].scope_logs[0]; + assert_eq!(group.scope.as_ref().unwrap().name, "my_app::handlers"); + assert_eq!(group.schema_url, "https://scope.example/schema"); + } + + #[test] + fn test_group_logs_preserves_scope_version_and_attributes_when_target_set() { + let resource = Resource::builder().build(); + let processor = MockProcessor {}; + let logger = SdkLoggerProvider::builder() + .with_log_processor(processor) + .build() + .logger("test"); + + let mut logrecord = logger.create_log_record(); + logrecord.set_timestamp(now()); + logrecord.set_observed_timestamp(now()); + logrecord.set_target(Cow::Borrowed("my_app::handlers")); + + let instrumentation = InstrumentationScope::builder("my-lib") + .with_version("1.0.0") + .with_attributes([KeyValue::new("feature", "metrics")]) + .build(); + + let logs = [(&logrecord, &instrumentation)]; + let log_batch = LogBatch::new(&logs); + let resource: ResourceAttributesWithSchema = (&resource).into(); + + let grouped_logs = + crate::transform::logs::tonic::group_logs_by_resource_and_scope(&log_batch, &resource); + + assert_eq!(grouped_logs.len(), 1); + let resource_logs = &grouped_logs[0]; + assert_eq!(resource_logs.scope_logs.len(), 1); + + let scope = resource_logs.scope_logs[0].scope.as_ref().unwrap(); + assert_eq!(scope.name, "my_app::handlers"); + assert_eq!(scope.version, "1.0.0"); + assert_eq!(scope.attributes.len(), 1); + assert_eq!(scope.attributes[0].key, "feature"); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/metrics.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/metrics.rs new file mode 100644 index 00000000..a84e147e --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/metrics.rs @@ -0,0 +1,334 @@ +// The prost currently will generate a non optional deprecated field for labels. +// We cannot assign value to it otherwise clippy will complain. +// We cannot ignore it as it's not an optional field. +// We can remove this after we removed the labels field from proto. +#[allow(deprecated)] +#[cfg(feature = "gen-tonic-messages")] +pub mod tonic { + use std::fmt::Debug; + + use opentelemetry::{otel_debug, Key, Value}; + use opentelemetry_sdk::metrics::data::{ + AggregatedMetrics, Exemplar as SdkExemplar, + ExponentialHistogram as SdkExponentialHistogram, Gauge as SdkGauge, + Histogram as SdkHistogram, Metric as SdkMetric, MetricData, ResourceMetrics, + ScopeMetrics as SdkScopeMetrics, Sum as SdkSum, + }; + use opentelemetry_sdk::metrics::Temporality; + use opentelemetry_sdk::Resource as SdkResource; + + use crate::proto::tonic::{ + collector::metrics::v1::ExportMetricsServiceRequest, + common::v1::KeyValue, + metrics::v1::{ + exemplar, exemplar::Value as TonicExemplarValue, + exponential_histogram_data_point::Buckets as TonicBuckets, + metric::Data as TonicMetricData, number_data_point, + number_data_point::Value as TonicDataPointValue, + AggregationTemporality as TonicTemporality, AggregationTemporality, + DataPointFlags as TonicDataPointFlags, Exemplar as TonicExemplar, + ExponentialHistogram as TonicExponentialHistogram, + ExponentialHistogramDataPoint as TonicExponentialHistogramDataPoint, + Gauge as TonicGauge, Histogram as TonicHistogram, + HistogramDataPoint as TonicHistogramDataPoint, Metric as TonicMetric, + NumberDataPoint as TonicNumberDataPoint, ResourceMetrics as TonicResourceMetrics, + ScopeMetrics as TonicScopeMetrics, Sum as TonicSum, + }, + resource::v1::Resource as TonicResource, + }; + use crate::transform::common::to_nanos; + + impl From for exemplar::Value { + fn from(value: u64) -> Self { + exemplar::Value::AsInt(i64::try_from(value).unwrap_or_default()) + } + } + + impl From for exemplar::Value { + fn from(value: i64) -> Self { + exemplar::Value::AsInt(value) + } + } + + impl From for exemplar::Value { + fn from(value: f64) -> Self { + exemplar::Value::AsDouble(value) + } + } + + impl From for number_data_point::Value { + fn from(value: u64) -> Self { + number_data_point::Value::AsInt(i64::try_from(value).unwrap_or_default()) + } + } + + impl From for number_data_point::Value { + fn from(value: i64) -> Self { + number_data_point::Value::AsInt(value) + } + } + + impl From for number_data_point::Value { + fn from(value: f64) -> Self { + number_data_point::Value::AsDouble(value) + } + } + + impl From<(&Key, &Value)> for KeyValue { + fn from(kv: (&Key, &Value)) -> Self { + KeyValue { + key: kv.0.to_string(), + value: Some(kv.1.clone().into()), + key_strindex: 0, + } + } + } + + impl From<&opentelemetry::KeyValue> for KeyValue { + fn from(kv: &opentelemetry::KeyValue) -> Self { + KeyValue { + key: kv.key.to_string(), + value: Some(kv.value.clone().into()), + key_strindex: 0, + } + } + } + + impl From for AggregationTemporality { + fn from(temporality: Temporality) -> Self { + match temporality { + Temporality::Cumulative => AggregationTemporality::Cumulative, + Temporality::Delta => AggregationTemporality::Delta, + other => { + otel_debug!( + name: "AggregationTemporality::Unknown", + message = "Unknown temporality,using default instead.", + unknown_temporality = format!("{:?}", other), + default_temporality = format!("{:?}", Temporality::Cumulative) + ); + AggregationTemporality::Cumulative + } + } + } + } + + impl From<&ResourceMetrics> for ExportMetricsServiceRequest { + fn from(rm: &ResourceMetrics) -> Self { + ExportMetricsServiceRequest { + resource_metrics: vec![TonicResourceMetrics { + resource: Some((rm.resource()).into()), + scope_metrics: rm.scope_metrics().map(Into::into).collect(), + schema_url: rm + .resource() + .schema_url() + .map(Into::into) + .unwrap_or_default(), + }], + } + } + } + + impl From<&SdkResource> for TonicResource { + fn from(resource: &SdkResource) -> Self { + TonicResource { + attributes: resource.iter().map(Into::into).collect(), + dropped_attributes_count: 0, + entity_refs: vec![], // internal and currently unused + } + } + } + + impl From<&SdkScopeMetrics> for TonicScopeMetrics { + fn from(sm: &SdkScopeMetrics) -> Self { + TonicScopeMetrics { + scope: Some((sm.scope(), None).into()), + metrics: sm.metrics().map(Into::into).collect(), + schema_url: sm + .scope() + .schema_url() + .map(ToOwned::to_owned) + .unwrap_or_default(), + } + } + } + + impl From<&SdkMetric> for TonicMetric { + fn from(metric: &SdkMetric) -> Self { + TonicMetric { + name: metric.name().to_string(), + description: metric.description().to_string(), + unit: metric.unit().to_string(), + metadata: vec![], // internal and currently unused + data: Some(match metric.data() { + AggregatedMetrics::F64(data) => data.into(), + AggregatedMetrics::U64(data) => data.into(), + AggregatedMetrics::I64(data) => data.into(), + }), + } + } + } + + impl From<&MetricData> for TonicMetricData + where + T: Numeric + Debug, + { + fn from(data: &MetricData) -> Self { + match data { + MetricData::Gauge(gauge) => TonicMetricData::Gauge(gauge.into()), + MetricData::Sum(sum) => TonicMetricData::Sum(sum.into()), + MetricData::Histogram(hist) => TonicMetricData::Histogram(hist.into()), + MetricData::ExponentialHistogram(hist) => { + TonicMetricData::ExponentialHistogram(hist.into()) + } + } + } + } + + trait Numeric: Into + Into + Copy { + // lossy at large values for u64 and i64 but otlp histograms only handle float values + fn into_f64(self) -> f64; + } + + impl Numeric for u64 { + fn into_f64(self) -> f64 { + self as f64 + } + } + + impl Numeric for i64 { + fn into_f64(self) -> f64 { + self as f64 + } + } + + impl Numeric for f64 { + fn into_f64(self) -> f64 { + self + } + } + + impl From<&SdkHistogram> for TonicHistogram + where + T: Numeric, + { + fn from(hist: &SdkHistogram) -> Self { + TonicHistogram { + data_points: hist + .data_points() + .map(|dp| TonicHistogramDataPoint { + attributes: dp.attributes().map(Into::into).collect(), + start_time_unix_nano: to_nanos(hist.start_time()), + time_unix_nano: to_nanos(hist.time()), + count: dp.count(), + sum: Some(dp.sum().into_f64()), + bucket_counts: dp.bucket_counts().collect(), + explicit_bounds: dp.bounds().collect(), + exemplars: dp.exemplars().map(Into::into).collect(), + flags: TonicDataPointFlags::default() as u32, + min: dp.min().map(Numeric::into_f64), + max: dp.max().map(Numeric::into_f64), + }) + .collect(), + aggregation_temporality: TonicTemporality::from(hist.temporality()).into(), + } + } + } + + impl From<&SdkExponentialHistogram> for TonicExponentialHistogram + where + T: Numeric, + { + fn from(hist: &SdkExponentialHistogram) -> Self { + TonicExponentialHistogram { + data_points: hist + .data_points() + .map(|dp| TonicExponentialHistogramDataPoint { + attributes: dp.attributes().map(Into::into).collect(), + start_time_unix_nano: to_nanos(hist.start_time()), + time_unix_nano: to_nanos(hist.time()), + count: dp.count() as u64, + sum: Some(dp.sum().into_f64()), + scale: dp.scale().into(), + zero_count: dp.zero_count(), + positive: Some(TonicBuckets { + offset: dp.positive_bucket().offset(), + bucket_counts: dp.positive_bucket().counts().collect(), + }), + negative: Some(TonicBuckets { + offset: dp.negative_bucket().offset(), + bucket_counts: dp.negative_bucket().counts().collect(), + }), + flags: TonicDataPointFlags::default() as u32, + exemplars: dp.exemplars().map(Into::into).collect(), + min: dp.min().map(Numeric::into_f64), + max: dp.max().map(Numeric::into_f64), + zero_threshold: dp.zero_threshold(), + }) + .collect(), + aggregation_temporality: TonicTemporality::from(hist.temporality()).into(), + } + } + } + + impl From<&SdkSum> for TonicSum + where + T: Debug + Into + Into + Copy, + { + fn from(sum: &SdkSum) -> Self { + TonicSum { + data_points: sum + .data_points() + .map(|dp| TonicNumberDataPoint { + attributes: dp.attributes().map(Into::into).collect(), + start_time_unix_nano: to_nanos(sum.start_time()), + time_unix_nano: to_nanos(sum.time()), + exemplars: dp.exemplars().map(Into::into).collect(), + flags: TonicDataPointFlags::default() as u32, + value: Some(dp.value().into()), + }) + .collect(), + aggregation_temporality: TonicTemporality::from(sum.temporality()).into(), + is_monotonic: sum.is_monotonic(), + } + } + } + + impl From<&SdkGauge> for TonicGauge + where + T: Debug + Into + Into + Copy, + { + fn from(gauge: &SdkGauge) -> Self { + TonicGauge { + data_points: gauge + .data_points() + .map(|dp| TonicNumberDataPoint { + attributes: dp.attributes().map(Into::into).collect(), + start_time_unix_nano: gauge.start_time().map(to_nanos).unwrap_or_default(), + time_unix_nano: to_nanos(gauge.time()), + exemplars: dp.exemplars().map(Into::into).collect(), + flags: TonicDataPointFlags::default() as u32, + value: Some(dp.value().into()), + }) + .collect(), + } + } + } + + impl From<&SdkExemplar> for TonicExemplar + where + T: Into + Copy, + { + fn from(ex: &SdkExemplar) -> Self { + TonicExemplar { + filtered_attributes: ex + .filtered_attributes() + .map(|kv| (&kv.key, &kv.value).into()) + .collect(), + time_unix_nano: to_nanos(ex.time()), + span_id: ex.span_id().into(), + trace_id: ex.trace_id().into(), + value: Some(ex.value.into()), + } + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/mod.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/mod.rs new file mode 100644 index 00000000..f0b7b86d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/mod.rs @@ -0,0 +1,16 @@ +pub mod common; + +#[cfg(feature = "metrics")] +pub mod metrics; + +#[cfg(feature = "trace")] +pub mod trace; + +#[cfg(feature = "logs")] +pub mod logs; + +#[cfg(feature = "zpages")] +pub mod tracez; + +#[cfg(feature = "profiles")] +pub mod profiles; diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/profiles.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/profiles.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/profiles.rs @@ -0,0 +1 @@ + diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/trace.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/trace.rs new file mode 100644 index 00000000..2fe69c68 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/trace.rs @@ -0,0 +1,524 @@ +#[cfg(feature = "gen-tonic-messages")] +/// Builds span flags based on the parent span's remote property. +/// This follows the OTLP specification for span flags. +pub(crate) fn build_span_flags(parent_span_is_remote: bool, base_flags: u32) -> u32 { + use crate::proto::tonic::trace::v1::SpanFlags; + let mut flags = base_flags; + flags |= SpanFlags::ContextHasIsRemoteMask as u32; + if parent_span_is_remote { + flags |= SpanFlags::ContextIsRemoteMask as u32; + } + flags +} + +#[cfg(feature = "gen-tonic-messages")] +pub mod tonic { + use crate::proto::tonic::resource::v1::Resource; + use crate::proto::tonic::trace::v1::{span, status, ResourceSpans, ScopeSpans, Span, Status}; + use crate::transform::common::{ + to_nanos, + tonic::{Attributes, ResourceAttributesWithSchema}, + }; + use opentelemetry::trace; + use opentelemetry::trace::{Link, SpanId, SpanKind}; + use opentelemetry_sdk::trace::SpanData; + use std::collections::HashMap; + + impl From for span::SpanKind { + fn from(span_kind: SpanKind) -> Self { + match span_kind { + SpanKind::Client => span::SpanKind::Client, + SpanKind::Consumer => span::SpanKind::Consumer, + SpanKind::Internal => span::SpanKind::Internal, + SpanKind::Producer => span::SpanKind::Producer, + SpanKind::Server => span::SpanKind::Server, + } + } + } + + impl From<&trace::Status> for status::StatusCode { + fn from(status: &trace::Status) -> Self { + match status { + trace::Status::Ok => status::StatusCode::Ok, + trace::Status::Unset => status::StatusCode::Unset, + trace::Status::Error { .. } => status::StatusCode::Error, + } + } + } + + impl From for span::Link { + fn from(link: Link) -> Self { + span::Link { + trace_id: link.span_context.trace_id().to_bytes().to_vec(), + span_id: link.span_context.span_id().to_bytes().to_vec(), + trace_state: link.span_context.trace_state().header(), + attributes: Attributes::from(link.attributes).0, + dropped_attributes_count: link.dropped_attributes_count, + flags: super::build_span_flags( + link.span_context.is_remote(), + link.span_context.trace_flags().to_u8() as u32, + ), + } + } + } + impl From for Span { + fn from(source_span: opentelemetry_sdk::trace::SpanData) -> Self { + let span_kind: span::SpanKind = source_span.span_kind.into(); + Span { + trace_id: source_span.span_context.trace_id().to_bytes().to_vec(), + span_id: source_span.span_context.span_id().to_bytes().to_vec(), + trace_state: source_span.span_context.trace_state().header(), + parent_span_id: { + if source_span.parent_span_id != SpanId::INVALID { + source_span.parent_span_id.to_bytes().to_vec() + } else { + vec![] + } + }, + flags: super::build_span_flags( + source_span.parent_span_is_remote, + source_span.span_context.trace_flags().to_u8() as u32, + ), + name: source_span.name.into_owned(), + kind: span_kind as i32, + start_time_unix_nano: to_nanos(source_span.start_time), + end_time_unix_nano: to_nanos(source_span.end_time), + dropped_attributes_count: source_span.dropped_attributes_count, + attributes: Attributes::from(source_span.attributes).0, + dropped_events_count: source_span.events.dropped_count, + events: source_span + .events + .into_iter() + .map(|event| span::Event { + time_unix_nano: to_nanos(event.timestamp), + name: event.name.into(), + attributes: Attributes::from(event.attributes).0, + dropped_attributes_count: event.dropped_attributes_count, + }) + .collect(), + dropped_links_count: source_span.links.dropped_count, + links: source_span.links.into_iter().map(Into::into).collect(), + status: Some(Status { + code: status::StatusCode::from(&source_span.status).into(), + message: match source_span.status { + trace::Status::Error { description } => description.to_string(), + _ => Default::default(), + }, + }), + } + } + } + + impl ResourceSpans { + pub fn new(source_span: SpanData, resource: &ResourceAttributesWithSchema) -> Self { + let span_kind: span::SpanKind = source_span.span_kind.into(); + ResourceSpans { + resource: Some(Resource { + attributes: resource.attributes.0.clone(), + dropped_attributes_count: 0, + entity_refs: vec![], + }), + schema_url: resource.schema_url.clone().unwrap_or_default(), + scope_spans: vec![ScopeSpans { + schema_url: source_span + .instrumentation_scope + .schema_url() + .map(ToOwned::to_owned) + .unwrap_or_default(), + scope: Some((source_span.instrumentation_scope, None).into()), + spans: vec![Span { + trace_id: source_span.span_context.trace_id().to_bytes().to_vec(), + span_id: source_span.span_context.span_id().to_bytes().to_vec(), + trace_state: source_span.span_context.trace_state().header(), + parent_span_id: { + if source_span.parent_span_id != SpanId::INVALID { + source_span.parent_span_id.to_bytes().to_vec() + } else { + vec![] + } + }, + flags: super::build_span_flags( + source_span.parent_span_is_remote, + source_span.span_context.trace_flags().to_u8() as u32, + ), + name: source_span.name.into_owned(), + kind: span_kind as i32, + start_time_unix_nano: to_nanos(source_span.start_time), + end_time_unix_nano: to_nanos(source_span.end_time), + dropped_attributes_count: source_span.dropped_attributes_count, + attributes: Attributes::from(source_span.attributes).0, + dropped_events_count: source_span.events.dropped_count, + events: source_span + .events + .into_iter() + .map(|event| span::Event { + time_unix_nano: to_nanos(event.timestamp), + name: event.name.into(), + attributes: Attributes::from(event.attributes).0, + dropped_attributes_count: event.dropped_attributes_count, + }) + .collect(), + dropped_links_count: source_span.links.dropped_count, + links: source_span.links.into_iter().map(Into::into).collect(), + status: Some(Status { + code: status::StatusCode::from(&source_span.status).into(), + message: match source_span.status { + trace::Status::Error { description } => description.to_string(), + _ => Default::default(), + }, + }), + }], + }], + } + } + } + + pub fn group_spans_by_resource_and_scope( + spans: Vec, + resource: &ResourceAttributesWithSchema, + ) -> Vec { + // Group spans by their instrumentation scope, converting each span as it + // is consumed. The batch is owned, so spans are moved rather than cloned. + let mut scope_indices: HashMap = HashMap::new(); + let mut scope_spans: Vec = Vec::new(); + + for span in spans { + let scope_index = match scope_indices.get(&span.instrumentation_scope) { + Some(&scope_index) => scope_index, + None => { + let instrumentation = span.instrumentation_scope.clone(); + let scope_index = scope_spans.len(); + scope_spans.push(ScopeSpans { + scope: Some((&instrumentation, None).into()), + schema_url: instrumentation + .schema_url() + .map(ToOwned::to_owned) + .unwrap_or_default(), + spans: Vec::new(), + }); + scope_indices.insert(instrumentation, scope_index); + scope_index + } + }; + + scope_spans[scope_index].spans.push(span.into()); + } + + // Wrap ScopeSpans into a single ResourceSpans + vec![ResourceSpans { + resource: Some(Resource { + attributes: resource.attributes.0.clone(), + dropped_attributes_count: 0, + entity_refs: vec![], + }), + scope_spans, + schema_url: resource.schema_url.clone().unwrap_or_default(), + }] + } +} + +#[cfg(all(test, feature = "gen-tonic-messages"))] +mod span_flags_tests { + use crate::proto::tonic::trace::v1::{Span, SpanFlags}; + use opentelemetry::trace::{SpanContext, SpanId, TraceFlags, TraceId, TraceState}; + use opentelemetry::InstrumentationScope; + use opentelemetry_sdk::trace::SpanData; + use std::borrow::Cow; + + #[test] + fn test_build_span_flags_local_parent() { + let flags = super::build_span_flags(false, 0); // is_remote = false + assert_eq!(flags, SpanFlags::ContextHasIsRemoteMask as u32); // 0x100 + } + + #[test] + fn test_build_span_flags_remote_parent() { + let flags = super::build_span_flags(true, 0); // is_remote = true + assert_eq!( + flags, + (SpanFlags::ContextHasIsRemoteMask as u32) | (SpanFlags::ContextIsRemoteMask as u32) + ); // 0x300 + } + + #[test] + fn test_build_span_flags_no_parent() { + let flags = super::build_span_flags(false, 0); // no parent = false + assert_eq!(flags, SpanFlags::ContextHasIsRemoteMask as u32); // 0x100 + } + + #[test] + fn test_build_span_flags_preserves_base_flags() { + let flags = super::build_span_flags(false, 0x01); // SAMPLED flag, local parent + assert_eq!(flags, 0x01 | (SpanFlags::ContextHasIsRemoteMask as u32)); // 0x101 + } + + #[test] + fn test_span_transformation_with_flags() { + let span_data = SpanData { + span_context: SpanContext::new( + TraceId::from(789), + SpanId::from(101112), + TraceFlags::default(), + false, + TraceState::default(), + ), + parent_span_id: SpanId::from(456), + parent_span_is_remote: false, + span_kind: opentelemetry::trace::SpanKind::Internal, + name: Cow::Borrowed("test_span"), + start_time: std::time::SystemTime::now(), + end_time: std::time::SystemTime::now(), + attributes: vec![], + dropped_attributes_count: 0, + events: opentelemetry_sdk::trace::SpanEvents::default(), + links: opentelemetry_sdk::trace::SpanLinks::default(), + status: opentelemetry::trace::Status::Unset, + instrumentation_scope: InstrumentationScope::builder("test").build(), + }; + + let otlp_span: Span = span_data.into(); + assert_eq!(otlp_span.flags, SpanFlags::ContextHasIsRemoteMask as u32); // 0x100 + } + + #[test] + fn test_span_transformation_with_remote_parent() { + let span_data = SpanData { + span_context: SpanContext::new( + TraceId::from(789), + SpanId::from(101112), + TraceFlags::default(), + false, + TraceState::default(), + ), + parent_span_id: SpanId::from(456), + parent_span_is_remote: true, + span_kind: opentelemetry::trace::SpanKind::Internal, + name: Cow::Borrowed("test_span"), + start_time: std::time::SystemTime::now(), + end_time: std::time::SystemTime::now(), + attributes: vec![], + dropped_attributes_count: 0, + events: opentelemetry_sdk::trace::SpanEvents::default(), + links: opentelemetry_sdk::trace::SpanLinks::default(), + status: opentelemetry::trace::Status::Unset, + instrumentation_scope: InstrumentationScope::builder("test").build(), + }; + + let otlp_span: Span = span_data.into(); + assert_eq!( + otlp_span.flags, + (SpanFlags::ContextHasIsRemoteMask as u32) | (SpanFlags::ContextIsRemoteMask as u32) + ); // 0x300 + } +} + +#[cfg(test)] +mod tests { + use crate::tonic::common::v1::any_value::Value; + use crate::transform::common::tonic::ResourceAttributesWithSchema; + use opentelemetry::time::now; + use opentelemetry::trace::{ + SpanContext, SpanId, SpanKind, Status, TraceFlags, TraceId, TraceState, + }; + use opentelemetry::InstrumentationScope; + use opentelemetry::KeyValue; + use opentelemetry_sdk::resource::Resource; + use opentelemetry_sdk::trace::SpanData; + use opentelemetry_sdk::trace::{SpanEvents, SpanLinks}; + use std::borrow::Cow; + use std::time::Duration; + + fn create_test_span_data(instrumentation_name: &'static str) -> SpanData { + let span_context = SpanContext::new( + TraceId::from(123), + SpanId::from(456), + TraceFlags::default(), + false, + TraceState::default(), + ); + + SpanData { + span_context, + parent_span_id: SpanId::from(0), + parent_span_is_remote: false, + span_kind: SpanKind::Internal, + name: Cow::Borrowed("test_span"), + start_time: now(), + end_time: now() + Duration::from_secs(1), + attributes: vec![KeyValue::new("key", "value")], + dropped_attributes_count: 0, + events: SpanEvents::default(), + links: SpanLinks::default(), + status: Status::Unset, + instrumentation_scope: InstrumentationScope::builder(instrumentation_name).build(), + } + } + + #[test] + fn test_group_spans_by_resource_and_scope_single_scope() { + let resource = Resource::builder_empty() + .with_attribute(KeyValue::new("resource_key", "resource_value")) + .build(); + let span_data = create_test_span_data("lib1"); + + let spans = vec![span_data.clone()]; + let resource: ResourceAttributesWithSchema = (&resource).into(); // Convert Resource to ResourceAttributesWithSchema + + let grouped_spans = + crate::transform::trace::tonic::group_spans_by_resource_and_scope(spans, &resource); + + assert_eq!(grouped_spans.len(), 1); + + let resource_spans = &grouped_spans[0]; + assert_eq!( + resource_spans.resource.as_ref().unwrap().attributes.len(), + 1 + ); + assert_eq!( + resource_spans.resource.as_ref().unwrap().attributes[0].key, + "resource_key" + ); + assert_eq!( + resource_spans.resource.as_ref().unwrap().attributes[0] + .value + .clone() + .unwrap() + .value + .unwrap(), + Value::StringValue("resource_value".to_string()) + ); + + let scope_spans = &resource_spans.scope_spans; + assert_eq!(scope_spans.len(), 1); + + let scope_span = &scope_spans[0]; + assert_eq!(scope_span.scope.as_ref().unwrap().name, "lib1"); + assert_eq!(scope_span.spans.len(), 1); + + assert_eq!( + scope_span.spans[0].trace_id, + span_data.span_context.trace_id().to_bytes().to_vec() + ); + } + + #[test] + fn test_group_spans_by_resource_and_scope_multiple_scopes() { + let resource = Resource::builder_empty() + .with_attribute(KeyValue::new("resource_key", "resource_value")) + .build(); + let span_data1 = create_test_span_data("lib1"); + let span_data2 = create_test_span_data("lib1"); + let span_data3 = create_test_span_data("lib2"); + + let spans = vec![span_data1.clone(), span_data2.clone(), span_data3.clone()]; + let resource: ResourceAttributesWithSchema = (&resource).into(); // Convert Resource to ResourceAttributesWithSchema + + let grouped_spans = + crate::transform::trace::tonic::group_spans_by_resource_and_scope(spans, &resource); + + assert_eq!(grouped_spans.len(), 1); + + let resource_spans = &grouped_spans[0]; + assert_eq!( + resource_spans.resource.as_ref().unwrap().attributes.len(), + 1 + ); + assert_eq!( + resource_spans.resource.as_ref().unwrap().attributes[0].key, + "resource_key" + ); + assert_eq!( + resource_spans.resource.as_ref().unwrap().attributes[0] + .value + .clone() + .unwrap() + .value + .unwrap(), + Value::StringValue("resource_value".to_string()) + ); + + let scope_spans = &resource_spans.scope_spans; + assert_eq!(scope_spans.len(), 2); + + // Check the scope spans for both lib1 and lib2 + let mut lib1_scope_span = None; + let mut lib2_scope_span = None; + + for scope_span in scope_spans { + match scope_span.scope.as_ref().unwrap().name.as_str() { + "lib1" => lib1_scope_span = Some(scope_span), + "lib2" => lib2_scope_span = Some(scope_span), + _ => {} + } + } + + let lib1_scope_span = lib1_scope_span.expect("lib1 scope span not found"); + let lib2_scope_span = lib2_scope_span.expect("lib2 scope span not found"); + + assert_eq!(lib1_scope_span.scope.as_ref().unwrap().name, "lib1"); + assert_eq!(lib2_scope_span.scope.as_ref().unwrap().name, "lib2"); + + assert_eq!(lib1_scope_span.spans.len(), 2); + assert_eq!(lib2_scope_span.spans.len(), 1); + + assert_eq!( + lib1_scope_span.spans[0].trace_id, + span_data1.span_context.trace_id().to_bytes().to_vec() + ); + assert_eq!( + lib1_scope_span.spans[1].trace_id, + span_data2.span_context.trace_id().to_bytes().to_vec() + ); + assert_eq!( + lib2_scope_span.spans[0].trace_id, + span_data3.span_context.trace_id().to_bytes().to_vec() + ); + } + + #[test] + fn test_scope_spans_uses_instrumentation_schema_url_not_resource() { + let resource = Resource::builder_empty() + .with_schema_url(vec![], "http://resource-schema") + .build(); + + let instrumentation_scope = InstrumentationScope::builder("test-lib") + .with_schema_url("http://instrumentation-schema") + .build(); + + let span_data = SpanData { + span_context: SpanContext::new( + TraceId::from(123), + SpanId::from(456), + TraceFlags::default(), + false, + TraceState::default(), + ), + parent_span_id: SpanId::from(0), + parent_span_is_remote: false, + span_kind: SpanKind::Internal, + name: Cow::Borrowed("test_span"), + start_time: now(), + end_time: now() + Duration::from_secs(1), + attributes: vec![], + dropped_attributes_count: 0, + events: SpanEvents::default(), + links: SpanLinks::default(), + status: Status::Unset, + instrumentation_scope, + }; + + let resource: ResourceAttributesWithSchema = (&resource).into(); + let grouped_spans = crate::transform::trace::tonic::group_spans_by_resource_and_scope( + vec![span_data], + &resource, + ); + + assert_eq!(grouped_spans.len(), 1); + let resource_spans = &grouped_spans[0]; + assert_eq!(resource_spans.schema_url, "http://resource-schema"); + + let scope_spans = &resource_spans.scope_spans; + assert_eq!(scope_spans.len(), 1); + assert_eq!(scope_spans[0].schema_url, "http://instrumentation-schema"); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/tracez.rs b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/tracez.rs new file mode 100644 index 00000000..3147a82c --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/src/transform/tracez.rs @@ -0,0 +1,72 @@ +#[cfg(all(feature = "gen-tonic-messages", feature = "zpages"))] +mod tonic { + use opentelemetry::trace::{Event, Status}; + use opentelemetry_sdk::trace::SpanData; + + use crate::proto::tonic::{ + trace::v1::{span::Event as SpanEvent, Status as SpanStatus}, + tracez::v1::{ErrorData, LatencyData, RunningData}, + }; + use crate::transform::common::{to_nanos, tonic::Attributes}; + + impl From for LatencyData { + fn from(span_data: SpanData) -> Self { + LatencyData { + traceid: span_data.span_context.trace_id().to_bytes().to_vec(), + spanid: span_data.span_context.span_id().to_bytes().to_vec(), + parentid: span_data.parent_span_id.to_bytes().to_vec(), + starttime: to_nanos(span_data.start_time), + endtime: to_nanos(span_data.end_time), + attributes: Attributes::from(span_data.attributes).0, + events: span_data.events.iter().cloned().map(Into::into).collect(), + links: span_data.links.iter().cloned().map(Into::into).collect(), + } + } + } + + impl From for ErrorData { + fn from(span_data: SpanData) -> Self { + ErrorData { + traceid: span_data.span_context.trace_id().to_bytes().to_vec(), + spanid: span_data.span_context.span_id().to_bytes().to_vec(), + parentid: span_data.parent_span_id.to_bytes().to_vec(), + starttime: to_nanos(span_data.start_time), + attributes: Attributes::from(span_data.attributes).0, + events: span_data.events.iter().cloned().map(Into::into).collect(), + links: span_data.links.iter().cloned().map(Into::into).collect(), + status: match span_data.status { + Status::Error { description } => Some(SpanStatus { + message: description.to_string(), + code: 2, + }), + _ => None, + }, + } + } + } + + impl From for RunningData { + fn from(span_data: SpanData) -> Self { + RunningData { + traceid: span_data.span_context.trace_id().to_bytes().to_vec(), + spanid: span_data.span_context.span_id().to_bytes().to_vec(), + parentid: span_data.parent_span_id.to_bytes().to_vec(), + starttime: to_nanos(span_data.start_time), + attributes: Attributes::from(span_data.attributes).0, + events: span_data.events.iter().cloned().map(Into::into).collect(), + links: span_data.links.iter().cloned().map(Into::into).collect(), + } + } + } + + impl From for SpanEvent { + fn from(event: Event) -> Self { + SpanEvent { + time_unix_nano: to_nanos(event.timestamp), + name: event.name.to_string(), + attributes: Attributes::from(event.attributes).0, + dropped_attributes_count: event.dropped_attributes_count, + } + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/tests/grpc_build.rs b/vendor/opentelemetry-rust/opentelemetry-proto/tests/grpc_build.rs new file mode 100644 index 00000000..4b7da4cc --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/tests/grpc_build.rs @@ -0,0 +1,265 @@ +use std::collections::HashMap; +use std::path::Path; +use tempfile::TempDir; + +const TONIC_OUT_DIR: &str = "src/proto/tonic"; +const TONIC_PROTO_FILES: &[&str] = &[ + "src/proto/opentelemetry-proto/opentelemetry/proto/common/v1/common.proto", + "src/proto/opentelemetry-proto/opentelemetry/proto/resource/v1/resource.proto", + "src/proto/opentelemetry-proto/opentelemetry/proto/trace/v1/trace.proto", + "src/proto/opentelemetry-proto/opentelemetry/proto/collector/trace/v1/trace_service.proto", + "src/proto/opentelemetry-proto/opentelemetry/proto/metrics/v1/metrics.proto", + "src/proto/opentelemetry-proto/opentelemetry/proto/collector/metrics/v1/metrics_service.proto", + "src/proto/opentelemetry-proto/opentelemetry/proto/logs/v1/logs.proto", + "src/proto/opentelemetry-proto/opentelemetry/proto/collector/logs/v1/logs_service.proto", + "src/proto/opentelemetry-proto/opentelemetry/proto/profiles/v1development/profiles.proto", + "src/proto/opentelemetry-proto/opentelemetry/proto/collector/profiles/v1development/profiles_service.proto", + "src/proto/tracez.proto", +]; +const TONIC_INCLUDES: &[&str] = &["src/proto/opentelemetry-proto", "src/proto"]; + +#[test] +fn build_tonic() { + let before_build = build_content_map(TONIC_OUT_DIR, true); + + let out_dir = TempDir::new().expect("failed to create temp dir to store the generated files"); + + // build the generated files into OUT_DIR for now so we don't have to touch the src unless we have to + let mut builder = tonic_prost_build::configure() + .build_server(true) + .build_client(true) + .server_mod_attribute(".", "#[cfg(feature = \"gen-tonic\")]") + .client_mod_attribute(".", "#[cfg(feature = \"gen-tonic\")]") + .type_attribute( + ".", + "#[cfg_attr(feature = \"with-schemars\", derive(schemars::JsonSchema))]", + ) + .type_attribute( + ".", + "#[cfg_attr(feature = \"with-serde\", derive(serde::Serialize, serde::Deserialize))]", + ) + .type_attribute( + ".", + "#[cfg_attr(feature = \"with-serde\", serde(rename_all = \"camelCase\"))]", + ); + + // Optional numeric, string and array fields need to default to their default value otherwise + // JSON files without those field cannot deserialize + // we cannot add serde(default) to all generated types because enums cannot be annotated with serde(default) + for path in [ + "collector.trace.v1.ExportTraceServiceRequest", + "collector.logs.v1.ExportLogsServiceRequest", + "collector.metrics.v1.ExportMetricsServiceRequest", + "collector.profiles.v1development.ExportProfilesServiceRequest", + "collector.trace.v1.ExportTracePartialSuccess", + "collector.logs.v1.ExportLogsPartialSuccess", + "collector.metrics.v1.ExportMetricsPartialSuccess", + "collector.profiles.v1development.ExportProfilesPartialSuccess", + "trace.v1.Span", + "trace.v1.Span.Link", + "trace.v1.ScopeSpans", + "trace.v1.ResourceSpans", + "common.v1.InstrumentationScope", + "resource.v1.Resource", + "trace.v1.Span.Event", + "trace.v1.Status", + "logs.v1.LogRecord", + "logs.v1.ScopeLogs", + "logs.v1.ResourceLogs", + "metrics.v1.Metric", + "metrics.v1.ResourceMetrics", + "metrics.v1.ScopeMetrics", + "metrics.v1.Gauge", + "metrics.v1.Sum", + "metrics.v1.Histogram", + "metrics.v1.ExponentialHistogram", + "metrics.v1.Summary", + "metrics.v1.NumberDataPoint", + "metrics.v1.HistogramDataPoint", + "metrics.v1.SummaryDataPoint", + "profiles.v1development.Function", + ] { + builder = builder.type_attribute( + path, + "#[cfg_attr(feature = \"with-serde\", serde(default))]", + ) + } + + // special serializer and deserializer for traceId and spanId + // OTLP/JSON format uses hex string for traceId and spanId + // the proto file uses bytes for traceId and spanId + // Thus, special serializer and deserializer are needed + for path in [ + "trace.v1.Span.trace_id", + "trace.v1.Span.span_id", + "trace.v1.Span.parent_span_id", + "trace.v1.Span.Link.trace_id", + "trace.v1.Span.Link.span_id", + "logs.v1.LogRecord.span_id", + "logs.v1.LogRecord.trace_id", + "metrics.v1.Exemplar.span_id", + "metrics.v1.Exemplar.trace_id", + "profiles.v1development.Profile.profile_id", + ] { + builder = builder + .field_attribute(path, "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_to_hex_string\", deserialize_with = \"crate::proto::serializers::deserialize_from_hex_string\"))]") + } + + // key_strindex is a profiling-only reference field added to common.v1.KeyValue. + // For non-profiles signals we keep the existing JSON shape by defaulting it on input + // and omitting it from output when it is zero. + builder = builder.field_attribute( + "common.v1.KeyValue.key_strindex", + "#[cfg_attr(feature = \"with-serde\", serde(default, skip_serializing_if = \"crate::proto::serializers::is_default\"))]", + ); + + // special serializer and deserializer for timestamp + // OTLP/JSON format may use string for timestamp + // the proto file uses u64 for timestamp + // Thus, special serializer and deserializer are needed + for path in [ + //trace + "trace.v1.Span.start_time_unix_nano", + "trace.v1.Span.end_time_unix_nano", + "trace.v1.Span.Event.time_unix_nano", + //logs + "logs.v1.LogRecord.time_unix_nano", + "logs.v1.LogRecord.observed_time_unix_nano", + //metrics + "metrics.v1.HistogramDataPoint.start_time_unix_nano", + "metrics.v1.HistogramDataPoint.time_unix_nano", + "metrics.v1.NumberDataPoint.start_time_unix_nano", + "metrics.v1.NumberDataPoint.time_unix_nano", + "metrics.v1.ExponentialHistogramDataPoint.start_time_unix_nano", + "metrics.v1.ExponentialHistogramDataPoint.time_unix_nano", + "metrics.v1.SummaryDataPoint.start_time_unix_nano", + "metrics.v1.SummaryDataPoint.time_unix_nano", + "metrics.v1.Exemplar.time_unix_nano", + ] { + builder = builder + .field_attribute(path, "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_string_to_u64\"))]") + } + for path in ["profiles.v1development.Profile.time_nanos"] { + builder = builder + .field_attribute(path, "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_i64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_string_to_i64\"))]") + } + for path in ["profiles.v1development.Sample.timestamps_unix_nano"] { + builder = builder + .field_attribute(path, "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_vec_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_vec_string_to_vec_u64\"))]") + } + + // special serializer and deserializer for metrics count + // OTLP/JSON format may use string for count + // the proto file uses u64 for count + // Thus, special serializer and deserializer are needed + for path in [ + // metrics count and bucket fields + "metrics.v1.HistogramDataPoint.count", + "metrics.v1.ExponentialHistogramDataPoint.count", + "metrics.v1.ExponentialHistogramDataPoint.zero_count", + "metrics.v1.SummaryDataPoint.count", + ] { + builder = builder.field_attribute( + path, + "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_string_to_u64\"))]", + ); + } + + // special serializer and deserializer for metrics bucket counts + // OTLP/JSON format may use string for bucket counts + // the proto file uses u64 for bucket counts + // Thus, special serializer and deserializer are needed + for path in [ + "metrics.v1.HistogramDataPoint.bucket_counts", + "metrics.v1.ExponentialHistogramDataPoint.Buckets.bucket_counts", + ] { + builder = builder.field_attribute( + path, + "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_vec_u64_to_string\", deserialize_with = \"crate::proto::serializers::deserialize_vec_string_to_vec_u64\"))]", + ); + } + + // Special handling for floating-point fields that might contain NaN, Infinity, or -Infinity + // TODO: More needs to be added here as we find more fields that need this special handling + for path in [ + // metrics + "metrics.v1.SummaryDataPoint.ValueAtQuantile.value", + "metrics.v1.SummaryDataPoint.ValueAtQuantile.quantile", + ] { + builder = builder.field_attribute( + path, + "#[cfg_attr(feature = \"with-serde\", serde(serialize_with = \"crate::proto::serializers::serialize_f64_special\", deserialize_with = \"crate::proto::serializers::deserialize_f64_special\"))]", + ); + } + + // special serializer and deserializer for value + // The Value::value field must be hidden + builder = builder + .field_attribute("common.v1.AnyValue.value", "#[cfg_attr(feature =\"with-serde\", serde(flatten, serialize_with = \"crate::proto::serializers::serialize_to_value\", deserialize_with = \"crate::proto::serializers::deserialize_from_value\"))]"); + + // flatten + for path in ["metrics.v1.Metric.data", "metrics.v1.NumberDataPoint.value"] { + builder = + builder.field_attribute(path, "#[cfg_attr(feature =\"with-serde\", serde(flatten))]"); + } + + builder + .out_dir(out_dir.path()) + .compile_protos(TONIC_PROTO_FILES, TONIC_INCLUDES) + .expect("cannot compile protobuf using tonic"); + + let after_build = build_content_map(out_dir.path(), true); + ensure_files_are_same(before_build, after_build, TONIC_OUT_DIR); +} + +fn build_content_map(path: impl AsRef, normalize_line_feed: bool) -> HashMap { + std::fs::read_dir(path) + .expect("cannot open dictionary of generated files") + .flatten() + .map(|entry| { + let path = entry.path(); + let file_name = path + .file_name() + .expect("file name should always exist for generated files"); + + let mut file_contents = std::fs::read_to_string(path.clone()) + .expect("cannot read from existing generated file"); + + if normalize_line_feed { + file_contents = get_platform_specific_string(file_contents); + } + + (file_name.to_string_lossy().to_string(), file_contents) + }) + .collect() +} + +/// Returns a String which uses the platform specific new line feed character. +fn get_platform_specific_string(input: String) -> String { + if cfg!(windows) && !input.ends_with("\r\n") && input.ends_with('\n') { + return input.replace('\n', "\r\n"); + } + input +} + +fn ensure_files_are_same( + before_build: HashMap, + after_build: HashMap, + target_dir: &'static str, +) { + if after_build == before_build { + return; + } + + if std::env::var("CI").is_ok() { + panic!("generated file has changed but it's a CI environment, please rerun this test locally and commit the changes"); + } + + // if there is at least one changes we will just copy the whole directory over + for (file_name, content) in after_build { + std::fs::write(Path::new(target_dir).join(file_name), content) + .expect("cannot write to the proto generate file. If it's happening in CI env, please return the test locally and commit the change"); + } + + panic!("generated file has changed, please commit the change file and rerun the test"); +} diff --git a/vendor/opentelemetry-rust/opentelemetry-proto/tests/json_serde.rs b/vendor/opentelemetry-rust/opentelemetry-proto/tests/json_serde.rs new file mode 100644 index 00000000..5a8837b6 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-proto/tests/json_serde.rs @@ -0,0 +1,2243 @@ +#[cfg(all(feature = "with-serde", feature = "gen-tonic-messages"))] +mod json_serde { + #[cfg(feature = "logs")] + use opentelemetry_proto::tonic::collector::logs::v1::{ + ExportLogsServiceRequest, ExportLogsServiceResponse, + }; + #[cfg(feature = "metrics")] + use opentelemetry_proto::tonic::collector::metrics::v1::{ + ExportMetricsServiceRequest, ExportMetricsServiceResponse, + }; + #[cfg(feature = "profiles")] + use opentelemetry_proto::tonic::collector::profiles::v1development::{ + ExportProfilesServiceRequest, ExportProfilesServiceResponse, + }; + #[cfg(feature = "trace")] + use opentelemetry_proto::tonic::collector::trace::v1::{ + ExportTraceServiceRequest, ExportTraceServiceResponse, + }; + use opentelemetry_proto::tonic::common::v1::any_value::Value; + use opentelemetry_proto::tonic::common::v1::{ + AnyValue, ArrayValue, InstrumentationScope, KeyValue, KeyValueList, + }; + #[cfg(feature = "logs")] + use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs}; + #[cfg(feature = "metrics")] + use opentelemetry_proto::tonic::metrics::v1::{ + metric::Data, number_data_point::Value as MetricValue, Gauge, Histogram, + HistogramDataPoint, Metric, NumberDataPoint, ResourceMetrics, ScopeMetrics, Sum, + }; + use opentelemetry_proto::tonic::resource::v1::Resource; + #[cfg(feature = "trace")] + use opentelemetry_proto::tonic::trace::v1::{ + span::{Event, Link}, + ResourceSpans, ScopeSpans, Span, Status, + }; + + #[cfg(feature = "profiles")] + mod export_profiles_service_request { + use super::*; + + #[test] + fn deserialize_omitted_resource_profiles() { + let empty: ExportProfilesServiceRequest = serde_json::from_str("{}").unwrap(); + assert!(empty.resource_profiles.is_empty()); + } + } + + #[cfg(feature = "trace")] + mod export_trace_service_request { + use super::*; + + #[test] + fn deserialize_omitted_resource_spans() { + let empty: ExportTraceServiceRequest = serde_json::from_str("{}").unwrap(); + assert!(empty.resource_spans.is_empty()); + } + + // `ExportTraceServiceRequest` from the OpenTelemetry proto examples + // see + mod example { + use super::*; + + fn value() -> ExportTraceServiceRequest { + ExportTraceServiceRequest { + resource_spans: vec![ResourceSpans { + resource: Some(Resource { + attributes: vec![KeyValue { + key: String::from("service.name"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from("my.service"))), + }), + key_strindex: 0, + }], + dropped_attributes_count: 0, + entity_refs: vec![], + }), + scope_spans: vec![ScopeSpans { + scope: Some(InstrumentationScope { + name: String::from("my.library"), + version: String::from("1.0.0"), + attributes: vec![KeyValue { + key: String::from("my.scope.attribute"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some scope attribute", + ))), + }), + key_strindex: 0, + }], + dropped_attributes_count: 0, + }), + spans: vec![Span { + trace_id: const_hex::decode("5b8efff798038103d269b633813fc60c") + .unwrap(), + span_id: const_hex::decode("eee19b7ec3c1b174").unwrap(), + trace_state: String::new(), + parent_span_id: const_hex::decode("eee19b7ec3c1b173").unwrap(), + flags: 0, + name: String::from("I'm a server span"), + kind: 2, + start_time_unix_nano: 1544712660000000000, + end_time_unix_nano: 1544712661000000000, + attributes: vec![KeyValue { + key: String::from("my.span.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from("some value"))), + }), + key_strindex: 0, + }], + dropped_attributes_count: 0, + events: vec![], + dropped_events_count: 0, + links: vec![], + dropped_links_count: 0, + status: None, + }], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + } + } + + // language=json + const CANONICAL: &str = r#"{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "my.service" + } + } + ], + "droppedAttributesCount": 0, + "entityRefs": [] + }, + "scopeSpans": [ + { + "scope": { + "name": "my.library", + "version": "1.0.0", + "attributes": [ + { + "key": "my.scope.attribute", + "value": { + "stringValue": "some scope attribute" + } + } + ], + "droppedAttributesCount": 0 + }, + "spans": [ + { + "traceId": "5b8efff798038103d269b633813fc60c", + "spanId": "eee19b7ec3c1b174", + "traceState": "", + "parentSpanId": "eee19b7ec3c1b173", + "flags": 0, + "name": "I'm a server span", + "kind": 2, + "startTimeUnixNano": "1544712660000000000", + "endTimeUnixNano": "1544712661000000000", + "attributes": [ + { + "key": "my.span.attr", + "value": { + "stringValue": "some value" + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "links": [], + "droppedLinksCount": 0, + "status": null + } + ], + "schemaUrl": "" + } + ], + "schemaUrl": "" + } + ] +}"#; + + // copied from the example json file + // language=json + const ALTERNATIVE: &str = r#"{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "my.service" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "my.library", + "version": "1.0.0", + "attributes": [ + { + "key": "my.scope.attribute", + "value": { + "stringValue": "some scope attribute" + } + } + ] + }, + "spans": [ + { + "traceId": "5B8EFFF798038103D269B633813FC60C", + "spanId": "EEE19B7EC3C1B174", + "parentSpanId": "EEE19B7EC3C1B173", + "name": "I'm a server span", + "startTimeUnixNano": "1544712660000000000", + "endTimeUnixNano": "1544712661000000000", + "kind": 2, + "attributes": [ + { + "key": "my.span.attr", + "value": { + "stringValue": "some value" + } + } + ] + } + ] + } + ] + } + ] +} +"#; + + #[test] + fn serialize() { + let input: ExportTraceServiceRequest = value(); + let actual = + serde_json::to_string_pretty(&input).expect("serialization must succeed"); + assert_eq!(actual, CANONICAL); + } + + #[test] + fn deserialize_canonical() { + let actual: ExportTraceServiceRequest = + serde_json::from_str(CANONICAL).expect("deserialization must succeed"); + let expected: ExportTraceServiceRequest = value(); + assert_eq!(actual, expected); + } + + #[test] + fn deserialize_alternative() { + let actual: ExportTraceServiceRequest = + serde_json::from_str(ALTERNATIVE).expect("deserialization must succeed"); + let expected: ExportTraceServiceRequest = value(); + assert_eq!(actual, expected); + } + } + + // request where all fields are set + mod complete { + use super::*; + + fn value() -> ExportTraceServiceRequest { + ExportTraceServiceRequest { + resource_spans: vec![ResourceSpans { + resource: Some(Resource { + attributes: vec![KeyValue { + key: String::from("service.name"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from("my.service"))), + }), + key_strindex: 0, + }], + dropped_attributes_count: 1, + entity_refs: vec![], + }), + scope_spans: vec![ScopeSpans { + scope: Some(InstrumentationScope { + name: String::from("my.library"), + version: String::from("1.0.0"), + attributes: vec![KeyValue { + key: String::from("my.scope.attribute"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some scope attribute", + ))), + }), + key_strindex: 0, + }], + dropped_attributes_count: 1, + }), + spans: vec![Span { + trace_id: const_hex::decode("5b8efff798038103d269b633813fc60c") + .unwrap(), + span_id: const_hex::decode("eee19b7ec3c1b174").unwrap(), + trace_state: String::from("browser=firefox,os=linux"), + parent_span_id: const_hex::decode("eee19b7ec3c1b173").unwrap(), + flags: 1, + name: String::from("I'm a server span"), + kind: 2, + start_time_unix_nano: 1544712660000000000, + end_time_unix_nano: 1544712661000000000, + attributes: vec![ + KeyValue { + key: String::from("my.span.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }, + KeyValue { + key: String::from("my.span.bytes.attr"), + value: Some(AnyValue { + value: Some(Value::BytesValue(vec![0x80, 0x80, 0x80])), + }), + key_strindex: 0, + }, + ], + dropped_attributes_count: 1, + events: vec![Event { + time_unix_nano: 1544712660500000000, + name: String::from("user.created"), + attributes: vec![KeyValue { + key: String::from("my.event.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "snowman", + ))), + }), + key_strindex: 0, + }], + dropped_attributes_count: 1, + }], + dropped_events_count: 1, + links: vec![Link { + trace_id: const_hex::decode("5b8efff798038103d269b633813fc60b") + .unwrap(), + span_id: const_hex::decode("eee19b7ec3c1b172").unwrap(), + trace_state: String::from("food=pizza,color=red"), + attributes: vec![KeyValue { + key: String::from("my.link.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from("rust"))), + }), + key_strindex: 0, + }], + dropped_attributes_count: 1, + flags: 0x0200, + }], + dropped_links_count: 1, + status: Some(Status { + message: String::from("service temporarily unavailable"), + code: 2, + }), + }], + schema_url: String::from("https://opentelemetry.io/schemas/1.24.0"), + }], + schema_url: String::from("https://opentelemetry.io/schemas/1.24.0"), + }], + } + } + + // language=json + const CANONICAL: &str = r#"{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "my.service" + } + } + ], + "droppedAttributesCount": 1, + "entityRefs": [] + }, + "scopeSpans": [ + { + "scope": { + "name": "my.library", + "version": "1.0.0", + "attributes": [ + { + "key": "my.scope.attribute", + "value": { + "stringValue": "some scope attribute" + } + } + ], + "droppedAttributesCount": 1 + }, + "spans": [ + { + "traceId": "5b8efff798038103d269b633813fc60c", + "spanId": "eee19b7ec3c1b174", + "traceState": "browser=firefox,os=linux", + "parentSpanId": "eee19b7ec3c1b173", + "flags": 1, + "name": "I'm a server span", + "kind": 2, + "startTimeUnixNano": "1544712660000000000", + "endTimeUnixNano": "1544712661000000000", + "attributes": [ + { + "key": "my.span.attr", + "value": { + "stringValue": "some value" + } + }, + { + "key": "my.span.bytes.attr", + "value": { + "bytesValue": "gICA" + } + } + ], + "droppedAttributesCount": 1, + "events": [ + { + "timeUnixNano": "1544712660500000000", + "name": "user.created", + "attributes": [ + { + "key": "my.event.attr", + "value": { + "stringValue": "snowman" + } + } + ], + "droppedAttributesCount": 1 + } + ], + "droppedEventsCount": 1, + "links": [ + { + "traceId": "5b8efff798038103d269b633813fc60b", + "spanId": "eee19b7ec3c1b172", + "traceState": "food=pizza,color=red", + "attributes": [ + { + "key": "my.link.attr", + "value": { + "stringValue": "rust" + } + } + ], + "droppedAttributesCount": 1, + "flags": 512 + } + ], + "droppedLinksCount": 1, + "status": { + "message": "service temporarily unavailable", + "code": 2 + } + } + ], + "schemaUrl": "https://opentelemetry.io/schemas/1.24.0" + } + ], + "schemaUrl": "https://opentelemetry.io/schemas/1.24.0" + } + ] +}"#; + + #[test] + fn serialize() { + let input: ExportTraceServiceRequest = value(); + let actual = + serde_json::to_string_pretty(&input).expect("serialization must succeed"); + assert_eq!(actual, CANONICAL); + } + + #[test] + fn deserialize_canonical() { + let actual: ExportTraceServiceRequest = + serde_json::from_str(CANONICAL).expect("deserialization must succeed"); + let expected: ExportTraceServiceRequest = value(); + assert_eq!(actual, expected); + } + } + } + + mod value { + use super::*; + + #[test] + fn string() { + let value = Value::StringValue(String::from("my.service")); + // language=json + let json = r#"{"stringValue":"my.service"}"#; + assert_eq!( + serde_json::to_string(&value).expect("serialization succeeds"), + json + ); + assert_eq!( + serde_json::from_str::(json).expect("deserialization succeeds"), + value + ); + } + + #[test] + fn bool() { + let value = Value::BoolValue(true); + // language=json + let json = r#"{"boolValue":true}"#; + assert_eq!( + serde_json::to_string(&value).expect("serialization succeeds"), + json + ); + assert_eq!( + serde_json::from_str::(json).expect("deserialization succeeds"), + value + ); + } + + #[test] + fn int() { + let value = Value::IntValue(123); + // language=json + let json = r#"{"intValue":123}"#; + assert_eq!( + serde_json::to_string(&value).expect("serialization succeeds"), + json + ); + assert_eq!( + serde_json::from_str::(json).expect("deserialization succeeds"), + value + ); + } + + #[test] + fn array_empty() { + let value = Value::ArrayValue(ArrayValue { values: vec![] }); + // language=json + let json = r#"{"arrayValue":{"values":[]}}"#; + assert_eq!( + serde_json::to_string(&value).expect("serialization succeeds"), + json + ); + assert_eq!( + serde_json::from_str::(json).expect("deserialization succeeds"), + value + ); + } + + #[test] + fn array_strings() { + let value = Value::ArrayValue(ArrayValue { + values: vec![ + AnyValue { + value: Some(Value::StringValue(String::from("foo"))), + }, + AnyValue { + value: Some(Value::StringValue(String::from("bar"))), + }, + ], + }); + // language=json + let json = r#"{ + "arrayValue": { + "values": [ + { + "stringValue": "foo" + }, + { + "stringValue": "bar" + } + ] + } +}"#; + assert_eq!( + serde_json::to_string_pretty(&value).expect("serialization succeeds"), + json + ); + assert_eq!( + serde_json::from_str::(json).expect("deserialization succeeds"), + value + ); + } + + #[test] + fn array_mixed() { + let value = Value::ArrayValue(ArrayValue { + values: vec![ + AnyValue { + value: Some(Value::StringValue(String::from("foo"))), + }, + AnyValue { + value: Some(Value::IntValue(1337)), + }, + ], + }); + // language=json + let json = r#"{ + "arrayValue": { + "values": [ + { + "stringValue": "foo" + }, + { + "intValue": "1337" + } + ] + } +}"#; + assert_eq!( + serde_json::to_string_pretty(&value).expect("serialization succeeds"), + json + ); + assert_eq!( + serde_json::from_str::(json).expect("deserialization succeeds"), + value + ); + } + + #[test] + fn map_single_string() { + let value = Value::KvlistValue(KeyValueList { + values: vec![KeyValue { + key: String::from("some.map.key"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from("some value"))), + }), + key_strindex: 0, + }], + }); + // language=json + let json = r#"{ + "kvlistValue": { + "values": [ + { + "key": "some.map.key", + "value": { + "stringValue": "some value" + } + } + ] + } +}"#; + assert_eq!( + serde_json::to_string_pretty(&value).expect("serialization succeeds"), + json + ); + assert_eq!( + serde_json::from_str::(json).expect("deserialization succeeds"), + value + ); + } + } + + mod empty_any_value { + use super::*; + + #[test] + fn deserialize_empty_and_unknown_only_values() { + for json in [r#"{}"#, r#"{"futureValue":"ignored"}"#] { + let actual: AnyValue = + serde_json::from_str(json).expect("empty AnyValue must deserialize"); + assert_eq!(actual, AnyValue { value: None }); + assert_eq!( + serde_json::to_string(&actual).expect("empty AnyValue must serialize"), + "{}" + ); + } + } + + #[test] + fn deserialize_null_values_as_unset() { + for field in [ + "stringValue", + "boolValue", + "intValue", + "doubleValue", + "arrayValue", + "kvlistValue", + "bytesValue", + ] { + let json = format!(r#"{{"{field}":null}}"#); + let actual: AnyValue = + serde_json::from_str(&json).expect("null AnyValue field must deserialize"); + assert_eq!(actual.value, None, "field: {field}"); + } + } + + #[test] + fn deserialize_null_field_preserves_non_null_value() { + for field in [ + "stringValue", + "boolValue", + "intValue", + "doubleValue", + "arrayValue", + "kvlistValue", + "bytesValue", + ] { + let (non_null_field, expected) = if field == "stringValue" { + (r#""intValue":"42""#, Value::IntValue(42)) + } else { + (r#""stringValue":"kept""#, Value::StringValue("kept".into())) + }; + for json in [ + format!(r#"{{"{field}":null,{non_null_field}}}"#), + format!(r#"{{{non_null_field},"{field}":null}}"#), + ] { + let actual: AnyValue = serde_json::from_str(&json).unwrap(); + assert_eq!(actual.value.as_ref(), Some(&expected), "input: {json}"); + } + } + } + + #[test] + fn deserialize_malformed_non_null_values_fails() { + for json in [ + r#"{"stringValue":42}"#, + r#"{"boolValue":"true"}"#, + r#"{"intValue":"not-an-integer"}"#, + r#"{"intValue":"9223372036854775808"}"#, + r#"{"doubleValue":true}"#, + r#"{"arrayValue":42}"#, + r#"{"kvlistValue":42}"#, + r#"{"bytesValue":"!"}"#, + ] { + assert!( + serde_json::from_str::(json).is_err(), + "input: {json}" + ); + } + } + + #[cfg(feature = "trace")] + #[test] + fn deserialize_empty_span_attribute() { + let json = r#"{ + "resourceSpans": [{ + "scopeSpans": [{ + "spans": [{ + "traceId": "00000000000000000000000000000001", + "spanId": "0000000000000001", + "name": "cloudflare-span", + "attributes": [{"key": "empty", "value": {}}] + }] + }] + }] + }"#; + + let request: ExportTraceServiceRequest = + serde_json::from_str(json).expect("trace request must deserialize"); + let value = &request.resource_spans[0].scope_spans[0].spans[0].attributes[0] + .value + .as_ref() + .expect("attribute must remain present") + .value; + assert_eq!(value, &None); + } + + #[cfg(feature = "logs")] + #[test] + fn deserialize_empty_log_field() { + let json = r#"{ + "resourceLogs": [{ + "scopeLogs": [{ + "logRecords": [{ + "body": { + "kvlistValue": { + "values": [{"key": "value", "value": {}}] + } + } + }] + }] + }] + }"#; + + let request: ExportLogsServiceRequest = + serde_json::from_str(json).expect("logs request must deserialize"); + let Some(Value::KvlistValue(body)) = request.resource_logs[0].scope_logs[0].log_records + [0] + .body + .as_ref() + .and_then(|body| body.value.as_ref()) else { + panic!("log body must remain a key-value list"); + }; + assert_eq!(body.values[0].value, Some(AnyValue { value: None })); + } + } + + mod key_value { + use super::*; + + mod string { + use super::*; + + fn value() -> KeyValue { + KeyValue { + key: String::from("service.name"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from("my.service"))), + }), + key_strindex: 0, + } + } + + // language=json + const CANONICAL: &str = r#"{ + "key": "service.name", + "value": { + "stringValue": "my.service" + } +}"#; + + #[test] + fn serialize() { + let input: KeyValue = value(); + let actual = + serde_json::to_string_pretty(&input).expect("serialization must succeed"); + assert_eq!(actual, CANONICAL); + } + + #[test] + fn deserialize_canonical() { + let actual: KeyValue = + serde_json::from_str(CANONICAL).expect("deserialization must succeed"); + let expected: KeyValue = value(); + assert_eq!(actual, expected); + } + } + + mod int { + use super::*; + + fn value() -> KeyValue { + KeyValue { + key: String::from("service.id"), + value: Some(AnyValue { + value: Some(Value::IntValue(33)), + }), + key_strindex: 0, + } + } + + // language=json + const CANONICAL: &str = r#"{ + "key": "service.id", + "value": { + "intValue": "33" + } +}"#; + + // unquoted int value + // language=json + const ALTERNATIVE: &str = r#"{ + "key": "service.id", + "value": { + "intValue": 33 + } +}"#; + + #[test] + fn serialize() { + let input: KeyValue = value(); + let actual = + serde_json::to_string_pretty(&input).expect("serialization must succeed"); + assert_eq!(actual, CANONICAL); + } + + #[test] + fn deserialize_canonical() { + let actual: KeyValue = + serde_json::from_str(CANONICAL).expect("deserialization must succeed"); + let expected: KeyValue = value(); + assert_eq!(actual, expected); + } + + #[test] + fn deserialize_alternative() { + let actual: KeyValue = + serde_json::from_str(ALTERNATIVE).expect("deserialization must succeed"); + let expected: KeyValue = value(); + assert_eq!(actual, expected); + } + } + } + + #[cfg(feature = "trace")] + mod event { + use super::*; + + mod simple { + use super::*; + + fn value() -> Event { + Event { + time_unix_nano: 1234567890, + name: String::from("my_event"), + attributes: vec![], + dropped_attributes_count: 0, + } + } + + // language=json + const CANONICAL: &str = r#"{ + "timeUnixNano": "1234567890", + "name": "my_event", + "attributes": [], + "droppedAttributesCount": 0 +}"#; + + // language=json + const ALTERNATIVE: &str = r#"{ + "name": "my_event", + "timeUnixNano": "1234567890" +}"#; + + #[test] + fn serialize() { + let input: Event = value(); + let actual = + serde_json::to_string_pretty(&input).expect("serialization must succeed"); + assert_eq!(actual, CANONICAL); + } + + #[test] + fn deserialize_canonical() { + let actual: Event = + serde_json::from_str(CANONICAL).expect("deserialization must succeed"); + let expected: Event = value(); + assert_eq!(actual, expected); + } + + #[test] + fn deserialize_alternative() { + let actual: Event = + serde_json::from_str(ALTERNATIVE).expect("deserialization must succeed"); + let expected: Event = value(); + assert_eq!(actual, expected); + } + } + } + + #[cfg(feature = "metrics")] + mod export_metrics_service_request { + use super::*; + + #[test] + fn deserialize_omitted_resource_metrics() { + let empty: ExportMetricsServiceRequest = serde_json::from_str("{}").unwrap(); + assert!(empty.resource_metrics.is_empty()); + } + + // `ExportTraceServiceRequest` from the OpenTelemetry proto examples + // see + mod example { + use super::*; + + fn value() -> ExportMetricsServiceRequest { + ExportMetricsServiceRequest { + resource_metrics: vec![ResourceMetrics { + resource: Some(Resource { + attributes: vec![KeyValue { + key: String::from("service.name"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from("my.service"))), + }), + key_strindex: 0, + }], + dropped_attributes_count: 0, + entity_refs: vec![], + }), + scope_metrics: vec![ScopeMetrics { + scope: Some(InstrumentationScope { + name: String::from("my.library"), + version: String::from("1.0.0"), + attributes: vec![KeyValue { + key: String::from("my.scope.attribute"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some scope attribute", + ))), + }), + key_strindex: 0, + }], + dropped_attributes_count: 0, + }), + metrics: vec![ + Metric { + name: String::from("my.counter"), + description: String::from("I am a Counter"), + unit: String::from("1"), + metadata: vec![], + data: Some(Data::Sum(Sum { + data_points: vec![NumberDataPoint { + attributes: vec![KeyValue { + key: String::from("my.counter.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }], + start_time_unix_nano: 1544712660300000000, + time_unix_nano: 1544712660300000000, + exemplars: vec![], + flags: 0, + value: Some(MetricValue::AsDouble(5.0)), + }], + aggregation_temporality: 1, + is_monotonic: true, + })), + }, + Metric { + name: String::from("my.gauge"), + description: String::from("I am a Gauge"), + unit: String::from("1"), + metadata: vec![], + data: Some(Data::Gauge(Gauge { + data_points: vec![NumberDataPoint { + attributes: vec![KeyValue { + key: String::from("my.gauge.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }], + start_time_unix_nano: 0, + time_unix_nano: 1544712660300000000, + exemplars: vec![], + flags: 0, + value: Some(MetricValue::AsDouble(10.0)), + }], + })), + }, + Metric { + name: String::from("my.histogram"), + description: String::from("I am a Histogram"), + unit: String::from("1"), + metadata: vec![], + data: Some(Data::Histogram(Histogram { + data_points: vec![HistogramDataPoint { + attributes: vec![KeyValue { + key: String::from("my.histogram.attr"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some value", + ))), + }), + key_strindex: 0, + }], + start_time_unix_nano: 1544712660300000000, + time_unix_nano: 1544712660300000000, + count: 2, + sum: Some(2.0), + bucket_counts: vec![1, 1], + explicit_bounds: vec![1.0], + exemplars: vec![], + flags: 0, + min: Some(0.0), + max: Some(2.0), + }], + aggregation_temporality: 1, + })), + }, + ], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + } + } + + // language=json + const CANONICAL: &str = r#"{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "my.service" + } + } + ], + "droppedAttributesCount": 0, + "entityRefs": [] + }, + "scopeMetrics": [ + { + "scope": { + "name": "my.library", + "version": "1.0.0", + "attributes": [ + { + "key": "my.scope.attribute", + "value": { + "stringValue": "some scope attribute" + } + } + ], + "droppedAttributesCount": 0 + }, + "metrics": [ + { + "name": "my.counter", + "description": "I am a Counter", + "unit": "1", + "metadata": [], + "sum": { + "dataPoints": [ + { + "attributes": [ + { + "key": "my.counter.attr", + "value": { + "stringValue": "some value" + } + } + ], + "startTimeUnixNano": "1544712660300000000", + "timeUnixNano": "1544712660300000000", + "exemplars": [], + "flags": 0, + "asDouble": 5.0 + } + ], + "aggregationTemporality": 1, + "isMonotonic": true + } + }, + { + "name": "my.gauge", + "description": "I am a Gauge", + "unit": "1", + "metadata": [], + "gauge": { + "dataPoints": [ + { + "attributes": [ + { + "key": "my.gauge.attr", + "value": { + "stringValue": "some value" + } + } + ], + "startTimeUnixNano": "0", + "timeUnixNano": "1544712660300000000", + "exemplars": [], + "flags": 0, + "asDouble": 10.0 + } + ] + } + }, + { + "name": "my.histogram", + "description": "I am a Histogram", + "unit": "1", + "metadata": [], + "histogram": { + "dataPoints": [ + { + "attributes": [ + { + "key": "my.histogram.attr", + "value": { + "stringValue": "some value" + } + } + ], + "startTimeUnixNano": "1544712660300000000", + "timeUnixNano": "1544712660300000000", + "count": "2", + "sum": 2.0, + "bucketCounts": [ + "1", + "1" + ], + "explicitBounds": [ + 1.0 + ], + "exemplars": [], + "flags": 0, + "min": 0.0, + "max": 2.0 + } + ], + "aggregationTemporality": 1 + } + } + ], + "schemaUrl": "" + } + ], + "schemaUrl": "" + } + ] +}"#; + + // copied from the example json file + // language=json + const ALTERNATIVE: &str = r#"{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "my.service" + } + } + ] + }, + "scopeMetrics": [ + { + "scope": { + "name": "my.library", + "version": "1.0.0", + "attributes": [ + { + "key": "my.scope.attribute", + "value": { + "stringValue": "some scope attribute" + } + } + ] + }, + "metrics": [ + { + "name": "my.counter", + "unit": "1", + "description": "I am a Counter", + "sum": { + "aggregationTemporality": 1, + "isMonotonic": true, + "dataPoints": [ + { + "asDouble": 5, + "startTimeUnixNano": "1544712660300000000", + "timeUnixNano": "1544712660300000000", + "attributes": [ + { + "key": "my.counter.attr", + "value": { + "stringValue": "some value" + } + } + ] + } + ] + } + }, + { + "name": "my.gauge", + "unit": "1", + "description": "I am a Gauge", + "gauge": { + "dataPoints": [ + { + "asDouble": 10, + "timeUnixNano": "1544712660300000000", + "attributes": [ + { + "key": "my.gauge.attr", + "value": { + "stringValue": "some value" + } + } + ] + } + ] + } + }, + { + "name": "my.histogram", + "unit": "1", + "description": "I am a Histogram", + "histogram": { + "aggregationTemporality": 1, + "dataPoints": [ + { + "startTimeUnixNano": "1544712660300000000", + "timeUnixNano": "1544712660300000000", + "count": "2", + "sum": 2, + "bucketCounts": ["1","1"], + "explicitBounds": [1], + "min": 0, + "max": 2, + "attributes": [ + { + "key": "my.histogram.attr", + "value": { + "stringValue": "some value" + } + } + ] + } + ] + } + } + ] + } + ] + } + ] +} +"#; + + #[test] + fn serialize() { + let input: ExportMetricsServiceRequest = value(); + let actual = + serde_json::to_string_pretty(&input).expect("serialization must succeed"); + assert_eq!(actual, CANONICAL); + } + + #[test] + fn deserialize_canonical() { + let actual: ExportMetricsServiceRequest = + serde_json::from_str(CANONICAL).expect("deserialization must succeed"); + let expected: ExportMetricsServiceRequest = value(); + assert_eq!(actual, expected); + } + + #[test] + fn deserialize_alternative() { + let actual: ExportMetricsServiceRequest = + serde_json::from_str(ALTERNATIVE).expect("deserialization must succeed"); + let expected: ExportMetricsServiceRequest = value(); + assert_eq!(actual, expected); + } + } + } + + #[cfg(feature = "logs")] + mod export_logs_service_response { + use super::*; + + #[test] + fn deserialize_empty_partial_success() { + let response: ExportLogsServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_log_records, 0); + assert!(partial_success.error_message.is_empty()); + } + + #[test] + fn deserialize_partial_success_with_omitted_rejected_log_records() { + let response: ExportLogsServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{"errorMessage":"backend warning"}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_log_records, 0); + assert_eq!(partial_success.error_message, "backend warning"); + } + + #[test] + fn deserialize_partial_success_with_omitted_error_message() { + let response: ExportLogsServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{"rejectedLogRecords":1}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_log_records, 1); + assert!(partial_success.error_message.is_empty()); + } + } + + #[cfg(feature = "trace")] + mod export_trace_service_response { + use super::*; + + #[test] + fn deserialize_empty_partial_success() { + let response: ExportTraceServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_spans, 0); + assert!(partial_success.error_message.is_empty()); + } + + #[test] + fn deserialize_partial_success_with_omitted_rejected_spans() { + let response: ExportTraceServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{"errorMessage":"backend warning"}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_spans, 0); + assert_eq!(partial_success.error_message, "backend warning"); + } + + #[test] + fn deserialize_partial_success_with_omitted_error_message() { + let response: ExportTraceServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{"rejectedSpans":1}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_spans, 1); + assert!(partial_success.error_message.is_empty()); + } + } + + #[cfg(feature = "metrics")] + mod export_metrics_service_response { + use super::*; + + #[test] + fn deserialize_empty_partial_success() { + let response: ExportMetricsServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_data_points, 0); + assert!(partial_success.error_message.is_empty()); + } + + #[test] + fn deserialize_partial_success_with_omitted_rejected_data_points() { + let response: ExportMetricsServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{"errorMessage":"backend warning"}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_data_points, 0); + assert_eq!(partial_success.error_message, "backend warning"); + } + + #[test] + fn deserialize_partial_success_with_omitted_error_message() { + let response: ExportMetricsServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{"rejectedDataPoints":1}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_data_points, 1); + assert!(partial_success.error_message.is_empty()); + } + } + + #[cfg(feature = "profiles")] + mod export_profiles_service_response { + use super::*; + + #[test] + fn deserialize_empty_partial_success() { + let response: ExportProfilesServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_profiles, 0); + assert!(partial_success.error_message.is_empty()); + } + + #[test] + fn deserialize_partial_success_with_omitted_rejected_profiles() { + let response: ExportProfilesServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{"errorMessage":"backend warning"}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_profiles, 0); + assert_eq!(partial_success.error_message, "backend warning"); + } + + #[test] + fn deserialize_partial_success_with_omitted_error_message() { + let response: ExportProfilesServiceResponse = + serde_json::from_str(r#"{"partialSuccess":{"rejectedProfiles":1}}"#) + .expect("deserialization must succeed"); + + let partial_success = response + .partial_success + .expect("partial success must be present"); + + assert_eq!(partial_success.rejected_profiles, 1); + assert!(partial_success.error_message.is_empty()); + } + } + + #[cfg(feature = "logs")] + mod export_logs_service_request { + use super::*; + + #[test] + fn deserialize_omitted_resource_logs() { + let empty: ExportLogsServiceRequest = serde_json::from_str("{}").unwrap(); + assert!(empty.resource_logs.is_empty()); + } + + // `ExportTraceServiceRequest` from the OpenTelemetry proto examples + // see + mod example { + use super::*; + + fn value() -> ExportLogsServiceRequest { + ExportLogsServiceRequest { + resource_logs: vec![ResourceLogs { + resource: Some(Resource { + attributes: vec![KeyValue { + key: String::from("service.name"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from("my.service"))), + }), + key_strindex: 0, + }], + dropped_attributes_count: 0, + entity_refs: vec![], + }), + scope_logs: vec![ScopeLogs { + scope: Some(InstrumentationScope { + name: String::from("my.library"), + version: String::from("1.0.0"), + attributes: vec![KeyValue { + key: String::from("my.scope.attribute"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some scope attribute", + ))), + }), + key_strindex: 0, + }], + dropped_attributes_count: 0, + }), + log_records: vec![LogRecord { + time_unix_nano: 1544712660300000000, + observed_time_unix_nano: 1544712660300000000, + severity_number: 10, + severity_text: String::from("Information"), + body: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "Example log record", + ))), + }), + event_name: "test_log_event".to_string(), + attributes: vec![ + KeyValue { + key: String::from("string.attribute"), + value: Some(AnyValue { + value: Some(Value::StringValue(String::from( + "some string", + ))), + }), + key_strindex: 0, + }, + KeyValue { + key: String::from("boolean.attribute"), + value: Some(AnyValue { + value: Some(Value::BoolValue(true)), + }), + key_strindex: 0, + }, + KeyValue { + key: String::from("int.attribute"), + value: Some(AnyValue { + value: Some(Value::IntValue(10)), + }), + key_strindex: 0, + }, + KeyValue { + key: String::from("double.attribute"), + value: Some(AnyValue { + value: Some(Value::DoubleValue(637.704)), + }), + key_strindex: 0, + }, + KeyValue { + key: String::from("array.attribute"), + value: Some(AnyValue { + value: Some(Value::ArrayValue(ArrayValue { + values: vec![ + AnyValue { + value: Some(Value::StringValue( + String::from("many"), + )), + }, + AnyValue { + value: Some(Value::StringValue( + String::from("values"), + )), + }, + ], + })), + }), + key_strindex: 0, + }, + KeyValue { + key: String::from("map.attribute"), + value: Some(AnyValue { + value: Some(Value::KvlistValue(KeyValueList { + values: vec![KeyValue { + key: String::from("some.map.key"), + value: Some(AnyValue { + value: Some(Value::StringValue( + String::from("some value"), + )), + }), + key_strindex: 0, + }], + })), + }), + key_strindex: 0, + }, + ], + dropped_attributes_count: 0, + flags: 0, + trace_id: const_hex::decode("5b8efff798038103d269b633813fc60c") + .unwrap(), + span_id: const_hex::decode("eee19b7ec3c1b174").unwrap(), + }], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + } + } + + // language=json + const CANONICAL: &str = r#"{ + "resourceLogs": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "my.service" + } + } + ], + "droppedAttributesCount": 0, + "entityRefs": [] + }, + "scopeLogs": [ + { + "scope": { + "name": "my.library", + "version": "1.0.0", + "attributes": [ + { + "key": "my.scope.attribute", + "value": { + "stringValue": "some scope attribute" + } + } + ], + "droppedAttributesCount": 0 + }, + "logRecords": [ + { + "timeUnixNano": "1544712660300000000", + "observedTimeUnixNano": "1544712660300000000", + "severityNumber": 10, + "severityText": "Information", + "body": { + "stringValue": "Example log record" + }, + "attributes": [ + { + "key": "string.attribute", + "value": { + "stringValue": "some string" + } + }, + { + "key": "boolean.attribute", + "value": { + "boolValue": true + } + }, + { + "key": "int.attribute", + "value": { + "intValue": "10" + } + }, + { + "key": "double.attribute", + "value": { + "doubleValue": 637.704 + } + }, + { + "key": "array.attribute", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "many" + }, + { + "stringValue": "values" + } + ] + } + } + }, + { + "key": "map.attribute", + "value": { + "kvlistValue": { + "values": [ + { + "key": "some.map.key", + "value": { + "stringValue": "some value" + } + } + ] + } + } + } + ], + "droppedAttributesCount": 0, + "flags": 0, + "traceId": "5b8efff798038103d269b633813fc60c", + "spanId": "eee19b7ec3c1b174", + "eventName": "test_log_event" + } + ], + "schemaUrl": "" + } + ], + "schemaUrl": "" + } + ] +}"#; + + // copied from the example json file with fix for + // language=json + const ALTERNATIVE: &str = r#"{ + "resourceLogs": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "my.service" + } + } + ] + }, + "scopeLogs": [ + { + "scope": { + "name": "my.library", + "version": "1.0.0", + "attributes": [ + { + "key": "my.scope.attribute", + "value": { + "stringValue": "some scope attribute" + } + } + ] + }, + "logRecords": [ + { + "timeUnixNano": "1544712660300000000", + "observedTimeUnixNano": "1544712660300000000", + "severityNumber": 10, + "severityText": "Information", + "traceId": "5B8EFFF798038103D269B633813FC60C", + "spanId": "EEE19B7EC3C1B174", + "body": { + "stringValue": "Example log record" + }, + "attributes": [ + { + "key": "string.attribute", + "value": { + "stringValue": "some string" + } + }, + { + "key": "boolean.attribute", + "value": { + "boolValue": true + } + }, + { + "key": "int.attribute", + "value": { + "intValue": "10" + } + }, + { + "key": "double.attribute", + "value": { + "doubleValue": 637.704 + } + }, + { + "key": "array.attribute", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "many" + }, + { + "stringValue": "values" + } + ] + } + } + }, + { + "key": "map.attribute", + "value": { + "kvlistValue": { + "values": [ + { + "key": "some.map.key", + "value": { + "stringValue": "some value" + } + } + ] + } + } + } + ], + "eventName": "test_log_event" + } + ] + } + ] + } + ] +} +"#; + + #[test] + fn serialize() { + let input: ExportLogsServiceRequest = value(); + let actual = + serde_json::to_string_pretty(&input).expect("serialization must succeed"); + assert_eq!(actual, CANONICAL); + } + + #[test] + fn deserialize_canonical() { + let actual: ExportLogsServiceRequest = + serde_json::from_str(CANONICAL).expect("deserialization must succeed"); + let expected: ExportLogsServiceRequest = value(); + assert_eq!(actual, expected); + } + + #[test] + fn deserialize_alternative() { + let actual: ExportLogsServiceRequest = + serde_json::from_str(ALTERNATIVE).expect("deserialization must succeed"); + let expected: ExportLogsServiceRequest = value(); + assert_eq!(actual, expected); + } + } + } + #[cfg(feature = "metrics")] + mod metrics_with_nan { + use super::*; + use opentelemetry_proto::tonic::metrics::v1::summary_data_point::ValueAtQuantile; + use opentelemetry_proto::tonic::metrics::v1::Summary; + use opentelemetry_proto::tonic::metrics::v1::SummaryDataPoint; + + fn value_with_nan() -> ExportMetricsServiceRequest { + ExportMetricsServiceRequest { + resource_metrics: vec![ResourceMetrics { + resource: Some(Resource { + attributes: vec![KeyValue { + key: String::from("service.name"), + value: None, + key_strindex: 0, + }], + dropped_attributes_count: 0, + entity_refs: vec![], + }), + scope_metrics: vec![ScopeMetrics { + scope: None, + metrics: vec![Metric { + name: String::from("example_metric"), + description: String::from("A sample metric with NaN values"), + unit: String::from("1"), + metadata: vec![], + data: Some( + opentelemetry_proto::tonic::metrics::v1::metric::Data::Summary( + Summary { + data_points: vec![SummaryDataPoint { + attributes: vec![], + start_time_unix_nano: 0, + time_unix_nano: 0, + count: 100, + sum: 500.0, + quantile_values: vec![ + ValueAtQuantile { + quantile: 0.5, + value: f64::NAN, + }, + ValueAtQuantile { + quantile: 0.9, + value: f64::NAN, + }, + ], + flags: 0, + }], + }, + ), + ), + }], + schema_url: String::new(), + }], + schema_url: String::new(), + }], + } + } + + // language=json + const CANONICAL_WITH_NAN: &str = r#"{ + "resourceMetrics": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": null + } + ], + "droppedAttributesCount": 0, + "entityRefs": [] + }, + "scopeMetrics": [ + { + "scope": null, + "metrics": [ + { + "name": "example_metric", + "description": "A sample metric with NaN values", + "unit": "1", + "metadata": [], + "summary": { + "dataPoints": [ + { + "attributes": [], + "startTimeUnixNano": "0", + "timeUnixNano": "0", + "count": "100", + "sum": 500.0, + "quantileValues": [ + { + "quantile": 0.5, + "value": "NaN" + }, + { + "quantile": 0.9, + "value": "NaN" + } + ], + "flags": 0 + } + ] + } + } + ], + "schemaUrl": "" + } + ], + "schemaUrl": "" + } + ] + }"#; + + #[test] + fn serialize_with_nan() { + let input: ExportMetricsServiceRequest = value_with_nan(); + + // Serialize the structure to JSON + let actual = serde_json::to_string_pretty(&input).expect("serialization must succeed"); + + // Normalize both the actual and expected JSON for comparison + let actual_value: serde_json::Value = + serde_json::from_str(&actual).expect("valid JSON"); + let expected_value: serde_json::Value = + serde_json::from_str(CANONICAL_WITH_NAN).expect("valid JSON"); + + // Compare the normalized JSON values + assert_eq!(actual_value, expected_value); + } + + #[test] + fn deserialize_with_nan() { + let actual: ExportMetricsServiceRequest = + serde_json::from_str(CANONICAL_WITH_NAN).expect("deserialization must succeed"); + + // Ensure the deserialized structure matches the expected values + assert_eq!(actual.resource_metrics.len(), 1); + + let resource_metric = &actual.resource_metrics[0]; + assert_eq!( + resource_metric.resource.as_ref().unwrap().attributes.len(), + 1 + ); + assert_eq!( + resource_metric.resource.as_ref().unwrap().attributes[0].key, + "service.name" + ); + assert!(resource_metric.resource.as_ref().unwrap().attributes[0] + .value + .is_none()); + + assert_eq!(resource_metric.scope_metrics.len(), 1); + + let scope_metric = &resource_metric.scope_metrics[0]; + assert!(scope_metric.scope.is_none()); + assert_eq!(scope_metric.metrics.len(), 1); + + let metric = &scope_metric.metrics[0]; + assert_eq!(metric.name, "example_metric"); + assert_eq!(metric.description, "A sample metric with NaN values"); + assert_eq!(metric.unit, "1"); + + if let Some(opentelemetry_proto::tonic::metrics::v1::metric::Data::Summary(summary)) = + &metric.data + { + assert_eq!(summary.data_points.len(), 1); + + let data_point = &summary.data_points[0]; + assert_eq!(data_point.attributes.len(), 0); + assert_eq!(data_point.start_time_unix_nano, 0); + assert_eq!(data_point.time_unix_nano, 0); + assert_eq!(data_point.count, 100); + assert_eq!(data_point.sum, 500.0); + + assert_eq!(data_point.quantile_values.len(), 2); + + // Verify that quantile values are NaN + assert!(data_point.quantile_values[0].value.is_nan()); + assert!(data_point.quantile_values[0].quantile == 0.5); + assert!(data_point.quantile_values[1].value.is_nan()); + assert!(data_point.quantile_values[1].quantile == 0.9); + } else { + panic!("Expected metric data to be of type Summary"); + } + } + } + + #[cfg(feature = "metrics")] + mod bare_number_deserialization { + use super::*; + + #[test] + fn u64_bare_number() { + // parsers must accept both bare and quoted numbers + let json = r#"{ + "resourceMetrics": [ + { + "scopeMetrics": [ + { + "metrics": [ + { + "name": "test", + "gauge": { + "dataPoints": [ + { + "startTimeUnixNano": 1544712660000000000, + "timeUnixNano": "1544712661000000000", + "asInt": "42" + } + ] + } + } + ] + } + ] + } + ] +}"#; + let result: ExportMetricsServiceRequest = + serde_json::from_str(json).expect("bare u64 numbers must deserialize"); + let dp = &result.resource_metrics[0].scope_metrics[0].metrics[0]; + if let Some(Data::Gauge(gauge)) = &dp.data { + assert_eq!( + gauge.data_points[0].start_time_unix_nano, + 1544712660000000000 + ); + assert_eq!(gauge.data_points[0].time_unix_nano, 1544712661000000000); + } else { + panic!("expected gauge data"); + } + } + + #[test] + fn vec_u64_bare_numbers() { + // bucket_counts should accept both quoted and bare numbers in arrays + let json = r#"{ + "resourceMetrics": [ + { + "scopeMetrics": [ + { + "metrics": [ + { + "name": "test_histogram", + "histogram": { + "dataPoints": [ + { + "startTimeUnixNano": "0", + "timeUnixNano": "0", + "count": 10, + "sum": 100.0, + "bucketCounts": [1, "2", 3], + "explicitBounds": [10.0, 20.0] + } + ], + "aggregationTemporality": 2 + } + } + ] + } + ] + } + ] +}"#; + let result: ExportMetricsServiceRequest = + serde_json::from_str(json).expect("bare u64 vec numbers must deserialize"); + let dp = &result.resource_metrics[0].scope_metrics[0].metrics[0]; + if let Some(Data::Histogram(hist)) = &dp.data { + assert_eq!(hist.data_points[0].bucket_counts, vec![1, 2, 3]); + assert_eq!(hist.data_points[0].count, 10); + } else { + panic!("expected histogram data"); + } + } + + #[test] + fn f64_bare_number_in_summary_quantile() { + // deserialize_f64_special should accept both quoted and bare numeric strings + let json = r#"{ + "resourceMetrics": [ + { + "scopeMetrics": [ + { + "metrics": [ + { + "name": "test_summary", + "summary": { + "dataPoints": [ + { + "count": "1", + "sum": 100.0, + "quantileValues": [ + { + "quantile": "0.5", + "value": 99.0 + } + ] + } + ] + } + } + ] + } + ] + } + ] +}"#; + let result: ExportMetricsServiceRequest = + serde_json::from_str(json).expect("quoted f64 numbers must deserialize"); + let dp = &result.resource_metrics[0].scope_metrics[0].metrics[0]; + if let Some(opentelemetry_proto::tonic::metrics::v1::metric::Data::Summary(summary)) = + &dp.data + { + let qv = &summary.data_points[0].quantile_values[0]; + // assert!((qv.quantile - 0.5).abs() < f64::EPSILON); + // assert!((qv.value - 99.0).abs() < f64::EPSILON); + assert_eq!(qv.quantile, 0.5); + assert_eq!(qv.value, 99.0); + } else { + panic!("expected summary data"); + } + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/.gitignore b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/.gitignore new file mode 100644 index 00000000..45d2571c --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/.gitignore @@ -0,0 +1 @@ +semantic-conventions/ diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/CHANGELOG.md b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/CHANGELOG.md new file mode 100644 index 00000000..db42f578 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/CHANGELOG.md @@ -0,0 +1,179 @@ +# Changelog + +## vNext + +## 0.33.0 + +Released 2026-Sep-18 + +## 0.32.1 + +Released 2026-Jun-26 + +- Update to [v1.42.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.42.0) of the semantic conventions. + +## 0.32.0 + +Released 2026-May-08 + +## 0.31.0 + +Released 2025-Sep-25 + +- Update to [v1.34.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.34.0) of the semantic conventions. + +## 0.30.0 + +Released 2025-May-23 + +- Update to [v1.32.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.32.0) of the semantic conventions. + +## 0.29.0 + +Released 2025-Mar-21 + +- Update to [v1.31.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.31.0) of the semantic conventions. + +## 0.28.0 + +Released 2025-Feb-10 + +- Update to [v1.29.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.29.0) of the semantic conventions. +- Bump msrv to 1.75.0. + +## 0.27.0 + +Released 2024-Nov-11 + +- Bump MSRV to 1.70 [#2179](https://github.com/open-telemetry/opentelemetry-rust/pull/2179) +- Update to [v1.28.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.28.0) of the semantic conventions. + +## v0.26.0 +Released 2024-Sep-30 + +### Changed + +- Starting with this version, this crate will use Weaver for the generation of + the semantic conventions. +- **Breaking** Introduced a new feature `semconv_experimental` to enable experimental semantic conventions. + This feature is disabled by default. + +## v0.25.0 +### Changed + +- Starting with this version, this crate will align with `opentelemetry` crate + on major,minor versions. +- Update to [v1.27.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.27.0) of the semantic conventions. + [#2000](https://github.com/open-telemetry/opentelemetry-rust/pull/2000) + +## v0.16.0 +### Changed + +- **Breaking** Moved duplicated (and unrelated) attributes from `opentelemetry_semantic_conventions::trace` and `opentelemetry_semantic_conventions::resource` into `opentelemetry_semantic_conventions::attribute` (which now contains all semantic attributes). `trace` and `resource` now only contain references to attributes which fall under their respective category. + +### Added + +- Created `opentelemetry_semantic_conventions::metric` to store metric semantic conventions. + +## v0.15.0 + +### Changed + +- Update to [v1.24.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.24.0) of the semantic conventions. + [#1596](https://github.com/open-telemetry/opentelemetry-rust/pull/1596) +- Update to [v1.25.0](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.25.0) of the semantic conventions. + [#1681](https://github.com/open-telemetry/opentelemetry-rust/pull/1681) + +## v0.14.0 + +### Changed + +- **Breaking** Replaced Key constants with &'static str for tracing compatibility + [#1334](https://github.com/open-telemetry/opentelemetry-rust/pull/1334) + +## v0.13.0 + +### Changed + +- Bump MSRV to 1.65 [#1318](https://github.com/open-telemetry/opentelemetry-rust/pull/1318) +- Bump MSRV to 1.64 [#1203](https://github.com/open-telemetry/opentelemetry-rust/pull/1203) + +## v0.12.0 + +### Changed + +- Update to v1.21.0 spec +- Update to opentelemetry-api v0.20.0 + +## v0.11.0 + +### Changed +- Update to `opentelemetry` v0.19. +- Update to `opentelemetry_http` v0.8. +- Bump MSRV to 1.57 [#953](https://github.com/open-telemetry/opentelemetry-rust/pull/953). +- Update to v1.17.0 spec [#960](https://github.com/open-telemetry/opentelemetry-rust/pull/960). +- Update dependencies and bump MSRV to 1.60 [#969](https://github.com/open-telemetry/opentelemetry-rust/pull/969). + +## v0.10.0 + +### Changed + +- update to v1.9 spec #754 +- Update to opentelemetry v0.18.0 + +## v0.9.0 + +### Changed + +- Update to opentelemetry v0.17.0 + +## v0.8.0 + +### Changed + +- Update to opentelemetry v0.16.0 + +## v0.7.0 + +### Changed + +- Update to spec version 1.4.0 #570 +- Update to opentelemetry v0.15.0 + +## v0.6.0 + +### Changed + +- Update to spec version 1.3.0 #547 +- Update to opentelemetry v0.14.0 + +## v0.5.0 + +### Changed +- Update to opentelemetry v0.13.0 + +### Removed +- Removed `from_env` and use environment variables to initialize the configurations by default #459 + +## v0.4.0 + +### Changed +- Update to opentelemetry v0.12.0 + +## v0.3.0 + +### Changed + +- Update to opentelemetry v0.11.0 + +## v0.2.0 + +### Changed + +- Update to opentelemetry v0.10.0 + +## v0.1.0 + +### Added + +- Semantic conventions for [trace](https://github.com/open-telemetry/opentelemetry-specification/tree/master/specification/trace/semantic_conventions) and [resource](https://github.com/open-telemetry/opentelemetry-specification/tree/master/specification/resource/semantic_conventions). diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/Cargo.toml b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/Cargo.toml new file mode 100644 index 00000000..8bd89420 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/Cargo.toml @@ -0,0 +1,75 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.75.0" +name = "opentelemetry-semantic-conventions" +version = "0.33.0" +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Semantic conventions for OpenTelemetry" +homepage = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-semantic-conventions" +readme = "README.md" +keywords = [ + "opentelemetry", + "tracing", + "async", +] +categories = [ + "development-tools::debugging", + "development-tools::profiling", + "asynchronous", +] +license = "Apache-2.0" +repository = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-semantic-conventions" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[features] +default = [] +semconv_experimental = [] + +[lib] +name = "opentelemetry_semantic_conventions" +path = "src/lib.rs" +bench = false + +[dev-dependencies.opentelemetry] +version = "0.33" +default-features = false + +[dev-dependencies.opentelemetry_sdk] +version = "0.33" +features = ["trace"] +default-features = false + +[lints.clippy.all] +level = "warn" +priority = 1 + +[lints.rust] +edition_2024_expr_fragment_specifier = "allow" +if_let_rescope = "allow" +tail_expr_drop_order = "allow" + +[lints.rust.rust_2024_compatibility] +level = "warn" +priority = -1 diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/README.md b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/README.md new file mode 100644 index 00000000..712f72fc --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/README.md @@ -0,0 +1,55 @@ +# OpenTelemetry Semantic Conventions + +![OpenTelemetry — An observability framework for cloud-native software.][splash] + +[splash]: https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo-text.png + +Semantic conventions for applications instrumented with [`OpenTelemetry`]. + +[![Crates.io: opentelemetry-semantic-conventions](https://img.shields.io/crates/v/opentelemetry-semantic-conventions.svg)](https://crates.io/crates/opentelemetry-semantic-conventions) +[![Documentation](https://docs.rs/opentelemetry-semantic-conventions/badge.svg)](https://docs.rs/opentelemetry-semantic-conventions) +[![LICENSE](https://img.shields.io/crates/l/opentelemetry-semantic-conventions)](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-semantic-conventions/LICENSE) +[![GitHub Actions CI](https://github.com/open-telemetry/opentelemetry-rust/workflows/CI/badge.svg)](https://github.com/open-telemetry/opentelemetry-rust/actions?query=workflow%3ACI+branch%3Amain) +[![Slack](https://img.shields.io/badge/slack-@cncf/otel/rust-brightgreen.svg?logo=slack)](https://cloud-native.slack.com/archives/C03GDP0H023) + +## Overview + +[`OpenTelemetry`] is a collection of tools, APIs, and SDKs used to instrument, +generate, collect, and export telemetry data (metrics, logs, and traces) for +analysis in order to understand your software's performance and behavior. This +crate provides standardized naming patterns for attributes, and +resources to help facilitate interoperability and compatibility with processing +and visualization tools. + +[`opentelemetry`]: https://crates.io/crates/opentelemetry + +*[Supported Rust Versions](#supported-rust-versions)* + +### What does this crate contain? + +This crate contains Rust constants generated from the [OpenTelemetry Semantic +Conventions](https://github.com/open-telemetry/semantic-conventions) +specification. It provides standardized attribute keys, metric names and +resource attributes so that instrumentation produced by different libraries +remains interoperable across processing and visualization tools. + +## Getting started + +See [docs](https://docs.rs/opentelemetry-semantic-conventions). + +## Release Notes + +You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-semantic-conventions/CHANGELOG.md). + +## Supported Rust Versions + +OpenTelemetry is built against the latest stable release. The minimum supported +version is 1.75.0. The current OpenTelemetry version is NOT guaranteed to build +on Rust versions earlier than the minimum supported version. + +The current stable Rust compiler and the three most recent minor versions +before it will always be supported. For example, if the current stable compiler +version is 1.49, the minimum supported version will not be increased past 1.46, +three minor versions prior. Increasing the minimum supported compiler version +is not considered a semver breaking change as long as doing so complies with +this policy. \ No newline at end of file diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/generate-consts-from-spec.sh b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/generate-consts-from-spec.sh new file mode 100755 index 00000000..dc73fc9e --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/generate-consts-from-spec.sh @@ -0,0 +1,79 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CRATE_DIR="${SCRIPT_DIR}/../" + +# freeze the spec version and generator version to make generation reproducible +SPEC_VERSION=1.42.0 +WEAVER_VERSION=v0.24.1 + +cd "$CRATE_DIR" + +rm -rf semantic-conventions || true +mkdir semantic-conventions +cd semantic-conventions + +git init +git remote add origin https://github.com/open-telemetry/semantic-conventions.git +git fetch origin "v$SPEC_VERSION" +git reset --hard FETCH_HEAD +cd "$CRATE_DIR" + +SED=(sed -i) +if [[ "$(uname)" = "Darwin" ]]; then + SED=(sed -i "") +fi + +# Keep `SCHEMA_URL` key in sync with spec version +"${SED[@]}" "s/\(opentelemetry.io\/schemas\/\)[^\"]*\"/\1$SPEC_VERSION\"/" scripts/templates/registry/rust/weaver.yaml + +docker run --rm \ + --mount type=bind,source=$CRATE_DIR/semantic-conventions/model,target=/home/weaver/source,readonly \ + --mount type=bind,source=$CRATE_DIR/scripts/templates,target=/home/weaver/templates,readonly \ + --mount type=bind,source=$CRATE_DIR/src,target=/home/weaver/target \ + otel/weaver:$WEAVER_VERSION \ + registry generate \ + --registry=/home/weaver/source \ + --templates=/home/weaver/templates \ + rust \ + /home/weaver/target/ + +# handle doc generation failures +"${SED[@]}" 's/\[2\]\.$//' src/attribute.rs # remove trailing [2] from few of the doc comments + +# handle escaping ranges like [0,n] / [0.0, ...] in descriptions/notes which will cause broken intra-doc links +# unescape any mistakenly escaped ranges which actually contained a link (not that we currently have any) +expression=' + s/\[([a-zA-Z0-9\.\s]+,[a-zA-Z0-9\.\s]+)\]/\\[\1\\]/g + s/\\\[([^\]]+)\]\(([^)]+)\)/[\1](\2)/g +' + +"${SED[@]}" -E "${expression}" src/metric.rs +"${SED[@]}" -E "${expression}" src/attribute.rs + +# Fix unclosed HTML tag warnings for in doc comments. +# Rustdoc treats as an unclosed HTML tag and fails the build with -D warnings. +# We replace with Markdown code formatting `key` to prevent the error. +# TODO: This workaround should be removed once the upstream generator handles this correctly. +"${SED[@]}" 's//`key`/g' src/attribute.rs + +# Patch: rustdoc warns about bare URLs in doc comments. +# The following line wraps the specific Kubernetes ResourceRequirements URL with <...> +# as suggested by rustdoc warnings, so it becomes a clickable link and the warning goes away. +"${SED[@]}" -E 's|(/// See )(https://kubernetes.io/docs/reference/generated/kubernetes-api/v[0-9]+\.[0-9]+/#resourcerequirements-v1-core)( for details)|\1<\2>\3|g' src/metric.rs + +# Tag unlabeled fenced code blocks in doc comments as `text` so rustdoc doesn't +# try to compile them as Rust. Some semconv descriptions include pseudo-code +# (e.g. with `←` arrows) that breaks `cargo test --doc`. +# TODO: Remove once the upstream generator emits language-tagged fences. +awk ' + /^\/\/\/ ```$/ { + if (in_block) { print; in_block = 0 } + else { sub(/```/, "```text"); print; in_block = 1 } + next + } + { print } +' src/attribute.rs > src/attribute.rs.tmp && mv src/attribute.rs.tmp src/attribute.rs + +cargo fmt diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/attribute.rs.j2 b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/attribute.rs.j2 new file mode 100644 index 00000000..b1d8140b --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/attribute.rs.j2 @@ -0,0 +1,26 @@ +{%- import 'macros.j2' as attr_macros -%} +// DO NOT EDIT, this is an auto-generated file +// +// If you want to update the file: +// - Edit the template at scripts/templates/registry/rust/attributes.rs.j2 +// - Run the script at scripts/generate-consts-from-spec.sh + +//! # Semantic Attributes +//! +//! The entire set of semantic attributes (or [conventions](https://opentelemetry.io/docs/concepts/semantic-conventions/)) defined by the project. The resource, metric, and trace modules reference these attributes. + +{% for root_ns in ctx %} + {% for attr in root_ns.attributes | rejectattr("name", "in", params.excluded_attributes) %} +{# Escape any `<...>` tags to `[...]` to avoid Rustdoc warnings and HTML parsing issues. #} +{% set safe_note = attr.note | replace('<', '[') | replace('>', ']') %} +{{ [attr.brief, concat_if("\n\n## Notes\n\n", safe_note), attr_macros.examples(attr)] | comment }} + {% if attr is experimental %} +#[cfg(feature = "semconv_experimental")] + {% endif %} + {% if attr is deprecated %} +#[deprecated(note="{{ attr.deprecated | replace('\n', ' ') | replace('"', '') | trim }}")] + {% endif %} +pub const {{ attr.name | screaming_snake_case }}: &str = "{{ attr.name }}"; + + {% endfor %} +{% endfor %} \ No newline at end of file diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/lib.rs.j2 b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/lib.rs.j2 new file mode 100644 index 00000000..d2793ca7 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/lib.rs.j2 @@ -0,0 +1,25 @@ +//! OpenTelemetry semantic conventions are agreed standardized naming patterns +//! for OpenTelemetry things. This crate aims to be the centralized place to +//! interact with these conventions. +#![warn( + future_incompatible, + missing_debug_implementations, + missing_docs, + nonstandard_style, + rust_2018_idioms, + unreachable_pub, + unused +)] +#![cfg_attr(test, deny(warnings))] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo.svg" +)] + +pub mod attribute; +pub mod metric; +pub mod resource; +pub mod trace; + +/// The schema URL that matches the version of the semantic conventions that +/// this crate defines. +pub const SCHEMA_URL: &str = "{{ params.schema_url }}"; diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/macros.j2 b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/macros.j2 new file mode 100644 index 00000000..c661ef2f --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/macros.j2 @@ -0,0 +1,13 @@ +{%- macro examples(entity) -%} + {% if entity.examples %} +# Examples + + {% if entity.examples is sequence %} + {% for example in entity.examples %} + - `{{ example | pprint }}` + {% endfor %} + {% else %} + - `{{ entity.examples | pprint }}` + {% endif %} + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/metric.rs.j2 b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/metric.rs.j2 new file mode 100644 index 00000000..21b3777e --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/metric.rs.j2 @@ -0,0 +1,79 @@ +{%- import 'macros.j2' as metric_macros -%} +// DO NOT EDIT, this is an auto-generated file +// +// If you want to update the file: +// - Edit the template at scripts/templates/registry/rust/metric.rs.j2 +// - Run the script at scripts/generate-consts-from-spec.sh + +//! # Metric Semantic Conventions +//! +//! The [metric semantic conventions] define a set of standardized attributes to +//! be used in `Meter`s. +//! +//! [metric semantic conventions]: https://opentelemetry.io/docs/specs/semconv/general/metrics/ +//! +//! ## Usage +//! +//! ```rust +//! use opentelemetry::{global, KeyValue}; +//! use opentelemetry_semantic_conventions as semconv; +//! +//! // Assumes we already have an initialized `MeterProvider` +//! // See: https://github.com/open-telemetry/opentelemetry-rust/blob/main/examples/metrics-basic/src/main.rs +//! // for an example +//! let meter = global::meter("mylibraryname"); +//! let histogram = meter +//! .u64_histogram(semconv::metric::HTTP_SERVER_REQUEST_DURATION) +//! .with_unit("s") +//! .with_description("Duration of HTTP server requests.") +//! .build(); +//! ``` + +{% for root_ns in ctx %} + {% for metric in root_ns.metrics %} +{{ ["## Description\n\n", metric.brief, concat_if("\n\n## Notes\n\n", metric.note), metric_macros.examples(metric)] | comment }} +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `{{ metric.instrument }}` | +/// | Unit: | `{{ metric.unit }}` | +/// | Status: | `{{ metric.stability | capitalize }}` | + {% if metric.attributes %} +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | + {% endif %} + {% for attribute in metric.attributes | rejectattr("name", "in", params.excluded_attributes) | sort(attribute="name") %} + {% if attribute.requirement_level %} + {% if attribute.requirement_level.conditionally_required %} + {% set req_level = "Conditionally_required" %} + {% set req_message = attribute.requirement_level.conditionally_required %} + {% else %} + {% set req_level = (attribute.requirement_level | capitalize) %} + {% set req_message = attribute.requirement_level_msg %} + {% endif %} + {% else %} + {% set req_level = "Unspecified" %} + {% set req_message = '' %} + {% endif %} +/// | [`crate::attribute::{{ attribute.name | screaming_snake_case }}`] | `{{ req_level }}`{{ (': ' + req_message.replace('\n', ' ') if req_message else '') }} + {% endfor %} + {% if metric.examples %} +/// +/// ## Examples +/// + {% for example in metric.examples %} +/// - `{{ example }}` + {% endfor %} + {% endif %} + {% if metric is experimental %} +#[cfg(feature = "semconv_experimental")] + {% endif %} + {% if metric is deprecated %} +#[deprecated(note="{{ metric.deprecated | replace('"', '') | replace('\n', ' ') | trim }}")] + {% endif %} +pub const {{ metric.metric_name | screaming_snake_case }}: &str = "{{ metric.metric_name }}"; + + {% endfor %} +{% endfor %} diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/resource.rs.j2 b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/resource.rs.j2 new file mode 100644 index 00000000..17cb385b --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/resource.rs.j2 @@ -0,0 +1,36 @@ +{%- import 'macros.j2' as attr_macros -%} +// DO NOT EDIT, this is an auto-generated file +// +// If you want to update the file: +// - Edit the template at scripts/templates/registry/rust/resource.rs.j2 +// - Run the script at scripts/generate-consts-from-spec.sh + +//! # Resource Semantic Conventions +//! +//! The [resource semantic conventions] define a set of standardized attributes +//! to be used in `Resource`s. +//! +//! [resource semantic conventions]: https://opentelemetry.io/docs/specs/semconv/resource/ +//! +//! ## Usage +//! +//! ```rust +//! use opentelemetry::KeyValue; +//! use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; +//! use opentelemetry_semantic_conventions as semconv; +//! +//! let _tracer = SdkTracerProvider::builder() +//! .with_resource(Resource::builder_empty().with_service_name("my-service").build()) +//! .build(); +//! ``` + +{% for attr in ctx | rejectattr("name", "in", params.excluded_attributes) %} + {% if attr is experimental %} +#[cfg(feature = "semconv_experimental")] + {% endif %} + {% if attr is deprecated %} +#[allow(deprecated)] + {% endif %} +pub use crate::attribute::{{ attr.name | screaming_snake_case }}; + +{% endfor %} diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/trace.rs.j2 b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/trace.rs.j2 new file mode 100644 index 00000000..e89a3dae --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/trace.rs.j2 @@ -0,0 +1,41 @@ +{%- import 'macros.j2' as attr_macros -%} +// DO NOT EDIT, this is an auto-generated file +// +// If you want to update the file: +// - Edit the template at scripts/templates/registry/rust/attributes.rs.j2 +// - Run the script at scripts/generate-consts-from-spec.sh + +//! # Trace Semantic Conventions +//! +//! The [trace semantic conventions] define a set of standardized attributes to +//! be used in `Span`s. +//! +//! [trace semantic conventions]: https://opentelemetry.io/docs/specs/semconv/general/trace/ +//! +//! ## Usage +//! +//! ```rust +//! use opentelemetry::KeyValue; +//! use opentelemetry::{global, trace::Tracer as _}; +//! use opentelemetry_semantic_conventions as semconv; +//! +//! let tracer = global::tracer("my-component"); +//! let _span = tracer +//! .span_builder("span-name") +//! .with_attributes([ +//! KeyValue::new(semconv::trace::CLIENT_ADDRESS, "example.org"), +//! KeyValue::new(semconv::trace::CLIENT_PORT, 80i64), +//! ]) +//! .start(&tracer); +//! ``` + +{% for attr in ctx | rejectattr("name", "in", params.excluded_attributes) %} + {% if attr is experimental %} +#[cfg(feature = "semconv_experimental")] + {% endif %} + {% if attr is deprecated %} +#[allow(deprecated)] + {% endif %} +pub use crate::attribute::{{ attr.name | screaming_snake_case }}; + +{% endfor %} \ No newline at end of file diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/weaver.yaml b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/weaver.yaml new file mode 100644 index 00000000..44f131c4 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/scripts/templates/registry/rust/weaver.yaml @@ -0,0 +1,46 @@ +# Whitespace control settings to simplify the definition of templates +whitespace_control: + trim_blocks: true + lstrip_blocks: true + +# Configuration for the comment formatting +comment_formats: + rust: + format: markdown + prefix: "/// " + trim: true + remove_trailing_dots: true + escape_square_brackets: true +default_comment_format: rust + +params: + schema_url: "https://opentelemetry.io/schemas/1.42.0" + exclude_root_namespace: [] + excluded_attributes: ["messaging.client_id"] + +templates: + - pattern: attribute.rs.j2 + filter: semconv_grouped_attributes($params) + application_mode: single + - pattern: metric.rs.j2 + filter: semconv_grouped_metrics($params) + application_mode: single + - pattern: resource.rs.j2 + filter: > + semconv_signal("resource"; $params) + | map(.attributes[]) + | unique_by(.name) + | sort_by(.name) + | map({name, brief, examples, deprecated, requirement_level, stability, type}) + application_mode: single + - pattern: trace.rs.j2 + filter: > + (semconv_signal("span"; $params) // []) + (semconv_signal("event"; $params) // []) + | map(.attributes // [] | .[]) + | unique_by(.name) + | sort_by(.name) + | map({name, brief, examples, deprecated, requirement_level, stability, type}) + application_mode: single + - pattern: lib.rs.j2 + filter: . + application_mode: single \ No newline at end of file diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/attribute.rs b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/attribute.rs new file mode 100644 index 00000000..300570ff --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/attribute.rs @@ -0,0 +1,11372 @@ +// DO NOT EDIT, this is an auto-generated file +// +// If you want to update the file: +// - Edit the template at scripts/templates/registry/rust/attributes.rs.j2 +// - Run the script at scripts/generate-consts-from-spec.sh + +//! # Semantic Attributes +//! +//! The entire set of semantic attributes (or [conventions](https://opentelemetry.io/docs/concepts/semantic-conventions/)) defined by the project. The resource, metric, and trace modules reference these attributes. + +/// This attribute represents the state of the application. +/// +/// ## Notes +/// +/// The Android lifecycle states are defined in [Activity lifecycle callbacks](https://developer.android.com/guide/components/activities/activity-lifecycle#lifecycle-callbacks), and from which the `OS identifiers` are derived. +/// +/// # Examples +/// +/// - `"created"` +#[cfg(feature = "semconv_experimental")] +pub const ANDROID_APP_STATE: &str = "android.app.state"; + +/// Uniquely identifies the framework API revision offered by a version (`os.version`) of the android operating system. More information can be found in the [Android API levels documentation](https://developer.android.com/guide/topics/manifest/uses-sdk-element#ApiLevels). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"33"` +/// - `"32"` +#[cfg(feature = "semconv_experimental")] +pub const ANDROID_OS_API_LEVEL: &str = "android.os.api_level"; + +/// Deprecated. Use `android.app.state` attribute instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `android.app.state`., reason: renamed, renamed_to: android.app.state}" +)] +pub const ANDROID_STATE: &str = "android.state"; + +/// Unique identifier for a particular build or compilation of the application. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"6cff0a7e-cefc-4668-96f5-1273d8b334d0"` +/// - `"9f2b833506aa6973a92fde9733e6271f"` +/// - `"my-app-1.0.0-code-123"` +#[cfg(feature = "semconv_experimental")] +pub const APP_BUILD_ID: &str = "app.build_id"; + +/// A unique identifier representing an instance of an end-user facing app crash. +/// +/// ## Notes +/// +/// Its value MAY be meaningful and be used as a reference for telemetry and metadata recorded by +/// the same instrumentation (e.g. it is an ID generated by an external source that captured the crash). +/// It MAY come from a source external to the instrumentation such that it can be used to look up additional +/// data from other sources as well as facilitate deduplication. +/// +/// # Examples +/// +/// - `"083d3d2d-9a0e-47f8-be3d-bc3c5538ba38"` +#[cfg(feature = "semconv_experimental")] +pub const APP_CRASH_ID: &str = "app.crash.id"; + +/// A unique identifier representing the installation of an application on a specific device +/// +/// ## Notes +/// +/// Its value SHOULD persist across launches of the same application installation, including through application upgrades. +/// It SHOULD change if the application is uninstalled or if all applications of the vendor are uninstalled. +/// Additionally, users might be able to reset this value (e.g. by clearing application data). +/// If an app is installed multiple times on the same device (e.g. in different accounts on Android), each `app.installation.id` SHOULD have a different value. +/// If multiple OpenTelemetry SDKs are used within the same application, they SHOULD use the same value for `app.installation.id`. +/// Hardware IDs (e.g. serial number, IMEI, MAC address) MUST NOT be used as the `app.installation.id`. +/// +/// For iOS, this value SHOULD be equal to the [vendor identifier](https://developer.apple.com/documentation/uikit/uidevice/identifierforvendor). +/// +/// For Android, examples of `app.installation.id` implementations include: +/// +/// - [Firebase Installation ID](https://firebase.google.com/docs/projects/manage-installations). +/// - A globally unique UUID which is persisted across sessions in your application. +/// - [App set ID](https://developer.android.com/identity/app-set-id). +/// - [`Settings.getString(Settings.Secure.ANDROID_ID)`](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID). +/// +/// More information about Android identifier best practices can be found in the [Android user data IDs guide](https://developer.android.com/training/articles/user-data-ids). +/// +/// # Examples +/// +/// - `"2ab2916d-a51f-4ac8-80ee-45ac31a28092"` +#[cfg(feature = "semconv_experimental")] +pub const APP_INSTALLATION_ID: &str = "app.installation.id"; + +/// A number of frame renders that experienced jank. +/// +/// ## Notes +/// +/// Depending on platform limitations, the value provided MAY be approximation. +/// +/// # Examples +/// +/// - `9` +/// - `42` +#[cfg(feature = "semconv_experimental")] +pub const APP_JANK_FRAME_COUNT: &str = "app.jank.frame_count"; + +/// The time period, in seconds, for which this jank is being reported. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1.0` +/// - `5.0` +/// - `10.24` +#[cfg(feature = "semconv_experimental")] +pub const APP_JANK_PERIOD: &str = "app.jank.period"; + +/// The minimum rendering threshold for this jank, in seconds. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `0.016` +/// - `0.7` +/// - `1.024` +#[cfg(feature = "semconv_experimental")] +pub const APP_JANK_THRESHOLD: &str = "app.jank.threshold"; + +/// The x (horizontal) coordinate of a screen coordinate, in screen pixels. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `0` +/// - `131` +#[cfg(feature = "semconv_experimental")] +pub const APP_SCREEN_COORDINATE_X: &str = "app.screen.coordinate.x"; + +/// The y (vertical) component of a screen coordinate, in screen pixels. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `12` +/// - `99` +#[cfg(feature = "semconv_experimental")] +pub const APP_SCREEN_COORDINATE_Y: &str = "app.screen.coordinate.y"; + +/// An identifier that uniquely differentiates this screen from other screens in the same application. +/// +/// ## Notes +/// +/// A screen represents only the part of the device display drawn by the app. It typically contains multiple widgets or UI components and is larger in scope than individual widgets. Multiple screens can coexist on the same display simultaneously (e.g., split view on tablets). +/// +/// # Examples +/// +/// - `"f9bc787d-ff05-48ad-90e1-fca1d46130b3"` +/// - `"com.example.app.MainActivity"` +/// - `"com.example.shop.ProductDetailFragment"` +/// - `"MyApp.ProfileView"` +/// - `"MyApp.ProfileViewController"` +#[cfg(feature = "semconv_experimental")] +pub const APP_SCREEN_ID: &str = "app.screen.id"; + +/// The name of an application screen. +/// +/// ## Notes +/// +/// A screen represents only the part of the device display drawn by the app. It typically contains multiple widgets or UI components and is larger in scope than individual widgets. Multiple screens can coexist on the same display simultaneously (e.g., split view on tablets). +/// +/// # Examples +/// +/// - `"MainActivity"` +/// - `"ProductDetailFragment"` +/// - `"ProfileView"` +/// - `"ProfileViewController"` +#[cfg(feature = "semconv_experimental")] +pub const APP_SCREEN_NAME: &str = "app.screen.name"; + +/// An identifier that uniquely differentiates this widget from other widgets in the same application. +/// +/// ## Notes +/// +/// A widget is an application component, typically an on-screen visual GUI element. +/// +/// # Examples +/// +/// - `"f9bc787d-ff05-48ad-90e1-fca1d46130b3"` +/// - `"submit_order_1829"` +#[cfg(feature = "semconv_experimental")] +pub const APP_WIDGET_ID: &str = "app.widget.id"; + +/// The name of an application widget. +/// +/// ## Notes +/// +/// A widget is an application component, typically an on-screen visual GUI element. +/// +/// # Examples +/// +/// - `"submit"` +/// - `"attack"` +/// - `"Clear Cart"` +#[cfg(feature = "semconv_experimental")] +pub const APP_WIDGET_NAME: &str = "app.widget.name"; + +/// The provenance filename of the built attestation which directly relates to the build artifact filename. This filename SHOULD accompany the artifact at publish time. See the [SLSA Relationship](https://slsa.dev/spec/v1.0/distributing-provenance#relationship-between-artifacts-and-attestations) specification for more information. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"golang-binary-amd64-v0.1.0.attestation"` +/// - `"docker-image-amd64-v0.1.0.intoto.json1"` +/// - `"release-1.tar.gz.attestation"` +/// - `"file-name-package.tar.gz.intoto.json1"` +#[cfg(feature = "semconv_experimental")] +pub const ARTIFACT_ATTESTATION_FILENAME: &str = "artifact.attestation.filename"; + +/// The full [hash value (see glossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf), of the built attestation. Some envelopes in the [software attestation space](https://github.com/in-toto/attestation/tree/main/spec) also refer to this as the **digest**. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1b31dfcd5b7f9267bf2ff47651df1cfb9147b9e4df1f335accf65b4cda498408"` +#[cfg(feature = "semconv_experimental")] +pub const ARTIFACT_ATTESTATION_HASH: &str = "artifact.attestation.hash"; + +/// The id of the build [software attestation](https://slsa.dev/attestation-model). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"123"` +#[cfg(feature = "semconv_experimental")] +pub const ARTIFACT_ATTESTATION_ID: &str = "artifact.attestation.id"; + +/// The human readable file name of the artifact, typically generated during build and release processes. Often includes the package name and version in the file name. +/// +/// ## Notes +/// +/// This file name can also act as the [Package Name](https://slsa.dev/spec/v1.0/terminology#package-model) +/// in cases where the package ecosystem maps accordingly. +/// Additionally, the artifact [can be published](https://slsa.dev/spec/v1.0/terminology#software-supply-chain) +/// for others, but that is not a guarantee. +/// +/// # Examples +/// +/// - `"golang-binary-amd64-v0.1.0"` +/// - `"docker-image-amd64-v0.1.0"` +/// - `"release-1.tar.gz"` +/// - `"file-name-package.tar.gz"` +#[cfg(feature = "semconv_experimental")] +pub const ARTIFACT_FILENAME: &str = "artifact.filename"; + +/// The full [hash value (see glossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf), often found in checksum.txt on a release of the artifact and used to verify package integrity. +/// +/// ## Notes +/// +/// The specific algorithm used to create the cryptographic hash value is +/// not defined. In situations where an artifact has multiple +/// cryptographic hashes, it is up to the implementer to choose which +/// hash value to set here; this should be the most secure hash algorithm +/// that is suitable for the situation and consistent with the +/// corresponding attestation. The implementer can then provide the other +/// hash values through an additional set of attribute extensions as they +/// deem necessary. +/// +/// # Examples +/// +/// - `"9ff4c52759e2c4ac70b7d517bc7fcdc1cda631ca0045271ddd1b192544f8a3e9"` +#[cfg(feature = "semconv_experimental")] +pub const ARTIFACT_HASH: &str = "artifact.hash"; + +/// The [Package URL](https://github.com/package-url/purl-spec) of the [package artifact](https://slsa.dev/spec/v1.0/terminology#package-model) provides a standard way to identify and locate the packaged artifact. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"pkg:github/package-url/purl-spec@1209109710924"` +/// - `"pkg:npm/foo@12.12.3"` +#[cfg(feature = "semconv_experimental")] +pub const ARTIFACT_PURL: &str = "artifact.purl"; + +/// The version of the artifact. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"v0.1.0"` +/// - `"1.2.1"` +/// - `"122691-build"` +#[cfg(feature = "semconv_experimental")] +pub const ARTIFACT_VERSION: &str = "artifact.version"; + +/// The result of the authentication operation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"success"` +/// - `"failure"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_AUTHENTICATION_RESULT: &str = "aspnetcore.authentication.result"; + +/// The identifier that names a particular authentication handler. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Cookies"` +/// - `"Bearer"` +/// - `"Identity.Application"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_AUTHENTICATION_SCHEME: &str = "aspnetcore.authentication.scheme"; + +/// The name of the authorization policy. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"RequireAdminRole"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_AUTHORIZATION_POLICY: &str = "aspnetcore.authorization.policy"; + +/// The result of calling the authorization service. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"success"` +/// - `"failure"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_AUTHORIZATION_RESULT: &str = "aspnetcore.authorization.result"; + +/// ASP.NET Core exception middleware handling result. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"handled"` +/// - `"unhandled"` +pub const ASPNETCORE_DIAGNOSTICS_EXCEPTION_RESULT: &str = "aspnetcore.diagnostics.exception.result"; + +/// Full type name of the [`IExceptionHandler`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.diagnostics.iexceptionhandler) implementation that handled the exception. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Contoso.MyHandler"` +pub const ASPNETCORE_DIAGNOSTICS_HANDLER_TYPE: &str = "aspnetcore.diagnostics.handler.type"; + +/// The error code for a failed identity operation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"DefaultError"` +/// - `"PasswordMismatch"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_ERROR_CODE: &str = "aspnetcore.identity.error_code"; + +/// The result from checking the password. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"success"` +/// - `"failure"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_PASSWORD_CHECK_RESULT: &str = + "aspnetcore.identity.password_check_result"; + +/// The result of the identity operation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"success"` +/// - `"failure"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_RESULT: &str = "aspnetcore.identity.result"; + +/// Whether the sign in result was success or failure. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"password"` +/// - `"two_factor"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_SIGN_IN_RESULT: &str = "aspnetcore.identity.sign_in.result"; + +/// The authentication type. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"password"` +/// - `"two_factor"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_SIGN_IN_TYPE: &str = "aspnetcore.identity.sign_in.type"; + +/// What the token will be used for. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"success"` +/// - `"failure"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_TOKEN_PURPOSE: &str = "aspnetcore.identity.token_purpose"; + +/// The result of token verification. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"success"` +/// - `"failure"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_TOKEN_VERIFIED: &str = "aspnetcore.identity.token_verified"; + +/// The user update type. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"update"` +/// - `"user_name"` +/// - `"reset_password"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_USER_UPDATE_TYPE: &str = "aspnetcore.identity.user.update_type"; + +/// The full name of the identity user type. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Contoso.ContosoUser"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_USER_TYPE: &str = "aspnetcore.identity.user_type"; + +/// The name of the library or subsystem using the memory pool instance. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"kestrel"` +/// - `"iis"` +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_MEMORY_POOL_OWNER: &str = "aspnetcore.memory_pool.owner"; + +/// Rate limiting policy name. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"fixed"` +/// - `"sliding"` +/// - `"token"` +pub const ASPNETCORE_RATE_LIMITING_POLICY: &str = "aspnetcore.rate_limiting.policy"; + +/// Rate-limiting result, shows whether the lease was acquired or contains a rejection reason +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"acquired"` +/// - `"request_canceled"` +pub const ASPNETCORE_RATE_LIMITING_RESULT: &str = "aspnetcore.rate_limiting.result"; + +/// Flag indicating if request was handled by the application pipeline. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `true` +pub const ASPNETCORE_REQUEST_IS_UNHANDLED: &str = "aspnetcore.request.is_unhandled"; + +/// A value that indicates whether the matched route is a fallback route. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `true` +pub const ASPNETCORE_ROUTING_IS_FALLBACK: &str = "aspnetcore.routing.is_fallback"; + +/// Match result - success or failure +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"success"` +/// - `"failure"` +pub const ASPNETCORE_ROUTING_MATCH_STATUS: &str = "aspnetcore.routing.match_status"; + +/// A flag indicating whether the sign in is persistent. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_SIGN_IN_IS_PERSISTENT: &str = "aspnetcore.sign_in.is_persistent"; + +/// A value that indicates whether the user is authenticated. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `true` +pub const ASPNETCORE_USER_IS_AUTHENTICATED: &str = "aspnetcore.user.is_authenticated"; + +/// The unique identifier of the AWS Bedrock Guardrail. A [guardrail](https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html) helps safeguard and prevent unwanted behavior from model responses or user messages. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"sgi5gkybzqak"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_BEDROCK_GUARDRAIL_ID: &str = "aws.bedrock.guardrail.id"; + +/// The unique identifier of the AWS Bedrock Knowledge base. A [knowledge base](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html) is a bank of information that can be queried by models to generate more relevant responses and augment prompts. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"XFWUPB9PAW"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_BEDROCK_KNOWLEDGE_BASE_ID: &str = "aws.bedrock.knowledge_base.id"; + +/// The JSON-serialized value of each item in the `AttributeDefinitions` request field. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "{ \"AttributeName\": \"string\", \"AttributeType\": \"string\" }", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS: &str = "aws.dynamodb.attribute_definitions"; + +/// The value of the `AttributesToGet` request parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "lives", +/// "id", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_ATTRIBUTES_TO_GET: &str = "aws.dynamodb.attributes_to_get"; + +/// The value of the `ConsistentRead` request parameter. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_CONSISTENT_READ: &str = "aws.dynamodb.consistent_read"; + +/// The JSON-serialized value of each item in the `ConsumedCapacity` response field. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "{ \"CapacityUnits\": number, \"GlobalSecondaryIndexes\": { \"string\" : { \"CapacityUnits\": number, \"ReadCapacityUnits\": number, \"WriteCapacityUnits\": number } }, \"LocalSecondaryIndexes\": { \"string\" : { \"CapacityUnits\": number, \"ReadCapacityUnits\": number, \"WriteCapacityUnits\": number } }, \"ReadCapacityUnits\": number, \"Table\": { \"CapacityUnits\": number, \"ReadCapacityUnits\": number, \"WriteCapacityUnits\": number }, \"TableName\": \"string\", \"WriteCapacityUnits\": number }", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_CONSUMED_CAPACITY: &str = "aws.dynamodb.consumed_capacity"; + +/// The value of the `Count` response parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `10` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_COUNT: &str = "aws.dynamodb.count"; + +/// The value of the `ExclusiveStartTableName` request parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Users"` +/// - `"CatsTable"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_EXCLUSIVE_START_TABLE: &str = "aws.dynamodb.exclusive_start_table"; + +/// The JSON-serialized value of each item in the `GlobalSecondaryIndexUpdates` request field. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "{ \"Create\": { \"IndexName\": \"string\", \"KeySchema\": [ { \"AttributeName\": \"string\", \"KeyType\": \"string\" } ], \"Projection\": { \"NonKeyAttributes\": [ \"string\" ], \"ProjectionType\": \"string\" }, \"ProvisionedThroughput\": { \"ReadCapacityUnits\": number, \"WriteCapacityUnits\": number } }", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES: &str = + "aws.dynamodb.global_secondary_index_updates"; + +/// The JSON-serialized value of each item of the `GlobalSecondaryIndexes` request field +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "{ \"IndexName\": \"string\", \"KeySchema\": [ { \"AttributeName\": \"string\", \"KeyType\": \"string\" } ], \"Projection\": { \"NonKeyAttributes\": [ \"string\" ], \"ProjectionType\": \"string\" }, \"ProvisionedThroughput\": { \"ReadCapacityUnits\": number, \"WriteCapacityUnits\": number } }", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES: &str = "aws.dynamodb.global_secondary_indexes"; + +/// The value of the `IndexName` request parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"name_to_group"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_INDEX_NAME: &str = "aws.dynamodb.index_name"; + +/// The JSON-serialized value of the `ItemCollectionMetrics` response field. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"{ \"string\" : [ { \"ItemCollectionKey\": { \"string\" : { \"B\": blob, \"BOOL\": boolean, \"BS\": [ blob ], \"L\": [ \"AttributeValue\" ], \"M\": { \"string\" : \"AttributeValue\" }, \"N\": \"string\", \"NS\": [ \"string\" ], \"NULL\": boolean, \"S\": \"string\", \"SS\": [ \"string\" ] } }, \"SizeEstimateRangeGB\": [ number ] } ] }"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_ITEM_COLLECTION_METRICS: &str = "aws.dynamodb.item_collection_metrics"; + +/// The value of the `Limit` request parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `10` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_LIMIT: &str = "aws.dynamodb.limit"; + +/// The JSON-serialized value of each item of the `LocalSecondaryIndexes` request field. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "{ \"IndexArn\": \"string\", \"IndexName\": \"string\", \"IndexSizeBytes\": number, \"ItemCount\": number, \"KeySchema\": [ { \"AttributeName\": \"string\", \"KeyType\": \"string\" } ], \"Projection\": { \"NonKeyAttributes\": [ \"string\" ], \"ProjectionType\": \"string\" } }", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES: &str = "aws.dynamodb.local_secondary_indexes"; + +/// The value of the `ProjectionExpression` request parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Title"` +/// - `"Title, Price, Color"` +/// - `"Title, Description, RelatedItems, ProductReviews"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_PROJECTION: &str = "aws.dynamodb.projection"; + +/// The value of the `ProvisionedThroughput.ReadCapacityUnits` request parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1.0` +/// - `2.0` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_PROVISIONED_READ_CAPACITY: &str = "aws.dynamodb.provisioned_read_capacity"; + +/// The value of the `ProvisionedThroughput.WriteCapacityUnits` request parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1.0` +/// - `2.0` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY: &str = "aws.dynamodb.provisioned_write_capacity"; + +/// The value of the `ScanIndexForward` request parameter. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_SCAN_FORWARD: &str = "aws.dynamodb.scan_forward"; + +/// The value of the `ScannedCount` response parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `50` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_SCANNED_COUNT: &str = "aws.dynamodb.scanned_count"; + +/// The value of the `Segment` request parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `10` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_SEGMENT: &str = "aws.dynamodb.segment"; + +/// The value of the `Select` request parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"ALL_ATTRIBUTES"` +/// - `"COUNT"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_SELECT: &str = "aws.dynamodb.select"; + +/// The number of items in the `TableNames` response parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `20` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_TABLE_COUNT: &str = "aws.dynamodb.table_count"; + +/// The keys in the `RequestItems` object field. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "Users", +/// "Cats", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_TABLE_NAMES: &str = "aws.dynamodb.table_names"; + +/// The value of the `TotalSegments` request parameter. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `100` +#[cfg(feature = "semconv_experimental")] +pub const AWS_DYNAMODB_TOTAL_SEGMENTS: &str = "aws.dynamodb.total_segments"; + +/// The ARN of an [ECS cluster](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/clusters.html). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"arn:aws:ecs:us-west-2:123456789123:cluster/my-cluster"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_ECS_CLUSTER_ARN: &str = "aws.ecs.cluster.arn"; + +/// The Amazon Resource Name (ARN) of an [ECS container instance](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_instances.html). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"arn:aws:ecs:us-west-1:123456789123:container/32624152-9086-4f0e-acae-1a75b14fe4d9"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_ECS_CONTAINER_ARN: &str = "aws.ecs.container.arn"; + +/// The [launch type](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html) for an ECS task. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const AWS_ECS_LAUNCHTYPE: &str = "aws.ecs.launchtype"; + +/// The ARN of a running [ECS task](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"arn:aws:ecs:us-west-1:123456789123:task/10838bed-421f-43ef-870a-f43feacbbb5b"` +/// - `"arn:aws:ecs:us-west-1:123456789123:task/my-cluster/task-id/23ebb8ac-c18f-46c6-8bbe-d55d0e37cfbd"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_ECS_TASK_ARN: &str = "aws.ecs.task.arn"; + +/// The family name of the [ECS task definition](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definitions.html) used to create the ECS task. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry-family"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_ECS_TASK_FAMILY: &str = "aws.ecs.task.family"; + +/// The ID of a running ECS task. The ID MUST be extracted from `task.arn`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"10838bed-421f-43ef-870a-f43feacbbb5b"` +/// - `"23ebb8ac-c18f-46c6-8bbe-d55d0e37cfbd"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_ECS_TASK_ID: &str = "aws.ecs.task.id"; + +/// The revision for the task definition used to create the ECS task. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"8"` +/// - `"26"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_ECS_TASK_REVISION: &str = "aws.ecs.task.revision"; + +/// The ARN of an EKS cluster. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"arn:aws:ecs:us-west-2:123456789123:cluster/my-cluster"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_EKS_CLUSTER_ARN: &str = "aws.eks.cluster.arn"; + +/// The AWS extended request ID as returned in the response header `x-amz-id-2`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"wzHcyEWfmOGDIE5QOhTAqFDoDWP3y8IUvpNINCwL9N4TEHbUw0/gZJ+VZTmCNCWR7fezEN3eCiQ="` +#[cfg(feature = "semconv_experimental")] +pub const AWS_EXTENDED_REQUEST_ID: &str = "aws.extended_request_id"; + +/// The name of the AWS Kinesis [stream](https://docs.aws.amazon.com/streams/latest/dev/introduction.html) the request refers to. Corresponds to the `--stream-name` parameter of the Kinesis [describe-stream](https://docs.aws.amazon.com/cli/latest/reference/kinesis/describe-stream.html) operation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"some-stream-name"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_KINESIS_STREAM_NAME: &str = "aws.kinesis.stream_name"; + +/// The full invoked ARN as provided on the `Context` passed to the function (`Lambda-Runtime-Invoked-Function-Arn` header on the `/runtime/invocation/next` applicable). +/// +/// ## Notes +/// +/// This may be different from `cloud.resource_id` if an alias is involved. +/// +/// # Examples +/// +/// - `"arn:aws:lambda:us-east-1:123456:function:myfunction:myalias"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_LAMBDA_INVOKED_ARN: &str = "aws.lambda.invoked_arn"; + +/// The UUID of the [AWS Lambda EvenSource Mapping](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-eventsourcemapping.html). An event source is mapped to a lambda function. It's contents are read by Lambda and used to trigger a function. This isn't available in the lambda execution context or the lambda runtime environment. This is going to be populated by the AWS SDK for each language when that UUID is present. Some of these operations are Create/Delete/Get/List/Update EventSourceMapping. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"587ad24b-03b9-4413-8202-bbd56b36e5b7"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_LAMBDA_RESOURCE_MAPPING_ID: &str = "aws.lambda.resource_mapping.id"; + +/// The Amazon Resource Name(s) (ARN) of the AWS log group(s). +/// +/// ## Notes +/// +/// See the [log group ARN format documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). +/// +/// # Examples +/// +/// - `[ +/// "arn:aws:logs:us-west-1:123456789012:log-group:/aws/my/group:*", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AWS_LOG_GROUP_ARNS: &str = "aws.log.group.arns"; + +/// The name(s) of the AWS log group(s) an application is writing to. +/// +/// ## Notes +/// +/// Multiple log groups must be supported for cases like multi-container applications, where a single application has sidecar containers, and each write to their own log group. +/// +/// # Examples +/// +/// - `[ +/// "/aws/lambda/my-function", +/// "opentelemetry-service", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AWS_LOG_GROUP_NAMES: &str = "aws.log.group.names"; + +/// The ARN(s) of the AWS log stream(s). +/// +/// ## Notes +/// +/// See the [log stream ARN format documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-access-control-overview-cwl.html#CWL_ARN_Format). One log group can contain several log streams, so these ARNs necessarily identify both a log group and a log stream. +/// +/// # Examples +/// +/// - `[ +/// "arn:aws:logs:us-west-1:123456789012:log-group:/aws/my/group:log-stream:logs/main/10838bed-421f-43ef-870a-f43feacbbb5b", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AWS_LOG_STREAM_ARNS: &str = "aws.log.stream.arns"; + +/// The name(s) of the AWS log stream(s) an application is writing to. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "logs/main/10838bed-421f-43ef-870a-f43feacbbb5b", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AWS_LOG_STREAM_NAMES: &str = "aws.log.stream.names"; + +/// The AWS request ID as returned in the response headers `x-amzn-requestid`, `x-amzn-request-id` or `x-amz-request-id`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"79b9da39-b7ae-508a-a6bc-864b2829c622"` +/// - `"C9ER4AJX75574TDJ"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_REQUEST_ID: &str = "aws.request_id"; + +/// The S3 bucket name the request refers to. Corresponds to the `--bucket` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. +/// +/// ## Notes +/// +/// The `bucket` attribute is applicable to all S3 operations that reference a bucket, i.e. that require the bucket name as a mandatory parameter. +/// This applies to almost all S3 operations except `list-buckets`. +/// +/// # Examples +/// +/// - `"some-bucket-name"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_S3_BUCKET: &str = "aws.s3.bucket"; + +/// The source object (in the form `bucket`/`key`) for the copy operation. +/// +/// ## Notes +/// +/// The `copy_source` attribute applies to S3 copy operations and corresponds to the `--copy-source` parameter +/// of the [copy-object operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html). +/// This applies in particular to the following operations: +/// +/// - [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) +/// - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html) +/// +/// # Examples +/// +/// - `"someFile.yml"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_S3_COPY_SOURCE: &str = "aws.s3.copy_source"; + +/// The delete request container that specifies the objects to be deleted. +/// +/// ## Notes +/// +/// The `delete` attribute is only applicable to the [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) operation. +/// The `delete` attribute corresponds to the `--delete` parameter of the +/// [delete-objects operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-objects.html). +/// +/// # Examples +/// +/// - `"Objects=[{Key=string,VersionId=string},{Key=string,VersionId=string}],Quiet=boolean"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_S3_DELETE: &str = "aws.s3.delete"; + +/// The S3 object key the request refers to. Corresponds to the `--key` parameter of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) operations. +/// +/// ## Notes +/// +/// The `key` attribute is applicable to all object-related S3 operations, i.e. that require the object key as a mandatory parameter. +/// This applies in particular to the following operations: +/// +/// - [copy-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/copy-object.html) +/// - [delete-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html) +/// - [get-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/get-object.html) +/// - [head-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/head-object.html) +/// - [put-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-object.html) +/// - [restore-object](https://docs.aws.amazon.com/cli/latest/reference/s3api/restore-object.html) +/// - [select-object-content](https://docs.aws.amazon.com/cli/latest/reference/s3api/select-object-content.html) +/// - [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) +/// - [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) +/// - [create-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/create-multipart-upload.html) +/// - [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) +/// - [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) +/// - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html) +/// +/// # Examples +/// +/// - `"someFile.yml"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_S3_KEY: &str = "aws.s3.key"; + +/// The part number of the part being uploaded in a multipart-upload operation. This is a positive integer between 1 and 10,000. +/// +/// ## Notes +/// +/// The `part_number` attribute is only applicable to the [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) +/// and [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html) operations. +/// The `part_number` attribute corresponds to the `--part-number` parameter of the +/// [upload-part operation within the S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html). +/// +/// # Examples +/// +/// - `3456` +#[cfg(feature = "semconv_experimental")] +pub const AWS_S3_PART_NUMBER: &str = "aws.s3.part_number"; + +/// Upload ID that identifies the multipart upload. +/// +/// ## Notes +/// +/// The `upload_id` attribute applies to S3 multipart-upload operations and corresponds to the `--upload-id` parameter +/// of the [S3 API](https://docs.aws.amazon.com/cli/latest/reference/s3api/index.html) multipart operations. +/// This applies in particular to the following operations: +/// +/// - [abort-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/abort-multipart-upload.html) +/// - [complete-multipart-upload](https://docs.aws.amazon.com/cli/latest/reference/s3api/complete-multipart-upload.html) +/// - [list-parts](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-parts.html) +/// - [upload-part](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part.html) +/// - [upload-part-copy](https://docs.aws.amazon.com/cli/latest/reference/s3api/upload-part-copy.html) +/// +/// # Examples +/// +/// - `"dfRtDYWFbkRONycy.Yxwh66Yjlx.cph0gtNBtJ"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_S3_UPLOAD_ID: &str = "aws.s3.upload_id"; + +/// The ARN of the Secret stored in the Secrets Manager +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"arn:aws:secretsmanager:us-east-1:123456789012:secret:SecretName-6RandomCharacters"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_SECRETSMANAGER_SECRET_ARN: &str = "aws.secretsmanager.secret.arn"; + +/// The ARN of the AWS SNS Topic. An Amazon SNS [topic](https://docs.aws.amazon.com/sns/latest/dg/sns-create-topic.html) is a logical access point that acts as a communication channel. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"arn:aws:sns:us-east-1:123456789012:mystack-mytopic-NZJ5JSMVGFIE"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_SNS_TOPIC_ARN: &str = "aws.sns.topic.arn"; + +/// The URL of the AWS SQS Queue. It's a unique identifier for a queue in Amazon Simple Queue Service (SQS) and is used to access the queue and perform actions on it. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"https://sqs.us-east-1.amazonaws.com/123456789012/MyQueue"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_SQS_QUEUE_URL: &str = "aws.sqs.queue.url"; + +/// The ARN of the AWS Step Functions Activity. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"arn:aws:states:us-east-1:123456789012:activity:get-greeting"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_STEP_FUNCTIONS_ACTIVITY_ARN: &str = "aws.step_functions.activity.arn"; + +/// The ARN of the AWS Step Functions State Machine. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"arn:aws:states:us-east-1:123456789012:stateMachine:myStateMachine:1"` +#[cfg(feature = "semconv_experimental")] +pub const AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN: &str = "aws.step_functions.state_machine.arn"; + +/// Deprecated, use `azure.resource_provider.namespace` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Microsoft.Storage"` +/// - `"Microsoft.KeyVault"` +/// - `"Microsoft.ServiceBus"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `azure.resource_provider.namespace`., reason: renamed, renamed_to: azure.resource_provider.namespace}" +)] +pub const AZ_NAMESPACE: &str = "az.namespace"; + +/// Deprecated, use `azure.service.request.id` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"00000000-0000-0000-0000-000000000000"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `azure.service.request.id`., reason: renamed, renamed_to: azure.service.request.id}" +)] +pub const AZ_SERVICE_REQUEST_ID: &str = "az.service_request_id"; + +/// The unique identifier of the client instance. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"3ba4827d-4422-483f-b59f-85b74211c11d"` +/// - `"storage-client-1"` +#[cfg(feature = "semconv_experimental")] +pub const AZURE_CLIENT_ID: &str = "azure.client.id"; + +/// Cosmos client connection mode. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const AZURE_COSMOSDB_CONNECTION_MODE: &str = "azure.cosmosdb.connection.mode"; + +/// Account or request [consistency level](https://learn.microsoft.com/azure/cosmos-db/consistency-levels). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Eventual"` +/// - `"ConsistentPrefix"` +/// - `"BoundedStaleness"` +/// - `"Strong"` +/// - `"Session"` +#[cfg(feature = "semconv_experimental")] +pub const AZURE_COSMOSDB_CONSISTENCY_LEVEL: &str = "azure.cosmosdb.consistency.level"; + +/// List of regions contacted during operation in the order that they were contacted. If there is more than one region listed, it indicates that the operation was performed on multiple regions i.e. cross-regional call. +/// +/// ## Notes +/// +/// Region name matches the format of `displayName` in [Azure Location API](https://learn.microsoft.com/rest/api/resources/subscriptions/list-locations) +/// +/// # Examples +/// +/// - `[ +/// "North Central US", +/// "Australia East", +/// "Australia Southeast", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const AZURE_COSMOSDB_OPERATION_CONTACTED_REGIONS: &str = + "azure.cosmosdb.operation.contacted_regions"; + +/// The number of request units consumed by the operation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `46.18` +/// - `1.0` +#[cfg(feature = "semconv_experimental")] +pub const AZURE_COSMOSDB_OPERATION_REQUEST_CHARGE: &str = "azure.cosmosdb.operation.request_charge"; + +/// Request payload size in bytes. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const AZURE_COSMOSDB_REQUEST_BODY_SIZE: &str = "azure.cosmosdb.request.body.size"; + +/// Cosmos DB sub status code. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1000` +/// - `1002` +#[cfg(feature = "semconv_experimental")] +pub const AZURE_COSMOSDB_RESPONSE_SUB_STATUS_CODE: &str = "azure.cosmosdb.response.sub_status_code"; + +/// [Azure Resource Provider Namespace](https://learn.microsoft.com/azure/azure-resource-manager/management/azure-services-resource-providers) as recognized by the client. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Microsoft.Storage"` +/// - `"Microsoft.KeyVault"` +/// - `"Microsoft.ServiceBus"` +#[cfg(feature = "semconv_experimental")] +pub const AZURE_RESOURCE_PROVIDER_NAMESPACE: &str = "azure.resource_provider.namespace"; + +/// The unique identifier of the service request. It's generated by the Azure service and returned with the response. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"00000000-0000-0000-0000-000000000000"` +#[cfg(feature = "semconv_experimental")] +pub const AZURE_SERVICE_REQUEST_ID: &str = "azure.service.request.id"; + +/// Array of brand name and version separated by a space +/// +/// ## Notes +/// +/// This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.brands`). +/// +/// # Examples +/// +/// - `[ +/// " Not A;Brand 99", +/// "Chromium 99", +/// "Chrome 99", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const BROWSER_BRANDS: &str = "browser.brands"; + +/// Absolute URL of the current browser document according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"https://www.example.com/search?q=OpenTelemetry#SemConv"` +#[cfg(feature = "semconv_experimental")] +pub const BROWSER_DOCUMENT_URL_FULL: &str = "browser.document.url.full"; + +/// Preferred language of the user using the browser +/// +/// ## Notes +/// +/// This value is intended to be taken from the Navigator API `navigator.language`. +/// +/// # Examples +/// +/// - `"en"` +/// - `"en-US"` +/// - `"fr"` +/// - `"fr-FR"` +#[cfg(feature = "semconv_experimental")] +pub const BROWSER_LANGUAGE: &str = "browser.language"; + +/// A boolean that is true if the browser is running on a mobile device +/// +/// ## Notes +/// +/// This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.mobile`). If unavailable, this attribute SHOULD be left unset +#[cfg(feature = "semconv_experimental")] +pub const BROWSER_MOBILE: &str = "browser.mobile"; + +/// The platform on which the browser is running +/// +/// ## Notes +/// +/// This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.platform`). If unavailable, the legacy `navigator.platform` API SHOULD NOT be used instead and this attribute SHOULD be left unset in order for the values to be consistent. +/// The list of possible values is defined in the [W3C User-Agent Client Hints specification](https://wicg.github.io/ua-client-hints/#sec-ch-ua-platform). Note that some (but not all) of these values can overlap with values in the [`os.type` and `os.name` attributes](./os.md). However, for consistency, the values in the `browser.platform` attribute should capture the exact value that the user agent provides. +/// +/// # Examples +/// +/// - `"Windows"` +/// - `"macOS"` +/// - `"Android"` +#[cfg(feature = "semconv_experimental")] +pub const BROWSER_PLATFORM: &str = "browser.platform"; + +/// The consistency level of the query. Based on consistency values from [CQL](https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/dml/dmlConfigConsistency.html). +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const CASSANDRA_CONSISTENCY_LEVEL: &str = "cassandra.consistency.level"; + +/// The data center of the coordinating node for a query. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"us-west-2"` +#[cfg(feature = "semconv_experimental")] +pub const CASSANDRA_COORDINATOR_DC: &str = "cassandra.coordinator.dc"; + +/// The ID of the coordinating node for a query. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"be13faa2-8574-4d71-926d-27f16cf8a7af"` +#[cfg(feature = "semconv_experimental")] +pub const CASSANDRA_COORDINATOR_ID: &str = "cassandra.coordinator.id"; + +/// The fetch size used for paging, i.e. how many rows will be returned at once. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `5000` +#[cfg(feature = "semconv_experimental")] +pub const CASSANDRA_PAGE_SIZE: &str = "cassandra.page.size"; + +/// Whether or not the query is idempotent. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const CASSANDRA_QUERY_IDEMPOTENT: &str = "cassandra.query.idempotent"; + +/// The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `0` +/// - `2` +#[cfg(feature = "semconv_experimental")] +pub const CASSANDRA_SPECULATIVE_EXECUTION_COUNT: &str = "cassandra.speculative_execution.count"; + +/// The kind of action a pipeline run is performing. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"BUILD"` +/// - `"RUN"` +/// - `"SYNC"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_ACTION_NAME: &str = "cicd.pipeline.action.name"; + +/// The human readable name of the pipeline within a CI/CD system. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Build and Test"` +/// - `"Lint"` +/// - `"Deploy Go Project"` +/// - `"deploy_to_environment"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_NAME: &str = "cicd.pipeline.name"; + +/// The result of a pipeline run. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"success"` +/// - `"failure"` +/// - `"timeout"` +/// - `"skipped"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_RESULT: &str = "cicd.pipeline.result"; + +/// The unique identifier of a pipeline run within a CI/CD system. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"120912"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_RUN_ID: &str = "cicd.pipeline.run.id"; + +/// The pipeline run goes through these states during its lifecycle. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"pending"` +/// - `"executing"` +/// - `"finalizing"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_RUN_STATE: &str = "cicd.pipeline.run.state"; + +/// The [URL](https://wikipedia.org/wiki/URL) of the pipeline run, providing the complete address in order to locate and identify the pipeline run. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"https://github.com/open-telemetry/semantic-conventions/actions/runs/9753949763?pr=1075"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_RUN_URL_FULL: &str = "cicd.pipeline.run.url.full"; + +/// The human readable name of a task within a pipeline. Task here most closely aligns with a [computing process](https://wikipedia.org/wiki/Pipeline_(computing)) in a pipeline. Other terms for tasks include commands, steps, and procedures. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Run GoLang Linter"` +/// - `"Go Build"` +/// - `"go-test"` +/// - `"deploy_binary"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_TASK_NAME: &str = "cicd.pipeline.task.name"; + +/// The unique identifier of a task run within a pipeline. +/// +/// ## Notes +/// +/// For a given pipeline run and task, the `cicd.pipeline.task.run.id` MUST be unique within that run. For the same task across different runs of the same pipeline, the `cicd.pipeline.task.run.id` MAY remain the same, enabling correlation of `cicd.pipeline.task.run.result` values across multiple pipeline runs. +/// +/// # Examples +/// +/// - `"12097"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_TASK_RUN_ID: &str = "cicd.pipeline.task.run.id"; + +/// The result of a task run. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"success"` +/// - `"failure"` +/// - `"timeout"` +/// - `"skipped"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_TASK_RUN_RESULT: &str = "cicd.pipeline.task.run.result"; + +/// The [URL](https://wikipedia.org/wiki/URL) of the pipeline task run, providing the complete address in order to locate and identify the pipeline task run. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"https://github.com/open-telemetry/semantic-conventions/actions/runs/9753949763/job/26920038674?pr=1075"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_TASK_RUN_URL_FULL: &str = "cicd.pipeline.task.run.url.full"; + +/// The type of the task within a pipeline. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"build"` +/// - `"test"` +/// - `"deploy"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_TASK_TYPE: &str = "cicd.pipeline.task.type"; + +/// The name of a component of the CI/CD system. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"controller"` +/// - `"scheduler"` +/// - `"agent"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_SYSTEM_COMPONENT: &str = "cicd.system.component"; + +/// The unique identifier of a worker within a CI/CD system. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"abc123"` +/// - `"10.0.1.2"` +/// - `"controller"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_WORKER_ID: &str = "cicd.worker.id"; + +/// The name of a worker within a CI/CD system. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"agent-abc"` +/// - `"controller"` +/// - `"Ubuntu LTS"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_WORKER_NAME: &str = "cicd.worker.name"; + +/// The state of a CI/CD worker / agent. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"idle"` +/// - `"busy"` +/// - `"down"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_WORKER_STATE: &str = "cicd.worker.state"; + +/// The [URL](https://wikipedia.org/wiki/URL) of the worker, providing the complete address in order to locate and identify the worker. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"https://cicd.example.org/worker/abc123"` +#[cfg(feature = "semconv_experimental")] +pub const CICD_WORKER_URL_FULL: &str = "cicd.worker.url.full"; + +/// Client address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. +/// +/// ## Notes +/// +/// When observed from the server side, and when communicating through an intermediary, `client.address` SHOULD represent the client address behind any intermediaries, for example proxies, if it's available. +/// +/// # Examples +/// +/// - `"client.example.com"` +/// - `"10.1.2.80"` +/// - `"/tmp/my.sock"` +pub const CLIENT_ADDRESS: &str = "client.address"; + +/// Client port number. +/// +/// ## Notes +/// +/// When observed from the server side, and when communicating through an intermediary, `client.port` SHOULD represent the client port behind any intermediaries, for example proxies, if it's available. +/// +/// # Examples +/// +/// - `65123` +pub const CLIENT_PORT: &str = "client.port"; + +/// The cloud account ID the resource is assigned to. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"111111111111"` +/// - `"opentelemetry"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUD_ACCOUNT_ID: &str = "cloud.account.id"; + +/// Cloud regions often have multiple, isolated locations known as zones to increase availability. Availability zone represents the zone where the resource is running. +/// +/// ## Notes +/// +/// Availability zones are called "zones" on Alibaba Cloud and Google Cloud. +/// +/// # Examples +/// +/// - `"us-east-1c"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUD_AVAILABILITY_ZONE: &str = "cloud.availability_zone"; + +/// The cloud platform in use. +/// +/// ## Notes +/// +/// The prefix of the service SHOULD match the one specified in `cloud.provider` +#[cfg(feature = "semconv_experimental")] +pub const CLOUD_PLATFORM: &str = "cloud.platform"; + +/// Name of the cloud provider. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const CLOUD_PROVIDER: &str = "cloud.provider"; + +/// The geographical region within a cloud provider. When associated with a resource, this attribute specifies the region where the resource operates. When calling services or APIs deployed on a cloud, this attribute identifies the region where the called destination is deployed. +/// +/// ## Notes +/// +/// Refer to your provider's docs to see the available regions, for example [Alibaba Cloud regions](https://www.alibabacloud.com/help/doc-detail/40654.htm), [AWS regions](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/), [Azure regions](https://azure.microsoft.com/global-infrastructure/geographies/), [Google Cloud regions](https://cloud.google.com/about/locations), or [Tencent Cloud regions](https://www.tencentcloud.com/document/product/213/6091). +/// +/// # Examples +/// +/// - `"us-central1"` +/// - `"us-east-1"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUD_REGION: &str = "cloud.region"; + +/// Cloud provider-specific native identifier of the monitored cloud resource (e.g. an [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html) on AWS, a [fully qualified resource ID](https://learn.microsoft.com/rest/api/resources/resources/get-by-id) on Azure, a [full resource name](https://google.aip.dev/122#full-resource-names) on GCP) +/// +/// ## Notes +/// +/// On some cloud providers, it may not be possible to determine the full ID at startup, +/// so it may be necessary to set `cloud.resource_id` as a span attribute instead. +/// +/// The exact value to use for `cloud.resource_id` depends on the cloud provider. +/// The following well-known definitions MUST be used if you set this attribute and they apply: +/// +/// - **AWS Lambda:** The function [ARN](https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html). +/// Take care not to use the "invoked ARN" directly but replace any +/// [alias suffix](https://docs.aws.amazon.com/lambda/latest/dg/configuration-aliases.html) +/// with the resolved function version, as the same runtime instance may be invocable with +/// multiple different aliases. +/// - **GCP:** The [URI of the resource](https://cloud.google.com/iam/docs/full-resource-names) +/// - **Azure:** The [Fully Qualified Resource ID](https://learn.microsoft.com/rest/api/resources/resources/get-by-id) of the invoked function, +/// *not* the function app, having the form +/// `/subscriptions/[SUBSCRIPTION_GUID]/resourceGroups/[RG]/providers/Microsoft.Web/sites/[FUNCAPP]/functions/[FUNC]`. +/// This means that a span attribute MUST be used, as an Azure function app can host multiple functions that would usually share +/// a TracerProvider. +/// +/// # Examples +/// +/// - `"arn:aws:lambda:REGION:ACCOUNT_ID:function:my-function"` +/// - `"//run.googleapis.com/projects/PROJECT_ID/locations/LOCATION_ID/services/SERVICE_ID"` +/// - `"/subscriptions//resourceGroups//providers/Microsoft.Web/sites//functions/"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUD_RESOURCE_ID: &str = "cloud.resource_id"; + +/// The [event_id](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#id) uniquely identifies the event. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"123e4567-e89b-12d3-a456-426614174000"` +/// - `"0001"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDEVENTS_EVENT_ID: &str = "cloudevents.event_id"; + +/// The [source](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#source-1) identifies the context in which an event happened. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"https://github.com/cloudevents"` +/// - `"/cloudevents/spec/pull/123"` +/// - `"my-service"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDEVENTS_EVENT_SOURCE: &str = "cloudevents.event_source"; + +/// The [version of the CloudEvents specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#specversion) which the event uses. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1.0"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDEVENTS_EVENT_SPEC_VERSION: &str = "cloudevents.event_spec_version"; + +/// The [subject](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#subject) of the event in the context of the event producer (identified by source). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"mynewfile.jpg"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDEVENTS_EVENT_SUBJECT: &str = "cloudevents.event_subject"; + +/// The [event_type](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#type) contains a value describing the type of event related to the originating occurrence. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"com.github.pull_request.opened"` +/// - `"com.example.object.deleted.v2"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDEVENTS_EVENT_TYPE: &str = "cloudevents.event_type"; + +/// The guid of the application. +/// +/// ## Notes +/// +/// Application instrumentation should use the value from environment +/// variable `VCAP_APPLICATION.application_id`. This is the same value as +/// reported by `cf app [app-name] --guid`. +/// +/// # Examples +/// +/// - `"218fc5a9-a5f1-4b54-aa05-46717d0ab26d"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDFOUNDRY_APP_ID: &str = "cloudfoundry.app.id"; + +/// The index of the application instance. 0 when just one instance is active. +/// +/// ## Notes +/// +/// CloudFoundry defines the `instance_id` in the [Loggregator v2 envelope](https://github.com/cloudfoundry/loggregator-api#v2-envelope). +/// It is used for logs and metrics emitted by CloudFoundry. It is +/// supposed to contain the application instance index for applications +/// deployed on the runtime. +/// +/// Application instrumentation should use the value from environment +/// variable `CF_INSTANCE_INDEX`. +/// +/// # Examples +/// +/// - `"0"` +/// - `"1"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDFOUNDRY_APP_INSTANCE_ID: &str = "cloudfoundry.app.instance.id"; + +/// The name of the application. +/// +/// ## Notes +/// +/// Application instrumentation should use the value from environment +/// variable `VCAP_APPLICATION.application_name`. This is the same value +/// as reported by `cf apps`. +/// +/// # Examples +/// +/// - `"my-app-name"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDFOUNDRY_APP_NAME: &str = "cloudfoundry.app.name"; + +/// The guid of the CloudFoundry org the application is running in. +/// +/// ## Notes +/// +/// Application instrumentation should use the value from environment +/// variable `VCAP_APPLICATION.org_id`. This is the same value as +/// reported by `cf org [org-name] --guid`. +/// +/// # Examples +/// +/// - `"218fc5a9-a5f1-4b54-aa05-46717d0ab26d"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDFOUNDRY_ORG_ID: &str = "cloudfoundry.org.id"; + +/// The name of the CloudFoundry organization the app is running in. +/// +/// ## Notes +/// +/// Application instrumentation should use the value from environment +/// variable `VCAP_APPLICATION.org_name`. This is the same value as +/// reported by `cf orgs`. +/// +/// # Examples +/// +/// - `"my-org-name"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDFOUNDRY_ORG_NAME: &str = "cloudfoundry.org.name"; + +/// The UID identifying the process. +/// +/// ## Notes +/// +/// Application instrumentation should use the value from environment +/// variable `VCAP_APPLICATION.process_id`. It is supposed to be equal to +/// `VCAP_APPLICATION.app_id` for applications deployed to the runtime. +/// For system components, this could be the actual PID. +/// +/// # Examples +/// +/// - `"218fc5a9-a5f1-4b54-aa05-46717d0ab26d"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDFOUNDRY_PROCESS_ID: &str = "cloudfoundry.process.id"; + +/// The type of process. +/// +/// ## Notes +/// +/// CloudFoundry applications can consist of multiple jobs. Usually the +/// main process will be of type `web`. There can be additional background +/// tasks or side-cars with different process types. +/// +/// # Examples +/// +/// - `"web"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDFOUNDRY_PROCESS_TYPE: &str = "cloudfoundry.process.type"; + +/// The guid of the CloudFoundry space the application is running in. +/// +/// ## Notes +/// +/// Application instrumentation should use the value from environment +/// variable `VCAP_APPLICATION.space_id`. This is the same value as +/// reported by `cf space [space-name] --guid`. +/// +/// # Examples +/// +/// - `"218fc5a9-a5f1-4b54-aa05-46717d0ab26d"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDFOUNDRY_SPACE_ID: &str = "cloudfoundry.space.id"; + +/// The name of the CloudFoundry space the application is running in. +/// +/// ## Notes +/// +/// Application instrumentation should use the value from environment +/// variable `VCAP_APPLICATION.space_name`. This is the same value as +/// reported by `cf spaces`. +/// +/// # Examples +/// +/// - `"my-space-name"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDFOUNDRY_SPACE_NAME: &str = "cloudfoundry.space.name"; + +/// A guid or another name describing the event source. +/// +/// ## Notes +/// +/// CloudFoundry defines the `source_id` in the [Loggregator v2 envelope](https://github.com/cloudfoundry/loggregator-api#v2-envelope). +/// It is used for logs and metrics emitted by CloudFoundry. It is +/// supposed to contain the component name, e.g. "gorouter", for +/// CloudFoundry components. +/// +/// When system components are instrumented, values from the +/// [Bosh spec](https://bosh.io/docs/jobs/#properties-spec) +/// should be used. The `system.id` should be set to +/// `spec.deployment/spec.name`. +/// +/// # Examples +/// +/// - `"cf/gorouter"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDFOUNDRY_SYSTEM_ID: &str = "cloudfoundry.system.id"; + +/// A guid describing the concrete instance of the event source. +/// +/// ## Notes +/// +/// CloudFoundry defines the `instance_id` in the [Loggregator v2 envelope](https://github.com/cloudfoundry/loggregator-api#v2-envelope). +/// It is used for logs and metrics emitted by CloudFoundry. It is +/// supposed to contain the vm id for CloudFoundry components. +/// +/// When system components are instrumented, values from the +/// [Bosh spec](https://bosh.io/docs/jobs/#properties-spec) +/// should be used. The `system.instance.id` should be set to `spec.id`. +/// +/// # Examples +/// +/// - `"218fc5a9-a5f1-4b54-aa05-46717d0ab26d"` +#[cfg(feature = "semconv_experimental")] +pub const CLOUDFOUNDRY_SYSTEM_INSTANCE_ID: &str = "cloudfoundry.system.instance.id"; + +/// Deprecated, use `code.column.number` +/// +/// ## Notes +/// +/// # Examples +/// +/// - `16` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `code.column.number`., reason: renamed, renamed_to: code.column.number}" +)] +pub const CODE_COLUMN: &str = "code.column"; + +/// The column number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Line'. This constraint is imposed to prevent redundancy and maintain data integrity. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `16` +pub const CODE_COLUMN_NUMBER: &str = "code.column.number"; + +/// The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/usr/local/MyApplication/content_root/app/index.php"` +pub const CODE_FILE_PATH: &str = "code.file.path"; + +/// Deprecated, use `code.file.path` instead +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/usr/local/MyApplication/content_root/app/index.php"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `code.file.path`., reason: renamed, renamed_to: code.file.path}" +)] +pub const CODE_FILEPATH: &str = "code.filepath"; + +/// Deprecated, use `code.function.name` instead +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"serveRequest"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Value should be included in `code.function.name` which is expected to be a fully-qualified name.\n, reason: uncategorized}" +)] +pub const CODE_FUNCTION: &str = "code.function"; + +/// The method or function fully-qualified name without arguments. The value should fit the natural representation of the language runtime, which is also likely the same used within `code.stacktrace` attribute value. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Function'. This constraint is imposed to prevent redundancy and maintain data integrity. +/// +/// ## Notes +/// +/// Values and format depends on each language runtime, thus it is impossible to provide an exhaustive list of examples. +/// The values are usually the same (or prefixes of) the ones found in native stack trace representation stored in +/// `code.stacktrace` without information on arguments. +/// +/// Examples: +/// +/// - Java method: `com.example.MyHttpService.serveRequest` +/// - Java anonymous class method: `com.mycompany.Main$1.myMethod` +/// - Java lambda method: `com.mycompany.Main$$Lambda/0x0000748ae4149c00.myMethod` +/// - PHP function: `GuzzleHttp\Client::transfer` +/// - Go function: `github.com/my/repo/pkg.foo.func5` +/// - Elixir: `OpenTelemetry.Ctx.new` +/// - Erlang: `opentelemetry_ctx:new` +/// - Rust: `playground::my_module::my_cool_func` +/// - C function: `fopen` +/// +/// # Examples +/// +/// - `"com.example.MyHttpService.serveRequest"` +/// - `"GuzzleHttp\\Client::transfer"` +/// - `"fopen"` +pub const CODE_FUNCTION_NAME: &str = "code.function.name"; + +/// The line number in `code.file.path` best representing the operation. It SHOULD point within the code unit named in `code.function.name`. This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Line'. This constraint is imposed to prevent redundancy and maintain data integrity. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `42` +pub const CODE_LINE_NUMBER: &str = "code.line.number"; + +/// Deprecated, use `code.line.number` instead +/// +/// ## Notes +/// +/// # Examples +/// +/// - `42` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `code.line.number`., reason: renamed, renamed_to: code.line.number}" +)] +pub const CODE_LINENO: &str = "code.lineno"; + +/// Deprecated, namespace is now included into `code.function.name` +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"com.example.MyHttpService"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Value should be included in `code.function.name` which is expected to be a fully-qualified name.\n, reason: uncategorized}" +)] +pub const CODE_NAMESPACE: &str = "code.namespace"; + +/// A stacktrace as a string in the natural representation for the language runtime. The representation is identical to [`exception.stacktrace`](/docs/exceptions/exceptions-spans.md#stacktrace-representation). This attribute MUST NOT be used on the Profile signal since the data is already captured in 'message Location'. This constraint is imposed to prevent redundancy and maintain data integrity. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"at com.example.GenerateTrace.methodB(GenerateTrace.java:13)\\n at com.example.GenerateTrace.methodA(GenerateTrace.java:9)\\n at com.example.GenerateTrace.main(GenerateTrace.java:5)\n"` +pub const CODE_STACKTRACE: &str = "code.stacktrace"; + +/// The command used to run the container (i.e. the command name). +/// +/// ## Notes +/// +/// If using embedded credentials or sensitive data, it is recommended to remove them to prevent potential leakage. +/// +/// # Examples +/// +/// - `"otelcontribcol"` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_COMMAND: &str = "container.command"; + +/// All the command arguments (including the command/executable itself) run by the container. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "otelcontribcol", +/// "--config", +/// "config.yaml", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_COMMAND_ARGS: &str = "container.command_args"; + +/// The full command run by the container as a single string representing the full command. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"otelcontribcol --config config.yaml"` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_COMMAND_LINE: &str = "container.command_line"; + +/// Deprecated, use `cpu.mode` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"user"` +/// - `"kernel"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Replaced by `cpu.mode`., reason: renamed, renamed_to: cpu.mode}")] +pub const CONTAINER_CPU_STATE: &str = "container.cpu.state"; + +/// The name of the CSI ([Container Storage Interface](https://github.com/container-storage-interface/spec)) plugin used by the volume. +/// +/// ## Notes +/// +/// This can sometimes be referred to as a "driver" in CSI implementations. This should represent the `name` field of the GetPluginInfo RPC. +/// +/// # Examples +/// +/// - `"pd.csi.storage.gke.io"` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_CSI_PLUGIN_NAME: &str = "container.csi.plugin.name"; + +/// The unique volume ID returned by the CSI ([Container Storage Interface](https://github.com/container-storage-interface/spec)) plugin. +/// +/// ## Notes +/// +/// This can sometimes be referred to as a "volume handle" in CSI implementations. This should represent the `Volume.volume_id` field in CSI spec. +/// +/// # Examples +/// +/// - `"projects/my-gcp-project/zones/my-gcp-zone/disks/my-gcp-disk"` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_CSI_VOLUME_ID: &str = "container.csi.volume.id"; + +/// Container ID. Usually a UUID, as for example used to [identify Docker containers](https://docs.docker.com/engine/containers/run/#container-identification). The UUID might be abbreviated. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"a3bf90e006b2"` +pub const CONTAINER_ID: &str = "container.id"; + +/// Runtime specific image identifier. Usually a hash algorithm followed by a UUID. +/// +/// ## Notes +/// +/// Docker defines a sha256 of the image id; `container.image.id` corresponds to the `Image` field from the Docker container inspect [API](https://docs.docker.com/reference/api/engine/version/v1.52/#tag/Container/operation/ContainerInspect) endpoint. +/// K8s defines a link to the container registry repository with digest `"imageID": "registry.azurecr.io /namespace/service/dockerfile@sha256:bdeabd40c3a8a492eaf9e8e44d0ebbb84bac7ee25ac0cf8a7159d25f62555625"`. +/// The ID is assigned by the container runtime and can vary in different environments. Consider using `oci.manifest.digest` if it is important to identify the same image in different environments/runtimes. +/// +/// # Examples +/// +/// - `"sha256:19c92d0a00d1b66d897bceaa7319bee0dd38a10a851c60bcec9474aa3f01e50f"` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_IMAGE_ID: &str = "container.image.id"; + +/// Name of the image the container was built on. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"gcr.io/opentelemetry/operator"` +pub const CONTAINER_IMAGE_NAME: &str = "container.image.name"; + +/// Repo digests of the container image as provided by the container runtime. +/// +/// ## Notes +/// +/// [Docker](https://docs.docker.com/reference/api/engine/version/v1.52/#tag/Image/operation/ImageInspect) and [CRI](https://github.com/kubernetes/cri-api/blob/c75ef5b473bbe2d0a4fc92f82235efd665ea8e9f/pkg/apis/runtime/v1/api.proto#L1237-L1238) report those under the `RepoDigests` field. +/// +/// # Examples +/// +/// - `[ +/// "example@sha256:afcc7f1ac1b49db317a7196c902e61c6c3c4607d63599ee1a82d702d249a0ccb", +/// "internal.registry.example.com:5000/example@sha256:b69959407d21e8a062e0416bf13405bb2b71ed7a84dde4158ebafacfa06f5578", +/// ]` +pub const CONTAINER_IMAGE_REPO_DIGESTS: &str = "container.image.repo_digests"; + +/// Container image tags. An example can be found in [Docker Image Inspect](https://docs.docker.com/reference/api/engine/version/v1.52/#tag/Image/operation/ImageInspect). Should be only the `` section of the full name for example from `registry.example.com/my-org/my-image:`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "v1.27.1", +/// "3.5.7-0", +/// ]` +pub const CONTAINER_IMAGE_TAGS: &str = "container.image.tags"; + +/// Container labels, ``key`` being the label name, the value being the label value. +/// +/// ## Notes +/// +/// For example, a docker container label `app` with value `nginx` SHOULD be recorded as the `container.label.app` attribute with value `"nginx"`. +/// +/// # Examples +/// +/// - `"nginx"` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_LABEL: &str = "container.label"; + +/// Deprecated, use `container.label` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"nginx"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `container.label`., reason: renamed, renamed_to: container.label}" +)] +pub const CONTAINER_LABELS: &str = "container.labels"; + +/// Container name used by container runtime. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry-autoconf"` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_NAME: &str = "container.name"; + +/// The container runtime managing this container. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"docker"` +/// - `"containerd"` +/// - `"rkt"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `container.runtime.name`., reason: renamed, renamed_to: container.runtime.name}" +)] +pub const CONTAINER_RUNTIME: &str = "container.runtime"; + +/// A description about the runtime which could include, for example details about the CRI/API version being used or other customizations. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"docker://19.3.1 - CRI: 1.22.0"` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_RUNTIME_DESCRIPTION: &str = "container.runtime.description"; + +/// The container runtime managing this container. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"docker"` +/// - `"containerd"` +/// - `"rkt"` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_RUNTIME_NAME: &str = "container.runtime.name"; + +/// The version of the runtime of this process, as returned by the runtime without modification. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1.0.0"` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_RUNTIME_VERSION: &str = "container.runtime.version"; + +/// The logical CPU number \[0..n-1\] +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1` +#[cfg(feature = "semconv_experimental")] +pub const CPU_LOGICAL_NUMBER: &str = "cpu.logical_number"; + +/// The mode of the CPU +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"user"` +/// - `"system"` +#[cfg(feature = "semconv_experimental")] +pub const CPU_MODE: &str = "cpu.mode"; + +/// Value of the garbage collector collection generation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `0` +/// - `1` +/// - `2` +#[cfg(feature = "semconv_experimental")] +pub const CPYTHON_GC_GENERATION: &str = "cpython.gc.generation"; + +/// Deprecated, use `cassandra.consistency.level` instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `cassandra.consistency.level`., reason: renamed, renamed_to: cassandra.consistency.level}" +)] +pub const DB_CASSANDRA_CONSISTENCY_LEVEL: &str = "db.cassandra.consistency_level"; + +/// Deprecated, use `cassandra.coordinator.dc` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"us-west-2"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `cassandra.coordinator.dc`., reason: renamed, renamed_to: cassandra.coordinator.dc}" +)] +pub const DB_CASSANDRA_COORDINATOR_DC: &str = "db.cassandra.coordinator.dc"; + +/// Deprecated, use `cassandra.coordinator.id` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"be13faa2-8574-4d71-926d-27f16cf8a7af"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `cassandra.coordinator.id`., reason: renamed, renamed_to: cassandra.coordinator.id}" +)] +pub const DB_CASSANDRA_COORDINATOR_ID: &str = "db.cassandra.coordinator.id"; + +/// Deprecated, use `cassandra.query.idempotent` instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `cassandra.query.idempotent`., reason: renamed, renamed_to: cassandra.query.idempotent}" +)] +pub const DB_CASSANDRA_IDEMPOTENCE: &str = "db.cassandra.idempotence"; + +/// Deprecated, use `cassandra.page.size` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `5000` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `cassandra.page.size`., reason: renamed, renamed_to: cassandra.page.size}" +)] +pub const DB_CASSANDRA_PAGE_SIZE: &str = "db.cassandra.page_size"; + +/// Deprecated, use `cassandra.speculative_execution.count` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `0` +/// - `2` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `cassandra.speculative_execution.count`., reason: renamed, renamed_to: cassandra.speculative_execution.count}" +)] +pub const DB_CASSANDRA_SPECULATIVE_EXECUTION_COUNT: &str = + "db.cassandra.speculative_execution_count"; + +/// Deprecated, use `db.collection.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"mytable"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.collection.name`., reason: renamed, renamed_to: db.collection.name}" +)] +pub const DB_CASSANDRA_TABLE: &str = "db.cassandra.table"; + +/// The name of the connection pool; unique within the instrumented application. In case the connection pool implementation doesn't provide a name, instrumentation SHOULD use a combination of parameters that would make the name unique, for example, combining attributes `server.address`, `server.port`, and `db.namespace`, formatted as `server.address:server.port/db.namespace`. Instrumentations that generate connection pool name following different patterns SHOULD document it. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"myDataSource"` +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_CONNECTION_POOL_NAME: &str = "db.client.connection.pool.name"; + +/// The state of a connection in the pool +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"idle"` +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_CONNECTION_STATE: &str = "db.client.connection.state"; + +/// Deprecated, use `db.client.connection.pool.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"myDataSource"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.pool.name`., reason: renamed, renamed_to: db.client.connection.pool.name}" +)] +pub const DB_CLIENT_CONNECTIONS_POOL_NAME: &str = "db.client.connections.pool.name"; + +/// Deprecated, use `db.client.connection.state` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"idle"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.state`., reason: renamed, renamed_to: db.client.connection.state}" +)] +pub const DB_CLIENT_CONNECTIONS_STATE: &str = "db.client.connections.state"; + +/// The name of a collection (table, container) within the database. +/// +/// ## Notes +/// +/// It is RECOMMENDED to capture the value as provided by the application +/// without attempting to do any case normalization. +/// +/// The collection name SHOULD NOT be extracted from `db.query.text`, +/// when the database system supports query text with multiple collections +/// in non-batch operations. +/// +/// For batch operations, if the individual operations are known to have the same +/// collection name then that collection name SHOULD be used. +/// +/// # Examples +/// +/// - `"public.users"` +/// - `"customers"` +pub const DB_COLLECTION_NAME: &str = "db.collection.name"; + +/// Deprecated, use `server.address`, `server.port` attributes instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Server=(localdb)\\v11.0;Integrated Security=true;"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `server.address` and `server.port`.\n, reason: uncategorized}" +)] +pub const DB_CONNECTION_STRING: &str = "db.connection_string"; + +/// Deprecated, use `azure.client.id` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"3ba4827d-4422-483f-b59f-85b74211c11d"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `azure.client.id`., reason: renamed, renamed_to: azure.client.id}" +)] +pub const DB_COSMOSDB_CLIENT_ID: &str = "db.cosmosdb.client_id"; + +/// Deprecated, use `azure.cosmosdb.connection.mode` instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `azure.cosmosdb.connection.mode`., reason: renamed, renamed_to: azure.cosmosdb.connection.mode}" +)] +pub const DB_COSMOSDB_CONNECTION_MODE: &str = "db.cosmosdb.connection_mode"; + +/// Deprecated, use `cosmosdb.consistency.level` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Eventual"` +/// - `"ConsistentPrefix"` +/// - `"BoundedStaleness"` +/// - `"Strong"` +/// - `"Session"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `azure.cosmosdb.consistency.level`., reason: renamed, renamed_to: azure.cosmosdb.consistency.level}" +)] +pub const DB_COSMOSDB_CONSISTENCY_LEVEL: &str = "db.cosmosdb.consistency_level"; + +/// Deprecated, use `db.collection.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"mytable"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.collection.name`., reason: renamed, renamed_to: db.collection.name}" +)] +pub const DB_COSMOSDB_CONTAINER: &str = "db.cosmosdb.container"; + +/// Deprecated, no replacement at this time. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time.\n, reason: obsoleted}")] +pub const DB_COSMOSDB_OPERATION_TYPE: &str = "db.cosmosdb.operation_type"; + +/// Deprecated, use `azure.cosmosdb.operation.contacted_regions` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "North Central US", +/// "Australia East", +/// "Australia Southeast", +/// ]` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `azure.cosmosdb.operation.contacted_regions`., reason: renamed, renamed_to: azure.cosmosdb.operation.contacted_regions}" +)] +pub const DB_COSMOSDB_REGIONS_CONTACTED: &str = "db.cosmosdb.regions_contacted"; + +/// Deprecated, use `azure.cosmosdb.operation.request_charge` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `46.18` +/// - `1.0` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `azure.cosmosdb.operation.request_charge`., reason: renamed, renamed_to: azure.cosmosdb.operation.request_charge}" +)] +pub const DB_COSMOSDB_REQUEST_CHARGE: &str = "db.cosmosdb.request_charge"; + +/// Deprecated, use `azure.cosmosdb.request.body.size` instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `azure.cosmosdb.request.body.size`., reason: renamed, renamed_to: azure.cosmosdb.request.body.size}" +)] +pub const DB_COSMOSDB_REQUEST_CONTENT_LENGTH: &str = "db.cosmosdb.request_content_length"; + +/// Deprecated, use `db.response.status_code` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `200` +/// - `201` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Use `db.response.status_code` instead., reason: uncategorized}")] +pub const DB_COSMOSDB_STATUS_CODE: &str = "db.cosmosdb.status_code"; + +/// Deprecated, use `azure.cosmosdb.response.sub_status_code` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1000` +/// - `1002` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `azure.cosmosdb.response.sub_status_code`., reason: renamed, renamed_to: azure.cosmosdb.response.sub_status_code}" +)] +pub const DB_COSMOSDB_SUB_STATUS_CODE: &str = "db.cosmosdb.sub_status_code"; + +/// Deprecated, use `db.namespace` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"e9106fc68e3044f0b1475b04bf4ffd5f"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.namespace`., reason: renamed, renamed_to: db.namespace}" +)] +pub const DB_ELASTICSEARCH_CLUSTER_NAME: &str = "db.elasticsearch.cluster.name"; + +/// Deprecated, use `elasticsearch.node.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"instance-0000000001"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `elasticsearch.node.name`., reason: renamed, renamed_to: elasticsearch.node.name}" +)] +pub const DB_ELASTICSEARCH_NODE_NAME: &str = "db.elasticsearch.node.name"; + +/// Deprecated, use `db.operation.parameter` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"test-index"` +/// - `"123"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.operation.parameter`., reason: renamed, renamed_to: db.operation.parameter}" +)] +pub const DB_ELASTICSEARCH_PATH_PARTS: &str = "db.elasticsearch.path_parts"; + +/// Deprecated, no general replacement at this time. For Elasticsearch, use `db.elasticsearch.node.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"mysql-e26b99z.example.com"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Removed, no general replacement at this time. For Elasticsearch, use `db.elasticsearch.node.name` instead.\n, reason: obsoleted}" +)] +pub const DB_INSTANCE_ID: &str = "db.instance.id"; + +/// Removed, no replacement at this time. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"org.postgresql.Driver"` +/// - `"com.microsoft.sqlserver.jdbc.SQLServerDriver"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time.\n, reason: obsoleted}")] +pub const DB_JDBC_DRIVER_CLASSNAME: &str = "db.jdbc.driver_classname"; + +/// Deprecated, use `db.collection.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"mytable"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.collection.name`., reason: renamed, renamed_to: db.collection.name}" +)] +pub const DB_MONGODB_COLLECTION: &str = "db.mongodb.collection"; + +/// Deprecated, SQL Server instance is now populated as a part of `db.namespace` attribute. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"MSSQLSERVER"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const DB_MSSQL_INSTANCE_NAME: &str = "db.mssql.instance_name"; + +/// Deprecated, use `db.namespace` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"customers"` +/// - `"main"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.namespace`., reason: renamed, renamed_to: db.namespace}" +)] +pub const DB_NAME: &str = "db.name"; + +/// The name of the database, fully qualified within the server address and port. +/// +/// ## Notes +/// +/// If a database system has multiple namespace components, they SHOULD be concatenated from the most general to the most specific namespace component, using `|` as a separator between the components. Any missing components (and their associated separators) SHOULD be omitted. +/// Semantic conventions for individual database systems SHOULD document what `db.namespace` means in the context of that system. +/// It is RECOMMENDED to capture the value as provided by the application without attempting to do any case normalization. +/// +/// # Examples +/// +/// - `"customers"` +/// - `"test.users"` +pub const DB_NAMESPACE: &str = "db.namespace"; + +/// Deprecated, use `db.operation.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"findAndModify"` +/// - `"HMSET"` +/// - `"SELECT"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.operation.name`., reason: renamed, renamed_to: db.operation.name}" +)] +pub const DB_OPERATION: &str = "db.operation"; + +/// The number of queries included in a batch operation. +/// +/// ## Notes +/// +/// Operations are only considered batches when they contain two or more operations, and so `db.operation.batch.size` SHOULD never be `1`. +/// +/// # Examples +/// +/// - `2` +/// - `3` +/// - `4` +pub const DB_OPERATION_BATCH_SIZE: &str = "db.operation.batch.size"; + +/// The name of the operation or command being executed. +/// +/// ## Notes +/// +/// It is RECOMMENDED to capture the value as provided by the application +/// without attempting to do any case normalization. +/// +/// The operation name SHOULD NOT be extracted from `db.query.text`, +/// when the database system supports query text with multiple operations +/// in non-batch operations. +/// +/// If spaces can occur in the operation name, multiple consecutive spaces +/// SHOULD be normalized to a single space. +/// +/// For batch operations, if the individual operations are known to have the same operation name +/// then that operation name SHOULD be used prepended by `BATCH `, +/// otherwise `db.operation.name` SHOULD be `BATCH` or some other database +/// system specific term if more applicable. +/// +/// # Examples +/// +/// - `"findAndModify"` +/// - `"HMSET"` +/// - `"SELECT"` +pub const DB_OPERATION_NAME: &str = "db.operation.name"; + +/// A database operation parameter, with ``key`` being the parameter name, and the attribute value being a string representation of the parameter value. +/// +/// ## Notes +/// +/// For example, a client-side maximum number of rows to read from the database +/// MAY be recorded as the `db.operation.parameter.max_rows` attribute. +/// +/// `db.query.text` parameters SHOULD be captured using `db.query.parameter.[key]` +/// instead of `db.operation.parameter.[key]`. +/// +/// # Examples +/// +/// - `"someval"` +/// - `"55"` +#[cfg(feature = "semconv_experimental")] +pub const DB_OPERATION_PARAMETER: &str = "db.operation.parameter"; + +/// A database query parameter, with ``key`` being the parameter name, and the attribute value being a string representation of the parameter value. +/// +/// ## Notes +/// +/// If a query parameter has no name and instead is referenced only by index, +/// then `[key]` SHOULD be the 0-based index. +/// +/// `db.query.parameter.[key]` SHOULD match +/// up with the parameterized placeholders present in `db.query.text`. +/// +/// It is RECOMMENDED to capture the value as provided by the application +/// without attempting to do any case normalization. +/// +/// `db.query.parameter.[key]` SHOULD NOT be captured on batch operations. +/// +/// Examples: +/// +/// - For a query `SELECT * FROM users where username = %s` with the parameter `"jdoe"`, +/// the attribute `db.query.parameter.0` SHOULD be set to `"jdoe"`. +/// - For a query `"SELECT * FROM users WHERE username = %(userName)s;` with parameter +/// `userName = "jdoe"`, the attribute `db.query.parameter.userName` SHOULD be set to `"jdoe"`. +/// +/// # Examples +/// +/// - `"someval"` +/// - `"55"` +#[cfg(feature = "semconv_experimental")] +pub const DB_QUERY_PARAMETER: &str = "db.query.parameter"; + +/// Low cardinality summary of a database query. +/// +/// ## Notes +/// +/// The query summary describes a class of database queries and is useful +/// as a grouping key, especially when analyzing telemetry for database +/// calls involving complex queries. +/// +/// Summary may be available to the instrumentation through +/// instrumentation hooks or other means. If it is not available, instrumentations +/// that support query parsing SHOULD generate a summary following +/// [Generating query summary](/docs/db/database-spans.md#generating-a-summary-of-the-query) +/// section. +/// +/// For batch operations, if the individual operations are known to have the same query summary +/// then that query summary SHOULD be used prepended by `BATCH `, +/// otherwise `db.query.summary` SHOULD be `BATCH` or some other database +/// system specific term if more applicable. +/// +/// # Examples +/// +/// - `"SELECT wuser_table"` +/// - `"INSERT shipping_details SELECT orders"` +/// - `"get user by id"` +pub const DB_QUERY_SUMMARY: &str = "db.query.summary"; + +/// The database query being executed. +/// +/// ## Notes +/// +/// For sanitization see [Sanitization of `db.query.text`](/docs/db/database-spans.md#sanitization-of-dbquerytext). +/// For batch operations, if the individual operations are known to have the same query text then that query text SHOULD be used, otherwise all of the individual query texts SHOULD be concatenated with separator `; ` or some other database system specific separator if more applicable. +/// Parameterized query text SHOULD NOT be sanitized. Even though parameterized query text can potentially have sensitive data, by using a parameterized query the user is giving a strong signal that any sensitive data will be passed as parameter values, and the benefit to observability of capturing the static part of the query text by default outweighs the risk. +/// +/// # Examples +/// +/// - `"SELECT * FROM wuser_table where username = ?"` +/// - `"SET mykey ?"` +pub const DB_QUERY_TEXT: &str = "db.query.text"; + +/// Deprecated, use `db.namespace` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `0` +/// - `1` +/// - `15` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Uncategorized., reason: uncategorized}")] +pub const DB_REDIS_DATABASE_INDEX: &str = "db.redis.database_index"; + +/// Number of rows returned by the operation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `10` +/// - `30` +/// - `1000` +#[cfg(feature = "semconv_experimental")] +pub const DB_RESPONSE_RETURNED_ROWS: &str = "db.response.returned_rows"; + +/// Database response status code. +/// +/// ## Notes +/// +/// The status code returned by the database. Usually it represents an error code, but may also represent partial success, warning, or differentiate between various types of successful outcomes. +/// Semantic conventions for individual database systems SHOULD document what `db.response.status_code` means in the context of that system. +/// +/// # Examples +/// +/// - `"102"` +/// - `"ORA-17002"` +/// - `"08P01"` +/// - `"404"` +pub const DB_RESPONSE_STATUS_CODE: &str = "db.response.status_code"; + +/// Deprecated, use `db.collection.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"mytable"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.collection.name`, but only if not extracting the value from `db.query.text`., reason: uncategorized}" +)] +pub const DB_SQL_TABLE: &str = "db.sql.table"; + +/// The database statement being executed. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"SELECT * FROM wuser_table"` +/// - `"SET mykey \"WuValue\""` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.query.text`., reason: renamed, renamed_to: db.query.text}" +)] +pub const DB_STATEMENT: &str = "db.statement"; + +/// The name of a stored procedure within the database. +/// +/// ## Notes +/// +/// It is RECOMMENDED to capture the value as provided by the application +/// without attempting to do any case normalization. +/// +/// For batch operations, if the individual operations are known to have the same +/// stored procedure name then that stored procedure name SHOULD be used. +/// +/// # Examples +/// +/// - `"GetCustomer"` +pub const DB_STORED_PROCEDURE_NAME: &str = "db.stored_procedure.name"; + +/// Deprecated, use `db.system.name` instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.system.name`., reason: renamed, renamed_to: db.system.name}" +)] +pub const DB_SYSTEM: &str = "db.system"; + +/// The database management system (DBMS) product as identified by the client instrumentation. +/// +/// ## Notes +/// +/// The actual DBMS may differ from the one identified by the client. For example, when using PostgreSQL client libraries to connect to a CockroachDB, the `db.system.name` is set to `postgresql` based on the instrumentation's best knowledge +pub const DB_SYSTEM_NAME: &str = "db.system.name"; + +/// Deprecated, no replacement at this time. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"readonly_user"` +/// - `"reporting_user"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const DB_USER: &str = "db.user"; + +/// Deprecated, use `deployment.environment.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"staging"` +/// - `"production"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `deployment.environment.name`., reason: renamed, renamed_to: deployment.environment.name}" +)] +pub const DEPLOYMENT_ENVIRONMENT: &str = "deployment.environment"; + +/// Name of the [deployment environment](https://wikipedia.org/wiki/Deployment_environment) (aka deployment tier). +/// +/// ## Notes +/// +/// `deployment.environment.name` does not affect the uniqueness constraints defined through +/// the `service.namespace`, `service.name` and `service.instance.id` resource attributes. +/// This implies that resources carrying the following attribute combinations MUST be +/// considered to be identifying the same service: +/// +/// - `service.name=frontend`, `deployment.environment.name=production` +/// - `service.name=frontend`, `deployment.environment.name=staging`. +/// +/// # Examples +/// +/// - `"staging"` +/// - `"production"` +pub const DEPLOYMENT_ENVIRONMENT_NAME: &str = "deployment.environment.name"; + +/// The id of the deployment. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1208"` +#[cfg(feature = "semconv_experimental")] +pub const DEPLOYMENT_ID: &str = "deployment.id"; + +/// The name of the deployment. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"deploy my app"` +/// - `"deploy-frontend"` +#[cfg(feature = "semconv_experimental")] +pub const DEPLOYMENT_NAME: &str = "deployment.name"; + +/// The status of the deployment. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const DEPLOYMENT_STATUS: &str = "deployment.status"; + +/// Destination address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. +/// +/// ## Notes +/// +/// When observed from the source side, and when communicating through an intermediary, `destination.address` SHOULD represent the destination address behind any intermediaries, for example proxies, if it's available. +/// +/// # Examples +/// +/// - `"destination.example.com"` +/// - `"10.1.2.80"` +/// - `"/tmp/my.sock"` +#[cfg(feature = "semconv_experimental")] +pub const DESTINATION_ADDRESS: &str = "destination.address"; + +/// Destination port number +/// +/// ## Notes +/// +/// # Examples +/// +/// - `3389` +/// - `2888` +#[cfg(feature = "semconv_experimental")] +pub const DESTINATION_PORT: &str = "destination.port"; + +/// A unique identifier representing the device +/// +/// ## Notes +/// +/// Its value SHOULD be identical for all apps on a device and it SHOULD NOT change if an app is uninstalled and re-installed. +/// However, it might be resettable by the user for all apps on a device. +/// Hardware IDs (e.g. vendor-specific serial number, IMEI or MAC address) MAY be used as values. +/// +/// More information about Android identifier best practices can be found in the [Android user data IDs guide](https://developer.android.com/training/articles/user-data-ids). +/// +/// \] \[!WARNING\] +/// \] +/// \] This attribute may contain sensitive (PII) information. Caution should be taken when storing personal data or anything which can identify a user. GDPR and data protection laws may apply, +/// \] ensure you do your own due diligence. +/// \] +/// \] Due to these reasons, this identifier is not recommended for consumer applications and will likely result in rejection from both Google Play and App Store. +/// \] However, it may be appropriate for specific enterprise scenarios, such as kiosk devices or enterprise-managed devices, with appropriate compliance clearance. +/// \] Any instrumentation providing this identifier MUST implement it as an opt-in feature. +/// \] +/// \] See [`app.installation.id`](/docs/registry/attributes/app.md#app-installation-id) for a more privacy-preserving alternative. +/// +/// # Examples +/// +/// - `"123456789012345"` +/// - `"01:23:45:67:89:AB"` +#[cfg(feature = "semconv_experimental")] +pub const DEVICE_ID: &str = "device.id"; + +/// The name of the device manufacturer +/// +/// ## Notes +/// +/// The Android OS provides this field via [Build](https://developer.android.com/reference/android/os/Build#MANUFACTURER). iOS apps SHOULD hardcode the value `Apple`. +/// +/// # Examples +/// +/// - `"Apple"` +/// - `"Samsung"` +#[cfg(feature = "semconv_experimental")] +pub const DEVICE_MANUFACTURER: &str = "device.manufacturer"; + +/// The model identifier for the device +/// +/// ## Notes +/// +/// It's recommended this value represents a machine-readable version of the model identifier rather than the market or consumer-friendly name of the device. +/// +/// # Examples +/// +/// - `"iPhone3,4"` +/// - `"SM-G920F"` +#[cfg(feature = "semconv_experimental")] +pub const DEVICE_MODEL_IDENTIFIER: &str = "device.model.identifier"; + +/// The marketing name for the device model +/// +/// ## Notes +/// +/// It's recommended this value represents a human-readable version of the device model rather than a machine-readable alternative. +/// +/// # Examples +/// +/// - `"iPhone 6s Plus"` +/// - `"Samsung Galaxy S6"` +#[cfg(feature = "semconv_experimental")] +pub const DEVICE_MODEL_NAME: &str = "device.model.name"; + +/// The disk IO operation direction. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"read"` +#[cfg(feature = "semconv_experimental")] +pub const DISK_IO_DIRECTION: &str = "disk.io.direction"; + +/// The list of IPv4 or IPv6 addresses resolved during DNS lookup. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "10.0.0.1", +/// "2001:0db8:85a3:0000:0000:8a2e:0370:7334", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const DNS_ANSWERS: &str = "dns.answers"; + +/// The name being queried. +/// +/// ## Notes +/// +/// The name represents the queried domain name as it appears in the DNS query without any additional normalization. +/// +/// # Examples +/// +/// - `"www.example.com"` +/// - `"opentelemetry.io"` +#[cfg(feature = "semconv_experimental")] +pub const DNS_QUESTION_NAME: &str = "dns.question.name"; + +/// Name of the garbage collector managed heap generation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"gen0"` +/// - `"gen1"` +/// - `"gen2"` +pub const DOTNET_GC_HEAP_GENERATION: &str = "dotnet.gc.heap.generation"; + +/// Represents the human-readable identifier of the node/instance to which a request was routed. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"instance-0000000001"` +#[cfg(feature = "semconv_experimental")] +pub const ELASTICSEARCH_NODE_NAME: &str = "elasticsearch.node.name"; + +/// Unique identifier of an end user in the system. It maybe a username, email address, or other identifier. +/// +/// ## Notes +/// +/// Unique identifier of an end user in the system. +/// +/// \] \[!Warning\] +/// \] This field contains sensitive (PII) information. +/// +/// # Examples +/// +/// - `"username"` +#[cfg(feature = "semconv_experimental")] +pub const ENDUSER_ID: &str = "enduser.id"; + +/// Pseudonymous identifier of an end user. This identifier should be a random value that is not directly linked or associated with the end user's actual identity. +/// +/// ## Notes +/// +/// Pseudonymous identifier of an end user. +/// +/// \] \[!Warning\] +/// \] This field contains sensitive (linkable PII) information. +/// +/// # Examples +/// +/// - `"QdH5CAWJgqVT4rOr0qtumf"` +#[cfg(feature = "semconv_experimental")] +pub const ENDUSER_PSEUDO_ID: &str = "enduser.pseudo.id"; + +/// Deprecated, use `user.roles` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"admin"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Use `user.roles` instead., reason: uncategorized}")] +pub const ENDUSER_ROLE: &str = "enduser.role"; + +/// Deprecated, no replacement at this time. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"read:message, write:files"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const ENDUSER_SCOPE: &str = "enduser.scope"; + +/// A message providing more detail about an error in human-readable form. +/// +/// ## Notes +/// +/// `error.message` should provide additional context and detail about an error. +/// It is NOT RECOMMENDED to duplicate the value of `error.type` in `error.message`. +/// It is also NOT RECOMMENDED to duplicate the value of `exception.message` in `error.message`. +/// +/// `error.message` is NOT RECOMMENDED for metrics or spans due to its unbounded cardinality and overlap with span status. +/// +/// # Examples +/// +/// - `"Unexpected input type: string"` +/// - `"The user has exceeded their storage quota"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Use domain-specific error message attribute. For example, use `feature_flag.error.message` for feature flag errors., reason: obsoleted}" +)] +pub const ERROR_MESSAGE: &str = "error.message"; + +/// Describes a class of error the operation ended with. +/// +/// ## Notes +/// +/// The `error.type` SHOULD be predictable, and SHOULD have low cardinality. +/// +/// When `error.type` is set to a type (e.g., an exception type), its +/// canonical class name identifying the type within the artifact SHOULD be used. +/// +/// If the recorded error type is a wrapper that is not meaningful for +/// failure classification, instrumentation MAY use the type of the inner +/// error instead. For example, in Go, errors created with `fmt.Errorf` +/// using `%w` MAY be unwrapped when the wrapper type does not help +/// classify the failure. +/// +/// Instrumentations SHOULD document the list of errors they report. +/// +/// The cardinality of `error.type` within one instrumentation library SHOULD be low. +/// Telemetry consumers that aggregate data from multiple instrumentation libraries and applications +/// should be prepared for `error.type` to have high cardinality at query time when no +/// additional filters are applied. +/// +/// If the operation has completed successfully, instrumentations SHOULD NOT set `error.type`. +/// +/// If a specific domain defines its own set of error identifiers (such as HTTP or RPC status codes), +/// it's RECOMMENDED to: +/// +/// - Use a domain-specific attribute +/// - Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not. +/// +/// # Examples +/// +/// - `"timeout"` +/// - `"java.net.UnknownHostException"` +/// - `"server_certificate_invalid"` +/// - `"500"` +pub const ERROR_TYPE: &str = "error.type"; + +/// Identifies the class / type of event. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"browser.mouse.click"` +/// - `"device.app.lifecycle"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: The value of this attribute MUST now be set as the value of the EventName field on the LogRecord to indicate that the LogRecord represents an Event.\n, reason: uncategorized}" +)] +pub const EVENT_NAME: &str = "event.name"; + +/// Indicates that the exception is escaping the scope of the span. +/// +/// ## Notes +#[deprecated( + note = "{note: It's no longer recommended to record exceptions that are handled and do not escape the scope of a span.\n, reason: obsoleted}" +)] +pub const EXCEPTION_ESCAPED: &str = "exception.escaped"; + +/// The exception message. +/// +/// ## Notes +/// +/// \] \[!WARNING\] +/// \] +/// \] This attribute may contain sensitive information. +/// +/// # Examples +/// +/// - `"Division by zero"` +/// - `"Can't convert 'int' object to str implicitly"` +pub const EXCEPTION_MESSAGE: &str = "exception.message"; + +/// A stacktrace as a string in the natural representation for the language runtime. The representation is to be determined and documented by each language SIG. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Exception in thread \"main\" java.lang.RuntimeException: Test exception\\n at com.example.GenerateTrace.methodB(GenerateTrace.java:13)\\n at com.example.GenerateTrace.methodA(GenerateTrace.java:9)\\n at com.example.GenerateTrace.main(GenerateTrace.java:5)\n"` +pub const EXCEPTION_STACKTRACE: &str = "exception.stacktrace"; + +/// The type of the exception (its fully-qualified class name, if applicable). The dynamic type of the exception should be preferred over the static type in languages that support it. +/// +/// ## Notes +/// +/// If the recorded exception type is a wrapper that is not meaningful for +/// failure classification, instrumentation MAY use the type of the inner +/// exception instead. For example, in Go, errors created with `fmt.Errorf` +/// using `%w` MAY be unwrapped when the wrapper type does not help +/// classify the failure. +/// +/// # Examples +/// +/// - `"java.net.ConnectException"` +/// - `"OSError"` +pub const EXCEPTION_TYPE: &str = "exception.type"; + +/// A boolean that is true if the serverless function is executed for the first time (aka cold-start). +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const FAAS_COLDSTART: &str = "faas.coldstart"; + +/// A string containing the schedule period as [Cron Expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"0/5 * * * ? *"` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_CRON: &str = "faas.cron"; + +/// The name of the source on which the triggering operation was performed. For example, in Cloud Storage or S3 corresponds to the bucket name, and in Cosmos DB to the database name. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"myBucketName"` +/// - `"myDbName"` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_DOCUMENT_COLLECTION: &str = "faas.document.collection"; + +/// The document name/table subjected to the operation. For example, in Cloud Storage or S3 is the name of the file, and in Cosmos DB the table name. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"myFile.txt"` +/// - `"myTableName"` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_DOCUMENT_NAME: &str = "faas.document.name"; + +/// Describes the type of the operation that was performed on the data. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const FAAS_DOCUMENT_OPERATION: &str = "faas.document.operation"; + +/// A string containing the time when the data was accessed in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2020-01-23T13:47:06Z"` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_DOCUMENT_TIME: &str = "faas.document.time"; + +/// The execution environment ID as a string, that will be potentially reused for other invocations to the same function/function version. +/// +/// ## Notes +/// +/// - **AWS Lambda:** Use the (full) log stream name. +/// +/// # Examples +/// +/// - `"2021/06/28/[$LATEST]2f399eb14537447da05ab2a2e39309de"` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_INSTANCE: &str = "faas.instance"; + +/// The invocation ID of the current function invocation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"af9d5aa4-a685-4c5f-a22b-444f80b3cc28"` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_INVOCATION_ID: &str = "faas.invocation_id"; + +/// The name of the invoked function. +/// +/// ## Notes +/// +/// SHOULD be equal to the `faas.name` resource attribute of the invoked function. +/// +/// # Examples +/// +/// - `"my-function"` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_INVOKED_NAME: &str = "faas.invoked_name"; + +/// The cloud provider of the invoked function. +/// +/// ## Notes +/// +/// SHOULD be equal to the `cloud.provider` resource attribute of the invoked function +#[cfg(feature = "semconv_experimental")] +pub const FAAS_INVOKED_PROVIDER: &str = "faas.invoked_provider"; + +/// The cloud region of the invoked function. +/// +/// ## Notes +/// +/// SHOULD be equal to the `cloud.region` resource attribute of the invoked function. +/// +/// # Examples +/// +/// - `"eu-central-1"` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_INVOKED_REGION: &str = "faas.invoked_region"; + +/// The amount of memory available to the serverless function converted to Bytes. +/// +/// ## Notes +/// +/// It's recommended to set this attribute since e.g. too little memory can easily stop a Java AWS Lambda function from working correctly. On AWS Lambda, the environment variable `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` provides this information (which must be multiplied by 1,048,576). +/// +/// # Examples +/// +/// - `134217728` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_MAX_MEMORY: &str = "faas.max_memory"; + +/// The name of the single function that this runtime instance executes. +/// +/// ## Notes +/// +/// This is the name of the function as configured/deployed on the FaaS +/// platform and is usually different from the name of the callback +/// function (which may be stored in the +/// [`code.namespace`/`code.function.name`](/docs/general/attributes.md#source-code-attributes) +/// span attributes). +/// +/// For some cloud providers, the above definition is ambiguous. The following +/// definition of function name MUST be used for this attribute +/// (and consequently the span name) for the listed cloud providers/products: +/// +/// - **Azure:** The full name `[FUNCAPP]/[FUNC]`, i.e., function app name +/// followed by a forward slash followed by the function name (this form +/// can also be seen in the resource JSON for the function). +/// This means that a span attribute MUST be used, as an Azure function +/// app can host multiple functions that would usually share +/// a TracerProvider (see also the `cloud.resource_id` attribute). +/// +/// # Examples +/// +/// - `"my-function"` +/// - `"myazurefunctionapp/some-function-name"` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_NAME: &str = "faas.name"; + +/// A string containing the function invocation time in the [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format expressed in [UTC](https://www.w3.org/TR/NOTE-datetime). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2020-01-23T13:47:06Z"` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_TIME: &str = "faas.time"; + +/// Type of the trigger which caused this function invocation. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const FAAS_TRIGGER: &str = "faas.trigger"; + +/// The immutable version of the function being executed. +/// +/// ## Notes +/// +/// Depending on the cloud provider and platform, use: +/// +/// - **AWS Lambda:** The [function version](https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html) +/// (an integer represented as a decimal string). +/// - **Google Cloud Run (Services):** The [revision](https://cloud.google.com/run/docs/managing/revisions) +/// (i.e., the function name plus the revision suffix). +/// - **Google Cloud Functions:** The value of the +/// [`K_REVISION` environment variable](https://cloud.google.com/run/docs/container-contract#services-env-vars). +/// - **Azure Functions:** Not applicable. Do not set this attribute. +/// +/// # Examples +/// +/// - `"26"` +/// - `"pinkfroid-00002"` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_VERSION: &str = "faas.version"; + +/// The unique identifier for the flag evaluation context. For example, the targeting key. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"5157782b-2203-4c80-a857-dbbd5e7761db"` +#[cfg(feature = "semconv_experimental")] +pub const FEATURE_FLAG_CONTEXT_ID: &str = "feature_flag.context.id"; + +/// A message providing more detail about an error that occurred during feature flag evaluation in human-readable form. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Unexpected input type: string"` +/// - `"The user has exceeded their storage quota"` +#[cfg(feature = "semconv_experimental")] +pub const FEATURE_FLAG_ERROR_MESSAGE: &str = "feature_flag.error.message"; + +/// Deprecated, use `feature_flag.error.message` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Flag `header-color`expected type`string`but found type`number`"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `feature_flag.error.message`., reason: renamed, renamed_to: feature_flag.error.message}" +)] +pub const FEATURE_FLAG_EVALUATION_ERROR_MESSAGE: &str = "feature_flag.evaluation.error.message"; + +/// Deprecated, use `feature_flag.result.reason` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"static"` +/// - `"targeting_match"` +/// - `"error"` +/// - `"default"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `feature_flag.result.reason`., reason: renamed, renamed_to: feature_flag.result.reason}" +)] +pub const FEATURE_FLAG_EVALUATION_REASON: &str = "feature_flag.evaluation.reason"; + +/// The lookup key of the feature flag. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"logo-color"` +#[cfg(feature = "semconv_experimental")] +pub const FEATURE_FLAG_KEY: &str = "feature_flag.key"; + +/// Identifies the feature flag provider. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Flag Manager"` +#[cfg(feature = "semconv_experimental")] +pub const FEATURE_FLAG_PROVIDER_NAME: &str = "feature_flag.provider.name"; + +/// The reason code which shows how a feature flag value was determined. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"static"` +/// - `"targeting_match"` +/// - `"error"` +/// - `"default"` +#[cfg(feature = "semconv_experimental")] +pub const FEATURE_FLAG_RESULT_REASON: &str = "feature_flag.result.reason"; + +/// The evaluated value of the feature flag. +/// +/// ## Notes +/// +/// With some feature flag providers, feature flag results can be quite large or contain private or sensitive details. +/// Because of this, `feature_flag.result.variant` is often the preferred attribute if it is available. +/// +/// It may be desirable to redact or otherwise limit the size and scope of `feature_flag.result.value` if possible. +/// Because the evaluated flag value is unstructured and may be any type, it is left to the instrumentation author to determine how best to achieve this. +/// +/// # Examples +/// +/// - `"#ff0000"` +/// - `true` +/// - `3` +#[cfg(feature = "semconv_experimental")] +pub const FEATURE_FLAG_RESULT_VALUE: &str = "feature_flag.result.value"; + +/// A semantic identifier for an evaluated flag value. +/// +/// ## Notes +/// +/// A semantic identifier, commonly referred to as a variant, provides a means +/// for referring to a value without including the value itself. This can +/// provide additional context for understanding the meaning behind a value. +/// For example, the variant `red` maybe be used for the value `#c05543`. +/// +/// # Examples +/// +/// - `"red"` +/// - `"true"` +/// - `"on"` +#[cfg(feature = "semconv_experimental")] +pub const FEATURE_FLAG_RESULT_VARIANT: &str = "feature_flag.result.variant"; + +/// The identifier of the [flag set](https://openfeature.dev/specification/glossary/#flag-set) to which the feature flag belongs. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"proj-1"` +/// - `"ab98sgs"` +/// - `"service1/dev"` +#[cfg(feature = "semconv_experimental")] +pub const FEATURE_FLAG_SET_ID: &str = "feature_flag.set.id"; + +/// Deprecated, use `feature_flag.result.variant` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"red"` +/// - `"true"` +/// - `"on"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `feature_flag.result.variant`., reason: renamed, renamed_to: feature_flag.result.variant}" +)] +pub const FEATURE_FLAG_VARIANT: &str = "feature_flag.variant"; + +/// The version of the ruleset used during the evaluation. This may be any stable value which uniquely identifies the ruleset. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1"` +/// - `"01ABCDEF"` +#[cfg(feature = "semconv_experimental")] +pub const FEATURE_FLAG_VERSION: &str = "feature_flag.version"; + +/// Time when the file was last accessed, in ISO 8601 format. +/// +/// ## Notes +/// +/// This attribute might not be supported by some file systems — NFS, FAT32, in embedded OS, etc. +/// +/// # Examples +/// +/// - `"2021-01-01T12:00:00Z"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_ACCESSED: &str = "file.accessed"; + +/// Array of file attributes. +/// +/// ## Notes +/// +/// Attributes names depend on the OS or file system. Here’s a non-exhaustive list of values expected for this attribute: `archive`, `compressed`, `directory`, `encrypted`, `execute`, `hidden`, `immutable`, `journaled`, `read`, `readonly`, `symbolic link`, `system`, `temporary`, `write`. +/// +/// # Examples +/// +/// - `[ +/// "readonly", +/// "hidden", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const FILE_ATTRIBUTES: &str = "file.attributes"; + +/// Time when the file attributes or metadata was last changed, in ISO 8601 format. +/// +/// ## Notes +/// +/// `file.changed` captures the time when any of the file's properties or attributes (including the content) are changed, while `file.modified` captures the timestamp when the file content is modified. +/// +/// # Examples +/// +/// - `"2021-01-01T12:00:00Z"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_CHANGED: &str = "file.changed"; + +/// Time when the file was created, in ISO 8601 format. +/// +/// ## Notes +/// +/// This attribute might not be supported by some file systems — NFS, FAT32, in embedded OS, etc. +/// +/// # Examples +/// +/// - `"2021-01-01T12:00:00Z"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_CREATED: &str = "file.created"; + +/// Directory where the file is located. It should include the drive letter, when appropriate. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/home/user"` +/// - `"C:\\Program Files\\MyApp"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_DIRECTORY: &str = "file.directory"; + +/// File extension, excluding the leading dot. +/// +/// ## Notes +/// +/// When the file name has multiple extensions (example.tar.gz), only the last one should be captured ("gz", not "tar.gz"). +/// +/// # Examples +/// +/// - `"png"` +/// - `"gz"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_EXTENSION: &str = "file.extension"; + +/// Name of the fork. A fork is additional data associated with a filesystem object. +/// +/// ## Notes +/// +/// On Linux, a resource fork is used to store additional data with a filesystem object. A file always has at least one fork for the data portion, and additional forks may exist. +/// On NTFS, this is analogous to an Alternate Data Stream (ADS), and the default data stream for a file is just called $DATA. Zone.Identifier is commonly used by Windows to track contents downloaded from the Internet. An ADS is typically of the form: C:\path\to\filename.extension:some_fork_name, and some_fork_name is the value that should populate `fork_name`. `filename.extension` should populate `file.name`, and `extension` should populate `file.extension`. The full path, `file.path`, will include the fork name. +/// +/// # Examples +/// +/// - `"Zone.Identifier"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_FORK_NAME: &str = "file.fork_name"; + +/// Primary Group ID (GID) of the file. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1000"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_GROUP_ID: &str = "file.group.id"; + +/// Primary group name of the file. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"users"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_GROUP_NAME: &str = "file.group.name"; + +/// Inode representing the file in the filesystem. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"256383"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_INODE: &str = "file.inode"; + +/// The lock mechanism such as noted by [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"POSIX"` +/// - `"FLOCK"` +/// - `"DELEG"` +/// - `"LEASE"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_LOCK_MECHANISM: &str = "file.lock.mechanism"; + +/// Mode of lock or operation such as documented by [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"ADVISORY"` +/// - `"MANDATORY"` +/// - `"BREAKING"` +/// - `"ACTIVE"` +/// - `"BREAKER"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_LOCK_MODE: &str = "file.lock.mode"; + +/// The lock type as represented by i.e. [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html)'s l_type. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"read"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_LOCK_TYPE: &str = "file.lock.type"; + +/// Mode of the file in octal representation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"0640"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_MODE: &str = "file.mode"; + +/// Time when the file content was last modified, in ISO 8601 format. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2021-01-01T12:00:00Z"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_MODIFIED: &str = "file.modified"; + +/// Name of the file including the extension, without the directory. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"example.png"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_NAME: &str = "file.name"; + +/// The user ID (UID) or security identifier (SID) of the file owner. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1000"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_OWNER_ID: &str = "file.owner.id"; + +/// Username of the file owner. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"root"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_OWNER_NAME: &str = "file.owner.name"; + +/// Full path to the file, including the file name. It should include the drive letter, when appropriate. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/home/alice/example.png"` +/// - `"C:\\Program Files\\MyApp\\myapp.exe"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_PATH: &str = "file.path"; + +/// File size in bytes. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const FILE_SIZE: &str = "file.size"; + +/// Path to the target of a symbolic link. +/// +/// ## Notes +/// +/// This attribute is only applicable to symbolic links. +/// +/// # Examples +/// +/// - `"/usr/bin/python3"` +#[cfg(feature = "semconv_experimental")] +pub const FILE_SYMBOLIC_LINK_TARGET_PATH: &str = "file.symbolic_link.target_path"; + +/// The container within GCP where the AppHub application is defined. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"projects/my-container-project"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_APPLICATION_CONTAINER: &str = "gcp.apphub.application.container"; + +/// The name of the application as configured in AppHub. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-application"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_APPLICATION_ID: &str = "gcp.apphub.application.id"; + +/// The GCP zone or region where the application is defined. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"us-central1"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_APPLICATION_LOCATION: &str = "gcp.apphub.application.location"; + +/// Criticality of a service indicates its importance to the business. +/// +/// ## Notes +/// +/// [See AppHub type enum](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type) +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_SERVICE_CRITICALITY_TYPE: &str = "gcp.apphub.service.criticality_type"; + +/// Environment of a service is the stage of a software lifecycle. +/// +/// ## Notes +/// +/// [See AppHub environment type](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type_1) +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_SERVICE_ENVIRONMENT_TYPE: &str = "gcp.apphub.service.environment_type"; + +/// The name of the service as configured in AppHub. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-service"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_SERVICE_ID: &str = "gcp.apphub.service.id"; + +/// Criticality of a workload indicates its importance to the business. +/// +/// ## Notes +/// +/// [See AppHub type enum](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type) +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_WORKLOAD_CRITICALITY_TYPE: &str = "gcp.apphub.workload.criticality_type"; + +/// Environment of a workload is the stage of a software lifecycle. +/// +/// ## Notes +/// +/// [See AppHub environment type](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type_1) +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_WORKLOAD_ENVIRONMENT_TYPE: &str = "gcp.apphub.workload.environment_type"; + +/// The name of the workload as configured in AppHub. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-workload"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_WORKLOAD_ID: &str = "gcp.apphub.workload.id"; + +/// The container within GCP where the AppHub destination application is defined. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"projects/my-container-project"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_DESTINATION_APPLICATION_CONTAINER: &str = + "gcp.apphub_destination.application.container"; + +/// The name of the destination application as configured in AppHub. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-application"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_DESTINATION_APPLICATION_ID: &str = "gcp.apphub_destination.application.id"; + +/// The GCP zone or region where the destination application is defined. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"us-central1"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_DESTINATION_APPLICATION_LOCATION: &str = + "gcp.apphub_destination.application.location"; + +/// Criticality of a destination workload indicates its importance to the business as specified in [AppHub type enum](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type) +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_DESTINATION_SERVICE_CRITICALITY_TYPE: &str = + "gcp.apphub_destination.service.criticality_type"; + +/// Software lifecycle stage of a destination service as defined [AppHub environment type](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type_1) +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_DESTINATION_SERVICE_ENVIRONMENT_TYPE: &str = + "gcp.apphub_destination.service.environment_type"; + +/// The name of the destination service as configured in AppHub. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-service"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_DESTINATION_SERVICE_ID: &str = "gcp.apphub_destination.service.id"; + +/// Criticality of a destination workload indicates its importance to the business as specified in [AppHub type enum](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type) +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_DESTINATION_WORKLOAD_CRITICALITY_TYPE: &str = + "gcp.apphub_destination.workload.criticality_type"; + +/// Environment of a destination workload is the stage of a software lifecycle as provided in the [AppHub environment type](https://cloud.google.com/app-hub/docs/reference/rest/v1/Attributes#type_1) +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_DESTINATION_WORKLOAD_ENVIRONMENT_TYPE: &str = + "gcp.apphub_destination.workload.environment_type"; + +/// The name of the destination workload as configured in AppHub. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-workload"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_APPHUB_DESTINATION_WORKLOAD_ID: &str = "gcp.apphub_destination.workload.id"; + +/// Identifies the Google Cloud service for which the official client library is intended. +/// +/// ## Notes +/// +/// Intended to be a stable identifier for Google Cloud client libraries that is uniform across implementation languages. The value should be derived from the canonical service domain for the service; for example, 'foo.googleapis.com' should result in a value of 'foo'. +/// +/// # Examples +/// +/// - `"appengine"` +/// - `"run"` +/// - `"firestore"` +/// - `"alloydb"` +/// - `"spanner"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_CLIENT_SERVICE: &str = "gcp.client.service"; + +/// The name of the Cloud Run [execution](https://cloud.google.com/run/docs/managing/job-executions) being run for the Job, as set by the [`CLOUD_RUN_EXECUTION`](https://cloud.google.com/run/docs/container-contract#jobs-env-vars) environment variable. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"job-name-xxxx"` +/// - `"sample-job-mdw84"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_CLOUD_RUN_JOB_EXECUTION: &str = "gcp.cloud_run.job.execution"; + +/// The index for a task within an execution as provided by the [`CLOUD_RUN_TASK_INDEX`](https://cloud.google.com/run/docs/container-contract#jobs-env-vars) environment variable. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `0` +/// - `1` +#[cfg(feature = "semconv_experimental")] +pub const GCP_CLOUD_RUN_JOB_TASK_INDEX: &str = "gcp.cloud_run.job.task_index"; + +/// The hostname of a GCE instance. This is the full value of the default or [custom hostname](https://cloud.google.com/compute/docs/instances/custom-hostname-vm). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-host1234.example.com"` +/// - `"sample-vm.us-west1-b.c.my-project.internal"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_GCE_INSTANCE_HOSTNAME: &str = "gcp.gce.instance.hostname"; + +/// GCE instance labels, ``key`` being the label name and the value being the label value. +/// +/// ## Notes +/// +/// For example, a GCE instance label `team` with value `observability` SHOULD be recorded as the `gcp.gce.instance.labels.team` attribute with value `"observability"`. The `[key]` MUST be the exact GCE instance label key. +/// +/// # Examples +/// +/// - `"observability"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_GCE_INSTANCE_LABELS: &str = "gcp.gce.instance.labels"; + +/// The instance name of a GCE instance. This is the value provided by `host.name`, the visible name of the instance in the Cloud Console UI, and the prefix for the default hostname of the instance as defined by the [default internal DNS name](https://cloud.google.com/compute/docs/internal-dns#instance-fully-qualified-domain-names). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"instance-1"` +/// - `"my-vm-name"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_GCE_INSTANCE_NAME: &str = "gcp.gce.instance.name"; + +/// The name of the Instance Group Manager (IGM) that manages this VM, if any. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"web-igm"` +/// - `"my-managed-group"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_GCE_INSTANCE_GROUP_MANAGER_NAME: &str = "gcp.gce.instance_group_manager.name"; + +/// The region of a **regional** Instance Group Manager (e.g., `us-central1`). Set this **only** when the IGM is regional. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"us-central1"` +/// - `"europe-west1"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_GCE_INSTANCE_GROUP_MANAGER_REGION: &str = "gcp.gce.instance_group_manager.region"; + +/// The zone of a **zonal** Instance Group Manager (e.g., `us-central1-a`). Set this **only** when the IGM is zonal. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"us-central1-a"` +/// - `"europe-west1-b"` +#[cfg(feature = "semconv_experimental")] +pub const GCP_GCE_INSTANCE_GROUP_MANAGER_ZONE: &str = "gcp.gce.instance_group_manager.zone"; + +/// Free-form description of the GenAI agent provided by the application. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Helps with math problems"` +/// - `"Generates fiction stories"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_AGENT_DESCRIPTION: &str = "gen_ai.agent.description"; + +/// The unique identifier of the GenAI agent. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"asst_5j66UpCpwteGg4YSxUnt7lPY"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_AGENT_ID: &str = "gen_ai.agent.id"; + +/// Human-readable name of the GenAI agent provided by the application. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Math Tutor"` +/// - `"Fiction Writer"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_AGENT_NAME: &str = "gen_ai.agent.name"; + +/// The version of the GenAI agent. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1.0.0"` +/// - `"2025-05-01"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_AGENT_VERSION: &str = "gen_ai.agent.version"; + +/// Deprecated, use Event API to report completions contents. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"[{'role': 'assistant', 'content': 'The capital of France is Paris.'}]"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const GEN_AI_COMPLETION: &str = "gen_ai.completion"; + +/// The unique identifier for a conversation (session, thread), used to store and correlate messages within this conversation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"conv_5j66UpCpwteGg4YSxUnt7lPY"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_CONVERSATION_ID: &str = "gen_ai.conversation.id"; + +/// The data source identifier. +/// +/// ## Notes +/// +/// Data sources are used by AI agents and RAG applications to store grounding data. A data source may be an external database, object store, document collection, website, or any other storage system used by the GenAI agent or application. The `gen_ai.data_source.id` SHOULD match the identifier used by the GenAI system rather than a name specific to the external storage, such as a database or object store. Semantic conventions referencing `gen_ai.data_source.id` MAY also leverage additional attributes, such as `db.*`, to further identify and describe the data source. +/// +/// # Examples +/// +/// - `"H7STPQYOND"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_DATA_SOURCE_ID: &str = "gen_ai.data_source.id"; + +/// The number of dimensions the resulting output embeddings should have. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `512` +/// - `1024` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_EMBEDDINGS_DIMENSION_COUNT: &str = "gen_ai.embeddings.dimension.count"; + +/// A free-form explanation for the assigned score provided by the evaluator. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"The response is factually accurate but lacks sufficient detail to fully address the question."` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_EVALUATION_EXPLANATION: &str = "gen_ai.evaluation.explanation"; + +/// The name of the evaluation metric used for the GenAI response. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Relevance"` +/// - `"IntentResolution"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_EVALUATION_NAME: &str = "gen_ai.evaluation.name"; + +/// Human readable label for evaluation. +/// +/// ## Notes +/// +/// This attribute provides a human-readable interpretation of the evaluation score produced by an evaluator. For example, a score value of 1 could mean "relevant" in one evaluation system and "not relevant" in another, depending on the scoring range and evaluator. The label SHOULD have low cardinality. Possible values depend on the evaluation metric and evaluator used; implementations SHOULD document the possible values. +/// +/// # Examples +/// +/// - `"relevant"` +/// - `"not_relevant"` +/// - `"correct"` +/// - `"incorrect"` +/// - `"pass"` +/// - `"fail"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_EVALUATION_SCORE_LABEL: &str = "gen_ai.evaluation.score.label"; + +/// The evaluation score returned by the evaluator. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `4.0` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_EVALUATION_SCORE_VALUE: &str = "gen_ai.evaluation.score.value"; + +/// The chat history provided to the model as an input. +/// +/// ## Notes +/// +/// Instrumentations MUST follow [Input messages JSON schema](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/gen-ai/gen-ai-input-messages.json). +/// When the attribute is recorded on events, it MUST be recorded in structured +/// form. When recorded on spans, it MAY be recorded as a JSON string if structured +/// format is not supported and SHOULD be recorded in structured form otherwise. +/// +/// Messages MUST be provided in the order they were sent to the model. +/// Instrumentations MAY provide a way for users to filter or truncate +/// input messages. +/// +/// \] \[!Warning\] +/// \] This attribute is likely to contain sensitive information including user/PII data. +/// +/// See [Recording content on attributes](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/gen-ai/gen-ai-spans.md#recording-content-on-attributes) +/// section for more details. +/// +/// # Examples +/// +/// - `"[\n {\n \"role\": \"user\",\n \"parts\": [\n {\n \"type\": \"text\",\n \"content\": \"Weather in Paris?\"\n }\n ]\n },\n {\n \"role\": \"assistant\",\n \"parts\": [\n {\n \"type\": \"tool_call\",\n \"id\": \"call_VSPygqKTWdrhaFErNvMV18Yl\",\n \"name\": \"get_weather\",\n \"arguments\": {\n \"location\": \"Paris\"\n }\n }\n ]\n },\n {\n \"role\": \"tool\",\n \"parts\": [\n {\n \"type\": \"tool_call_response\",\n \"id\": \" call_VSPygqKTWdrhaFErNvMV18Yl\",\n \"result\": \"rainy, 57°F\"\n }\n ]\n }\n]\n"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_INPUT_MESSAGES: &str = "gen_ai.input.messages"; + +/// Deprecated, use `gen_ai.output.type`. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `gen_ai.output.type`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT: &str = "gen_ai.openai.request.response_format"; + +/// Deprecated, use `gen_ai.request.seed`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `100` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `gen_ai.request.seed`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_OPENAI_REQUEST_SEED: &str = "gen_ai.openai.request.seed"; + +/// Deprecated, use `openai.request.service_tier`. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `openai.request.service_tier`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_OPENAI_REQUEST_SERVICE_TIER: &str = "gen_ai.openai.request.service_tier"; + +/// Deprecated, use `openai.response.service_tier`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"scale"` +/// - `"default"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `openai.response.service_tier`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_OPENAI_RESPONSE_SERVICE_TIER: &str = "gen_ai.openai.response.service_tier"; + +/// Deprecated, use `openai.response.system_fingerprint`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"fp_44709d6fcb"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `openai.response.system_fingerprint`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_OPENAI_RESPONSE_SYSTEM_FINGERPRINT: &str = + "gen_ai.openai.response.system_fingerprint"; + +/// The name of the operation being performed. +/// +/// ## Notes +/// +/// If one of the predefined values applies, but specific system uses a different name it's RECOMMENDED to document it in the semantic conventions for specific GenAI system and use system-specific name in the instrumentation. If a different name is not documented, instrumentation libraries SHOULD use applicable predefined value +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_OPERATION_NAME: &str = "gen_ai.operation.name"; + +/// Messages returned by the model where each message represents a specific model response (choice, candidate). +/// +/// ## Notes +/// +/// Instrumentations MUST follow [Output messages JSON schema](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/gen-ai/gen-ai-output-messages.json) +/// +/// Each message represents a single output choice/candidate generated by +/// the model. Each message corresponds to exactly one generation +/// (choice/candidate) and vice versa - one choice cannot be split across +/// multiple messages or one message cannot contain parts from multiple choices. +/// +/// When the attribute is recorded on events, it MUST be recorded in structured +/// form. When recorded on spans, it MAY be recorded as a JSON string if structured +/// format is not supported and SHOULD be recorded in structured form otherwise. +/// +/// Instrumentations MAY provide a way for users to filter or truncate +/// output messages. +/// +/// \] \[!Warning\] +/// \] This attribute is likely to contain sensitive information including user/PII data. +/// +/// See [Recording content on attributes](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/gen-ai/gen-ai-spans.md#recording-content-on-attributes) +/// section for more details. +/// +/// # Examples +/// +/// - `"[\n {\n \"role\": \"assistant\",\n \"parts\": [\n {\n \"type\": \"text\",\n \"content\": \"The weather in Paris is currently rainy with a temperature of 57°F.\"\n }\n ],\n \"finish_reason\": \"stop\"\n }\n]\n"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_OUTPUT_MESSAGES: &str = "gen_ai.output.messages"; + +/// Represents the content type requested by the client. +/// +/// ## Notes +/// +/// This attribute SHOULD be used when the client requests output of a specific type. The model may return zero or more outputs of this type. +/// This attribute specifies the output modality and not the actual output format. For example, if an image is requested, the actual output could be a URL pointing to an image file. +/// Additional output format details may be recorded in the future in the `gen_ai.output.{type}.*` attributes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_OUTPUT_TYPE: &str = "gen_ai.output.type"; + +/// Deprecated, use Event API to report prompt contents. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"[{'role': 'user', 'content': 'What is the capital of France?'}]"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const GEN_AI_PROMPT: &str = "gen_ai.prompt"; + +/// The name of the prompt that uniquely identifies it. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"analyze-code"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_PROMPT_NAME: &str = "gen_ai.prompt.name"; + +/// The Generative AI provider as identified by the client or server instrumentation. +/// +/// ## Notes +/// +/// The attribute SHOULD be set based on the instrumentation's best +/// knowledge and may differ from the actual model provider. +/// +/// Multiple providers, including Azure OpenAI, Gemini, and AI hosting platforms +/// are accessible using the OpenAI REST API and corresponding client libraries, +/// but may proxy or host models from different providers. +/// +/// The `gen_ai.request.model`, `gen_ai.response.model`, and `server.address` +/// attributes may help identify the actual system in use. +/// +/// The `gen_ai.provider.name` attribute acts as a discriminator that +/// identifies the GenAI telemetry format flavor specific to that provider +/// within GenAI semantic conventions. +/// It SHOULD be set consistently with provider-specific attributes and signals. +/// For example, GenAI spans, metrics, and events related to AWS Bedrock +/// should have the `gen_ai.provider.name` set to `aws.bedrock` and include +/// applicable `aws.bedrock.*` attributes and are not expected to include +/// `openai.*` attributes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_PROVIDER_NAME: &str = "gen_ai.provider.name"; + +/// The target number of candidate completions to return. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `3` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_CHOICE_COUNT: &str = "gen_ai.request.choice.count"; + +/// The encoding formats requested in an embeddings operation, if specified. +/// +/// ## Notes +/// +/// In some GenAI systems the encoding formats are called embedding types. Also, some GenAI systems only accept a single format per request. +/// +/// # Examples +/// +/// - `[ +/// "base64", +/// ]` +/// - `[ +/// "float", +/// "binary", +/// ]` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_ENCODING_FORMATS: &str = "gen_ai.request.encoding_formats"; + +/// The frequency penalty setting for the GenAI request. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `0.1` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_FREQUENCY_PENALTY: &str = "gen_ai.request.frequency_penalty"; + +/// The maximum number of tokens the model generates for a request. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `100` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_MAX_TOKENS: &str = "gen_ai.request.max_tokens"; + +/// The name of the GenAI model a request is being made to. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"gpt-4"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_MODEL: &str = "gen_ai.request.model"; + +/// The presence penalty setting for the GenAI request. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `0.1` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_PRESENCE_PENALTY: &str = "gen_ai.request.presence_penalty"; + +/// Requests with same seed value more likely to return same result. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `100` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_SEED: &str = "gen_ai.request.seed"; + +/// List of sequences that the model will use to stop generating further tokens. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "forest", +/// "lived", +/// ]` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_STOP_SEQUENCES: &str = "gen_ai.request.stop_sequences"; + +/// Indicates whether the GenAI request was made in streaming mode. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_STREAM: &str = "gen_ai.request.stream"; + +/// The temperature setting for the GenAI request. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `0.0` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_TEMPERATURE: &str = "gen_ai.request.temperature"; + +/// The top_k sampling setting for the GenAI request. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1.0` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_TOP_K: &str = "gen_ai.request.top_k"; + +/// The top_p sampling setting for the GenAI request. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1.0` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_REQUEST_TOP_P: &str = "gen_ai.request.top_p"; + +/// Array of reasons the model stopped generating tokens, corresponding to each generation received. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "stop", +/// ]` +/// - `[ +/// "stop", +/// "length", +/// ]` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_RESPONSE_FINISH_REASONS: &str = "gen_ai.response.finish_reasons"; + +/// The unique identifier for the completion. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"chatcmpl-123"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_RESPONSE_ID: &str = "gen_ai.response.id"; + +/// The name of the model that generated the response. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"gpt-4-0613"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_RESPONSE_MODEL: &str = "gen_ai.response.model"; + +/// Time to first chunk in a streaming response, measured from request issuance, in seconds. The value is measured from when the client issues the generation request to when the first chunk is received in the response stream. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `0.5` +/// - `1.2` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK: &str = "gen_ai.response.time_to_first_chunk"; + +/// The documents retrieved. +/// +/// ## Notes +/// +/// Instrumentations MUST follow [Retrieval documents JSON schema](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/gen-ai/gen-ai-retrieval-documents.json). +/// When the attribute is recorded on events, it MUST be recorded in structured +/// form. When recorded on spans, it MAY be recorded as a JSON string if structured +/// format is not supported and SHOULD be recorded in structured form otherwise. +/// +/// Each document object SHOULD contain at least the following properties: +/// `id` (string): A unique identifier for the document, `score` (double): The relevance score of the document +/// +/// # Examples +/// +/// - `"[\n {\n \"id\": \"doc_123\",\n \"score\": 0.95\n },\n {\n \"id\": \"doc_456\",\n \"score\": 0.87\n },\n {\n \"id\": \"doc_789\",\n \"score\": 0.82\n }\n]\n"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_RETRIEVAL_DOCUMENTS: &str = "gen_ai.retrieval.documents"; + +/// The query text used for retrieval. +/// +/// ## Notes +/// +/// \] \[!Warning\] +/// \] This attribute may contain sensitive information. +/// +/// # Examples +/// +/// - `"What is the capital of France?"` +/// - `"weather in Paris"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_RETRIEVAL_QUERY_TEXT: &str = "gen_ai.retrieval.query.text"; + +/// Deprecated, use `gen_ai.provider.name` instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `gen_ai.provider.name`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_SYSTEM: &str = "gen_ai.system"; + +/// The system message or instructions provided to the GenAI model separately from the chat history. +/// +/// ## Notes +/// +/// This attribute SHOULD be used when the corresponding provider or API +/// allows to provide system instructions or messages separately from the +/// chat history. +/// +/// Instructions that are part of the chat history SHOULD be recorded in +/// `gen_ai.input.messages` attribute instead. +/// +/// Instrumentations MUST follow [System instructions JSON schema](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/gen-ai/gen-ai-system-instructions.json). +/// +/// When recorded on spans, it MAY be recorded as a JSON string if structured +/// format is not supported and SHOULD be recorded in structured form otherwise. +/// +/// Instrumentations MAY provide a way for users to filter or truncate +/// system instructions. +/// +/// \] \[!Warning\] +/// \] This attribute may contain sensitive information. +/// +/// See [Recording content on attributes](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/gen-ai/gen-ai-spans.md#recording-content-on-attributes) +/// section for more details. +/// +/// # Examples +/// +/// - `"[\n {\n \"type\": \"text\",\n \"content\": \"You are an Agent that greet users, always use greetings tool to respond\"\n }\n]\n"` +/// - `"[\n {\n \"type\": \"text\",\n \"content\": \"You are a language translator.\"\n },\n {\n \"type\": \"text\",\n \"content\": \"Your mission is to translate text in English to French.\"\n }\n]\n"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_SYSTEM_INSTRUCTIONS: &str = "gen_ai.system_instructions"; + +/// The type of token being counted. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"input"` +/// - `"output"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_TOKEN_TYPE: &str = "gen_ai.token.type"; + +/// Parameters passed to the tool call. +/// +/// ## Notes +/// +/// \] \[!WARNING\] +/// \] This attribute may contain sensitive information. +/// +/// It's expected to be an object - in case a serialized string is available +/// to the instrumentation, the instrumentation SHOULD do the best effort to +/// deserialize it to an object. When recorded on spans, it MAY be recorded as a JSON string if structured format is not supported and SHOULD be recorded in structured form otherwise. +/// +/// # Examples +/// +/// - `"{\n \"location\": \"San Francisco?\",\n \"date\": \"2025-10-01\"\n}\n"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_TOOL_CALL_ARGUMENTS: &str = "gen_ai.tool.call.arguments"; + +/// The tool call identifier. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"call_mszuSIzqtI65i1wAUOE8w5H4"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_TOOL_CALL_ID: &str = "gen_ai.tool.call.id"; + +/// The result returned by the tool call (if any and if execution was successful). +/// +/// ## Notes +/// +/// \] \[!WARNING\] +/// \] This attribute may contain sensitive information. +/// +/// It's expected to be an object - in case a serialized string is available +/// to the instrumentation, the instrumentation SHOULD do the best effort to +/// deserialize it to an object. When recorded on spans, it MAY be recorded as a JSON string if structured format is not supported and SHOULD be recorded in structured form otherwise. +/// +/// # Examples +/// +/// - `"{\n \"temperature_range\": {\n \"high\": 75,\n \"low\": 60\n },\n \"conditions\": \"sunny\"\n}\n"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_TOOL_CALL_RESULT: &str = "gen_ai.tool.call.result"; + +/// The list of tool definitions available to the GenAI agent or model. +/// +/// ## Notes +/// +/// Instrumentations MUST follow [Tool Definitions JSON Schema](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.0/docs/gen-ai/gen-ai-tool-definitions.json). +/// +/// When the attribute is recorded on events, it MUST be recorded in structured +/// form. When recorded on spans, it MAY be recorded as a JSON string if structured +/// format is not supported and SHOULD be recorded in structured form otherwise. +/// +/// Since this attribute could be large, it's NOT RECOMMENDED to populate +/// non-required properties by default. Instrumentations MAY provide a way +/// to enable populating optional properties. +/// +/// # Examples +/// +/// - `"[\n {\n \"type\": \"function\",\n \"name\": \"get_current_weather\",\n \"description\": \"Get the current weather in a given location\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"location\": {\n \"type\": \"string\",\n \"description\": \"The city and state, e.g. San Francisco, CA\"\n },\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\n \"celsius\",\n \"fahrenheit\"\n ]\n }\n },\n \"required\": [\n \"location\",\n \"unit\"\n ]\n }\n }\n]\n"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_TOOL_DEFINITIONS: &str = "gen_ai.tool.definitions"; + +/// The tool description. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Multiply two numbers"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_TOOL_DESCRIPTION: &str = "gen_ai.tool.description"; + +/// Name of the tool utilized by the agent. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Flights"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_TOOL_NAME: &str = "gen_ai.tool.name"; + +/// Type of the tool utilized by the agent +/// +/// ## Notes +/// +/// Extension: A tool executed on the agent-side to directly call external APIs, bridging the gap between the agent and real-world systems. +/// Agent-side operations involve actions that are performed by the agent on the server or within the agent's controlled environment. +/// Function: A tool executed on the client-side, where the agent generates parameters for a predefined function, and the client executes the logic. +/// Client-side operations are actions taken on the user's end or within the client application. +/// Datastore: A tool used by the agent to access and query structured or unstructured external data for retrieval-augmented tasks or knowledge updates. +/// +/// # Examples +/// +/// - `"function"` +/// - `"extension"` +/// - `"datastore"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_TOOL_TYPE: &str = "gen_ai.tool.type"; + +/// The number of input tokens written to a provider-managed cache. +/// +/// ## Notes +/// +/// The value SHOULD be included in `gen_ai.usage.input_tokens`. +/// +/// # Examples +/// +/// - `25` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS: &str = + "gen_ai.usage.cache_creation.input_tokens"; + +/// The number of input tokens served from a provider-managed cache. +/// +/// ## Notes +/// +/// The value SHOULD be included in `gen_ai.usage.input_tokens`. +/// +/// # Examples +/// +/// - `50` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS: &str = "gen_ai.usage.cache_read.input_tokens"; + +/// Deprecated, use `gen_ai.usage.output_tokens` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `42` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `gen_ai.usage.output_tokens`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_USAGE_COMPLETION_TOKENS: &str = "gen_ai.usage.completion_tokens"; + +/// The number of tokens used in the GenAI input (prompt). +/// +/// ## Notes +/// +/// This value SHOULD include all types of input tokens, including cached tokens. +/// Instrumentations SHOULD make a best effort to populate this value, using a total +/// provided by the provider when available or, depending on the provider API, +/// by summing different token types parsed from the provider output. +/// +/// # Examples +/// +/// - `100` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_USAGE_INPUT_TOKENS: &str = "gen_ai.usage.input_tokens"; + +/// The number of tokens used in the GenAI response (completion). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `180` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_USAGE_OUTPUT_TOKENS: &str = "gen_ai.usage.output_tokens"; + +/// Deprecated, use `gen_ai.usage.input_tokens` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `42` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `gen_ai.usage.input_tokens`, which has moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_USAGE_PROMPT_TOKENS: &str = "gen_ai.usage.prompt_tokens"; + +/// The number of output tokens used for reasoning (e.g. chain-of-thought, extended thinking). +/// +/// ## Notes +/// +/// The value SHOULD be included in `gen_ai.usage.output_tokens`. +/// +/// # Examples +/// +/// - `50` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_USAGE_REASONING_OUTPUT_TOKENS: &str = "gen_ai.usage.reasoning.output_tokens"; + +/// Human-readable name of the GenAI workflow provided by the application. +/// +/// ## Notes +/// +/// This attribute can be populated in different frameworks eg: name of the first chain in LangChain OR name of the crew in CrewAI. +/// +/// # Examples +/// +/// - `"multi_agent_rag"` +/// - `"customer_support_pipeline"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_WORKFLOW_NAME: &str = "gen_ai.workflow.name"; + +/// Two-letter code representing continent’s name. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const GEO_CONTINENT_CODE: &str = "geo.continent.code"; + +/// Two-letter ISO Country Code ([ISO 3166-1 alpha2](https://wikipedia.org/wiki/ISO_3166-1#Codes)). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"CA"` +#[cfg(feature = "semconv_experimental")] +pub const GEO_COUNTRY_ISO_CODE: &str = "geo.country.iso_code"; + +/// Locality name. Represents the name of a city, town, village, or similar populated place. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Montreal"` +/// - `"Berlin"` +#[cfg(feature = "semconv_experimental")] +pub const GEO_LOCALITY_NAME: &str = "geo.locality.name"; + +/// Latitude of the geo location in [WGS84](https://wikipedia.org/wiki/World_Geodetic_System#WGS84). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `45.505918` +#[cfg(feature = "semconv_experimental")] +pub const GEO_LOCATION_LAT: &str = "geo.location.lat"; + +/// Longitude of the geo location in [WGS84](https://wikipedia.org/wiki/World_Geodetic_System#WGS84). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `-73.61483` +#[cfg(feature = "semconv_experimental")] +pub const GEO_LOCATION_LON: &str = "geo.location.lon"; + +/// Postal code associated with the location. Values appropriate for this field may also be known as a postcode or ZIP code and will vary widely from country to country. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"94040"` +#[cfg(feature = "semconv_experimental")] +pub const GEO_POSTAL_CODE: &str = "geo.postal_code"; + +/// Region ISO code ([ISO 3166-2](https://wikipedia.org/wiki/ISO_3166-2)). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"CA-QC"` +#[cfg(feature = "semconv_experimental")] +pub const GEO_REGION_ISO_CODE: &str = "geo.region.iso_code"; + +/// The detailed state of the CPU. +/// +/// ## Notes +/// +/// Value SHOULD match the specific CPU class reported by the Go runtime under `/cpu/classes/...`. The list of possible values is subject to change with the Go version used. +/// +/// # Examples +/// +/// - `"gc/pause"` +/// - `"gc/mark/assist"` +#[cfg(feature = "semconv_experimental")] +pub const GO_CPU_DETAILED_STATE: &str = "go.cpu.detailed_state"; + +/// The state of the CPU. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"user"` +/// - `"gc"` +#[cfg(feature = "semconv_experimental")] +pub const GO_CPU_STATE: &str = "go.cpu.state"; + +/// The detailed type of memory. +/// +/// ## Notes +/// +/// Value SHOULD match the specific memory class reported by the Go runtime under `/memory/classes/...`. The list of possible values is subject to change with the Go version used. +/// +/// # Examples +/// +/// - `"heap/objects"` +/// - `"heap/free"` +#[cfg(feature = "semconv_experimental")] +pub const GO_MEMORY_DETAILED_TYPE: &str = "go.memory.detailed_type"; + +/// The type of memory. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"other"` +/// - `"stack"` +#[cfg(feature = "semconv_experimental")] +pub const GO_MEMORY_TYPE: &str = "go.memory.type"; + +/// The GraphQL document being executed. +/// +/// ## Notes +/// +/// If instrumentation can reliably identify and redact sensitive information it SHOULD do it. +/// +/// # Examples +/// +/// - `"query findBookById { bookById(id: ?) { name } }"` +#[cfg(feature = "semconv_experimental")] +pub const GRAPHQL_DOCUMENT: &str = "graphql.document"; + +/// The name of the operation being executed. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"findBookById"` +#[cfg(feature = "semconv_experimental")] +pub const GRAPHQL_OPERATION_NAME: &str = "graphql.operation.name"; + +/// The type of the operation being executed. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"query"` +/// - `"mutation"` +/// - `"subscription"` +#[cfg(feature = "semconv_experimental")] +pub const GRAPHQL_OPERATION_TYPE: &str = "graphql.operation.type"; + +/// Unique identifier for the application +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2daa2797-e42b-4624-9322-ec3f968df4da"` +#[cfg(feature = "semconv_experimental")] +pub const HEROKU_APP_ID: &str = "heroku.app.id"; + +/// Commit hash for the current release +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"e6134959463efd8966b20e75b913cafe3f5ec"` +#[cfg(feature = "semconv_experimental")] +pub const HEROKU_RELEASE_COMMIT: &str = "heroku.release.commit"; + +/// Time and date the release was created +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2022-10-23T18:00:42Z"` +#[cfg(feature = "semconv_experimental")] +pub const HEROKU_RELEASE_CREATION_TIMESTAMP: &str = "heroku.release.creation_timestamp"; + +/// The CPU architecture the host system is running on. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const HOST_ARCH: &str = "host.arch"; + +/// The amount of level 2 memory cache available to the processor (in Bytes). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `12288000` +#[cfg(feature = "semconv_experimental")] +pub const HOST_CPU_CACHE_L2_SIZE: &str = "host.cpu.cache.l2.size"; + +/// Family or generation of the CPU. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"6"` +/// - `"PA-RISC 1.1e"` +#[cfg(feature = "semconv_experimental")] +pub const HOST_CPU_FAMILY: &str = "host.cpu.family"; + +/// Model identifier. It provides more granular information about the CPU, distinguishing it from other CPUs within the same family. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"6"` +/// - `"9000/778/B180L"` +#[cfg(feature = "semconv_experimental")] +pub const HOST_CPU_MODEL_ID: &str = "host.cpu.model.id"; + +/// Model designation of the processor. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz"` +#[cfg(feature = "semconv_experimental")] +pub const HOST_CPU_MODEL_NAME: &str = "host.cpu.model.name"; + +/// Stepping or core revisions. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1"` +/// - `"r1p1"` +#[cfg(feature = "semconv_experimental")] +pub const HOST_CPU_STEPPING: &str = "host.cpu.stepping"; + +/// Processor manufacturer identifier. A maximum 12-character string. +/// +/// ## Notes +/// +/// [CPUID](https://wiki.osdev.org/CPUID) command returns the vendor ID string in EBX, EDX and ECX registers. Writing these to memory in this order results in a 12-character string. +/// +/// # Examples +/// +/// - `"GenuineIntel"` +#[cfg(feature = "semconv_experimental")] +pub const HOST_CPU_VENDOR_ID: &str = "host.cpu.vendor.id"; + +/// Unique host ID. For Cloud, this must be the instance_id assigned by the cloud provider. For non-containerized systems, this should be the `machine-id`. See the table below for the sources to use to determine the `machine-id` based on operating system. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"fdbf79e8af94cb7f9e8df36789187052"` +#[cfg(feature = "semconv_experimental")] +pub const HOST_ID: &str = "host.id"; + +/// VM image ID or host OS image ID. For Cloud, this value is from the provider. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"ami-07b06b442921831e5"` +#[cfg(feature = "semconv_experimental")] +pub const HOST_IMAGE_ID: &str = "host.image.id"; + +/// Name of the VM image or OS install the host was instantiated from. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"infra-ami-eks-worker-node-7d4ec78312"` +/// - `"CentOS-8-x86_64-1905"` +#[cfg(feature = "semconv_experimental")] +pub const HOST_IMAGE_NAME: &str = "host.image.name"; + +/// The version string of the VM image or host OS as defined in [Version Attributes](/docs/resource/README.md#version-attributes). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"0.1"` +#[cfg(feature = "semconv_experimental")] +pub const HOST_IMAGE_VERSION: &str = "host.image.version"; + +/// Available IP addresses of the host, excluding loopback interfaces. +/// +/// ## Notes +/// +/// IPv4 Addresses MUST be specified in dotted-quad notation. IPv6 addresses MUST be specified in the [RFC 5952](https://www.rfc-editor.org/rfc/rfc5952.html) format. +/// +/// # Examples +/// +/// - `[ +/// "192.168.1.140", +/// "fe80::abc2:4a28:737a:609e", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const HOST_IP: &str = "host.ip"; + +/// Available MAC addresses of the host, excluding loopback interfaces. +/// +/// ## Notes +/// +/// MAC Addresses MUST be represented in [IEEE RA hexadecimal form](https://standards.ieee.org/wp-content/uploads/import/documents/tutorials/eui.pdf): as hyphen-separated octets in uppercase hexadecimal form from most to least significant. +/// +/// # Examples +/// +/// - `[ +/// "AC-DE-48-23-45-67", +/// "AC-DE-48-23-45-67-01-9F", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const HOST_MAC: &str = "host.mac"; + +/// Name of the host. On Unix systems, it may contain what the hostname command returns, or the fully qualified hostname, or another name specified by the user. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry-test"` +#[cfg(feature = "semconv_experimental")] +pub const HOST_NAME: &str = "host.name"; + +/// Type of host. For Cloud, this must be the machine type. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"n1-standard-1"` +#[cfg(feature = "semconv_experimental")] +pub const HOST_TYPE: &str = "host.type"; + +/// Deprecated, use `client.address` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"83.164.160.102"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `client.address`., reason: renamed, renamed_to: client.address}" +)] +pub const HTTP_CLIENT_IP: &str = "http.client_ip"; + +/// State of the HTTP connection in the HTTP connection pool. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"active"` +/// - `"idle"` +#[cfg(feature = "semconv_experimental")] +pub const HTTP_CONNECTION_STATE: &str = "http.connection.state"; + +/// Deprecated, use `network.protocol.name` and `network.protocol.version` instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Split into `network.protocol.name` and `network.protocol.version`, reason: uncategorized}" +)] +pub const HTTP_FLAVOR: &str = "http.flavor"; + +/// Deprecated, use one of `server.address`, `client.address` or `http.request.header.host` instead, depending on the usage. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"www.example.org"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by one of `server.address`, `client.address` or `http.request.header.host`, depending on the usage.\n, reason: uncategorized}" +)] +pub const HTTP_HOST: &str = "http.host"; + +/// Deprecated, use `http.request.method` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"GET"` +/// - `"POST"` +/// - `"HEAD"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `http.request.method`., reason: renamed, renamed_to: http.request.method}" +)] +pub const HTTP_METHOD: &str = "http.method"; + +/// The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `3495` +#[cfg(feature = "semconv_experimental")] +pub const HTTP_REQUEST_BODY_SIZE: &str = "http.request.body.size"; + +/// HTTP request headers, ``key`` being the normalized HTTP Header name (lowercase), the value being the header values. +/// +/// ## Notes +/// +/// Instrumentations SHOULD require an explicit configuration of which headers are to be captured. +/// Including all request headers can be a security risk - explicit configuration helps avoid leaking sensitive information. +/// +/// The `User-Agent` header is already captured in the `user_agent.original` attribute. +/// Users MAY explicitly configure instrumentations to capture them even though it is not recommended. +/// +/// The attribute value MUST consist of either multiple header values as an array of strings +/// or a single-item array containing a possibly comma-concatenated string, depending on the way +/// the HTTP library provides access to headers. +/// +/// Examples: +/// +/// - A header `Content-Type: application/json` SHOULD be recorded as the `http.request.header.content-type` +/// attribute with value `["application/json"]`. +/// - A header `X-Forwarded-For: 1.2.3.4, 1.2.3.5` SHOULD be recorded as the `http.request.header.x-forwarded-for` +/// attribute with value `["1.2.3.4", "1.2.3.5"]` or `["1.2.3.4, 1.2.3.5"]` depending on the HTTP library. +/// +/// # Examples +/// +/// - `[ +/// "application/json", +/// ]` +/// - `[ +/// "1.2.3.4", +/// "1.2.3.5", +/// ]` +pub const HTTP_REQUEST_HEADER: &str = "http.request.header"; + +/// HTTP request method. +/// +/// ## Notes +/// +/// HTTP request method value SHOULD be "known" to the instrumentation. +/// By default, this convention defines "known" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods), +/// the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html) +/// and the QUERY method defined in [httpbis-safe-method-w-body](https://datatracker.ietf.org/doc/draft-ietf-httpbis-safe-method-w-body/?include_text=1). +/// +/// If the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER`. +/// +/// If the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override +/// the list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named +/// OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods. +/// +/// +/// If this override is done via declarative configuration, then the list MUST be configurable via the `known_methods` property +/// (an array of case-sensitive strings with minimum items 0) under `.instrumentation/development.general.http.client` and/or +/// `.instrumentation/development.general.http.server`. +/// +/// In either case, this list MUST be a full override of the default known methods, +/// it is not a list of known methods in addition to the defaults. +/// +/// HTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly. +/// Instrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent. +/// Tracing instrumentations that do so, MUST also set `http.request.method_original` to the original value. +/// +/// # Examples +/// +/// - `"GET"` +/// - `"POST"` +/// - `"HEAD"` +pub const HTTP_REQUEST_METHOD: &str = "http.request.method"; + +/// Original HTTP method sent by the client in the request line. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"GeT"` +/// - `"ACL"` +/// - `"foo"` +pub const HTTP_REQUEST_METHOD_ORIGINAL: &str = "http.request.method_original"; + +/// The ordinal number of request resending attempt (for any reason, including redirects). +/// +/// ## Notes +/// +/// The resend count SHOULD be updated each time an HTTP request gets resent by the client, regardless of what was the cause of the resending (e.g. redirection, authorization failure, 503 Server Unavailable, network issues, or any other). +/// +/// # Examples +/// +/// - `3` +pub const HTTP_REQUEST_RESEND_COUNT: &str = "http.request.resend_count"; + +/// The total size of the request in bytes. This should be the total number of bytes sent over the wire, including the request line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and request body if any. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1437` +#[cfg(feature = "semconv_experimental")] +pub const HTTP_REQUEST_SIZE: &str = "http.request.size"; + +/// Deprecated, use `http.request.header.content-length` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `3495` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `http.request.header.content-length`., reason: uncategorized}" +)] +pub const HTTP_REQUEST_CONTENT_LENGTH: &str = "http.request_content_length"; + +/// Deprecated, use `http.request.body.size` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `5493` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `http.request.body.size`., reason: renamed, renamed_to: http.request.body.size}" +)] +pub const HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED: &str = + "http.request_content_length_uncompressed"; + +/// The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `3495` +#[cfg(feature = "semconv_experimental")] +pub const HTTP_RESPONSE_BODY_SIZE: &str = "http.response.body.size"; + +/// HTTP response headers, ``key`` being the normalized HTTP Header name (lowercase), the value being the header values. +/// +/// ## Notes +/// +/// Instrumentations SHOULD require an explicit configuration of which headers are to be captured. +/// Including all response headers can be a security risk - explicit configuration helps avoid leaking sensitive information. +/// +/// Users MAY explicitly configure instrumentations to capture them even though it is not recommended. +/// +/// The attribute value MUST consist of either multiple header values as an array of strings +/// or a single-item array containing a possibly comma-concatenated string, depending on the way +/// the HTTP library provides access to headers. +/// +/// Examples: +/// +/// - A header `Content-Type: application/json` header SHOULD be recorded as the `http.request.response.content-type` +/// attribute with value `["application/json"]`. +/// - A header `My-custom-header: abc, def` header SHOULD be recorded as the `http.response.header.my-custom-header` +/// attribute with value `["abc", "def"]` or `["abc, def"]` depending on the HTTP library. +/// +/// # Examples +/// +/// - `[ +/// "application/json", +/// ]` +/// - `[ +/// "abc", +/// "def", +/// ]` +pub const HTTP_RESPONSE_HEADER: &str = "http.response.header"; + +/// The total size of the response in bytes. This should be the total number of bytes sent over the wire, including the status line (HTTP/1.1), framing (HTTP/2 and HTTP/3), headers, and response body and trailers if any. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1437` +#[cfg(feature = "semconv_experimental")] +pub const HTTP_RESPONSE_SIZE: &str = "http.response.size"; + +/// [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `200` +pub const HTTP_RESPONSE_STATUS_CODE: &str = "http.response.status_code"; + +/// Deprecated, use `http.response.header.content-length` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `3495` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `http.response.header.content-length`., reason: uncategorized}" +)] +pub const HTTP_RESPONSE_CONTENT_LENGTH: &str = "http.response_content_length"; + +/// Deprecated, use `http.response.body.size` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `5493` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `http.response.body.size`., reason: renamed, renamed_to: http.response.body.size}" +)] +pub const HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED: &str = + "http.response_content_length_uncompressed"; + +/// The matched route template for the request. This MUST be low-cardinality and include all static path segments, with dynamic path segments represented with placeholders. +/// +/// ## Notes +/// +/// MUST NOT be populated when this is not supported by the HTTP server framework as the route attribute should have low-cardinality and the URI path can NOT substitute it. +/// SHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one. +/// +/// A static path segment is a part of the route template with a fixed, low-cardinality value. This includes literal strings like `/users/` and placeholders that +/// are constrained to a finite, predefined set of values, e.g. `{controller}` or `{action}`. +/// +/// A dynamic path segment is a placeholder for a value that can have high cardinality and is not constrained to a predefined list like static path segments. +/// +/// Instrumentations SHOULD use routing information provided by the corresponding web framework. They SHOULD pick the most precise source of routing information and MAY +/// support custom route formatting. Instrumentations SHOULD document the format and the API used to obtain the route string. +/// +/// # Examples +/// +/// - `"/users/:userID?"` +/// - `"my-controller/my-action/{id?}"` +pub const HTTP_ROUTE: &str = "http.route"; + +/// Deprecated, use `url.scheme` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"http"` +/// - `"https"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Replaced by `url.scheme`., reason: renamed, renamed_to: url.scheme}")] +pub const HTTP_SCHEME: &str = "http.scheme"; + +/// Deprecated, use `server.address` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"example.com"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `server.address`., reason: renamed, renamed_to: server.address}" +)] +pub const HTTP_SERVER_NAME: &str = "http.server_name"; + +/// Deprecated, use `http.response.status_code` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `200` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `http.response.status_code`., reason: renamed, renamed_to: http.response.status_code}" +)] +pub const HTTP_STATUS_CODE: &str = "http.status_code"; + +/// Deprecated, use `url.path` and `url.query` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/search?q=OpenTelemetry#SemConv"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Split to `url.path` and `url.query`., reason: obsoleted}")] +pub const HTTP_TARGET: &str = "http.target"; + +/// Deprecated, use `url.full` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"https://www.foo.bar/search?q=OpenTelemetry#SemConv"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Replaced by `url.full`., reason: renamed, renamed_to: url.full}")] +pub const HTTP_URL: &str = "http.url"; + +/// Deprecated, use `user_agent.original` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"CERN-LineMode/2.15 libwww/2.17b3"` +/// - `"Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `user_agent.original`., reason: renamed, renamed_to: user_agent.original}" +)] +pub const HTTP_USER_AGENT: &str = "http.user_agent"; + +/// Design capacity in Watts-hours or Ampere-hours +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"9.3Ah"` +/// - `"50Wh"` +#[cfg(feature = "semconv_experimental")] +pub const HW_BATTERY_CAPACITY: &str = "hw.battery.capacity"; + +/// Battery [chemistry](https://schemas.dmtf.org/wbem/cim-html/2.31.0/CIM_Battery.html), e.g. Lithium-Ion, Nickel-Cadmium, etc. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Li-ion"` +/// - `"NiMH"` +#[cfg(feature = "semconv_experimental")] +pub const HW_BATTERY_CHEMISTRY: &str = "hw.battery.chemistry"; + +/// The current state of the battery +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const HW_BATTERY_STATE: &str = "hw.battery.state"; + +/// BIOS version of the hardware component +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1.2.3"` +#[cfg(feature = "semconv_experimental")] +pub const HW_BIOS_VERSION: &str = "hw.bios_version"; + +/// Driver version for the hardware component +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"10.2.1-3"` +#[cfg(feature = "semconv_experimental")] +pub const HW_DRIVER_VERSION: &str = "hw.driver_version"; + +/// Type of the enclosure (useful for modular systems) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Computer"` +/// - `"Storage"` +/// - `"Switch"` +#[cfg(feature = "semconv_experimental")] +pub const HW_ENCLOSURE_TYPE: &str = "hw.enclosure.type"; + +/// Firmware version of the hardware component +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2.0.1"` +#[cfg(feature = "semconv_experimental")] +pub const HW_FIRMWARE_VERSION: &str = "hw.firmware_version"; + +/// Type of task the GPU is performing +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const HW_GPU_TASK: &str = "hw.gpu.task"; + +/// An identifier for the hardware component, unique within the monitored host +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"win32battery_battery_testsysa33_1"` +#[cfg(feature = "semconv_experimental")] +pub const HW_ID: &str = "hw.id"; + +/// Type of limit for hardware components +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const HW_LIMIT_TYPE: &str = "hw.limit_type"; + +/// RAID Level of the logical disk +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"RAID0+1"` +/// - `"RAID5"` +/// - `"RAID10"` +#[cfg(feature = "semconv_experimental")] +pub const HW_LOGICAL_DISK_RAID_LEVEL: &str = "hw.logical_disk.raid_level"; + +/// State of the logical disk space usage +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const HW_LOGICAL_DISK_STATE: &str = "hw.logical_disk.state"; + +/// Type of the memory module +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"DDR4"` +/// - `"DDR5"` +/// - `"LPDDR5"` +#[cfg(feature = "semconv_experimental")] +pub const HW_MEMORY_TYPE: &str = "hw.memory.type"; + +/// Descriptive model name of the hardware component +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"PERC H740P"` +/// - `"Intel(R) Core(TM) i7-10700K"` +/// - `"Dell XPS 15 Battery"` +#[cfg(feature = "semconv_experimental")] +pub const HW_MODEL: &str = "hw.model"; + +/// An easily-recognizable name for the hardware component +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"eth0"` +#[cfg(feature = "semconv_experimental")] +pub const HW_NAME: &str = "hw.name"; + +/// Logical addresses of the adapter (e.g. IP address, or WWPN) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "172.16.8.21", +/// "57.11.193.42", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const HW_NETWORK_LOGICAL_ADDRESSES: &str = "hw.network.logical_addresses"; + +/// Physical address of the adapter (e.g. MAC address, or WWNN) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"00-90-F5-E9-7B-36"` +#[cfg(feature = "semconv_experimental")] +pub const HW_NETWORK_PHYSICAL_ADDRESS: &str = "hw.network.physical_address"; + +/// Unique identifier of the parent component (typically the `hw.id` attribute of the enclosure, or disk controller) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"dellStorage_perc_0"` +#[cfg(feature = "semconv_experimental")] +pub const HW_PARENT: &str = "hw.parent"; + +/// [S.M.A.R.T.](https://wikipedia.org/wiki/S.M.A.R.T.) (Self-Monitoring, Analysis, and Reporting Technology) attribute of the physical disk +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Spin Retry Count"` +/// - `"Seek Error Rate"` +/// - `"Raw Read Error Rate"` +#[cfg(feature = "semconv_experimental")] +pub const HW_PHYSICAL_DISK_SMART_ATTRIBUTE: &str = "hw.physical_disk.smart_attribute"; + +/// State of the physical disk endurance utilization +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const HW_PHYSICAL_DISK_STATE: &str = "hw.physical_disk.state"; + +/// Type of the physical disk +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"HDD"` +/// - `"SSD"` +/// - `"10K"` +#[cfg(feature = "semconv_experimental")] +pub const HW_PHYSICAL_DISK_TYPE: &str = "hw.physical_disk.type"; + +/// Location of the sensor +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"cpu0"` +/// - `"ps1"` +/// - `"INLET"` +/// - `"CPU0_DIE"` +/// - `"AMBIENT"` +/// - `"MOTHERBOARD"` +/// - `"PS0 V3_3"` +/// - `"MAIN_12V"` +/// - `"CPU_VCORE"` +#[cfg(feature = "semconv_experimental")] +pub const HW_SENSOR_LOCATION: &str = "hw.sensor_location"; + +/// Serial number of the hardware component +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"CNFCP0123456789"` +#[cfg(feature = "semconv_experimental")] +pub const HW_SERIAL_NUMBER: &str = "hw.serial_number"; + +/// The current state of the component +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const HW_STATE: &str = "hw.state"; + +/// Type of tape drive operation +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const HW_TAPE_DRIVE_OPERATION_TYPE: &str = "hw.tape_drive.operation_type"; + +/// Type of the component +/// +/// ## Notes +/// +/// Describes the category of the hardware component for which `hw.state` is being reported. For example, `hw.type=temperature` along with `hw.state=degraded` would indicate that the temperature of the hardware component has been reported as `degraded` +#[cfg(feature = "semconv_experimental")] +pub const HW_TYPE: &str = "hw.type"; + +/// Vendor name of the hardware component +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Dell"` +/// - `"HP"` +/// - `"Intel"` +/// - `"AMD"` +/// - `"LSI"` +/// - `"Lenovo"` +#[cfg(feature = "semconv_experimental")] +pub const HW_VENDOR: &str = "hw.vendor"; + +/// This attribute represents the state of the application. +/// +/// ## Notes +/// +/// The iOS lifecycle states are defined in the [UIApplicationDelegate documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate), and from which the `OS terminology` column values are derived +#[cfg(feature = "semconv_experimental")] +pub const IOS_APP_STATE: &str = "ios.app.state"; + +/// Deprecated. Use the `ios.app.state` attribute. +/// +/// ## Notes +/// +/// The iOS lifecycle states are defined in the [UIApplicationDelegate documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate), and from which the `OS terminology` column values are derived +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `ios.app.state`., reason: renamed, renamed_to: ios.app.state}" +)] +pub const IOS_STATE: &str = "ios.state"; + +/// Protocol version, as specified in the `jsonrpc` property of the request and its corresponding response. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2.0"` +/// - `"1.0"` +#[cfg(feature = "semconv_experimental")] +pub const JSONRPC_PROTOCOL_VERSION: &str = "jsonrpc.protocol.version"; + +/// A string representation of the `id` property of the request and its corresponding response. +/// +/// ## Notes +/// +/// Under the [JSON-RPC specification](https://www.jsonrpc.org/specification), the `id` property may be a string, number, null, or omitted entirely. When omitted, the request is treated as a notification. Using `null` is not equivalent to omitting the `id`, but it is discouraged. +/// Instrumentations SHOULD NOT capture this attribute when the `id` is `null` or omitted. +/// +/// # Examples +/// +/// - `"10"` +/// - `"request-7"` +#[cfg(feature = "semconv_experimental")] +pub const JSONRPC_REQUEST_ID: &str = "jsonrpc.request.id"; + +/// Name of the buffer pool. +/// +/// ## Notes +/// +/// Pool names are generally obtained via [BufferPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/BufferPoolMXBean.html#getName()). +/// +/// # Examples +/// +/// - `"mapped"` +/// - `"direct"` +#[cfg(feature = "semconv_experimental")] +pub const JVM_BUFFER_POOL_NAME: &str = "jvm.buffer.pool.name"; + +/// Name of the garbage collector action. +/// +/// ## Notes +/// +/// Garbage collector action is generally obtained via [GarbageCollectionNotificationInfo#getGcAction()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcAction()). +/// +/// # Examples +/// +/// - `"end of minor GC"` +/// - `"end of major GC"` +pub const JVM_GC_ACTION: &str = "jvm.gc.action"; + +/// Name of the garbage collector cause. +/// +/// ## Notes +/// +/// Garbage collector cause is generally obtained via [GarbageCollectionNotificationInfo#getGcCause()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcCause()). +/// +/// # Examples +/// +/// - `"System.gc()"` +/// - `"Allocation Failure"` +#[cfg(feature = "semconv_experimental")] +pub const JVM_GC_CAUSE: &str = "jvm.gc.cause"; + +/// Name of the garbage collector. +/// +/// ## Notes +/// +/// Garbage collector name is generally obtained via [GarbageCollectionNotificationInfo#getGcName()](https://docs.oracle.com/en/java/javase/11/docs/api/jdk.management/com/sun/management/GarbageCollectionNotificationInfo.html#getGcName()). +/// +/// # Examples +/// +/// - `"G1 Young Generation"` +/// - `"G1 Old Generation"` +pub const JVM_GC_NAME: &str = "jvm.gc.name"; + +/// Name of the memory pool. +/// +/// ## Notes +/// +/// Pool names are generally obtained via [MemoryPoolMXBean#getName()](https://docs.oracle.com/en/java/javase/11/docs/api/java.management/java/lang/management/MemoryPoolMXBean.html#getName()). +/// +/// # Examples +/// +/// - `"G1 Old Gen"` +/// - `"G1 Eden space"` +/// - `"G1 Survivor Space"` +pub const JVM_MEMORY_POOL_NAME: &str = "jvm.memory.pool.name"; + +/// The type of memory. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"heap"` +/// - `"non_heap"` +pub const JVM_MEMORY_TYPE: &str = "jvm.memory.type"; + +/// Whether the thread is daemon or not. +/// +/// ## Notes +pub const JVM_THREAD_DAEMON: &str = "jvm.thread.daemon"; + +/// State of the thread. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"runnable"` +/// - `"blocked"` +pub const JVM_THREAD_STATE: &str = "jvm.thread.state"; + +/// The name of the cluster. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry-cluster"` +pub const K8S_CLUSTER_NAME: &str = "k8s.cluster.name"; + +/// A pseudo-ID for the cluster, set to the UID of the `kube-system` namespace. +/// +/// ## Notes +/// +/// K8s doesn't have support for obtaining a cluster ID. If this is ever +/// added, we will recommend collecting the `k8s.cluster.uid` through the +/// official APIs. In the meantime, we are able to use the `uid` of the +/// `kube-system` namespace as a proxy for cluster ID. Read on for the +/// rationale. +/// +/// Every object created in a K8s cluster is assigned a distinct UID. The +/// `kube-system` namespace is used by Kubernetes itself and will exist +/// for the lifetime of the cluster. Using the `uid` of the `kube-system` +/// namespace is a reasonable proxy for the K8s ClusterID as it will only +/// change if the cluster is rebuilt. Furthermore, Kubernetes UIDs are +/// UUIDs as standardized by +/// [ISO/IEC 9834-8 and ITU-T X.667](https://www.itu.int/ITU-T/studygroups/com17/oid.html). +/// Which states: +/// +/// \] If generated according to one of the mechanisms defined in Rec. +/// \] ITU-T X.667 | ISO/IEC 9834-8, a UUID is either guaranteed to be +/// \] different from all other UUIDs generated before 3603 A.D., or is +/// \] extremely likely to be different (depending on the mechanism chosen). +/// +/// Therefore, UIDs between clusters should be extremely unlikely to +/// conflict. +/// +/// # Examples +/// +/// - `"218fc5a9-a5f1-4b54-aa05-46717d0ab26d"` +pub const K8S_CLUSTER_UID: &str = "k8s.cluster.uid"; + +/// The type of file system component for ephemeral storage. +/// +/// ## Notes +/// +/// Eviction decisions based on ephemeral-storage resource limits are made based on the total container usage. +/// +/// # Examples +/// +/// - `"rootfs"` +/// - `"logs"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_EPHEMERAL_STORAGE_FS_TYPE: &str = "k8s.container.ephemeral_storage.fs_type"; + +/// The name of the Container from Pod specification, must be unique within a Pod. Container runtime usually uses different globally unique name (`container.name`). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"redis"` +pub const K8S_CONTAINER_NAME: &str = "k8s.container.name"; + +/// Number of times the container was restarted. This attribute can be used to identify a particular container (running or stopped) within a container spec. +/// +/// ## Notes +pub const K8S_CONTAINER_RESTART_COUNT: &str = "k8s.container.restart_count"; + +/// Last terminated reason of the Container. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Evicted"` +/// - `"Error"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_STATUS_LAST_TERMINATED_REASON: &str = + "k8s.container.status.last_terminated_reason"; + +/// The reason for the container state. Corresponds to the `reason` field of the: [K8s ContainerStateWaiting](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatewaiting-v1-core) or [K8s ContainerStateTerminated](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstateterminated-v1-core) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"ContainerCreating"` +/// - `"CrashLoopBackOff"` +/// - `"CreateContainerConfigError"` +/// - `"ErrImagePull"` +/// - `"ImagePullBackOff"` +/// - `"OOMKilled"` +/// - `"Completed"` +/// - `"Error"` +/// - `"ContainerCannotRun"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_STATUS_REASON: &str = "k8s.container.status.reason"; + +/// The state of the container. [K8s ContainerState](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstate-v1-core) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"terminated"` +/// - `"running"` +/// - `"waiting"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_STATUS_STATE: &str = "k8s.container.status.state"; + +/// The cronjob annotation placed on the CronJob, the ``key`` being the annotation name, the value being the annotation value. +/// +/// ## Notes +/// +/// Examples: +/// +/// - An annotation `retries` with value `4` SHOULD be recorded as the +/// `k8s.cronjob.annotation.retries` attribute with value `"4"`. +/// - An annotation `data` with empty string value SHOULD be recorded as +/// the `k8s.cronjob.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"4"` +/// - `""` +pub const K8S_CRONJOB_ANNOTATION: &str = "k8s.cronjob.annotation"; + +/// The label placed on the CronJob, the ``key`` being the label name, the value being the label value. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `type` with value `weekly` SHOULD be recorded as the +/// `k8s.cronjob.label.type` attribute with value `"weekly"`. +/// - A label `automated` with empty string value SHOULD be recorded as +/// the `k8s.cronjob.label.automated` attribute with value `""`. +/// +/// # Examples +/// +/// - `"weekly"` +/// - `""` +pub const K8S_CRONJOB_LABEL: &str = "k8s.cronjob.label"; + +/// The name of the CronJob. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry"` +pub const K8S_CRONJOB_NAME: &str = "k8s.cronjob.name"; + +/// The UID of the CronJob. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +pub const K8S_CRONJOB_UID: &str = "k8s.cronjob.uid"; + +/// The annotation placed on the DaemonSet, the ``key`` being the annotation name, the value being the annotation value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `replicas` with value `1` SHOULD be recorded +/// as the `k8s.daemonset.annotation.replicas` attribute with value `"1"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.daemonset.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"1"` +/// - `""` +pub const K8S_DAEMONSET_ANNOTATION: &str = "k8s.daemonset.annotation"; + +/// The label placed on the DaemonSet, the ``key`` being the label name, the value being the label value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `app` with value `guestbook` SHOULD be recorded +/// as the `k8s.daemonset.label.app` attribute with value `"guestbook"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.daemonset.label.injected` attribute with value `""`. +/// +/// # Examples +/// +/// - `"guestbook"` +/// - `""` +pub const K8S_DAEMONSET_LABEL: &str = "k8s.daemonset.label"; + +/// The name of the DaemonSet. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry"` +pub const K8S_DAEMONSET_NAME: &str = "k8s.daemonset.name"; + +/// The UID of the DaemonSet. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +pub const K8S_DAEMONSET_UID: &str = "k8s.daemonset.uid"; + +/// The annotation placed on the Deployment, the ``key`` being the annotation name, the value being the annotation value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `replicas` with value `1` SHOULD be recorded +/// as the `k8s.deployment.annotation.replicas` attribute with value `"1"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.deployment.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"1"` +/// - `""` +pub const K8S_DEPLOYMENT_ANNOTATION: &str = "k8s.deployment.annotation"; + +/// The label placed on the Deployment, the ``key`` being the label name, the value being the label value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `replicas` with value `0` SHOULD be recorded +/// as the `k8s.deployment.label.app` attribute with value `"guestbook"`. +/// - A label `injected` with empty string value SHOULD be recorded as +/// the `k8s.deployment.label.injected` attribute with value `""`. +/// +/// # Examples +/// +/// - `"guestbook"` +/// - `""` +pub const K8S_DEPLOYMENT_LABEL: &str = "k8s.deployment.label"; + +/// The name of the Deployment. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry"` +pub const K8S_DEPLOYMENT_NAME: &str = "k8s.deployment.name"; + +/// The UID of the Deployment. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +pub const K8S_DEPLOYMENT_UID: &str = "k8s.deployment.uid"; + +/// The type of metric source for the horizontal pod autoscaler. +/// +/// ## Notes +/// +/// This attribute reflects the `type` field of spec.metrics\[\] in the HPA. +/// +/// # Examples +/// +/// - `"Resource"` +/// - `"ContainerResource"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_METRIC_TYPE: &str = "k8s.hpa.metric.type"; + +/// The name of the horizontal pod autoscaler. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_NAME: &str = "k8s.hpa.name"; + +/// The API version of the target resource to scale for the HorizontalPodAutoscaler. +/// +/// ## Notes +/// +/// This maps to the `apiVersion` field in the `scaleTargetRef` of the HPA spec. +/// +/// # Examples +/// +/// - `"apps/v1"` +/// - `"autoscaling/v2"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_SCALETARGETREF_API_VERSION: &str = "k8s.hpa.scaletargetref.api_version"; + +/// The kind of the target resource to scale for the HorizontalPodAutoscaler. +/// +/// ## Notes +/// +/// This maps to the `kind` field in the `scaleTargetRef` of the HPA spec. +/// +/// # Examples +/// +/// - `"Deployment"` +/// - `"StatefulSet"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_SCALETARGETREF_KIND: &str = "k8s.hpa.scaletargetref.kind"; + +/// The name of the target resource to scale for the HorizontalPodAutoscaler. +/// +/// ## Notes +/// +/// This maps to the `name` field in the `scaleTargetRef` of the HPA spec. +/// +/// # Examples +/// +/// - `"my-deployment"` +/// - `"my-statefulset"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_SCALETARGETREF_NAME: &str = "k8s.hpa.scaletargetref.name"; + +/// The UID of the horizontal pod autoscaler. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_UID: &str = "k8s.hpa.uid"; + +/// The size (identifier) of the K8s huge page. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2Mi"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_HUGEPAGE_SIZE: &str = "k8s.hugepage.size"; + +/// The annotation placed on the Job, the ``key`` being the annotation name, the value being the annotation value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `number` with value `1` SHOULD be recorded +/// as the `k8s.job.annotation.number` attribute with value `"1"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.job.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"1"` +/// - `""` +pub const K8S_JOB_ANNOTATION: &str = "k8s.job.annotation"; + +/// The label placed on the Job, the ``key`` being the label name, the value being the label value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `jobtype` with value `ci` SHOULD be recorded +/// as the `k8s.job.label.jobtype` attribute with value `"ci"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.job.label.automated` attribute with value `""`. +/// +/// # Examples +/// +/// - `"ci"` +/// - `""` +pub const K8S_JOB_LABEL: &str = "k8s.job.label"; + +/// The name of the Job. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry"` +pub const K8S_JOB_NAME: &str = "k8s.job.name"; + +/// The UID of the Job. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +pub const K8S_JOB_UID: &str = "k8s.job.uid"; + +/// The annotation placed on the Namespace, the ``key`` being the annotation name, the value being the annotation value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `ttl` with value `0` SHOULD be recorded +/// as the `k8s.namespace.annotation.ttl` attribute with value `"0"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.namespace.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"0"` +/// - `""` +pub const K8S_NAMESPACE_ANNOTATION: &str = "k8s.namespace.annotation"; + +/// The label placed on the Namespace, the ``key`` being the label name, the value being the label value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `kubernetes.io/metadata.name` with value `default` SHOULD be recorded +/// as the `k8s.namespace.label.kubernetes.io/metadata.name` attribute with value `"default"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.namespace.label.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"default"` +/// - `""` +pub const K8S_NAMESPACE_LABEL: &str = "k8s.namespace.label"; + +/// The name of the namespace that the pod is running in. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"default"` +pub const K8S_NAMESPACE_NAME: &str = "k8s.namespace.name"; + +/// The phase of the K8s namespace. +/// +/// ## Notes +/// +/// This attribute aligns with the `phase` field of the +/// [K8s NamespaceStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#namespacestatus-v1-core) +/// +/// # Examples +/// +/// - `"active"` +/// - `"terminating"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_NAMESPACE_PHASE: &str = "k8s.namespace.phase"; + +/// The annotation placed on the Node, the ``key`` being the annotation name, the value being the annotation value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - An annotation `node.alpha.kubernetes.io/ttl` with value `0` SHOULD be recorded as +/// the `k8s.node.annotation.node.alpha.kubernetes.io/ttl` attribute with value `"0"`. +/// - An annotation `data` with empty string value SHOULD be recorded as +/// the `k8s.node.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"0"` +/// - `""` +pub const K8S_NODE_ANNOTATION: &str = "k8s.node.annotation"; + +/// The status of the condition, one of True, False, Unknown. +/// +/// ## Notes +/// +/// This attribute aligns with the `status` field of the +/// [NodeCondition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#nodecondition-v1-core) +/// +/// # Examples +/// +/// - `"true"` +/// - `"false"` +/// - `"unknown"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_CONDITION_STATUS: &str = "k8s.node.condition.status"; + +/// The condition type of a K8s Node. +/// +/// ## Notes +/// +/// K8s Node conditions as described +/// by [K8s documentation](https://kubernetes.io/docs/reference/node/node-status/#condition). +/// +/// This attribute aligns with the `type` field of the +/// [NodeCondition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#nodecondition-v1-core) +/// +/// The set of possible values is not limited to those listed here. Managed Kubernetes environments, +/// or custom controllers MAY introduce additional node condition types. +/// When this occurs, the exact value as reported by the Kubernetes API SHOULD be used. +/// +/// # Examples +/// +/// - `"Ready"` +/// - `"DiskPressure"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_CONDITION_TYPE: &str = "k8s.node.condition.type"; + +/// The label placed on the Node, the ``key`` being the label name, the value being the label value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `kubernetes.io/arch` with value `arm64` SHOULD be recorded +/// as the `k8s.node.label.kubernetes.io/arch` attribute with value `"arm64"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.node.label.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"arm64"` +/// - `""` +pub const K8S_NODE_LABEL: &str = "k8s.node.label"; + +/// The name of the Node. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"node-1"` +pub const K8S_NODE_NAME: &str = "k8s.node.name"; + +/// The name of the system container running on the K8s Node. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"kubelet"` +/// - `"runtime"` +/// - `"pods"` +/// - `"misc"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_SYSTEM_CONTAINER_NAME: &str = "k8s.node.system_container.name"; + +/// The UID of the Node. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1eb3a0c6-0477-4080-a9cb-0cb7db65c6a2"` +pub const K8S_NODE_UID: &str = "k8s.node.uid"; + +/// The annotation placed on the PersistentVolume, the ``key`` being the annotation name, the value being the annotation value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - An annotation `pv.kubernetes.io/provisioned-by` with value `kubernetes.io/aws-ebs` SHOULD be recorded as +/// the `k8s.persistentvolume.annotation.pv.kubernetes.io/provisioned-by` attribute with value `"kubernetes.io/aws-ebs"`. +/// - An annotation `data` with empty string value SHOULD be recorded as +/// the `k8s.persistentvolume.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"kubernetes.io/aws-ebs"` +/// - `""` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUME_ANNOTATION: &str = "k8s.persistentvolume.annotation"; + +/// The label placed on the PersistentVolume, the ``key`` being the label name, the value being the label value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `type` with value `ssd` SHOULD be recorded as +/// the `k8s.persistentvolume.label.type` attribute with value `"ssd"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.persistentvolume.label.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"ssd"` +/// - `""` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUME_LABEL: &str = "k8s.persistentvolume.label"; + +/// The name of the PersistentVolume. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"pv-data-01"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUME_NAME: &str = "k8s.persistentvolume.name"; + +/// The reclaim policy of the PersistentVolume. +/// +/// ## Notes +/// +/// This attribute aligns with the `persistentVolumeReclaimPolicy` field of the +/// [K8s PersistentVolumeSpec](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/#PersistentVolumeSpec). +/// +/// # Examples +/// +/// - `"Delete"` +/// - `"Retain"` +/// - `"Recycle"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUME_RECLAIM_POLICY: &str = "k8s.persistentvolume.reclaim_policy"; + +/// The phase of the PersistentVolume. +/// +/// ## Notes +/// +/// This attribute aligns with the `phase` field of the +/// [K8s PersistentVolumeStatus](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/#PersistentVolumeStatus). +/// +/// # Examples +/// +/// - `"Pending"` +/// - `"Available"` +/// - `"Bound"` +/// - `"Released"` +/// - `"Failed"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUME_STATUS_PHASE: &str = "k8s.persistentvolume.status.phase"; + +/// The UID of the PersistentVolume. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUME_UID: &str = "k8s.persistentvolume.uid"; + +/// The annotation placed on the PersistentVolumeClaim, the ``key`` being the annotation name, the value being the annotation value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - An annotation `volume.beta.kubernetes.io/storage-provisioner` with value `kubernetes.io/aws-ebs` SHOULD be recorded as +/// the `k8s.persistentvolumeclaim.annotation.volume.beta.kubernetes.io/storage-provisioner` attribute with value `"kubernetes.io/aws-ebs"`. +/// - An annotation `data` with empty string value SHOULD be recorded as +/// the `k8s.persistentvolumeclaim.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"kubernetes.io/aws-ebs"` +/// - `""` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUMECLAIM_ANNOTATION: &str = "k8s.persistentvolumeclaim.annotation"; + +/// The label placed on the PersistentVolumeClaim, the ``key`` being the label name, the value being the label value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `app` with value `my-app` SHOULD be recorded as +/// the `k8s.persistentvolumeclaim.label.app` attribute with value `"my-app"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.persistentvolumeclaim.label.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"my-app"` +/// - `""` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUMECLAIM_LABEL: &str = "k8s.persistentvolumeclaim.label"; + +/// The name of the PersistentVolumeClaim. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"pvc-data-01"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUMECLAIM_NAME: &str = "k8s.persistentvolumeclaim.name"; + +/// The phase of the PersistentVolumeClaim. +/// +/// ## Notes +/// +/// This attribute aligns with the `phase` field of the +/// [K8s PersistentVolumeClaimStatus](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-claim-v1/#PersistentVolumeClaimStatus). +/// +/// # Examples +/// +/// - `"Pending"` +/// - `"Bound"` +/// - `"Lost"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUMECLAIM_STATUS_PHASE: &str = "k8s.persistentvolumeclaim.status.phase"; + +/// The UID of the PersistentVolumeClaim. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUMECLAIM_UID: &str = "k8s.persistentvolumeclaim.uid"; + +/// The annotation placed on the Pod, the ``key`` being the annotation name, the value being the annotation value. +/// +/// ## Notes +/// +/// Examples: +/// +/// - An annotation `kubernetes.io/enforce-mountable-secrets` with value `true` SHOULD be recorded as +/// the `k8s.pod.annotation.kubernetes.io/enforce-mountable-secrets` attribute with value `"true"`. +/// - An annotation `mycompany.io/arch` with value `x64` SHOULD be recorded as +/// the `k8s.pod.annotation.mycompany.io/arch` attribute with value `"x64"`. +/// - An annotation `data` with empty string value SHOULD be recorded as +/// the `k8s.pod.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"true"` +/// - `"x64"` +/// - `""` +pub const K8S_POD_ANNOTATION: &str = "k8s.pod.annotation"; + +/// Specifies the hostname of the Pod. +/// +/// ## Notes +/// +/// The K8s Pod spec has an optional hostname field, which can be used to specify a hostname. +/// Refer to [K8s docs](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-hostname-and-subdomain-field) +/// for more information about this field. +/// +/// This attribute aligns with the `hostname` field of the +/// [K8s PodSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podspec-v1-core). +/// +/// # Examples +/// +/// - `"collector-gateway"` +pub const K8S_POD_HOSTNAME: &str = "k8s.pod.hostname"; + +/// IP address allocated to the Pod. +/// +/// ## Notes +/// +/// This attribute aligns with the `podIP` field of the +/// [K8s PodStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podstatus-v1-core). +/// +/// # Examples +/// +/// - `"172.18.0.2"` +pub const K8S_POD_IP: &str = "k8s.pod.ip"; + +/// The label placed on the Pod, the ``key`` being the label name, the value being the label value. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `app` with value `my-app` SHOULD be recorded as +/// the `k8s.pod.label.app` attribute with value `"my-app"`. +/// - A label `mycompany.io/arch` with value `x64` SHOULD be recorded as +/// the `k8s.pod.label.mycompany.io/arch` attribute with value `"x64"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.pod.label.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"my-app"` +/// - `"x64"` +/// - `""` +pub const K8S_POD_LABEL: &str = "k8s.pod.label"; + +/// Deprecated, use `k8s.pod.label` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-app"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.pod.label`., reason: renamed, renamed_to: k8s.pod.label}" +)] +pub const K8S_POD_LABELS: &str = "k8s.pod.labels"; + +/// The name of the Pod. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry-pod-autoconf"` +pub const K8S_POD_NAME: &str = "k8s.pod.name"; + +/// The start timestamp of the Pod. +/// +/// ## Notes +/// +/// Date and time at which the object was acknowledged by the Kubelet. +/// This is before the Kubelet pulled the container image(s) for the pod. +/// +/// This attribute aligns with the `startTime` field of the +/// [K8s PodStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#podstatus-v1-core), +/// in ISO 8601 (RFC 3339 compatible) format. +/// +/// # Examples +/// +/// - `"2025-12-04T08:41:03Z"` +pub const K8S_POD_START_TIME: &str = "k8s.pod.start_time"; + +/// The phase for the pod. Corresponds to the `phase` field of the: [K8s PodStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.33/#podstatus-v1-core) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Pending"` +/// - `"Running"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_STATUS_PHASE: &str = "k8s.pod.status.phase"; + +/// The reason for the pod state. Corresponds to the `reason` field of the: [K8s PodStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.33/#podstatus-v1-core) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Evicted"` +/// - `"NodeAffinity"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_STATUS_REASON: &str = "k8s.pod.status.reason"; + +/// The UID of the Pod. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +pub const K8S_POD_UID: &str = "k8s.pod.uid"; + +/// The annotation placed on the ReplicaSet, the ``key`` being the annotation name, the value being the annotation value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `replicas` with value `0` SHOULD be recorded +/// as the `k8s.replicaset.annotation.replicas` attribute with value `"0"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.replicaset.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"0"` +/// - `""` +pub const K8S_REPLICASET_ANNOTATION: &str = "k8s.replicaset.annotation"; + +/// The label placed on the ReplicaSet, the ``key`` being the label name, the value being the label value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `app` with value `guestbook` SHOULD be recorded +/// as the `k8s.replicaset.label.app` attribute with value `"guestbook"`. +/// - A label `injected` with empty string value SHOULD be recorded as +/// the `k8s.replicaset.label.injected` attribute with value `""`. +/// +/// # Examples +/// +/// - `"guestbook"` +/// - `""` +pub const K8S_REPLICASET_LABEL: &str = "k8s.replicaset.label"; + +/// The name of the ReplicaSet. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry"` +pub const K8S_REPLICASET_NAME: &str = "k8s.replicaset.name"; + +/// The UID of the ReplicaSet. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +pub const K8S_REPLICASET_UID: &str = "k8s.replicaset.uid"; + +/// The name of the replication controller. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_REPLICATIONCONTROLLER_NAME: &str = "k8s.replicationcontroller.name"; + +/// The UID of the replication controller. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_REPLICATIONCONTROLLER_UID: &str = "k8s.replicationcontroller.uid"; + +/// The name of the resource quota. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_NAME: &str = "k8s.resourcequota.name"; + +/// The name of the K8s resource a resource quota defines. +/// +/// ## Notes +/// +/// The value for this attribute can be either the full `count/[resource][.[group]]` string (e.g., count/deployments.apps, count/pods), or, for certain core Kubernetes resources, just the resource name (e.g., pods, services, configmaps). Both forms are supported by Kubernetes for object count quotas. See [Kubernetes Resource Quotas documentation](https://kubernetes.io/docs/concepts/policy/resource-quotas/#quota-on-object-count) for more details. +/// +/// # Examples +/// +/// - `"count/replicationcontrollers"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_RESOURCE_NAME: &str = "k8s.resourcequota.resource_name"; + +/// The UID of the resource quota. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_UID: &str = "k8s.resourcequota.uid"; + +/// The annotation placed on the Service, the ``key`` being the annotation name, the value being the annotation value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - An annotation `prometheus.io/scrape` with value `true` SHOULD be recorded as +/// the `k8s.service.annotation.prometheus.io/scrape` attribute with value `"true"`. +/// - An annotation `data` with empty string value SHOULD be recorded as +/// the `k8s.service.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"true"` +/// - `""` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_ANNOTATION: &str = "k8s.service.annotation"; + +/// The address type of the service endpoint. +/// +/// ## Notes +/// +/// The network address family or type of the endpoint. +/// This attribute aligns with the `addressType` field of the +/// [K8s EndpointSlice](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/). +/// It is used to differentiate metrics when a Service is backed by multiple address types +/// (e.g., in dual-stack clusters). +/// +/// # Examples +/// +/// - `"IPv4"` +/// - `"IPv6"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_ENDPOINT_ADDRESS_TYPE: &str = "k8s.service.endpoint.address_type"; + +/// The condition of the service endpoint. +/// +/// ## Notes +/// +/// The current operational condition of the service endpoint. +/// An endpoint can have multiple conditions set at once (e.g., both `serving` and `terminating` during rollout). +/// This attribute aligns with the condition fields in the [K8s EndpointSlice](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/). +/// +/// # Examples +/// +/// - `"ready"` +/// - `"serving"` +/// - `"terminating"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_ENDPOINT_CONDITION: &str = "k8s.service.endpoint.condition"; + +/// The zone of the service endpoint. +/// +/// ## Notes +/// +/// The zone where the endpoint is located, typically corresponding to a failure domain. +/// This attribute aligns with the `zone` field of endpoints in the +/// [K8s EndpointSlice](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/). +/// It enables zone-aware monitoring of service endpoint distribution and supports +/// features like [Topology Aware Routing](https://kubernetes.io/docs/concepts/services-networking/topology-aware-routing/). +/// +/// If the zone is not populated (e.g., nodes without the `topology.kubernetes.io/zone` label), +/// the attribute value will be an empty string. +/// +/// # Examples +/// +/// - `"us-east-1a"` +/// - `"us-west-2b"` +/// - `"zone-a"` +/// - `""` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_ENDPOINT_ZONE: &str = "k8s.service.endpoint.zone"; + +/// The label placed on the Service, the ``key`` being the label name, the value being the label value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `app` with value `my-service` SHOULD be recorded as +/// the `k8s.service.label.app` attribute with value `"my-service"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.service.label.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"my-service"` +/// - `""` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_LABEL: &str = "k8s.service.label"; + +/// The name of the Service. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-service"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_NAME: &str = "k8s.service.name"; + +/// Whether the Service publishes not-ready endpoints. +/// +/// ## Notes +/// +/// Whether the Service is configured to publish endpoints before the pods are ready. +/// This attribute is typically used to indicate that a Service (such as a headless +/// Service for a StatefulSet) allows peer discovery before pods pass their readiness probes. +/// It aligns with the `publishNotReadyAddresses` field of the +/// [K8s ServiceSpec](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/#ServiceSpec). +/// +/// # Examples +/// +/// - `true` +/// - `false` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_PUBLISH_NOT_READY_ADDRESSES: &str = "k8s.service.publish_not_ready_addresses"; + +/// The selector key-value pair placed on the Service, the ``key`` being the selector key, the value being the selector value. +/// +/// ## Notes +/// +/// These selectors are used to correlate with pod labels. Each selector key-value pair becomes a separate attribute. +/// +/// Examples: +/// +/// - A selector `app=my-app` SHOULD be recorded as +/// the `k8s.service.selector.app` attribute with value `"my-app"`. +/// - A selector `version=v1` SHOULD be recorded as +/// the `k8s.service.selector.version` attribute with value `"v1"`. +/// +/// # Examples +/// +/// - `"my-app"` +/// - `"v1"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_SELECTOR: &str = "k8s.service.selector"; + +/// The traffic distribution policy for the Service. +/// +/// ## Notes +/// +/// Specifies how traffic is distributed to endpoints for this Service. +/// This attribute aligns with the `trafficDistribution` field of the +/// [K8s ServiceSpec](https://kubernetes.io/docs/reference/networking/virtual-ips/#traffic-distribution). +/// Known values include `PreferSameZone` (prefer endpoints in the same zone as the client) and +/// `PreferSameNode` (prefer endpoints on the same node, fallback to same zone, then cluster-wide). +/// If this field is not set on the Service, the attribute SHOULD NOT be emitted. +/// When not set, Kubernetes distributes traffic evenly across all endpoints cluster-wide. +/// +/// # Examples +/// +/// - `"PreferSameZone"` +/// - `"PreferSameNode"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_TRAFFIC_DISTRIBUTION: &str = "k8s.service.traffic_distribution"; + +/// The type of the Kubernetes Service. +/// +/// ## Notes +/// +/// This attribute aligns with the `type` field of the +/// [K8s ServiceSpec](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/#ServiceSpec). +/// +/// # Examples +/// +/// - `"ClusterIP"` +/// - `"NodePort"` +/// - `"LoadBalancer"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_TYPE: &str = "k8s.service.type"; + +/// The UID of the Service. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_UID: &str = "k8s.service.uid"; + +/// The annotation placed on the StatefulSet, the ``key`` being the annotation name, the value being the annotation value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `replicas` with value `1` SHOULD be recorded +/// as the `k8s.statefulset.annotation.replicas` attribute with value `"1"`. +/// - A label `data` with empty string value SHOULD be recorded as +/// the `k8s.statefulset.annotation.data` attribute with value `""`. +/// +/// # Examples +/// +/// - `"1"` +/// - `""` +pub const K8S_STATEFULSET_ANNOTATION: &str = "k8s.statefulset.annotation"; + +/// The label placed on the StatefulSet, the ``key`` being the label name, the value being the label value, even if the value is empty. +/// +/// ## Notes +/// +/// Examples: +/// +/// - A label `replicas` with value `0` SHOULD be recorded +/// as the `k8s.statefulset.label.app` attribute with value `"guestbook"`. +/// - A label `injected` with empty string value SHOULD be recorded as +/// the `k8s.statefulset.label.injected` attribute with value `""`. +/// +/// # Examples +/// +/// - `"guestbook"` +/// - `""` +pub const K8S_STATEFULSET_LABEL: &str = "k8s.statefulset.label"; + +/// The name of the StatefulSet. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry"` +pub const K8S_STATEFULSET_NAME: &str = "k8s.statefulset.name"; + +/// The UID of the StatefulSet. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +pub const K8S_STATEFULSET_UID: &str = "k8s.statefulset.uid"; + +/// The name of K8s [StorageClass](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#storageclass-v1-storage-k8s-io) object. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"gold.storageclass.storage.k8s.io"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_STORAGECLASS_NAME: &str = "k8s.storageclass.name"; + +/// The name of the K8s volume. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"volume0"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_VOLUME_NAME: &str = "k8s.volume.name"; + +/// The type of the K8s volume. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"emptyDir"` +/// - `"persistentVolumeClaim"` +#[cfg(feature = "semconv_experimental")] +pub const K8S_VOLUME_TYPE: &str = "k8s.volume.type"; + +/// The Linux Slab memory state +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"reclaimable"` +/// - `"unreclaimable"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.memory.linux.slab.state`., reason: renamed, renamed_to: system.memory.linux.slab.state}" +)] +pub const LINUX_MEMORY_SLAB_STATE: &str = "linux.memory.slab.state"; + +/// The basename of the file. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"audit.log"` +#[cfg(feature = "semconv_experimental")] +pub const LOG_FILE_NAME: &str = "log.file.name"; + +/// The basename of the file, with symlinks resolved. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"uuid.log"` +#[cfg(feature = "semconv_experimental")] +pub const LOG_FILE_NAME_RESOLVED: &str = "log.file.name_resolved"; + +/// The full path to the file. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/var/log/mysql/audit.log"` +#[cfg(feature = "semconv_experimental")] +pub const LOG_FILE_PATH: &str = "log.file.path"; + +/// The full path to the file, with symlinks resolved. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/var/lib/docker/uuid.log"` +#[cfg(feature = "semconv_experimental")] +pub const LOG_FILE_PATH_RESOLVED: &str = "log.file.path_resolved"; + +/// The stream associated with the log. See below for a list of well-known values. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const LOG_IOSTREAM: &str = "log.iostream"; + +/// The complete original Log Record. +/// +/// ## Notes +/// +/// This value MAY be added when processing a Log Record which was originally transmitted as a string or equivalent data type AND the Body field of the Log Record does not contain the same value. (e.g. a syslog or a log record read from a file.) +/// +/// # Examples +/// +/// - `"77 <86>1 2015-08-06T21:58:59.694Z 192.168.2.133 inactive - - - Something happened"` +/// - `"[INFO] 8/3/24 12:34:56 Something happened"` +#[cfg(feature = "semconv_experimental")] +pub const LOG_RECORD_ORIGINAL: &str = "log.record.original"; + +/// A unique identifier for the Log Record. +/// +/// ## Notes +/// +/// If an id is provided, other log records with the same id will be considered duplicates and can be removed safely. This means, that two distinguishable log records MUST have different values. +/// The id MAY be an [Universally Unique Lexicographically Sortable Identifier (ULID)](https://github.com/ulid/spec), but other identifiers (e.g. UUID) may be used as needed. +/// +/// # Examples +/// +/// - `"01ARZ3NDEKTSV4RRFFQ69G5FAV"` +#[cfg(feature = "semconv_experimental")] +pub const LOG_RECORD_UID: &str = "log.record.uid"; + +/// Name of the logical partition that hosts a systems with a mainframe operating system. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"LPAR01"` +#[cfg(feature = "semconv_experimental")] +pub const MAINFRAME_LPAR_NAME: &str = "mainframe.lpar.name"; + +/// The name of the request or notification method. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const MCP_METHOD_NAME: &str = "mcp.method.name"; + +/// The [version](https://modelcontextprotocol.io/specification/versioning) of the Model Context Protocol used. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2025-06-18"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const MCP_PROTOCOL_VERSION: &str = "mcp.protocol.version"; + +/// The value of the resource uri. +/// +/// ## Notes +/// +/// This is a URI of the resource provided in the following requests or notifications: `resources/read`, `resources/subscribe`, `resources/unsubscribe`, or `notifications/resources/updated`. +/// +/// # Examples +/// +/// - `"postgres://database/customers/schema"` +/// - `"file:///home/user/documents/report.pdf"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const MCP_RESOURCE_URI: &str = "mcp.resource.uri"; + +/// Identifies [MCP session](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#session-management). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"191c4850af6c49e08843a3f6c80e5046"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const MCP_SESSION_ID: &str = "mcp.session.id"; + +/// Deprecated, no replacement at this time. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Deprecated, no replacement at this time., reason: obsoleted}")] +pub const MESSAGE_COMPRESSED_SIZE: &str = "message.compressed_size"; + +/// Deprecated, no replacement at this time. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Deprecated, no replacement at this time., reason: obsoleted}")] +pub const MESSAGE_ID: &str = "message.id"; + +/// Deprecated, no replacement at this time. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Deprecated, no replacement at this time., reason: obsoleted}")] +pub const MESSAGE_TYPE: &str = "message.type"; + +/// Deprecated, no replacement at this time. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Deprecated, no replacement at this time., reason: obsoleted}")] +pub const MESSAGE_UNCOMPRESSED_SIZE: &str = "message.uncompressed_size"; + +/// The number of messages sent, received, or processed in the scope of the batching operation. +/// +/// ## Notes +/// +/// Instrumentations SHOULD NOT set `messaging.batch.message_count` on spans that operate with a single message. When a messaging client library supports both batch and single-message API for the same operation, instrumentations SHOULD use `messaging.batch.message_count` for batching APIs and SHOULD NOT use it for single-message APIs. +/// +/// # Examples +/// +/// - `0` +/// - `1` +/// - `2` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_BATCH_MESSAGE_COUNT: &str = "messaging.batch.message_count"; + +/// A unique identifier for the client that consumes or produces a message. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"client-5"` +/// - `"myhost@8742@s8083jm"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_CLIENT_ID: &str = "messaging.client.id"; + +/// The name of the consumer group with which a consumer is associated. +/// +/// ## Notes +/// +/// Semantic conventions for individual messaging systems SHOULD document whether `messaging.consumer.group.name` is applicable and what it means in the context of that system. +/// +/// # Examples +/// +/// - `"my-group"` +/// - `"indexer"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_CONSUMER_GROUP_NAME: &str = "messaging.consumer.group.name"; + +/// A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name). +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_DESTINATION_ANONYMOUS: &str = "messaging.destination.anonymous"; + +/// The message destination name +/// +/// ## Notes +/// +/// Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If +/// the broker doesn't have such notion, the destination name SHOULD uniquely identify the broker. +/// +/// # Examples +/// +/// - `"MyQueue"` +/// - `"MyTopic"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_DESTINATION_NAME: &str = "messaging.destination.name"; + +/// The identifier of the partition messages are sent to or received from, unique within the `messaging.destination.name`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_DESTINATION_PARTITION_ID: &str = "messaging.destination.partition.id"; + +/// The name of the destination subscription from which a message is consumed. +/// +/// ## Notes +/// +/// Semantic conventions for individual messaging systems SHOULD document whether `messaging.destination.subscription.name` is applicable and what it means in the context of that system. +/// +/// # Examples +/// +/// - `"subscription-a"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_DESTINATION_SUBSCRIPTION_NAME: &str = "messaging.destination.subscription.name"; + +/// Low cardinality representation of the messaging destination name +/// +/// ## Notes +/// +/// Destination names could be constructed from templates. An example would be a destination name involving a user name or product id. Although the destination name in this case is of high cardinality, the underlying template is of low cardinality and can be effectively used for grouping and aggregation. +/// +/// # Examples +/// +/// - `"/customers/{customerId}"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_DESTINATION_TEMPLATE: &str = "messaging.destination.template"; + +/// A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_DESTINATION_TEMPORARY: &str = "messaging.destination.temporary"; + +/// Deprecated, no replacement at this time. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed. No replacement at this time., reason: obsoleted}")] +pub const MESSAGING_DESTINATION_PUBLISH_ANONYMOUS: &str = "messaging.destination_publish.anonymous"; + +/// Deprecated, no replacement at this time. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"MyQueue"` +/// - `"MyTopic"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed. No replacement at this time., reason: obsoleted}")] +pub const MESSAGING_DESTINATION_PUBLISH_NAME: &str = "messaging.destination_publish.name"; + +/// Deprecated, use `messaging.consumer.group.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"$Default"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.consumer.group.name`., reason: renamed, renamed_to: messaging.consumer.group.name}" +)] +pub const MESSAGING_EVENTHUBS_CONSUMER_GROUP: &str = "messaging.eventhubs.consumer.group"; + +/// The UTC epoch seconds at which the message has been accepted and stored in the entity. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1701393730` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_EVENTHUBS_MESSAGE_ENQUEUED_TIME: &str = + "messaging.eventhubs.message.enqueued_time"; + +/// The ack deadline in seconds set for the modify ack deadline request. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `10` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_GCP_PUBSUB_MESSAGE_ACK_DEADLINE: &str = + "messaging.gcp_pubsub.message.ack_deadline"; + +/// The ack id for a given message. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"ack_id"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_GCP_PUBSUB_MESSAGE_ACK_ID: &str = "messaging.gcp_pubsub.message.ack_id"; + +/// The delivery attempt for a given message. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `2` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_GCP_PUBSUB_MESSAGE_DELIVERY_ATTEMPT: &str = + "messaging.gcp_pubsub.message.delivery_attempt"; + +/// The ordering key for a given message. If the attribute is not present, the message does not have an ordering key. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"ordering_key"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_GCP_PUBSUB_MESSAGE_ORDERING_KEY: &str = + "messaging.gcp_pubsub.message.ordering_key"; + +/// Deprecated, use `messaging.consumer.group.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-group"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.consumer.group.name`., reason: renamed, renamed_to: messaging.consumer.group.name}" +)] +pub const MESSAGING_KAFKA_CONSUMER_GROUP: &str = "messaging.kafka.consumer.group"; + +/// Deprecated, use `messaging.destination.partition.id` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `2` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Record string representation of the partition id in `messaging.destination.partition.id` attribute., reason: uncategorized}" +)] +pub const MESSAGING_KAFKA_DESTINATION_PARTITION: &str = "messaging.kafka.destination.partition"; + +/// Message keys in Kafka are used for grouping alike messages to ensure they're processed on the same partition. They differ from `messaging.message.id` in that they're not unique. If the key is `null`, the attribute MUST NOT be set. +/// +/// ## Notes +/// +/// If the key type is not string, it's string representation has to be supplied for the attribute. If the key has no unambiguous, canonical string form, don't include its value. +/// +/// # Examples +/// +/// - `"myKey"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_KAFKA_MESSAGE_KEY: &str = "messaging.kafka.message.key"; + +/// Deprecated, use `messaging.kafka.offset` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `42` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.kafka.offset`., reason: renamed, renamed_to: messaging.kafka.offset}" +)] +pub const MESSAGING_KAFKA_MESSAGE_OFFSET: &str = "messaging.kafka.message.offset"; + +/// A boolean that is true if the message is a tombstone. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_KAFKA_MESSAGE_TOMBSTONE: &str = "messaging.kafka.message.tombstone"; + +/// The offset of a record in the corresponding Kafka partition. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `42` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_KAFKA_OFFSET: &str = "messaging.kafka.offset"; + +/// The size of the message body in bytes. +/// +/// ## Notes +/// +/// This can refer to both the compressed or uncompressed body size. If both sizes are known, the uncompressed +/// body size should be used. +/// +/// # Examples +/// +/// - `1439` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_MESSAGE_BODY_SIZE: &str = "messaging.message.body.size"; + +/// The conversation ID identifying the conversation to which the message belongs, represented as a string. Sometimes called "Correlation ID". +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"MyConversationId"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_MESSAGE_CONVERSATION_ID: &str = "messaging.message.conversation_id"; + +/// The size of the message body and metadata in bytes. +/// +/// ## Notes +/// +/// This can refer to both the compressed or uncompressed size. If both sizes are known, the uncompressed +/// size should be used. +/// +/// # Examples +/// +/// - `2738` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_MESSAGE_ENVELOPE_SIZE: &str = "messaging.message.envelope.size"; + +/// A value used by the messaging system as an identifier for the message, represented as a string. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"452a7c7c7c7048c2f887f61572b18fc2"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_MESSAGE_ID: &str = "messaging.message.id"; + +/// Deprecated, use `messaging.operation.type` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"publish"` +/// - `"create"` +/// - `"process"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.operation.type`., reason: renamed, renamed_to: messaging.operation.type}" +)] +pub const MESSAGING_OPERATION: &str = "messaging.operation"; + +/// The system-specific name of the messaging operation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"ack"` +/// - `"nack"` +/// - `"send"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_OPERATION_NAME: &str = "messaging.operation.name"; + +/// A string identifying the type of the messaging operation. +/// +/// ## Notes +/// +/// If a custom value is used, it MUST be of low cardinality +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_OPERATION_TYPE: &str = "messaging.operation.type"; + +/// RabbitMQ message routing key. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"myKey"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY: &str = + "messaging.rabbitmq.destination.routing_key"; + +/// RabbitMQ message delivery tag +/// +/// ## Notes +/// +/// # Examples +/// +/// - `123` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_RABBITMQ_MESSAGE_DELIVERY_TAG: &str = "messaging.rabbitmq.message.delivery_tag"; + +/// Deprecated, use `messaging.consumer.group.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"myConsumerGroup"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.consumer.group.name` on the consumer spans. No replacement for producer spans.\n, reason: uncategorized}" +)] +pub const MESSAGING_ROCKETMQ_CLIENT_GROUP: &str = "messaging.rocketmq.client_group"; + +/// Model of message consumption. This only applies to consumer spans. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_ROCKETMQ_CONSUMPTION_MODEL: &str = "messaging.rocketmq.consumption_model"; + +/// The delay time level for delay message, which determines the message delay time. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `3` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_ROCKETMQ_MESSAGE_DELAY_TIME_LEVEL: &str = + "messaging.rocketmq.message.delay_time_level"; + +/// The timestamp in milliseconds that the delay message is expected to be delivered to consumer. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1665987217045` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_ROCKETMQ_MESSAGE_DELIVERY_TIMESTAMP: &str = + "messaging.rocketmq.message.delivery_timestamp"; + +/// It is essential for FIFO message. Messages that belong to the same message group are always processed one by one within the same consumer group. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"myMessageGroup"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_ROCKETMQ_MESSAGE_GROUP: &str = "messaging.rocketmq.message.group"; + +/// Key(s) of message, another way to mark message besides message id. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "keyA", +/// "keyB", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_ROCKETMQ_MESSAGE_KEYS: &str = "messaging.rocketmq.message.keys"; + +/// The secondary classifier of message besides topic. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"tagA"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_ROCKETMQ_MESSAGE_TAG: &str = "messaging.rocketmq.message.tag"; + +/// Type of message. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_ROCKETMQ_MESSAGE_TYPE: &str = "messaging.rocketmq.message.type"; + +/// Namespace of RocketMQ resources, resources in different namespaces are individual. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"myNamespace"` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_ROCKETMQ_NAMESPACE: &str = "messaging.rocketmq.namespace"; + +/// Deprecated, use `messaging.destination.subscription.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"subscription-a"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.destination.subscription.name`., reason: renamed, renamed_to: messaging.destination.subscription.name}" +)] +pub const MESSAGING_SERVICEBUS_DESTINATION_SUBSCRIPTION_NAME: &str = + "messaging.servicebus.destination.subscription_name"; + +/// Describes the [settlement type](https://learn.microsoft.com/azure/service-bus-messaging/message-transfers-locks-settlement#peeklock). +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_SERVICEBUS_DISPOSITION_STATUS: &str = "messaging.servicebus.disposition_status"; + +/// Number of deliveries that have been attempted for this message. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `2` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_SERVICEBUS_MESSAGE_DELIVERY_COUNT: &str = + "messaging.servicebus.message.delivery_count"; + +/// The UTC epoch seconds at which the message has been accepted and stored in the entity. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1701393730` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_SERVICEBUS_MESSAGE_ENQUEUED_TIME: &str = + "messaging.servicebus.message.enqueued_time"; + +/// The messaging system as identified by the client instrumentation. +/// +/// ## Notes +/// +/// The actual messaging system may differ from the one known by the client. For example, when using Kafka client libraries to communicate with Azure Event Hubs, the `messaging.system` is set to `kafka` based on the instrumentation's best knowledge +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_SYSTEM: &str = "messaging.system"; + +/// Deprecated, use `network.local.address`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"192.168.0.1"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `network.local.address`., reason: renamed, renamed_to: network.local.address}" +)] +pub const NET_HOST_IP: &str = "net.host.ip"; + +/// Deprecated, use `server.address`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"example.com"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `server.address`., reason: renamed, renamed_to: server.address}" +)] +pub const NET_HOST_NAME: &str = "net.host.name"; + +/// Deprecated, use `server.port`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `8080` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Replaced by `server.port`., reason: renamed, renamed_to: server.port}")] +pub const NET_HOST_PORT: &str = "net.host.port"; + +/// Deprecated, use `network.peer.address`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"127.0.0.1"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `network.peer.address`., reason: renamed, renamed_to: network.peer.address}" +)] +pub const NET_PEER_IP: &str = "net.peer.ip"; + +/// Deprecated, use `server.address` on client spans and `client.address` on server spans. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"example.com"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `server.address` on client spans and `client.address` on server spans., reason: uncategorized}" +)] +pub const NET_PEER_NAME: &str = "net.peer.name"; + +/// Deprecated, use `server.port` on client spans and `client.port` on server spans. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `8080` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `server.port` on client spans and `client.port` on server spans., reason: uncategorized}" +)] +pub const NET_PEER_PORT: &str = "net.peer.port"; + +/// Deprecated, use `network.protocol.name`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"amqp"` +/// - `"http"` +/// - `"mqtt"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `network.protocol.name`., reason: renamed, renamed_to: network.protocol.name}" +)] +pub const NET_PROTOCOL_NAME: &str = "net.protocol.name"; + +/// Deprecated, use `network.protocol.version`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"3.1.1"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `network.protocol.version`., reason: renamed, renamed_to: network.protocol.version}" +)] +pub const NET_PROTOCOL_VERSION: &str = "net.protocol.version"; + +/// Deprecated, use `network.transport` and `network.type`. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Split to `network.transport` and `network.type`., reason: uncategorized}" +)] +pub const NET_SOCK_FAMILY: &str = "net.sock.family"; + +/// Deprecated, use `network.local.address`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/var/my.sock"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `network.local.address`., reason: renamed, renamed_to: network.local.address}" +)] +pub const NET_SOCK_HOST_ADDR: &str = "net.sock.host.addr"; + +/// Deprecated, use `network.local.port`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `8080` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `network.local.port`., reason: renamed, renamed_to: network.local.port}" +)] +pub const NET_SOCK_HOST_PORT: &str = "net.sock.host.port"; + +/// Deprecated, use `network.peer.address`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"192.168.0.1"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `network.peer.address`., reason: renamed, renamed_to: network.peer.address}" +)] +pub const NET_SOCK_PEER_ADDR: &str = "net.sock.peer.addr"; + +/// Deprecated, no replacement at this time. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/var/my.sock"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed. No replacement at this time., reason: obsoleted}")] +pub const NET_SOCK_PEER_NAME: &str = "net.sock.peer.name"; + +/// Deprecated, use `network.peer.port`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `65531` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `network.peer.port`., reason: renamed, renamed_to: network.peer.port}" +)] +pub const NET_SOCK_PEER_PORT: &str = "net.sock.peer.port"; + +/// Deprecated, use `network.transport`. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `network.transport`., reason: renamed, renamed_to: network.transport}" +)] +pub const NET_TRANSPORT: &str = "net.transport"; + +/// The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"DE"` +#[cfg(feature = "semconv_experimental")] +pub const NETWORK_CARRIER_ICC: &str = "network.carrier.icc"; + +/// The mobile carrier country code. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"310"` +#[cfg(feature = "semconv_experimental")] +pub const NETWORK_CARRIER_MCC: &str = "network.carrier.mcc"; + +/// The mobile carrier network code. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"001"` +#[cfg(feature = "semconv_experimental")] +pub const NETWORK_CARRIER_MNC: &str = "network.carrier.mnc"; + +/// The name of the mobile carrier. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"sprint"` +#[cfg(feature = "semconv_experimental")] +pub const NETWORK_CARRIER_NAME: &str = "network.carrier.name"; + +/// The state of network connection +/// +/// ## Notes +/// +/// Connection states are defined as part of the [rfc9293](https://datatracker.ietf.org/doc/html/rfc9293#section-3.3.2) +/// +/// # Examples +/// +/// - `"close_wait"` +#[cfg(feature = "semconv_experimental")] +pub const NETWORK_CONNECTION_STATE: &str = "network.connection.state"; + +/// This describes more details regarding the connection.type. It may be the type of cell technology connection, but it could be used for describing details about a wifi connection. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"LTE"` +#[cfg(feature = "semconv_experimental")] +pub const NETWORK_CONNECTION_SUBTYPE: &str = "network.connection.subtype"; + +/// The internet connection type. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"wifi"` +#[cfg(feature = "semconv_experimental")] +pub const NETWORK_CONNECTION_TYPE: &str = "network.connection.type"; + +/// The network interface name. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"lo"` +/// - `"eth0"` +#[cfg(feature = "semconv_experimental")] +pub const NETWORK_INTERFACE_NAME: &str = "network.interface.name"; + +/// The network IO operation direction. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"transmit"` +#[cfg(feature = "semconv_experimental")] +pub const NETWORK_IO_DIRECTION: &str = "network.io.direction"; + +/// Local address of the network connection - IP address or Unix domain socket name. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"10.1.2.80"` +/// - `"/tmp/my.sock"` +pub const NETWORK_LOCAL_ADDRESS: &str = "network.local.address"; + +/// Local port number of the network connection. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `65123` +pub const NETWORK_LOCAL_PORT: &str = "network.local.port"; + +/// Peer address of the network connection - IP address or Unix domain socket name. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"10.1.2.80"` +/// - `"/tmp/my.sock"` +pub const NETWORK_PEER_ADDRESS: &str = "network.peer.address"; + +/// Peer port number of the network connection. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `65123` +pub const NETWORK_PEER_PORT: &str = "network.peer.port"; + +/// [OSI application layer](https://wikipedia.org/wiki/Application_layer) or non-OSI equivalent. +/// +/// ## Notes +/// +/// The value SHOULD be normalized to lowercase. +/// +/// # Examples +/// +/// - `"amqp"` +/// - `"http"` +/// - `"mqtt"` +pub const NETWORK_PROTOCOL_NAME: &str = "network.protocol.name"; + +/// The actual version of the protocol used for network communication. +/// +/// ## Notes +/// +/// If protocol version is subject to negotiation (for example using [ALPN](https://www.rfc-editor.org/rfc/rfc7301.html)), this attribute SHOULD be set to the negotiated version. If the actual protocol version is not known, this attribute SHOULD NOT be set. +/// +/// # Examples +/// +/// - `"1.1"` +/// - `"2"` +pub const NETWORK_PROTOCOL_VERSION: &str = "network.protocol.version"; + +/// [OSI transport layer](https://wikipedia.org/wiki/Transport_layer) or [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication). +/// +/// ## Notes +/// +/// The value SHOULD be normalized to lowercase. +/// +/// Consider always setting the transport when setting a port number, since +/// a port number is ambiguous without knowing the transport. For example +/// different processes could be listening on TCP port 12345 and UDP port 12345. +/// +/// # Examples +/// +/// - `"tcp"` +/// - `"udp"` +pub const NETWORK_TRANSPORT: &str = "network.transport"; + +/// [OSI network layer](https://wikipedia.org/wiki/Network_layer) or non-OSI equivalent. +/// +/// ## Notes +/// +/// The value SHOULD be normalized to lowercase. +/// +/// # Examples +/// +/// - `"ipv4"` +/// - `"ipv6"` +pub const NETWORK_TYPE: &str = "network.type"; + +/// NFSv4+ operation name. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"OPEN"` +/// - `"READ"` +/// - `"GETATTR"` +#[cfg(feature = "semconv_experimental")] +pub const NFS_OPERATION_NAME: &str = "nfs.operation.name"; + +/// Linux: one of "hit" (NFSD_STATS_RC_HITS), "miss" (NFSD_STATS_RC_MISSES), or "nocache" (NFSD_STATS_RC_NOCACHE -- uncacheable) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"hit"` +#[cfg(feature = "semconv_experimental")] +pub const NFS_SERVER_REPCACHE_STATUS: &str = "nfs.server.repcache.status"; + +/// The state of event loop time. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const NODEJS_EVENTLOOP_STATE: &str = "nodejs.eventloop.state"; + +/// The digest of the OCI image manifest. For container images specifically is the digest by which the container image is known. +/// +/// ## Notes +/// +/// Follows [OCI Image Manifest Specification](https://github.com/opencontainers/image-spec/blob/main/manifest.md), and specifically the [Digest property](https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests). +/// An example can be found in [Example Image Manifest](https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-image-manifest). +/// +/// # Examples +/// +/// - `"sha256:e4ca62c0d62f3e886e684806dfe9d4e0cda60d54986898173c1083856cfda0f4"` +#[cfg(feature = "semconv_experimental")] +pub const OCI_MANIFEST_DIGEST: &str = "oci.manifest.digest"; + +/// ONC/Sun RPC procedure name. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"OPEN"` +/// - `"READ"` +/// - `"GETATTR"` +#[cfg(feature = "semconv_experimental")] +pub const ONC_RPC_PROCEDURE_NAME: &str = "onc_rpc.procedure.name"; + +/// ONC/Sun RPC procedure number. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const ONC_RPC_PROCEDURE_NUMBER: &str = "onc_rpc.procedure.number"; + +/// ONC/Sun RPC program name. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"portmapper"` +/// - `"nfs"` +#[cfg(feature = "semconv_experimental")] +pub const ONC_RPC_PROGRAM_NAME: &str = "onc_rpc.program.name"; + +/// ONC/Sun RPC program version. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const ONC_RPC_VERSION: &str = "onc_rpc.version"; + +/// The type of OpenAI API being used. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const OPENAI_API_TYPE: &str = "openai.api.type"; + +/// The service tier requested. May be a specific tier, default, or auto. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"auto"` +/// - `"default"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const OPENAI_REQUEST_SERVICE_TIER: &str = "openai.request.service_tier"; + +/// The service tier used for the response. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"scale"` +/// - `"default"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const OPENAI_RESPONSE_SERVICE_TIER: &str = "openai.response.service_tier"; + +/// A fingerprint to track any eventual change in the Generative AI environment. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"fp_44709d6fcb"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const OPENAI_RESPONSE_SYSTEM_FINGERPRINT: &str = "openai.response.system_fingerprint"; + +/// The name of the cluster quota. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry"` +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_NAME: &str = "openshift.clusterquota.name"; + +/// The UID of the cluster quota. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"275ecb36-5aa8-4c2a-9c47-d8bb681b9aff"` +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_UID: &str = "openshift.clusterquota.uid"; + +/// Parent-child Reference type +/// +/// ## Notes +/// +/// The causal relationship between a child Span and a parent Span +#[cfg(feature = "semconv_experimental")] +pub const OPENTRACING_REF_TYPE: &str = "opentracing.ref_type"; + +/// The database domain associated with the connection. +/// +/// ## Notes +/// +/// This attribute SHOULD be set to the value of the `DB_DOMAIN` initialization parameter, +/// as exposed in `v$parameter`. `DB_DOMAIN` defines the domain portion of the global +/// database name and SHOULD be configured when a database is, or may become, part of a +/// distributed environment. Its value consists of one or more valid identifiers +/// (alphanumeric ASCII characters) separated by periods. +/// +/// # Examples +/// +/// - `"example.com"` +/// - `"corp.internal"` +/// - `"prod.db.local"` +#[cfg(feature = "semconv_experimental")] +pub const ORACLE_DB_DOMAIN: &str = "oracle.db.domain"; + +/// The instance name associated with the connection in an Oracle Real Application Clusters environment. +/// +/// ## Notes +/// +/// There can be multiple instances associated with a single database service. It indicates the +/// unique instance name to which the connection is currently bound. For non-RAC databases, this value +/// defaults to the `oracle.db.name`. +/// +/// # Examples +/// +/// - `"ORCL1"` +/// - `"ORCL2"` +/// - `"ORCL3"` +#[cfg(feature = "semconv_experimental")] +pub const ORACLE_DB_INSTANCE_NAME: &str = "oracle.db.instance.name"; + +/// The database name associated with the connection. +/// +/// ## Notes +/// +/// This attribute SHOULD be set to the value of the parameter `DB_NAME` exposed in `v$parameter`. +/// +/// # Examples +/// +/// - `"ORCL1"` +/// - `"FREE"` +#[cfg(feature = "semconv_experimental")] +pub const ORACLE_DB_NAME: &str = "oracle.db.name"; + +/// The pluggable database (PDB) name associated with the connection. +/// +/// ## Notes +/// +/// This attribute SHOULD reflect the PDB that the session is currently connected to. +/// If instrumentation cannot reliably obtain the active PDB name for each operation +/// without issuing an additional query (such as `SELECT SYS_CONTEXT`), it is +/// RECOMMENDED to fall back to the PDB name specified at connection establishment. +/// +/// # Examples +/// +/// - `"PDB1"` +/// - `"FREEPDB"` +#[cfg(feature = "semconv_experimental")] +pub const ORACLE_DB_PDB: &str = "oracle.db.pdb"; + +/// The service name currently associated with the database connection. +/// +/// ## Notes +/// +/// The effective service name for a connection can change during its lifetime, +/// for example after executing sql, `ALTER SESSION`. If an instrumentation cannot reliably +/// obtain the current service name for each operation without issuing an additional +/// query (such as `SELECT SYS_CONTEXT`), it is RECOMMENDED to fall back to the +/// service name originally provided at connection establishment. +/// +/// # Examples +/// +/// - `"order-processing-service"` +/// - `"db_low.adb.oraclecloud.com"` +/// - `"db_high.adb.oraclecloud.com"` +#[cfg(feature = "semconv_experimental")] +pub const ORACLE_DB_SERVICE: &str = "oracle.db.service"; + +/// The OCI realm identifier that indicates the isolated partition in which the tenancy and its resources reside. +/// +/// ## Notes +/// +/// See [OCI documentation on realms](https://docs.oracle.com/iaas/Content/General/Concepts/regions.htm) +/// +/// # Examples +/// +/// - `"oc1"` +/// - `"oc2"` +#[cfg(feature = "semconv_experimental")] +pub const ORACLE_CLOUD_REALM: &str = "oracle_cloud.realm"; + +/// Unique identifier for a particular build or compilation of the operating system. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"TQ3C.230805.001.B2"` +/// - `"20E247"` +/// - `"22621"` +#[cfg(feature = "semconv_experimental")] +pub const OS_BUILD_ID: &str = "os.build_id"; + +/// Human readable (not intended to be parsed) OS version information, like e.g. reported by `ver` or `lsb_release -a` commands. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Microsoft Windows [Version 10.0.18363.778]"` +/// - `"Ubuntu 18.04.1 LTS"` +#[cfg(feature = "semconv_experimental")] +pub const OS_DESCRIPTION: &str = "os.description"; + +/// Human readable operating system name. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"iOS"` +/// - `"Android"` +/// - `"Ubuntu"` +#[cfg(feature = "semconv_experimental")] +pub const OS_NAME: &str = "os.name"; + +/// The operating system type. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const OS_TYPE: &str = "os.type"; + +/// The version string of the operating system as defined in [Version Attributes](/docs/resource/README.md#version-attributes). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"14.2.1"` +/// - `"18.04.1"` +#[cfg(feature = "semconv_experimental")] +pub const OS_VERSION: &str = "os.version"; + +/// A name uniquely identifying the instance of the OpenTelemetry component within its containing SDK instance. +/// +/// ## Notes +/// +/// Implementations SHOULD ensure a low cardinality for this attribute, even across application or SDK restarts. +/// E.g. implementations MUST NOT use UUIDs as values for this attribute. +/// +/// Implementations MAY achieve these goals by following a `[otel.component.type]/[instance-counter]` pattern, e.g. `batching_span_processor/0`. +/// Hereby `otel.component.type` refers to the corresponding attribute value of the component. +/// +/// The value of `instance-counter` MAY be automatically assigned by the component and uniqueness within the enclosing SDK instance MUST be guaranteed. +/// For example, `[instance-counter]` MAY be implemented by using a monotonically increasing counter (starting with `0`), which is incremented every time an +/// instance of the given component type is started. +/// +/// With this implementation, for example the first Batching Span Processor would have `batching_span_processor/0` +/// as `otel.component.name`, the second one `batching_span_processor/1` and so on. +/// These values will therefore be reused in the case of an application restart. +/// +/// # Examples +/// +/// - `"otlp_grpc_span_exporter/0"` +/// - `"custom-name"` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_COMPONENT_NAME: &str = "otel.component.name"; + +/// A name identifying the type of the OpenTelemetry component. +/// +/// ## Notes +/// +/// If none of the standardized values apply, implementations SHOULD use the language-defined name of the type. +/// E.g. for Java the fully qualified classname SHOULD be used in this case. +/// +/// # Examples +/// +/// - `"batching_span_processor"` +/// - `"com.example.MySpanExporter"` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_COMPONENT_TYPE: &str = "otel.component.type"; + +/// Identifies the class / type of event. +/// +/// ## Notes +/// +/// This attribute SHOULD be used by non-OTLP exporters when destination does not support `EventName` or equivalent field. This attribute MAY be used by applications using existing logging libraries so that it can be used to set the `EventName` field by Collector or SDK components. +/// +/// # Examples +/// +/// - `"browser.mouse.click"` +/// - `"device.app.lifecycle"` +pub const OTEL_EVENT_NAME: &str = "otel.event.name"; + +/// Deprecated. Use the `otel.scope.name` attribute +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"io.opentelemetry.contrib.mongodb"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `otel.scope.name`., reason: renamed, renamed_to: otel.scope.name}" +)] +pub const OTEL_LIBRARY_NAME: &str = "otel.library.name"; + +/// Deprecated. Use the `otel.scope.version` attribute. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1.0.0"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `otel.scope.version`., reason: renamed, renamed_to: otel.scope.version}" +)] +pub const OTEL_LIBRARY_VERSION: &str = "otel.library.version"; + +/// The name of the instrumentation scope - (`InstrumentationScope.Name` in OTLP). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"io.opentelemetry.contrib.mongodb"` +pub const OTEL_SCOPE_NAME: &str = "otel.scope.name"; + +/// The schema URL of the instrumentation scope. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"https://opentelemetry.io/schemas/1.31.0"` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SCOPE_SCHEMA_URL: &str = "otel.scope.schema_url"; + +/// The version of the instrumentation scope - (`InstrumentationScope.Version` in OTLP). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1.0.0"` +pub const OTEL_SCOPE_VERSION: &str = "otel.scope.version"; + +/// Determines whether the span has a parent span, and if so, [whether it is a remote parent](https://opentelemetry.io/docs/specs/otel/trace/api/#isremote) +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SPAN_PARENT_ORIGIN: &str = "otel.span.parent.origin"; + +/// The result value of the sampler for this span +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SPAN_SAMPLING_RESULT: &str = "otel.span.sampling_result"; + +/// Name of the code, either "OK" or "ERROR". MUST NOT be set if the status code is UNSET. +/// +/// ## Notes +pub const OTEL_STATUS_CODE: &str = "otel.status_code"; + +/// Description of the Status if it has a value, otherwise not set. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"resource not found"` +pub const OTEL_STATUS_DESCRIPTION: &str = "otel.status_description"; + +/// Deprecated, use `db.client.connection.state` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"idle"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.state`., reason: renamed, renamed_to: db.client.connection.state}" +)] +pub const STATE: &str = "state"; + +/// The [`service.name`](/docs/resource/README.md#service) of the remote service. SHOULD be equal to the actual `service.name` resource attribute of the remote service if any. +/// +/// ## Notes +/// +/// Examples of `peer.service` that users may specify: +/// +/// - A Redis cache of auth tokens as `peer.service="AuthTokenCache"`. +/// - A gRPC service `rpc.service="io.opentelemetry.AuthService"` may be hosted in both a gateway, `peer.service="ExternalApiService"` and a backend, `peer.service="AuthService"`. +/// +/// # Examples +/// +/// - `"AuthTokenCache"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `service.peer.name`., reason: renamed, renamed_to: service.peer.name}" +)] +pub const PEER_SERVICE: &str = "peer.service"; + +/// Deprecated, use `db.client.connection.pool.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"myDataSource"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.pool.name`., reason: renamed, renamed_to: db.client.connection.pool.name}" +)] +pub const POOL_NAME: &str = "pool.name"; + +/// Provides an indication that multiple symbols map to this location's address, for example due to identical code folding by the linker. In that case the line information represents one of the multiple symbols. This field must be recomputed when the symbolization state of the profile changes. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const PPROF_LOCATION_IS_FOLDED: &str = "pprof.location.is_folded"; + +/// Indicates that there are filenames related to this mapping. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const PPROF_MAPPING_HAS_FILENAMES: &str = "pprof.mapping.has_filenames"; + +/// Indicates that there are functions related to this mapping. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const PPROF_MAPPING_HAS_FUNCTIONS: &str = "pprof.mapping.has_functions"; + +/// Indicates that there are inline frames related to this mapping. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const PPROF_MAPPING_HAS_INLINE_FRAMES: &str = "pprof.mapping.has_inline_frames"; + +/// Indicates that there are line numbers related to this mapping. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const PPROF_MAPPING_HAS_LINE_NUMBERS: &str = "pprof.mapping.has_line_numbers"; + +/// Free-form text associated with the profile. This field should not be used to store any machine-readable information, it is only for human-friendly content. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "hello world", +/// "bazinga", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const PPROF_PROFILE_COMMENT: &str = "pprof.profile.comment"; + +/// Documentation link for this profile type. +/// +/// ## Notes +/// +/// The URL must be absolute and may be missing if the profile was generated by code that did not supply a link +/// +/// # Examples +/// +/// - `"http://pprof.example.com/cpu-profile.html"` +#[cfg(feature = "semconv_experimental")] +pub const PPROF_PROFILE_DOC_URL: &str = "pprof.profile.doc_url"; + +/// Frames with Function.function_name fully matching the regexp will be dropped from the samples, along with their successors. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/foobar/"` +#[cfg(feature = "semconv_experimental")] +pub const PPROF_PROFILE_DROP_FRAMES: &str = "pprof.profile.drop_frames"; + +/// Frames with Function.function_name fully matching the regexp will be kept, even if it matches drop_frames. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/bazinga/"` +#[cfg(feature = "semconv_experimental")] +pub const PPROF_PROFILE_KEEP_FRAMES: &str = "pprof.profile.keep_frames"; + +/// Records the pprof's default_sample_type in the original profile. Not set if the default sample type was missing. +/// +/// ## Notes +/// +/// This attribute, if present, MUST be set at the scope level (resource_profiles\[\].scope_profiles\[\].scope.attributes\[\]). +/// +/// # Examples +/// +/// - `"cpu"` +#[cfg(feature = "semconv_experimental")] +pub const PPROF_SCOPE_DEFAULT_SAMPLE_TYPE: &str = "pprof.scope.default_sample_type"; + +/// Records the indexes of the sample types in the original profile. +/// +/// ## Notes +/// +/// This attribute, if present, MUST be set at the scope level (resource_profiles\[\].scope_profiles\[\].scope.attributes\[\]). +/// +/// # Examples +/// +/// - `[ +/// 3, +/// 0, +/// 1, +/// 2, +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const PPROF_SCOPE_SAMPLE_TYPE_ORDER: &str = "pprof.scope.sample_type_order"; + +/// Length of the process.command_args array +/// +/// ## Notes +/// +/// This field can be useful for querying or performing bucket analysis on how many arguments were provided to start a process. More arguments may be an indication of suspicious activity. +/// +/// # Examples +/// +/// - `4` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_ARGS_COUNT: &str = "process.args_count"; + +/// The command used to launch the process (i.e. the command name). On Linux based systems, can be set to the zeroth string in `proc/[pid]/cmdline`. On Windows, can be set to the first parameter extracted from `GetCommandLineW`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"cmd/otelcol"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_COMMAND: &str = "process.command"; + +/// All the command arguments (including the command/executable itself) as received by the process. On Linux-based systems (and some other Unixoid systems supporting procfs), can be set according to the list of null-delimited strings extracted from `proc/[pid]/cmdline`. For libc-based executables, this would be the full argv vector passed to `main`. SHOULD NOT be collected by default unless there is sanitization that excludes sensitive data. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "cmd/otecol", +/// "--config=config.yaml", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_COMMAND_ARGS: &str = "process.command_args"; + +/// The full command used to launch the process as a single string representing the full command. On Windows, can be set to the result of `GetCommandLineW`. Do not set this if you have to assemble it just for monitoring; use `process.command_args` instead. SHOULD NOT be collected by default unless there is sanitization that excludes sensitive data. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"C:\\cmd\\otecol --config=\"my directory\\config.yaml\""` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_COMMAND_LINE: &str = "process.command_line"; + +/// Specifies whether the context switches for this data point were voluntary or involuntary. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_CONTEXT_SWITCH_TYPE: &str = "process.context_switch.type"; + +/// Deprecated, use `cpu.mode` instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Replaced by `cpu.mode`., reason: renamed, renamed_to: cpu.mode}")] +pub const PROCESS_CPU_STATE: &str = "process.cpu.state"; + +/// The date and time the process was created, in ISO 8601 format. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2023-11-21T09:25:34.853Z"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_CREATION_TIME: &str = "process.creation.time"; + +/// Process environment variables, ``key`` being the environment variable name, the value being the environment variable value. +/// +/// ## Notes +/// +/// Examples: +/// +/// - an environment variable `USER` with value `"ubuntu"` SHOULD be recorded +/// as the `process.environment_variable.USER` attribute with value `"ubuntu"`. +/// - an environment variable `PATH` with value `"/usr/local/bin:/usr/bin"` +/// SHOULD be recorded as the `process.environment_variable.PATH` attribute +/// with value `"/usr/local/bin:/usr/bin"`. +/// +/// # Examples +/// +/// - `"ubuntu"` +/// - `"/usr/local/bin:/usr/bin"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_ENVIRONMENT_VARIABLE: &str = "process.environment_variable"; + +/// The GNU build ID as found in the `.note.gnu.build-id` ELF section (hex string). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"c89b11207f6479603b0d49bf291c092c2b719293"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_EXECUTABLE_BUILD_ID_GNU: &str = "process.executable.build_id.gnu"; + +/// The Go build ID as retrieved by `go tool buildid `. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"foh3mEXu7BLZjsN9pOwG/kATcXlYVCDEFouRMQed_/WwRFB1hPo9LBkekthSPG/x8hMC8emW2cCjXD0_1aY"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_EXECUTABLE_BUILD_ID_GO: &str = "process.executable.build_id.go"; + +/// Deterministic build ID for executables. +/// +/// ## Notes +/// +/// GNU and Go build IDs may be stripped or unavailable in some environments +/// (e.g., Alpine Linux, Docker images). This attribute provides a deterministic +/// build ID computed by hashing the first and last 4096 bytes of the file +/// along with its length: +/// ```text +/// Input ← Concat(File[:4096], File[-4096:], BigEndianUInt64(Len(File))) +/// Digest ← SHA256(Input) +/// BuildID ← Digest[:16] +/// ``` +/// +/// The result is the first 16 bytes (128 bits) of the SHA256 digest, +/// represented as a hex string. +/// +/// # Examples +/// +/// - `"600DCAFE4A110000F2BF38C493F5FB92"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_EXECUTABLE_BUILD_ID_HTLHASH: &str = "process.executable.build_id.htlhash"; + +/// "Deprecated, use `process.executable.build_id.htlhash` instead." +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"600DCAFE4A110000F2BF38C493F5FB92"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `process.executable.build_id.htlhash`., reason: renamed, renamed_to: process.executable.build_id.htlhash}" +)] +pub const PROCESS_EXECUTABLE_BUILD_ID_PROFILING: &str = "process.executable.build_id.profiling"; + +/// The name of the process executable. On Linux based systems, this SHOULD be set to the base name of the target of `/proc/[pid]/exe`. On Windows, this SHOULD be set to the base name of `GetProcessImageFileNameW`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"otelcol"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_EXECUTABLE_NAME: &str = "process.executable.name"; + +/// The full path to the process executable. On Linux based systems, can be set to the target of `proc/[pid]/exe`. On Windows, can be set to the result of `GetProcessImageFileNameW`. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/usr/bin/cmd/otelcol"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_EXECUTABLE_PATH: &str = "process.executable.path"; + +/// The exit code of the process. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `127` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_EXIT_CODE: &str = "process.exit.code"; + +/// The date and time the process exited, in ISO 8601 format. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2023-11-21T09:26:12.315Z"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_EXIT_TIME: &str = "process.exit.time"; + +/// The PID of the process's group leader. This is also the process group ID (PGID) of the process. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `23` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_GROUP_LEADER_PID: &str = "process.group_leader.pid"; + +/// Whether the process is connected to an interactive shell. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_INTERACTIVE: &str = "process.interactive"; + +/// The control group associated with the process. +/// +/// ## Notes +/// +/// Control groups (cgroups) are a kernel feature used to organize and manage process resources. This attribute provides the path(s) to the cgroup(s) associated with the process, which should match the contents of the [/proc/\[PID\]/cgroup](https://man7.org/linux/man-pages/man7/cgroups.7.html) file. +/// +/// # Examples +/// +/// - `"1:name=systemd:/user.slice/user-1000.slice/session-3.scope"` +/// - `"0::/user.slice/user-1000.slice/user@1000.service/tmux-spawn-0267755b-4639-4a27-90ed-f19f88e53748.scope"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_LINUX_CGROUP: &str = "process.linux.cgroup"; + +/// The username of the user that owns the process. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"root"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_OWNER: &str = "process.owner"; + +/// Deprecated, use `system.paging.fault.type` instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.paging.fault.type`., reason: renamed, renamed_to: system.paging.fault.type}" +)] +pub const PROCESS_PAGING_FAULT_TYPE: &str = "process.paging.fault_type"; + +/// Parent Process identifier (PPID). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `111` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_PARENT_PID: &str = "process.parent_pid"; + +/// Process identifier (PID). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1234` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_PID: &str = "process.pid"; + +/// The real user ID (RUID) of the process. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1000` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_REAL_USER_ID: &str = "process.real_user.id"; + +/// The username of the real user of the process. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"operator"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_REAL_USER_NAME: &str = "process.real_user.name"; + +/// An additional description about the runtime of the process, for example a specific vendor customization of the runtime environment. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Eclipse OpenJ9 Eclipse OpenJ9 VM openj9-0.21.0"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_RUNTIME_DESCRIPTION: &str = "process.runtime.description"; + +/// The name of the runtime of this process. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"OpenJDK Runtime Environment"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_RUNTIME_NAME: &str = "process.runtime.name"; + +/// The version of the runtime of this process, as returned by the runtime without modification. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"14.0.2"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_RUNTIME_VERSION: &str = "process.runtime.version"; + +/// The saved user ID (SUID) of the process. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1002` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_SAVED_USER_ID: &str = "process.saved_user.id"; + +/// The username of the saved user. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"operator"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_SAVED_USER_NAME: &str = "process.saved_user.name"; + +/// The PID of the process's session leader. This is also the session ID (SID) of the process. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `14` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_SESSION_LEADER_PID: &str = "process.session_leader.pid"; + +/// The process state, e.g., [Linux Process State Codes](https://man7.org/linux/man-pages/man1/ps.1.html#PROCESS_STATE_CODES) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"running"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_STATE: &str = "process.state"; + +/// Process title (proctitle) +/// +/// ## Notes +/// +/// In many Unix-like systems, process title (proctitle), is the string that represents the name or command line of a running process, displayed by system monitoring tools like ps, top, and htop. +/// +/// # Examples +/// +/// - `"cat /etc/hostname"` +/// - `"xfce4-session"` +/// - `"bash"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_TITLE: &str = "process.title"; + +/// The effective user ID (EUID) of the process. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1001` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_USER_ID: &str = "process.user.id"; + +/// The username of the effective user of the process. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"root"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_USER_NAME: &str = "process.user.name"; + +/// Virtual process identifier. +/// +/// ## Notes +/// +/// The process ID within a PID namespace. This is not necessarily unique across all processes on the host but it is unique within the process namespace that the process exists within. +/// +/// # Examples +/// +/// - `12` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_VPID: &str = "process.vpid"; + +/// The working directory of the process. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/root"` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_WORKING_DIRECTORY: &str = "process.working_directory"; + +/// Describes the interpreter or compiler of a single frame. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"cpython"` +#[cfg(feature = "semconv_experimental")] +pub const PROFILE_FRAME_TYPE: &str = "profile.frame.type"; + +/// Deprecated, use `rpc.response.status_code` attribute instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `rpc.response.status_code`., reason: renamed, renamed_to: rpc.response.status_code}" +)] +pub const RPC_CONNECT_RPC_ERROR_CODE: &str = "rpc.connect_rpc.error_code"; + +/// Deprecated, use `rpc.request.metadata` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "1.2.3.4", +/// "1.2.3.5", +/// ]` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `rpc.request.metadata`., reason: renamed, renamed_to: rpc.request.metadata}" +)] +pub const RPC_CONNECT_RPC_REQUEST_METADATA: &str = "rpc.connect_rpc.request.metadata"; + +/// Deprecated, use `rpc.response.metadata` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "attribute_value", +/// ]` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `rpc.response.metadata`., reason: renamed, renamed_to: rpc.response.metadata}" +)] +pub const RPC_CONNECT_RPC_RESPONSE_METADATA: &str = "rpc.connect_rpc.response.metadata"; + +/// Deprecated, use `rpc.request.metadata` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "1.2.3.4", +/// "1.2.3.5", +/// ]` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `rpc.request.metadata`., reason: renamed, renamed_to: rpc.request.metadata}" +)] +pub const RPC_GRPC_REQUEST_METADATA: &str = "rpc.grpc.request.metadata"; + +/// Deprecated, use `rpc.response.metadata` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "attribute_value", +/// ]` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `rpc.response.metadata`., reason: renamed, renamed_to: rpc.response.metadata}" +)] +pub const RPC_GRPC_RESPONSE_METADATA: &str = "rpc.grpc.response.metadata"; + +/// Deprecated, use string representation on the `rpc.response.status_code` attribute instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Use string representation of the gRPC status code on the `rpc.response.status_code` attribute., reason: uncategorized}" +)] +pub const RPC_GRPC_STATUS_CODE: &str = "rpc.grpc.status_code"; + +/// Deprecated, use string representation on the `rpc.response.status_code` attribute instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `-32700` +/// - `100` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Use string representation of the error code on the `rpc.response.status_code` attribute., reason: uncategorized}" +)] +pub const RPC_JSONRPC_ERROR_CODE: &str = "rpc.jsonrpc.error_code"; + +/// Deprecated, use the span status description when reporting JSON-RPC spans. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Parse error"` +/// - `"User already exists"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Use the span status description when reporting JSON-RPC spans., reason: uncategorized}" +)] +pub const RPC_JSONRPC_ERROR_MESSAGE: &str = "rpc.jsonrpc.error_message"; + +/// Deprecated, use `jsonrpc.request.id` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"10"` +/// - `"request-7"` +/// - `""` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `jsonrpc.request.id`., reason: renamed, renamed_to: jsonrpc.request.id}" +)] +pub const RPC_JSONRPC_REQUEST_ID: &str = "rpc.jsonrpc.request_id"; + +/// Deprecated, use `jsonrpc.protocol.version` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2.0"` +/// - `"1.0"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `jsonrpc.protocol.version`., reason: renamed, renamed_to: jsonrpc.protocol.version}" +)] +pub const RPC_JSONRPC_VERSION: &str = "rpc.jsonrpc.version"; + +/// Compressed size of the message in bytes. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Deprecated, no replacement at this time., reason: obsoleted}")] +pub const RPC_MESSAGE_COMPRESSED_SIZE: &str = "rpc.message.compressed_size"; + +/// MUST be calculated as two different counters starting from `1` one for sent messages and one for received message. +/// +/// ## Notes +/// +/// This way we guarantee that the values will be consistent between different implementations +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Deprecated, no replacement at this time., reason: obsoleted}")] +pub const RPC_MESSAGE_ID: &str = "rpc.message.id"; + +/// Whether this is a received or sent message. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Deprecated, no replacement at this time., reason: obsoleted}")] +pub const RPC_MESSAGE_TYPE: &str = "rpc.message.type"; + +/// Uncompressed size of the message in bytes. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Deprecated, no replacement at this time., reason: obsoleted}")] +pub const RPC_MESSAGE_UNCOMPRESSED_SIZE: &str = "rpc.message.uncompressed_size"; + +/// The fully-qualified logical name of the method from the RPC interface perspective. +/// +/// ## Notes +/// +/// The method name MAY have unbounded cardinality in edge or error cases. +/// +/// Some RPC frameworks or libraries provide a fixed set of recognized methods +/// for client stubs and server implementations. Instrumentations for such +/// frameworks MUST set this attribute to the original method name only +/// when the method is recognized by the framework or library. +/// +/// When the method is not recognized, for example, when the server receives +/// a request for a method that is not predefined on the server, or when +/// instrumentation is not able to reliably detect if the method is predefined, +/// the attribute MUST be set to `_OTHER`. In such cases, tracing +/// instrumentations MUST also set `rpc.method_original` attribute to +/// the original method value. +/// +/// If the RPC instrumentation could end up converting valid RPC methods to +/// `_OTHER`, then it SHOULD provide a way to configure the list of recognized +/// RPC methods. +/// +/// The `rpc.method` can be different from the name of any implementing +/// method/function. +/// The `code.function.name` attribute may be used to record the fully-qualified +/// method actually executing the call on the server side, or the +/// RPC client stub method on the client side. +/// +/// # Examples +/// +/// - `"com.example.ExampleService/exampleMethod"` +/// - `"EchoService/Echo"` +/// - `"_OTHER"` +#[cfg(feature = "semconv_experimental")] +pub const RPC_METHOD: &str = "rpc.method"; + +/// The original name of the method used by the client. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"com.myservice.EchoService/catchAll"` +/// - `"com.myservice.EchoService/unknownMethod"` +/// - `"InvalidMethod"` +#[cfg(feature = "semconv_experimental")] +pub const RPC_METHOD_ORIGINAL: &str = "rpc.method_original"; + +/// RPC request metadata, ``key`` being the normalized RPC metadata key (lowercase), the value being the metadata values. +/// +/// ## Notes +/// +/// Instrumentations SHOULD require an explicit configuration of which metadata values are to be captured. +/// Including all request metadata values can be a security risk - explicit configuration helps avoid leaking sensitive information. +/// +/// For example, a property `my-custom-key` with value `["1.2.3.4", "1.2.3.5"]` SHOULD be recorded as +/// `rpc.request.metadata.my-custom-key` attribute with value `["1.2.3.4", "1.2.3.5"]` +/// +/// # Examples +/// +/// - `[ +/// "1.2.3.4", +/// "1.2.3.5", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const RPC_REQUEST_METADATA: &str = "rpc.request.metadata"; + +/// RPC response metadata, ``key`` being the normalized RPC metadata key (lowercase), the value being the metadata values. +/// +/// ## Notes +/// +/// Instrumentations SHOULD require an explicit configuration of which metadata values are to be captured. +/// Including all response metadata values can be a security risk - explicit configuration helps avoid leaking sensitive information. +/// +/// For example, a property `my-custom-key` with value `["attribute_value"]` SHOULD be recorded as +/// the `rpc.response.metadata.my-custom-key` attribute with value `["attribute_value"]` +/// +/// # Examples +/// +/// - `[ +/// "attribute_value", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const RPC_RESPONSE_METADATA: &str = "rpc.response.metadata"; + +/// Status code of the RPC returned by the RPC server or generated by the client +/// +/// ## Notes +/// +/// Usually it represents an error code, but may also represent partial success, warning, or differentiate between various types of successful outcomes. +/// Semantic conventions for individual RPC frameworks SHOULD document what `rpc.response.status_code` means in the context of that system and which values are considered to represent errors. +/// +/// # Examples +/// +/// - `"OK"` +/// - `"DEADLINE_EXCEEDED"` +/// - `"-32602"` +#[cfg(feature = "semconv_experimental")] +pub const RPC_RESPONSE_STATUS_CODE: &str = "rpc.response.status_code"; + +/// Deprecated, use fully-qualified `rpc.method` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"myservice.EchoService"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Value should be included in `rpc.method` which is expected to be a fully-qualified name., reason: uncategorized}" +)] +pub const RPC_SERVICE: &str = "rpc.service"; + +/// Deprecated, use `rpc.system.name` attribute instead. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `rpc.system.name`., reason: renamed, renamed_to: rpc.system.name}" +)] +pub const RPC_SYSTEM: &str = "rpc.system"; + +/// The Remote Procedure Call (RPC) system. +/// +/// ## Notes +/// +/// The client and server RPC systems may differ for the same RPC interaction. For example, a client may use Apache Dubbo or Connect RPC to communicate with a server that uses gRPC since both protocols provide compatibility with gRPC +#[cfg(feature = "semconv_experimental")] +pub const RPC_SYSTEM_NAME: &str = "rpc.system.name"; + +/// A categorization value keyword used by the entity using the rule for detection of this event +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Attempted Information Leak"` +#[cfg(feature = "semconv_experimental")] +pub const SECURITY_RULE_CATEGORY: &str = "security_rule.category"; + +/// The description of the rule generating the event. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Block requests to public DNS over HTTPS / TLS protocols"` +#[cfg(feature = "semconv_experimental")] +pub const SECURITY_RULE_DESCRIPTION: &str = "security_rule.description"; + +/// Name of the license under which the rule used to generate this event is made available. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Apache 2.0"` +#[cfg(feature = "semconv_experimental")] +pub const SECURITY_RULE_LICENSE: &str = "security_rule.license"; + +/// The name of the rule or signature generating the event. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"BLOCK_DNS_over_TLS"` +#[cfg(feature = "semconv_experimental")] +pub const SECURITY_RULE_NAME: &str = "security_rule.name"; + +/// Reference URL to additional information about the rule used to generate this event. +/// +/// ## Notes +/// +/// The URL can point to the vendor’s documentation about the rule. If that’s not available, it can also be a link to a more general page describing this type of alert. +/// +/// # Examples +/// +/// - `"https://en.wikipedia.org/wiki/DNS_over_TLS"` +#[cfg(feature = "semconv_experimental")] +pub const SECURITY_RULE_REFERENCE: &str = "security_rule.reference"; + +/// Name of the ruleset, policy, group, or parent category in which the rule used to generate this event is a member. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Standard_Protocol_Filters"` +#[cfg(feature = "semconv_experimental")] +pub const SECURITY_RULE_RULESET_NAME: &str = "security_rule.ruleset.name"; + +/// A rule ID that is unique within the scope of a set or group of agents, observers, or other entities using the rule for detection of this event. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"550e8400-e29b-41d4-a716-446655440000"` +/// - `"1100110011"` +#[cfg(feature = "semconv_experimental")] +pub const SECURITY_RULE_UUID: &str = "security_rule.uuid"; + +/// The version / revision of the rule being used for analysis. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1.0.0"` +#[cfg(feature = "semconv_experimental")] +pub const SECURITY_RULE_VERSION: &str = "security_rule.version"; + +/// Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. +/// +/// ## Notes +/// +/// When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent the server address behind any intermediaries, for example proxies, if it's available. +/// +/// # Examples +/// +/// - `"example.com"` +/// - `"10.1.2.80"` +/// - `"/tmp/my.sock"` +pub const SERVER_ADDRESS: &str = "server.address"; + +/// Server port number. +/// +/// ## Notes +/// +/// When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries, for example proxies, if it's available. +/// +/// # Examples +/// +/// - `80` +/// - `8080` +/// - `443` +pub const SERVER_PORT: &str = "server.port"; + +/// The operational criticality of the service. +/// +/// ## Notes +/// +/// Application developers are encouraged to set `service.criticality` to express the operational importance of their services. Telemetry consumers MAY use this attribute to optimize telemetry collection or improve user experience. +/// +/// # Examples +/// +/// - `"critical"` +/// - `"high"` +/// - `"medium"` +/// - `"low"` +#[cfg(feature = "semconv_experimental")] +pub const SERVICE_CRITICALITY: &str = "service.criticality"; + +/// The string ID of the service instance. +/// +/// ## Notes +/// +/// MUST be unique for each instance of the same `service.namespace,service.name` pair (in other words +/// `service.namespace,service.name,service.instance.id` triplet MUST be globally unique). The ID helps to +/// distinguish instances of the same service that exist at the same time (e.g. instances of a horizontally scaled +/// service). +/// +/// Implementations, such as SDKs, are recommended to generate a random Version 1 or Version 4 [RFC +/// 4122](https://www.ietf.org/rfc/rfc4122.txt) UUID, but are free to use an inherent unique ID as the source of +/// this value if stability is desirable. In that case, the ID SHOULD be used as source of a UUID Version 5 and +/// SHOULD use the following UUID as the namespace: `4d63009a-8d0f-11ee-aad7-4c796ed8e320`. +/// +/// UUIDs are typically recommended, as only an opaque value for the purposes of identifying a service instance is +/// needed. Similar to what can be seen in the man page for the +/// [`/etc/machine-id`](https://www.freedesktop.org/software/systemd/man/latest/machine-id.html) file, the underlying +/// data, such as pod name and namespace should be treated as confidential, being the user's choice to expose it +/// or not via another resource attribute. +/// +/// For applications running behind an application server (like unicorn), we do not recommend using one identifier +/// for all processes participating in the application. Instead, it's recommended each division (e.g. a worker +/// thread in unicorn) to have its own instance.id. +/// +/// It's not recommended for a Collector to set `service.instance.id` if it can't unambiguously determine the +/// service instance that is generating that telemetry. For instance, creating an UUID based on `pod.name` will +/// likely be wrong, as the Collector might not know from which container within that pod the telemetry originated. +/// However, Collectors can set the `service.instance.id` if they can unambiguously determine the service instance +/// for that telemetry. This is typically the case for scraping receivers, as they know the target address and +/// port. +/// +/// # Examples +/// +/// - `"627cc493-f310-47de-96bd-71410b7dec09"` +pub const SERVICE_INSTANCE_ID: &str = "service.instance.id"; + +/// Logical name of the service. +/// +/// ## Notes +/// +/// MUST be the same for all instances of horizontally scaled services. If the value was not specified, SDKs MUST fallback to `unknown_service:` concatenated with the process executable name, e.g. `unknown_service:bash`. If the process executable name is not available, the value MUST be set to `unknown_service`. +/// The process executable name is the name of the process executable, the same value as described by the [`process.executable.name`](process.md) resource attribute. +/// +/// # Examples +/// +/// - `"shoppingcart"` +pub const SERVICE_NAME: &str = "service.name"; + +/// A namespace for `service.name`. +/// +/// ## Notes +/// +/// A string value having a meaning that helps to distinguish a group of services, for example the team name that owns a group of services. `service.name` is expected to be unique within the same namespace. If `service.namespace` is not specified in the Resource then `service.name` is expected to be unique for all services that have no explicit namespace defined (so the empty/unspecified namespace is simply one more valid namespace). Zero-length namespace string is assumed equal to unspecified namespace. +/// +/// # Examples +/// +/// - `"Shop"` +pub const SERVICE_NAMESPACE: &str = "service.namespace"; + +/// Logical name of the service on the other side of the connection. SHOULD be equal to the actual [`service.name`](/docs/resource/README.md#service) resource attribute of the remote service if any. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"shoppingcart"` +#[cfg(feature = "semconv_experimental")] +pub const SERVICE_PEER_NAME: &str = "service.peer.name"; + +/// Logical namespace of the service on the other side of the connection. SHOULD be equal to the actual [`service.namespace`](/docs/resource/README.md#service) resource attribute of the remote service if any. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Shop"` +#[cfg(feature = "semconv_experimental")] +pub const SERVICE_PEER_NAMESPACE: &str = "service.peer.namespace"; + +/// The version string of the service component. The format is not defined by these conventions. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2.0.0"` +/// - `"a01dbef8a"` +pub const SERVICE_VERSION: &str = "service.version"; + +/// A unique id to identify a session. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"00112233-4455-6677-8899-aabbccddeeff"` +#[cfg(feature = "semconv_experimental")] +pub const SESSION_ID: &str = "session.id"; + +/// The previous `session.id` for this user, when known. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"00112233-4455-6677-8899-aabbccddeeff"` +#[cfg(feature = "semconv_experimental")] +pub const SESSION_PREVIOUS_ID: &str = "session.previous_id"; + +/// SignalR HTTP connection closure status. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"app_shutdown"` +/// - `"timeout"` +pub const SIGNALR_CONNECTION_STATUS: &str = "signalr.connection.status"; + +/// [SignalR transport type](https://github.com/dotnet/aspnetcore/blob/main/src/SignalR/docs/specs/TransportProtocols.md) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"web_sockets"` +/// - `"long_polling"` +pub const SIGNALR_TRANSPORT: &str = "signalr.transport"; + +/// Source address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. +/// +/// ## Notes +/// +/// When observed from the destination side, and when communicating through an intermediary, `source.address` SHOULD represent the source address behind any intermediaries, for example proxies, if it's available. +/// +/// # Examples +/// +/// - `"source.example.com"` +/// - `"10.1.2.80"` +/// - `"/tmp/my.sock"` +#[cfg(feature = "semconv_experimental")] +pub const SOURCE_ADDRESS: &str = "source.address"; + +/// Source port number +/// +/// ## Notes +/// +/// # Examples +/// +/// - `3389` +/// - `2888` +#[cfg(feature = "semconv_experimental")] +pub const SOURCE_PORT: &str = "source.port"; + +/// Deprecated, use `cpu.logical_number` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `1` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `cpu.logical_number`., reason: renamed, renamed_to: cpu.logical_number}" +)] +pub const SYSTEM_CPU_LOGICAL_NUMBER: &str = "system.cpu.logical_number"; + +/// Deprecated, use `cpu.mode` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"idle"` +/// - `"interrupt"` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Replaced by `cpu.mode`., reason: renamed, renamed_to: cpu.mode}")] +pub const SYSTEM_CPU_STATE: &str = "system.cpu.state"; + +/// The device identifier +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"(identifier)"` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_DEVICE: &str = "system.device"; + +/// The filesystem mode +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"rw, ro"` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_FILESYSTEM_MODE: &str = "system.filesystem.mode"; + +/// The filesystem mount path +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/mnt/data"` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_FILESYSTEM_MOUNTPOINT: &str = "system.filesystem.mountpoint"; + +/// The filesystem state +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"used"` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_FILESYSTEM_STATE: &str = "system.filesystem.state"; + +/// The filesystem type +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"ext4"` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_FILESYSTEM_TYPE: &str = "system.filesystem.type"; + +/// The Linux HugePages memory state +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"free"` +/// - `"used"` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LINUX_HUGEPAGES_STATE: &str = "system.memory.linux.hugepages.state"; + +/// The Linux Slab memory state +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"reclaimable"` +/// - `"unreclaimable"` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LINUX_SLAB_STATE: &str = "system.memory.linux.slab.state"; + +/// The memory state +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"free"` +/// - `"cached"` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_STATE: &str = "system.memory.state"; + +/// Deprecated, use `network.connection.state` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"close_wait"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `network.connection.state`., reason: renamed, renamed_to: network.connection.state}" +)] +pub const SYSTEM_NETWORK_STATE: &str = "system.network.state"; + +/// The paging access direction +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"in"` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_PAGING_DIRECTION: &str = "system.paging.direction"; + +/// The paging fault type +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"minor"` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_PAGING_FAULT_TYPE: &str = "system.paging.fault.type"; + +/// The memory paging state +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"free"` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_PAGING_STATE: &str = "system.paging.state"; + +/// Deprecated, use `system.paging.fault.type` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"minor"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.paging.fault.type`., reason: renamed, renamed_to: system.paging.fault.type}" +)] +pub const SYSTEM_PAGING_TYPE: &str = "system.paging.type"; + +/// Deprecated, use `process.state` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"running"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `process.state`., reason: renamed, renamed_to: process.state}" +)] +pub const SYSTEM_PROCESS_STATUS: &str = "system.process.status"; + +/// Deprecated, use `process.state` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"running"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `process.state`., reason: renamed, renamed_to: process.state}" +)] +pub const SYSTEM_PROCESSES_STATUS: &str = "system.processes.status"; + +/// The name of the auto instrumentation agent or distribution, if used. +/// +/// ## Notes +/// +/// Official auto instrumentation agents and distributions SHOULD set the `telemetry.distro.name` attribute to +/// a string starting with `opentelemetry-`, e.g. `opentelemetry-java-instrumentation`. +/// +/// # Examples +/// +/// - `"parts-unlimited-java"` +pub const TELEMETRY_DISTRO_NAME: &str = "telemetry.distro.name"; + +/// The version string of the auto instrumentation agent or distribution, if used. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1.2.3"` +pub const TELEMETRY_DISTRO_VERSION: &str = "telemetry.distro.version"; + +/// The language of the telemetry SDK. +/// +/// ## Notes +pub const TELEMETRY_SDK_LANGUAGE: &str = "telemetry.sdk.language"; + +/// The name of the telemetry SDK as defined above. +/// +/// ## Notes +/// +/// The OpenTelemetry SDK MUST set the `telemetry.sdk.name` attribute to `opentelemetry`. +/// If another SDK, like a fork or a vendor-provided implementation, is used, this SDK MUST set the +/// `telemetry.sdk.name` attribute to the fully-qualified class or module name of this SDK's main entry point +/// or another suitable identifier depending on the language. +/// The identifier `opentelemetry` is reserved and MUST NOT be used in this case. +/// All custom identifiers SHOULD be stable across different versions of an implementation. +/// +/// # Examples +/// +/// - `"opentelemetry"` +pub const TELEMETRY_SDK_NAME: &str = "telemetry.sdk.name"; + +/// The version string of the telemetry SDK. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1.2.3"` +pub const TELEMETRY_SDK_VERSION: &str = "telemetry.sdk.version"; + +/// The fully qualified human readable name of the [test case](https://wikipedia.org/wiki/Test_case). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"org.example.TestCase1.test1"` +/// - `"example/tests/TestCase1.test1"` +/// - `"ExampleTestCase1_test1"` +#[cfg(feature = "semconv_experimental")] +pub const TEST_CASE_NAME: &str = "test.case.name"; + +/// The status of the actual test case result from test execution. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"pass"` +/// - `"fail"` +#[cfg(feature = "semconv_experimental")] +pub const TEST_CASE_RESULT_STATUS: &str = "test.case.result.status"; + +/// The human readable name of a [test suite](https://wikipedia.org/wiki/Test_suite). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"TestSuite1"` +#[cfg(feature = "semconv_experimental")] +pub const TEST_SUITE_NAME: &str = "test.suite.name"; + +/// The status of the test suite run. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"success"` +/// - `"failure"` +/// - `"skipped"` +/// - `"aborted"` +/// - `"timed_out"` +/// - `"in_progress"` +#[cfg(feature = "semconv_experimental")] +pub const TEST_SUITE_RUN_STATUS: &str = "test.suite.run.status"; + +/// Current "managed" thread ID (as opposed to OS thread ID). +/// +/// ## Notes +/// +/// Examples of where the value can be extracted from: +/// +/// | Language or platform | Source | +/// | --- | --- | +/// | JVM | `Thread.currentThread().threadId()` | +/// | .NET | `Thread.CurrentThread.ManagedThreadId` | +/// | Python | `threading.current_thread().ident` | +/// | Ruby | `Thread.current.object_id` | +/// | C++ | `std::this_thread::get_id()` | +/// | Erlang | `erlang:self()` | +/// +/// # Examples +/// +/// - `42` +#[cfg(feature = "semconv_experimental")] +pub const THREAD_ID: &str = "thread.id"; + +/// Current thread name. +/// +/// ## Notes +/// +/// Examples of where the value can be extracted from: +/// +/// | Language or platform | Source | +/// | --- | --- | +/// | JVM | `Thread.currentThread().getName()` | +/// | .NET | `Thread.CurrentThread.Name` | +/// | Python | `threading.current_thread().name` | +/// | Ruby | `Thread.current.name` | +/// | Erlang | `erlang:process_info(self(), registered_name)` | +/// +/// # Examples +/// +/// - `"main"` +#[cfg(feature = "semconv_experimental")] +pub const THREAD_NAME: &str = "thread.name"; + +/// String indicating the [cipher](https://datatracker.ietf.org/doc/html/rfc5246#appendix-A.5) used during the current connection. +/// +/// ## Notes +/// +/// The values allowed for `tls.cipher` MUST be one of the `Descriptions` of the [registered TLS Cipher Suits](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#table-tls-parameters-4). +/// +/// # Examples +/// +/// - `"TLS_RSA_WITH_3DES_EDE_CBC_SHA"` +/// - `"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CIPHER: &str = "tls.cipher"; + +/// PEM-encoded stand-alone certificate offered by the client. This is usually mutually-exclusive of `client.certificate_chain` since this value also exists in that list. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"MII..."` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CLIENT_CERTIFICATE: &str = "tls.client.certificate"; + +/// Array of PEM-encoded certificates that make up the certificate chain offered by the client. This is usually mutually-exclusive of `client.certificate` since that value should be the first certificate in the chain. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "MII...", +/// "MI...", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CLIENT_CERTIFICATE_CHAIN: &str = "tls.client.certificate_chain"; + +/// Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CLIENT_HASH_MD5: &str = "tls.client.hash.md5"; + +/// Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"9E393D93138888D288266C2D915214D1D1CCEB2A"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CLIENT_HASH_SHA1: &str = "tls.client.hash.sha1"; + +/// Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CLIENT_HASH_SHA256: &str = "tls.client.hash.sha256"; + +/// Distinguished name of [subject](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6) of the issuer of the x.509 certificate presented by the client. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"CN=Example Root CA, OU=Infrastructure Team, DC=example, DC=com"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CLIENT_ISSUER: &str = "tls.client.issuer"; + +/// A hash that identifies clients based on how they perform an SSL/TLS handshake. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"d4e5b18d6b55c71272893221c96ba240"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CLIENT_JA3: &str = "tls.client.ja3"; + +/// Date/Time indicating when client certificate is no longer considered valid. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2021-01-01T00:00:00.000Z"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CLIENT_NOT_AFTER: &str = "tls.client.not_after"; + +/// Date/Time indicating when client certificate is first considered valid. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1970-01-01T00:00:00.000Z"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CLIENT_NOT_BEFORE: &str = "tls.client.not_before"; + +/// Deprecated, use `server.address` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"opentelemetry.io"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `server.address`., reason: renamed, renamed_to: server.address}" +)] +pub const TLS_CLIENT_SERVER_NAME: &str = "tls.client.server_name"; + +/// Distinguished name of subject of the x.509 certificate presented by the client. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"CN=myclient, OU=Documentation Team, DC=example, DC=com"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CLIENT_SUBJECT: &str = "tls.client.subject"; + +/// Array of ciphers offered by the client during the client hello. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", +/// "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CLIENT_SUPPORTED_CIPHERS: &str = "tls.client.supported_ciphers"; + +/// String indicating the curve used for the given cipher, when applicable +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"secp256r1"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_CURVE: &str = "tls.curve"; + +/// Boolean flag indicating if the TLS negotiation was successful and transitioned to an encrypted tunnel. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `true` +#[cfg(feature = "semconv_experimental")] +pub const TLS_ESTABLISHED: &str = "tls.established"; + +/// String indicating the protocol being tunneled. Per the values in the [IANA registry](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids), this string should be lower case. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"http/1.1"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_NEXT_PROTOCOL: &str = "tls.next_protocol"; + +/// Normalized lowercase protocol name parsed from original string of the negotiated [SSL/TLS protocol version](https://docs.openssl.org/1.1.1/man3/SSL_get_version/#return-values) +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const TLS_PROTOCOL_NAME: &str = "tls.protocol.name"; + +/// Numeric part of the version parsed from the original string of the negotiated [SSL/TLS protocol version](https://docs.openssl.org/1.1.1/man3/SSL_get_version/#return-values) +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1.2"` +/// - `"3"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_PROTOCOL_VERSION: &str = "tls.protocol.version"; + +/// Boolean flag indicating if this TLS connection was resumed from an existing TLS negotiation. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `true` +#[cfg(feature = "semconv_experimental")] +pub const TLS_RESUMED: &str = "tls.resumed"; + +/// PEM-encoded stand-alone certificate offered by the server. This is usually mutually-exclusive of `server.certificate_chain` since this value also exists in that list. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"MII..."` +#[cfg(feature = "semconv_experimental")] +pub const TLS_SERVER_CERTIFICATE: &str = "tls.server.certificate"; + +/// Array of PEM-encoded certificates that make up the certificate chain offered by the server. This is usually mutually-exclusive of `server.certificate` since that value should be the first certificate in the chain. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "MII...", +/// "MI...", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const TLS_SERVER_CERTIFICATE_CHAIN: &str = "tls.server.certificate_chain"; + +/// Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_SERVER_HASH_MD5: &str = "tls.server.hash.md5"; + +/// Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"9E393D93138888D288266C2D915214D1D1CCEB2A"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_SERVER_HASH_SHA1: &str = "tls.server.hash.sha1"; + +/// Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_SERVER_HASH_SHA256: &str = "tls.server.hash.sha256"; + +/// Distinguished name of [subject](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6) of the issuer of the x.509 certificate presented by the client. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"CN=Example Root CA, OU=Infrastructure Team, DC=example, DC=com"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_SERVER_ISSUER: &str = "tls.server.issuer"; + +/// A hash that identifies servers based on how they perform an SSL/TLS handshake. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"d4e5b18d6b55c71272893221c96ba240"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_SERVER_JA3S: &str = "tls.server.ja3s"; + +/// Date/Time indicating when server certificate is no longer considered valid. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"2021-01-01T00:00:00.000Z"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_SERVER_NOT_AFTER: &str = "tls.server.not_after"; + +/// Date/Time indicating when server certificate is first considered valid. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"1970-01-01T00:00:00.000Z"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_SERVER_NOT_BEFORE: &str = "tls.server.not_before"; + +/// Distinguished name of subject of the x.509 certificate presented by the server. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"CN=myserver, OU=Documentation Team, DC=example, DC=com"` +#[cfg(feature = "semconv_experimental")] +pub const TLS_SERVER_SUBJECT: &str = "tls.server.subject"; + +/// Domain extracted from the `url.full`, such as "opentelemetry.io". +/// +/// ## Notes +/// +/// In some cases a URL may refer to an IP and/or port directly, without a domain name. In this case, the IP address would go to the domain field. If the URL contains a [literal IPv6 address](https://www.rfc-editor.org/rfc/rfc2732#section-2) enclosed by `[` and `]`, the `[` and `]` characters should also be captured in the domain field. +/// +/// # Examples +/// +/// - `"www.foo.bar"` +/// - `"opentelemetry.io"` +/// - `"3.12.167.2"` +/// - `"[1080:0:0:0:8:800:200C:417A]"` +#[cfg(feature = "semconv_experimental")] +pub const URL_DOMAIN: &str = "url.domain"; + +/// The file extension extracted from the `url.full`, excluding the leading dot. +/// +/// ## Notes +/// +/// The file extension is only set if it exists, as not every url has a file extension. When the file name has multiple extensions `example.tar.gz`, only the last one should be captured `gz`, not `tar.gz`. +/// +/// # Examples +/// +/// - `"png"` +/// - `"gz"` +#[cfg(feature = "semconv_experimental")] +pub const URL_EXTENSION: &str = "url.extension"; + +/// The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"SemConv"` +pub const URL_FRAGMENT: &str = "url.fragment"; + +/// Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) +/// +/// ## Notes +/// +/// For network calls, URL usually has `scheme://host[:port][path][?query][#fragment]` format, where the fragment +/// is not transmitted over HTTP, but if it is known, it SHOULD be included nevertheless. +/// +/// `url.full` MUST NOT contain credentials passed via URL in form of `https://username:password@www.example.com/`. +/// In such case username and password SHOULD be redacted and attribute's value SHOULD be `https://REDACTED:REDACTED@www.example.com/`. +/// +/// `url.full` SHOULD capture the absolute URL when it is available (or can be reconstructed). +/// +/// Sensitive content provided in `url.full` SHOULD be scrubbed when instrumentations can identify it. +/// +/// +/// Query string values for the following keys SHOULD be redacted by default and replaced by the +/// value `REDACTED`: +/// +/// - [`X-Amz-Signature`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) +/// - [`X-Amz-Credential`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) +/// - [`X-Amz-Security-Token`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) +/// - [`sig`](https://learn.microsoft.com/azure/storage/common/storage-sas-overview#sas-token) +/// - [`X-Goog-Signature`](https://cloud.google.com/storage/docs/access-control/signed-urls) +/// +/// This list is subject to change over time. +/// +/// Matching of query parameter keys against the sensitive list SHOULD be case-sensitive. +/// +/// +/// Instrumentation MAY provide a way to override this list via declarative configuration. +/// If so, it SHOULD use the `sensitive_query_parameters` property +/// (an array of case-sensitive strings with minimum items 0) under +/// `.instrumentation/development.general.sanitization.url`. +/// This list is a full override of the default sensitive query parameter keys, +/// it is not a list of keys in addition to the defaults. +/// +/// When a query string value is redacted, the query string key SHOULD still be preserved, e.g. +/// `https://www.example.com/path?color=blue&sig=REDACTED`. +/// +/// # Examples +/// +/// - `"https://www.foo.bar/search?q=OpenTelemetry#SemConv"` +/// - `"//localhost"` +pub const URL_FULL: &str = "url.full"; + +/// Unmodified original URL as seen in the event source. +/// +/// ## Notes +/// +/// In network monitoring, the observed URL may be a full URL, whereas in access logs, the URL is often just represented as a path. This field is meant to represent the URL as it was observed, complete or not. +/// `url.original` might contain credentials passed via URL in form of `https://username:password@www.example.com/`. In such case password and username SHOULD NOT be redacted and attribute's value SHOULD remain the same. +/// +/// # Examples +/// +/// - `"https://www.foo.bar/search?q=OpenTelemetry#SemConv"` +/// - `"search?q=OpenTelemetry"` +#[cfg(feature = "semconv_experimental")] +pub const URL_ORIGINAL: &str = "url.original"; + +/// The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component +/// +/// ## Notes +/// +/// Sensitive content provided in `url.path` SHOULD be scrubbed when instrumentations can identify it. +/// +/// # Examples +/// +/// - `"/search"` +pub const URL_PATH: &str = "url.path"; + +/// Port extracted from the `url.full` +/// +/// ## Notes +/// +/// # Examples +/// +/// - `443` +#[cfg(feature = "semconv_experimental")] +pub const URL_PORT: &str = "url.port"; + +/// The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component +/// +/// ## Notes +/// +/// Sensitive content provided in `url.query` SHOULD be scrubbed when instrumentations can identify it. +/// +/// +/// Query string values for the following keys SHOULD be redacted by default and replaced by the value `REDACTED`: +/// +/// - [`X-Amz-Signature`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) +/// - [`X-Amz-Credential`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) +/// - [`X-Amz-Security-Token`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-authentication-methods.html) +/// - [`sig`](https://learn.microsoft.com/azure/storage/common/storage-sas-overview#sas-token) +/// - [`X-Goog-Signature`](https://cloud.google.com/storage/docs/access-control/signed-urls) +/// +/// This list is subject to change over time. +/// +/// Matching of query parameter keys against the sensitive list SHOULD be case-sensitive. +/// +/// Instrumentation MAY provide a way to override this list via declarative configuration. +/// If so, it SHOULD use the `sensitive_query_parameters` property +/// (an array of case-sensitive strings with minimum items 0) under +/// `.instrumentation/development.general.sanitization.url`. +/// This list is a full override of the default sensitive query parameter keys, +/// it is not a list of keys in addition to the defaults. +/// +/// When a query string value is redacted, the query string key SHOULD still be preserved, e.g. +/// `q=OpenTelemetry&sig=REDACTED`. +/// +/// # Examples +/// +/// - `"q=OpenTelemetry"` +pub const URL_QUERY: &str = "url.query"; + +/// The highest registered url domain, stripped of the subdomain. +/// +/// ## Notes +/// +/// This value can be determined precisely with the [public suffix list](https://publicsuffix.org/). For example, the registered domain for `foo.example.com` is `example.com`. Trying to approximate this by simply taking the last two labels will not work well for TLDs such as `co.uk`. +/// +/// # Examples +/// +/// - `"example.com"` +/// - `"foo.co.uk"` +#[cfg(feature = "semconv_experimental")] +pub const URL_REGISTERED_DOMAIN: &str = "url.registered_domain"; + +/// The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"https"` +/// - `"ftp"` +/// - `"telnet"` +pub const URL_SCHEME: &str = "url.scheme"; + +/// The subdomain portion of a fully qualified domain name includes all of the names except the host name under the registered_domain. In a partially qualified domain, or if the qualification level of the full name cannot be determined, subdomain contains all of the names below the registered domain. +/// +/// ## Notes +/// +/// The subdomain portion of `www.east.mydomain.co.uk` is `east`. If the domain has multiple levels of subdomain, such as `sub2.sub1.example.com`, the subdomain field should contain `sub2.sub1`, with no trailing period. +/// +/// # Examples +/// +/// - `"east"` +/// - `"sub2.sub1"` +#[cfg(feature = "semconv_experimental")] +pub const URL_SUBDOMAIN: &str = "url.subdomain"; + +/// The low-cardinality template of an [absolute path reference](https://www.rfc-editor.org/rfc/rfc3986#section-4.2). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"/users/{id}"` +/// - `"/users/:id"` +/// - `"/users?id={id}"` +#[cfg(feature = "semconv_experimental")] +pub const URL_TEMPLATE: &str = "url.template"; + +/// The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for example.com is `com`. +/// +/// ## Notes +/// +/// This value can be determined precisely with the [public suffix list](https://publicsuffix.org/). +/// +/// # Examples +/// +/// - `"com"` +/// - `"co.uk"` +#[cfg(feature = "semconv_experimental")] +pub const URL_TOP_LEVEL_DOMAIN: &str = "url.top_level_domain"; + +/// User email address. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"a.einstein@example.com"` +#[cfg(feature = "semconv_experimental")] +pub const USER_EMAIL: &str = "user.email"; + +/// User's full name +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Albert Einstein"` +#[cfg(feature = "semconv_experimental")] +pub const USER_FULL_NAME: &str = "user.full_name"; + +/// Unique user hash to correlate information for a user in anonymized form. +/// +/// ## Notes +/// +/// Useful if `user.id` or `user.name` contain confidential information and cannot be used. +/// +/// # Examples +/// +/// - `"364fc68eaf4c8acec74a4e52d7d1feaa"` +#[cfg(feature = "semconv_experimental")] +pub const USER_HASH: &str = "user.hash"; + +/// Unique identifier of the user. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"S-1-5-21-202424912787-2692429404-2351956786-1000"` +#[cfg(feature = "semconv_experimental")] +pub const USER_ID: &str = "user.id"; + +/// Short name or login/username of the user. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"a.einstein"` +#[cfg(feature = "semconv_experimental")] +pub const USER_NAME: &str = "user.name"; + +/// Array of user roles at the time of the event. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `[ +/// "admin", +/// "reporting_user", +/// ]` +#[cfg(feature = "semconv_experimental")] +pub const USER_ROLES: &str = "user.roles"; + +/// Name of the user-agent extracted from original. Usually refers to the browser's name. +/// +/// ## Notes +/// +/// [Example](https://uaparser.dev/#demo) of extracting browser's name from original string. In the case of using a user-agent for non-browser products, such as microservices with multiple names/versions inside the `user_agent.original`, the most significant name SHOULD be selected. In such a scenario it should align with `user_agent.version` +/// +/// # Examples +/// +/// - `"Safari"` +/// - `"YourApp"` +#[cfg(feature = "semconv_experimental")] +pub const USER_AGENT_NAME: &str = "user_agent.name"; + +/// Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"CERN-LineMode/2.15 libwww/2.17b3"` +/// - `"Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1"` +/// - `"YourApp/1.0.0 grpc-java-okhttp/1.27.2"` +pub const USER_AGENT_ORIGINAL: &str = "user_agent.original"; + +/// Human readable operating system name. +/// +/// ## Notes +/// +/// For mapping user agent strings to OS names, libraries such as [ua-parser](https://github.com/ua-parser) can be utilized. +/// +/// # Examples +/// +/// - `"iOS"` +/// - `"Android"` +/// - `"Ubuntu"` +#[cfg(feature = "semconv_experimental")] +pub const USER_AGENT_OS_NAME: &str = "user_agent.os.name"; + +/// The version string of the operating system as defined in [Version Attributes](/docs/resource/README.md#version-attributes). +/// +/// ## Notes +/// +/// For mapping user agent strings to OS versions, libraries such as [ua-parser](https://github.com/ua-parser) can be utilized. +/// +/// # Examples +/// +/// - `"14.2.1"` +/// - `"18.04.1"` +#[cfg(feature = "semconv_experimental")] +pub const USER_AGENT_OS_VERSION: &str = "user_agent.os.version"; + +/// Specifies the category of synthetic traffic, such as tests or bots. +/// +/// ## Notes +/// +/// This attribute MAY be derived from the contents of the `user_agent.original` attribute. Components that populate the attribute are responsible for determining what they consider to be synthetic bot or test traffic. This attribute can either be set for self-identification purposes, or on telemetry detected to be generated as a result of a synthetic request. This attribute is useful for distinguishing between genuine client traffic and synthetic traffic generated by bots or tests +#[cfg(feature = "semconv_experimental")] +pub const USER_AGENT_SYNTHETIC_TYPE: &str = "user_agent.synthetic.type"; + +/// Version of the user-agent extracted from original. Usually refers to the browser's version +/// +/// ## Notes +/// +/// [Example](https://uaparser.dev/#demo) of extracting browser's version from original string. In the case of using a user-agent for non-browser products, such as microservices with multiple names/versions inside the `user_agent.original`, the most significant version SHOULD be selected. In such a scenario it should align with `user_agent.name` +/// +/// # Examples +/// +/// - `"14.1.2"` +/// - `"1.0.0"` +#[cfg(feature = "semconv_experimental")] +pub const USER_AGENT_VERSION: &str = "user_agent.version"; + +/// The type of garbage collection. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const V8JS_GC_TYPE: &str = "v8js.gc.type"; + +/// The name of the space type of heap memory. +/// +/// ## Notes +/// +/// Value can be retrieved from value `space_name` of [`v8.getHeapSpaceStatistics()`](https://nodejs.org/api/v8.html#v8getheapspacestatistics) +#[cfg(feature = "semconv_experimental")] +pub const V8JS_HEAP_SPACE_NAME: &str = "v8js.heap.space.name"; + +/// The type of resource keeping the event loop active. +/// +/// ## Notes +#[cfg(feature = "semconv_experimental")] +pub const V8JS_RESOURCE_TYPE: &str = "v8js.resource.type"; + +/// The ID of the change (pull request/merge request/changelist) if applicable. This is usually a unique (within repository) identifier generated by the VCS system. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"123"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_CHANGE_ID: &str = "vcs.change.id"; + +/// The state of the change (pull request/merge request/changelist). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"open"` +/// - `"closed"` +/// - `"merged"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_CHANGE_STATE: &str = "vcs.change.state"; + +/// The human readable title of the change (pull request/merge request/changelist). This title is often a brief summary of the change and may get merged in to a ref as the commit summary. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Fixes broken thing"` +/// - `"feat: add my new feature"` +/// - `"[chore] update dependency"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_CHANGE_TITLE: &str = "vcs.change.title"; + +/// The type of line change being measured on a branch or change. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"added"` +/// - `"removed"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_LINE_CHANGE_TYPE: &str = "vcs.line_change.type"; + +/// The group owner within the version control system. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-org"` +/// - `"myteam"` +/// - `"business-unit"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_OWNER_NAME: &str = "vcs.owner.name"; + +/// The name of the version control system provider. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"github"` +/// - `"gitlab"` +/// - `"gitea"` +/// - `"bitbucket"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_PROVIDER_NAME: &str = "vcs.provider.name"; + +/// The name of the [reference](https://git-scm.com/docs/gitglossary#def_ref) such as **branch** or **tag** in the repository. +/// +/// ## Notes +/// +/// `base` refers to the starting point of a change. For example, `main` +/// would be the base reference of type branch if you've created a new +/// reference of type branch from it and created new commits. +/// +/// # Examples +/// +/// - `"my-feature-branch"` +/// - `"tag-1-test"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REF_BASE_NAME: &str = "vcs.ref.base.name"; + +/// The revision, literally [revised version](https://www.merriam-webster.com/dictionary/revision), The revision most often refers to a commit object in Git, or a revision number in SVN. +/// +/// ## Notes +/// +/// `base` refers to the starting point of a change. For example, `main` +/// would be the base reference of type branch if you've created a new +/// reference of type branch from it and created new commits. The +/// revision can be a full [hash value (see +/// glossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf), +/// of the recorded change to a ref within a repository pointing to a +/// commit [commit](https://git-scm.com/docs/git-commit) object. It does +/// not necessarily have to be a hash; it can simply define a [revision +/// number](https://svnbook.red-bean.com/en/1.7/svn.tour.revs.specifiers.html) +/// which is an integer that is monotonically increasing. In cases where +/// it is identical to the `ref.base.name`, it SHOULD still be included. +/// It is up to the implementer to decide which value to set as the +/// revision based on the VCS system and situational context. +/// +/// # Examples +/// +/// - `"9d59409acf479dfa0df1aa568182e43e43df8bbe28d60fcf2bc52e30068802cc"` +/// - `"main"` +/// - `"123"` +/// - `"HEAD"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REF_BASE_REVISION: &str = "vcs.ref.base.revision"; + +/// The type of the [reference](https://git-scm.com/docs/gitglossary#def_ref) in the repository. +/// +/// ## Notes +/// +/// `base` refers to the starting point of a change. For example, `main` +/// would be the base reference of type branch if you've created a new +/// reference of type branch from it and created new commits. +/// +/// # Examples +/// +/// - `"branch"` +/// - `"tag"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REF_BASE_TYPE: &str = "vcs.ref.base.type"; + +/// The name of the [reference](https://git-scm.com/docs/gitglossary#def_ref) such as **branch** or **tag** in the repository. +/// +/// ## Notes +/// +/// `head` refers to where you are right now; the current reference at a +/// given time. +/// +/// # Examples +/// +/// - `"my-feature-branch"` +/// - `"tag-1-test"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REF_HEAD_NAME: &str = "vcs.ref.head.name"; + +/// The revision, literally [revised version](https://www.merriam-webster.com/dictionary/revision), The revision most often refers to a commit object in Git, or a revision number in SVN. +/// +/// ## Notes +/// +/// `head` refers to where you are right now; the current reference at a +/// given time.The revision can be a full [hash value (see +/// glossary)](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf), +/// of the recorded change to a ref within a repository pointing to a +/// commit [commit](https://git-scm.com/docs/git-commit) object. It does +/// not necessarily have to be a hash; it can simply define a [revision +/// number](https://svnbook.red-bean.com/en/1.7/svn.tour.revs.specifiers.html) +/// which is an integer that is monotonically increasing. In cases where +/// it is identical to the `ref.head.name`, it SHOULD still be included. +/// It is up to the implementer to decide which value to set as the +/// revision based on the VCS system and situational context. +/// +/// # Examples +/// +/// - `"9d59409acf479dfa0df1aa568182e43e43df8bbe28d60fcf2bc52e30068802cc"` +/// - `"main"` +/// - `"123"` +/// - `"HEAD"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REF_HEAD_REVISION: &str = "vcs.ref.head.revision"; + +/// The type of the [reference](https://git-scm.com/docs/gitglossary#def_ref) in the repository. +/// +/// ## Notes +/// +/// `head` refers to where you are right now; the current reference at a +/// given time. +/// +/// # Examples +/// +/// - `"branch"` +/// - `"tag"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REF_HEAD_TYPE: &str = "vcs.ref.head.type"; + +/// The type of the [reference](https://git-scm.com/docs/gitglossary#def_ref) in the repository. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"branch"` +/// - `"tag"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REF_TYPE: &str = "vcs.ref.type"; + +/// Deprecated, use `vcs.change.id` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"123"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `vcs.change.id`., reason: renamed, renamed_to: vcs.change.id}" +)] +pub const VCS_REPOSITORY_CHANGE_ID: &str = "vcs.repository.change.id"; + +/// Deprecated, use `vcs.change.title` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"Fixes broken thing"` +/// - `"feat: add my new feature"` +/// - `"[chore] update dependency"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `vcs.change.title`., reason: renamed, renamed_to: vcs.change.title}" +)] +pub const VCS_REPOSITORY_CHANGE_TITLE: &str = "vcs.repository.change.title"; + +/// The human readable name of the repository. It SHOULD NOT include any additional identifier like Group/SubGroup in GitLab or organization in GitHub. +/// +/// ## Notes +/// +/// Due to it only being the name, it can clash with forks of the same +/// repository if collecting telemetry across multiple orgs or groups in +/// the same backends. +/// +/// # Examples +/// +/// - `"semantic-conventions"` +/// - `"my-cool-repo"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REPOSITORY_NAME: &str = "vcs.repository.name"; + +/// Deprecated, use `vcs.ref.head.name` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"my-feature-branch"` +/// - `"tag-1-test"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `vcs.ref.head.name`., reason: renamed, renamed_to: vcs.ref.head.name}" +)] +pub const VCS_REPOSITORY_REF_NAME: &str = "vcs.repository.ref.name"; + +/// Deprecated, use `vcs.ref.head.revision` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"9d59409acf479dfa0df1aa568182e43e43df8bbe28d60fcf2bc52e30068802cc"` +/// - `"main"` +/// - `"123"` +/// - `"HEAD"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `vcs.ref.head.revision`., reason: renamed, renamed_to: vcs.ref.head.revision}" +)] +pub const VCS_REPOSITORY_REF_REVISION: &str = "vcs.repository.ref.revision"; + +/// Deprecated, use `vcs.ref.head.type` instead. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"branch"` +/// - `"tag"` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `vcs.ref.head.type`., reason: renamed, renamed_to: vcs.ref.head.type}" +)] +pub const VCS_REPOSITORY_REF_TYPE: &str = "vcs.repository.ref.type"; + +/// The [canonical URL](https://support.google.com/webmasters/answer/10347851) of the repository providing the complete HTTP(S) address in order to locate and identify the repository through a browser. +/// +/// ## Notes +/// +/// In Git Version Control Systems, the canonical URL SHOULD NOT include +/// the `.git` extension. +/// +/// # Examples +/// +/// - `"https://github.com/opentelemetry/open-telemetry-collector-contrib"` +/// - `"https://gitlab.com/my-org/my-project/my-projects-project/repo"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REPOSITORY_URL_FULL: &str = "vcs.repository.url.full"; + +/// The type of revision comparison. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"ahead"` +/// - `"behind"` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REVISION_DELTA_DIRECTION: &str = "vcs.revision_delta.direction"; + +/// Additional description of the web engine (e.g. detailed version and edition information). +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"WildFly Full 21.0.0.Final (WildFly Core 13.0.1.Final) - 2.2.2.Final"` +#[cfg(feature = "semconv_experimental")] +pub const WEBENGINE_DESCRIPTION: &str = "webengine.description"; + +/// The name of the web engine. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"WildFly"` +#[cfg(feature = "semconv_experimental")] +pub const WEBENGINE_NAME: &str = "webengine.name"; + +/// The version of the web engine. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"21.0.0"` +#[cfg(feature = "semconv_experimental")] +pub const WEBENGINE_VERSION: &str = "webengine.version"; + +/// The System Management Facility (SMF) Identifier uniquely identified a z/OS system within a SYSPLEX or mainframe environment and is used for system and performance analysis. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"SYS1"` +#[cfg(feature = "semconv_experimental")] +pub const ZOS_SMF_ID: &str = "zos.smf.id"; + +/// The name of the SYSPLEX to which the z/OS system belongs too. +/// +/// ## Notes +/// +/// # Examples +/// +/// - `"SYSPLEX1"` +#[cfg(feature = "semconv_experimental")] +pub const ZOS_SYSPLEX_NAME: &str = "zos.sysplex.name"; diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/lib.rs b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/lib.rs new file mode 100644 index 00000000..626592b3 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/lib.rs @@ -0,0 +1,25 @@ +//! OpenTelemetry semantic conventions are agreed standardized naming patterns +//! for OpenTelemetry things. This crate aims to be the centralized place to +//! interact with these conventions. +#![warn( + future_incompatible, + missing_debug_implementations, + missing_docs, + nonstandard_style, + rust_2018_idioms, + unreachable_pub, + unused +)] +#![cfg_attr(test, deny(warnings))] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo.svg" +)] + +pub mod attribute; +pub mod metric; +pub mod resource; +pub mod trace; + +/// The schema URL that matches the version of the semantic conventions that +/// this crate defines. +pub const SCHEMA_URL: &str = "https://opentelemetry.io/schemas/1.42.0"; diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/metric.rs b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/metric.rs new file mode 100644 index 00000000..fe452004 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/metric.rs @@ -0,0 +1,10734 @@ +// DO NOT EDIT, this is an auto-generated file +// +// If you want to update the file: +// - Edit the template at scripts/templates/registry/rust/metric.rs.j2 +// - Run the script at scripts/generate-consts-from-spec.sh + +//! # Metric Semantic Conventions +//! +//! The [metric semantic conventions] define a set of standardized attributes to +//! be used in `Meter`s. +//! +//! [metric semantic conventions]: https://opentelemetry.io/docs/specs/semconv/general/metrics/ +//! +//! ## Usage +//! +//! ```rust +//! use opentelemetry::{global, KeyValue}; +//! use opentelemetry_semantic_conventions as semconv; +//! +//! // Assumes we already have an initialized `MeterProvider` +//! // See: https://github.com/open-telemetry/opentelemetry-rust/blob/main/examples/metrics-basic/src/main.rs +//! // for an example +//! let meter = global::meter("mylibraryname"); +//! let histogram = meter +//! .u64_histogram(semconv::metric::HTTP_SERVER_REQUEST_DURATION) +//! .with_unit("s") +//! .with_description("Duration of HTTP server requests.") +//! .build(); +//! ``` + +/// ## Description +/// +/// The authentication duration for a request. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Authentication`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_AUTHENTICATION_RESULT`] | `Required` +/// | [`crate::attribute::ASPNETCORE_AUTHENTICATION_SCHEME`] | `Conditionally_required`: if a scheme is specified during authentication. +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_AUTHENTICATION_AUTHENTICATE_DURATION: &str = + "aspnetcore.authentication.authenticate.duration"; + +/// ## Description +/// +/// The total number of times a scheme is challenged. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Authentication`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{challenge}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_AUTHENTICATION_SCHEME`] | `Conditionally_required`: if a scheme is specified during authentication. +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_AUTHENTICATION_CHALLENGES: &str = "aspnetcore.authentication.challenges"; + +/// ## Description +/// +/// The total number of times an authenticated user attempts to access a resource they are not permitted to access. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Authentication`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{forbid}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_AUTHENTICATION_SCHEME`] | `Conditionally_required`: if a scheme is specified during authentication. +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_AUTHENTICATION_FORBIDS: &str = "aspnetcore.authentication.forbids"; + +/// ## Description +/// +/// The total number of times a principal is signed in with a scheme. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Authentication`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{sign_in}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_AUTHENTICATION_SCHEME`] | `Conditionally_required`: if a scheme is specified during authentication. +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_AUTHENTICATION_SIGN_INS: &str = "aspnetcore.authentication.sign_ins"; + +/// ## Description +/// +/// The total number of times a principal is signed out with a scheme. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Authentication`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{sign_out}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_AUTHENTICATION_SCHEME`] | `Conditionally_required`: if a scheme is specified during authentication. +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_AUTHENTICATION_SIGN_OUTS: &str = "aspnetcore.authentication.sign_outs"; + +/// ## Description +/// +/// The total number of authorization attempts. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Authorization`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{attempt}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_AUTHORIZATION_POLICY`] | `Conditionally_required`: if a policy is specified. +/// | [`crate::attribute::ASPNETCORE_AUTHORIZATION_RESULT`] | `Conditionally_required`: if no exception was thrown. +/// | [`crate::attribute::ASPNETCORE_USER_IS_AUTHENTICATED`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_AUTHORIZATION_ATTEMPTS: &str = "aspnetcore.authorization.attempts"; + +/// ## Description +/// +/// Number of exceptions caught by exception handling middleware. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Diagnostics`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{exception}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_DIAGNOSTICS_EXCEPTION_RESULT`] | `Required` +/// | [`crate::attribute::ASPNETCORE_DIAGNOSTICS_HANDLER_TYPE`] | `Conditionally_required`: if and only if the exception was handled by this handler. +/// | [`crate::attribute::ERROR_TYPE`] | `Required` +pub const ASPNETCORE_DIAGNOSTICS_EXCEPTIONS: &str = "aspnetcore.diagnostics.exceptions"; + +/// ## Description +/// +/// The duration of authenticate attempts. The authenticate metrics is recorded by sign in methods such as PasswordSignInAsync and TwoFactorSignInAsync. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_AUTHENTICATION_SCHEME`] | `Required` +/// | [`crate::attribute::ASPNETCORE_IDENTITY_SIGN_IN_RESULT`] | `Conditionally_required`: if no exception was thrown. +/// | [`crate::attribute::ASPNETCORE_IDENTITY_SIGN_IN_TYPE`] | `Required` +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ASPNETCORE_SIGN_IN_IS_PERSISTENT`] | `Conditionally_required`: if no exception was thrown. +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_SIGN_IN_AUTHENTICATE_DURATION: &str = + "aspnetcore.identity.sign_in.authenticate.duration"; + +/// ## Description +/// +/// The total number of check password attempts. Checks that the account is in a state that can log in and that the password is valid using the UserManager.CheckPasswordAsync method. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{attempt}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_IDENTITY_SIGN_IN_RESULT`] | `Conditionally_required`: if no exception was thrown. +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_SIGN_IN_CHECK_PASSWORD_ATTEMPTS: &str = + "aspnetcore.identity.sign_in.check_password_attempts"; + +/// ## Description +/// +/// The total number of calls to sign in user principals. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{sign_in}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_AUTHENTICATION_SCHEME`] | `Required` +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ASPNETCORE_SIGN_IN_IS_PERSISTENT`] | `Conditionally_required`: if no exception was thrown. +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_SIGN_IN_SIGN_INS: &str = "aspnetcore.identity.sign_in.sign_ins"; + +/// ## Description +/// +/// The total number of calls to sign out user principals. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{sign_out}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_AUTHENTICATION_SCHEME`] | `Required` +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_SIGN_IN_SIGN_OUTS: &str = "aspnetcore.identity.sign_in.sign_outs"; + +/// ## Description +/// +/// The total number of two factor clients forgotten. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{client}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_AUTHENTICATION_SCHEME`] | `Required` +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_SIGN_IN_TWO_FACTOR_CLIENTS_FORGOTTEN: &str = + "aspnetcore.identity.sign_in.two_factor_clients_forgotten"; + +/// ## Description +/// +/// The total number of two factor clients remembered. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{client}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_AUTHENTICATION_SCHEME`] | `Required` +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_SIGN_IN_TWO_FACTOR_CLIENTS_REMEMBERED: &str = + "aspnetcore.identity.sign_in.two_factor_clients_remembered"; + +/// ## Description +/// +/// The number of check password attempts. Only checks whether the password is valid and not whether the user account is in a state that can log in. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{attempt}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_IDENTITY_PASSWORD_CHECK_RESULT`] | `Conditionally_required`: if no exception was thrown. +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_USER_CHECK_PASSWORD_ATTEMPTS: &str = + "aspnetcore.identity.user.check_password_attempts"; + +/// ## Description +/// +/// The duration of user creation operations. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_IDENTITY_ERROR_CODE`] | `Conditionally_required`: if an error was set on a failed identity result. +/// | [`crate::attribute::ASPNETCORE_IDENTITY_RESULT`] | `Conditionally_required`: if no exception was thrown. +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_USER_CREATE_DURATION: &str = + "aspnetcore.identity.user.create.duration"; + +/// ## Description +/// +/// The duration of user deletion operations. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_IDENTITY_ERROR_CODE`] | `Conditionally_required`: if an error was set on a failed identity result. +/// | [`crate::attribute::ASPNETCORE_IDENTITY_RESULT`] | `Conditionally_required`: if no exception was thrown. +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_USER_DELETE_DURATION: &str = + "aspnetcore.identity.user.delete.duration"; + +/// ## Description +/// +/// The total number of token generations. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{count}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_IDENTITY_TOKEN_PURPOSE`] | `Required` +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_USER_GENERATED_TOKENS: &str = + "aspnetcore.identity.user.generated_tokens"; + +/// ## Description +/// +/// The duration of user update operations. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_IDENTITY_ERROR_CODE`] | `Conditionally_required`: if an error was set on a failed identity result. +/// | [`crate::attribute::ASPNETCORE_IDENTITY_RESULT`] | `Conditionally_required`: if no exception was thrown. +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_UPDATE_TYPE`] | `Required` +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_USER_UPDATE_DURATION: &str = + "aspnetcore.identity.user.update.duration"; + +/// ## Description +/// +/// The total number of token verification attempts. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Identity`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{attempt}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_IDENTITY_TOKEN_PURPOSE`] | `Required` +/// | [`crate::attribute::ASPNETCORE_IDENTITY_TOKEN_VERIFIED`] | `Conditionally_required`: if no exception was thrown. +/// | [`crate::attribute::ASPNETCORE_IDENTITY_USER_TYPE`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_IDENTITY_USER_VERIFY_TOKEN_ATTEMPTS: &str = + "aspnetcore.identity.user.verify_token_attempts"; + +/// ## Description +/// +/// Total number of bytes allocated by the memory pool. Allocation occurs when a memory rental request exceeds the available pooled memory. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.MemoryPool`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_MEMORY_POOL_OWNER`] | `Conditionally_required`: if owner is specified when the memory pool is created. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_MEMORY_POOL_ALLOCATED: &str = "aspnetcore.memory_pool.allocated"; + +/// ## Description +/// +/// Total number of bytes evicted from the memory pool. Eviction occurs when idle pooled memory is reclaimed. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.MemoryPool`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_MEMORY_POOL_OWNER`] | `Conditionally_required`: if owner is specified when the memory pool is created. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_MEMORY_POOL_EVICTED: &str = "aspnetcore.memory_pool.evicted"; + +/// ## Description +/// +/// Number of bytes currently pooled and available for reuse. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.MemoryPool`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_MEMORY_POOL_OWNER`] | `Conditionally_required`: if owner is specified when the memory pool is created. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_MEMORY_POOL_POOLED: &str = "aspnetcore.memory_pool.pooled"; + +/// ## Description +/// +/// Total number of bytes rented from the memory pool. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.MemoryPool`; Added in: ASP.NET Core 10.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_MEMORY_POOL_OWNER`] | `Conditionally_required`: if owner is specified when the memory pool is created. +#[cfg(feature = "semconv_experimental")] +pub const ASPNETCORE_MEMORY_POOL_RENTED: &str = "aspnetcore.memory_pool.rented"; + +/// ## Description +/// +/// Number of requests that are currently active on the server that hold a rate limiting lease. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.RateLimiting`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{request}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_RATE_LIMITING_POLICY`] | `Conditionally_required`: if the matched endpoint for the request had a rate-limiting policy. +pub const ASPNETCORE_RATE_LIMITING_ACTIVE_REQUEST_LEASES: &str = + "aspnetcore.rate_limiting.active_request_leases"; + +/// ## Description +/// +/// Number of requests that are currently queued, waiting to acquire a rate limiting lease. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.RateLimiting`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{request}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_RATE_LIMITING_POLICY`] | `Conditionally_required`: if the matched endpoint for the request had a rate-limiting policy. +pub const ASPNETCORE_RATE_LIMITING_QUEUED_REQUESTS: &str = + "aspnetcore.rate_limiting.queued_requests"; + +/// ## Description +/// +/// The time the request spent in a queue waiting to acquire a rate limiting lease. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.RateLimiting`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_RATE_LIMITING_POLICY`] | `Conditionally_required`: if the matched endpoint for the request had a rate-limiting policy. +/// | [`crate::attribute::ASPNETCORE_RATE_LIMITING_RESULT`] | `Required` +pub const ASPNETCORE_RATE_LIMITING_REQUEST_TIME_IN_QUEUE: &str = + "aspnetcore.rate_limiting.request.time_in_queue"; + +/// ## Description +/// +/// The duration of rate limiting lease held by requests on the server. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.RateLimiting`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_RATE_LIMITING_POLICY`] | `Conditionally_required`: if the matched endpoint for the request had a rate-limiting policy. +pub const ASPNETCORE_RATE_LIMITING_REQUEST_LEASE_DURATION: &str = + "aspnetcore.rate_limiting.request_lease.duration"; + +/// ## Description +/// +/// Number of requests that tried to acquire a rate limiting lease. +/// +/// ## Notes +/// +/// Requests could be: +/// +/// - Rejected by global or endpoint rate limiting policies +/// - Canceled while waiting for the lease. +/// +/// Meter name: `Microsoft.AspNetCore.RateLimiting`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{request}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_RATE_LIMITING_POLICY`] | `Conditionally_required`: if the matched endpoint for the request had a rate-limiting policy. +/// | [`crate::attribute::ASPNETCORE_RATE_LIMITING_RESULT`] | `Required` +pub const ASPNETCORE_RATE_LIMITING_REQUESTS: &str = "aspnetcore.rate_limiting.requests"; + +/// ## Description +/// +/// Number of requests that were attempted to be matched to an endpoint. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Routing`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{match_attempt}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ASPNETCORE_ROUTING_IS_FALLBACK`] | `Conditionally_required`: if and only if a route was successfully matched. +/// | [`crate::attribute::ASPNETCORE_ROUTING_MATCH_STATUS`] | `Required` +/// | [`crate::attribute::HTTP_ROUTE`] | `Conditionally_required`: if and only if a route was successfully matched. +pub const ASPNETCORE_ROUTING_MATCH_ATTEMPTS: &str = "aspnetcore.routing.match_attempts"; + +/// ## Description +/// +/// Number of active client instances +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{instance}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If using a port other than the default port for this DBMS and if `server.address` is set. +#[cfg(feature = "semconv_experimental")] +pub const AZURE_COSMOSDB_CLIENT_ACTIVE_INSTANCE_COUNT: &str = + "azure.cosmosdb.client.active_instance.count"; + +/// ## Description +/// +/// [Request units](https://learn.microsoft.com/azure/cosmos-db/request-units) consumed by the operation +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `{request_unit}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::AZURE_COSMOSDB_CONSISTENCY_LEVEL`] | `Conditionally_required`: If available. +/// | [`crate::attribute::AZURE_COSMOSDB_OPERATION_CONTACTED_REGIONS`] | `{"recommended": "if available"}` +/// | [`crate::attribute::AZURE_COSMOSDB_RESPONSE_SUB_STATUS_CODE`] | `Conditionally_required`: when response was received and contained sub-code. +/// | [`crate::attribute::DB_COLLECTION_NAME`] | `Conditionally_required`: If available. +/// | [`crate::attribute::DB_NAMESPACE`] | `Conditionally_required`: If available. +/// | [`crate::attribute::DB_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::DB_RESPONSE_STATUS_CODE`] | `Conditionally_required`: If the operation failed and status code is available. +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If using a port other than the default port for this DBMS and if `server.address` is set. +#[cfg(feature = "semconv_experimental")] +pub const AZURE_COSMOSDB_CLIENT_OPERATION_REQUEST_CHARGE: &str = + "azure.cosmosdb.client.operation.request_charge"; + +/// ## Description +/// +/// The number of pipeline runs currently active in the system by state +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{run}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CICD_PIPELINE_NAME`] | `Required` +/// | [`crate::attribute::CICD_PIPELINE_RUN_STATE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_RUN_ACTIVE: &str = "cicd.pipeline.run.active"; + +/// ## Description +/// +/// Duration of a pipeline run grouped by pipeline, state and result +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CICD_PIPELINE_NAME`] | `Required` +/// | [`crate::attribute::CICD_PIPELINE_RESULT`] | `Conditionally_required`: If and only if the pipeline run result has been set during that state. +/// | [`crate::attribute::CICD_PIPELINE_RUN_STATE`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the pipeline run failed. +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_RUN_DURATION: &str = "cicd.pipeline.run.duration"; + +/// ## Description +/// +/// The number of errors encountered in pipeline runs (eg. compile, test failures). +/// +/// ## Notes +/// +/// There might be errors in a pipeline run that are non fatal (eg. they are suppressed) or in a parallel stage multiple stages could have a fatal error. +/// This means that this error count might not be the same as the count of metric `cicd.pipeline.run.duration` with run result `failure` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{error}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CICD_PIPELINE_NAME`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const CICD_PIPELINE_RUN_ERRORS: &str = "cicd.pipeline.run.errors"; + +/// ## Description +/// +/// The number of errors in a component of the CI/CD system (eg. controller, scheduler, agent). +/// +/// ## Notes +/// +/// Errors in pipeline run execution are explicitly excluded. Ie a test failure is not counted in this metric +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{error}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CICD_SYSTEM_COMPONENT`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const CICD_SYSTEM_ERRORS: &str = "cicd.system.errors"; + +/// ## Description +/// +/// The number of workers on the CI/CD system by state +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{count}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CICD_WORKER_STATE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const CICD_WORKER_COUNT: &str = "cicd.worker.count"; + +/// ## Description +/// +/// CPU time consumed. +/// +/// ## Notes +/// +/// CPU time consumed by the specific container on all available CPU cores +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Release_candidate` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPU_MODE`] | `Conditionally_required`: Required if mode is available, i.e. metrics coming from the Docker Stats API, containerd stats or cAdvisor. +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_CPU_TIME: &str = "container.cpu.time"; + +/// ## Description +/// +/// Container's CPU usage, measured in cpus. Range from 0 to the number of allocatable CPUs. +/// +/// ## Notes +/// +/// CPU usage of the specific container on all available CPU cores, averaged over the sample window +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPU_MODE`] | `Conditionally_required`: Required if mode is available, i.e. metrics coming from the Docker Stats API, containerd stats or cAdvisor. +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_CPU_USAGE: &str = "container.cpu.usage"; + +/// ## Description +/// +/// Disk bytes for the container. +/// +/// ## Notes +/// +/// The total number of bytes read/written successfully (aggregated from all disks) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DISK_IO_DIRECTION`] | `Recommended` +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_DISK_IO: &str = "container.disk.io"; + +/// ## Description +/// +/// Container filesystem available bytes. +/// +/// ## Notes +/// +/// In K8s, this metric is derived from the +/// [FsStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field +/// of the [ContainerStats.Rootfs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#ContainerStats) +/// of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_FILESYSTEM_AVAILABLE: &str = "container.filesystem.available"; + +/// ## Description +/// +/// Container filesystem capacity. +/// +/// ## Notes +/// +/// In K8s, this metric is derived from the +/// [FsStats.CapacityBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field +/// of the [ContainerStats.Rootfs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#ContainerStats) +/// of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_FILESYSTEM_CAPACITY: &str = "container.filesystem.capacity"; + +/// ## Description +/// +/// Container filesystem usage. +/// +/// ## Notes +/// +/// This may not equal capacity - available. +/// +/// In K8s, this metric is derived from the +/// [FsStats.UsedBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field +/// of the [ContainerStats.Rootfs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#ContainerStats) +/// of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_FILESYSTEM_USAGE: &str = "container.filesystem.usage"; + +/// ## Description +/// +/// Container memory available. +/// +/// ## Notes +/// +/// Available memory for use. This is defined as the memory limit - workingSetBytes. If memory limit is undefined, the available bytes is omitted. +/// In general, this metric can be derived from [cadvisor](https://github.com/google/cadvisor/blob/v0.53.0/docs/storage/prometheus.md#prometheus-container-metrics) and by subtracting the `container_memory_working_set_bytes` metric from the `container_spec_memory_limit_bytes` metric. +/// In K8s, this metric is derived from the [MemoryStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_MEMORY_AVAILABLE: &str = "container.memory.available"; + +/// ## Description +/// +/// Container memory paging faults. +/// +/// ## Notes +/// +/// In general, this metric can be derived from [cadvisor](https://github.com/google/cadvisor/blob/v0.53.0/docs/storage/prometheus.md#prometheus-container-metrics) and specifically the `container_memory_failures_total{failure_type=pgfault, scope=container}` and `container_memory_failures_total{failure_type=pgmajfault, scope=container}`metric. +/// In K8s, this metric is derived from the [MemoryStats.PageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) and [MemoryStats.MajorPageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{fault}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_PAGING_FAULT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_MEMORY_PAGING_FAULTS: &str = "container.memory.paging.faults"; + +/// ## Description +/// +/// Container memory RSS. +/// +/// ## Notes +/// +/// In general, this metric can be derived from [cadvisor](https://github.com/google/cadvisor/blob/v0.53.0/docs/storage/prometheus.md#prometheus-container-metrics) and specifically the `container_memory_rss` metric. +/// In K8s, this metric is derived from the [MemoryStats.RSSBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_MEMORY_RSS: &str = "container.memory.rss"; + +/// ## Description +/// +/// Memory usage of the container. +/// +/// ## Notes +/// +/// Memory usage of the container +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_MEMORY_USAGE: &str = "container.memory.usage"; + +/// ## Description +/// +/// Container memory working set. +/// +/// ## Notes +/// +/// In general, this metric can be derived from [cadvisor](https://github.com/google/cadvisor/blob/v0.53.0/docs/storage/prometheus.md#prometheus-container-metrics) and specifically the `container_memory_working_set_bytes` metric. +/// In K8s, this metric is derived from the [MemoryStats.WorkingSetBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_MEMORY_WORKING_SET: &str = "container.memory.working_set"; + +/// ## Description +/// +/// Network bytes for the container. +/// +/// ## Notes +/// +/// The number of bytes sent/received on all network interfaces by the container +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_INTERFACE_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_NETWORK_IO: &str = "container.network.io"; + +/// ## Description +/// +/// The time the container has been running. +/// +/// ## Notes +/// +/// Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available. +/// The actual accuracy would depend on the instrumentation and operating system +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const CONTAINER_UPTIME: &str = "container.uptime"; + +/// ## Description +/// +/// Deprecated. Use `system.cpu.frequency` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{Hz}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.cpu.frequency`., reason: renamed, renamed_to: system.cpu.frequency}" +)] +pub const CPU_FREQUENCY: &str = "cpu.frequency"; + +/// ## Description +/// +/// Deprecated. Use `system.cpu.time` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPU_LOGICAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::CPU_MODE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.cpu.time`., reason: renamed, renamed_to: system.cpu.time}" +)] +pub const CPU_TIME: &str = "cpu.time"; + +/// ## Description +/// +/// Deprecated. Use `system.cpu.utilization` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPU_LOGICAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::CPU_MODE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.cpu.utilization`., reason: renamed, renamed_to: system.cpu.utilization}" +)] +pub const CPU_UTILIZATION: &str = "cpu.utilization"; + +/// ## Description +/// +/// The total number of objects collected inside a generation since interpreter start. +/// +/// ## Notes +/// +/// This metric reports data from [`gc.stats()`](https://docs.python.org/3/library/gc.html#gc.get_stats) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{object}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPYTHON_GC_GENERATION`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const CPYTHON_GC_COLLECTED_OBJECTS: &str = "cpython.gc.collected_objects"; + +/// ## Description +/// +/// The number of times a generation was collected since interpreter start. +/// +/// ## Notes +/// +/// This metric reports data from [`gc.stats()`](https://docs.python.org/3/library/gc.html#gc.get_stats) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{collection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPYTHON_GC_GENERATION`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const CPYTHON_GC_COLLECTIONS: &str = "cpython.gc.collections"; + +/// ## Description +/// +/// The total number of objects which were found to be uncollectable inside a generation since interpreter start. +/// +/// ## Notes +/// +/// This metric reports data from [`gc.stats()`](https://docs.python.org/3/library/gc.html#gc.get_stats) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{object}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPYTHON_GC_GENERATION`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const CPYTHON_GC_UNCOLLECTABLE_OBJECTS: &str = "cpython.gc.uncollectable_objects"; + +/// ## Description +/// +/// The number of connections that are currently in state described by the `state` attribute +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTION_POOL_NAME`] | `Required` +/// | [`crate::attribute::DB_CLIENT_CONNECTION_STATE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_CONNECTION_COUNT: &str = "db.client.connection.count"; + +/// ## Description +/// +/// The time it took to create a new connection +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTION_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_CONNECTION_CREATE_TIME: &str = "db.client.connection.create_time"; + +/// ## Description +/// +/// The maximum number of idle open connections allowed +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTION_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_CONNECTION_IDLE_MAX: &str = "db.client.connection.idle.max"; + +/// ## Description +/// +/// The minimum number of idle open connections allowed +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTION_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_CONNECTION_IDLE_MIN: &str = "db.client.connection.idle.min"; + +/// ## Description +/// +/// The maximum number of open connections allowed +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTION_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_CONNECTION_MAX: &str = "db.client.connection.max"; + +/// ## Description +/// +/// The number of current pending requests for an open connection +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{request}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTION_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_CONNECTION_PENDING_REQUESTS: &str = "db.client.connection.pending_requests"; + +/// ## Description +/// +/// The number of connection timeouts that have occurred trying to obtain a connection from the pool +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{timeout}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTION_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_CONNECTION_TIMEOUTS: &str = "db.client.connection.timeouts"; + +/// ## Description +/// +/// The time between borrowing a connection and returning it to the pool +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTION_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_CONNECTION_USE_TIME: &str = "db.client.connection.use_time"; + +/// ## Description +/// +/// The time it took to obtain an open connection from the pool +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTION_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_CONNECTION_WAIT_TIME: &str = "db.client.connection.wait_time"; + +/// ## Description +/// +/// Deprecated, use `db.client.connection.create_time` instead. Note: the unit also changed from `ms` to `s` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `ms` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTIONS_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.create_time` with unit `s`., reason: uncategorized}" +)] +pub const DB_CLIENT_CONNECTIONS_CREATE_TIME: &str = "db.client.connections.create_time"; + +/// ## Description +/// +/// Deprecated, use `db.client.connection.idle.max` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTIONS_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.idle.max`., reason: renamed, renamed_to: db.client.connection.idle.max}" +)] +pub const DB_CLIENT_CONNECTIONS_IDLE_MAX: &str = "db.client.connections.idle.max"; + +/// ## Description +/// +/// Deprecated, use `db.client.connection.idle.min` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTIONS_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.idle.min`., reason: renamed, renamed_to: db.client.connection.idle.min}" +)] +pub const DB_CLIENT_CONNECTIONS_IDLE_MIN: &str = "db.client.connections.idle.min"; + +/// ## Description +/// +/// Deprecated, use `db.client.connection.max` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTIONS_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.max`., reason: renamed, renamed_to: db.client.connection.max}" +)] +pub const DB_CLIENT_CONNECTIONS_MAX: &str = "db.client.connections.max"; + +/// ## Description +/// +/// Deprecated, use `db.client.connection.pending_requests` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{request}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTIONS_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.pending_requests`., reason: renamed, renamed_to: db.client.connection.pending_requests}" +)] +pub const DB_CLIENT_CONNECTIONS_PENDING_REQUESTS: &str = "db.client.connections.pending_requests"; + +/// ## Description +/// +/// Deprecated, use `db.client.connection.timeouts` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{timeout}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTIONS_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.timeouts`., reason: renamed, renamed_to: db.client.connection.timeouts}" +)] +pub const DB_CLIENT_CONNECTIONS_TIMEOUTS: &str = "db.client.connections.timeouts"; + +/// ## Description +/// +/// Deprecated, use `db.client.connection.count` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTIONS_POOL_NAME`] | `Required` +/// | [`crate::attribute::DB_CLIENT_CONNECTIONS_STATE`] | `Required` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.count`., reason: renamed, renamed_to: db.client.connection.count}" +)] +pub const DB_CLIENT_CONNECTIONS_USAGE: &str = "db.client.connections.usage"; + +/// ## Description +/// +/// Deprecated, use `db.client.connection.use_time` instead. Note: the unit also changed from `ms` to `s` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `ms` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTIONS_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.use_time` with unit `s`., reason: uncategorized}" +)] +pub const DB_CLIENT_CONNECTIONS_USE_TIME: &str = "db.client.connections.use_time"; + +/// ## Description +/// +/// Deprecated, use `db.client.connection.wait_time` instead. Note: the unit also changed from `ms` to `s` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `ms` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_CLIENT_CONNECTIONS_POOL_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `db.client.connection.wait_time` with unit `s`., reason: uncategorized}" +)] +pub const DB_CLIENT_CONNECTIONS_WAIT_TIME: &str = "db.client.connections.wait_time"; + +/// ## Description +/// +/// Deprecated, use `azure.cosmosdb.client.active_instance.count` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{instance}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If using a port other than the default port for this DBMS and if `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `azure.cosmosdb.client.active_instance.count`., reason: renamed, renamed_to: azure.cosmosdb.client.active_instance.count}" +)] +pub const DB_CLIENT_COSMOSDB_ACTIVE_INSTANCE_COUNT: &str = + "db.client.cosmosdb.active_instance.count"; + +/// ## Description +/// +/// Deprecated, use `azure.cosmosdb.client.operation.request_charge` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `{request_unit}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_COLLECTION_NAME`] | `Conditionally_required`: If available. +/// | [`crate::attribute::DB_COSMOSDB_CONSISTENCY_LEVEL`] | `Conditionally_required`: If available. +/// | [`crate::attribute::DB_COSMOSDB_REGIONS_CONTACTED`] | `{"recommended": "if available"}` +/// | [`crate::attribute::DB_COSMOSDB_SUB_STATUS_CODE`] | `Conditionally_required`: when response was received and contained sub-code. +/// | [`crate::attribute::DB_NAMESPACE`] | `Conditionally_required`: If available. +/// | [`crate::attribute::DB_OPERATION_NAME`] | `Conditionally_required`: If readily available and if there is a single operation name that describes the database call. The operation name MAY be parsed from the query text, in which case it SHOULD be the single operation name found in the query. +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `azure.cosmosdb.client.operation.request_charge`., reason: renamed, renamed_to: azure.cosmosdb.client.operation.request_charge}" +)] +pub const DB_CLIENT_COSMOSDB_OPERATION_REQUEST_CHARGE: &str = + "db.client.cosmosdb.operation.request_charge"; + +/// ## Description +/// +/// Duration of database client operations. +/// +/// ## Notes +/// +/// Batch operations SHOULD be recorded as a single operation +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_COLLECTION_NAME`] | `Conditionally_required`: If readily available and if a database call is performed on a single collection. +/// | [`crate::attribute::DB_NAMESPACE`] | `Conditionally_required`: If available. +/// | [`crate::attribute::DB_OPERATION_NAME`] | `Conditionally_required`: If readily available and if there is a single operation name that describes the database call. +/// | [`crate::attribute::DB_QUERY_SUMMARY`] | `{"recommended": "if available through instrumentation hooks or if the instrumentation supports generating a query summary."}` +/// | [`crate::attribute::DB_QUERY_TEXT`] | `Opt_in` +/// | [`crate::attribute::DB_RESPONSE_STATUS_CODE`] | `Conditionally_required`: If the operation failed and status code is available. +/// | [`crate::attribute::DB_STORED_PROCEDURE_NAME`] | `{"recommended": "if operation applies to a specific stored procedure."}` +/// | [`crate::attribute::DB_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::NETWORK_PEER_ADDRESS`] | `{"recommended": "if applicable for this database system."}` +/// | [`crate::attribute::NETWORK_PEER_PORT`] | `{"recommended": "if and only if `network.peer.address` is set."}` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If using a port other than the default port for this DBMS and if `server.address` is set. +pub const DB_CLIENT_OPERATION_DURATION: &str = "db.client.operation.duration"; + +/// ## Description +/// +/// The actual number of records returned by the database operation +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `{row}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DB_COLLECTION_NAME`] | `Conditionally_required`: If readily available and if a database call is performed on a single collection. +/// | [`crate::attribute::DB_NAMESPACE`] | `Conditionally_required`: If available. +/// | [`crate::attribute::DB_OPERATION_NAME`] | `Conditionally_required`: If readily available and if there is a single operation name that describes the database call. +/// | [`crate::attribute::DB_QUERY_SUMMARY`] | `{"recommended": "if available through instrumentation hooks or if the instrumentation supports generating a query summary."}` +/// | [`crate::attribute::DB_QUERY_TEXT`] | `Opt_in` +/// | [`crate::attribute::DB_RESPONSE_STATUS_CODE`] | `Conditionally_required`: If the operation failed and status code is available. +/// | [`crate::attribute::DB_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::NETWORK_PEER_ADDRESS`] | `{"recommended": "if applicable for this database system."}` +/// | [`crate::attribute::NETWORK_PEER_PORT`] | `{"recommended": "if and only if `network.peer.address` is set."}` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If using a port other than the default port for this DBMS and if `server.address` is set. +#[cfg(feature = "semconv_experimental")] +pub const DB_CLIENT_RESPONSE_RETURNED_ROWS: &str = "db.client.response.returned_rows"; + +/// ## Description +/// +/// Measures the time taken to perform a DNS lookup +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DNS_QUESTION_NAME`] | `Required` +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +#[cfg(feature = "semconv_experimental")] +pub const DNS_LOOKUP_DURATION: &str = "dns.lookup.duration"; + +/// ## Description +/// +/// The number of .NET assemblies that are currently loaded. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`AppDomain.CurrentDomain.GetAssemblies().Length`](https://learn.microsoft.com/dotnet/api/system.appdomain.getassemblies) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{assembly}` | +/// | Status: | `Stable` | +pub const DOTNET_ASSEMBLY_COUNT: &str = "dotnet.assembly.count"; + +/// ## Description +/// +/// The number of exceptions that have been thrown in managed code. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as counting calls to [`AppDomain.CurrentDomain.FirstChanceException`](https://learn.microsoft.com/dotnet/api/system.appdomain.firstchanceexception) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{exception}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Required` +pub const DOTNET_EXCEPTIONS: &str = "dotnet.exceptions"; + +/// ## Description +/// +/// The number of garbage collections that have occurred since the process has started. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric uses the [`GC.CollectionCount(int generation)`](https://learn.microsoft.com/dotnet/api/system.gc.collectioncount) API to calculate exclusive collections per generation +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{collection}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DOTNET_GC_HEAP_GENERATION`] | `Required` +pub const DOTNET_GC_COLLECTIONS: &str = "dotnet.gc.collections"; + +/// ## Description +/// +/// The *approximate* number of bytes allocated on the managed GC heap since the process has started. The returned value does not include any native allocations. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`GC.GetTotalAllocatedBytes()`](https://learn.microsoft.com/dotnet/api/system.gc.gettotalallocatedbytes) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Stable` | +pub const DOTNET_GC_HEAP_TOTAL_ALLOCATED: &str = "dotnet.gc.heap.total_allocated"; + +/// ## Description +/// +/// The heap fragmentation, as observed during the latest garbage collection. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`GC.GetGCMemoryInfo().GenerationInfo.FragmentationAfterBytes`](https://learn.microsoft.com/dotnet/api/system.gcgenerationinfo.fragmentationafterbytes) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DOTNET_GC_HEAP_GENERATION`] | `Required` +pub const DOTNET_GC_LAST_COLLECTION_HEAP_FRAGMENTATION_SIZE: &str = + "dotnet.gc.last_collection.heap.fragmentation.size"; + +/// ## Description +/// +/// The managed GC heap size (including fragmentation), as observed during the latest garbage collection. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`GC.GetGCMemoryInfo().GenerationInfo.SizeAfterBytes`](https://learn.microsoft.com/dotnet/api/system.gcgenerationinfo.sizeafterbytes) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DOTNET_GC_HEAP_GENERATION`] | `Required` +pub const DOTNET_GC_LAST_COLLECTION_HEAP_SIZE: &str = "dotnet.gc.last_collection.heap.size"; + +/// ## Description +/// +/// The amount of committed virtual memory in use by the .NET GC, as observed during the latest garbage collection. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`GC.GetGCMemoryInfo().TotalCommittedBytes`](https://learn.microsoft.com/dotnet/api/system.gcmemoryinfo.totalcommittedbytes). Committed virtual memory may be larger than the heap size because it includes both memory for storing existing objects (the heap size) and some extra memory that is ready to handle newly allocated objects in the future +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Stable` | +pub const DOTNET_GC_LAST_COLLECTION_MEMORY_COMMITTED_SIZE: &str = + "dotnet.gc.last_collection.memory.committed_size"; + +/// ## Description +/// +/// The total amount of time paused in GC since the process has started. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`GC.GetTotalPauseDuration()`](https://learn.microsoft.com/dotnet/api/system.gc.gettotalpauseduration) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +pub const DOTNET_GC_PAUSE_TIME: &str = "dotnet.gc.pause.time"; + +/// ## Description +/// +/// The amount of time the JIT compiler has spent compiling methods since the process has started. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`JitInfo.GetCompilationTime()`](https://learn.microsoft.com/dotnet/api/system.runtime.jitinfo.getcompilationtime) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +pub const DOTNET_JIT_COMPILATION_TIME: &str = "dotnet.jit.compilation.time"; + +/// ## Description +/// +/// Count of bytes of intermediate language that have been compiled since the process has started. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`JitInfo.GetCompiledILBytes()`](https://learn.microsoft.com/dotnet/api/system.runtime.jitinfo.getcompiledilbytes) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Stable` | +pub const DOTNET_JIT_COMPILED_IL_SIZE: &str = "dotnet.jit.compiled_il.size"; + +/// ## Description +/// +/// The number of times the JIT compiler (re)compiled methods since the process has started. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`JitInfo.GetCompiledMethodCount()`](https://learn.microsoft.com/dotnet/api/system.runtime.jitinfo.getcompiledmethodcount) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{method}` | +/// | Status: | `Stable` | +pub const DOTNET_JIT_COMPILED_METHODS: &str = "dotnet.jit.compiled_methods"; + +/// ## Description +/// +/// The number of times there was contention when trying to acquire a monitor lock since the process has started. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`Monitor.LockContentionCount`](https://learn.microsoft.com/dotnet/api/system.threading.monitor.lockcontentioncount) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{contention}` | +/// | Status: | `Stable` | +pub const DOTNET_MONITOR_LOCK_CONTENTIONS: &str = "dotnet.monitor.lock_contentions"; + +/// ## Description +/// +/// The number of processors available to the process. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as accessing [`Environment.ProcessorCount`](https://learn.microsoft.com/dotnet/api/system.environment.processorcount) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Stable` | +pub const DOTNET_PROCESS_CPU_COUNT: &str = "dotnet.process.cpu.count"; + +/// ## Description +/// +/// CPU time used by the process. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as accessing the corresponding processor time properties on [`System.Diagnostics.Process`](https://learn.microsoft.com/dotnet/api/system.diagnostics.process) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPU_MODE`] | `Required` +pub const DOTNET_PROCESS_CPU_TIME: &str = "dotnet.process.cpu.time"; + +/// ## Description +/// +/// The number of bytes of physical memory mapped to the process context. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`Environment.WorkingSet`](https://learn.microsoft.com/dotnet/api/system.environment.workingset) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Stable` | +pub const DOTNET_PROCESS_MEMORY_WORKING_SET: &str = "dotnet.process.memory.working_set"; + +/// ## Description +/// +/// The number of work items that are currently queued to be processed by the thread pool. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`ThreadPool.PendingWorkItemCount`](https://learn.microsoft.com/dotnet/api/system.threading.threadpool.pendingworkitemcount) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{work_item}` | +/// | Status: | `Stable` | +pub const DOTNET_THREAD_POOL_QUEUE_LENGTH: &str = "dotnet.thread_pool.queue.length"; + +/// ## Description +/// +/// The number of thread pool threads that currently exist. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`ThreadPool.ThreadCount`](https://learn.microsoft.com/dotnet/api/system.threading.threadpool.threadcount) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{thread}` | +/// | Status: | `Stable` | +pub const DOTNET_THREAD_POOL_THREAD_COUNT: &str = "dotnet.thread_pool.thread.count"; + +/// ## Description +/// +/// The number of work items that the thread pool has completed since the process has started. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`ThreadPool.CompletedWorkItemCount`](https://learn.microsoft.com/dotnet/api/system.threading.threadpool.completedworkitemcount) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{work_item}` | +/// | Status: | `Stable` | +pub const DOTNET_THREAD_POOL_WORK_ITEM_COUNT: &str = "dotnet.thread_pool.work_item.count"; + +/// ## Description +/// +/// The number of timer instances that are currently active. +/// +/// ## Notes +/// +/// Meter name: `System.Runtime`; Added in: .NET 9.0. +/// This metric reports the same values as calling [`Timer.ActiveCount`](https://learn.microsoft.com/dotnet/api/system.threading.timer.activecount) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{timer}` | +/// | Status: | `Stable` | +pub const DOTNET_TIMER_COUNT: &str = "dotnet.timer.count"; + +/// ## Description +/// +/// Number of invocation cold starts +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{coldstart}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::FAAS_TRIGGER`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_COLDSTARTS: &str = "faas.coldstarts"; + +/// ## Description +/// +/// Distribution of CPU usage per invocation +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::FAAS_TRIGGER`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_CPU_USAGE: &str = "faas.cpu_usage"; + +/// ## Description +/// +/// Number of invocation errors +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{error}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::FAAS_TRIGGER`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_ERRORS: &str = "faas.errors"; + +/// ## Description +/// +/// Measures the duration of the function's initialization, such as a cold start +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::FAAS_TRIGGER`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_INIT_DURATION: &str = "faas.init_duration"; + +/// ## Description +/// +/// Number of successful invocations +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{invocation}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::FAAS_TRIGGER`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_INVOCATIONS: &str = "faas.invocations"; + +/// ## Description +/// +/// Measures the duration of the function's logic execution +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::FAAS_TRIGGER`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_INVOKE_DURATION: &str = "faas.invoke_duration"; + +/// ## Description +/// +/// Distribution of max memory usage per invocation +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::FAAS_TRIGGER`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_MEM_USAGE: &str = "faas.mem_usage"; + +/// ## Description +/// +/// Distribution of net I/O usage per invocation +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::FAAS_TRIGGER`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_NET_IO: &str = "faas.net_io"; + +/// ## Description +/// +/// Number of invocation timeouts +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{timeout}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::FAAS_TRIGGER`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const FAAS_TIMEOUTS: &str = "faas.timeouts"; + +/// ## Description +/// +/// GenAI operation duration +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if the operation ended in an error +/// | [`crate::attribute::GEN_AI_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_PROVIDER_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_REQUEST_MODEL`] | `Conditionally_required`: If available. +/// | [`crate::attribute::GEN_AI_RESPONSE_MODEL`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_CLIENT_OPERATION_DURATION: &str = "gen_ai.client.operation.duration"; + +/// ## Description +/// +/// Time per output chunk, recorded for each chunk received after the first one, measured as the time elapsed from the end of the previous chunk to the end of the current chunk. +/// +/// ## Notes +/// +/// This metrics SHOULD be reported for streaming calls and SHOULD NOT be reported otherwise +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::GEN_AI_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_PROVIDER_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_REQUEST_MODEL`] | `Conditionally_required`: If available. +/// | [`crate::attribute::GEN_AI_RESPONSE_MODEL`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_CLIENT_OPERATION_TIME_PER_OUTPUT_CHUNK: &str = + "gen_ai.client.operation.time_per_output_chunk"; + +/// ## Description +/// +/// Time to receive the first chunk, measured from when the client issues the generation request to when the first chunk is received in the response stream. +/// +/// ## Notes +/// +/// This metrics SHOULD be reported for streaming calls and SHOULD NOT be reported otherwise +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::GEN_AI_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_PROVIDER_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_REQUEST_MODEL`] | `Conditionally_required`: If available. +/// | [`crate::attribute::GEN_AI_RESPONSE_MODEL`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_CLIENT_OPERATION_TIME_TO_FIRST_CHUNK: &str = + "gen_ai.client.operation.time_to_first_chunk"; + +/// ## Description +/// +/// Number of input and output tokens used +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `{token}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::GEN_AI_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_PROVIDER_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_REQUEST_MODEL`] | `Conditionally_required`: If available. +/// | [`crate::attribute::GEN_AI_RESPONSE_MODEL`] | `Recommended` +/// | [`crate::attribute::GEN_AI_TOKEN_TYPE`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_CLIENT_TOKEN_USAGE: &str = "gen_ai.client.token.usage"; + +/// ## Description +/// +/// Generative AI server request duration such as time-to-last byte or last output token +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if the operation ended in an error +/// | [`crate::attribute::GEN_AI_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_PROVIDER_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_REQUEST_MODEL`] | `Conditionally_required`: If available. +/// | [`crate::attribute::GEN_AI_RESPONSE_MODEL`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_SERVER_REQUEST_DURATION: &str = "gen_ai.server.request.duration"; + +/// ## Description +/// +/// Time per output token generated after the first token for successful responses +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::GEN_AI_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_PROVIDER_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_REQUEST_MODEL`] | `Conditionally_required`: If available. +/// | [`crate::attribute::GEN_AI_RESPONSE_MODEL`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_SERVER_TIME_PER_OUTPUT_TOKEN: &str = "gen_ai.server.time_per_output_token"; + +/// ## Description +/// +/// Time to generate first token for successful responses +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::GEN_AI_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_PROVIDER_NAME`] | `Required` +/// | [`crate::attribute::GEN_AI_REQUEST_MODEL`] | `Conditionally_required`: If available. +/// | [`crate::attribute::GEN_AI_RESPONSE_MODEL`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: If `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const GEN_AI_SERVER_TIME_TO_FIRST_TOKEN: &str = "gen_ai.server.time_to_first_token"; + +/// ## Description +/// +/// Heap size target percentage configured by the user, otherwise 100. +/// +/// ## Notes +/// +/// The value range is \\[0.0,100.0\\]. Computed from `/gc/gogc:percent` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `%` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const GO_CONFIG_GOGC: &str = "go.config.gogc"; + +/// ## Description +/// +/// Estimated CPU time spent by the Go runtime. +/// +/// ## Notes +/// +/// Computed from `/cpu/classes/...` metrics. This metric is an overestimate, and not directly comparable to system CPU time measurements. Compare only with other `go.cpu.time` metrics +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::GO_CPU_DETAILED_STATE`] | `Opt_in` +/// | [`crate::attribute::GO_CPU_STATE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const GO_CPU_TIME: &str = "go.cpu.time"; + +/// ## Description +/// +/// Count of live goroutines. +/// +/// ## Notes +/// +/// Computed from `/sched/goroutines:goroutines` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{goroutine}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const GO_GOROUTINE_COUNT: &str = "go.goroutine.count"; + +/// ## Description +/// +/// Memory allocated to the heap by the application. +/// +/// ## Notes +/// +/// Computed from `/gc/heap/allocs:bytes` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const GO_MEMORY_ALLOCATED: &str = "go.memory.allocated"; + +/// ## Description +/// +/// Count of allocations to the heap by the application. +/// +/// ## Notes +/// +/// Computed from `/gc/heap/allocs:objects` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{allocation}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const GO_MEMORY_ALLOCATIONS: &str = "go.memory.allocations"; + +/// ## Description +/// +/// Number of completed GC cycles. +/// +/// ## Notes +/// +/// Computed from `/gc/cycles/total:gc-cycles` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{gc_cycle}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const GO_MEMORY_GC_CYCLES: &str = "go.memory.gc.cycles"; + +/// ## Description +/// +/// Heap size target for the end of the GC cycle. +/// +/// ## Notes +/// +/// Computed from `/gc/heap/goal:bytes` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const GO_MEMORY_GC_GOAL: &str = "go.memory.gc.goal"; + +/// ## Description +/// +/// Distribution of individual GC-related stop-the-world pause latencies. This is the time from deciding to stop the world until the world is started again. +/// +/// ## Notes +/// +/// Computed from `/sched/pauses/total/gc:seconds`. Bucket boundaries are provided by the runtime, and are subject to change +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const GO_MEMORY_GC_PAUSE_DURATION: &str = "go.memory.gc.pause.duration"; + +/// ## Description +/// +/// Go runtime memory limit configured by the user, if a limit exists. +/// +/// ## Notes +/// +/// Computed from `/gc/gomemlimit:bytes`. This metric is excluded if the limit obtained from the Go runtime is math.MaxInt64 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const GO_MEMORY_LIMIT: &str = "go.memory.limit"; + +/// ## Description +/// +/// Memory used by the Go runtime. +/// +/// ## Notes +/// +/// Computed from `(/memory/classes/total:bytes - /memory/classes/heap/released:bytes)` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::GO_MEMORY_DETAILED_TYPE`] | `Opt_in` +/// | [`crate::attribute::GO_MEMORY_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const GO_MEMORY_USED: &str = "go.memory.used"; + +/// ## Description +/// +/// The number of OS threads that can execute user-level Go code simultaneously. +/// +/// ## Notes +/// +/// Computed from `/sched/gomaxprocs:threads` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{thread}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const GO_PROCESSOR_LIMIT: &str = "go.processor.limit"; + +/// ## Description +/// +/// The time goroutines have spent in the scheduler in a runnable state before actually running. +/// +/// ## Notes +/// +/// Computed from `/sched/latencies:seconds`. Bucket boundaries are provided by the runtime, and are subject to change +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const GO_SCHEDULE_DURATION: &str = "go.schedule.duration"; + +/// ## Description +/// +/// Number of active HTTP requests +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{request}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HTTP_REQUEST_METHOD`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Required` +/// | [`crate::attribute::SERVER_PORT`] | `Required` +/// | [`crate::attribute::URL_SCHEME`] | `Opt_in` +/// | [`crate::attribute::URL_TEMPLATE`] | `Conditionally_required`: If available. +#[cfg(feature = "semconv_experimental")] +pub const HTTP_CLIENT_ACTIVE_REQUESTS: &str = "http.client.active_requests"; + +/// ## Description +/// +/// The duration of the successfully established outbound HTTP connections +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_PEER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Required` +/// | [`crate::attribute::SERVER_PORT`] | `Required` +/// | [`crate::attribute::URL_SCHEME`] | `Opt_in` +#[cfg(feature = "semconv_experimental")] +pub const HTTP_CLIENT_CONNECTION_DURATION: &str = "http.client.connection.duration"; + +/// ## Description +/// +/// Number of outbound HTTP connections that are currently active or idle on the client +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HTTP_CONNECTION_STATE`] | `Required` +/// | [`crate::attribute::NETWORK_PEER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Required` +/// | [`crate::attribute::SERVER_PORT`] | `Required` +/// | [`crate::attribute::URL_SCHEME`] | `Opt_in` +#[cfg(feature = "semconv_experimental")] +pub const HTTP_CLIENT_OPEN_CONNECTIONS: &str = "http.client.open_connections"; + +/// ## Description +/// +/// Size of HTTP client request bodies. +/// +/// ## Notes +/// +/// The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If request has ended with an error. +/// | [`crate::attribute::HTTP_REQUEST_METHOD`] | `Required` +/// | [`crate::attribute::HTTP_RESPONSE_STATUS_CODE`] | `Conditionally_required`: If and only if one was received/sent. +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `Conditionally_required`: If not `http` and `network.protocol.version` is set. +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Required` +/// | [`crate::attribute::SERVER_PORT`] | `Required` +/// | [`crate::attribute::URL_SCHEME`] | `Opt_in` +/// | [`crate::attribute::URL_TEMPLATE`] | `Conditionally_required`: If available. +#[cfg(feature = "semconv_experimental")] +pub const HTTP_CLIENT_REQUEST_BODY_SIZE: &str = "http.client.request.body.size"; + +/// ## Description +/// +/// Duration of HTTP client requests +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If request has ended with an error. +/// | [`crate::attribute::HTTP_REQUEST_METHOD`] | `Required` +/// | [`crate::attribute::HTTP_RESPONSE_STATUS_CODE`] | `Conditionally_required`: If and only if one was received/sent. +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `Conditionally_required`: If not `http` and `network.protocol.version` is set. +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Required` +/// | [`crate::attribute::SERVER_PORT`] | `Required` +/// | [`crate::attribute::URL_SCHEME`] | `Opt_in` +/// | [`crate::attribute::URL_TEMPLATE`] | `Opt_in` +pub const HTTP_CLIENT_REQUEST_DURATION: &str = "http.client.request.duration"; + +/// ## Description +/// +/// Size of HTTP client response bodies. +/// +/// ## Notes +/// +/// The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If request has ended with an error. +/// | [`crate::attribute::HTTP_REQUEST_METHOD`] | `Required` +/// | [`crate::attribute::HTTP_RESPONSE_STATUS_CODE`] | `Conditionally_required`: If and only if one was received/sent. +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `Conditionally_required`: If not `http` and `network.protocol.version` is set. +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Required` +/// | [`crate::attribute::SERVER_PORT`] | `Required` +/// | [`crate::attribute::URL_SCHEME`] | `Opt_in` +/// | [`crate::attribute::URL_TEMPLATE`] | `Conditionally_required`: If available. +#[cfg(feature = "semconv_experimental")] +pub const HTTP_CLIENT_RESPONSE_BODY_SIZE: &str = "http.client.response.body.size"; + +/// ## Description +/// +/// Number of active HTTP server requests +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{request}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HTTP_REQUEST_METHOD`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::SERVER_PORT`] | `Opt_in` +/// | [`crate::attribute::URL_SCHEME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const HTTP_SERVER_ACTIVE_REQUESTS: &str = "http.server.active_requests"; + +/// ## Description +/// +/// Size of HTTP server request bodies. +/// +/// ## Notes +/// +/// The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If request has ended with an error. +/// | [`crate::attribute::HTTP_REQUEST_METHOD`] | `Required` +/// | [`crate::attribute::HTTP_RESPONSE_STATUS_CODE`] | `Conditionally_required`: If and only if one was received/sent. +/// | [`crate::attribute::HTTP_ROUTE`] | `Conditionally_required`: If and only if it's available +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `Conditionally_required`: If not `http` and `network.protocol.version` is set. +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::SERVER_PORT`] | `Opt_in` +/// | [`crate::attribute::URL_SCHEME`] | `Required` +/// | [`crate::attribute::USER_AGENT_SYNTHETIC_TYPE`] | `Opt_in` +#[cfg(feature = "semconv_experimental")] +pub const HTTP_SERVER_REQUEST_BODY_SIZE: &str = "http.server.request.body.size"; + +/// ## Description +/// +/// Duration of HTTP server requests +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If request has ended with an error. +/// | [`crate::attribute::HTTP_REQUEST_METHOD`] | `Required` +/// | [`crate::attribute::HTTP_RESPONSE_STATUS_CODE`] | `Conditionally_required`: If and only if one was received/sent. +/// | [`crate::attribute::HTTP_ROUTE`] | `Conditionally_required`: If and only if it's available +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `Conditionally_required`: If not `http` and `network.protocol.version` is set. +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::SERVER_PORT`] | `Opt_in` +/// | [`crate::attribute::URL_SCHEME`] | `Required` +/// | [`crate::attribute::USER_AGENT_SYNTHETIC_TYPE`] | `Opt_in` +pub const HTTP_SERVER_REQUEST_DURATION: &str = "http.server.request.duration"; + +/// ## Description +/// +/// Size of HTTP server response bodies. +/// +/// ## Notes +/// +/// The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length) header. For requests using transport encoding, this should be the compressed size +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If request has ended with an error. +/// | [`crate::attribute::HTTP_REQUEST_METHOD`] | `Required` +/// | [`crate::attribute::HTTP_RESPONSE_STATUS_CODE`] | `Conditionally_required`: If and only if one was received/sent. +/// | [`crate::attribute::HTTP_ROUTE`] | `Conditionally_required`: If and only if it's available +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `Conditionally_required`: If not `http` and `network.protocol.version` is set. +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::SERVER_PORT`] | `Opt_in` +/// | [`crate::attribute::URL_SCHEME`] | `Required` +/// | [`crate::attribute::USER_AGENT_SYNTHETIC_TYPE`] | `Opt_in` +#[cfg(feature = "semconv_experimental")] +pub const HTTP_SERVER_RESPONSE_BODY_SIZE: &str = "http.server.response.body.size"; + +/// ## Description +/// +/// Remaining fraction of battery charge +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_BATTERY_CAPACITY`] | `Recommended` +/// | [`crate::attribute::HW_BATTERY_CHEMISTRY`] | `Recommended` +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_BATTERY_CHARGE: &str = "hw.battery.charge"; + +/// ## Description +/// +/// Lower limit of battery charge fraction to ensure proper operation +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_BATTERY_CAPACITY`] | `Recommended` +/// | [`crate::attribute::HW_BATTERY_CHEMISTRY`] | `Recommended` +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_LIMIT_TYPE`] | `Recommended` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_BATTERY_CHARGE_LIMIT: &str = "hw.battery.charge.limit"; + +/// ## Description +/// +/// Time left before battery is completely charged or discharged +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_BATTERY_CAPACITY`] | `Recommended` +/// | [`crate::attribute::HW_BATTERY_CHEMISTRY`] | `Recommended` +/// | [`crate::attribute::HW_BATTERY_STATE`] | `Conditionally_required`: If the battery is charging or discharging +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_STATE`] | `Required` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_BATTERY_TIME_LEFT: &str = "hw.battery.time_left"; + +/// ## Description +/// +/// CPU current frequency +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `Hz` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_CPU_SPEED: &str = "hw.cpu.speed"; + +/// ## Description +/// +/// CPU maximum frequency +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `Hz` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_LIMIT_TYPE`] | `Recommended` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_CPU_SPEED_LIMIT: &str = "hw.cpu.speed.limit"; + +/// ## Description +/// +/// Energy consumed by the component +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `J` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_TYPE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const HW_ENERGY: &str = "hw.energy"; + +/// ## Description +/// +/// Number of errors encountered by the component +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{error}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_TYPE`] | `Required` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_ERRORS: &str = "hw.errors"; + +/// ## Description +/// +/// Fan speed in revolutions per minute +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `rpm` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SENSOR_LOCATION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_FAN_SPEED: &str = "hw.fan.speed"; + +/// ## Description +/// +/// Speed limit in rpm +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `rpm` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_LIMIT_TYPE`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SENSOR_LOCATION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_FAN_SPEED_LIMIT: &str = "hw.fan.speed.limit"; + +/// ## Description +/// +/// Fan speed expressed as a fraction of its maximum speed +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SENSOR_LOCATION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_FAN_SPEED_RATIO: &str = "hw.fan.speed_ratio"; + +/// ## Description +/// +/// Received and transmitted bytes by the GPU +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_DRIVER_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_FIRMWARE_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const HW_GPU_IO: &str = "hw.gpu.io"; + +/// ## Description +/// +/// Size of the GPU memory +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_DRIVER_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_FIRMWARE_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_GPU_MEMORY_LIMIT: &str = "hw.gpu.memory.limit"; + +/// ## Description +/// +/// GPU memory used +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_DRIVER_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_FIRMWARE_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_GPU_MEMORY_USAGE: &str = "hw.gpu.memory.usage"; + +/// ## Description +/// +/// Fraction of GPU memory used +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_DRIVER_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_FIRMWARE_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_GPU_MEMORY_UTILIZATION: &str = "hw.gpu.memory.utilization"; + +/// ## Description +/// +/// Fraction of time spent in a specific task +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_DRIVER_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_FIRMWARE_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_GPU_TASK`] | `Recommended` +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_GPU_UTILIZATION: &str = "hw.gpu.utilization"; + +/// ## Description +/// +/// Ambient (external) temperature of the physical host +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `Cel` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_HOST_AMBIENT_TEMPERATURE: &str = "hw.host.ambient_temperature"; + +/// ## Description +/// +/// Total energy consumed by the entire physical host, in joules. +/// +/// ## Notes +/// +/// The overall energy usage of a host MUST be reported using the specific `hw.host.energy` and `hw.host.power` metrics **only**, instead of the generic `hw.energy` and `hw.power` described in the previous section, to prevent summing up overlapping values +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `J` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_HOST_ENERGY: &str = "hw.host.energy"; + +/// ## Description +/// +/// By how many degrees Celsius the temperature of the physical host can be increased, before reaching a warning threshold on one of the internal sensors +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `Cel` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_HOST_HEATING_MARGIN: &str = "hw.host.heating_margin"; + +/// ## Description +/// +/// Instantaneous power consumed by the entire physical host in Watts (`hw.host.energy` is preferred). +/// +/// ## Notes +/// +/// The overall energy usage of a host MUST be reported using the specific `hw.host.energy` and `hw.host.power` metrics **only**, instead of the generic `hw.energy` and `hw.power` described in the previous section, to prevent summing up overlapping values +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `W` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_HOST_POWER: &str = "hw.host.power"; + +/// ## Description +/// +/// Size of the logical disk +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_LOGICAL_DISK_RAID_LEVEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_LOGICAL_DISK_LIMIT: &str = "hw.logical_disk.limit"; + +/// ## Description +/// +/// Logical disk space usage +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_LOGICAL_DISK_RAID_LEVEL`] | `Recommended` +/// | [`crate::attribute::HW_LOGICAL_DISK_STATE`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_LOGICAL_DISK_USAGE: &str = "hw.logical_disk.usage"; + +/// ## Description +/// +/// Logical disk space utilization as a fraction +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_LOGICAL_DISK_RAID_LEVEL`] | `Recommended` +/// | [`crate::attribute::HW_LOGICAL_DISK_STATE`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_LOGICAL_DISK_UTILIZATION: &str = "hw.logical_disk.utilization"; + +/// ## Description +/// +/// Size of the memory module +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MEMORY_TYPE`] | `Recommended` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_MEMORY_SIZE: &str = "hw.memory.size"; + +/// ## Description +/// +/// Link speed +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By/s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_NETWORK_LOGICAL_ADDRESSES`] | `Recommended` +/// | [`crate::attribute::HW_NETWORK_PHYSICAL_ADDRESS`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_NETWORK_BANDWIDTH_LIMIT: &str = "hw.network.bandwidth.limit"; + +/// ## Description +/// +/// Utilization of the network bandwidth as a fraction +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_NETWORK_LOGICAL_ADDRESSES`] | `Recommended` +/// | [`crate::attribute::HW_NETWORK_PHYSICAL_ADDRESS`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_NETWORK_BANDWIDTH_UTILIZATION: &str = "hw.network.bandwidth.utilization"; + +/// ## Description +/// +/// Received and transmitted network traffic in bytes +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_NETWORK_LOGICAL_ADDRESSES`] | `Recommended` +/// | [`crate::attribute::HW_NETWORK_PHYSICAL_ADDRESS`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const HW_NETWORK_IO: &str = "hw.network.io"; + +/// ## Description +/// +/// Received and transmitted network traffic in packets (or frames) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{packet}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_NETWORK_LOGICAL_ADDRESSES`] | `Recommended` +/// | [`crate::attribute::HW_NETWORK_PHYSICAL_ADDRESS`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const HW_NETWORK_PACKETS: &str = "hw.network.packets"; + +/// ## Description +/// +/// Link status: `1` (up) or `0` (down) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_NETWORK_LOGICAL_ADDRESSES`] | `Recommended` +/// | [`crate::attribute::HW_NETWORK_PHYSICAL_ADDRESS`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_NETWORK_UP: &str = "hw.network.up"; + +/// ## Description +/// +/// Endurance remaining for this SSD disk +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_FIRMWARE_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_PHYSICAL_DISK_STATE`] | `Required` +/// | [`crate::attribute::HW_PHYSICAL_DISK_TYPE`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_PHYSICAL_DISK_ENDURANCE_UTILIZATION: &str = "hw.physical_disk.endurance_utilization"; + +/// ## Description +/// +/// Size of the disk +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_FIRMWARE_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_PHYSICAL_DISK_TYPE`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_PHYSICAL_DISK_SIZE: &str = "hw.physical_disk.size"; + +/// ## Description +/// +/// Value of the corresponding [S.M.A.R.T.](https://wikipedia.org/wiki/S.M.A.R.T.) (Self-Monitoring, Analysis, and Reporting Technology) attribute +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_FIRMWARE_VERSION`] | `Recommended` +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_PHYSICAL_DISK_SMART_ATTRIBUTE`] | `Recommended` +/// | [`crate::attribute::HW_PHYSICAL_DISK_TYPE`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_PHYSICAL_DISK_SMART: &str = "hw.physical_disk.smart"; + +/// ## Description +/// +/// Instantaneous power consumed by the component. +/// +/// ## Notes +/// +/// It is recommended to report `hw.energy` instead of `hw.power` when possible +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `W` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_TYPE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const HW_POWER: &str = "hw.power"; + +/// ## Description +/// +/// Maximum power output of the power supply +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `W` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_LIMIT_TYPE`] | `Recommended` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_POWER_SUPPLY_LIMIT: &str = "hw.power_supply.limit"; + +/// ## Description +/// +/// Current power output of the power supply +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `W` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_POWER_SUPPLY_USAGE: &str = "hw.power_supply.usage"; + +/// ## Description +/// +/// Utilization of the power supply as a fraction of its maximum output +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_POWER_SUPPLY_UTILIZATION: &str = "hw.power_supply.utilization"; + +/// ## Description +/// +/// Operational status: `1` (true) or `0` (false) for each of the possible states. +/// +/// ## Notes +/// +/// `hw.status` is currently specified as an *UpDownCounter* but would ideally be represented using a [*StateSet* as defined in OpenMetrics](https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#stateset). This semantic convention will be updated once *StateSet* is specified in OpenTelemetry. This planned change is not expected to have any consequence on the way users query their timeseries backend to retrieve the values of `hw.status` over time +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_STATE`] | `Required` +/// | [`crate::attribute::HW_TYPE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const HW_STATUS: &str = "hw.status"; + +/// ## Description +/// +/// Operations performed by the tape drive +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{operation}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_MODEL`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SERIAL_NUMBER`] | `Recommended` +/// | [`crate::attribute::HW_TAPE_DRIVE_OPERATION_TYPE`] | `Recommended` +/// | [`crate::attribute::HW_VENDOR`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_TAPE_DRIVE_OPERATIONS: &str = "hw.tape_drive.operations"; + +/// ## Description +/// +/// Temperature in degrees Celsius +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `Cel` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SENSOR_LOCATION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_TEMPERATURE: &str = "hw.temperature"; + +/// ## Description +/// +/// Temperature limit in degrees Celsius +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `Cel` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_LIMIT_TYPE`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SENSOR_LOCATION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_TEMPERATURE_LIMIT: &str = "hw.temperature.limit"; + +/// ## Description +/// +/// Voltage measured by the sensor +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `V` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SENSOR_LOCATION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_VOLTAGE: &str = "hw.voltage"; + +/// ## Description +/// +/// Voltage limit in Volts +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `V` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_LIMIT_TYPE`] | `Recommended` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SENSOR_LOCATION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_VOLTAGE_LIMIT: &str = "hw.voltage.limit"; + +/// ## Description +/// +/// Nominal (expected) voltage +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `V` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::HW_ID`] | `Required` +/// | [`crate::attribute::HW_NAME`] | `Recommended` +/// | [`crate::attribute::HW_PARENT`] | `Recommended` +/// | [`crate::attribute::HW_SENSOR_LOCATION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const HW_VOLTAGE_NOMINAL: &str = "hw.voltage.nominal"; + +/// ## Description +/// +/// Number of buffers in the pool +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{buffer}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::JVM_BUFFER_POOL_NAME`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const JVM_BUFFER_COUNT: &str = "jvm.buffer.count"; + +/// ## Description +/// +/// Measure of total memory capacity of buffers +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::JVM_BUFFER_POOL_NAME`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const JVM_BUFFER_MEMORY_LIMIT: &str = "jvm.buffer.memory.limit"; + +/// ## Description +/// +/// Deprecated, use `jvm.buffer.memory.used` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::JVM_BUFFER_POOL_NAME`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `jvm.buffer.memory.used`., reason: renamed, renamed_to: jvm.buffer.memory.used}" +)] +pub const JVM_BUFFER_MEMORY_USAGE: &str = "jvm.buffer.memory.usage"; + +/// ## Description +/// +/// Measure of memory used by buffers +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::JVM_BUFFER_POOL_NAME`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const JVM_BUFFER_MEMORY_USED: &str = "jvm.buffer.memory.used"; + +/// ## Description +/// +/// Number of classes currently loaded +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{class}` | +/// | Status: | `Stable` | +pub const JVM_CLASS_COUNT: &str = "jvm.class.count"; + +/// ## Description +/// +/// Number of classes loaded since JVM start +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{class}` | +/// | Status: | `Stable` | +pub const JVM_CLASS_LOADED: &str = "jvm.class.loaded"; + +/// ## Description +/// +/// Number of classes unloaded since JVM start +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{class}` | +/// | Status: | `Stable` | +pub const JVM_CLASS_UNLOADED: &str = "jvm.class.unloaded"; + +/// ## Description +/// +/// Number of processors available to the Java virtual machine +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Stable` | +pub const JVM_CPU_COUNT: &str = "jvm.cpu.count"; + +/// ## Description +/// +/// Recent CPU utilization for the process as reported by the JVM. +/// +/// ## Notes +/// +/// The value range is \\[0.0,1.0\\]. This utilization is not defined as being for the specific interval since last measurement (unlike `system.cpu.utilization`). [Reference](https://docs.oracle.com/en/java/javase/17/docs/api/jdk.management/com/sun/management/OperatingSystemMXBean.html#getProcessCpuLoad()) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Stable` | +pub const JVM_CPU_RECENT_UTILIZATION: &str = "jvm.cpu.recent_utilization"; + +/// ## Description +/// +/// CPU time used by the process as reported by the JVM +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +pub const JVM_CPU_TIME: &str = "jvm.cpu.time"; + +/// ## Description +/// +/// Number of open file descriptors as reported by the JVM +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{file_descriptor}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const JVM_FILE_DESCRIPTOR_COUNT: &str = "jvm.file_descriptor.count"; + +/// ## Description +/// +/// Measure of max open file descriptors as reported by the JVM +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{file_descriptor}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const JVM_FILE_DESCRIPTOR_LIMIT: &str = "jvm.file_descriptor.limit"; + +/// ## Description +/// +/// Duration of JVM garbage collection actions +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::JVM_GC_ACTION`] | `Recommended` +/// | [`crate::attribute::JVM_GC_CAUSE`] | `Opt_in` +/// | [`crate::attribute::JVM_GC_NAME`] | `Recommended` +pub const JVM_GC_DURATION: &str = "jvm.gc.duration"; + +/// ## Description +/// +/// Measure of memory committed +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::JVM_MEMORY_POOL_NAME`] | `Recommended` +/// | [`crate::attribute::JVM_MEMORY_TYPE`] | `Recommended` +pub const JVM_MEMORY_COMMITTED: &str = "jvm.memory.committed"; + +/// ## Description +/// +/// Measure of initial memory requested +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::JVM_MEMORY_POOL_NAME`] | `Recommended` +/// | [`crate::attribute::JVM_MEMORY_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const JVM_MEMORY_INIT: &str = "jvm.memory.init"; + +/// ## Description +/// +/// Measure of max obtainable memory +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::JVM_MEMORY_POOL_NAME`] | `Recommended` +/// | [`crate::attribute::JVM_MEMORY_TYPE`] | `Recommended` +pub const JVM_MEMORY_LIMIT: &str = "jvm.memory.limit"; + +/// ## Description +/// +/// Measure of memory used +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::JVM_MEMORY_POOL_NAME`] | `Recommended` +/// | [`crate::attribute::JVM_MEMORY_TYPE`] | `Recommended` +pub const JVM_MEMORY_USED: &str = "jvm.memory.used"; + +/// ## Description +/// +/// Measure of memory used, as measured after the most recent garbage collection event on this pool +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::JVM_MEMORY_POOL_NAME`] | `Recommended` +/// | [`crate::attribute::JVM_MEMORY_TYPE`] | `Recommended` +pub const JVM_MEMORY_USED_AFTER_LAST_GC: &str = "jvm.memory.used_after_last_gc"; + +/// ## Description +/// +/// Average CPU load of the whole system for the last minute as reported by the JVM. +/// +/// ## Notes +/// +/// The value range is \\[0,n\\], where n is the number of CPU cores - or a negative number if the value is not available. This utilization is not defined as being for the specific interval since last measurement (unlike `system.cpu.utilization`). [Reference](https://docs.oracle.com/en/java/javase/17/docs/api/java.management/java/lang/management/OperatingSystemMXBean.html#getSystemLoadAverage()) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{run_queue_item}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const JVM_SYSTEM_CPU_LOAD_1M: &str = "jvm.system.cpu.load_1m"; + +/// ## Description +/// +/// Recent CPU utilization for the whole system as reported by the JVM. +/// +/// ## Notes +/// +/// The value range is \\[0.0,1.0\\]. This utilization is not defined as being for the specific interval since last measurement (unlike `system.cpu.utilization`). [Reference](https://docs.oracle.com/en/java/javase/17/docs/api/jdk.management/com/sun/management/OperatingSystemMXBean.html#getCpuLoad()) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const JVM_SYSTEM_CPU_UTILIZATION: &str = "jvm.system.cpu.utilization"; + +/// ## Description +/// +/// Number of executing platform threads +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{thread}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::JVM_THREAD_DAEMON`] | `Recommended` +/// | [`crate::attribute::JVM_THREAD_STATE`] | `Recommended` +pub const JVM_THREAD_COUNT: &str = "jvm.thread.count"; + +/// ## Description +/// +/// Deprecated, use `k8s.container.cpu.limit.desired` and `k8s.container.cpu.limit.current` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.container.cpu.limit.desired`., reason: renamed, renamed_to: k8s.container.cpu.limit.desired}" +)] +pub const K8S_CONTAINER_CPU_LIMIT: &str = "k8s.container.cpu.limit"; + +/// ## Description +/// +/// Maximum CPU resource limit currently configured for a running container. +/// +/// ## Notes +/// +/// This metric aligns with the limit in the +/// [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of +/// [K8s ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core) +/// (status.containerStatuses\[*\].resources). Also see `Actual Resources` in +/// [https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) for more details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_CPU_LIMIT_CURRENT: &str = "k8s.container.cpu.limit.current"; + +/// ## Description +/// +/// Maximum CPU resource limit as defined by the container spec. +/// +/// ## Notes +/// +/// This metric aligns with the limit in the +/// [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of +/// [K8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#container-v1-core) +/// (spec.containers\[*\].resources). Also see `Desired Resources` in +/// [https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) for more details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_CPU_LIMIT_DESIRED: &str = "k8s.container.cpu.limit.desired"; + +/// ## Description +/// +/// The ratio of container CPU usage to its current CPU limit. +/// +/// ## Notes +/// +/// The current CPU limit reflects the actual resources applied to the container, as reported by +/// [ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core). +/// The value range is \\[0.0,1.0\\]. A value of 1.0 means the container is using 100% of its actual CPU limit. +/// If the CPU limit is not set, this metric SHOULD NOT be emitted for that container +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_CPU_LIMIT_UTILIZATION: &str = "k8s.container.cpu.limit.utilization"; + +/// ## Description +/// +/// Deprecated, use `k8s.container.cpu.request.desired` and `k8s.container.cpu.request.current` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.container.cpu.request.desired`., reason: renamed, renamed_to: k8s.container.cpu.request.desired}" +)] +pub const K8S_CONTAINER_CPU_REQUEST: &str = "k8s.container.cpu.request"; + +/// ## Description +/// +/// CPU resource requested currently configured for a running container. +/// +/// ## Notes +/// +/// This metric aligns with the request in the +/// [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of +/// [K8s ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core) +/// (status.containerStatuses\[*\].resources). Also see `Actual Resources` in +/// [https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) for more details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_CPU_REQUEST_CURRENT: &str = "k8s.container.cpu.request.current"; + +/// ## Description +/// +/// CPU resource requested as defined by the container spec. +/// +/// ## Notes +/// +/// This metric aligns with the request in the +/// [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of +/// [K8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#container-v1-core) +/// (spec.containers\[*\].resources). Also see `Desired Resources` in +/// [https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) for more details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_CPU_REQUEST_DESIRED: &str = "k8s.container.cpu.request.desired"; + +/// ## Description +/// +/// The ratio of container CPU usage to its current CPU request. +/// +/// ## Notes +/// +/// The current CPU request reflects the request applied to the running container, as reported by +/// [ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core). +/// The value range is \\[0.0,1.0\\]. A value of 1.0 means the container is using 100% of its actual CPU request. +/// If the CPU request is not set, this metric SHOULD NOT be emitted for that container +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_CPU_REQUEST_UTILIZATION: &str = "k8s.container.cpu.request.utilization"; + +/// ## Description +/// +/// Maximum ephemeral storage resource limit set for the container. +/// +/// ## Notes +/// +/// See for details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_EPHEMERAL_STORAGE_LIMIT: &str = "k8s.container.ephemeral_storage.limit"; + +/// ## Description +/// +/// Ephemeral storage resource requested for the container. +/// +/// ## Notes +/// +/// See for details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_EPHEMERAL_STORAGE_REQUEST: &str = "k8s.container.ephemeral_storage.request"; + +/// ## Description +/// +/// The ephemeral storage used by a container. +/// +/// ## Notes +/// +/// The value for this metric can be compared against `metric.k8s.container.ephemeral_storage.request` and `metric.k8s.container.ephemeral_storage.limit` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_CONTAINER_EPHEMERAL_STORAGE_FS_TYPE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_EPHEMERAL_STORAGE_USAGE: &str = "k8s.container.ephemeral_storage.usage"; + +/// ## Description +/// +/// Deprecated, use `k8s.container.memory.limit.desired` and `k8s.container.memory.limit.current` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.container.memory.limit.desired`., reason: renamed, renamed_to: k8s.container.memory.limit.desired}" +)] +pub const K8S_CONTAINER_MEMORY_LIMIT: &str = "k8s.container.memory.limit"; + +/// ## Description +/// +/// Maximum memory resource limit currently configured for a running container. +/// +/// ## Notes +/// +/// This metric aligns with the limit in the +/// [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of +/// [K8s ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core) +/// (status.containerStatuses\[*\].resources). Also see `Actual Resources` in +/// [https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) for more details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_MEMORY_LIMIT_CURRENT: &str = "k8s.container.memory.limit.current"; + +/// ## Description +/// +/// Maximum memory resource limit as defined by the container spec. +/// +/// ## Notes +/// +/// This metric aligns with the limit in the +/// [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of +/// [K8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#container-v1-core) +/// (spec.containers\[*\].resources). Also see `Desired Resources` in +/// [https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) for more details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_MEMORY_LIMIT_DESIRED: &str = "k8s.container.memory.limit.desired"; + +/// ## Description +/// +/// Deprecated, use `k8s.container.memory.request.desired` and `k8s.container.memory.request.current` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.container.memory.request.desired`., reason: renamed, renamed_to: k8s.container.memory.request.desired}" +)] +pub const K8S_CONTAINER_MEMORY_REQUEST: &str = "k8s.container.memory.request"; + +/// ## Description +/// +/// Memory resource request currently configured for a running container. +/// +/// ## Notes +/// +/// This metric aligns with the request in the +/// [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of +/// [K8s ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core) +/// (status.containerStatuses\[*\].resources). Also see `Actual Resources` in +/// [https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) for more details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_MEMORY_REQUEST_CURRENT: &str = "k8s.container.memory.request.current"; + +/// ## Description +/// +/// Memory resource requested as defined by the container spec. +/// +/// ## Notes +/// +/// This metric aligns with the request in the +/// [`resources`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcerequirements-v1-core) field of +/// [K8s Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#container-v1-core) +/// (spec.containers\[*\].resources). Also see `Desired Resources` in +/// [https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/](https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/) for more details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_MEMORY_REQUEST_DESIRED: &str = "k8s.container.memory.request.desired"; + +/// ## Description +/// +/// Indicates whether the container is currently marked as ready to accept traffic, based on its readiness probe (1 = ready, 0 = not ready). +/// +/// ## Notes +/// +/// This metric SHOULD reflect the value of the `ready` field in the +/// [K8s ContainerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#containerstatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{container}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_READY: &str = "k8s.container.ready"; + +/// ## Description +/// +/// Describes how many times the container has restarted (since the last counter reset). +/// +/// ## Notes +/// +/// This value is pulled directly from the K8s API and the value can go indefinitely high and be reset to 0 +/// at any time depending on how your kubelet is configured to prune dead containers. +/// It is best to not depend too much on the exact value but rather look at it as +/// either == 0, in which case you can conclude there were no restarts in the recent past, or > 0, in which case +/// you can conclude there were restarts in the recent past, and not try and analyze the value beyond that +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{restart}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_RESTART_COUNT: &str = "k8s.container.restart.count"; + +/// ## Description +/// +/// Describes the number of K8s containers that are currently in a state for a given reason. +/// +/// ## Notes +/// +/// All possible container state reasons will be reported at each time interval to avoid missing metrics. +/// Only the value corresponding to the current state reason will be non-zero +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{container}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_CONTAINER_STATUS_REASON`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_STATUS_REASON: &str = "k8s.container.status.reason"; + +/// ## Description +/// +/// Describes the number of K8s containers that are currently in a given state. +/// +/// ## Notes +/// +/// All possible container states will be reported at each time interval to avoid missing metrics. +/// Only the value corresponding to the current state will be non-zero +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{container}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_CONTAINER_STATUS_STATE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_STATUS_STATE: &str = "k8s.container.status.state"; + +/// ## Description +/// +/// Maximum storage resource limit set for the container. +/// +/// ## Notes +/// +/// See for details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_STORAGE_LIMIT: &str = "k8s.container.storage.limit"; + +/// ## Description +/// +/// Storage resource requested for the container. +/// +/// ## Notes +/// +/// See for details +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CONTAINER_STORAGE_REQUEST: &str = "k8s.container.storage.request"; + +/// ## Description +/// +/// Deprecated, use `k8s.cronjob.job.active` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `active` field of the +/// [K8s CronJobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#cronjobstatus-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{job}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.cronjob.job.active`., reason: renamed, renamed_to: k8s.cronjob.job.active}" +)] +pub const K8S_CRONJOB_ACTIVE_JOBS: &str = "k8s.cronjob.active_jobs"; + +/// ## Description +/// +/// The number of actively running jobs for a cronjob. +/// +/// ## Notes +/// +/// This metric aligns with the `active` field of the +/// [K8s CronJobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#cronjobstatus-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{job}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_CRONJOB_JOB_ACTIVE: &str = "k8s.cronjob.job.active"; + +/// ## Description +/// +/// Deprecated, use `k8s.daemonset.node.current_scheduled` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `currentNumberScheduled` field of the +/// [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{node}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.daemonset.node.current_scheduled`., reason: renamed, renamed_to: k8s.daemonset.node.current_scheduled}" +)] +pub const K8S_DAEMONSET_CURRENT_SCHEDULED_NODES: &str = "k8s.daemonset.current_scheduled_nodes"; + +/// ## Description +/// +/// Deprecated, use `k8s.daemonset.node.desired_scheduled` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `desiredNumberScheduled` field of the +/// [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{node}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.daemonset.node.desired_scheduled`., reason: renamed, renamed_to: k8s.daemonset.node.desired_scheduled}" +)] +pub const K8S_DAEMONSET_DESIRED_SCHEDULED_NODES: &str = "k8s.daemonset.desired_scheduled_nodes"; + +/// ## Description +/// +/// Deprecated, use `k8s.daemonset.node.misscheduled` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `numberMisscheduled` field of the +/// [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{node}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.daemonset.node.misscheduled`., reason: renamed, renamed_to: k8s.daemonset.node.misscheduled}" +)] +pub const K8S_DAEMONSET_MISSCHEDULED_NODES: &str = "k8s.daemonset.misscheduled_nodes"; + +/// ## Description +/// +/// Number of nodes that are running at least 1 daemon pod and are supposed to run the daemon pod. +/// +/// ## Notes +/// +/// This metric aligns with the `currentNumberScheduled` field of the +/// [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{node}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_DAEMONSET_NODE_CURRENT_SCHEDULED: &str = "k8s.daemonset.node.current_scheduled"; + +/// ## Description +/// +/// Number of nodes that should be running the daemon pod (including nodes currently running the daemon pod). +/// +/// ## Notes +/// +/// This metric aligns with the `desiredNumberScheduled` field of the +/// [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{node}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_DAEMONSET_NODE_DESIRED_SCHEDULED: &str = "k8s.daemonset.node.desired_scheduled"; + +/// ## Description +/// +/// Number of nodes that are running the daemon pod, but are not supposed to run the daemon pod. +/// +/// ## Notes +/// +/// This metric aligns with the `numberMisscheduled` field of the +/// [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{node}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_DAEMONSET_NODE_MISSCHEDULED: &str = "k8s.daemonset.node.misscheduled"; + +/// ## Description +/// +/// Number of nodes that should be running the daemon pod and have one or more of the daemon pod running and ready. +/// +/// ## Notes +/// +/// This metric aligns with the `numberReady` field of the +/// [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{node}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_DAEMONSET_NODE_READY: &str = "k8s.daemonset.node.ready"; + +/// ## Description +/// +/// Deprecated, use `k8s.daemonset.node.ready` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `numberReady` field of the +/// [K8s DaemonSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#daemonsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{node}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.daemonset.node.ready`., reason: renamed, renamed_to: k8s.daemonset.node.ready}" +)] +pub const K8S_DAEMONSET_READY_NODES: &str = "k8s.daemonset.ready_nodes"; + +/// ## Description +/// +/// Deprecated, use `k8s.deployment.pod.available` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `availableReplicas` field of the +/// [K8s DeploymentStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#deploymentstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.deployment.pod.available`., reason: renamed, renamed_to: k8s.deployment.pod.available}" +)] +pub const K8S_DEPLOYMENT_AVAILABLE_PODS: &str = "k8s.deployment.available_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.deployment.pod.desired` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `replicas` field of the +/// [K8s DeploymentSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#deploymentspec-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.deployment.pod.desired`., reason: renamed, renamed_to: k8s.deployment.pod.desired}" +)] +pub const K8S_DEPLOYMENT_DESIRED_PODS: &str = "k8s.deployment.desired_pods"; + +/// ## Description +/// +/// Total number of available replica pods (ready for at least minReadySeconds) targeted by this deployment. +/// +/// ## Notes +/// +/// This metric aligns with the `availableReplicas` field of the +/// [K8s DeploymentStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#deploymentstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_DEPLOYMENT_POD_AVAILABLE: &str = "k8s.deployment.pod.available"; + +/// ## Description +/// +/// Number of desired replica pods in this deployment. +/// +/// ## Notes +/// +/// This metric aligns with the `replicas` field of the +/// [K8s DeploymentSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#deploymentspec-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_DEPLOYMENT_POD_DESIRED: &str = "k8s.deployment.pod.desired"; + +/// ## Description +/// +/// Deprecated, use `k8s.hpa.pod.current` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `currentReplicas` field of the +/// [K8s HorizontalPodAutoscalerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerstatus-v2-autoscaling) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.hpa.pod.current`., reason: renamed, renamed_to: k8s.hpa.pod.current}" +)] +pub const K8S_HPA_CURRENT_PODS: &str = "k8s.hpa.current_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.hpa.pod.desired` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `desiredReplicas` field of the +/// [K8s HorizontalPodAutoscalerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerstatus-v2-autoscaling) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.hpa.pod.desired`., reason: renamed, renamed_to: k8s.hpa.pod.desired}" +)] +pub const K8S_HPA_DESIRED_PODS: &str = "k8s.hpa.desired_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.hpa.pod.max` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `maxReplicas` field of the +/// [K8s HorizontalPodAutoscalerSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerspec-v2-autoscaling) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.hpa.pod.max`., reason: renamed, renamed_to: k8s.hpa.pod.max}" +)] +pub const K8S_HPA_MAX_PODS: &str = "k8s.hpa.max_pods"; + +/// ## Description +/// +/// Target average utilization, in percentage, for CPU resource in HPA config. +/// +/// ## Notes +/// +/// This metric aligns with the `averageUtilization` field of the +/// [K8s HPA MetricTarget](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#metrictarget-v2-autoscaling). +/// If the type of the metric is [`ContainerResource`](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#support-for-metrics-apis), +/// the `k8s.container.name` attribute MUST be set to identify the specific container within the pod to which the metric applies +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_CONTAINER_NAME`] | `Conditionally_required`: if and only if k8s.hpa.metric.type is ContainerResource. +/// | [`crate::attribute::K8S_HPA_METRIC_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_METRIC_TARGET_CPU_AVERAGE_UTILIZATION: &str = + "k8s.hpa.metric.target.cpu.average_utilization"; + +/// ## Description +/// +/// Target average value for CPU resource in HPA config. +/// +/// ## Notes +/// +/// This metric aligns with the `averageValue` field of the +/// [K8s HPA MetricTarget](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#metrictarget-v2-autoscaling). +/// If the type of the metric is [`ContainerResource`](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#support-for-metrics-apis), +/// the `k8s.container.name` attribute MUST be set to identify the specific container within the pod to which the metric applies +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_CONTAINER_NAME`] | `Conditionally_required`: if and only if k8s.hpa.metric.type is ContainerResource +/// | [`crate::attribute::K8S_HPA_METRIC_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_METRIC_TARGET_CPU_AVERAGE_VALUE: &str = "k8s.hpa.metric.target.cpu.average_value"; + +/// ## Description +/// +/// Target value for CPU resource in HPA config. +/// +/// ## Notes +/// +/// This metric aligns with the `value` field of the +/// [K8s HPA MetricTarget](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#metrictarget-v2-autoscaling). +/// If the type of the metric is [`ContainerResource`](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#support-for-metrics-apis), +/// the `k8s.container.name` attribute MUST be set to identify the specific container within the pod to which the metric applies +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_CONTAINER_NAME`] | `Conditionally_required`: if and only if k8s.hpa.metric.type is ContainerResource +/// | [`crate::attribute::K8S_HPA_METRIC_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_METRIC_TARGET_CPU_VALUE: &str = "k8s.hpa.metric.target.cpu.value"; + +/// ## Description +/// +/// Deprecated, use `k8s.hpa.pod.min` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `minReplicas` field of the +/// [K8s HorizontalPodAutoscalerSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerspec-v2-autoscaling) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.hpa.pod.min`., reason: renamed, renamed_to: k8s.hpa.pod.min}" +)] +pub const K8S_HPA_MIN_PODS: &str = "k8s.hpa.min_pods"; + +/// ## Description +/// +/// Current number of replica pods managed by this horizontal pod autoscaler, as last seen by the autoscaler. +/// +/// ## Notes +/// +/// This metric aligns with the `currentReplicas` field of the +/// [K8s HorizontalPodAutoscalerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerstatus-v2-autoscaling) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_POD_CURRENT: &str = "k8s.hpa.pod.current"; + +/// ## Description +/// +/// Desired number of replica pods managed by this horizontal pod autoscaler, as last calculated by the autoscaler. +/// +/// ## Notes +/// +/// This metric aligns with the `desiredReplicas` field of the +/// [K8s HorizontalPodAutoscalerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerstatus-v2-autoscaling) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_POD_DESIRED: &str = "k8s.hpa.pod.desired"; + +/// ## Description +/// +/// The upper limit for the number of replica pods to which the autoscaler can scale up. +/// +/// ## Notes +/// +/// This metric aligns with the `maxReplicas` field of the +/// [K8s HorizontalPodAutoscalerSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerspec-v2-autoscaling) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_POD_MAX: &str = "k8s.hpa.pod.max"; + +/// ## Description +/// +/// The lower limit for the number of replica pods to which the autoscaler can scale down. +/// +/// ## Notes +/// +/// This metric aligns with the `minReplicas` field of the +/// [K8s HorizontalPodAutoscalerSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#horizontalpodautoscalerspec-v2-autoscaling) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_HPA_POD_MIN: &str = "k8s.hpa.pod.min"; + +/// ## Description +/// +/// Deprecated, use `k8s.job.pod.active` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `active` field of the +/// [K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobstatus-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.job.pod.active`., reason: renamed, renamed_to: k8s.job.pod.active}" +)] +pub const K8S_JOB_ACTIVE_PODS: &str = "k8s.job.active_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.job.pod.desired_successful` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `completions` field of the +/// [K8s JobSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobspec-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.job.pod.desired_successful`., reason: renamed, renamed_to: k8s.job.pod.desired_successful}" +)] +pub const K8S_JOB_DESIRED_SUCCESSFUL_PODS: &str = "k8s.job.desired_successful_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.job.pod.failed` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `failed` field of the +/// [K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobstatus-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.job.pod.failed`., reason: renamed, renamed_to: k8s.job.pod.failed}" +)] +pub const K8S_JOB_FAILED_PODS: &str = "k8s.job.failed_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.job.pod.max_parallel` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `parallelism` field of the +/// [K8s JobSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobspec-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.job.pod.max_parallel`., reason: renamed, renamed_to: k8s.job.pod.max_parallel}" +)] +pub const K8S_JOB_MAX_PARALLEL_PODS: &str = "k8s.job.max_parallel_pods"; + +/// ## Description +/// +/// The number of pending and actively running pods for a job. +/// +/// ## Notes +/// +/// This metric aligns with the `active` field of the +/// [K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobstatus-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_JOB_POD_ACTIVE: &str = "k8s.job.pod.active"; + +/// ## Description +/// +/// The desired number of successfully finished pods the job should be run with. +/// +/// ## Notes +/// +/// This metric aligns with the `completions` field of the +/// [K8s JobSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobspec-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_JOB_POD_DESIRED_SUCCESSFUL: &str = "k8s.job.pod.desired_successful"; + +/// ## Description +/// +/// The number of pods which reached phase Failed for a job. +/// +/// ## Notes +/// +/// This metric aligns with the `failed` field of the +/// [K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobstatus-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_JOB_POD_FAILED: &str = "k8s.job.pod.failed"; + +/// ## Description +/// +/// The max desired number of pods the job should run at any given time. +/// +/// ## Notes +/// +/// This metric aligns with the `parallelism` field of the +/// [K8s JobSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobspec-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_JOB_POD_MAX_PARALLEL: &str = "k8s.job.pod.max_parallel"; + +/// ## Description +/// +/// The number of pods which reached phase Succeeded for a job. +/// +/// ## Notes +/// +/// This metric aligns with the `succeeded` field of the +/// [K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobstatus-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_JOB_POD_SUCCESSFUL: &str = "k8s.job.pod.successful"; + +/// ## Description +/// +/// Deprecated, use `k8s.job.pod.successful` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `succeeded` field of the +/// [K8s JobStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#jobstatus-v1-batch) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.job.pod.successful`., reason: renamed, renamed_to: k8s.job.pod.successful}" +)] +pub const K8S_JOB_SUCCESSFUL_PODS: &str = "k8s.job.successful_pods"; + +/// ## Description +/// +/// Describes number of K8s namespaces that are currently in a given phase +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{namespace}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_NAMESPACE_PHASE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_NAMESPACE_PHASE: &str = "k8s.namespace.phase"; + +/// ## Description +/// +/// Deprecated, use `k8s.node.cpu.allocatable` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.node.cpu.allocatable`., reason: renamed, renamed_to: k8s.node.cpu.allocatable}" +)] +pub const K8S_NODE_ALLOCATABLE_CPU: &str = "k8s.node.allocatable.cpu"; + +/// ## Description +/// +/// Deprecated, use `k8s.node.ephemeral_storage.allocatable` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.node.ephemeral_storage.allocatable`., reason: renamed, renamed_to: k8s.node.ephemeral_storage.allocatable}" +)] +pub const K8S_NODE_ALLOCATABLE_EPHEMERAL_STORAGE: &str = "k8s.node.allocatable.ephemeral_storage"; + +/// ## Description +/// +/// Deprecated, use `k8s.node.memory.allocatable` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.node.memory.allocatable`., reason: renamed, renamed_to: k8s.node.memory.allocatable}" +)] +pub const K8S_NODE_ALLOCATABLE_MEMORY: &str = "k8s.node.allocatable.memory"; + +/// ## Description +/// +/// Deprecated, use `k8s.node.pod.allocatable` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.node.pod.allocatable`., reason: renamed, renamed_to: k8s.node.pod.allocatable}" +)] +pub const K8S_NODE_ALLOCATABLE_PODS: &str = "k8s.node.allocatable.pods"; + +/// ## Description +/// +/// Describes the condition of a particular Node. +/// +/// ## Notes +/// +/// All possible node condition pairs (type and status) will be reported at each time interval to avoid missing metrics. Condition pairs corresponding to the current conditions' statuses will be non-zero +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{node}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_NODE_CONDITION_STATUS`] | `Required` +/// | [`crate::attribute::K8S_NODE_CONDITION_TYPE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_CONDITION_STATUS: &str = "k8s.node.condition.status"; + +/// ## Description +/// +/// Amount of cpu allocatable on the node +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_CPU_ALLOCATABLE: &str = "k8s.node.cpu.allocatable"; + +/// ## Description +/// +/// Total CPU time consumed. +/// +/// ## Notes +/// +/// Total CPU time consumed by the specific Node on all available CPU cores +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Release_candidate` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_CPU_TIME: &str = "k8s.node.cpu.time"; + +/// ## Description +/// +/// Node's CPU usage, measured in cpus. Range from 0 to the number of allocatable CPUs. +/// +/// ## Notes +/// +/// CPU usage of the specific Node on all available CPU cores, averaged over the sample window +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_CPU_USAGE: &str = "k8s.node.cpu.usage"; + +/// ## Description +/// +/// Amount of ephemeral-storage allocatable on the node +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_EPHEMERAL_STORAGE_ALLOCATABLE: &str = "k8s.node.ephemeral_storage.allocatable"; + +/// ## Description +/// +/// Node filesystem available bytes. +/// +/// ## Notes +/// +/// This metric is derived from the +/// [FsStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field +/// of the [NodeStats.Fs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#NodeStats) +/// of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_FILESYSTEM_AVAILABLE: &str = "k8s.node.filesystem.available"; + +/// ## Description +/// +/// Node filesystem capacity. +/// +/// ## Notes +/// +/// This metric is derived from the +/// [FsStats.CapacityBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field +/// of the [NodeStats.Fs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#NodeStats) +/// of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_FILESYSTEM_CAPACITY: &str = "k8s.node.filesystem.capacity"; + +/// ## Description +/// +/// Node filesystem usage. +/// +/// ## Notes +/// +/// This may not equal capacity - available. +/// +/// This metric is derived from the +/// [FsStats.UsedBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field +/// of the [NodeStats.Fs](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#NodeStats) +/// of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_FILESYSTEM_USAGE: &str = "k8s.node.filesystem.usage"; + +/// ## Description +/// +/// Amount of memory allocatable on the node +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_MEMORY_ALLOCATABLE: &str = "k8s.node.memory.allocatable"; + +/// ## Description +/// +/// Node memory available. +/// +/// ## Notes +/// +/// Available memory for use. This is defined as the memory limit - workingSetBytes. If memory limit is undefined, the available bytes is omitted. +/// This metric is derived from the [MemoryStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [NodeStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_MEMORY_AVAILABLE: &str = "k8s.node.memory.available"; + +/// ## Description +/// +/// Node memory paging faults. +/// +/// ## Notes +/// +/// Cumulative number of major/minor page faults. +/// This metric is derived from the [MemoryStats.PageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) and [MemoryStats.MajorPageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) fields of the [NodeStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{fault}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_PAGING_FAULT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_MEMORY_PAGING_FAULTS: &str = "k8s.node.memory.paging.faults"; + +/// ## Description +/// +/// Node memory RSS. +/// +/// ## Notes +/// +/// The amount of anonymous and swap cache memory (includes transparent hugepages). +/// This metric is derived from the [MemoryStats.RSSBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [NodeStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_MEMORY_RSS: &str = "k8s.node.memory.rss"; + +/// ## Description +/// +/// Memory usage of the Node. +/// +/// ## Notes +/// +/// Total memory usage of the Node +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_MEMORY_USAGE: &str = "k8s.node.memory.usage"; + +/// ## Description +/// +/// Node memory working set. +/// +/// ## Notes +/// +/// The amount of working set memory. This includes recently accessed memory, dirty memory, and kernel memory. WorkingSetBytes is <= UsageBytes. +/// This metric is derived from the [MemoryStats.WorkingSetBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [NodeStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#NodeStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_MEMORY_WORKING_SET: &str = "k8s.node.memory.working_set"; + +/// ## Description +/// +/// Node network errors +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{error}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_INTERFACE_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_NETWORK_ERRORS: &str = "k8s.node.network.errors"; + +/// ## Description +/// +/// Network bytes for the Node +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_INTERFACE_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_NETWORK_IO: &str = "k8s.node.network.io"; + +/// ## Description +/// +/// Amount of pods allocatable on the node +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_POD_ALLOCATABLE: &str = "k8s.node.pod.allocatable"; + +/// ## Description +/// +/// Node's system container CPU time. +/// +/// ## Notes +/// +/// This metric is derived from the [CPUStats.UsageCoreNanoSeconds](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L236) field of the [ContainerStats](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L157C6-L157C20) of [Node.SystemContainers](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L40) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_SYSTEM_CONTAINER_CPU_TIME: &str = "k8s.node.system_container.cpu.time"; + +/// ## Description +/// +/// Node's system container CPU usage, measured in cpus. +/// +/// ## Notes +/// +/// This metric is derived from the [CPUStats.UsageNanoCores](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L233) field of the [ContainerStats](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L157C6-L157C20) of [Node.SystemContainers](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L40) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_SYSTEM_CONTAINER_CPU_USAGE: &str = "k8s.node.system_container.cpu.usage"; + +/// ## Description +/// +/// Node's system container memory usage. +/// +/// ## Notes +/// +/// This metric is derived from the [MemoryStats.UsageBytes](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L252) field of the [ContainerStats](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L157C6-L157C20) of [Node.SystemContainers](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L40) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_SYSTEM_CONTAINER_MEMORY_USAGE: &str = "k8s.node.system_container.memory.usage"; + +/// ## Description +/// +/// The amount of working set memory. +/// +/// ## Notes +/// +/// This metric is derived from the [MemoryStats.WorkingSetBytes](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L256) field of the [ContainerStats](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L157C6-L157C20) of [Node.SystemContainers](https://github.com/kubernetes/kubelet/blob/v0.35.2/pkg/apis/stats/v1alpha1/types.go#L40) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_SYSTEM_CONTAINER_MEMORY_WORKING_SET: &str = + "k8s.node.system_container.memory.working_set"; + +/// ## Description +/// +/// The time the Node has been running. +/// +/// ## Notes +/// +/// Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available. +/// The actual accuracy would depend on the instrumentation and operating system +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_NODE_UPTIME: &str = "k8s.node.uptime"; + +/// ## Description +/// +/// Number of PersistentVolumes in a given phase. +/// +/// ## Notes +/// +/// All possible phases should be reported at each interval to avoid gaps in the time series. +/// This metric is derived from the `.status.phase` field of the +/// [K8s PersistentVolumeStatus](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/#PersistentVolumeStatus) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{persistentvolume}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_PERSISTENTVOLUME_STATUS_PHASE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUME_STATUS_PHASE: &str = "k8s.persistentvolume.status.phase"; + +/// ## Description +/// +/// The storage capacity of the PersistentVolume. +/// +/// ## Notes +/// +/// This metric is derived from the `.spec.capacity.storage` field of the [K8s PersistentVolumeSpec](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-v1/#PersistentVolumeSpec) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUME_STORAGE_CAPACITY: &str = "k8s.persistentvolume.storage.capacity"; + +/// ## Description +/// +/// Number of PersistentVolumeClaims in a given phase. +/// +/// ## Notes +/// +/// All possible phases should be reported at each interval to avoid gaps in the time series. +/// This metric is derived from the `.status.phase` field of the +/// [K8s PersistentVolumeClaimStatus](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-claim-v1/#PersistentVolumeClaimStatus) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{persistentvolumeclaim}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_PERSISTENTVOLUMECLAIM_STATUS_PHASE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUMECLAIM_STATUS_PHASE: &str = "k8s.persistentvolumeclaim.status.phase"; + +/// ## Description +/// +/// The actual storage capacity provisioned for the PersistentVolumeClaim. +/// +/// ## Notes +/// +/// Only available when the PVC is bound. May differ from the requested capacity due to provisioner rounding. +/// This metric is derived from the `.status.capacity.storage` field of the +/// [K8s PersistentVolumeClaimStatus](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-claim-v1/#PersistentVolumeClaimStatus) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUMECLAIM_STORAGE_CAPACITY: &str = + "k8s.persistentvolumeclaim.storage.capacity"; + +/// ## Description +/// +/// The storage requested by the PersistentVolumeClaim. +/// +/// ## Notes +/// +/// This metric is derived from the `.spec.resources.requests.storage` field of the [K8s PersistentVolumeClaimSpec](https://kubernetes.io/docs/reference/kubernetes-api/config-and-storage-resources/persistent-volume-claim-v1/#PersistentVolumeClaimSpec) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_PERSISTENTVOLUMECLAIM_STORAGE_REQUEST: &str = + "k8s.persistentvolumeclaim.storage.request"; + +/// ## Description +/// +/// Total CPU time consumed. +/// +/// ## Notes +/// +/// Total CPU time consumed by the specific Pod on all available CPU cores +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Release_candidate` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_CPU_TIME: &str = "k8s.pod.cpu.time"; + +/// ## Description +/// +/// Pod's CPU usage, measured in cpus. Range from 0 to the number of allocatable CPUs. +/// +/// ## Notes +/// +/// CPU usage of the specific Pod on all available CPU cores, averaged over the sample window +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_CPU_USAGE: &str = "k8s.pod.cpu.usage"; + +/// ## Description +/// +/// Pod filesystem available bytes. +/// +/// ## Notes +/// +/// This metric is derived from the +/// [FsStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field +/// of the [PodStats.EphemeralStorage](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) +/// of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_FILESYSTEM_AVAILABLE: &str = "k8s.pod.filesystem.available"; + +/// ## Description +/// +/// Pod filesystem capacity. +/// +/// ## Notes +/// +/// This metric is derived from the +/// [FsStats.CapacityBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field +/// of the [PodStats.EphemeralStorage](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) +/// of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_FILESYSTEM_CAPACITY: &str = "k8s.pod.filesystem.capacity"; + +/// ## Description +/// +/// Pod filesystem usage. +/// +/// ## Notes +/// +/// This may not equal capacity - available. +/// +/// This metric is derived from the +/// [FsStats.UsedBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#FsStats) field +/// of the [PodStats.EphemeralStorage](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) +/// of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_FILESYSTEM_USAGE: &str = "k8s.pod.filesystem.usage"; + +/// ## Description +/// +/// Pod memory available. +/// +/// ## Notes +/// +/// Available memory for use. This is defined as the memory limit - workingSetBytes. If memory limit is undefined, the available bytes is omitted. +/// This metric is derived from the [MemoryStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_MEMORY_AVAILABLE: &str = "k8s.pod.memory.available"; + +/// ## Description +/// +/// Pod memory paging faults. +/// +/// ## Notes +/// +/// Cumulative number of major/minor page faults. +/// This metric is derived from the [MemoryStats.PageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) and [MemoryStats.MajorPageFaults](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{fault}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_PAGING_FAULT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_MEMORY_PAGING_FAULTS: &str = "k8s.pod.memory.paging.faults"; + +/// ## Description +/// +/// Pod memory RSS. +/// +/// ## Notes +/// +/// The amount of anonymous and swap cache memory (includes transparent hugepages). +/// This metric is derived from the [MemoryStats.RSSBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_MEMORY_RSS: &str = "k8s.pod.memory.rss"; + +/// ## Description +/// +/// Memory usage of the Pod. +/// +/// ## Notes +/// +/// Total memory usage of the Pod +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_MEMORY_USAGE: &str = "k8s.pod.memory.usage"; + +/// ## Description +/// +/// Pod memory working set. +/// +/// ## Notes +/// +/// The amount of working set memory. This includes recently accessed memory, dirty memory, and kernel memory. WorkingSetBytes is <= UsageBytes. +/// This metric is derived from the [MemoryStats.WorkingSetBytes](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#MemoryStats) field of the [PodStats.Memory](https://pkg.go.dev/k8s.io/kubelet@v0.34.0/pkg/apis/stats/v1alpha1#PodStats) of the Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_MEMORY_WORKING_SET: &str = "k8s.pod.memory.working_set"; + +/// ## Description +/// +/// Pod network errors +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{error}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_INTERFACE_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_NETWORK_ERRORS: &str = "k8s.pod.network.errors"; + +/// ## Description +/// +/// Network bytes for the Pod +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_INTERFACE_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_NETWORK_IO: &str = "k8s.pod.network.io"; + +/// ## Description +/// +/// Describes number of K8s Pods that are currently in a given phase. +/// +/// ## Notes +/// +/// All possible pod phases will be reported at each time interval to avoid missing metrics. +/// Only the value corresponding to the current phase will be non-zero +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_POD_STATUS_PHASE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_STATUS_PHASE: &str = "k8s.pod.status.phase"; + +/// ## Description +/// +/// Describes the number of K8s Pods that are currently in a state for a given reason. +/// +/// ## Notes +/// +/// All possible pod status reasons will be reported at each time interval to avoid missing metrics. +/// Only the value corresponding to the current reason will be non-zero +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_POD_STATUS_REASON`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_STATUS_REASON: &str = "k8s.pod.status.reason"; + +/// ## Description +/// +/// The time the Pod has been running. +/// +/// ## Notes +/// +/// Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available. +/// The actual accuracy would depend on the instrumentation and operating system +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_UPTIME: &str = "k8s.pod.uptime"; + +/// ## Description +/// +/// Pod volume storage space available. +/// +/// ## Notes +/// +/// This metric is derived from the +/// [VolumeStats.AvailableBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field +/// of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the +/// Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_VOLUME_NAME`] | `Required` +/// | [`crate::attribute::K8S_VOLUME_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_VOLUME_AVAILABLE: &str = "k8s.pod.volume.available"; + +/// ## Description +/// +/// Pod volume total capacity. +/// +/// ## Notes +/// +/// This metric is derived from the +/// [VolumeStats.CapacityBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field +/// of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the +/// Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_VOLUME_NAME`] | `Required` +/// | [`crate::attribute::K8S_VOLUME_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_VOLUME_CAPACITY: &str = "k8s.pod.volume.capacity"; + +/// ## Description +/// +/// The total inodes in the filesystem of the Pod's volume. +/// +/// ## Notes +/// +/// This metric is derived from the +/// [VolumeStats.Inodes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field +/// of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the +/// Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{inode}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_VOLUME_NAME`] | `Required` +/// | [`crate::attribute::K8S_VOLUME_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_VOLUME_INODE_COUNT: &str = "k8s.pod.volume.inode.count"; + +/// ## Description +/// +/// The free inodes in the filesystem of the Pod's volume. +/// +/// ## Notes +/// +/// This metric is derived from the +/// [VolumeStats.InodesFree](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field +/// of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the +/// Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{inode}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_VOLUME_NAME`] | `Required` +/// | [`crate::attribute::K8S_VOLUME_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_VOLUME_INODE_FREE: &str = "k8s.pod.volume.inode.free"; + +/// ## Description +/// +/// The inodes used by the filesystem of the Pod's volume. +/// +/// ## Notes +/// +/// This metric is derived from the +/// [VolumeStats.InodesUsed](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field +/// of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the +/// Kubelet's stats API. +/// +/// This may not be equal to `inodes - free` because filesystem may share inodes with other filesystems +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{inode}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_VOLUME_NAME`] | `Required` +/// | [`crate::attribute::K8S_VOLUME_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_VOLUME_INODE_USED: &str = "k8s.pod.volume.inode.used"; + +/// ## Description +/// +/// Pod volume usage. +/// +/// ## Notes +/// +/// This may not equal capacity - available. +/// +/// This metric is derived from the +/// [VolumeStats.UsedBytes](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#VolumeStats) field +/// of the [PodStats](https://pkg.go.dev/k8s.io/kubelet@v0.33.0/pkg/apis/stats/v1alpha1#PodStats) of the +/// Kubelet's stats API +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_VOLUME_NAME`] | `Required` +/// | [`crate::attribute::K8S_VOLUME_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_POD_VOLUME_USAGE: &str = "k8s.pod.volume.usage"; + +/// ## Description +/// +/// Deprecated, use `k8s.replicaset.pod.available` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `availableReplicas` field of the +/// [K8s ReplicaSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#replicasetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.replicaset.pod.available`., reason: renamed, renamed_to: k8s.replicaset.pod.available}" +)] +pub const K8S_REPLICASET_AVAILABLE_PODS: &str = "k8s.replicaset.available_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.replicaset.pod.desired` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `replicas` field of the +/// [K8s ReplicaSetSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#replicasetspec-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.replicaset.pod.desired`., reason: renamed, renamed_to: k8s.replicaset.pod.desired}" +)] +pub const K8S_REPLICASET_DESIRED_PODS: &str = "k8s.replicaset.desired_pods"; + +/// ## Description +/// +/// Total number of available replica pods (ready for at least minReadySeconds) targeted by this replicaset. +/// +/// ## Notes +/// +/// This metric aligns with the `availableReplicas` field of the +/// [K8s ReplicaSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#replicasetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_REPLICASET_POD_AVAILABLE: &str = "k8s.replicaset.pod.available"; + +/// ## Description +/// +/// Number of desired replica pods in this replicaset. +/// +/// ## Notes +/// +/// This metric aligns with the `replicas` field of the +/// [K8s ReplicaSetSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#replicasetspec-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_REPLICASET_POD_DESIRED: &str = "k8s.replicaset.pod.desired"; + +/// ## Description +/// +/// Deprecated, use `k8s.replicationcontroller.pod.available` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.replicationcontroller.pod.available`., reason: renamed, renamed_to: k8s.replicationcontroller.pod.available}" +)] +pub const K8S_REPLICATION_CONTROLLER_AVAILABLE_PODS: &str = + "k8s.replication_controller.available_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.replicationcontroller.pod.desired` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.replicationcontroller.pod.desired`., reason: renamed, renamed_to: k8s.replicationcontroller.pod.desired}" +)] +pub const K8S_REPLICATION_CONTROLLER_DESIRED_PODS: &str = "k8s.replication_controller.desired_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.replicationcontroller.pod.available` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.replicationcontroller.pod.available`., reason: renamed, renamed_to: k8s.replicationcontroller.pod.available}" +)] +pub const K8S_REPLICATIONCONTROLLER_AVAILABLE_PODS: &str = + "k8s.replicationcontroller.available_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.replicationcontroller.pod.desired` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.replicationcontroller.pod.desired`., reason: renamed, renamed_to: k8s.replicationcontroller.pod.desired}" +)] +pub const K8S_REPLICATIONCONTROLLER_DESIRED_PODS: &str = "k8s.replicationcontroller.desired_pods"; + +/// ## Description +/// +/// Total number of available replica pods (ready for at least minReadySeconds) targeted by this replication controller. +/// +/// ## Notes +/// +/// This metric aligns with the `availableReplicas` field of the +/// [K8s ReplicationControllerStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#replicationcontrollerstatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_REPLICATIONCONTROLLER_POD_AVAILABLE: &str = "k8s.replicationcontroller.pod.available"; + +/// ## Description +/// +/// Number of desired replica pods in this replication controller. +/// +/// ## Notes +/// +/// This metric aligns with the `replicas` field of the +/// [K8s ReplicationControllerSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#replicationcontrollerspec-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_REPLICATIONCONTROLLER_POD_DESIRED: &str = "k8s.replicationcontroller.pod.desired"; + +/// ## Description +/// +/// The CPU limits in a specific namespace. +/// The value represents the configured quota limit of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_CPU_LIMIT_HARD: &str = "k8s.resourcequota.cpu.limit.hard"; + +/// ## Description +/// +/// The CPU limits in a specific namespace. +/// The value represents the current observed total usage of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_CPU_LIMIT_USED: &str = "k8s.resourcequota.cpu.limit.used"; + +/// ## Description +/// +/// The CPU requests in a specific namespace. +/// The value represents the configured quota limit of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_CPU_REQUEST_HARD: &str = "k8s.resourcequota.cpu.request.hard"; + +/// ## Description +/// +/// The CPU requests in a specific namespace. +/// The value represents the current observed total usage of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_CPU_REQUEST_USED: &str = "k8s.resourcequota.cpu.request.used"; + +/// ## Description +/// +/// The sum of local ephemeral storage limits in the namespace. +/// The value represents the configured quota limit of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_LIMIT_HARD: &str = + "k8s.resourcequota.ephemeral_storage.limit.hard"; + +/// ## Description +/// +/// The sum of local ephemeral storage limits in the namespace. +/// The value represents the current observed total usage of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_LIMIT_USED: &str = + "k8s.resourcequota.ephemeral_storage.limit.used"; + +/// ## Description +/// +/// The sum of local ephemeral storage requests in the namespace. +/// The value represents the configured quota limit of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_REQUEST_HARD: &str = + "k8s.resourcequota.ephemeral_storage.request.hard"; + +/// ## Description +/// +/// The sum of local ephemeral storage requests in the namespace. +/// The value represents the current observed total usage of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_EPHEMERAL_STORAGE_REQUEST_USED: &str = + "k8s.resourcequota.ephemeral_storage.request.used"; + +/// ## Description +/// +/// The huge page requests in a specific namespace. +/// The value represents the configured quota limit of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{hugepage}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_HUGEPAGE_SIZE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_HUGEPAGE_COUNT_REQUEST_HARD: &str = + "k8s.resourcequota.hugepage_count.request.hard"; + +/// ## Description +/// +/// The huge page requests in a specific namespace. +/// The value represents the current observed total usage of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{hugepage}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_HUGEPAGE_SIZE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_HUGEPAGE_COUNT_REQUEST_USED: &str = + "k8s.resourcequota.hugepage_count.request.used"; + +/// ## Description +/// +/// The memory limits in a specific namespace. +/// The value represents the configured quota limit of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_MEMORY_LIMIT_HARD: &str = "k8s.resourcequota.memory.limit.hard"; + +/// ## Description +/// +/// The memory limits in a specific namespace. +/// The value represents the current observed total usage of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_MEMORY_LIMIT_USED: &str = "k8s.resourcequota.memory.limit.used"; + +/// ## Description +/// +/// The memory requests in a specific namespace. +/// The value represents the configured quota limit of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_MEMORY_REQUEST_HARD: &str = "k8s.resourcequota.memory.request.hard"; + +/// ## Description +/// +/// The memory requests in a specific namespace. +/// The value represents the current observed total usage of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_MEMORY_REQUEST_USED: &str = "k8s.resourcequota.memory.request.used"; + +/// ## Description +/// +/// The object count limits in a specific namespace. +/// The value represents the configured quota limit of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{object}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_RESOURCEQUOTA_RESOURCE_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_OBJECT_COUNT_HARD: &str = "k8s.resourcequota.object_count.hard"; + +/// ## Description +/// +/// The object count limits in a specific namespace. +/// The value represents the current observed total usage of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{object}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_RESOURCEQUOTA_RESOURCE_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_OBJECT_COUNT_USED: &str = "k8s.resourcequota.object_count.used"; + +/// ## Description +/// +/// The total number of PersistentVolumeClaims that can exist in the namespace. +/// The value represents the configured quota limit of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). +/// +/// The `k8s.storageclass.name` should be required when a resource quota is defined for a specific +/// storage class +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{persistentvolumeclaim}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_STORAGECLASS_NAME`] | `Conditionally_required`: The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_PERSISTENTVOLUMECLAIM_COUNT_HARD: &str = + "k8s.resourcequota.persistentvolumeclaim_count.hard"; + +/// ## Description +/// +/// The total number of PersistentVolumeClaims that can exist in the namespace. +/// The value represents the current observed total usage of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). +/// +/// The `k8s.storageclass.name` should be required when a resource quota is defined for a specific +/// storage class +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{persistentvolumeclaim}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_STORAGECLASS_NAME`] | `Conditionally_required`: The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_PERSISTENTVOLUMECLAIM_COUNT_USED: &str = + "k8s.resourcequota.persistentvolumeclaim_count.used"; + +/// ## Description +/// +/// The storage requests in a specific namespace. +/// The value represents the configured quota limit of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). +/// +/// The `k8s.storageclass.name` should be required when a resource quota is defined for a specific +/// storage class +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_STORAGECLASS_NAME`] | `Conditionally_required`: The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_STORAGE_REQUEST_HARD: &str = "k8s.resourcequota.storage.request.hard"; + +/// ## Description +/// +/// The storage requests in a specific namespace. +/// The value represents the current observed total usage of the resource in the namespace. +/// +/// ## Notes +/// +/// This metric is retrieved from the `used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core). +/// +/// The `k8s.storageclass.name` should be required when a resource quota is defined for a specific +/// storage class +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_STORAGECLASS_NAME`] | `Conditionally_required`: The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. +#[cfg(feature = "semconv_experimental")] +pub const K8S_RESOURCEQUOTA_STORAGE_REQUEST_USED: &str = "k8s.resourcequota.storage.request.used"; + +/// ## Description +/// +/// Number of endpoints for a service by condition and address type. +/// +/// ## Notes +/// +/// This metric is derived from the Kubernetes [EndpointSlice API](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/). +/// It reports the number of network endpoints backing a Service, broken down by their condition and address type. +/// +/// In dual-stack or multi-protocol clusters, separate counts are reported for each address family (`IPv4`, `IPv6`, `FQDN`). +/// +/// When the optional `zone` attribute is enabled, counts are further broken down by availability zone for zone-aware monitoring. +/// +/// An endpoint may be reported under multiple conditions simultaneously (e.g., both `serving` and `terminating` during a graceful shutdown). +/// See [K8s EndpointConditions](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/) for more details. +/// +/// The conditions represent: +/// +/// - `ready`: Endpoints capable of receiving new connections. +/// - `serving`: Endpoints currently handling traffic. +/// - `terminating`: Endpoints that are being phased out but may still be handling existing connections. +/// +/// For Services with `publishNotReadyAddresses` enabled (common for headless StatefulSets), +/// this metric will include endpoints that are published despite not being ready. +/// The `k8s.service.publish_not_ready_addresses` resource attribute indicates this setting +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{endpoint}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_SERVICE_ENDPOINT_ADDRESS_TYPE`] | `Required` +/// | [`crate::attribute::K8S_SERVICE_ENDPOINT_CONDITION`] | `Required` +/// | [`crate::attribute::K8S_SERVICE_ENDPOINT_ZONE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_ENDPOINT_COUNT: &str = "k8s.service.endpoint.count"; + +/// ## Description +/// +/// Number of load balancer ingress points (external IPs/hostnames) assigned to the service. +/// +/// ## Notes +/// +/// This metric reports the number of external ingress points (IP addresses or hostnames) +/// assigned to a LoadBalancer Service. +/// +/// It is only emitted for Services of type `LoadBalancer` and reflects the assignments +/// made by the underlying infrastructure's load balancer controller in the +/// [.status.loadBalancer.ingress](https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/#ServiceStatus) field. +/// +/// A value of `0` indicates that no ingress points have been assigned yet (e.g., during provisioning). +/// A value greater than `1` may occur when multiple IPs or hostnames are assigned (e.g., dual-stack configurations). +/// +/// This metric signals that external endpoints have been assigned by the load balancer controller, but it does not +/// guarantee that the load balancer is healthy +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{ingress}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_SERVICE_LOAD_BALANCER_INGRESS_COUNT: &str = "k8s.service.load_balancer.ingress.count"; + +/// ## Description +/// +/// Deprecated, use `k8s.statefulset.pod.current` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `currentReplicas` field of the +/// [K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.statefulset.pod.current`., reason: renamed, renamed_to: k8s.statefulset.pod.current}" +)] +pub const K8S_STATEFULSET_CURRENT_PODS: &str = "k8s.statefulset.current_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.statefulset.pod.desired` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `replicas` field of the +/// [K8s StatefulSetSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetspec-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.statefulset.pod.desired`., reason: renamed, renamed_to: k8s.statefulset.pod.desired}" +)] +pub const K8S_STATEFULSET_DESIRED_PODS: &str = "k8s.statefulset.desired_pods"; + +/// ## Description +/// +/// The number of replica pods created by the statefulset controller from the statefulset version indicated by currentRevision. +/// +/// ## Notes +/// +/// This metric aligns with the `currentReplicas` field of the +/// [K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_STATEFULSET_POD_CURRENT: &str = "k8s.statefulset.pod.current"; + +/// ## Description +/// +/// Number of desired replica pods in this statefulset. +/// +/// ## Notes +/// +/// This metric aligns with the `replicas` field of the +/// [K8s StatefulSetSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetspec-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_STATEFULSET_POD_DESIRED: &str = "k8s.statefulset.pod.desired"; + +/// ## Description +/// +/// The number of replica pods created for this statefulset with a Ready Condition. +/// +/// ## Notes +/// +/// This metric aligns with the `readyReplicas` field of the +/// [K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_STATEFULSET_POD_READY: &str = "k8s.statefulset.pod.ready"; + +/// ## Description +/// +/// Number of replica pods created by the statefulset controller from the statefulset version indicated by updateRevision. +/// +/// ## Notes +/// +/// This metric aligns with the `updatedReplicas` field of the +/// [K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const K8S_STATEFULSET_POD_UPDATED: &str = "k8s.statefulset.pod.updated"; + +/// ## Description +/// +/// Deprecated, use `k8s.statefulset.pod.ready` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `readyReplicas` field of the +/// [K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.statefulset.pod.ready`., reason: renamed, renamed_to: k8s.statefulset.pod.ready}" +)] +pub const K8S_STATEFULSET_READY_PODS: &str = "k8s.statefulset.ready_pods"; + +/// ## Description +/// +/// Deprecated, use `k8s.statefulset.pod.updated` instead. +/// +/// ## Notes +/// +/// This metric aligns with the `updatedReplicas` field of the +/// [K8s StatefulSetStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#statefulsetstatus-v1-apps) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{pod}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `k8s.statefulset.pod.updated`., reason: renamed, renamed_to: k8s.statefulset.pod.updated}" +)] +pub const K8S_STATEFULSET_UPDATED_PODS: &str = "k8s.statefulset.updated_pods"; + +/// ## Description +/// +/// Number of connections that are currently active on the server. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Server.Kestrel`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +/// | [`crate::attribute::NETWORK_TYPE`] | `{"recommended": "if the transport is `tcp` or `udp`"}` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +pub const KESTREL_ACTIVE_CONNECTIONS: &str = "kestrel.active_connections"; + +/// ## Description +/// +/// Number of TLS handshakes that are currently in progress on the server. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Server.Kestrel`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{handshake}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +/// | [`crate::attribute::NETWORK_TYPE`] | `{"recommended": "if the transport is `tcp` or `udp`"}` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +pub const KESTREL_ACTIVE_TLS_HANDSHAKES: &str = "kestrel.active_tls_handshakes"; + +/// ## Description +/// +/// The duration of connections on the server. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Server.Kestrel`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +/// | [`crate::attribute::NETWORK_TYPE`] | `{"recommended": "if the transport is `tcp` or `udp`"}` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +/// | [`crate::attribute::TLS_PROTOCOL_VERSION`] | `Recommended` +pub const KESTREL_CONNECTION_DURATION: &str = "kestrel.connection.duration"; + +/// ## Description +/// +/// Number of connections that are currently queued and are waiting to start. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Server.Kestrel`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +/// | [`crate::attribute::NETWORK_TYPE`] | `{"recommended": "if the transport is `tcp` or `udp`"}` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +pub const KESTREL_QUEUED_CONNECTIONS: &str = "kestrel.queued_connections"; + +/// ## Description +/// +/// Number of HTTP requests on multiplexed connections (HTTP/2 and HTTP/3) that are currently queued and are waiting to start. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Server.Kestrel`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{request}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +/// | [`crate::attribute::NETWORK_TYPE`] | `{"recommended": "if the transport is `tcp` or `udp`"}` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +pub const KESTREL_QUEUED_REQUESTS: &str = "kestrel.queued_requests"; + +/// ## Description +/// +/// Number of connections rejected by the server. +/// +/// ## Notes +/// +/// Connections are rejected when the currently active count exceeds the value configured with `MaxConcurrentConnections`. +/// Meter name: `Microsoft.AspNetCore.Server.Kestrel`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{connection}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +/// | [`crate::attribute::NETWORK_TYPE`] | `{"recommended": "if the transport is `tcp` or `udp`"}` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +pub const KESTREL_REJECTED_CONNECTIONS: &str = "kestrel.rejected_connections"; + +/// ## Description +/// +/// The duration of TLS handshakes on the server. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Server.Kestrel`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: if and only if an error has occurred. +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +/// | [`crate::attribute::NETWORK_TYPE`] | `{"recommended": "if the transport is `tcp` or `udp`"}` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +/// | [`crate::attribute::TLS_PROTOCOL_VERSION`] | `Recommended` +pub const KESTREL_TLS_HANDSHAKE_DURATION: &str = "kestrel.tls_handshake.duration"; + +/// ## Description +/// +/// Number of connections that are currently upgraded (WebSockets). . +/// +/// ## Notes +/// +/// The counter only tracks HTTP/1.1 connections. +/// +/// Meter name: `Microsoft.AspNetCore.Server.Kestrel`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +/// | [`crate::attribute::NETWORK_TYPE`] | `{"recommended": "if the transport is `tcp` or `udp`"}` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Recommended` +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +pub const KESTREL_UPGRADED_CONNECTIONS: &str = "kestrel.upgraded_connections"; + +/// ## Description +/// +/// The duration of the MCP request or notification as observed on the sender from the time it was sent until the response or ack is received +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation fails. +/// | [`crate::attribute::GEN_AI_OPERATION_NAME`] | `{"recommended": "should be set to `execute_tool` when the operation describes a tool call and should not be set otherwise.\n"}` +/// | [`crate::attribute::GEN_AI_PROMPT_NAME`] | `Conditionally_required`: When operation is related to a specific prompt. +/// | [`crate::attribute::GEN_AI_TOOL_NAME`] | `Conditionally_required`: When operation is related to a specific tool. +/// | [`crate::attribute::JSONRPC_PROTOCOL_VERSION`] | `{"recommended": "when it's not `2.0`."}` +/// | [`crate::attribute::MCP_METHOD_NAME`] | `Required` +/// | [`crate::attribute::MCP_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::MCP_RESOURCE_URI`] | `Opt_in` +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `{"recommended": "when applicable."}` +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `{"recommended": "when applicable."}` +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: If response contains an error code. +/// | [`crate::attribute::SERVER_ADDRESS`] | `{"recommended": "if applicable"}` +/// | [`crate::attribute::SERVER_PORT`] | `{"recommended": "when `server.address` is set"}` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const MCP_CLIENT_OPERATION_DURATION: &str = "mcp.client.operation.duration"; + +/// ## Description +/// +/// The duration of the MCP session as observed on the MCP client +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if session ends with an error. +/// | [`crate::attribute::JSONRPC_PROTOCOL_VERSION`] | `{"recommended": "when it's not `2.0`."}` +/// | [`crate::attribute::MCP_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `{"recommended": "when applicable."}` +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `{"recommended": "when applicable."}` +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `{"recommended": "if applicable"}` +/// | [`crate::attribute::SERVER_PORT`] | `{"recommended": "when `server.address` is set"}` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const MCP_CLIENT_SESSION_DURATION: &str = "mcp.client.session.duration"; + +/// ## Description +/// +/// MCP request or notification duration as observed on the receiver from the time it was received until the result or ack is sent +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation fails. +/// | [`crate::attribute::GEN_AI_OPERATION_NAME`] | `{"recommended": "should be set to `execute_tool` when the operation describes a tool call and should not be set otherwise.\n"}` +/// | [`crate::attribute::GEN_AI_PROMPT_NAME`] | `Conditionally_required`: When operation is related to a specific prompt. +/// | [`crate::attribute::GEN_AI_TOOL_NAME`] | `Conditionally_required`: When operation is related to a specific tool. +/// | [`crate::attribute::JSONRPC_PROTOCOL_VERSION`] | `{"recommended": "when it's not `2.0`."}` +/// | [`crate::attribute::MCP_METHOD_NAME`] | `Required` +/// | [`crate::attribute::MCP_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::MCP_RESOURCE_URI`] | `Opt_in` +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `{"recommended": "when applicable."}` +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `{"recommended": "when applicable."}` +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: If response contains an error code. +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const MCP_SERVER_OPERATION_DURATION: &str = "mcp.server.operation.duration"; + +/// ## Description +/// +/// The duration of the MCP session as observed on the MCP server +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if session ends with an error. +/// | [`crate::attribute::JSONRPC_PROTOCOL_VERSION`] | `{"recommended": "when it's not `2.0`."}` +/// | [`crate::attribute::MCP_PROTOCOL_VERSION`] | `Recommended` +/// | [`crate::attribute::NETWORK_PROTOCOL_NAME`] | `{"recommended": "when applicable."}` +/// | [`crate::attribute::NETWORK_PROTOCOL_VERSION`] | `{"recommended": "when applicable."}` +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Moved to the [OpenTelemetry GenAI semantic conventions repository](https://github.com/open-telemetry/semantic-conventions-genai).\n, reason: uncategorized}" +)] +pub const MCP_SERVER_SESSION_DURATION: &str = "mcp.server.session.duration"; + +/// ## Description +/// +/// Number of messages that were delivered to the application. +/// +/// ## Notes +/// +/// Records the number of messages pulled from the broker or number of messages dispatched to the application in push-based scenarios. +/// The metric SHOULD be reported once per message delivery. For example, if receiving and processing operations are both instrumented for a single message delivery, this counter is incremented when the message is received and not reported when it is processed +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{message}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the messaging operation has failed. +/// | [`crate::attribute::MESSAGING_CONSUMER_GROUP_NAME`] | `Conditionally_required`: if applicable. +/// | [`crate::attribute::MESSAGING_DESTINATION_NAME`] | `Conditionally_required`: if and only if `messaging.destination.name` is known to have low cardinality. Otherwise, `messaging.destination.template` MAY be populated. +/// | [`crate::attribute::MESSAGING_DESTINATION_PARTITION_ID`] | `Recommended` +/// | [`crate::attribute::MESSAGING_DESTINATION_SUBSCRIPTION_NAME`] | `Conditionally_required`: if applicable. +/// | [`crate::attribute::MESSAGING_DESTINATION_TEMPLATE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::MESSAGING_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::MESSAGING_SYSTEM`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_CLIENT_CONSUMED_MESSAGES: &str = "messaging.client.consumed.messages"; + +/// ## Description +/// +/// Duration of messaging operation initiated by a producer or consumer client. +/// +/// ## Notes +/// +/// This metric SHOULD NOT be used to report processing duration - processing duration is reported in `messaging.process.duration` metric +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the messaging operation has failed. +/// | [`crate::attribute::MESSAGING_CONSUMER_GROUP_NAME`] | `Conditionally_required`: if applicable. +/// | [`crate::attribute::MESSAGING_DESTINATION_NAME`] | `Conditionally_required`: if and only if `messaging.destination.name` is known to have low cardinality. Otherwise, `messaging.destination.template` MAY be populated. +/// | [`crate::attribute::MESSAGING_DESTINATION_PARTITION_ID`] | `Recommended` +/// | [`crate::attribute::MESSAGING_DESTINATION_SUBSCRIPTION_NAME`] | `Conditionally_required`: if applicable. +/// | [`crate::attribute::MESSAGING_DESTINATION_TEMPLATE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::MESSAGING_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::MESSAGING_OPERATION_TYPE`] | `Conditionally_required`: If applicable. +/// | [`crate::attribute::MESSAGING_SYSTEM`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_CLIENT_OPERATION_DURATION: &str = "messaging.client.operation.duration"; + +/// ## Description +/// +/// Deprecated. Use `messaging.client.sent.messages` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{message}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the messaging operation has failed. +/// | [`crate::attribute::MESSAGING_DESTINATION_NAME`] | `Conditionally_required`: if and only if `messaging.destination.name` is known to have low cardinality. Otherwise, `messaging.destination.template` MAY be populated. +/// | [`crate::attribute::MESSAGING_DESTINATION_PARTITION_ID`] | `Recommended` +/// | [`crate::attribute::MESSAGING_DESTINATION_TEMPLATE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::MESSAGING_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::MESSAGING_SYSTEM`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.client.sent.messages`., reason: renamed, renamed_to: messaging.client.sent.messages}" +)] +pub const MESSAGING_CLIENT_PUBLISHED_MESSAGES: &str = "messaging.client.published.messages"; + +/// ## Description +/// +/// Number of messages producer attempted to send to the broker. +/// +/// ## Notes +/// +/// This metric MUST NOT count messages that were created but haven't yet been sent +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{message}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the messaging operation has failed. +/// | [`crate::attribute::MESSAGING_DESTINATION_NAME`] | `Conditionally_required`: if and only if `messaging.destination.name` is known to have low cardinality. Otherwise, `messaging.destination.template` MAY be populated. +/// | [`crate::attribute::MESSAGING_DESTINATION_PARTITION_ID`] | `Recommended` +/// | [`crate::attribute::MESSAGING_DESTINATION_TEMPLATE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::MESSAGING_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::MESSAGING_SYSTEM`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_CLIENT_SENT_MESSAGES: &str = "messaging.client.sent.messages"; + +/// ## Description +/// +/// Duration of processing operation. +/// +/// ## Notes +/// +/// This metric MUST be reported for operations with `messaging.operation.type` that matches `process` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the messaging operation has failed. +/// | [`crate::attribute::MESSAGING_CONSUMER_GROUP_NAME`] | `Conditionally_required`: if applicable. +/// | [`crate::attribute::MESSAGING_DESTINATION_NAME`] | `Conditionally_required`: if and only if `messaging.destination.name` is known to have low cardinality. Otherwise, `messaging.destination.template` MAY be populated. +/// | [`crate::attribute::MESSAGING_DESTINATION_PARTITION_ID`] | `Recommended` +/// | [`crate::attribute::MESSAGING_DESTINATION_SUBSCRIPTION_NAME`] | `Conditionally_required`: if applicable. +/// | [`crate::attribute::MESSAGING_DESTINATION_TEMPLATE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::MESSAGING_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::MESSAGING_SYSTEM`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const MESSAGING_PROCESS_DURATION: &str = "messaging.process.duration"; + +/// ## Description +/// +/// Deprecated. Use `messaging.client.consumed.messages` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{message}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the messaging operation has failed. +/// | [`crate::attribute::MESSAGING_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.client.consumed.messages`., reason: renamed, renamed_to: messaging.client.consumed.messages}" +)] +pub const MESSAGING_PROCESS_MESSAGES: &str = "messaging.process.messages"; + +/// ## Description +/// +/// Deprecated. Use `messaging.client.operation.duration` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the messaging operation has failed. +/// | [`crate::attribute::MESSAGING_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.client.operation.duration`., reason: renamed, renamed_to: messaging.client.operation.duration}" +)] +pub const MESSAGING_PUBLISH_DURATION: &str = "messaging.publish.duration"; + +/// ## Description +/// +/// Deprecated. Use `messaging.client.sent.messages` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{message}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the messaging operation has failed. +/// | [`crate::attribute::MESSAGING_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.client.sent.messages`., reason: renamed, renamed_to: messaging.client.sent.messages}" +)] +pub const MESSAGING_PUBLISH_MESSAGES: &str = "messaging.publish.messages"; + +/// ## Description +/// +/// Deprecated. Use `messaging.client.operation.duration` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the messaging operation has failed. +/// | [`crate::attribute::MESSAGING_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.client.operation.duration`., reason: renamed, renamed_to: messaging.client.operation.duration}" +)] +pub const MESSAGING_RECEIVE_DURATION: &str = "messaging.receive.duration"; + +/// ## Description +/// +/// Deprecated. Use `messaging.client.consumed.messages` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{message}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the messaging operation has failed. +/// | [`crate::attribute::MESSAGING_OPERATION_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `messaging.client.consumed.messages`., reason: renamed, renamed_to: messaging.client.consumed.messages}" +)] +pub const MESSAGING_RECEIVE_MESSAGES: &str = "messaging.receive.messages"; + +/// ## Description +/// +/// Reports the count of kernel NFS client TCP segments and UDP datagrams handled. +/// +/// ## Notes +/// +/// Linux: this metric is taken from the Linux kernel's svc_stat.netudpcnt and svc_stat.nettcpcnt +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{record}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const NFS_CLIENT_NET_COUNT: &str = "nfs.client.net.count"; + +/// ## Description +/// +/// Reports the count of kernel NFS client TCP connections accepted. +/// +/// ## Notes +/// +/// Linux: this metric is taken from the Linux kernel's svc_stat.nettcpconn +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NFS_CLIENT_NET_TCP_CONNECTION_ACCEPTED: &str = "nfs.client.net.tcp.connection.accepted"; + +/// ## Description +/// +/// Reports the count of kernel NFSv4+ client operations +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{operation}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NFS_OPERATION_NAME`] | `Recommended` +/// | [`crate::attribute::ONC_RPC_VERSION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const NFS_CLIENT_OPERATION_COUNT: &str = "nfs.client.operation.count"; + +/// ## Description +/// +/// Reports the count of kernel NFS client procedures +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{procedure}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ONC_RPC_PROCEDURE_NAME`] | `Recommended` +/// | [`crate::attribute::ONC_RPC_VERSION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const NFS_CLIENT_PROCEDURE_COUNT: &str = "nfs.client.procedure.count"; + +/// ## Description +/// +/// Reports the count of kernel NFS client RPC authentication refreshes. +/// +/// ## Notes +/// +/// Linux: this metric is taken from the Linux kernel's svc_stat.rpcauthrefresh +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{authrefresh}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NFS_CLIENT_RPC_AUTHREFRESH_COUNT: &str = "nfs.client.rpc.authrefresh.count"; + +/// ## Description +/// +/// Reports the count of kernel NFS client RPCs sent, regardless of whether they're accepted/rejected by the server. +/// +/// ## Notes +/// +/// Linux: this metric is taken from the Linux kernel's svc_stat.rpccnt +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{request}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NFS_CLIENT_RPC_COUNT: &str = "nfs.client.rpc.count"; + +/// ## Description +/// +/// Reports the count of kernel NFS client RPC retransmits. +/// +/// ## Notes +/// +/// Linux: this metric is taken from the Linux kernel's svc_stat.rpcretrans +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{retransmit}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NFS_CLIENT_RPC_RETRANSMIT_COUNT: &str = "nfs.client.rpc.retransmit.count"; + +/// ## Description +/// +/// Reports the count of kernel NFS server stale file handles. +/// +/// ## Notes +/// +/// Linux: this metric is taken from the Linux kernel NFSD_STATS_FH_STALE counter in the nfsd_net struct +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{fh}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NFS_SERVER_FH_STALE_COUNT: &str = "nfs.server.fh.stale.count"; + +/// ## Description +/// +/// Reports the count of kernel NFS server bytes returned to receive and transmit (read and write) requests. +/// +/// ## Notes +/// +/// Linux: this metric is taken from the Linux kernel NFSD_STATS_IO_READ and NFSD_STATS_IO_WRITE counters in the nfsd_net struct +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const NFS_SERVER_IO: &str = "nfs.server.io"; + +/// ## Description +/// +/// Reports the count of kernel NFS server TCP segments and UDP datagrams handled. +/// +/// ## Notes +/// +/// Linux: this metric is taken from the Linux kernel's svc_stat.nettcpcnt and svc_stat.netudpcnt +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{record}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const NFS_SERVER_NET_COUNT: &str = "nfs.server.net.count"; + +/// ## Description +/// +/// Reports the count of kernel NFS server TCP connections accepted. +/// +/// ## Notes +/// +/// Linux: this metric is taken from the Linux kernel's svc_stat.nettcpconn +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NFS_SERVER_NET_TCP_CONNECTION_ACCEPTED: &str = "nfs.server.net.tcp.connection.accepted"; + +/// ## Description +/// +/// Reports the count of kernel NFSv4+ server operations +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{operation}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NFS_OPERATION_NAME`] | `Recommended` +/// | [`crate::attribute::ONC_RPC_VERSION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const NFS_SERVER_OPERATION_COUNT: &str = "nfs.server.operation.count"; + +/// ## Description +/// +/// Reports the count of kernel NFS server procedures +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{procedure}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ONC_RPC_PROCEDURE_NAME`] | `Recommended` +/// | [`crate::attribute::ONC_RPC_VERSION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const NFS_SERVER_PROCEDURE_COUNT: &str = "nfs.server.procedure.count"; + +/// ## Description +/// +/// Reports the kernel NFS server reply cache request count by cache hit status +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{request}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NFS_SERVER_REPCACHE_STATUS`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const NFS_SERVER_REPCACHE_REQUESTS: &str = "nfs.server.repcache.requests"; + +/// ## Description +/// +/// Reports the count of kernel NFS server RPCs handled. +/// +/// ## Notes +/// +/// Linux: this metric is taken from the Linux kernel's svc_stat.rpccnt, the count of good RPCs. This metric can have +/// an error.type of "format", "auth", or "client" for svc_stat.badfmt, svc_stat.badauth, and svc_stat.badclnt +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{request}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const NFS_SERVER_RPC_COUNT: &str = "nfs.server.rpc.count"; + +/// ## Description +/// +/// Reports the count of kernel NFS server available threads. +/// +/// ## Notes +/// +/// Linux: this metric is taken from the Linux kernel nfsd_th_cnt variable +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{thread}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NFS_SERVER_THREAD_COUNT: &str = "nfs.server.thread.count"; + +/// ## Description +/// +/// Event loop maximum delay. +/// +/// ## Notes +/// +/// Value can be retrieved from value `histogram.max` of [`perf_hooks.monitorEventLoopDelay([options])`](https://nodejs.org/api/perf_hooks.html#perf_hooksmonitoreventloopdelayoptions) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NODEJS_EVENTLOOP_DELAY_MAX: &str = "nodejs.eventloop.delay.max"; + +/// ## Description +/// +/// Event loop mean delay. +/// +/// ## Notes +/// +/// Value can be retrieved from value `histogram.mean` of [`perf_hooks.monitorEventLoopDelay([options])`](https://nodejs.org/api/perf_hooks.html#perf_hooksmonitoreventloopdelayoptions) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NODEJS_EVENTLOOP_DELAY_MEAN: &str = "nodejs.eventloop.delay.mean"; + +/// ## Description +/// +/// Event loop minimum delay. +/// +/// ## Notes +/// +/// Value can be retrieved from value `histogram.min` of [`perf_hooks.monitorEventLoopDelay([options])`](https://nodejs.org/api/perf_hooks.html#perf_hooksmonitoreventloopdelayoptions) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NODEJS_EVENTLOOP_DELAY_MIN: &str = "nodejs.eventloop.delay.min"; + +/// ## Description +/// +/// Event loop 50 percentile delay. +/// +/// ## Notes +/// +/// Value can be retrieved from value `histogram.percentile(50)` of [`perf_hooks.monitorEventLoopDelay([options])`](https://nodejs.org/api/perf_hooks.html#perf_hooksmonitoreventloopdelayoptions) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NODEJS_EVENTLOOP_DELAY_P50: &str = "nodejs.eventloop.delay.p50"; + +/// ## Description +/// +/// Event loop 90 percentile delay. +/// +/// ## Notes +/// +/// Value can be retrieved from value `histogram.percentile(90)` of [`perf_hooks.monitorEventLoopDelay([options])`](https://nodejs.org/api/perf_hooks.html#perf_hooksmonitoreventloopdelayoptions) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NODEJS_EVENTLOOP_DELAY_P90: &str = "nodejs.eventloop.delay.p90"; + +/// ## Description +/// +/// Event loop 99 percentile delay. +/// +/// ## Notes +/// +/// Value can be retrieved from value `histogram.percentile(99)` of [`perf_hooks.monitorEventLoopDelay([options])`](https://nodejs.org/api/perf_hooks.html#perf_hooksmonitoreventloopdelayoptions) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NODEJS_EVENTLOOP_DELAY_P99: &str = "nodejs.eventloop.delay.p99"; + +/// ## Description +/// +/// Event loop standard deviation delay. +/// +/// ## Notes +/// +/// Value can be retrieved from value `histogram.stddev` of [`perf_hooks.monitorEventLoopDelay([options])`](https://nodejs.org/api/perf_hooks.html#perf_hooksmonitoreventloopdelayoptions) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NODEJS_EVENTLOOP_DELAY_STDDEV: &str = "nodejs.eventloop.delay.stddev"; + +/// ## Description +/// +/// Cumulative duration of time the event loop has been in each state. +/// +/// ## Notes +/// +/// Value can be retrieved from [`performance.eventLoopUtilization([utilization1[, utilization2]])`](https://nodejs.org/api/perf_hooks.html#performanceeventlooputilizationutilization1-utilization2) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NODEJS_EVENTLOOP_STATE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const NODEJS_EVENTLOOP_TIME: &str = "nodejs.eventloop.time"; + +/// ## Description +/// +/// Event loop utilization. +/// +/// ## Notes +/// +/// The value range is \[0.0, 1.0\] and can be retrieved from [`performance.eventLoopUtilization([utilization1[, utilization2]])`](https://nodejs.org/api/perf_hooks.html#performanceeventlooputilizationutilization1-utilization2) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const NODEJS_EVENTLOOP_UTILIZATION: &str = "nodejs.eventloop.utilization"; + +/// ## Description +/// +/// The enforced hard limit of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_CPU_LIMIT_HARD: &str = "openshift.clusterquota.cpu.limit.hard"; + +/// ## Description +/// +/// The current observed total usage of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_CPU_LIMIT_USED: &str = "openshift.clusterquota.cpu.limit.used"; + +/// ## Description +/// +/// The enforced hard limit of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_CPU_REQUEST_HARD: &str = "openshift.clusterquota.cpu.request.hard"; + +/// ## Description +/// +/// The current observed total usage of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_CPU_REQUEST_USED: &str = "openshift.clusterquota.cpu.request.used"; + +/// ## Description +/// +/// The enforced hard limit of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_LIMIT_HARD: &str = + "openshift.clusterquota.ephemeral_storage.limit.hard"; + +/// ## Description +/// +/// The current observed total usage of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_LIMIT_USED: &str = + "openshift.clusterquota.ephemeral_storage.limit.used"; + +/// ## Description +/// +/// The enforced hard limit of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_REQUEST_HARD: &str = + "openshift.clusterquota.ephemeral_storage.request.hard"; + +/// ## Description +/// +/// The current observed total usage of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_EPHEMERAL_STORAGE_REQUEST_USED: &str = + "openshift.clusterquota.ephemeral_storage.request.used"; + +/// ## Description +/// +/// The enforced hard limit of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{hugepage}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_HUGEPAGE_SIZE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_HUGEPAGE_COUNT_REQUEST_HARD: &str = + "openshift.clusterquota.hugepage_count.request.hard"; + +/// ## Description +/// +/// The current observed total usage of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{hugepage}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_HUGEPAGE_SIZE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_HUGEPAGE_COUNT_REQUEST_USED: &str = + "openshift.clusterquota.hugepage_count.request.used"; + +/// ## Description +/// +/// The enforced hard limit of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_MEMORY_LIMIT_HARD: &str = + "openshift.clusterquota.memory.limit.hard"; + +/// ## Description +/// +/// The current observed total usage of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_MEMORY_LIMIT_USED: &str = + "openshift.clusterquota.memory.limit.used"; + +/// ## Description +/// +/// The enforced hard limit of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_MEMORY_REQUEST_HARD: &str = + "openshift.clusterquota.memory.request.hard"; + +/// ## Description +/// +/// The current observed total usage of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_MEMORY_REQUEST_USED: &str = + "openshift.clusterquota.memory.request.used"; + +/// ## Description +/// +/// The enforced hard limit of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{object}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_RESOURCEQUOTA_RESOURCE_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_OBJECT_COUNT_HARD: &str = + "openshift.clusterquota.object_count.hard"; + +/// ## Description +/// +/// The current observed total usage of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{object}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_RESOURCEQUOTA_RESOURCE_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_OBJECT_COUNT_USED: &str = + "openshift.clusterquota.object_count.used"; + +/// ## Description +/// +/// The enforced hard limit of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). +/// +/// The `k8s.storageclass.name` should be required when a resource quota is defined for a specific +/// storage class +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{persistentvolumeclaim}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_STORAGECLASS_NAME`] | `Conditionally_required`: The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_PERSISTENTVOLUMECLAIM_COUNT_HARD: &str = + "openshift.clusterquota.persistentvolumeclaim_count.hard"; + +/// ## Description +/// +/// The current observed total usage of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). +/// +/// The `k8s.storageclass.name` should be required when a resource quota is defined for a specific +/// storage class +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{persistentvolumeclaim}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_STORAGECLASS_NAME`] | `Conditionally_required`: The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_PERSISTENTVOLUMECLAIM_COUNT_USED: &str = + "openshift.clusterquota.persistentvolumeclaim_count.used"; + +/// ## Description +/// +/// The enforced hard limit of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Hard` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). +/// +/// The `k8s.storageclass.name` should be required when a resource quota is defined for a specific +/// storage class +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_STORAGECLASS_NAME`] | `Conditionally_required`: The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_STORAGE_REQUEST_HARD: &str = + "openshift.clusterquota.storage.request.hard"; + +/// ## Description +/// +/// The current observed total usage of the resource across all projects. +/// +/// ## Notes +/// +/// This metric is retrieved from the `Status.Total.Used` field of the +/// [K8s ResourceQuotaStatus](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.34/#resourcequotastatus-v1-core) +/// of the +/// [ClusterResourceQuota](https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/schedule_and_quota_apis/clusterresourcequota-quota-openshift-io-v1#status-total). +/// +/// The `k8s.storageclass.name` should be required when a resource quota is defined for a specific +/// storage class +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::K8S_STORAGECLASS_NAME`] | `Conditionally_required`: The `k8s.storageclass.name` should be required when a resource quota is defined for a specific storage class. +#[cfg(feature = "semconv_experimental")] +pub const OPENSHIFT_CLUSTERQUOTA_STORAGE_REQUEST_USED: &str = + "openshift.clusterquota.storage.request.used"; + +/// ## Description +/// +/// The number of log records for which the export has finished, either successful or failed. +/// +/// ## Notes +/// +/// For successful exports, `error.type` MUST NOT be set. For failed exports, `error.type` MUST contain the failure cause. +/// For exporters with partial success semantics (e.g. OTLP with `rejected_log_records`), rejected log records MUST count as failed and only non-rejected log records count as success. +/// If no rejection reason is available, `rejected` SHOULD be used as value for `error.type`. +/// If the exporter retries failed export attempts, the export operation is considered finished only after the final attempt has concluded. +/// Each log record MUST be counted exactly once per export operation: intermediate failed attempts that are followed by a retry MUST NOT increment the counter, +/// and `error.type` reflects the cause of the final attempt +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{log_record}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `{"recommended": "when applicable"}` +/// | [`crate::attribute::SERVER_PORT`] | `{"recommended": "when applicable"}` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_EXPORTER_LOG_EXPORTED: &str = "otel.sdk.exporter.log.exported"; + +/// ## Description +/// +/// The number of log records which were passed to the exporter, but that have not been exported yet (neither successful, nor failed). +/// +/// ## Notes +/// +/// Log records are counted as inflight from when they are passed to the exporter until the export operation has concluded. +/// If the exporter retries failed export attempts, log records remain inflight across all retry attempts and any backoff between them +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{log_record}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `{"recommended": "when applicable"}` +/// | [`crate::attribute::SERVER_PORT`] | `{"recommended": "when applicable"}` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_EXPORTER_LOG_INFLIGHT: &str = "otel.sdk.exporter.log.inflight"; + +/// ## Description +/// +/// The number of metric data points for which the export has finished, either successful or failed. +/// +/// ## Notes +/// +/// For successful exports, `error.type` MUST NOT be set. For failed exports, `error.type` MUST contain the failure cause. +/// For exporters with partial success semantics (e.g. OTLP with `rejected_data_points`), rejected data points MUST count as failed and only non-rejected data points count as success. +/// If no rejection reason is available, `rejected` SHOULD be used as value for `error.type`. +/// If the exporter retries failed export attempts, the export operation is considered finished only after the final attempt has concluded. +/// Each metric data point MUST be counted exactly once per export operation: intermediate failed attempts that are followed by a retry MUST NOT increment the counter, +/// and `error.type` reflects the cause of the final attempt +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{data_point}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `{"recommended": "when applicable"}` +/// | [`crate::attribute::SERVER_PORT`] | `{"recommended": "when applicable"}` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_EXPORTER_METRIC_DATA_POINT_EXPORTED: &str = + "otel.sdk.exporter.metric_data_point.exported"; + +/// ## Description +/// +/// The number of metric data points which were passed to the exporter, but that have not been exported yet (neither successful, nor failed). +/// +/// ## Notes +/// +/// Metric data points are counted as inflight from when they are passed to the exporter until the export operation has concluded. +/// If the exporter retries failed export attempts, metric data points remain inflight across all retry attempts and any backoff between them +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{data_point}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `{"recommended": "when applicable"}` +/// | [`crate::attribute::SERVER_PORT`] | `{"recommended": "when applicable"}` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_EXPORTER_METRIC_DATA_POINT_INFLIGHT: &str = + "otel.sdk.exporter.metric_data_point.inflight"; + +/// ## Description +/// +/// The duration of exporting a batch of telemetry records. +/// +/// ## Notes +/// +/// This metric defines successful operations using the full success definitions for [http](https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/docs/specification.md#full-success-1) +/// and [grpc](https://github.com/open-telemetry/opentelemetry-proto/blob/v1.5.0/docs/specification.md#full-success). Anything else is defined as an unsuccessful operation. For successful +/// operations, `error.type` MUST NOT be set. For unsuccessful export operations, `error.type` MUST contain a relevant failure cause. +/// If the exporter retries failed export attempts, exactly one observation MUST be recorded per export operation, +/// covering the wall-clock duration from the start of the first attempt through the conclusion of the final attempt (including any backoff between attempts). +/// `error.type` reflects the cause of the final attempt +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If operation has ended with an error +/// | [`crate::attribute::HTTP_RESPONSE_STATUS_CODE`] | `{"recommended": "when applicable"}` +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `{"recommended": "when applicable"}` +/// | [`crate::attribute::SERVER_ADDRESS`] | `{"recommended": "when applicable"}` +/// | [`crate::attribute::SERVER_PORT`] | `{"recommended": "when applicable"}` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_EXPORTER_OPERATION_DURATION: &str = "otel.sdk.exporter.operation.duration"; + +/// ## Description +/// +/// The number of spans for which the export has finished, either successful or failed. +/// +/// ## Notes +/// +/// For successful exports, `error.type` MUST NOT be set. For failed exports, `error.type` MUST contain the failure cause. +/// For exporters with partial success semantics (e.g. OTLP with `rejected_spans`), rejected spans MUST count as failed and only non-rejected spans count as success. +/// If no rejection reason is available, `rejected` SHOULD be used as value for `error.type`. +/// If the exporter retries failed export attempts, the export operation is considered finished only after the final attempt has concluded. +/// Each span MUST be counted exactly once per export operation: intermediate failed attempts that are followed by a retry MUST NOT increment the counter, +/// and `error.type` reflects the cause of the final attempt +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `{"recommended": "when applicable"}` +/// | [`crate::attribute::SERVER_PORT`] | `{"recommended": "when applicable"}` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_EXPORTER_SPAN_EXPORTED: &str = "otel.sdk.exporter.span.exported"; + +/// ## Description +/// +/// Deprecated, use `otel.sdk.exporter.span.exported` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `otel.sdk.exporter.span.exported`., reason: renamed, renamed_to: otel.sdk.exporter.span.exported}" +)] +pub const OTEL_SDK_EXPORTER_SPAN_EXPORTED_COUNT: &str = "otel.sdk.exporter.span.exported.count"; + +/// ## Description +/// +/// The number of spans which were passed to the exporter, but that have not been exported yet (neither successful, nor failed). +/// +/// ## Notes +/// +/// Spans are counted as inflight from when they are passed to the exporter until the export operation has concluded. +/// If the exporter retries failed export attempts, spans remain inflight across all retry attempts and any backoff between them +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +/// | [`crate::attribute::SERVER_ADDRESS`] | `{"recommended": "when applicable"}` +/// | [`crate::attribute::SERVER_PORT`] | `{"recommended": "when applicable"}` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_EXPORTER_SPAN_INFLIGHT: &str = "otel.sdk.exporter.span.inflight"; + +/// ## Description +/// +/// Deprecated, use `otel.sdk.exporter.span.inflight` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `otel.sdk.exporter.span.inflight`., reason: renamed, renamed_to: otel.sdk.exporter.span.inflight}" +)] +pub const OTEL_SDK_EXPORTER_SPAN_INFLIGHT_COUNT: &str = "otel.sdk.exporter.span.inflight.count"; + +/// ## Description +/// +/// The number of logs submitted to enabled SDK Loggers +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{log_record}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_LOG_CREATED: &str = "otel.sdk.log.created"; + +/// ## Description +/// +/// The duration of the collect operation of the metric reader. +/// +/// ## Notes +/// +/// For successful collections, `error.type` MUST NOT be set. For failed collections, `error.type` SHOULD contain the failure cause. +/// It can happen that metrics collection is successful for some MetricProducers, while others fail. In that case `error.type` SHOULD be set to any of the failure causes +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_METRIC_READER_COLLECTION_DURATION: &str = + "otel.sdk.metric_reader.collection.duration"; + +/// ## Description +/// +/// The number of log records for which the processing has finished, either successful or failed. +/// +/// ## Notes +/// +/// For successful processing, `error.type` MUST NOT be set. For failed processing, `error.type` MUST contain the failure cause. +/// SDK Batching Log Record Processors MUST use `queue_full` as the value of `error.type` for log records dropped due to a full queue. +/// SDK Log Record Processors MUST use `already_shutdown` as the value of `error.type` for log records dropped because the processor has already been shut down. +/// For the SDK Simple and Batching Log Record Processor a log record is considered to be processed already when it has been submitted to the exporter, +/// not when the corresponding export call has finished +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{log_record}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_PROCESSOR_LOG_PROCESSED: &str = "otel.sdk.processor.log.processed"; + +/// ## Description +/// +/// The maximum number of log records the queue of a given instance of an SDK Log Record processor can hold. +/// +/// ## Notes +/// +/// Only applies to Log Record processors which use a queue, e.g. the SDK Batching Log Record Processor +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{log_record}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_PROCESSOR_LOG_QUEUE_CAPACITY: &str = "otel.sdk.processor.log.queue.capacity"; + +/// ## Description +/// +/// The number of log records in the queue of a given instance of an SDK log processor. +/// +/// ## Notes +/// +/// Only applies to log record processors which use a queue, e.g. the SDK Batching Log Record Processor +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{log_record}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_PROCESSOR_LOG_QUEUE_SIZE: &str = "otel.sdk.processor.log.queue.size"; + +/// ## Description +/// +/// The number of spans for which the processing has finished, either successful or failed. +/// +/// ## Notes +/// +/// For successful processing, `error.type` MUST NOT be set. For failed processing, `error.type` MUST contain the failure cause. +/// SDK Batching Span Processors MUST use `queue_full` as the value of `error.type` for spans dropped due to a full queue. +/// SDK Span Processors MUST use `already_shutdown` as the value of `error.type` for spans dropped because the processor has already been shut down. +/// For the SDK Simple and Batching Span Processor a span is considered to be processed already when it has been submitted to the exporter, not when the corresponding export call has finished +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_PROCESSOR_SPAN_PROCESSED: &str = "otel.sdk.processor.span.processed"; + +/// ## Description +/// +/// Deprecated, use `otel.sdk.processor.span.processed` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `otel.sdk.processor.span.processed`., reason: renamed, renamed_to: otel.sdk.processor.span.processed}" +)] +pub const OTEL_SDK_PROCESSOR_SPAN_PROCESSED_COUNT: &str = "otel.sdk.processor.span.processed.count"; + +/// ## Description +/// +/// The maximum number of spans the queue of a given instance of an SDK span processor can hold. +/// +/// ## Notes +/// +/// Only applies to span processors which use a queue, e.g. the SDK Batching Span Processor +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_PROCESSOR_SPAN_QUEUE_CAPACITY: &str = "otel.sdk.processor.span.queue.capacity"; + +/// ## Description +/// +/// The number of spans in the queue of a given instance of an SDK span processor. +/// +/// ## Notes +/// +/// Only applies to span processors which use a queue, e.g. the SDK Batching Span Processor +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::OTEL_COMPONENT_NAME`] | `Recommended` +/// | [`crate::attribute::OTEL_COMPONENT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_PROCESSOR_SPAN_QUEUE_SIZE: &str = "otel.sdk.processor.span.queue.size"; + +/// ## Description +/// +/// Use `otel.sdk.span.started` minus `otel.sdk.span.live` to derive this value +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Obsoleted., reason: obsoleted}")] +pub const OTEL_SDK_SPAN_ENDED: &str = "otel.sdk.span.ended"; + +/// ## Description +/// +/// Use `otel.sdk.span.started` minus `otel.sdk.span.live` to derive this value +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Obsoleted., reason: obsoleted}")] +pub const OTEL_SDK_SPAN_ENDED_COUNT: &str = "otel.sdk.span.ended.count"; + +/// ## Description +/// +/// The number of created spans with `recording=true` for which the end operation has not been called yet +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::OTEL_SPAN_SAMPLING_RESULT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_SPAN_LIVE: &str = "otel.sdk.span.live"; + +/// ## Description +/// +/// Deprecated, use `otel.sdk.span.live` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `otel.sdk.span.live`., reason: renamed, renamed_to: otel.sdk.span.live}" +)] +pub const OTEL_SDK_SPAN_LIVE_COUNT: &str = "otel.sdk.span.live.count"; + +/// ## Description +/// +/// The number of created spans. +/// +/// ## Notes +/// +/// Implementations MUST record this metric for all spans, even for non-recording ones +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{span}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::OTEL_SPAN_PARENT_ORIGIN`] | `Recommended` +/// | [`crate::attribute::OTEL_SPAN_SAMPLING_RESULT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const OTEL_SDK_SPAN_STARTED: &str = "otel.sdk.span.started"; + +/// ## Description +/// +/// Number of times the process has been context switched +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{context_switch}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::PROCESS_CONTEXT_SWITCH_TYPE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_CONTEXT_SWITCHES: &str = "process.context_switches"; + +/// ## Description +/// +/// Total CPU seconds broken down by different CPU modes +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPU_MODE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_CPU_TIME: &str = "process.cpu.time"; + +/// ## Description +/// +/// Difference in process.cpu.time since the last measurement, divided by the elapsed time and number of CPUs available to the process +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPU_MODE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_CPU_UTILIZATION: &str = "process.cpu.utilization"; + +/// ## Description +/// +/// Disk bytes transferred +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DISK_IO_DIRECTION`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_DISK_IO: &str = "process.disk.io"; + +/// ## Description +/// +/// The amount of physical memory in use +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_MEMORY_USAGE: &str = "process.memory.usage"; + +/// ## Description +/// +/// The amount of committed virtual memory +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_MEMORY_VIRTUAL: &str = "process.memory.virtual"; + +/// ## Description +/// +/// Network bytes transferred +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_NETWORK_IO: &str = "process.network.io"; + +/// ## Description +/// +/// Deprecated, use `process.unix.file_descriptor.count` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{file_descriptor}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `process.unix.file_descriptor.count`., reason: renamed, renamed_to: process.unix.file_descriptor.count}" +)] +pub const PROCESS_OPEN_FILE_DESCRIPTOR_COUNT: &str = "process.open_file_descriptor.count"; + +/// ## Description +/// +/// Number of page faults the process has made +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{fault}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_PAGING_FAULT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_PAGING_FAULTS: &str = "process.paging.faults"; + +/// ## Description +/// +/// Process threads count +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{thread}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_THREAD_COUNT: &str = "process.thread.count"; + +/// ## Description +/// +/// Number of unix file descriptors in use by the process +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{file_descriptor}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_UNIX_FILE_DESCRIPTOR_COUNT: &str = "process.unix.file_descriptor.count"; + +/// ## Description +/// +/// The time the process has been running. +/// +/// ## Notes +/// +/// Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available. +/// The actual accuracy would depend on the instrumentation and operating system +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_UPTIME: &str = "process.uptime"; + +/// ## Description +/// +/// Number of handles held by the process +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{handle}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const PROCESS_WINDOWS_HANDLE_COUNT: &str = "process.windows.handle.count"; + +/// ## Description +/// +/// Measures the duration of an outgoing Remote Procedure Call (RPC). +/// +/// ## Notes +/// +/// When this metric is reported alongside an RPC client span, the metric value +/// SHOULD be the same as the RPC client span duration +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Release_candidate` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: if applicable and if `server.address` is set. +#[cfg(feature = "semconv_experimental")] +pub const RPC_CLIENT_CALL_DURATION: &str = "rpc.client.call.duration"; + +/// ## Description +/// +/// Deprecated, use `rpc.client.call.duration` instead. Note: the unit also changed from `ms` to `s`. +/// +/// ## Notes +/// +/// While streaming RPCs may record this metric as start-of-batch +/// to end-of-batch, it's hard to interpret in practice. +/// +/// **Streaming**: N/A +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `ms` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: if applicable and if `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `rpc.client.call.duration` with unit `s`., reason: uncategorized}" +)] +pub const RPC_CLIENT_DURATION: &str = "rpc.client.duration"; + +/// ## Description +/// +/// Measures the size of RPC request messages (uncompressed). +/// +/// ## Notes +/// +/// **Streaming**: Recorded per message in a streaming batch +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: if applicable and if `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const RPC_CLIENT_REQUEST_SIZE: &str = "rpc.client.request.size"; + +/// ## Description +/// +/// Measures the number of messages received per RPC. +/// +/// ## Notes +/// +/// Should be 1 for all non-streaming RPCs. +/// +/// **Streaming**: This metric is required for server and client streaming RPCs +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `{count}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: if applicable and if `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const RPC_CLIENT_REQUESTS_PER_RPC: &str = "rpc.client.requests_per_rpc"; + +/// ## Description +/// +/// Measures the size of RPC response messages (uncompressed). +/// +/// ## Notes +/// +/// **Streaming**: Recorded per response in a streaming batch +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: if applicable and if `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const RPC_CLIENT_RESPONSE_SIZE: &str = "rpc.client.response.size"; + +/// ## Description +/// +/// Measures the number of messages sent per RPC. +/// +/// ## Notes +/// +/// Should be 1 for all non-streaming RPCs. +/// +/// **Streaming**: This metric is required for server and client streaming RPCs +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `{count}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Conditionally_required`: If available. +/// | [`crate::attribute::SERVER_PORT`] | `Conditionally_required`: if applicable and if `server.address` is set. +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const RPC_CLIENT_RESPONSES_PER_RPC: &str = "rpc.client.responses_per_rpc"; + +/// ## Description +/// +/// Measures the duration of an incoming Remote Procedure Call (RPC). +/// +/// ## Notes +/// +/// When this metric is reported alongside an RPC server span, the metric value +/// SHOULD be the same as the RPC server span duration +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Release_candidate` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::SERVER_PORT`] | `Opt_in` +#[cfg(feature = "semconv_experimental")] +pub const RPC_SERVER_CALL_DURATION: &str = "rpc.server.call.duration"; + +/// ## Description +/// +/// Deprecated, use `rpc.server.call.duration` instead. Note: the unit also changed from `ms` to `s`. +/// +/// ## Notes +/// +/// While streaming RPCs may record this metric as start-of-batch +/// to end-of-batch, it's hard to interpret in practice. +/// +/// **Streaming**: N/A +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `ms` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::SERVER_PORT`] | `Opt_in` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `rpc.server.call.duration` with unit `s`., reason: uncategorized}" +)] +pub const RPC_SERVER_DURATION: &str = "rpc.server.duration"; + +/// ## Description +/// +/// Measures the size of RPC request messages (uncompressed). +/// +/// ## Notes +/// +/// **Streaming**: Recorded per message in a streaming batch +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::SERVER_PORT`] | `Opt_in` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const RPC_SERVER_REQUEST_SIZE: &str = "rpc.server.request.size"; + +/// ## Description +/// +/// Measures the number of messages received per RPC. +/// +/// ## Notes +/// +/// Should be 1 for all non-streaming RPCs. +/// +/// **Streaming** : This metric is required for server and client streaming RPCs +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `{count}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::SERVER_PORT`] | `Opt_in` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const RPC_SERVER_REQUESTS_PER_RPC: &str = "rpc.server.requests_per_rpc"; + +/// ## Description +/// +/// Measures the size of RPC response messages (uncompressed). +/// +/// ## Notes +/// +/// **Streaming**: Recorded per response in a streaming batch +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::SERVER_PORT`] | `Opt_in` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const RPC_SERVER_RESPONSE_SIZE: &str = "rpc.server.response.size"; + +/// ## Description +/// +/// Measures the number of messages sent per RPC. +/// +/// ## Notes +/// +/// Should be 1 for all non-streaming RPCs. +/// +/// **Streaming**: This metric is required for server and client streaming RPCs +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `{count}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::ERROR_TYPE`] | `Conditionally_required`: If and only if the operation failed. +/// | [`crate::attribute::RPC_METHOD`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_RESPONSE_STATUS_CODE`] | `Conditionally_required`: if available. +/// | [`crate::attribute::RPC_SYSTEM_NAME`] | `Required` +/// | [`crate::attribute::SERVER_ADDRESS`] | `Opt_in` +/// | [`crate::attribute::SERVER_PORT`] | `Opt_in` +#[cfg(feature = "semconv_experimental")] +#[deprecated(note = "{note: Removed, no replacement at this time., reason: obsoleted}")] +pub const RPC_SERVER_RESPONSES_PER_RPC: &str = "rpc.server.responses_per_rpc"; + +/// ## Description +/// +/// Number of connections that are currently active on the server. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Http.Connections`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SIGNALR_CONNECTION_STATUS`] | `Recommended` +/// | [`crate::attribute::SIGNALR_TRANSPORT`] | `Recommended` +pub const SIGNALR_SERVER_ACTIVE_CONNECTIONS: &str = "signalr.server.active_connections"; + +/// ## Description +/// +/// The duration of connections on the server. +/// +/// ## Notes +/// +/// Meter name: `Microsoft.AspNetCore.Http.Connections`; Added in: ASP.NET Core 8.0 +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Stable` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SIGNALR_CONNECTION_STATUS`] | `Recommended` +/// | [`crate::attribute::SIGNALR_TRANSPORT`] | `Recommended` +pub const SIGNALR_SERVER_CONNECTION_DURATION: &str = "signalr.server.connection.duration"; + +/// ## Description +/// +/// Operating frequency of the logical CPU in Hertz +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `Hz` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPU_LOGICAL_NUMBER`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_CPU_FREQUENCY: &str = "system.cpu.frequency"; + +/// ## Description +/// +/// Reports the number of logical (virtual) processor cores created by the operating system to manage multitasking. +/// +/// ## Notes +/// +/// Calculated by multiplying the number of sockets by the number of cores per socket, and then by the number of threads per core +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_CPU_LOGICAL_COUNT: &str = "system.cpu.logical.count"; + +/// ## Description +/// +/// Reports the number of actual physical processor cores on the hardware. +/// +/// ## Notes +/// +/// Calculated by multiplying the number of sockets by the number of cores per socket +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{cpu}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_CPU_PHYSICAL_COUNT: &str = "system.cpu.physical.count"; + +/// ## Description +/// +/// Seconds each logical CPU spent on each mode +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPU_LOGICAL_NUMBER`] | `Opt_in` +/// | [`crate::attribute::CPU_MODE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_CPU_TIME: &str = "system.cpu.time"; + +/// ## Description +/// +/// For each logical CPU, the utilization is calculated as the change in cumulative CPU time (cpu.time) over a measurement interval, divided by the elapsed time +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::CPU_LOGICAL_NUMBER`] | `Opt_in` +/// | [`crate::attribute::CPU_MODE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_CPU_UTILIZATION: &str = "system.cpu.utilization"; + +/// ## Description +/// +/// Disk bytes transferred +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DISK_IO_DIRECTION`] | `Recommended` +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_DISK_IO: &str = "system.disk.io"; + +/// ## Description +/// +/// Time disk spent activated. +/// +/// ## Notes +/// +/// The real elapsed time ("wall clock") used in the I/O path (time from operations running in parallel are not counted). Measured as: +/// +/// - Linux: Field 13 from [procfs-diskstats](https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats) +/// - Windows: The complement of +/// ["Disk% Idle Time"](https://learn.microsoft.com/archive/blogs/askcore/windows-performance-monitor-disk-counters-explained#windows-performance-monitor-disk-counters-explained) +/// performance counter: `uptime * (100 - "Disk\% Idle Time") / 100` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_DISK_IO_TIME: &str = "system.disk.io_time"; + +/// ## Description +/// +/// The total storage capacity of the disk +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_DISK_LIMIT: &str = "system.disk.limit"; + +/// ## Description +/// +/// The number of disk reads/writes merged into single physical disk access operations +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{operation}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DISK_IO_DIRECTION`] | `Recommended` +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_DISK_MERGED: &str = "system.disk.merged"; + +/// ## Description +/// +/// Sum of the time each operation took to complete. +/// +/// ## Notes +/// +/// Because it is the sum of time each request took, parallel-issued requests each contribute to make the count grow. Measured as: +/// +/// - Linux: Fields 7 & 11 from [procfs-diskstats](https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats) +/// - Windows: "Avg. Disk sec/Read" perf counter multiplied by "Disk Reads/sec" perf counter (similar for Writes) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DISK_IO_DIRECTION`] | `Recommended` +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_DISK_OPERATION_TIME: &str = "system.disk.operation_time"; + +/// ## Description +/// +/// Disk operations count +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{operation}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::DISK_IO_DIRECTION`] | `Recommended` +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_DISK_OPERATIONS: &str = "system.disk.operations"; + +/// ## Description +/// +/// The total storage capacity of the filesystem +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +/// | [`crate::attribute::SYSTEM_FILESYSTEM_MODE`] | `Recommended` +/// | [`crate::attribute::SYSTEM_FILESYSTEM_MOUNTPOINT`] | `Recommended` +/// | [`crate::attribute::SYSTEM_FILESYSTEM_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_FILESYSTEM_LIMIT: &str = "system.filesystem.limit"; + +/// ## Description +/// +/// Filesystem lock counts +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{lock}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::FILE_LOCK_MECHANISM`] | `Recommended` +/// | [`crate::attribute::FILE_LOCK_MODE`] | `Recommended` +/// | [`crate::attribute::FILE_LOCK_TYPE`] | `Recommended` +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_FILESYSTEM_LOCK_COUNT: &str = "system.filesystem.lock.count"; + +/// ## Description +/// +/// Reports a filesystem's space usage across different states. +/// +/// ## Notes +/// +/// The sum of all `system.filesystem.usage` values over the different `system.filesystem.state` attributes +/// SHOULD equal the total storage capacity of the filesystem, that is `system.filesystem.limit` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +/// | [`crate::attribute::SYSTEM_FILESYSTEM_MODE`] | `Recommended` +/// | [`crate::attribute::SYSTEM_FILESYSTEM_MOUNTPOINT`] | `Recommended` +/// | [`crate::attribute::SYSTEM_FILESYSTEM_STATE`] | `Recommended` +/// | [`crate::attribute::SYSTEM_FILESYSTEM_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_FILESYSTEM_USAGE: &str = "system.filesystem.usage"; + +/// ## Description +/// +/// Fraction of filesystem bytes used +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +/// | [`crate::attribute::SYSTEM_FILESYSTEM_MODE`] | `Recommended` +/// | [`crate::attribute::SYSTEM_FILESYSTEM_MOUNTPOINT`] | `Recommended` +/// | [`crate::attribute::SYSTEM_FILESYSTEM_STATE`] | `Recommended` +/// | [`crate::attribute::SYSTEM_FILESYSTEM_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_FILESYSTEM_UTILIZATION: &str = "system.filesystem.utilization"; + +/// ## Description +/// +/// The number of packets transferred +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{packet}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.memory.linux.available`., reason: renamed, renamed_to: system.memory.linux.available}" +)] +pub const SYSTEM_LINUX_MEMORY_AVAILABLE: &str = "system.linux.memory.available"; + +/// ## Description +/// +/// The number of packets transferred +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{packet}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::LINUX_MEMORY_SLAB_STATE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.memory.linux.slab.usage`., reason: renamed, renamed_to: system.memory.linux.slab.usage}" +)] +pub const SYSTEM_LINUX_MEMORY_SLAB_USAGE: &str = "system.linux.memory.slab.usage"; + +/// ## Description +/// +/// Total virtual memory available in the system +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LIMIT: &str = "system.memory.limit"; + +/// ## Description +/// +/// An estimate of how much memory is available for starting new applications, without causing swapping. +/// +/// ## Notes +/// +/// This is an alternative to `system.memory.usage` metric with `state=free`. +/// Linux starting from 3.14 exports "available" memory. It takes "free" memory as a baseline, and then factors in kernel-specific values. +/// This is supposed to be more accurate than just "free" memory. +/// For reference, see the calculations [here](https://superuser.com/a/980821). +/// See also `MemAvailable` in [/proc/meminfo](https://man7.org/linux/man-pages/man5/proc.5.html) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LINUX_AVAILABLE: &str = "system.memory.linux.available"; + +/// ## Description +/// +/// Total number of hugepages available +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{page}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LINUX_HUGEPAGES_LIMIT: &str = "system.memory.linux.hugepages.limit"; + +/// ## Description +/// +/// System hugepage size in bytes +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LINUX_HUGEPAGES_PAGE_SIZE: &str = "system.memory.linux.hugepages.page_size"; + +/// ## Description +/// +/// Number of reserved hugepages. +/// +/// ## Notes +/// +/// Hugepages for which a commitment to allocate has been made, but no allocation has yet been made. +/// This is reported as a separate metric rather than a `usage` state because reserved pages are already counted in `free` pages. +/// They represent a subset of free pages that cannot be used for non-reserved allocations +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{page}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LINUX_HUGEPAGES_RESERVED: &str = "system.memory.linux.hugepages.reserved"; + +/// ## Description +/// +/// Number of surplus hugepages. +/// +/// ## Notes +/// +/// Overcommitted hugepages beyond the persistent pool. +/// This is reported as a separate metric rather than a `usage` state because surplus pages can be in either `used` or `free` state. +/// Including them in `usage` would break the convention that `usage` states sum to the `limit` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{page}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LINUX_HUGEPAGES_SURPLUS: &str = "system.memory.linux.hugepages.surplus"; + +/// ## Description +/// +/// Number of hugepages in use by state +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{page}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_MEMORY_LINUX_HUGEPAGES_STATE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LINUX_HUGEPAGES_USAGE: &str = "system.memory.linux.hugepages.usage"; + +/// ## Description +/// +/// Percentage of hugepages in use by state +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_MEMORY_LINUX_HUGEPAGES_STATE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LINUX_HUGEPAGES_UTILIZATION: &str = + "system.memory.linux.hugepages.utilization"; + +/// ## Description +/// +/// Shared memory used (mostly by tmpfs). +/// +/// ## Notes +/// +/// Equivalent of `shared` from [`free` command](https://man7.org/linux/man-pages/man1/free.1.html) or +/// `Shmem` from [`/proc/meminfo`](https://man7.org/linux/man-pages/man5/proc.5.html)" +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LINUX_SHARED: &str = "system.memory.linux.shared"; + +/// ## Description +/// +/// Reports the memory used by the Linux kernel for managing caches of frequently used objects. +/// +/// ## Notes +/// +/// The sum over the `reclaimable` and `unreclaimable` state values in `memory.linux.slab.usage` SHOULD be equal to the total slab memory available on the system. +/// Note that the total slab memory is not constant and may vary over time. +/// See also the [Slab allocator](https://blogs.oracle.com/linux/post/understanding-linux-kernel-memory-statistics) and `Slab` in [/proc/meminfo](https://man7.org/linux/man-pages/man5/proc.5.html) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_MEMORY_LINUX_SLAB_STATE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_LINUX_SLAB_USAGE: &str = "system.memory.linux.slab.usage"; + +/// ## Description +/// +/// Deprecated, use `system.memory.linux.shared` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.memory.linux.shared`., reason: renamed, renamed_to: system.memory.linux.shared}" +)] +pub const SYSTEM_MEMORY_SHARED: &str = "system.memory.shared"; + +/// ## Description +/// +/// Reports memory in use by state +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_MEMORY_STATE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_USAGE: &str = "system.memory.usage"; + +/// ## Description +/// +/// Percentage of memory bytes in use +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_MEMORY_STATE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_MEMORY_UTILIZATION: &str = "system.memory.utilization"; + +/// ## Description +/// +/// The number of connections +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_CONNECTION_STATE`] | `Recommended` +/// | [`crate::attribute::NETWORK_INTERFACE_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_NETWORK_CONNECTION_COUNT: &str = "system.network.connection.count"; + +/// ## Description +/// +/// Deprecated, use `system.network.connection.count` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{connection}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_CONNECTION_STATE`] | `Recommended` +/// | [`crate::attribute::NETWORK_INTERFACE_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_TRANSPORT`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.network.connection.count`., reason: renamed, renamed_to: system.network.connection.count}" +)] +pub const SYSTEM_NETWORK_CONNECTIONS: &str = "system.network.connections"; + +/// ## Description +/// +/// Count of packets that are dropped or discarded even though there was no error. +/// +/// ## Notes +/// +/// Measured as: +/// +/// - Linux: the `drop` column in `/proc/dev/net` ([source](https://web.archive.org/web/20180321091318/http://www.onlamp.com/pub/a/linux/2000/11/16/LinuxAdmin.html)) +/// - Windows: [`InDiscards`/`OutDiscards`](https://docs.microsoft.com/windows/win32/api/netioapi/ns-netioapi-mib_if_row2) +/// from [`GetIfEntry2`](https://docs.microsoft.com/windows/win32/api/netioapi/nf-netioapi-getifentry2) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{packet}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_INTERFACE_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.network.packet.dropped`., reason: renamed, renamed_to: system.network.packet.dropped}" +)] +pub const SYSTEM_NETWORK_DROPPED: &str = "system.network.dropped"; + +/// ## Description +/// +/// Count of network errors detected. +/// +/// ## Notes +/// +/// Measured as: +/// +/// - Linux: the `errs` column in `/proc/net/dev` ([source](https://web.archive.org/web/20180321091318/http://www.onlamp.com/pub/a/linux/2000/11/16/LinuxAdmin.html)). +/// - Windows: [`InErrors`/`OutErrors`](https://docs.microsoft.com/windows/win32/api/netioapi/ns-netioapi-mib_if_row2) +/// from [`GetIfEntry2`](https://docs.microsoft.com/windows/win32/api/netioapi/nf-netioapi-getifentry2) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{error}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_INTERFACE_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_NETWORK_ERRORS: &str = "system.network.errors"; + +/// ## Description +/// +/// The number of bytes transmitted and received +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_INTERFACE_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_NETWORK_IO: &str = "system.network.io"; + +/// ## Description +/// +/// The number of packets transferred +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{packet}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_NETWORK_PACKET_COUNT: &str = "system.network.packet.count"; + +/// ## Description +/// +/// Count of packets that are dropped or discarded even though there was no error. +/// +/// ## Notes +/// +/// Measured as: +/// +/// - Linux: the `drop` column in `/proc/net/dev` ([source](https://web.archive.org/web/20180321091318/http://www.onlamp.com/pub/a/linux/2000/11/16/LinuxAdmin.html)) +/// - Windows: [`InDiscards`/`OutDiscards`](https://docs.microsoft.com/windows/win32/api/netioapi/ns-netioapi-mib_if_row2) +/// from [`GetIfEntry2`](https://docs.microsoft.com/windows/win32/api/netioapi/nf-netioapi-getifentry2) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{packet}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_INTERFACE_NAME`] | `Recommended` +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_NETWORK_PACKET_DROPPED: &str = "system.network.packet.dropped"; + +/// ## Description +/// +/// The number of packets transferred +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{packet}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::NETWORK_IO_DIRECTION`] | `Recommended` +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `system.network.packet.count`., reason: renamed, renamed_to: system.network.packet.count}" +)] +pub const SYSTEM_NETWORK_PACKETS: &str = "system.network.packets"; + +/// ## Description +/// +/// The number of page faults +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{fault}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_PAGING_FAULT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_PAGING_FAULTS: &str = "system.paging.faults"; + +/// ## Description +/// +/// The number of paging operations +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{operation}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_PAGING_DIRECTION`] | `Recommended` +/// | [`crate::attribute::SYSTEM_PAGING_FAULT_TYPE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_PAGING_OPERATIONS: &str = "system.paging.operations"; + +/// ## Description +/// +/// Unix swap or windows pagefile usage +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +/// | [`crate::attribute::SYSTEM_PAGING_STATE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_PAGING_USAGE: &str = "system.paging.usage"; + +/// ## Description +/// +/// Swap (unix) or pagefile (windows) utilization +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `1` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::SYSTEM_DEVICE`] | `Recommended` +/// | [`crate::attribute::SYSTEM_PAGING_STATE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_PAGING_UTILIZATION: &str = "system.paging.utilization"; + +/// ## Description +/// +/// Total number of processes in each state +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{process}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::PROCESS_STATE`] | `Recommended` +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_PROCESS_COUNT: &str = "system.process.count"; + +/// ## Description +/// +/// Total number of processes created over uptime of the host +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `counter` | +/// | Unit: | `{process}` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_PROCESS_CREATED: &str = "system.process.created"; + +/// ## Description +/// +/// The time the system has been running. +/// +/// ## Notes +/// +/// Instrumentations SHOULD use a gauge with type `double` and measure uptime in seconds as a floating point number with the highest precision available. +/// The actual accuracy would depend on the instrumentation and operating system +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +pub const SYSTEM_UPTIME: &str = "system.uptime"; + +/// ## Description +/// +/// Garbage collection duration. +/// +/// ## Notes +/// +/// The values can be retrieved from [`perf_hooks.PerformanceObserver(...).observe({ entryTypes: ['gc'] })`](https://nodejs.org/api/perf_hooks.html#performanceobserverobserveoptions) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `histogram` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::V8JS_GC_TYPE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const V8JS_GC_DURATION: &str = "v8js.gc.duration"; + +/// ## Description +/// +/// Deprecated, use `v8js.memory.heap.space.available_size` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::V8JS_HEAP_SPACE_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `v8js.memory.heap.space.available_size`., reason: renamed, renamed_to: v8js.memory.heap.space.available_size}" +)] +pub const V8JS_HEAP_SPACE_AVAILABLE_SIZE: &str = "v8js.heap.space.available_size"; + +/// ## Description +/// +/// Deprecated, use `v8js.memory.heap.space.physical_size` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::V8JS_HEAP_SPACE_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `v8js.memory.heap.space.physical_size`., reason: renamed, renamed_to: v8js.memory.heap.space.physical_size}" +)] +pub const V8JS_HEAP_SPACE_PHYSICAL_SIZE: &str = "v8js.heap.space.physical_size"; + +/// ## Description +/// +/// Deprecated, use `v8js.memory.heap.space.size` instead +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +#[cfg(feature = "semconv_experimental")] +#[deprecated( + note = "{note: Replaced by `v8js.memory.heap.space.size`., reason: renamed, renamed_to: v8js.memory.heap.space.size}" +)] +pub const V8JS_MEMORY_HEAP_LIMIT: &str = "v8js.memory.heap.limit"; + +/// ## Description +/// +/// Heap space available size. +/// +/// ## Notes +/// +/// Value can be retrieved from value `space_available_size` of [`v8.getHeapSpaceStatistics()`](https://nodejs.org/api/v8.html#v8getheapspacestatistics) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::V8JS_HEAP_SPACE_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const V8JS_MEMORY_HEAP_SPACE_AVAILABLE_SIZE: &str = "v8js.memory.heap.space.available_size"; + +/// ## Description +/// +/// Committed size of a heap space. +/// +/// ## Notes +/// +/// Value can be retrieved from value `physical_space_size` of [`v8.getHeapSpaceStatistics()`](https://nodejs.org/api/v8.html#v8getheapspacestatistics) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::V8JS_HEAP_SPACE_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const V8JS_MEMORY_HEAP_SPACE_PHYSICAL_SIZE: &str = "v8js.memory.heap.space.physical_size"; + +/// ## Description +/// +/// Total heap memory size pre-allocated for a heap space. +/// +/// ## Notes +/// +/// The value can be retrieved from value `space_size` of [`v8.getHeapSpaceStatistics()`](https://nodejs.org/api/v8.html#v8getheapspacestatistics) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::V8JS_HEAP_SPACE_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const V8JS_MEMORY_HEAP_SPACE_SIZE: &str = "v8js.memory.heap.space.size"; + +/// ## Description +/// +/// Heap Memory size allocated. +/// +/// ## Notes +/// +/// The value can be retrieved from value `space_used_size` of [`v8.getHeapSpaceStatistics()`](https://nodejs.org/api/v8.html#v8getheapspacestatistics) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `By` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::V8JS_HEAP_SPACE_NAME`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const V8JS_MEMORY_HEAP_USED: &str = "v8js.memory.heap.used"; + +/// ## Description +/// +/// Gauge of the active resources that are currently keeping the event loop alive. +/// +/// ## Notes +/// +/// The values can be retrieved from [`process.getActiveResourcesInfo()`](https://nodejs.org/api/process.html#processgetactiveresourcesinfo) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{resource}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::V8JS_RESOURCE_TYPE`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const V8JS_RESOURCE_ACTIVE: &str = "v8js.resource.active"; + +/// ## Description +/// +/// The number of changes (pull requests/merge requests/changelists) in a repository, categorized by their state (e.g. open or merged) +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{change}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::VCS_CHANGE_STATE`] | `Required` +/// | [`crate::attribute::VCS_OWNER_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_PROVIDER_NAME`] | `Opt_in` +/// | [`crate::attribute::VCS_REPOSITORY_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_REPOSITORY_URL_FULL`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const VCS_CHANGE_COUNT: &str = "vcs.change.count"; + +/// ## Description +/// +/// The time duration a change (pull request/merge request/changelist) has been in a given state +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::VCS_CHANGE_STATE`] | `Required` +/// | [`crate::attribute::VCS_OWNER_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_PROVIDER_NAME`] | `Opt_in` +/// | [`crate::attribute::VCS_REF_HEAD_NAME`] | `Required` +/// | [`crate::attribute::VCS_REPOSITORY_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_REPOSITORY_URL_FULL`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const VCS_CHANGE_DURATION: &str = "vcs.change.duration"; + +/// ## Description +/// +/// The amount of time since its creation it took a change (pull request/merge request/changelist) to get the first approval +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::VCS_OWNER_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_PROVIDER_NAME`] | `Opt_in` +/// | [`crate::attribute::VCS_REF_BASE_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_REF_BASE_REVISION`] | `Opt_in` +/// | [`crate::attribute::VCS_REF_HEAD_NAME`] | `Required` +/// | [`crate::attribute::VCS_REF_HEAD_REVISION`] | `Opt_in` +/// | [`crate::attribute::VCS_REPOSITORY_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_REPOSITORY_URL_FULL`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const VCS_CHANGE_TIME_TO_APPROVAL: &str = "vcs.change.time_to_approval"; + +/// ## Description +/// +/// The amount of time since its creation it took a change (pull request/merge request/changelist) to get merged into the target(base) ref +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::VCS_OWNER_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_PROVIDER_NAME`] | `Opt_in` +/// | [`crate::attribute::VCS_REF_BASE_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_REF_BASE_REVISION`] | `Opt_in` +/// | [`crate::attribute::VCS_REF_HEAD_NAME`] | `Required` +/// | [`crate::attribute::VCS_REF_HEAD_REVISION`] | `Opt_in` +/// | [`crate::attribute::VCS_REPOSITORY_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_REPOSITORY_URL_FULL`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const VCS_CHANGE_TIME_TO_MERGE: &str = "vcs.change.time_to_merge"; + +/// ## Description +/// +/// The number of unique contributors to a repository +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{contributor}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::VCS_OWNER_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_PROVIDER_NAME`] | `Opt_in` +/// | [`crate::attribute::VCS_REPOSITORY_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_REPOSITORY_URL_FULL`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const VCS_CONTRIBUTOR_COUNT: &str = "vcs.contributor.count"; + +/// ## Description +/// +/// The number of refs of type branch or tag in a repository +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{ref}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::VCS_OWNER_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_PROVIDER_NAME`] | `Opt_in` +/// | [`crate::attribute::VCS_REF_TYPE`] | `Required` +/// | [`crate::attribute::VCS_REPOSITORY_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_REPOSITORY_URL_FULL`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REF_COUNT: &str = "vcs.ref.count"; + +/// ## Description +/// +/// The number of lines added/removed in a ref (branch) relative to the ref from the `vcs.ref.base.name` attribute. +/// +/// ## Notes +/// +/// This metric should be reported for each `vcs.line_change.type` value. For example if a ref added 3 lines and removed 2 lines, +/// instrumentation SHOULD report two measurements: 3 and 2 (both positive numbers). +/// If number of lines added/removed should be calculated from the start of time, then `vcs.ref.base.name` SHOULD be set to an empty string +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{line}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::VCS_CHANGE_ID`] | `Conditionally_required`: if a change is associate with the ref. +/// | [`crate::attribute::VCS_LINE_CHANGE_TYPE`] | `Required` +/// | [`crate::attribute::VCS_OWNER_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_PROVIDER_NAME`] | `Opt_in` +/// | [`crate::attribute::VCS_REF_BASE_NAME`] | `Required` +/// | [`crate::attribute::VCS_REF_BASE_TYPE`] | `Required` +/// | [`crate::attribute::VCS_REF_HEAD_NAME`] | `Required` +/// | [`crate::attribute::VCS_REF_HEAD_TYPE`] | `Required` +/// | [`crate::attribute::VCS_REPOSITORY_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_REPOSITORY_URL_FULL`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REF_LINES_DELTA: &str = "vcs.ref.lines_delta"; + +/// ## Description +/// +/// The number of revisions (commits) a ref (branch) is ahead/behind the branch from the `vcs.ref.base.name` attribute. +/// +/// ## Notes +/// +/// This metric should be reported for each `vcs.revision_delta.direction` value. For example if branch `a` is 3 commits behind and 2 commits ahead of `trunk`, +/// instrumentation SHOULD report two measurements: 3 and 2 (both positive numbers) and `vcs.ref.base.name` is set to `trunk` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `{revision}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::VCS_CHANGE_ID`] | `Conditionally_required`: if a change is associate with the ref. +/// | [`crate::attribute::VCS_OWNER_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_PROVIDER_NAME`] | `Opt_in` +/// | [`crate::attribute::VCS_REF_BASE_NAME`] | `Required` +/// | [`crate::attribute::VCS_REF_BASE_TYPE`] | `Required` +/// | [`crate::attribute::VCS_REF_HEAD_NAME`] | `Required` +/// | [`crate::attribute::VCS_REF_HEAD_TYPE`] | `Required` +/// | [`crate::attribute::VCS_REPOSITORY_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_REPOSITORY_URL_FULL`] | `Required` +/// | [`crate::attribute::VCS_REVISION_DELTA_DIRECTION`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REF_REVISIONS_DELTA: &str = "vcs.ref.revisions_delta"; + +/// ## Description +/// +/// Time a ref (branch) created from the default branch (trunk) has existed. The `ref.type` attribute will always be `branch` +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `gauge` | +/// | Unit: | `s` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::VCS_OWNER_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_PROVIDER_NAME`] | `Opt_in` +/// | [`crate::attribute::VCS_REF_HEAD_NAME`] | `Required` +/// | [`crate::attribute::VCS_REF_HEAD_TYPE`] | `Required` +/// | [`crate::attribute::VCS_REPOSITORY_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_REPOSITORY_URL_FULL`] | `Required` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REF_TIME: &str = "vcs.ref.time"; + +/// ## Description +/// +/// The number of repositories in an organization +/// ## Metadata +/// | | | +/// |:-|:- +/// | Instrument: | `updowncounter` | +/// | Unit: | `{repository}` | +/// | Status: | `Development` | +/// +/// ## Attributes +/// | Name | Requirement | +/// |:-|:- | +/// | [`crate::attribute::VCS_OWNER_NAME`] | `Recommended` +/// | [`crate::attribute::VCS_PROVIDER_NAME`] | `Opt_in` +#[cfg(feature = "semconv_experimental")] +pub const VCS_REPOSITORY_COUNT: &str = "vcs.repository.count"; diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/resource.rs b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/resource.rs new file mode 100644 index 00000000..c2f0b3df --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/resource.rs @@ -0,0 +1,631 @@ +// DO NOT EDIT, this is an auto-generated file +// +// If you want to update the file: +// - Edit the template at scripts/templates/registry/rust/resource.rs.j2 +// - Run the script at scripts/generate-consts-from-spec.sh + +//! # Resource Semantic Conventions +//! +//! The [resource semantic conventions] define a set of standardized attributes +//! to be used in `Resource`s. +//! +//! [resource semantic conventions]: https://opentelemetry.io/docs/specs/semconv/resource/ +//! +//! ## Usage +//! +//! ```rust +//! use opentelemetry::KeyValue; +//! use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; +//! use opentelemetry_semantic_conventions as semconv; +//! +//! let _tracer = SdkTracerProvider::builder() +//! .with_resource(Resource::builder_empty().with_service_name("my-service").build()) +//! .build(); +//! ``` + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::ANDROID_OS_API_LEVEL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_BUILD_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_INSTALLATION_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_ECS_CLUSTER_ARN; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_ECS_CONTAINER_ARN; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_ECS_LAUNCHTYPE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_ECS_TASK_ARN; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_ECS_TASK_FAMILY; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_ECS_TASK_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_ECS_TASK_REVISION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_EKS_CLUSTER_ARN; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_LOG_GROUP_ARNS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_LOG_GROUP_NAMES; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_LOG_STREAM_ARNS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_LOG_STREAM_NAMES; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::BROWSER_BRANDS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::BROWSER_DOCUMENT_URL_FULL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::BROWSER_LANGUAGE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::BROWSER_MOBILE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::BROWSER_PLATFORM; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_PIPELINE_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_PIPELINE_RUN_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_PIPELINE_RUN_URL_FULL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_WORKER_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_WORKER_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_WORKER_URL_FULL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUD_ACCOUNT_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUD_AVAILABILITY_ZONE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUD_PLATFORM; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUD_PROVIDER; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUD_REGION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUD_RESOURCE_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUDFOUNDRY_APP_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUDFOUNDRY_APP_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUDFOUNDRY_ORG_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUDFOUNDRY_ORG_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUDFOUNDRY_PROCESS_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUDFOUNDRY_PROCESS_TYPE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUDFOUNDRY_SPACE_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUDFOUNDRY_SPACE_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUDFOUNDRY_SYSTEM_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUDFOUNDRY_SYSTEM_INSTANCE_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CONTAINER_COMMAND; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CONTAINER_COMMAND_ARGS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CONTAINER_COMMAND_LINE; + +pub use crate::attribute::CONTAINER_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CONTAINER_IMAGE_ID; + +pub use crate::attribute::CONTAINER_IMAGE_NAME; + +pub use crate::attribute::CONTAINER_IMAGE_REPO_DIGESTS; + +pub use crate::attribute::CONTAINER_IMAGE_TAGS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CONTAINER_LABEL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CONTAINER_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CONTAINER_RUNTIME_DESCRIPTION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CONTAINER_RUNTIME_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CONTAINER_RUNTIME_VERSION; + +pub use crate::attribute::DEPLOYMENT_ENVIRONMENT_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::DEVICE_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::DEVICE_MANUFACTURER; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::DEVICE_MODEL_IDENTIFIER; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::DEVICE_MODEL_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_INSTANCE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_MAX_MEMORY; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_VERSION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_APPHUB_APPLICATION_CONTAINER; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_APPHUB_APPLICATION_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_APPHUB_APPLICATION_LOCATION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_APPHUB_SERVICE_CRITICALITY_TYPE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_APPHUB_SERVICE_ENVIRONMENT_TYPE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_APPHUB_SERVICE_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_APPHUB_WORKLOAD_CRITICALITY_TYPE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_APPHUB_WORKLOAD_ENVIRONMENT_TYPE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_APPHUB_WORKLOAD_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_CLOUD_RUN_JOB_EXECUTION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_CLOUD_RUN_JOB_TASK_INDEX; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_GCE_INSTANCE_HOSTNAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_GCE_INSTANCE_LABELS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_GCE_INSTANCE_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_GCE_INSTANCE_GROUP_MANAGER_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_GCE_INSTANCE_GROUP_MANAGER_REGION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GCP_GCE_INSTANCE_GROUP_MANAGER_ZONE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HEROKU_APP_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HEROKU_RELEASE_COMMIT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HEROKU_RELEASE_CREATION_TIMESTAMP; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_ARCH; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_CPU_CACHE_L2_SIZE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_CPU_FAMILY; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_CPU_MODEL_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_CPU_MODEL_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_CPU_STEPPING; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_CPU_VENDOR_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_IMAGE_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_IMAGE_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_IMAGE_VERSION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_IP; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_MAC; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HOST_TYPE; + +pub use crate::attribute::K8S_CLUSTER_NAME; + +pub use crate::attribute::K8S_CLUSTER_UID; + +pub use crate::attribute::K8S_CONTAINER_NAME; + +pub use crate::attribute::K8S_CONTAINER_RESTART_COUNT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_CONTAINER_STATUS_LAST_TERMINATED_REASON; + +pub use crate::attribute::K8S_CRONJOB_ANNOTATION; + +pub use crate::attribute::K8S_CRONJOB_LABEL; + +pub use crate::attribute::K8S_CRONJOB_NAME; + +pub use crate::attribute::K8S_CRONJOB_UID; + +pub use crate::attribute::K8S_DAEMONSET_ANNOTATION; + +pub use crate::attribute::K8S_DAEMONSET_LABEL; + +pub use crate::attribute::K8S_DAEMONSET_NAME; + +pub use crate::attribute::K8S_DAEMONSET_UID; + +pub use crate::attribute::K8S_DEPLOYMENT_ANNOTATION; + +pub use crate::attribute::K8S_DEPLOYMENT_LABEL; + +pub use crate::attribute::K8S_DEPLOYMENT_NAME; + +pub use crate::attribute::K8S_DEPLOYMENT_UID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_HPA_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_HPA_SCALETARGETREF_API_VERSION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_HPA_SCALETARGETREF_KIND; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_HPA_SCALETARGETREF_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_HPA_UID; + +pub use crate::attribute::K8S_JOB_ANNOTATION; + +pub use crate::attribute::K8S_JOB_LABEL; + +pub use crate::attribute::K8S_JOB_NAME; + +pub use crate::attribute::K8S_JOB_UID; + +pub use crate::attribute::K8S_NAMESPACE_ANNOTATION; + +pub use crate::attribute::K8S_NAMESPACE_LABEL; + +pub use crate::attribute::K8S_NAMESPACE_NAME; + +pub use crate::attribute::K8S_NODE_ANNOTATION; + +pub use crate::attribute::K8S_NODE_LABEL; + +pub use crate::attribute::K8S_NODE_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_NODE_SYSTEM_CONTAINER_NAME; + +pub use crate::attribute::K8S_NODE_UID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_PERSISTENTVOLUME_ANNOTATION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_PERSISTENTVOLUME_LABEL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_PERSISTENTVOLUME_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_PERSISTENTVOLUME_RECLAIM_POLICY; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_PERSISTENTVOLUME_UID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_PERSISTENTVOLUMECLAIM_ANNOTATION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_PERSISTENTVOLUMECLAIM_LABEL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_PERSISTENTVOLUMECLAIM_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_PERSISTENTVOLUMECLAIM_UID; + +pub use crate::attribute::K8S_POD_ANNOTATION; + +pub use crate::attribute::K8S_POD_HOSTNAME; + +pub use crate::attribute::K8S_POD_IP; + +pub use crate::attribute::K8S_POD_LABEL; + +pub use crate::attribute::K8S_POD_NAME; + +pub use crate::attribute::K8S_POD_START_TIME; + +pub use crate::attribute::K8S_POD_UID; + +pub use crate::attribute::K8S_REPLICASET_ANNOTATION; + +pub use crate::attribute::K8S_REPLICASET_LABEL; + +pub use crate::attribute::K8S_REPLICASET_NAME; + +pub use crate::attribute::K8S_REPLICASET_UID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_REPLICATIONCONTROLLER_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_REPLICATIONCONTROLLER_UID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_RESOURCEQUOTA_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_RESOURCEQUOTA_UID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_SERVICE_ANNOTATION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_SERVICE_LABEL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_SERVICE_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_SERVICE_PUBLISH_NOT_READY_ADDRESSES; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_SERVICE_SELECTOR; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_SERVICE_TRAFFIC_DISTRIBUTION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_SERVICE_TYPE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_SERVICE_UID; + +pub use crate::attribute::K8S_STATEFULSET_ANNOTATION; + +pub use crate::attribute::K8S_STATEFULSET_LABEL; + +pub use crate::attribute::K8S_STATEFULSET_NAME; + +pub use crate::attribute::K8S_STATEFULSET_UID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::K8S_STORAGECLASS_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::MAINFRAME_LPAR_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::OCI_MANIFEST_DIGEST; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::OPENSHIFT_CLUSTERQUOTA_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::OPENSHIFT_CLUSTERQUOTA_UID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::OS_BUILD_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::OS_DESCRIPTION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::OS_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::OS_TYPE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::OS_VERSION; + +pub use crate::attribute::OTEL_SCOPE_NAME; + +pub use crate::attribute::OTEL_SCOPE_VERSION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_ARGS_COUNT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_COMMAND; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_COMMAND_ARGS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_COMMAND_LINE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_CREATION_TIME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_EXECUTABLE_BUILD_ID_GNU; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_EXECUTABLE_BUILD_ID_GO; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_EXECUTABLE_BUILD_ID_HTLHASH; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_EXECUTABLE_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_EXECUTABLE_PATH; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_INTERACTIVE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_LINUX_CGROUP; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_OWNER; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_PARENT_PID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_PID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_RUNTIME_DESCRIPTION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_RUNTIME_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_RUNTIME_VERSION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_TITLE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_WORKING_DIRECTORY; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::SERVICE_CRITICALITY; + +pub use crate::attribute::SERVICE_INSTANCE_ID; + +pub use crate::attribute::SERVICE_NAME; + +pub use crate::attribute::SERVICE_NAMESPACE; + +pub use crate::attribute::SERVICE_VERSION; + +pub use crate::attribute::TELEMETRY_DISTRO_NAME; + +pub use crate::attribute::TELEMETRY_DISTRO_VERSION; + +pub use crate::attribute::TELEMETRY_SDK_LANGUAGE; + +pub use crate::attribute::TELEMETRY_SDK_NAME; + +pub use crate::attribute::TELEMETRY_SDK_VERSION; + +pub use crate::attribute::USER_AGENT_ORIGINAL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::VCS_REF_HEAD_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::VCS_REF_HEAD_REVISION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::VCS_REF_TYPE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::VCS_REPOSITORY_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::VCS_REPOSITORY_URL_FULL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::WEBENGINE_DESCRIPTION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::WEBENGINE_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::WEBENGINE_VERSION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::ZOS_SMF_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::ZOS_SYSPLEX_NAME; diff --git a/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/trace.rs b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/trace.rs new file mode 100644 index 00000000..2e280cdb --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry-semantic-conventions/src/trace.rs @@ -0,0 +1,755 @@ +// DO NOT EDIT, this is an auto-generated file +// +// If you want to update the file: +// - Edit the template at scripts/templates/registry/rust/attributes.rs.j2 +// - Run the script at scripts/generate-consts-from-spec.sh + +//! # Trace Semantic Conventions +//! +//! The [trace semantic conventions] define a set of standardized attributes to +//! be used in `Span`s. +//! +//! [trace semantic conventions]: https://opentelemetry.io/docs/specs/semconv/general/trace/ +//! +//! ## Usage +//! +//! ```rust +//! use opentelemetry::KeyValue; +//! use opentelemetry::{global, trace::Tracer as _}; +//! use opentelemetry_semantic_conventions as semconv; +//! +//! let tracer = global::tracer("my-component"); +//! let _span = tracer +//! .span_builder("span-name") +//! .with_attributes([ +//! KeyValue::new(semconv::trace::CLIENT_ADDRESS, "example.org"), +//! KeyValue::new(semconv::trace::CLIENT_PORT, 80i64), +//! ]) +//! .start(&tracer); +//! ``` + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::ANDROID_APP_STATE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_BUILD_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_CRASH_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_JANK_FRAME_COUNT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_JANK_PERIOD; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_JANK_THRESHOLD; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_SCREEN_COORDINATE_X; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_SCREEN_COORDINATE_Y; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_SCREEN_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_SCREEN_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_WIDGET_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::APP_WIDGET_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_BEDROCK_GUARDRAIL_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_BEDROCK_KNOWLEDGE_BASE_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_ATTRIBUTES_TO_GET; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_CONSISTENT_READ; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_CONSUMED_CAPACITY; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_COUNT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_EXCLUSIVE_START_TABLE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_INDEX_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_ITEM_COLLECTION_METRICS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_LIMIT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_PROJECTION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_PROVISIONED_READ_CAPACITY; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_SCAN_FORWARD; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_SCANNED_COUNT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_SEGMENT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_SELECT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_TABLE_COUNT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_TABLE_NAMES; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_DYNAMODB_TOTAL_SEGMENTS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_EXTENDED_REQUEST_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_LAMBDA_INVOKED_ARN; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_LAMBDA_RESOURCE_MAPPING_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_REQUEST_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_S3_BUCKET; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_S3_COPY_SOURCE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_S3_DELETE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_S3_KEY; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_S3_PART_NUMBER; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AWS_S3_UPLOAD_ID; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::AZ_SERVICE_REQUEST_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AZURE_CLIENT_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AZURE_COSMOSDB_CONNECTION_MODE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AZURE_COSMOSDB_CONSISTENCY_LEVEL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AZURE_COSMOSDB_OPERATION_CONTACTED_REGIONS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AZURE_COSMOSDB_OPERATION_REQUEST_CHARGE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AZURE_COSMOSDB_REQUEST_BODY_SIZE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AZURE_COSMOSDB_RESPONSE_SUB_STATUS_CODE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AZURE_RESOURCE_PROVIDER_NAMESPACE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::AZURE_SERVICE_REQUEST_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CASSANDRA_CONSISTENCY_LEVEL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CASSANDRA_COORDINATOR_DC; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CASSANDRA_COORDINATOR_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CASSANDRA_PAGE_SIZE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CASSANDRA_QUERY_IDEMPOTENT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CASSANDRA_SPECULATIVE_EXECUTION_COUNT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_PIPELINE_ACTION_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_PIPELINE_RESULT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_PIPELINE_TASK_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_PIPELINE_TASK_RUN_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_PIPELINE_TASK_RUN_RESULT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CICD_PIPELINE_TASK_RUN_URL_FULL; + +pub use crate::attribute::CLIENT_ADDRESS; + +pub use crate::attribute::CLIENT_PORT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUD_REGION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::CLOUD_RESOURCE_ID; + +pub use crate::attribute::DB_COLLECTION_NAME; + +pub use crate::attribute::DB_NAMESPACE; + +pub use crate::attribute::DB_OPERATION_BATCH_SIZE; + +pub use crate::attribute::DB_OPERATION_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::DB_OPERATION_PARAMETER; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::DB_QUERY_PARAMETER; + +pub use crate::attribute::DB_QUERY_SUMMARY; + +pub use crate::attribute::DB_QUERY_TEXT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::DB_RESPONSE_RETURNED_ROWS; + +pub use crate::attribute::DB_RESPONSE_STATUS_CODE; + +pub use crate::attribute::DB_STORED_PROCEDURE_NAME; + +pub use crate::attribute::DB_SYSTEM_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::DNS_ANSWERS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::DNS_QUESTION_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::ELASTICSEARCH_NODE_NAME; + +pub use crate::attribute::ERROR_TYPE; + +#[allow(deprecated)] +pub use crate::attribute::EXCEPTION_ESCAPED; + +pub use crate::attribute::EXCEPTION_MESSAGE; + +pub use crate::attribute::EXCEPTION_STACKTRACE; + +pub use crate::attribute::EXCEPTION_TYPE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_COLDSTART; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_CRON; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_DOCUMENT_COLLECTION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_DOCUMENT_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_DOCUMENT_OPERATION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_DOCUMENT_TIME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_INVOKED_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_INVOKED_PROVIDER; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_INVOKED_REGION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_TIME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FAAS_TRIGGER; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FEATURE_FLAG_CONTEXT_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FEATURE_FLAG_ERROR_MESSAGE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FEATURE_FLAG_KEY; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FEATURE_FLAG_PROVIDER_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FEATURE_FLAG_RESULT_REASON; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FEATURE_FLAG_RESULT_VALUE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FEATURE_FLAG_RESULT_VARIANT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FEATURE_FLAG_SET_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::FEATURE_FLAG_VERSION; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_AGENT_DESCRIPTION; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_AGENT_ID; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_AGENT_NAME; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_AGENT_VERSION; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_CONVERSATION_ID; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_DATA_SOURCE_ID; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_EMBEDDINGS_DIMENSION_COUNT; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_EVALUATION_EXPLANATION; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_EVALUATION_NAME; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_EVALUATION_SCORE_LABEL; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_EVALUATION_SCORE_VALUE; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_INPUT_MESSAGES; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_OPERATION_NAME; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_OUTPUT_MESSAGES; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_OUTPUT_TYPE; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_PROMPT_NAME; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_PROVIDER_NAME; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_CHOICE_COUNT; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_ENCODING_FORMATS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_FREQUENCY_PENALTY; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_MAX_TOKENS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_MODEL; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_PRESENCE_PENALTY; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_SEED; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_STOP_SEQUENCES; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_STREAM; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_TEMPERATURE; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_TOP_K; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_REQUEST_TOP_P; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_RESPONSE_FINISH_REASONS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_RESPONSE_ID; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_RESPONSE_MODEL; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_RETRIEVAL_DOCUMENTS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_RETRIEVAL_QUERY_TEXT; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_SYSTEM; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_SYSTEM_INSTRUCTIONS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_TOOL_CALL_ARGUMENTS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_TOOL_CALL_ID; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_TOOL_CALL_RESULT; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_TOOL_DEFINITIONS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_TOOL_DESCRIPTION; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_TOOL_NAME; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_TOOL_TYPE; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_USAGE_INPUT_TOKENS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_USAGE_OUTPUT_TOKENS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_USAGE_REASONING_OUTPUT_TOKENS; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::GEN_AI_WORKFLOW_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GRAPHQL_DOCUMENT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GRAPHQL_OPERATION_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::GRAPHQL_OPERATION_TYPE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HTTP_REQUEST_BODY_SIZE; + +pub use crate::attribute::HTTP_REQUEST_HEADER; + +pub use crate::attribute::HTTP_REQUEST_METHOD; + +pub use crate::attribute::HTTP_REQUEST_METHOD_ORIGINAL; + +pub use crate::attribute::HTTP_REQUEST_RESEND_COUNT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HTTP_REQUEST_SIZE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HTTP_RESPONSE_BODY_SIZE; + +pub use crate::attribute::HTTP_RESPONSE_HEADER; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::HTTP_RESPONSE_SIZE; + +pub use crate::attribute::HTTP_RESPONSE_STATUS_CODE; + +pub use crate::attribute::HTTP_ROUTE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::IOS_APP_STATE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::JSONRPC_PROTOCOL_VERSION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::JSONRPC_REQUEST_ID; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::MCP_METHOD_NAME; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::MCP_PROTOCOL_VERSION; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::MCP_RESOURCE_URI; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::MCP_SESSION_ID; + +pub use crate::attribute::NETWORK_LOCAL_ADDRESS; + +pub use crate::attribute::NETWORK_LOCAL_PORT; + +pub use crate::attribute::NETWORK_PEER_ADDRESS; + +pub use crate::attribute::NETWORK_PEER_PORT; + +pub use crate::attribute::NETWORK_PROTOCOL_NAME; + +pub use crate::attribute::NETWORK_PROTOCOL_VERSION; + +pub use crate::attribute::NETWORK_TRANSPORT; + +pub use crate::attribute::NETWORK_TYPE; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::OPENAI_API_TYPE; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::OPENAI_REQUEST_SERVICE_TIER; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::OPENAI_RESPONSE_SERVICE_TIER; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::OPENAI_RESPONSE_SYSTEM_FINGERPRINT; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::ORACLE_DB_DOMAIN; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::ORACLE_DB_INSTANCE_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::ORACLE_DB_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::ORACLE_DB_PDB; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::ORACLE_DB_SERVICE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::OS_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::OS_VERSION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_COMMAND_ARGS; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_EXECUTABLE_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_EXECUTABLE_PATH; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_EXIT_CODE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::PROCESS_PID; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::RPC_MESSAGE_COMPRESSED_SIZE; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::RPC_MESSAGE_ID; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::RPC_MESSAGE_TYPE; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::RPC_MESSAGE_UNCOMPRESSED_SIZE; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::RPC_METHOD; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::RPC_METHOD_ORIGINAL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::RPC_REQUEST_METADATA; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::RPC_RESPONSE_METADATA; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::RPC_RESPONSE_STATUS_CODE; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::RPC_SERVICE; + +#[cfg(feature = "semconv_experimental")] +#[allow(deprecated)] +pub use crate::attribute::RPC_SYSTEM; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::RPC_SYSTEM_NAME; + +pub use crate::attribute::SERVER_ADDRESS; + +pub use crate::attribute::SERVER_PORT; + +pub use crate::attribute::SERVICE_VERSION; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::SESSION_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::SESSION_PREVIOUS_ID; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::TLS_PROTOCOL_NAME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::TLS_PROTOCOL_VERSION; + +pub use crate::attribute::URL_FULL; + +pub use crate::attribute::URL_PATH; + +pub use crate::attribute::URL_QUERY; + +pub use crate::attribute::URL_SCHEME; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::URL_TEMPLATE; + +pub use crate::attribute::USER_AGENT_ORIGINAL; + +#[cfg(feature = "semconv_experimental")] +pub use crate::attribute::USER_AGENT_SYNTHETIC_TYPE; diff --git a/vendor/opentelemetry-rust/opentelemetry/CHANGELOG.md b/vendor/opentelemetry-rust/opentelemetry/CHANGELOG.md new file mode 100644 index 00000000..e3d3d9ea --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/CHANGELOG.md @@ -0,0 +1,952 @@ +# Changelog + +## vNext + +## 0.33.0 + +Released 2026-Sep-18 + +- Fix `TraceState` accepting more than the 32 list-members the W3C trace-context + specification allows. `from_str`, `from_key_value` and `insert` now keep at most + 32, dropping members from the end of the list as the specification prescribes, so + neither a parsed nor a locally built `tracestate` can exceed the limit. +- **Added** experimental support for a global context event observer. A + `ContextObserver` can be registered via `GlobalContextObserver::set` to be + notified of context transitions through the `on_context_enter` and + `on_context_exit` callbacks. This feature is primarily intended to publish a + different view of the current context (the `ObserverContextView`) through + alternative channels that let external readers (e.g. an eBPF profiler) track + the current context. See the associated + [OTEP](https://github.com/open-telemetry/opentelemetry-specification/pull/4947). + Gated behind the `experimental_context_observer` feature flag. +- `otel_info!`, `otel_warn!`, `otel_debug!`, and `otel_error!` macros now accept quoted-key fields + (e.g. `"otel.component.type" = "value"`) for dotted attribute names. +- **Added** `BoundGauge` and `BoundUpDownCounter` types (and the + corresponding `Gauge::bind()` / `UpDownCounter::bind()` methods), completing + the experimental bound-instrument API across all sync instruments + (`Counter`, `UpDownCounter`, `Histogram`, `Gauge`). Gated behind the + `experimental_metrics_bound_instruments` feature flag. +- Fix a panic when a value stored with `Context::with_value()` calls + `Context::current()` from its `Drop` implementation. + +## 0.32.0 + +Released 2026-May-08 + +- **Added** `BoundCounter` and `BoundHistogram` types that cache resolved + aggregator references for a fixed attribute set. Created via `Counter::bind()` + and `Histogram::bind()`, bound instruments bypass per-call attribute lookup, + providing significant performance improvements for hot paths where the same + attributes are used repeatedly. Both types implement `Clone` so a single bound + state can be shared across threads or modules without re-binding. Also adds + the `SyncInstrument::bind()` trait method and `BoundSyncInstrument` trait + for SDK implementors; the trait method has a no-op default so custom + `SyncInstrument` impls degrade gracefully without panicking. Gated behind the + `experimental_metrics_bound_instruments` feature flag. +- Add `reserve` method to `opentelemetry::propagation::Injector` to hint at the number of elements that will be added to avoid multiple resize operations of the underlying data structure. Has an empty default implementation. +- **Breaking** Removed the following public fields and methods from the `SpanBuilder` [#3227][3227]: + - `trace_id`, `span_id`, `end_time`, `status`, `sampling_result` + - `with_trace_id`, `with_span_id`, `with_end_time`, `with_status`, `with_sampling_result` +- **Added** `#[must_use]` attribute to `opentelemetry::metrics::AsyncInstrumentBuilder` to add compile time warning when `.build()` is not called on observable instrument builders, preventing silent failures where callbacks are never registered and metrics are never reported. +- **Breaking** Moved the following SDK sampling types from `opentelemetry::trace` to `opentelemetry_sdk::trace` [#3277][3277]: + - `SamplingDecision`, `SamplingResult` + - These types are SDK implementation details and should be imported from `opentelemetry_sdk::trace` instead. +- "spec_unstable_logs_enabled" feature flag is removed. The capability (and the + backing specification) is now stable and is enabled by default. + [3278](https://github.com/open-telemetry/opentelemetry-rust/pull/3278) +- Remove the empty "message" field from `tracing` events emitted via the `internal-logs` feature +- Fix panic when calling `Context::current()` from `Drop` implementations triggered by `ContextGuard` cleanup ([#3262][3262]). + +[3227]: https://github.com/open-telemetry/opentelemetry-rust/pull/3227 +[3262]: https://github.com/open-telemetry/opentelemetry-rust/pull/3262 +[3277]: https://github.com/open-telemetry/opentelemetry-rust/pull/3277 + +## v0.31.0 + +Released 2025-Sep-25 + +- *Breaking* Change return type of `opentelemetry::global::set_tracer_provider` to Unit to align with metrics counterpart +- Add `get_all` method to `opentelemetry::propagation::Extractor` to return all values of the given propagation key and provide a default implementation. +- Add an `IntoIterator` implementation for `opentelemetry::trace::TraceState` to allow iterating through its key-value pair collection. + +## 0.30.0 + +Released 2025-May-23 + +[#2821](https://github.com/open-telemetry/opentelemetry-rust/pull/2821) Context +based suppression capabilities added: Added the ability to prevent recursive +telemetry generation through new context-based suppression mechanisms. This +feature helps prevent feedback loops and excessive telemetry when OpenTelemetry +components perform their own operations. + +New methods added to `Context`: + +- `is_telemetry_suppressed()` - Checks if telemetry is suppressed in this + context +- `with_telemetry_suppressed()` - Creates a new context with telemetry + suppression enabled +- `is_current_telemetry_suppressed()` - Efficiently checks if the current thread's context + has telemetry suppressed +- `enter_telemetry_suppressed_scope()` - Convenience method to enter a scope where telemetry is + suppressed + +These methods allow SDK components, exporters, and processors to temporarily +disable telemetry generation during their internal operations, ensuring more +predictable and efficient observability pipelines. + +- re-export `tracing` for `internal-logs` feature to remove the need of adding `tracing` as a dependency + +## 0.29.1 + +Release 2025-Apr-01 + +- Bug Fix: Re-export `WithContext` at `opentelemetry::trace::context::WithContext` [#2879](https://github.com/open-telemetry/opentelemetry-rust/pull/2879) to restore backwards compatibility + - The new path for `WithContext` and `FutureExt` are in `opentelemetry::context` as they are independent of the trace signal. Users should prefer this path. + +## 0.29.0 + +Released 2025-Mar-21 + +- *Breaking* Moved `ExportError` trait from `opentelemetry::trace::ExportError` to `opentelemetry_sdk::export::ExportError` +- *Breaking* Moved `TraceError` enum from `opentelemetry::trace::TraceError` to `opentelemetry_sdk::trace::TraceError` +- *Breaking* Moved `TraceResult` type alias from `opentelemetry::trace::TraceResult` to `opentelemetry_sdk::trace::TraceResult` +- Bug Fix: `InstrumentationScope` implementation for `PartialEq` and `Hash` fixed to include Attributes also. +- **Breaking changes for baggage users**: [#2717](https://github.com/open-telemetry/opentelemetry-rust/issues/2717) + - Changed value type of `Baggage` from `Value` to `StringValue` + - Updated `Baggage` constants to reflect latest standard (`MAX_KEY_VALUE_PAIRS` - 180 -> 64, `MAX_BYTES_FOR_ONE_PAIR` - removed) and increased insert performance see #[2284](https://github.com/open-telemetry/opentelemetry-rust/pull/2284). + - Align `Baggage.remove()` signature with `.get()` to take the key as a reference + - `Baggage` can't be retrieved from the `Context` directly anymore and needs to be accessed via `context.baggage()` + - `with_baggage()` and `current_with_baggage()` override any existing `Baggage` in the `Context` + - `Baggage` keys can't be empty and only allow ASCII visual chars, except `"(),/:;<=>?@[\]{}` (see [RFC7230, Section 3.2.6](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6)) + - `KeyValueMetadata` does not publicly expose its fields. This should be transparent change to the users. +- Changed `Context` to use a stack to properly handle out of order dropping of `ContextGuard`. This imposes a limit of `65535` nested contexts on a single thread. See #[2378](https://github.com/open-telemetry/opentelemetry-rust/pull/2284) and #[1887](https://github.com/open-telemetry/opentelemetry-rust/issues/1887). +- Added additional `name: Option<&str>` parameter to the `event_enabled` method + on the `Logger` trait. This allows implementations (SDK, processor, exporters) + to leverage this additional information to determine if an event is enabled. + +## 0.28.0 + +Released 2025-Feb-10 + +- Bump msrv to 1.75.0. +- **Breaking** `opentelemetry::global::shutdown_tracer_provider()` Removed from this crate, should now use `tracer_provider.shutdown()` see [#2369](https://github.com/open-telemetry/opentelemetry-rust/pull/2369) for a migration example. +- *Breaking* Removed unused `opentelemetry::PropagationError` struct. + +## 0.27.1 + +Released 2024-Nov-27 + +## 0.27.0 + +Released 2024-Nov-11 + +- Bump MSRV to 1.70 [#2179](https://github.com/open-telemetry/opentelemetry-rust/pull/2179) +- Add `LogRecord::set_trace_context`; an optional method conditional on the `trace` feature for setting trace context on a log record. +- Removed unnecessary public methods named `as_any` from `AsyncInstrument` trait and the implementing instruments: `ObservableCounter`, `ObservableGauge`, and `ObservableUpDownCounter` [#2187](https://github.com/open-telemetry/opentelemetry-rust/pull/2187) +- Introduced `SyncInstrument` trait to replace the individual synchronous instrument traits (`SyncCounter`, `SyncGauge`, `SyncHistogram`, `SyncUpDownCounter`) which are meant for SDK implementation. [#2207](https://github.com/open-telemetry/opentelemetry-rust/pull/2207) +- Ensured that `observe` method on asynchronous instruments can only be called inside a callback. This was done by removing the implementation of `AsyncInstrument` trait for each of the asynchronous instruments. [#2210](https://github.com/open-telemetry/opentelemetry-rust/pull/2210) +- Removed `PartialOrd` and `Ord` implementations for `KeyValue`. [#2215](https://github.com/open-telemetry/opentelemetry-rust/pull/2215) +- **Breaking change for exporter authors:** Marked `KeyValue` related structs and enums as `non_exhaustive`. [#2228](https://github.com/open-telemetry/opentelemetry-rust/pull/2228) +- **Breaking change for log exporter authors:** Marked `AnyValue` enum as `non_exhaustive`. [#2230](https://github.com/open-telemetry/opentelemetry-rust/pull/2230) +- **Breaking change for Metrics users:** The `init` method used to create instruments has been renamed to `build`. Also, `try_init()` method is removed from instrument builders. The return types of `InstrumentProvider` trait methods modified to return the instrument struct, instead of `Result`. [#2227](https://github.com/open-telemetry/opentelemetry-rust/pull/2227) + +Before: +```rust +let counter = meter.u64_counter("my_counter").init(); +``` + +Now: +```rust +let counter = meter.u64_counter("my_counter").build(); +``` +- **Breaking change**: [#2220](https://github.com/open-telemetry/opentelemetry-rust/pull/2220) + - Removed deprecated method `InstrumentationLibrary::new` + - Renamed `InstrumentationLibrary` to `InstrumentationScope` + - Renamed `InstrumentationLibraryBuilder` to `InstrumentationScopeBuilder` + - Removed deprecated methods `LoggerProvider::versioned_logger` and `TracerProvider::versioned_tracer` + - Removed methods `LoggerProvider::logger_builder`, `TracerProvider::tracer_builder` and `MeterProvider::versioned_meter` + - Replaced these methods with `LoggerProvider::logger_with_scope`, `TracerProvider::logger_with_scope`, `MeterProvider::meter_with_scope` + - Replaced `global::meter_with_version` with `global::meter_with_scope` + - Added `global::tracer_with_scope` + - Refer to PR description for migration guide. +- **Breaking change**: replaced `InstrumentationScope` public attributes by getters [#2275](https://github.com/open-telemetry/opentelemetry-rust/pull/2275) + +- **Breaking change**: [#2260](https://github.com/open-telemetry/opentelemetry-rust/pull/2260) + - Removed `global::set_error_handler` and `global::handle_error`. + - `global::handle_error` usage inside the opentelemetry crates has been replaced with `global::otel_info`, `otel_warn`, `otel_debug` and `otel_error` macros based on the severity of the internal logs. + - The default behavior of `global::handle_error` was to log the error using `eprintln!`. With otel macros, the internal logs get emitted via `tracing` macros of matching severity. Users now need to configure a `tracing` layer/subscriber to capture these logs. + - Refer to PR description for migration guide. Also refer to [self-diagnostics](https://github.com/open-telemetry/opentelemetry-rust/tree/main/examples/self-diagnostics) example to learn how to view internal logs in stdout using `tracing::fmt` layer. + +- **Breaking change for exporter/processor authors:** [#2266](https://github.com/open-telemetry/opentelemetry-rust/pull/2266) + - Moved `ExportError` trait from `opentelemetry::ExportError` to `opentelemetry_sdk::export::ExportError` + - Created new trait `opentelemetry::trace::ExportError` for trace API. This would be eventually be consolidated with ExportError in the SDK. + - Moved `LogError` enum from `opentelemetry::logs::LogError` to `opentelemetry_sdk::logs::LogError` + - Moved `LogResult` type alias from `opentelemetry::logs::LogResult` to `opentelemetry_sdk::logs::LogResult` + - Moved `MetricError` enum from `opentelemetry::metrics::MetricError` to `opentelemetry_sdk::metrics::MetricError` + - Moved `MetricResult` type alias from `opentelemetry::metrics::MetricResult` to `opentelemetry_sdk::metrics::MetricResult` + These changes shouldn't directly affect the users of OpenTelemetry crate, as these constructs are used in SDK and Exporters. If you are an author of an sdk component/plug-in, like an exporter etc. please use these types from sdk. Refer [CHANGELOG.md](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-sdk/CHANGELOG.md) for more details, under same version section. +- **Breaking** [2291](https://github.com/open-telemetry/opentelemetry-rust/pull/2291) Rename `logs_level_enabled flag` to `spec_unstable_logs_enabled`. Please enable this updated flag if the feature is needed. This flag will be removed once the feature is stabilized in the specifications. + +## v0.26.0 +Released 2024-Sep-30 + +- **BREAKING** Public API changes: + - **Removed**: `Key.bool()`, `Key.i64()`, `Key.f64()`, `Key.string()`, `Key.array()` [#2090](https://github.com/open-telemetry/opentelemetry-rust/issues/2090). These APIs were redundant as they didn't offer any additional functionality. The existing `KeyValue::new()` API covers all the scenarios offered by these APIs. + + - **Removed**: `ObjectSafeMeterProvider` and `GlobalMeterProvider` [#2112](https://github.com/open-telemetry/opentelemetry-rust/pull/2112). These APIs were unnecessary and were mainly meant for internal use. + + - **Modified**: `MeterProvider.meter()` and `MeterProvider.versioned_meter()` argument types have been updated to `&'static str` instead of `impl Into>>` [#2112](https://github.com/open-telemetry/opentelemetry-rust/pull/2112). These APIs were modified to enforce the Meter `name`, `version`, and `schema_url` to be `&'static str`. + + - **Renamed**: `NoopMeterCore` to `NoopMeter` + +- Added `with_boundaries` API to allow users to provide custom bounds for Histogram instruments. [#2135](https://github.com/open-telemetry/opentelemetry-rust/pull/2135) + +## v0.25.0 + +- **BREAKING** [#1993](https://github.com/open-telemetry/opentelemetry-rust/pull/1993) Box complex types in AnyValue enum +Before: +```rust +#[derive(Debug, Clone, PartialEq)] +pub enum AnyValue { + /// An integer value + Int(i64), + /// A double value + Double(f64), + /// A string value + String(StringValue), + /// A boolean value + Boolean(bool), + /// A byte array + Bytes(Vec), + /// An array of `Any` values + ListAny(Vec), + /// A map of string keys to `Any` values, arbitrarily nested. + Map(HashMap), +} +``` + +After: +```rust +#[derive(Debug, Clone, PartialEq)] +pub enum AnyValue { + /// An integer value + Int(i64), + /// A double value + Double(f64), + /// A string value + String(StringValue), + /// A boolean value + Boolean(bool), + /// A byte array + Bytes(Box>), + /// An array of `Any` values + ListAny(Box>), + /// A map of string keys to `Any` values, arbitrarily nested. + Map(Box>), +} +``` +So the custom log appenders should box these types while adding them in message body, or +attribute values. Similarly, the custom exporters should dereference these complex type values +before serializing. + +*Breaking* : +[#2015](https://github.com/open-telemetry/opentelemetry-rust/pull/2015) Removed +the ability to register callbacks for Observable instruments on Meter directly. +If you were using `meter.register_callback` to provide the callback, provide +them using `with_callback` method, while creating the Observable instrument +itself. +[1715](https://github.com/open-telemetry/opentelemetry-rust/pull/1715/files) +shows the exact changes needed to make this migration. If you are starting new, +refer to the +[examples](https://github.com/open-telemetry/opentelemetry-rust/blob/main/examples/metrics-basic/src/main.rs) +to learn how to provide Observable callbacks. + +## v0.24.0 + +- Add "metrics", "logs" to default features. With this, default feature list is + "trace", "metrics" and "logs". +- When "metrics" feature is enabled, `KeyValue` implements `PartialEq`, `Eq`, + `PartialOrder`, `Order`, `Hash`. This is meant to be used for metrics + aggregation purposes only. +- Removed `Unit` struct for specifying Instrument units. Unit is treated as an + opaque string. Migration: Replace `.with_unit(Unit::new("myunit"))` with + `.with_unit("myunit")`. + +- [1869](https://github.com/open-telemetry/opentelemetry-rust/pull/1869) Introduced the `LogRecord::set_target()` method in the log bridge API. +This method allows appenders to set the target/component emitting the logs. + +## v0.23.0 + +### Added + +- [#1640](https://github.com/open-telemetry/opentelemetry-rust/pull/1640) Add `PropagationError` +- [#1701](https://github.com/open-telemetry/opentelemetry-rust/pull/1701) `Gauge` no longer requires `otel-unstable` feature flag, as OpenTelemetry specification for `Gauge` instrument is stable. + +### Removed + +- Remove `urlencoding` crate dependency. [#1613](https://github.com/open-telemetry/opentelemetry-rust/pull/1613) +- Remove global providers for Logs [$1691](https://github.com/open-telemetry/opentelemetry-rust/pull/1691) + LoggerProviders are not meant for end users to get loggers from. It is only required for the log bridges. + Below global constructs for the logs are removed from API: + - opentelemetry::global::logger + - opentelemetry::global::set_logger_provider + - opentelemetry::global::shutdown_logger_provider + - opentelemetry::global::logger_provider + - opentelemetry::global::GlobalLoggerProvider + - opentelemetry::global::ObjectSafeLoggerProvider + For creating appenders using Logging bridge API, refer to the opentelemetry-tracing-appender [example](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-appender-tracing/examples/basic.rs) + +### Changed + +- **BREAKING** Moving LogRecord implementation to the SDK. [1702](https://github.com/open-telemetry/opentelemetry-rust/pull/1702). + - Relocated `LogRecord` struct to SDK. + - Introduced the `LogRecord` trait in the API for populating log records. This trait is implemented by the SDK. + This is the breaking change for the authors of Log Appenders. Refer to the [opentelemetry-appender-tracing](https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-appender-tracing) for more details. + +- Deprecate `versioned_logger()` in favor of `logger_builder()` [1567](https://github.com/open-telemetry/opentelemetry-rust/pull/1567). + +Before: + +```rust +let logger = provider.versioned_logger( + "my-logger-name", + Some(env!("CARGO_PKG_VERSION")), + Some("https://opentelemetry.io/schemas/1.0.0"), + Some(vec![KeyValue::new("key", "value")]), +); +``` + +After: + +```rust +let logger = provider + .logger_builder("my-logger-name") + .with_version(env!("CARGO_PKG_VERSION")) + .with_schema_url("https://opentelemetry.io/schemas/1.0.0") + .with_attributes(vec![KeyValue::new("key", "value")]) + .build(); +``` + +- Deprecate `versioned_tracer()` in favor of `tracer_builder()` [1567](https://github.com/open-telemetry/opentelemetry-rust/pull/1567). + +Before: + +```rust +let tracer = provider.versioned_tracer( + "my-tracer-name", + Some(env!("CARGO_PKG_VERSION")), + Some("https://opentelemetry.io/schemas/1.0.0"), + Some(vec![KeyValue::new("key", "value")]), +); +``` + +After: + +```rust +let tracer = provider + .tracer_builder("my-tracer-name") + .with_version(env!("CARGO_PKG_VERSION")) + .with_schema_url("https://opentelemetry.io/schemas/1.0.0") + .with_attributes(vec![KeyValue::new("key", "value")]) + .build(); +``` + +## v0.22.0 + +### Added + +- [#1410](https://github.com/open-telemetry/opentelemetry-rust/pull/1410) Add experimental synchronous gauge. This is behind the feature flag, and can be enabled by enabling the feature `otel_unstable` for opentelemetry crate. + +- [#1410](https://github.com/open-telemetry/opentelemetry-rust/pull/1410) Guidelines to add new unstable/experimental features. + +### Changed + +- Modified `AnyValue.Map` to be backed by `HashMap` instead of custom `OrderMap`, +which internally used `IndexMap`. There was no requirement to maintain the order +of entries, so moving from `IndexMap` to `HashMap` offers slight performance +gains, and avoids `IndexMap` dependency. This affects `body` and `attributes` of +`LogRecord`. +[#1353](https://github.com/open-telemetry/opentelemetry-rust/pull/1353) +- Add `TextMapCompositePropagator` [#1373](https://github.com/open-telemetry/opentelemetry-rust/pull/1373) +- Turned off events for `NoopLogger` to save on operations + [#1455](https://github.com/open-telemetry/opentelemetry-rust/pull/1455) + +### Removed + +- Removed `OrderMap` type as there was no requirement to use this over regular +`HashMap`. +[#1353](https://github.com/open-telemetry/opentelemetry-rust/pull/1353) +- Remove API for Creating Histograms with signed integers. [#1371](https://github.com/open-telemetry/opentelemetry-rust/pull/1371) +- Remove `global::shutdown_meter_provider`, use `SdkMeterProvider::shutdown` + directly instead [#1412](https://github.com/open-telemetry/opentelemetry-rust/pull/1412). + +## [v0.21.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.20.0...v0.21.0) + +This release should been seen as 1.0-rc4 following 1.0-rc3 in v0.20.0. Refer to CHANGELOG.md in individual creates for details on changes made in different creates. + +### Changed + +- Bump MSRV to 1.65 [#1318](https://github.com/open-telemetry/opentelemetry-rust/pull/1318) +- Bump MSRV to 1.64 [#1203](https://github.com/open-telemetry/opentelemetry-rust/pull/1203) +- `opentelemetry` crate now only carries the API types [#1186](https://github.com/open-telemetry/opentelemetry-rust/issues/1186). Use the `opentelemetry_sdk` crate for the SDK types. +- `trace::noop::NoopSpan` no longer implements `Default` and instead exposes + a `const DEFAULT` value. [#1270](https://github.com/open-telemetry/opentelemetry-rust/pull/1270) +- Updated crate documentation and examples. + [#1256](https://github.com/open-telemetry/opentelemetry-rust/issues/1256) +- **Breaking** `SpanBuilder` attributes changed from `OrderMap` to + `Vec` and `with_attributes_map` method is removed from `SpanBuilder`. + This implies that OpenTelemetry API will no longer perform + de-dup of attribute Keys. + [#1293](https://github.com/open-telemetry/opentelemetry-rust/issues/1293). + Please share [feedback + here](https://github.com/open-telemetry/opentelemetry-rust/issues/1300), if + you are affected. + +## [v0.20.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.19.0...v0.20.0) +This release should been seen as 1.0-rc3 following 1.0-rc2 in v0.19.0. Refer to CHANGELOG.md in individual creates for details on changes made in different creates. + +### Added + +- Add `new` method to `BoxedTracer` #1009 +- Add js-sys as dependency for api crate when building wasm targets #1078 +- Create tracer using a shared instrumentation library #1129 +- Add `Context::map_current` #1140 +- Add unit/doc tests for metrics #1213 +- Add `opentelemetry::sdk::logs::config()` for parity with `opentelemetry::sdk::trace::config()` (#1197) + +### Changed + +- `OtelString::Owned` carries `Box` instead of `String` #1096 + +### Removed + +- Drop include_trace_context parameter from Logs API/SDK. [#1133](https://github.com/open-telemetry/opentelemetry-rust/issues/1133) +- Synchronous instruments no longer accepts `Context` while reporting + measurements. [#1076](https://github.com/open-telemetry/opentelemetry-rust/pull/1076). +- Fallible conversions from `futures-channel` error types to `LogError` and + `TraceError` removed. + [#1201](https://github.com/open-telemetry/opentelemetry-rust/issues/1201) + +### Fixed + +- Fix `SpanRef::set_attributes` mutability requirement. [#1038](https://github.com/open-telemetry/opentelemetry-rust/pull/1038) +- Move OrderMap module to root of otel-api crate. [#1061](https://github.com/open-telemetry/opentelemetry-rust/pull/1061) +- Use the browser-only js-sys workaround only when actually targeting a browser #1008 + +## [v0.19.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.18.0...v0.19.0) +This release should been seen as 1.0-rc2 following 1.0-rc1 in v0.18.0. Refer to CHANGELOG.md in individual creates for details on changes made in different creates. + +### Added +- Add `WithContext` to public api [#893](https://github.com/open-telemetry/opentelemetry-rust/pull/893). +- Add support for instrumentation scope attributes [#1021](https://github.com/open-telemetry/opentelemetry-rust/pull/1021). + +### Changed +- Implement `Display` on `Baggage` [#921](https://github.com/open-telemetry/opentelemetry-rust/pull/921). +- Bump MSRV to 1.57 [#953](https://github.com/open-telemetry/opentelemetry-rust/pull/953). +- Update dependencies and bump MSRV to 1.60 [#969](https://github.com/open-telemetry/opentelemetry-rust/pull/969). + +## [v0.18.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.17.0...v0.18.0) + +This release is the first beta release of the `trace` API and SDK. If no other +breaking changes are necessary, the next release will be 1.0. The `metrics` API +and SDK are still unstable. + +### Added + +- Pull sampling probability from `OTEL_TRACES_SAMPLER_ARG` in default sdk config #737 +- Add `schema_url` to `Tracer` #743 +- Add `schema_url` to `Resource` #775 +- Add `Span::set_attributes` #638 +- Support concurrent exports #781 +- Add jaeger remote sampler #797 +- Allow Custom Samplers #833 +- Add `SpanExporter::force_flush` and default implementation #845 + +### Changed + +- Deprecate metrics `ValueRecorder` in favor of `Histogram` #728 +- Move `IdGenerator` to SDK, rename to `RandomIdGenerator` #742 +- `meter_with_version` accepts optional parameter for `version` and `schema_url` #752 +- Unify `Event` and `Link` access patterns #757 +- move `SpanKind` display format impl to jaeger crate #758 +- make `TraceStateError` private #755 +- rename `Span::record_exception` to `Span::record_error` #756 +- Replace `StatusCode` and `message` with `Status` #760 +- Move `TracerProvider::force_flush` to SDK #658 +- Switch to static resource references #790 +- Allow O(1) get operations for `SpanBuilder::attributes` [breaking] #799 +- Allow ref counted keys and values #821 +- Bump MSRV from 1.49 to 1.55 #811 +- bump MSRV to 1.56 #866 +- Update metrics API and SDK for latest spec #819 +- Switch to `pin-project-lite` #830 + +### Fixed + +- Update dashmap to avoid soundness hole #818 +- Perform sampling as explained in the specification #839 +- Remove internal message queue between exporter and exporting tasks #848 +- Fix span processor exporting unsampled spans #871 + +### Removed + +- Remove `serialize` feature #738 +- Remove `StatusCode::as_str` #741 +- Remove `Tracer::with_span` #746 + +## [v0.17.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.16.0...v0.17.0) + +### Changed + +- Implement `Serialize` & `Deserialize` for `Sampler`, `SpanLimits` #622, #626 +- Allow `&'static str` and `string` in span methods #654 +- Allow `String` data in instrumentation library. #670 +- Remove `std::fmt::Debug` and `'static` requirements from `TracerProvider`, + `Tracer`, and `Span` #664 +- Remove unused `Tracer::invalid` method #683 +- Split `TracerProvider::tracer` and `TracerProvider::versioned_tracer` methods #682 +- Reduce dependency on `futures` crate #684 +- Switch to parent context references #687 +- Spec-compliant trace and span ids #689 +- Optimize span creation internals #693 +- Add instrumentation library to `ShouldSample` parameters #695 + +### Fixed + +- Fix default resource detection for tracer provider #641 +- Detect `service.name` from `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES` #662 +- Fix `TraceState::valid_key` crashes #665 + +## [v0.16.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.15.0...v0.16.0) + +### Changed + +- Add default resource in `TracerProvider` #571 +- Rename `get_tracer` to `tracer` #586 +- Extract `trace::noop` module and update docs #587 +- Add `Hash` impl for span context and allow spans to clone and export current state #592 +- Enforce span status code's order #593 +- Make `SpanRef` public #600 +- Make `SpanProcessor::on_start` take a mutable span #601 +- Renamed `label` to `attribute` to align with otel specification #609 + +### Performance + +- Small performance boost for `Resource::get` #579 + +## [v0.15.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.14.0...v0.15.0) + +### Added + +- More resource detectors #573 + +### Changed + +- Expose the Error type to allow users to set custom error handlers #551 +- Allow users to use different channels based on runtime in batch span processor #560 +- Move `Unit` into `metrics` module #564 +- Update trace flags to match spec #565 + +### Fixed + +- Fix debug loop, add notes for `#[tokio::test]` #552 +- `TraceState` cannot insert new key-value pairs #567 + +## [v0.14.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.13.0...v0.14.0) + +## Added + +- Adding a dynamic dispatch to Aggregator Selector #497 +- Add `global::force_flush_tracer_provider` #512 +- Add config `max_attributes_per_event` and `max_attributes_per_link` #521 +- Add dropped attribute counts to events and links #529 + +## Changed + +- Remove unnecessary clone in `Key` type #491 +- Remove `#[must_use]` from `set_tracer_provider` #501 +- Rename remaining usage of `default_sampler` to `sampler` #509 +- Use current span for SDK-less context propagation #510 +- Always export span batch when limit reached #519 +- Rename message events to events #530 +- Update resource merge behavior #537 +- Ignore links with invalid context #538 + +## Removed + +- Remove remote span context #508 +- Remove metrics quantiles #525 + +# Fixed + +- Allow users to use custom export kind selector #526 + +## Performance + +- Improve simple span processor performance #502 +- Local span perf improvements #505 +- Reduce string allocations where possible #506 + +## [v0.13.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.12.0...v0.13.0) + +Upgrade note: exporter pipelines do not return an uninstall guard as of #444, +use `opentelemetry::global::shutdown_tracer_provider` explicitly instead. + +## Changed + +- Pull configurations from environment variables by default when creating BatchSpanProcessor #445 +- Convert doc links to intra-doc #466 +- Switch to Cow for event names #471 +- Use API to configure async runtime instead of features #481 +- Rename trace config with_default_sampler to with_sampler #482 + +## Removed + +- Removed tracer provider guard #444 +- Removed `from_env` and use environment variables to initialize the configurations by default #459 + +## [v0.12.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.11.2...v0.12.0) + +## Added + +- Instrumentation library support #402 +- Batch observer support #429 +- `with_unit` methods in metrics #431 +- Clone trait for noop tracer/tracer provider/span #479 +- Abstracted traits for different runtimes #480 + +## Changed + +- Dependencies updates #410 +- Add `Send`, `Sync` to `AsyncInstrument` in metrics #422 +- Add `Send`, `Sync` to `InstrumentCore` in metrics #423 +- Replace regex with custom logic #411 +- Update tokio to v1 #421 + +## Removed + +- Moved `http` dependencies into a new opentelemetry-http crate #415 +- Remove `tonic` dependency #414 + +## [v0.11.2](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.11.1...v0.11.2) + +# Fixed + +- Fix possible deadlock when dropping metric instruments #407 + +## [v0.11.1](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.11.0...v0.11.1) + +# Fixed + +- Fix remote implicit builder context sampling #405 + +## [v0.11.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.10.0...v0.11.0) + +## Added + +- Add `force_flush` method to span processors #358 +- Add timeout for `force_flush` and `shutdown` #362 + +## Changed + +- Implement Display trait for Key and Value types #353 +- Remove Option from Array values #359 +- Update `ShouldSample`'s parent parameter to be `Context` #368 +- Consolidate error types in `trace` module into `TraceError` #371 +- Add `#[must_use]` to uninstall structs #372 +- Move 3rd party propagators and merge exporter into `sdk::export` #375 +- Add instrumentation version to instrument config #392 +- Use instrumentation library in metrics #393 +- `start_from_context` renamed to `start_with_context` #399 +- Removed `build_with_context` as full context is now stored in builder #399 +- SpanBuilder's `with_parent` renamed to `with_parent_context` #399 + +# Fixed + +- Fix parent based sampling in tracer #354 +- StatusCode enum value ordering #377 +- Counter adding the delta from last collection #395 +- `HistogramAggregator` returning sum vs count #398 + +## [v0.10.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.9.1...v0.10.0) + +## Added + +- Add support for baggage metadata #287 + +## Changed + +- Remove `api` prefix from modules #305 +- Move `mark_as_active_span` and `get_active_span` functions into trace module #310 +- Revert renaming of `SpanContext` to `SpanReference` #299 +- Default trace propagator is now a no-op #329 +- Return references to span contexts instead of clones #325 +- Update exporter errors to be `Box` #284 +- Rename `GenericProvider` to `GenericTracerProvider` #313 +- Reduce `SpanStatus` enum to `Ok`, `Error`, and `Unset` variants #315 +- update B3 propagator to more closely match spec #319 +- Export missing pub global trace types #313 +- Ensure kv array values are homogeneous #333 +- Implement `Display` trait for `Key` and `Value` types #353 +- Move `SpanProcessor` trait into `sdk` module #334 +- Ensure `is_recording` is `false` and span is no-op after `end` #341 +- Move binary propagator and base64 format to contrib #343 +- Ensure metrics noop types go through constructors #345 +- Change `ExportResult` to use `std::result::Result` #347 +- Change `SpanExporter::export` to take `&mut self` instead of `&self` #350 +- Add MSRV 1.42.0 #296 + +## Fixed + +- Fix parent based sampling #354 + +## Removed + +- Remove support for `u64` and `bytes` kv values #323 +- Remove kv value conversion from `&str` #332 + +## [v0.9.1](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.9.0...v0.9.1) + +## Added + +- Allow metric instruments to be cloned #280 + +### Fixed + +- Fix single threaded runtime tokio feature bug #278 + +## [v0.9.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.8.0...v0.9.0) + +## Added + +- Add resource detector #174 +- Add `fields` method to TextMapFormat #178 +- Add support for `tracestate` in `TraceContextPropagator` #191 +- Propagate valid span context in noop tracer #197 +- Add end_with_timestamp method for trace span #199 +- Add ID methods for hex and byte array formatting #200 +- Add AWS X-Ray ID Generator #201 +- AWS X-Ray Trace Context Propagator #202 +- Add instrumentation library information to spans #207 +- Add keys method to extractors #209 +- Add `TraceState` to `SpanContext` #217 +- Add `from_env` config option for `BatchSpanProcessor` #228 +- Add pipeline uninstall mechanism to shut down trace pipelines #229 + +### Changed + +- Re-write metrics sdk to be spec compliant #179 +- Rename `Sampler::Probability` to `Sampler::TraceIdRatioBased` #188 +- Rename `HTTPTextPropagator` to `TextMapPropagator` #192 +- Ensure extractors are case insensitive #193 +- Rename `Provider` to `TracerProvider` #206 +- Rename `CorrelationContext` into `Baggage` #208 +- Pipeline builder for stdout trace exporter #224 +- Switch to async exporters #232 +- Allow `ShouldSample` implementation to modify trace state #237 +- Ensure context guard is `!Send` #239 +- Ensure trace noop structs use `new` constructor #240 +- Switch to w3c `baggage` header #246 +- Move trace module imports from `api` to `api::trace` #255 +- Update `tonic` feature to use version `0.3.x` #258 +- Update exporters to receive owned span data #264 +- Move propagators to `sdk::propagation` #266 +- Rename SpanContext to SpanReference #270 +- Rename `SamplingDecision`'s `NotRecord`, `Record` and `RecordAndSampled` to + `Drop` `RecordOnly` and `RecordAndSample` #247 + +## [v0.8.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.7.0...v0.8.0) + +## Added + +- Add custom span processors to `Provider::Builder` #166 + +### Changed + +- Separate `Carrier` into `Injector` and `Extractor` #164 +- Change the default sampler to be `ParentOrElse(AlwaysOn)` #163 +- Move the `Sampler` interface to the SDK #169 + +## [v0.7.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.6.0...v0.7.0) + +### Added + +- New `ParentOrElse` sampler for fallback logic if parent is not sampled. #128 +- Attributes can now have array values #146 +- Added `record_exception` and `record_exception_with_stacktrace` methods to `Span` #152 + +### Changed + +- Update sampler types #128 + - `Always` is now `AlwaysOn`. `Never` is now `AlwaysOff`. `Probability` now ignores parent + sampled state. +- `base64` and `binary_propagator` have been moved to `experimental` module. #134 +- `Correlation-Context` header has been updated to `otcorrelations` #145 +- `B3Propagator` has been updated to more closely follow the spec #148 + +## [v0.6.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.5.0...v0.6.0) + +### Added + +- Add `http` and `tonic` features to impl `Carrier` for common types. + +### Changed + +- Removed `span_id` from sampling parameters when implementing custom samplers. + +### Fixed + +- Make `Context` `Send + Sync` in #127 + +## [v0.5.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.4.0...v0.5.0) + +### Added + +- Derive `Clone` for `B3Propagator`, `SamplingResult`, and `SpanBuilder` +- Ability to configure the span id / trace id generator +- impl `From` for common `Key` and `Value` types +- Add global `tracer` method +- Add `Resource` API +- Add `Context` API +- Add `Correlations` API +- Add `HttpTextCompositePropagator` for composing `HttpTextPropagator`s +- Add `GlobalPropagator` for globally configuring a propagator +- Add `TraceContextExt` to provide methods for working with trace data in a context +- Expose `EvictedQueue` constructor + +### Changed + +- Ensure that impls of `Span` are `Send` and `Sync` when used in `global` +- Changed `Key` and `Value` method signatures to remove `Cow` references +- Tracer's `start` now uses the implicit current context instead of an explicit span context. + `start_with_context` may be used to specify a context if desired. +- `with_span` now accepts a span for naming consistency and managing the active state of a more + complex span (likely produced by a builder), and the previous functionality that accepts a + `&str` has been renamed to `in_span`, both of which now yield a context to the provided closure. +- Tracer's `get_active_span` now accepts a closure +- The `Instrument` trait has been renamed to `FutureExt` to avoid clashing with metric instruments, + and instead accepts contexts via `with_context`. +- Span's `get_context` method has been renamed to `span_context` to avoid ambiguity. +- `HttpTextPropagators` inject the current context instead of an explicit span context. The context + can be specified with `inject_context`. +- `SpanData`'s `context` has been renamed to `span_context` + +### Fixed + +- Update the probability sampler to match the spec +- Rename `Traceparent` header to `traceparent` + +### Removed + +- `TracerGenerics` methods have been folded in to the `Tracer` trait so it is longer needed +- Tracer's `mark_span_as_inactive` has been removed +- Exporters no longer require an `as_any` method +- Span's `mark_as_active`, `mark_as_inactive`, and `as_any` have been removed + +## [v0.4.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.3.0...v0.4.0) + +### Added + +- New async batch span processor +- New stdout exporter +- Add `trace_id` to `SpanBuilder` + +### Changed + +- Add `attributes` to `Event`s. +- Update `Span`'s `add_event` and `add_event_with_timestamp` to accept attributes. +- Record log fields in jaeger exporter +- Properly export span kind in jaeger exporter +- Add support for `Link`s +- Add `status_message` to `Span` and `SpanData` +- Rename `SpanStatus` to `StatusCode` +- Update `EvictedQueue` internals from LIFO to FIFO +- Switch span attributes to `EvictedHashMap` + +### Fixed + +- Call `shutdown` correctly when span processors and exporters are dropped + +## [v0.3.0](https://github.com/open-telemetry/opentelemetry-rust/compare/v0.2.0...v0.3.0) + +### Added + +- New Base64 propagator +- New SpanBuilder api +- Zipkin Exporter crate + +### Changed + +- Switch to `SpanId` and `TraceId` from `u64` and `u128` +- Remove `&mut self` requirements for `Span` API + +### Fixed + +- circular Tracer debug impl + +## [v0.2.0](https://github.com/open-telemetry/opentelemetry-rust/compare/b5918251cc07f9f6957434ccddc35306f68bd791..v0.2.0) + +### Added + +- Make trace and metrics features optional +- ExportResult as specified in the specification +- Add Futures compatibility API +- Added serde serialize support to SpanData +- Separate OpenTelemetry Jaeger crate + +### Changed + +- Rename HttpTraceContextPropagator to TraceContextPropagator +- Rename HttpB3Propagator to B3Propagator +- Switch to Apache 2 license +- Resolve agent addresses to allow non-static IP +- Remove tracer name prefix from span name + +### Removed + +- Remove add_link from spans + +## [v0.1.5](https://github.com/jtescher/opentelemetry-rust/compare/v0.1.4...v0.1.5) + +### Added + +- trace-context propagator + +### Changed + +- Prometheus API cleanup + +## [v0.1.4](https://github.com/jtescher/opentelemetry-rust/compare/v0.1.3...v0.1.4) + +### Added + +- Parent option for default sampler + +### Fixed + +- SDK tracer default span id + +## [v0.1.3](https://github.com/jtescher/opentelemetry-rust/compare/v0.1.2...v0.1.3) + +### Changed + +- Ensure spans are always send and sync +- Allow static lifetimes for span names +- Improve KeyValue ergonomics + +## [v0.1.2](https://github.com/jtescher/opentelemetry-rust/compare/v0.1.1...v0.1.2) + +### Added + +- Implement global provider + +## [v0.1.1](https://github.com/jtescher/opentelemetry-rust/compare/v0.1.0...v0.1.1) + +### Added + +- Documentation and API cleanup +- Tracking of active spans via thread local span stack + +## [v0.1.0](https://github.com/jtescher/opentelemetry-rust/commit/ea368ea965aa035f46728d75e1be3b096b6cd6ec) + +Initial debug alpha diff --git a/vendor/opentelemetry-rust/opentelemetry/Cargo.toml b/vendor/opentelemetry-rust/opentelemetry/Cargo.toml new file mode 100644 index 00000000..2a3ea071 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/Cargo.toml @@ -0,0 +1,172 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.75.0" +name = "opentelemetry" +version = "0.33.0" +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "OpenTelemetry API for Rust" +homepage = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry" +readme = "README.md" +keywords = [ + "opentelemetry", + "logging", + "tracing", + "metrics", +] +categories = [ + "development-tools::debugging", + "development-tools::profiling", + "api-bindings", + "asynchronous", +] +license = "Apache-2.0" +repository = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[features] +default = [ + "trace", + "metrics", + "logs", + "internal-logs", + "futures", +] +experimental_context_observer = [] +experimental_metrics_bound_instruments = ["metrics"] +futures = [ + "futures-core", + "futures-sink", + "pin-project-lite", +] +internal-logs = ["tracing"] +logs = [] +metrics = [] +testing = ["trace"] +trace = [ + "futures", + "thiserror", +] + +[lib] +name = "opentelemetry" +path = "src/lib.rs" +bench = false + +[[bench]] +name = "anyvalue" +path = "benches/anyvalue.rs" +harness = false + +[[bench]] +name = "attributes" +path = "benches/attributes.rs" +harness = false + +[[bench]] +name = "baggage" +path = "benches/baggage.rs" +harness = false + +[[bench]] +name = "context_attach" +path = "benches/context_attach.rs" +harness = false +required-features = ["tracing"] + +[[bench]] +name = "context_suppression" +path = "benches/context_suppression.rs" +harness = false + +[[bench]] +name = "logs" +path = "benches/logs.rs" +harness = false +required-features = ["logs"] + +[[bench]] +name = "metrics" +path = "benches/metrics.rs" +harness = false +required-features = ["metrics"] + +[[bench]] +name = "trace" +path = "benches/trace.rs" +harness = false +required-features = ["trace"] + +[dependencies.futures-core] +version = "0.3" +optional = true + +[dependencies.futures-sink] +version = "0.3" +optional = true + +[dependencies.pin-project-lite] +version = "0.2" +optional = true + +[dependencies.thiserror] +version = "2" +optional = true +default-features = false + +[dependencies.tracing] +version = ">=0.1.40" +optional = true +default-features = false + +[dev-dependencies.criterion] +version = "0.5" + +[dev-dependencies.rand] +version = "0.9" +features = [ + "os_rng", + "thread_rng", +] +default-features = false + +[dev-dependencies.tokio] +version = "1.0" +features = ["full"] + +[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies.js-sys] +version = "0.3.63" + +[lints.clippy.all] +level = "warn" +priority = 1 + +[lints.rust] +edition_2024_expr_fragment_specifier = "allow" +if_let_rescope = "allow" +tail_expr_drop_order = "allow" + +[lints.rust.rust_2024_compatibility] +level = "warn" +priority = -1 diff --git a/vendor/opentelemetry-rust/opentelemetry/README.md b/vendor/opentelemetry-rust/opentelemetry/README.md new file mode 100644 index 00000000..878e2213 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/README.md @@ -0,0 +1,144 @@ +# OpenTelemetry Rust API + +![OpenTelemetry — An observability framework for cloud-native software.][splash] + +[splash]: https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo-text.png + +This crate contains the [OpenTelemetry](https://opentelemetry.io/) API for Rust. + +[![Crates.io: opentelemetry](https://img.shields.io/crates/v/opentelemetry.svg)](https://crates.io/crates/opentelemetry) +[![Documentation](https://docs.rs/opentelemetry/badge.svg)](https://docs.rs/opentelemetry) +[![LICENSE](https://img.shields.io/crates/l/opentelemetry)](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry/LICENSE) +[![GitHub Actions CI](https://github.com/open-telemetry/opentelemetry-rust/workflows/CI/badge.svg)](https://github.com/open-telemetry/opentelemetry-rust/actions?query=workflow%3ACI+branch%3Amain) +[![codecov](https://codecov.io/gh/open-telemetry/opentelemetry-rust/branch/main/graph/badge.svg)](https://codecov.io/gh/open-telemetry/opentelemetry-rust) +[![Slack](https://img.shields.io/badge/slack-@cncf/otel/rust-brightgreen.svg?logo=slack)](https://cloud-native.slack.com/archives/C03GDP0H023) + +## Overview + +OpenTelemetry is an Observability framework and toolkit designed to create and +manage telemetry data such as traces, metrics, and logs. OpenTelemetry is +vendor- and tool-agnostic, meaning that it can be used with a broad variety of +Observability backends, including open source tools like [Jaeger] and +[Prometheus], as well as commercial offerings. + +OpenTelemetry is *not* an observability backend like Jaeger, Prometheus, or other +commercial vendors. OpenTelemetry is focused on the generation, collection, +management, and export of telemetry. A major goal of OpenTelemetry is that you +can easily instrument your applications or systems, no matter their language, +infrastructure, or runtime environment. Crucially, the storage and visualization +of telemetry is intentionally left to other tools. + +*[Supported Rust Versions](#supported-rust-versions)* + +[Prometheus]: https://prometheus.io +[Jaeger]: https://www.jaegertracing.io + +### What does this crate contain? + +This crate is basic foundation for integrating OpenTelemetry into libraries and +applications, encompassing several aspects of OpenTelemetry, such as context +management and propagation, baggage, logging, tracing, and metrics. It follows +the [OpenTelemetry +specification](https://github.com/open-telemetry/opentelemetry-specification). +Here's a breakdown of its components: + +- **[Context + API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/README.md):** + Provides a way to manage and propagate context, which is essential for keeping + track of trace execution across asynchronous tasks. +- **[Propagators + API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md):** + Defines how context can be shared across process boundaries, ensuring + continuity across microservices or distributed systems. +- **[Baggage + API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/baggage/api.md):** + Allows for the attachment of metadata (baggage) to telemetry, which can be + used for sharing application-specific information across service boundaries. +- **[Logs Bridge + API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/api.md):** + Allows to bridge existing logging mechanisms with OpenTelemetry logging. This + is **NOT** meant for end users to call, instead it is meant to enable writing + bridges/appenders for existing logging mechanisms such as + [log](https://crates.io/crates/log) or + [tracing](https://crates.io/crates/tracing). +- **[Tracing + API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md):** + Offers a set of primitives to produce distributed traces to understand the + flow of a request across system boundaries. +- **[Metrics + API](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md):** + Offers a set of primitives to produce measurements of operational metrics like + latency, throughput, or error rates. + +This crate serves as a facade or no-op implementation, meaning it defines the +traits for instrumentation but does not itself implement the processing or +exporting of telemetry data. This separation of concerns allows library authors +to depend on the API crate without tying themselves to a specific +implementation. + +Actual implementation and the heavy lifting of telemetry data collection, +processing, and exporting are delegated to the +[opentelemetry-sdk](https://crates.io/crates/opentelemetry-sdk) crate and +various exporter crates such as +[opentelemetry-otlp](https://crates.io/crates/opentelemetry-otlp). This +architecture ensures that the final application can light up the instrumentation +by integrating an SDK implementation. + +Library authors are recommended to depend on this crate *only*. This approach is +also aligned with the design philosophy of existing telemetry solutions in the +Rust ecosystem, like `tracing` or `log`, where these crates only offer a facade +and the actual functionality is enabled through additional crates. + +### Related crates + +Unless you are a library author, you will almost always need to use additional +crates along with this. Given this crate has no-op implementation only, an +OpenTelemetry SDK is always required. +[opentelemetry-sdk](https://crates.io/crates/opentelemetry-sdk) is the official +SDK implemented by OpenTelemetry itself, though it is possible to use a +different sdk. + +Additionally one or more exporters are also required to export telemetry to a +destination. OpenTelemetry provides the following exporters: + +- **[opentelemetry-stdout](https://crates.io/crates/opentelemetry-stdout):** + Prints telemetry to stdout, primarily used for learning/debugging purposes. +- **[opentelemetry-otlp](https://crates.io/crates/opentelemetry-otlp):** Exports + telemetry (logs, metrics and traces) in the [OTLP + format](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/protocol) + to an endpoint accepting OTLP. This could be the [OTel + Collector](https://github.com/open-telemetry/opentelemetry-collector), + telemetry backends like [Jaeger](https://www.jaegertracing.io/), + [Prometheus](https://prometheus.io/docs/prometheus/latest/feature_flags/#otlp-receiver) + or [vendor specific endpoints](https://opentelemetry.io/ecosystem/vendors/). +- **[opentelemetry-prometheus](https://crates.io/crates/opentelemetry-prometheus):** + Exports telemetry (metrics only) to Prometheus following [OpenTelemetry to + Prometheus + specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk_exporters/prometheus.md) + +OpenTelemetry Rust also has a [contrib +repo](https://github.com/open-telemetry/opentelemetry-rust-contrib), where +additional exporters could be found. Check [OpenTelemetry +Registry](https://opentelemetry.io/ecosystem/registry/?language=rust) for +additional exporters and other related components as well. + +## Getting started + +See [docs](https://docs.rs/opentelemetry). + +## Release Notes + +You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry/CHANGELOG.md). + +## Supported Rust Versions + +OpenTelemetry is built against the latest stable release. The minimum supported +version is 1.75.0. The current OpenTelemetry version is NOT guaranteed to build +on Rust versions earlier than the minimum supported version. + +The current stable Rust compiler and the three most recent minor versions +before it will always be supported. For example, if the current stable compiler +version is 1.49, the minimum supported version will not be increased past 1.46, +three minor versions prior. Increasing the minimum supported compiler version +is not considered a semver breaking change as long as doing so complies with +this policy. diff --git a/vendor/opentelemetry-rust/opentelemetry/allowed-external-types.toml b/vendor/opentelemetry-rust/opentelemetry/allowed-external-types.toml new file mode 100644 index 00000000..90d58d7e --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/allowed-external-types.toml @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# This is used with cargo-check-external-types to reduce the surface area of downstream crates from +# the public API. Ideally this can have a few exceptions as possible. +allowed_external_types = [ + "equivalent::Equivalent", + "futures_sink::Sink", # TODO: This is a pre-1.0 crate, we can't easily stabilize with this in the public API + "futures_core::stream::Stream", # TODO: This is a pre-1.0 crate, we can't easily stabilize with this in the public API +] diff --git a/vendor/opentelemetry-rust/opentelemetry/benches/anyvalue.rs b/vendor/opentelemetry-rust/opentelemetry/benches/anyvalue.rs new file mode 100644 index 00000000..96a53c02 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/benches/anyvalue.rs @@ -0,0 +1,51 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use opentelemetry::{logs::AnyValue, Value}; +use std::hint::black_box; + +// Run this benchmark with: +// cargo bench --bench anyvalue +// Results: +// CreateOTelValueString 1-2 ns +// CreateOTelAnyValueString 15 ns +// CreateOTelValueInt 1-2 ns +// CreateOTelAnyValueInt 15 ns + +fn criterion_benchmark(c: &mut Criterion) { + attributes_creation(c); +} + +fn attributes_creation(c: &mut Criterion) { + c.bench_function("CreateOTelValueString", |b| { + b.iter(|| { + let _v = black_box(Value::String("value1".into())); + }); + }); + + c.bench_function("CreateOTelAnyValueString", |b| { + b.iter(|| { + let _v = black_box(AnyValue::String("value1".into())); + }); + }); + + c.bench_function("CreateOTelValueInt", |b| { + b.iter(|| { + let _v = black_box(Value::I64(123)); + }); + }); + + c.bench_function("CreateOTelAnyValueInt", |b| { + b.iter(|| { + let _v = black_box(AnyValue::Int(123)); + }); + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} + +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry/benches/attributes.rs b/vendor/opentelemetry-rust/opentelemetry/benches/attributes.rs new file mode 100644 index 00000000..8d929e08 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/benches/attributes.rs @@ -0,0 +1,128 @@ +/* OS: Ubuntu 22.04.4 LTS (5.15.153.1-microsoft-standard-WSL2) +Hardware: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz, 16vCPUs, +RAM: 64.0 GB +| Test | Average time| +|--------------------------------------------------|-------------| +| CreateOTelKey_Static | 1.2 ns | +| CreateOTelKey_Owned | 12.6 ns | +| CreateOTelKey_Arc | 23.35 ns | +| CreateOTelKeyValue | 3.24 ns | +| CreateTupleKeyValue | 671 ps | +| CreateOtelKeyValueArray | 18.4 ns | +| CreateOtelKeyValueArrayWithMixedValueTypes | 18.1 ns | +| CreateOtelKeyValueArrayWithNonStaticValues | 90.1 ns | +| CreateTupleKeyValueArray | 2.73 ns | + +OS: macOS 15 +Hardware: Apple M4 Pro, 14 cores, RAM: 24 GB +| Test | Average time| +|--------------------------------------------------|-------------| +| CreateOTelKey_Static | 0.28 ns | +| CreateOTelKey_Owned | 19.16 ns | +| CreateOTelKey_Arc | 21.76 ns | +| CreateOTelKeyValue | 1.30 ns | +| CreateTupleKeyValue | 0.26 ns | +| CreateOtelKeyValueArray | 7.41 ns | +| CreateOtelKeyValueArrayWithMixedValueTypes | 5.50 ns | +| CreateOtelKeyValueArrayWithNonStaticValues | 84.94 ns | +| CreateTupleKeyValueArray | 1.46 ns | +*/ + +use criterion::{criterion_group, criterion_main, Criterion}; +use opentelemetry::{Key, KeyValue}; +use std::hint::black_box; +use std::sync::Arc; + +// Run this benchmark with: +// cargo bench --bench attributes + +fn criterion_benchmark(c: &mut Criterion) { + attributes_creation(c); +} + +fn attributes_creation(c: &mut Criterion) { + c.bench_function("CreateOTelKey_Static", |b| { + b.iter(|| { + let _v1 = black_box(Key::new("attribute1")); + }); + }); + + c.bench_function("CreateOTelKey_Owned", |b| { + b.iter(|| { + let _v1 = black_box(Key::new(String::from("attribute1"))); + }); + }); + + c.bench_function("CreateOTelKey_Arc", |b| { + b.iter(|| { + let _v1 = black_box(Key::new(Arc::from("attribute1"))); + }); + }); + + c.bench_function("CreateOTelKeyValue", |b| { + b.iter(|| { + let _v1 = black_box(KeyValue::new("attribute1", "value1")); + }); + }); + + c.bench_function("CreateTupleKeyValue", |b| { + b.iter(|| { + let _v1 = black_box(("attribute1", "value1")); + }); + }); + + c.bench_function("CreateOtelKeyValueArray", |b| { + b.iter(|| { + let _v1 = black_box([ + KeyValue::new("attribute1", "value1"), + KeyValue::new("attribute2", "value2"), + KeyValue::new("attribute3", "value3"), + KeyValue::new("attribute4", "value4"), + ]); + }); + }); + + c.bench_function("CreateOtelKeyValueArrayWithMixedValueTypes", |b| { + b.iter(|| { + let _v1 = black_box([ + KeyValue::new("attribute1", true), + KeyValue::new("attribute2", 100), + KeyValue::new("attribute3", 100.5), + KeyValue::new("attribute4", "value"), + ]); + }); + }); + + let value = "value1value2value3value4".to_owned(); + + c.bench_function("CreateOtelKeyValueArrayWithNonStaticValues", |b| { + b.iter(|| { + let _v1 = black_box([ + KeyValue::new("attribute1", value[0..6].to_owned()), // value[0..6] = "value1" + KeyValue::new("attribute2", value[6..12].to_owned()), // value[6..12] = "value2" + KeyValue::new("attribute3", value[12..18].to_owned()), // value[12..18] = "value3" + KeyValue::new("attribute4", value[18..24].to_owned()), // value[18..24] = "value4" + ]); + }); + }); + + c.bench_function("CreateTupleKeyValueArray", |b| { + b.iter(|| { + let _v1 = black_box([ + ("attribute1", "value1"), + ("attribute2", "value2"), + ("attribute3", "value3"), + ("attribute4", "value4"), + ]); + }); + }); +} + +criterion_group! { + name = benches; + config = Criterion::default().warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} + +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry/benches/baggage.rs b/vendor/opentelemetry-rust/opentelemetry/benches/baggage.rs new file mode 100644 index 00000000..72fa2f1d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/benches/baggage.rs @@ -0,0 +1,104 @@ +use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; +use opentelemetry::baggage::Baggage; +use rand::distr::{Alphanumeric, SampleString}; +use rand::Rng; + +const MAX_KEY_VALUE_PAIRS: usize = 64; + +// Run this benchmark with: +// cargo bench --bench baggage +// Adding results in comments for a quick reference. +// Apple M4 Pro +// Total Number of Cores: 14 (10 performance and 4 efficiency) +// Results: +// set_baggage_static_key_value 14 ns +// set_baggage_static_key 26 ns +// set_baggage_dynamic 54 ns +// set_baggage_dynamic_with_metadata 75 ns + +fn criterion_benchmark(c: &mut Criterion) { + set_baggage_static_key_value(c); + set_baggage_static_key(c); + set_baggage_dynamic(c); + set_baggage_dynamic_with_metadata(c); +} + +fn set_baggage_static_key_value(c: &mut Criterion) { + let mut baggage = Baggage::new(); + + c.bench_function("set_baggage_static_key_value", move |b| { + b.iter(|| { + baggage.insert("key", "value"); + }) + }); +} + +fn set_baggage_static_key(c: &mut Criterion) { + let mut baggage = Baggage::new(); + + c.bench_function("set_baggage_static_key", move |b| { + b.iter(|| { + baggage.insert("key", "value".to_string()); + }) + }); +} + +fn set_baggage_dynamic(c: &mut Criterion) { + let mut baggage = Baggage::new(); + + let mut rng = rand::rng(); + let key_value = (0..MAX_KEY_VALUE_PAIRS) + .map(|_| { + ( + Alphanumeric.sample_string(&mut rng, 4), + Alphanumeric.sample_string(&mut rng, 4), + ) + }) + .collect::>(); + + c.bench_function("set_baggage_dynamic", move |b| { + b.iter_batched( + || rng.random_range(0..MAX_KEY_VALUE_PAIRS), + |idx| { + let (key, value) = key_value[idx].clone(); + baggage.insert(key, value); + }, + BatchSize::SmallInput, + ) + }); +} + +fn set_baggage_dynamic_with_metadata(c: &mut Criterion) { + let mut baggage = Baggage::new(); + + let mut rng = rand::rng(); + let key_value_metadata = (0..MAX_KEY_VALUE_PAIRS) + .map(|_| { + ( + Alphanumeric.sample_string(&mut rng, 4), + Alphanumeric.sample_string(&mut rng, 4), + Alphanumeric.sample_string(&mut rng, 4), + ) + }) + .collect::>(); + + c.bench_function("set_baggage_dynamic_with_metadata", move |b| { + b.iter_batched( + || rng.random_range(0..MAX_KEY_VALUE_PAIRS), + |idx| { + let (key, value, metadata) = key_value_metadata[idx].clone(); + baggage.insert_with_metadata(key, value, metadata); + }, + BatchSize::SmallInput, + ) + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry/benches/context_attach.rs b/vendor/opentelemetry-rust/opentelemetry/benches/context_attach.rs new file mode 100644 index 00000000..f679d79b --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/benches/context_attach.rs @@ -0,0 +1,122 @@ +use criterion::{ + criterion_group, criterion_main, measurement::WallTime, BenchmarkGroup, BenchmarkId, Criterion, +}; +use opentelemetry::{ + trace::{SpanContext, TraceContextExt, TraceState}, + Context, SpanId, TraceFlags, TraceId, +}; +use std::hint::black_box; + +// Run this benchmark with: +// cargo bench --bench context_attach + +fn criterion_benchmark(c: &mut Criterion) { + let remote_context = SpanContext::new( + TraceId::from(47), + SpanId::from(11), + TraceFlags::SAMPLED, + true, + TraceState::NONE, + ); + let span_context = Context::new().with_remote_span_context(remote_context); + let contexts = vec![ + ("empty_cx", Context::new()), + ("single_value_cx", Context::new().with_value(Value(4711))), + ("span_cx", span_context), + ]; + for (name, cx) in contexts { + single_cx_scope(&mut group(c), name, &cx); + nested_cx_scope(&mut group(c), name, &cx); + overlapping_cx_scope(&mut group(c), name, &cx); + } +} + +fn single_cx_scope( + group: &mut BenchmarkGroup<'_, WallTime>, + context_type: &str, + context: &Context, +) { + group.bench_function(BenchmarkId::new("single_cx", context_type), |b| { + b.iter_batched( + || context.clone(), + |cx| { + single_cx(cx); + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +#[inline(never)] +fn single_cx(cx: Context) { + let _cx_guard = black_box(cx.attach()); + let _ = black_box(dummy_work()); +} + +fn nested_cx_scope(group: &mut BenchmarkGroup<'_, WallTime>, cx_type: &str, context: &Context) { + group.bench_function(BenchmarkId::new("nested_cx", cx_type), |b| { + b.iter_batched( + || (context.clone(), context.clone()), + |(cx1, cx2)| { + nested_cx(cx1, cx2); + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +#[inline(never)] +fn nested_cx(cx1: Context, cx2: Context) { + let _outer_cx_guard = black_box(cx1.attach()); + let _inner_cx_guard = black_box(cx2.attach()); + let _ = black_box(dummy_work()); +} + +fn overlapping_cx_scope( + group: &mut BenchmarkGroup<'_, WallTime>, + cx_type: &str, + context: &Context, +) { + // This is to ensure that the context is restored after the benchmark, + // see https://github.com/open-telemetry/opentelemetry-rust/issues/1887 + let _restore_cx_guard = Context::current().attach(); + group.bench_function(BenchmarkId::new("out_of_order_cx_drop", cx_type), |b| { + b.iter_batched( + || (context.clone(), context.clone()), + |(cx1, cx2)| { + out_of_order_cx_drop(cx1, cx2); + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +#[inline(never)] +fn out_of_order_cx_drop(cx1: Context, cx2: Context) { + let outer_cx_guard = cx1.attach(); + let inner_cx_guard = cx2.attach(); + let _ = black_box(dummy_work()); + drop(outer_cx_guard); + drop(inner_cx_guard); +} + +#[inline(never)] +fn dummy_work() -> i32 { + black_box(1 + 1) +} + +fn group(c: &mut Criterion) -> BenchmarkGroup<'_, WallTime> { + c.benchmark_group("context_attach") +} + +#[derive(Debug, PartialEq)] +struct Value(i32); + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry/benches/context_suppression.rs b/vendor/opentelemetry-rust/opentelemetry/benches/context_suppression.rs new file mode 100644 index 00000000..d12829c2 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/benches/context_suppression.rs @@ -0,0 +1,66 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use opentelemetry::Context; +use std::hint::black_box; + +// Run this benchmark with: +// cargo bench --bench context_suppression + +// The benchmark results: +// criterion = "0.5.1" +// Hardware: Apple M4 Pro +// Total Number of Cores:   14 (10 performance and 4 efficiency) +// | Benchmark | Time | +// |---------------------------------------|--------| +// | enter_telemetry_suppressed_scope | 8.3 ns | +// | normal_attach | 9.1 ns | +// | is_current_telemetry_suppressed_false | 750 ps | +// | is_current_telemetry_suppressed_true | 750 ps | + +fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("telemetry_suppression"); + + // Benchmark the cost of entering a suppressed scope + group.bench_function("enter_telemetry_suppressed_scope", |b| { + b.iter(|| { + let _guard = black_box(Context::enter_telemetry_suppressed_scope()); + }); + }); + + // For comparison - normal context attach + group.bench_function("normal_attach", |b| { + b.iter(|| { + let _guard = black_box(Context::current().attach()); + }); + }); + + // Benchmark checking if current is suppressed (when not suppressed) + group.bench_function("is_current_telemetry_suppressed_false", |b| { + // Make sure we're in a non-suppressed context + let _restore_ctx = Context::current().attach(); + b.iter(|| { + let is_suppressed = black_box(Context::is_current_telemetry_suppressed()); + black_box(is_suppressed); + }); + }); + + // Benchmark checking if current is suppressed (when suppressed) + group.bench_function("is_current_telemetry_suppressed_true", |b| { + // Enter suppressed context for the duration of the benchmark + let _suppressed_guard = Context::enter_telemetry_suppressed_scope(); + b.iter(|| { + let is_suppressed = black_box(Context::is_current_telemetry_suppressed()); + black_box(is_suppressed); + }); + }); + + group.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry/benches/logs.rs b/vendor/opentelemetry-rust/opentelemetry/benches/logs.rs new file mode 100644 index 00000000..51a8d136 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/benches/logs.rs @@ -0,0 +1,85 @@ +/* + Benchmark Results: + criterion = "0.5.1" + OS: macOS 15 + Hardware: Apple M4 Pro, 14 cores, RAM: 24 GB + | Test | Average time| + |--------------------------------------------------|-------------| + | CreateLogRecord_NoopLogger | 0.26 ns | + | CreateLogRecord_NoopLogger_WithAttributes | 0.26 ns | + | CreateLogRecord_NoopLogger_WithBody | 1.30 ns | + | CreateLogRecord_NoopLogger_Full | 1.30 ns | + | EventEnabled_NoopLogger | 0.26 ns | +*/ + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use opentelemetry::logs::{ + AnyValue, LogRecord, Logger, LoggerProvider, NoopLoggerProvider, Severity, +}; + +// Run this benchmark with: +// cargo bench --bench logs --features=logs + +fn criterion_benchmark(c: &mut Criterion) { + let provider = NoopLoggerProvider::new(); + let logger = provider.logger("bench"); + + c.bench_function("CreateLogRecord_NoopLogger", |b| { + b.iter(|| { + let record = logger.create_log_record(); + logger.emit(record); + }); + }); + + c.bench_function("CreateLogRecord_NoopLogger_WithAttributes", |b| { + b.iter(|| { + let mut record = logger.create_log_record(); + record.add_attribute("key1", "value1"); + record.add_attribute("key2", 123); + record.add_attribute("key3", true); + record.add_attribute("key4", 123.456); + logger.emit(record); + }); + }); + + c.bench_function("CreateLogRecord_NoopLogger_WithBody", |b| { + b.iter(|| { + let mut record = logger.create_log_record(); + record.set_body(AnyValue::String("This is a log message".into())); + record.set_severity_number(Severity::Info); + record.set_severity_text("INFO"); + logger.emit(record); + }); + }); + + c.bench_function("CreateLogRecord_NoopLogger_Full", |b| { + b.iter(|| { + let mut record = logger.create_log_record(); + record.set_body(AnyValue::String("This is a log message".into())); + record.set_severity_number(Severity::Warn); + record.set_severity_text("WARN"); + record.set_event_name("my.event"); + record.set_target("my_crate::my_module"); + record.add_attribute("key1", "value1"); + record.add_attribute("key2", 123); + record.add_attribute("key3", true); + record.add_attribute("key4", 123.456); + logger.emit(record); + }); + }); + + c.bench_function("EventEnabled_NoopLogger", |b| { + b.iter(|| { + black_box(logger.event_enabled(Severity::Info, "my_target", Some("my_event"))); + }); + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry/benches/metrics.rs b/vendor/opentelemetry-rust/opentelemetry/benches/metrics.rs new file mode 100644 index 00000000..b51c54f9 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/benches/metrics.rs @@ -0,0 +1,113 @@ +/* + Benchmark Results: + criterion = "0.5.1" + OS: macOS 15 + Hardware: Apple M4 Pro, 14 cores, RAM: 24 GB + | Test | Average time| + |-----------------------------------------------------|-------------| + | NoAttributes | 0.77 ns | + | AddWithInlineStaticAttributes | 8.10 ns | + | AddWithStaticArray | 0.77 ns | + | AddWithDynamicAttributes | 44.84 ns | + | AddWithDynamicAttributes_WithStringAllocation | 85.92 ns | +*/ + +use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; +use opentelemetry::{global, metrics::Counter, KeyValue}; +use std::hint::black_box; + +// Run this benchmark with: +// cargo bench --bench metrics + +fn create_counter() -> Counter { + let meter = global::meter("benchmarks"); + + meter.u64_counter("counter_bench").build() +} + +fn criterion_benchmark(c: &mut Criterion) { + counter_add(c); +} + +fn counter_add(c: &mut Criterion) { + let counter = create_counter(); + + c.bench_function("NoAttributes", |b| { + b.iter(|| { + counter.add(1, &[]); + }); + }); + + c.bench_function("AddWithInlineStaticAttributes", |b| { + b.iter(|| { + counter.add( + 1, + &[ + KeyValue::new("attribute1", "value1"), + KeyValue::new("attribute2", "value2"), + KeyValue::new("attribute3", "value3"), + KeyValue::new("attribute4", "value4"), + ], + ); + }); + }); + + let kv = [ + KeyValue::new("attribute1", "value1"), + KeyValue::new("attribute2", "value2"), + KeyValue::new("attribute3", "value3"), + KeyValue::new("attribute4", "value4"), + ]; + + c.bench_function("AddWithStaticArray", |b| { + b.iter(|| { + counter.add(1, &kv); + }); + }); + + c.bench_function("AddWithDynamicAttributes", |b| { + b.iter_batched( + || { + let value1 = "value1".to_string(); + let value2 = "value2".to_string(); + let value3 = "value3".to_string(); + let value4 = "value4".to_string(); + + (value1, value2, value3, value4) + }, + |values| { + let kv = &[ + KeyValue::new("attribute1", values.0), + KeyValue::new("attribute2", values.1), + KeyValue::new("attribute3", values.2), + KeyValue::new("attribute4", values.3), + ]; + + counter.add(1, kv); + }, + BatchSize::SmallInput, + ); + }); + + c.bench_function("AddWithDynamicAttributes_WithStringAllocation", |b| { + b.iter(|| { + let kv = &[ + KeyValue::new("attribute1", black_box("value1".to_string())), + KeyValue::new("attribute2", black_box("value2".to_string())), + KeyValue::new("attribute3", black_box("value3".to_string())), + KeyValue::new("attribute4", black_box("value4".to_string())), + ]; + + counter.add(1, kv); + }); + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry/benches/trace.rs b/vendor/opentelemetry-rust/opentelemetry/benches/trace.rs new file mode 100644 index 00000000..5d587373 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/benches/trace.rs @@ -0,0 +1,184 @@ +/* + Benchmark Results: + criterion = "0.5.1" + OS: macOS 15 + Hardware: Apple M4 Pro, 14 cores, RAM: 24 GB + | Test | Average time| + |--------------------------------------------------|-------------| + | CreateSpan_NoopTracer | 29.2 ns | + | CreateSpan_NoopTracer_4Attributes | 29.4 ns | + | CreateSpan_NoopTracer_AddEvent | 29.2 ns | + | CreateSpan_NoopTracer_AddLink | 29.2 ns | + | CreateSpan_NoopTracer_SetActive | 73.3 ns | + | CreateSpan_NoopTracer_WithActiveParent | 102.9 ns | + | CreateSpan_NoopTracer_InSpan | 78.0 ns | + | SpanBuilder_Creation | 22.8 ns | + | SpanBuilder_WithAttributes | 51.7 ns | + | SpanBuilder_WithLinks | 69.0 ns | +*/ + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use opentelemetry::{ + global, + trace::{ + noop::NoopTracerProvider, Span, SpanContext, SpanId, SpanKind, TraceFlags, TraceId, + TraceState, Tracer, + }, + KeyValue, +}; + +// Run this benchmark with: +// cargo bench --bench trace --features=trace + +fn valid_span_context() -> SpanContext { + SpanContext::new( + TraceId::from(12345u128), + SpanId::from(12345u64), + TraceFlags::SAMPLED, + true, + TraceState::default(), + ) +} + +fn criterion_benchmark(c: &mut Criterion) { + global::set_tracer_provider(NoopTracerProvider::new()); + let tracer = global::tracer("bench"); + + span_lifecycle(c, &tracer); + span_builder(c, &tracer); +} + +fn span_lifecycle( + c: &mut Criterion, + tracer: &impl Tracer, +) { + c.bench_function("CreateSpan_NoopTracer", |b| { + b.iter(|| { + let mut span = tracer.start("test-span"); + span.end(); + }); + }); + + c.bench_function("CreateSpan_NoopTracer_4Attributes", |b| { + b.iter(|| { + let mut span = tracer.start("test-span"); + if span.is_recording() { + span.set_attribute(KeyValue::new("key1", false)); + span.set_attribute(KeyValue::new("key2", "hello")); + span.set_attribute(KeyValue::new("key3", 123)); + span.set_attribute(KeyValue::new("key4", 123.456)); + } + span.end(); + }); + }); + + c.bench_function("CreateSpan_NoopTracer_AddEvent", |b| { + b.iter(|| { + let mut span = tracer.start("test-span"); + if span.is_recording() { + span.add_event( + "my-event", + vec![ + KeyValue::new("key1", "value1"), + KeyValue::new("key2", "value2"), + ], + ); + } + span.end(); + }); + }); + + c.bench_function("CreateSpan_NoopTracer_AddLink", |b| { + let link_context = valid_span_context(); + b.iter(|| { + let mut span = tracer.start("test-span"); + if span.is_recording() { + span.add_link( + link_context.clone(), + vec![ + KeyValue::new("key1", "value1"), + KeyValue::new("key2", "value2"), + ], + ); + } + span.end(); + }); + }); + + c.bench_function("CreateSpan_NoopTracer_SetActive", |b| { + b.iter(|| { + let span = tracer.start("test-span"); + let _guard = opentelemetry::trace::mark_span_as_active(span); + }); + }); + + c.bench_function("CreateSpan_NoopTracer_WithActiveParent", |b| { + b.iter(|| { + let parent = tracer.start("parent"); + let _guard = opentelemetry::trace::mark_span_as_active(parent); + let mut child = tracer.start("child"); + child.end(); + }); + }); + + c.bench_function("CreateSpan_NoopTracer_InSpan", |b| { + b.iter(|| { + tracer.in_span("test-span", |_cx| {}); + }); + }); +} + +fn span_builder(c: &mut Criterion, tracer: &impl Tracer) { + c.bench_function("SpanBuilder_Creation", |b| { + b.iter(|| { + let span = tracer + .span_builder("test-span") + .with_kind(SpanKind::Client) + .start(tracer); + black_box(span); + }); + }); + + c.bench_function("SpanBuilder_WithAttributes", |b| { + b.iter(|| { + let span = tracer + .span_builder("test-span") + .with_kind(SpanKind::Client) + .with_attributes(vec![ + KeyValue::new("key1", false), + KeyValue::new("key2", "hello"), + KeyValue::new("key3", 123), + KeyValue::new("key4", 123.456), + ]) + .start(tracer); + black_box(span); + }); + }); + + c.bench_function("SpanBuilder_WithLinks", |b| { + let link_context = valid_span_context(); + b.iter(|| { + let span = tracer + .span_builder("test-span") + .with_links(vec![opentelemetry::trace::Link::new( + link_context.clone(), + vec![ + KeyValue::new("key1", "value1"), + KeyValue::new("key2", "value2"), + ], + 0, + )]) + .start(tracer); + black_box(span); + }); + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry/src/baggage.rs b/vendor/opentelemetry-rust/opentelemetry/src/baggage.rs new file mode 100644 index 00000000..509623b9 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/baggage.rs @@ -0,0 +1,677 @@ +//! Primitives for sending name/value data across system boundaries. +//! +//! Baggage is used to annotate telemetry, adding context and information to +//! metrics, traces, and logs. It is a set of name/value pairs describing +//! user-defined properties. Each name in Baggage is associated with exactly one +//! value. +//! +//! Main types in this module are: +//! +//! * [`Baggage`]: A set of name/value pairs describing user-defined properties. +//! * [`BaggageExt`]: Extensions for managing `Baggage` in a [`Context`]. +//! +//! Baggage can be sent between systems using a baggage propagator in +//! accordance with the [W3C Baggage] specification. +//! +//! Note: Baggage is not automatically added to any telemetry. Users have to +//! explicitly add baggage entries to telemetry items. +//! +//! +//! [W3C Baggage]: https://w3c.github.io/baggage +use crate::{Context, Key, KeyValue, StringValue}; +use std::collections::hash_map::Entry; +use std::collections::{hash_map, HashMap}; +use std::fmt; +use std::sync::OnceLock; + +static DEFAULT_BAGGAGE: OnceLock = OnceLock::new(); + +const MAX_KEY_VALUE_PAIRS: usize = 64; +const MAX_LEN_OF_ALL_PAIRS: usize = 8192; + +// https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 +const INVALID_ASCII_KEY_CHARS: [u8; 17] = *b"(),/:;<=>?@[\\]{}\""; + +/// Returns the default baggage, ensuring it is initialized only once. +#[inline] +fn get_default_baggage() -> &'static Baggage { + DEFAULT_BAGGAGE.get_or_init(Baggage::default) +} + +/// A set of name/value pairs describing user-defined properties. +/// +/// ### Baggage Names +/// +/// * ASCII strings according to the token format, defined in [RFC2616, Section 2.2] +/// +/// ### Baggage Values +/// +/// * URL encoded UTF-8 strings. +/// +/// ### Baggage Value Metadata +/// +/// Additional metadata can be added to values in the form of a property set, +/// represented as semi-colon `;` delimited list of names and/or name/value pairs, +/// e.g. `;k1=v1;k2;k3=v3`. +/// +/// ### Limits +/// +/// * Maximum number of name/value pairs: `64`. +/// * Maximum total length of all name/value pairs: `8192`. +/// +/// +#[derive(Debug, Default)] +pub struct Baggage { + inner: HashMap, + kv_content_len: usize, // the length of key-value-metadata string in `inner` +} + +impl Baggage { + /// Creates an empty `Baggage`. + pub fn new() -> Self { + Baggage { + inner: HashMap::default(), + kv_content_len: 0, + } + } + + /// Returns a reference to the value associated with a given name + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{baggage::Baggage, StringValue}; + /// + /// let mut baggage = Baggage::new(); + /// let _ = baggage.insert("my-name", "my-value"); + /// + /// assert_eq!(baggage.get("my-name"), Some(&StringValue::from("my-value"))) + /// ``` + pub fn get>(&self, key: K) -> Option<&StringValue> { + self.inner.get(key.as_ref()).map(|(value, _metadata)| value) + } + + /// Returns a reference to the value and metadata associated with a given name + /// + /// # Examples + /// ``` + /// use opentelemetry::{baggage::{Baggage, BaggageMetadata}, StringValue}; + /// + /// let mut baggage = Baggage::new(); + /// let _ = baggage.insert("my-name", "my-value"); + /// + /// // By default, the metadata is empty + /// assert_eq!(baggage.get_with_metadata("my-name"), Some(&(StringValue::from("my-value"), BaggageMetadata::from("")))) + /// ``` + pub fn get_with_metadata>( + &self, + key: K, + ) -> Option<&(StringValue, BaggageMetadata)> { + self.inner.get(key.as_ref()) + } + + /// Inserts a name/value pair into the baggage. + /// + /// If the name was not present, [`None`] is returned. If the name was present, + /// the value is updated, and the old value is returned. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{baggage::Baggage, StringValue}; + /// + /// let mut baggage = Baggage::new(); + /// let _ = baggage.insert("my-name", "my-value"); + /// + /// assert_eq!(baggage.get("my-name"), Some(&StringValue::from("my-value"))) + /// ``` + pub fn insert(&mut self, key: K, value: V) -> Option + where + K: Into, + V: Into, + { + self.insert_with_metadata(key, value, BaggageMetadata::default()) + .map(|pair| pair.0) + } + + /// Inserts a name/value(+metadata) pair into the baggage. + /// + /// Same with `insert`, if the name was not present, [`None`] will be returned. + /// If the name is present, the old value and metadata will be returned. + /// + /// Also checks for [limits](https://w3c.github.io/baggage/#limits). + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{baggage::{Baggage, BaggageMetadata}, StringValue}; + /// + /// let mut baggage = Baggage::new(); + /// let _ = baggage.insert_with_metadata("my-name", "my-value", "test"); + /// + /// assert_eq!(baggage.get_with_metadata("my-name"), Some(&(StringValue::from("my-value"), BaggageMetadata::from("test")))) + /// ``` + pub fn insert_with_metadata( + &mut self, + key: K, + value: V, + metadata: S, + ) -> Option<(StringValue, BaggageMetadata)> + where + K: Into, + V: Into, + S: Into, + { + let (key, value, metadata) = (key.into(), value.into(), metadata.into()); + let entries_count = self.inner.len(); + match self.inner.entry(key) { + Entry::Occupied(mut occupied_entry) => { + let key_str = occupied_entry.key().as_str(); + let entry_content_len = + key_value_metadata_bytes_size(key_str, value.as_str(), metadata.as_str()); + let prev_content_len = key_value_metadata_bytes_size( + key_str, + occupied_entry.get().0.as_str(), + occupied_entry.get().1.as_str(), + ); + let new_content_len = self.kv_content_len + entry_content_len - prev_content_len; + if new_content_len > MAX_LEN_OF_ALL_PAIRS { + return None; + } + self.kv_content_len = new_content_len; + Some(occupied_entry.insert((value, metadata))) + } + Entry::Vacant(vacant_entry) => { + let key_str = vacant_entry.key().as_str(); + if !Self::is_key_valid(key_str.as_bytes()) { + return None; + } + if entries_count == MAX_KEY_VALUE_PAIRS { + return None; + } + let entry_content_len = + key_value_metadata_bytes_size(key_str, value.as_str(), metadata.as_str()); + let new_content_len = self.kv_content_len + entry_content_len; + if new_content_len > MAX_LEN_OF_ALL_PAIRS { + return None; + } + self.kv_content_len = new_content_len; + vacant_entry.insert((value, metadata)); + None + } + } + } + + /// Removes a name from the baggage, returning the value + /// corresponding to the name if the pair was previously in the map. + pub fn remove>(&mut self, key: K) -> Option<(StringValue, BaggageMetadata)> { + self.inner.remove(key.as_ref()) + } + + /// Returns the number of attributes for this baggage + pub fn len(&self) -> usize { + self.inner.len() + } + + /// Returns `true` if the baggage contains no items. + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Gets an iterator over the baggage items, in any order. + pub fn iter(&self) -> Iter<'_> { + self.into_iter() + } + + fn is_key_valid(key: &[u8]) -> bool { + !key.is_empty() + && key + .iter() + .all(|b| b.is_ascii_graphic() && !INVALID_ASCII_KEY_CHARS.contains(b)) + } +} + +/// Get the number of bytes for one key-value pair +fn key_value_metadata_bytes_size(key: &str, value: &str, metadata: &str) -> usize { + key.len() + value.len() + metadata.len() +} + +/// An iterator over the entries of a [`Baggage`]. +#[derive(Debug)] +pub struct Iter<'a>(hash_map::Iter<'a, Key, (StringValue, BaggageMetadata)>); + +impl<'a> Iterator for Iter<'a> { + type Item = (&'a Key, &'a (StringValue, BaggageMetadata)); + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl<'a> IntoIterator for &'a Baggage { + type Item = (&'a Key, &'a (StringValue, BaggageMetadata)); + type IntoIter = Iter<'a>; + + fn into_iter(self) -> Self::IntoIter { + Iter(self.inner.iter()) + } +} + +impl FromIterator<(Key, (StringValue, BaggageMetadata))> for Baggage { + fn from_iter>(iter: I) -> Self { + let mut baggage = Baggage::default(); + for (key, (value, metadata)) in iter.into_iter() { + baggage.insert_with_metadata(key, value, metadata); + } + baggage + } +} + +impl FromIterator for Baggage { + fn from_iter>(iter: I) -> Self { + let mut baggage = Baggage::default(); + for kv in iter.into_iter() { + baggage.insert(kv.key, kv.value); + } + baggage + } +} + +impl FromIterator for Baggage { + fn from_iter>(iter: I) -> Self { + let mut baggage = Baggage::default(); + for kvm in iter.into_iter() { + baggage.insert_with_metadata(kvm.key, kvm.value, kvm.metadata); + } + baggage + } +} + +impl From for Baggage +where + I: IntoIterator, + I::Item: Into, +{ + fn from(value: I) -> Self { + value.into_iter().map(Into::into).collect() + } +} + +fn encode(s: &str) -> String { + let mut encoded_string = String::with_capacity(s.len()); + + for byte in s.as_bytes() { + match *byte { + b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_' | b'~' => { + encoded_string.push(*byte as char) + } + b' ' => encoded_string.push_str("%20"), + _ => encoded_string.push_str(&format!("%{byte:02X}")), + } + } + encoded_string +} + +impl fmt::Display for Baggage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (i, (k, v)) in self.into_iter().enumerate() { + write!(f, "{}={}", k, encode(v.0.as_str()))?; + if !v.1.as_str().is_empty() { + write!(f, ";{}", v.1)?; + } + + if i < self.len() - 1 { + write!(f, ",")?; + } + } + + Ok(()) + } +} + +/// Methods for sorting and retrieving baggage data in a context. +pub trait BaggageExt { + /// Returns a clone of the given context with the included name/value pairs. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{baggage::{Baggage, BaggageExt}, Context, KeyValue, StringValue}; + /// + /// // Explicit `Baggage` creation + /// let mut baggage = Baggage::new(); + /// let _ = baggage.insert("my-name", "my-value"); + /// + /// let cx = Context::map_current(|cx| { + /// cx.with_baggage(baggage) + /// }); + /// + /// // Passing an iterator + /// let cx = Context::map_current(|cx| { + /// cx.with_baggage([KeyValue::new("my-name", "my-value")]) + /// }); + /// + /// assert_eq!( + /// cx.baggage().get("my-name"), + /// Some(&StringValue::from("my-value")), + /// ) + /// ``` + fn with_baggage>(&self, baggage: T) -> Self; + + /// Returns a clone of the current context with the included name/value pairs. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{baggage::{Baggage, BaggageExt}, Context, StringValue}; + /// + /// let mut baggage = Baggage::new(); + /// let _ = baggage.insert("my-name", "my-value"); + /// + /// let cx = Context::current_with_baggage(baggage); + /// + /// assert_eq!( + /// cx.baggage().get("my-name"), + /// Some(&StringValue::from("my-value")), + /// ) + /// ``` + fn current_with_baggage>(baggage: T) -> Self; + + /// Returns a clone of the given context with no baggage. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{baggage::BaggageExt, Context}; + /// + /// let cx = Context::map_current(|cx| cx.with_cleared_baggage()); + /// + /// assert_eq!(cx.baggage().len(), 0); + /// ``` + fn with_cleared_baggage(&self) -> Self; + + /// Returns a reference to this context's baggage, or the default + /// empty baggage if none has been set. + fn baggage(&self) -> &Baggage; +} + +/// Solely used to store `Baggage` in the `Context` without allowing direct access +#[derive(Debug)] +struct BaggageContextValue(Baggage); + +impl BaggageExt for Context { + fn with_baggage>(&self, baggage: T) -> Self { + self.with_value(BaggageContextValue(baggage.into())) + } + + fn current_with_baggage>(baggage: T) -> Self { + Context::map_current(|cx| cx.with_baggage(baggage)) + } + + fn with_cleared_baggage(&self) -> Self { + self.with_baggage(Baggage::new()) + } + + fn baggage(&self) -> &Baggage { + self.get::() + .map_or(get_default_baggage(), |b| &b.0) + } +} + +/// An optional property set that can be added to [`Baggage`] values. +/// +/// `BaggageMetadata` can be added to values in the form of a property set, +/// represented as semi-colon `;` delimited list of names and/or name/value +/// pairs, e.g. `;k1=v1;k2;k3=v3`. +#[derive(Clone, Debug, PartialOrd, PartialEq, Eq, Default)] +pub struct BaggageMetadata(String); + +impl BaggageMetadata { + /// Return underlying string + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl From for BaggageMetadata { + fn from(s: String) -> BaggageMetadata { + BaggageMetadata(s.trim().to_string()) + } +} + +impl From<&str> for BaggageMetadata { + fn from(s: &str) -> Self { + BaggageMetadata(s.trim().to_string()) + } +} + +impl fmt::Display for BaggageMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Ok(write!(f, "{}", self.as_str())?) + } +} + +/// [`Baggage`] name/value pairs with their associated metadata. +#[derive(Clone, Debug, PartialEq)] +pub struct KeyValueMetadata { + /// Dimension or event key + pub(crate) key: Key, + /// Dimension or event value + pub(crate) value: StringValue, + /// Metadata associate with this key value pair + pub(crate) metadata: BaggageMetadata, +} + +impl KeyValueMetadata { + /// Create a new `KeyValue` pair with metadata + pub fn new(key: K, value: V, metadata: S) -> Self + where + K: Into, + V: Into, + S: Into, + { + KeyValueMetadata { + key: key.into(), + value: value.into(), + metadata: metadata.into(), + } + } +} + +impl From for KeyValueMetadata { + fn from(kv: KeyValue) -> Self { + KeyValueMetadata { + key: kv.key, + value: kv.value.into(), + metadata: BaggageMetadata::default(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::StringValue; + + use super::*; + + #[test] + fn insert_non_ascii_key() { + let mut baggage = Baggage::new(); + baggage.insert("🚫", "not ascii key"); + assert_eq!(baggage.len(), 0, "did not insert invalid key"); + } + + #[test] + fn test_ascii_values() { + let string1 = "test_ 123"; + let string2 = "Hello123"; + let string3 = "This & That = More"; + let string4 = "Unicode: 😊"; + let string5 = "Non-ASCII: áéíóú"; + let string6 = "Unsafe: ~!@#$%^&*()_+{}[];:'\\\"<>?,./"; + let string7: &str = "🚀Unicode:"; + let string8 = "ΑΒΓ"; + + assert_eq!(encode(string1), "test_%20123"); + assert_eq!(encode(string2), "Hello123"); + assert_eq!(encode(string3), "This%20%26%20That%20%3D%20More"); + assert_eq!(encode(string4), "Unicode%3A%20%F0%9F%98%8A"); + assert_eq!( + encode(string5), + "Non-ASCII%3A%20%C3%A1%C3%A9%C3%AD%C3%B3%C3%BA" + ); + assert_eq!(encode(string6), "Unsafe%3A%20~%21%40%23%24%25%5E%26%2A%28%29_%2B%7B%7D%5B%5D%3B%3A%27%5C%22%3C%3E%3F%2C.%2F"); + assert_eq!(encode(string7), "%F0%9F%9A%80Unicode%3A"); + assert_eq!(encode(string8), "%CE%91%CE%92%CE%93"); + } + + #[test] + fn insert_too_much_baggage() { + // too many key pairs + let over_limit = MAX_KEY_VALUE_PAIRS + 1; + let mut data = Vec::with_capacity(over_limit); + for i in 0..over_limit { + data.push(KeyValue::new(format!("key{i}"), format!("key{i}"))) + } + let baggage = data.into_iter().collect::(); + assert_eq!(baggage.len(), MAX_KEY_VALUE_PAIRS) + } + + #[test] + fn insert_pairs_length_exceed() { + let mut data = vec![]; + for letter in vec!['a', 'b', 'c', 'd'].into_iter() { + data.push(KeyValue::new( + (0..MAX_LEN_OF_ALL_PAIRS / 3) + .map(|_| letter) + .collect::(), + "", + )); + } + let baggage = data.into_iter().collect::(); + assert_eq!(baggage.len(), 3) + } + + #[test] + fn serialize_baggage_as_string() { + // Empty baggage + let b = Baggage::default(); + assert_eq!("", b.to_string()); + + // "single member empty value no properties" + let mut b = Baggage::default(); + b.insert("foo", StringValue::from("")); + assert_eq!("foo=", b.to_string()); + + // "single member no properties" + let mut b = Baggage::default(); + b.insert("foo", StringValue::from("1")); + assert_eq!("foo=1", b.to_string()); + + // "URL encoded value" + let mut b = Baggage::default(); + b.insert("foo", StringValue::from("1=1")); + assert_eq!("foo=1%3D1", b.to_string()); + + // "single member empty value with properties" + let mut b = Baggage::default(); + b.insert_with_metadata( + "foo", + StringValue::from(""), + BaggageMetadata::from("red;state=on"), + ); + assert_eq!("foo=;red;state=on", b.to_string()); + + // "single member with properties" + let mut b = Baggage::default(); + b.insert_with_metadata("foo", StringValue::from("1"), "red;state=on;z=z=z"); + assert_eq!("foo=1;red;state=on;z=z=z", b.to_string()); + + // "two members with properties" + let mut b = Baggage::default(); + b.insert_with_metadata("foo", StringValue::from("1"), "red;state=on"); + b.insert_with_metadata("bar", StringValue::from("2"), "yellow"); + assert!(b.to_string().contains("bar=2;yellow")); + assert!(b.to_string().contains("foo=1;red;state=on")); + } + + #[test] + fn replace_existing_key() { + let half_minus2: StringValue = (0..MAX_LEN_OF_ALL_PAIRS / 2 - 2) + .map(|_| 'x') + .collect::() + .into(); + + let mut b = Baggage::default(); + b.insert("a", half_minus2.clone()); // +1 for key + b.insert("b", half_minus2); // +1 for key + b.insert("c", StringValue::from(".")); // total of 2 bytes + assert!(b.get("a").is_some()); + assert!(b.get("b").is_some()); + assert!(b.get("c").is_some()); + assert!(b.insert("c", StringValue::from("..")).is_none()); // exceeds MAX_LEN_OF_ALL_PAIRS + assert_eq!(b.insert("c", StringValue::from("!")).unwrap(), ".".into()); // replaces existing + } + + #[test] + fn test_crud_operations() { + let mut baggage = Baggage::default(); + assert!(baggage.is_empty()); + + // create + baggage.insert("foo", "1"); + assert_eq!(baggage.len(), 1); + + // get + assert_eq!(baggage.get("foo"), Some(&StringValue::from("1"))); + + // update + baggage.insert("foo", "2"); + assert_eq!(baggage.get("foo"), Some(&StringValue::from("2"))); + + // delete + baggage.remove("foo"); + assert!(baggage.is_empty()); + } + + #[test] + fn test_insert_invalid_key() { + let mut baggage = Baggage::default(); + + // empty + baggage.insert("", "1"); + assert!(baggage.is_empty()); + + // non-ascii + baggage.insert("Grüße", "1"); + assert!(baggage.is_empty()); + + // invalid ascii chars + baggage.insert("(example)", "1"); + assert!(baggage.is_empty()); + } + + #[test] + fn test_context_clear_baggage() { + let ctx = Context::new(); + let ctx = ctx.with_baggage([KeyValue::new("foo", 1)]); + let _guard = ctx.attach(); + + { + let ctx = Context::current(); + let baggage = ctx.baggage(); + // At this point baggage should still contain the inital value. + assert_eq!(baggage.len(), 1); + + // Baggage gets cleared. + let ctx = ctx.with_cleared_baggage(); + let _guard = ctx.attach(); + { + let ctx = Context::current(); + let baggage = ctx.baggage(); + // Baggage should contain no entries. + assert_eq!(baggage.len(), 0); + } + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/common.rs b/vendor/opentelemetry-rust/opentelemetry/src/common.rs new file mode 100644 index 00000000..df233a66 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/common.rs @@ -0,0 +1,761 @@ +use std::borrow::{Borrow, Cow}; +use std::sync::Arc; +use std::{fmt, hash}; + +use std::hash::{Hash, Hasher}; + +/// The key part of attribute [KeyValue] pairs. +/// +/// See the [naming] spec for guidelines. +/// +/// [naming]: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/general/naming.md +#[non_exhaustive] +#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Key(OtelString); + +impl Key { + /// Create a new `Key`. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::Key; + /// use std::sync::Arc; + /// + /// let key1 = Key::new("my_static_str"); + /// let key2 = Key::new(String::from("my_owned_string")); + /// let key3 = Key::new(Arc::from("my_ref_counted_str")); + /// ``` + pub fn new(value: impl Into) -> Self { + value.into() + } + + /// Create a new const `Key`. + pub const fn from_static_str(value: &'static str) -> Self { + Key(OtelString::Static(value)) + } + + /// Returns a reference to the underlying key name + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl From<&'static str> for Key { + /// Convert a `&str` to a `Key`. + fn from(key_str: &'static str) -> Self { + Key(OtelString::Static(key_str)) + } +} + +impl From for Key { + /// Convert a `String` to a `Key`. + fn from(string: String) -> Self { + Key(OtelString::Owned(string.into_boxed_str())) + } +} + +impl From> for Key { + /// Convert a `String` to a `Key`. + fn from(string: Arc) -> Self { + Key(OtelString::RefCounted(string)) + } +} + +impl From> for Key { + /// Convert a `Cow<'static, str>` to a `Key` + fn from(string: Cow<'static, str>) -> Self { + match string { + Cow::Borrowed(s) => Key(OtelString::Static(s)), + Cow::Owned(s) => Key(OtelString::Owned(s.into_boxed_str())), + } + } +} + +impl fmt::Debug for Key { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(fmt) + } +} + +impl From for String { + fn from(key: Key) -> Self { + match key.0 { + OtelString::Owned(s) => s.into_string(), + OtelString::Static(s) => s.to_owned(), + OtelString::RefCounted(s) => s.to_string(), + } + } +} + +impl fmt::Display for Key { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + OtelString::Owned(s) => s.fmt(fmt), + OtelString::Static(s) => s.fmt(fmt), + OtelString::RefCounted(s) => s.fmt(fmt), + } + } +} + +impl Borrow for Key { + fn borrow(&self) -> &str { + self.0.as_str() + } +} + +impl AsRef for Key { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +#[derive(Clone, Debug, Eq)] +enum OtelString { + Owned(Box), + Static(&'static str), + RefCounted(Arc), +} + +impl OtelString { + fn as_str(&self) -> &str { + match self { + OtelString::Owned(s) => s.as_ref(), + OtelString::Static(s) => s, + OtelString::RefCounted(s) => s.as_ref(), + } + } +} + +impl PartialOrd for OtelString { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for OtelString { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_str().cmp(other.as_str()) + } +} + +impl PartialEq for OtelString { + fn eq(&self, other: &Self) -> bool { + self.as_str().eq(other.as_str()) + } +} + +impl hash::Hash for OtelString { + fn hash(&self, state: &mut H) { + self.as_str().hash(state) + } +} + +/// A [Value::Array] containing homogeneous values. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq)] +pub enum Array { + /// Array of bools + Bool(Vec), + /// Array of integers + I64(Vec), + /// Array of floats + F64(Vec), + /// Array of strings + String(Vec), +} + +impl fmt::Display for Array { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Array::Bool(values) => display_array_str(values, fmt), + Array::I64(values) => display_array_str(values, fmt), + Array::F64(values) => display_array_str(values, fmt), + Array::String(values) => { + write!(fmt, "[")?; + for (i, t) in values.iter().enumerate() { + if i > 0 { + write!(fmt, ",")?; + } + write!(fmt, "\"{t}\"")?; + } + write!(fmt, "]") + } + } + } +} + +fn display_array_str(slice: &[T], fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "[")?; + for (i, t) in slice.iter().enumerate() { + if i > 0 { + write!(fmt, ",")?; + } + write!(fmt, "{t}")?; + } + write!(fmt, "]") +} + +macro_rules! into_array { + ($(($t:ty, $val:expr),)+) => { + $( + impl From<$t> for Array { + fn from(t: $t) -> Self { + $val(t) + } + } + )+ + } +} + +into_array!( + (Vec, Array::Bool), + (Vec, Array::I64), + (Vec, Array::F64), + (Vec, Array::String), +); + +/// The value part of attribute [KeyValue] pairs. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq)] +pub enum Value { + /// bool values + Bool(bool), + /// i64 values + I64(i64), + /// f64 values + F64(f64), + /// String values + String(StringValue), + /// Array of homogeneous values + Array(Array), +} + +/// Wrapper for string-like values +#[non_exhaustive] +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct StringValue(OtelString); + +impl fmt::Debug for StringValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for StringValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + OtelString::Owned(s) => s.fmt(f), + OtelString::Static(s) => s.fmt(f), + OtelString::RefCounted(s) => s.fmt(f), + } + } +} + +impl AsRef for StringValue { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl StringValue { + /// Returns a string slice to this value + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl From for String { + fn from(s: StringValue) -> Self { + match s.0 { + OtelString::Owned(s) => s.into_string(), + OtelString::Static(s) => s.to_owned(), + OtelString::RefCounted(s) => s.to_string(), + } + } +} + +impl From<&'static str> for StringValue { + fn from(s: &'static str) -> Self { + StringValue(OtelString::Static(s)) + } +} + +impl From for StringValue { + fn from(s: String) -> Self { + StringValue(OtelString::Owned(s.into_boxed_str())) + } +} + +impl From> for StringValue { + fn from(s: Arc) -> Self { + StringValue(OtelString::RefCounted(s)) + } +} + +impl From> for StringValue { + fn from(s: Cow<'static, str>) -> Self { + match s { + Cow::Owned(s) => StringValue(OtelString::Owned(s.into_boxed_str())), + Cow::Borrowed(s) => StringValue(OtelString::Static(s)), + } + } +} + +impl From for StringValue { + fn from(s: Value) -> Self { + match s { + Value::Bool(v) => format!("{v}").into(), + Value::I64(v) => format!("{v}").into(), + Value::F64(v) => format!("{v}").into(), + Value::String(v) => v, + Value::Array(v) => format!("{v}").into(), + } + } +} + +impl Value { + /// String representation of the `Value` + /// + /// This will allocate if the underlying value is not a `String`. + pub fn as_str(&self) -> Cow<'_, str> { + match self { + Value::Bool(v) => format!("{v}").into(), + Value::I64(v) => format!("{v}").into(), + Value::F64(v) => format!("{v}").into(), + Value::String(v) => Cow::Borrowed(v.as_str()), + Value::Array(v) => format!("{v}").into(), + } + } +} + +macro_rules! from_values { + ( + $( + ($t:ty, $val:expr); + )+ + ) => { + $( + impl From<$t> for Value { + fn from(t: $t) -> Self { + $val(t) + } + } + )+ + } +} + +from_values!( + (bool, Value::Bool); + (i64, Value::I64); + (f64, Value::F64); + (StringValue, Value::String); +); + +impl From<&'static str> for Value { + fn from(s: &'static str) -> Self { + Value::String(s.into()) + } +} + +impl From for Value { + fn from(s: String) -> Self { + Value::String(s.into()) + } +} + +impl From> for Value { + fn from(s: Arc) -> Self { + Value::String(s.into()) + } +} + +impl From> for Value { + fn from(s: Cow<'static, str>) -> Self { + Value::String(s.into()) + } +} + +impl fmt::Display for Value { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Value::Bool(v) => v.fmt(fmt), + Value::I64(v) => v.fmt(fmt), + Value::F64(v) => v.fmt(fmt), + Value::String(v) => fmt.write_str(v.as_str()), + Value::Array(v) => v.fmt(fmt), + } + } +} + +/// A key-value pair describing an attribute. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub struct KeyValue { + /// The attribute name + pub key: Key, + + /// The attribute value + pub value: Value, +} + +impl KeyValue { + /// Create a new `KeyValue` pair. + pub fn new(key: K, value: V) -> Self + where + K: Into, + V: Into, + { + KeyValue { + key: key.into(), + value: value.into(), + } + } +} + +struct F64Hashable(f64); + +impl PartialEq for F64Hashable { + fn eq(&self, other: &Self) -> bool { + self.0.to_bits() == other.0.to_bits() + } +} + +impl Eq for F64Hashable {} + +impl Hash for F64Hashable { + fn hash(&self, state: &mut H) { + self.0.to_bits().hash(state); + } +} + +impl Hash for KeyValue { + fn hash(&self, state: &mut H) { + self.key.hash(state); + match &self.value { + Value::F64(f) => F64Hashable(*f).hash(state), + Value::Array(a) => match a { + Array::Bool(b) => b.hash(state), + Array::I64(i) => i.hash(state), + Array::F64(f) => f.iter().for_each(|f| F64Hashable(*f).hash(state)), + Array::String(s) => s.hash(state), + }, + Value::Bool(b) => b.hash(state), + Value::I64(i) => i.hash(state), + Value::String(s) => s.hash(state), + }; + } +} + +impl Eq for KeyValue {} + +/// Information about a library or crate providing instrumentation. +/// +/// An instrumentation scope should be named to follow any naming conventions +/// of the instrumented library (e.g. 'middleware' for a web framework). +/// +/// See the [instrumentation libraries] spec for more information. +/// +/// [instrumentation libraries]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/overview.md#instrumentation-libraries +#[derive(Debug, Default, Clone)] +#[non_exhaustive] +pub struct InstrumentationScope { + /// The library name. + /// + /// This should be the name of the crate providing the instrumentation. + name: Cow<'static, str>, + + /// The library version. + version: Option>, + + /// [Schema URL] used by this library. + /// + /// [Schema URL]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url + schema_url: Option>, + + /// Specifies the instrumentation scope attributes to associate with emitted telemetry. + attributes: Vec, +} + +impl PartialEq for InstrumentationScope { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.version == other.version + && self.schema_url == other.schema_url + && self.attributes.len() == other.attributes.len() + && { + let mut self_attrs = Vec::from_iter(&self.attributes); + let mut other_attrs = Vec::from_iter(&other.attributes); + self_attrs.sort_unstable_by(|a, b| a.key.cmp(&b.key)); + other_attrs.sort_unstable_by(|a, b| a.key.cmp(&b.key)); + self_attrs == other_attrs + } + } +} + +impl Eq for InstrumentationScope {} + +impl hash::Hash for InstrumentationScope { + fn hash(&self, state: &mut H) { + self.name.hash(state); + self.version.hash(state); + self.schema_url.hash(state); + let mut sorted_attrs = Vec::from_iter(&self.attributes); + sorted_attrs.sort_unstable_by(|a, b| a.key.cmp(&b.key)); + for attribute in sorted_attrs { + attribute.hash(state); + } + } +} + +impl InstrumentationScope { + /// Create a new builder to create an [InstrumentationScope] + pub fn builder>>(name: T) -> InstrumentationScopeBuilder { + InstrumentationScopeBuilder { + name: name.into(), + version: None, + schema_url: None, + attributes: None, + } + } + + /// Returns the instrumentation library name. + #[inline] + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the instrumentation library version. + #[inline] + pub fn version(&self) -> Option<&str> { + self.version.as_deref() + } + + /// Returns the [Schema URL] used by this library. + /// + /// [Schema URL]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url + #[inline] + pub fn schema_url(&self) -> Option<&str> { + self.schema_url.as_deref() + } + + /// Returns the instrumentation scope attributes to associate with emitted telemetry. + #[inline] + pub fn attributes(&self) -> impl Iterator { + self.attributes.iter() + } +} + +/// Configuration options for [InstrumentationScope]. +/// +/// An instrumentation scope is a library or crate providing instrumentation. +/// It should be named to follow any naming conventions of the instrumented +/// library (e.g. 'middleware' for a web framework). +/// +/// Apart from the name, all other fields are optional. +/// +/// See the [instrumentation libraries] spec for more information. +/// +/// [instrumentation libraries]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/overview.md#instrumentation-libraries +#[derive(Debug)] +pub struct InstrumentationScopeBuilder { + name: Cow<'static, str>, + version: Option>, + schema_url: Option>, + attributes: Option>, +} + +impl InstrumentationScopeBuilder { + /// Configure the version for the instrumentation scope + /// + /// # Examples + /// + /// ``` + /// let scope = opentelemetry::InstrumentationScope::builder("my-crate") + /// .with_version("v0.1.0") + /// .build(); + /// ``` + pub fn with_version(mut self, version: impl Into>) -> Self { + self.version = Some(version.into()); + self + } + + /// Configure the Schema URL for the instrumentation scope + /// + /// # Examples + /// + /// ``` + /// let scope = opentelemetry::InstrumentationScope::builder("my-crate") + /// .with_schema_url("https://opentelemetry.io/schemas/1.17.0") + /// .build(); + /// ``` + pub fn with_schema_url(mut self, schema_url: impl Into>) -> Self { + self.schema_url = Some(schema_url.into()); + self + } + + /// Configure the attributes for the instrumentation scope + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::KeyValue; + /// + /// let scope = opentelemetry::InstrumentationScope::builder("my-crate") + /// .with_attributes([KeyValue::new("k", "v")]) + /// .build(); + /// ``` + pub fn with_attributes(mut self, attributes: I) -> Self + where + I: IntoIterator, + { + self.attributes = Some(attributes.into_iter().collect()); + self + } + + /// Create a new [InstrumentationScope] from this configuration + pub fn build(self) -> InstrumentationScope { + InstrumentationScope { + name: self.name, + version: self.version, + schema_url: self.schema_url, + attributes: self.attributes.unwrap_or_default(), + } + } +} + +#[cfg(test)] +mod tests { + use std::hash::{Hash, Hasher}; + + use crate::{InstrumentationScope, KeyValue}; + + use rand::random; + use std::collections::hash_map::DefaultHasher; + + #[test] + fn kv_float_equality() { + let kv1 = KeyValue::new("key", 1.0); + let kv2 = KeyValue::new("key", 1.0); + assert_eq!(kv1, kv2); + + let kv1 = KeyValue::new("key", 1.0); + let kv2 = KeyValue::new("key", 1.01); + assert_ne!(kv1, kv2); + + let kv1 = KeyValue::new("key", f64::NAN); + let kv2 = KeyValue::new("key", f64::NAN); + assert_ne!(kv1, kv2, "NAN is not equal to itself"); + + for float_val in [ + f64::INFINITY, + f64::NEG_INFINITY, + f64::MAX, + f64::MIN, + f64::MIN_POSITIVE, + ] + .iter() + { + let kv1 = KeyValue::new("key", *float_val); + let kv2 = KeyValue::new("key", *float_val); + assert_eq!(kv1, kv2); + } + + for _ in 0..100 { + let random_value = random::(); + let kv1 = KeyValue::new("key", random_value); + let kv2 = KeyValue::new("key", random_value); + assert_eq!(kv1, kv2); + } + } + + #[test] + fn kv_float_hash() { + for float_val in [ + f64::NAN, + f64::INFINITY, + f64::NEG_INFINITY, + f64::MAX, + f64::MIN, + f64::MIN_POSITIVE, + ] + .iter() + { + let kv1 = KeyValue::new("key", *float_val); + let kv2 = KeyValue::new("key", *float_val); + assert_eq!(hash_helper(&kv1), hash_helper(&kv2)); + } + + for _ in 0..100 { + let random_value = random::(); + let kv1 = KeyValue::new("key", random_value); + let kv2 = KeyValue::new("key", random_value); + assert_eq!(hash_helper(&kv1), hash_helper(&kv2)); + } + } + + fn hash_helper(item: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + item.hash(&mut hasher); + hasher.finish() + } + + #[test] + fn instrumentation_scope_equality() { + let scope1 = InstrumentationScope::builder("my-crate") + .with_version("v0.1.0") + .with_schema_url("https://opentelemetry.io/schemas/1.17.0") + .with_attributes([KeyValue::new("k", "v")]) + .build(); + let scope2 = InstrumentationScope::builder("my-crate") + .with_version("v0.1.0") + .with_schema_url("https://opentelemetry.io/schemas/1.17.0") + .with_attributes([KeyValue::new("k", "v")]) + .build(); + assert_eq!(scope1, scope2); + } + + #[test] + fn instrumentation_scope_equality_attributes_diff_order() { + let scope1 = InstrumentationScope::builder("my-crate") + .with_version("v0.1.0") + .with_schema_url("https://opentelemetry.io/schemas/1.17.0") + .with_attributes([KeyValue::new("k1", "v1"), KeyValue::new("k2", "v2")]) + .build(); + let scope2 = InstrumentationScope::builder("my-crate") + .with_version("v0.1.0") + .with_schema_url("https://opentelemetry.io/schemas/1.17.0") + .with_attributes([KeyValue::new("k2", "v2"), KeyValue::new("k1", "v1")]) + .build(); + assert_eq!(scope1, scope2); + + // assert hash are same for both scopes + let mut hasher1 = std::collections::hash_map::DefaultHasher::new(); + scope1.hash(&mut hasher1); + let mut hasher2 = std::collections::hash_map::DefaultHasher::new(); + scope2.hash(&mut hasher2); + assert_eq!(hasher1.finish(), hasher2.finish()); + } + + #[test] + fn instrumentation_scope_equality_different_attributes() { + let scope1 = InstrumentationScope::builder("my-crate") + .with_version("v0.1.0") + .with_schema_url("https://opentelemetry.io/schemas/1.17.0") + .with_attributes([KeyValue::new("k1", "v1"), KeyValue::new("k2", "v2")]) + .build(); + let scope2 = InstrumentationScope::builder("my-crate") + .with_version("v0.1.0") + .with_schema_url("https://opentelemetry.io/schemas/1.17.0") + .with_attributes([KeyValue::new("k2", "v3"), KeyValue::new("k4", "v5")]) + .build(); + assert_ne!(scope1, scope2); + + // assert hash are same for both scopes + let mut hasher1 = std::collections::hash_map::DefaultHasher::new(); + scope1.hash(&mut hasher1); + let mut hasher2 = std::collections::hash_map::DefaultHasher::new(); + scope2.hash(&mut hasher2); + assert_ne!(hasher1.finish(), hasher2.finish()); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/context.rs b/vendor/opentelemetry-rust/opentelemetry/src/context.rs new file mode 100644 index 00000000..636269a5 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/context.rs @@ -0,0 +1,1662 @@ +//! Execution-scoped context propagation. +//! +//! The `context` module provides mechanisms for propagating values across API boundaries and between +//! logically associated execution units. It enables cross-cutting concerns to access their data in-process +//! using a shared context object. +//! +//! # Main Types +//! +//! - [`Context`]: An immutable, execution-scoped collection of values. +//! + +use crate::otel_warn; +#[cfg(feature = "trace")] +use crate::trace::context::SynchronizedSpan; +use std::any::{Any, TypeId}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::fmt; +use std::hash::{BuildHasherDefault, Hasher}; +use std::marker::PhantomData; +use std::sync::Arc; +#[cfg(feature = "experimental_context_observer")] +use std::sync::OnceLock; + +#[cfg(feature = "futures")] +mod future_ext; + +#[cfg(feature = "futures")] +pub use future_ext::{FutureExt, WithContext}; + +#[cfg(feature = "experimental_context_observer")] +pub use context_observer::*; + +thread_local! { + static CURRENT_CONTEXT: RefCell = RefCell::new(ContextStack::default()); +} + +/// An execution-scoped collection of values. +/// +/// A [`Context`] is a propagation mechanism which carries execution-scoped +/// values across API boundaries and between logically associated execution +/// units. Cross-cutting concerns access their data in-process using the same +/// shared context object. +/// +/// [`Context`]s are immutable, and their write operations result in the creation +/// of a new context containing the original values and the new specified values. +/// +/// ## Context state +/// +/// Concerns can create and retrieve their local state in the current execution +/// state represented by a context through the [`get`] and [`with_value`] +/// methods. It is recommended to use application-specific types when storing new +/// context values to avoid unintentionally overwriting existing state. +/// +/// ## Managing the current context +/// +/// Contexts can be associated with the caller's current execution unit on a +/// given thread via the [`attach`] method, and previous contexts can be restored +/// by dropping the returned [`ContextGuard`]. Context can be nested, and will +/// restore their parent outer context when detached on drop. To access the +/// values of the context, a snapshot can be created via the [`Context::current`] +/// method. +/// +/// [`Context::current`]: Context::current() +/// [`get`]: Context::get() +/// [`with_value`]: Context::with_value() +/// [`attach`]: Context::attach() +/// +/// # Examples +/// +/// ``` +/// use opentelemetry::Context; +/// +/// // Application-specific `a` and `b` values +/// #[derive(Debug, PartialEq)] +/// struct ValueA(&'static str); +/// #[derive(Debug, PartialEq)] +/// struct ValueB(u64); +/// +/// let _outer_guard = Context::new().with_value(ValueA("a")).attach(); +/// +/// // Only value a has been set +/// let current = Context::current(); +/// assert_eq!(current.get::(), Some(&ValueA("a"))); +/// assert_eq!(current.get::(), None); +/// +/// { +/// let _inner_guard = Context::current_with_value(ValueB(42)).attach(); +/// // Both values are set in inner context +/// let current = Context::current(); +/// assert_eq!(current.get::(), Some(&ValueA("a"))); +/// assert_eq!(current.get::(), Some(&ValueB(42))); +/// } +/// +/// // Resets to only the `a` value when inner guard is dropped +/// let current = Context::current(); +/// assert_eq!(current.get::(), Some(&ValueA("a"))); +/// assert_eq!(current.get::(), None); +/// ``` +#[derive(Clone, Default)] +pub struct Context { + #[cfg(feature = "trace")] + pub(crate) span: Option>, + entries: Option>, + suppress_telemetry: bool, + /// A context's observer view of this [Context]. + /// + /// The main application for observers is to share information from the current context with + /// external readers (e.g. a full host eBPF profiler). In that case, the observer will attach + /// and detach some data on context events using its own mechanism. The shared data has no + /// reason to match our internal representation ([Context]). + /// + /// [observer_view] makes it possible to store the observer's view of the context alongside a + /// [Context] value, such that it can be attached and detached on context events. + #[cfg(feature = "experimental_context_observer")] + observer_view: Option>, +} + +type EntryMap = HashMap, BuildHasherDefault>; + +impl Context { + /// Creates an empty `Context`. + /// + /// The context is initially created with a capacity of 0, so it will not + /// allocate. Use [`with_value`] to create a new context that has entries. + /// + /// [`with_value`]: Context::with_value() + pub fn new() -> Self { + Context::default() + } + + /// Returns an immutable snapshot of the current thread's context. + /// + /// # Behavior During Context Drop + /// + /// When called from within a [`Drop`] implementation that is triggered by + /// a [`ContextGuard`] being dropped (e.g., when a [`Span`] is dropped as part + /// of context cleanup), this function returns **whatever context happens to be + /// current after the guard is popped**, not the context being dropped. + /// + /// **Important**: The returned context may be completely unrelated to the + /// context being dropped, as contexts can be activated in any order and are + /// not necessarily hierarchical. Do not rely on any relationship between them. + /// + /// This behavior is by design and prevents panics that would otherwise occur + /// from attempting to borrow the context while it's being mutably borrowed + /// for cleanup. See [issue #2871](https://github.com/open-telemetry/opentelemetry-rust/issues/2871) + /// for details. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::Context; + /// + /// #[derive(Debug, PartialEq)] + /// struct ValueA(&'static str); + /// + /// fn do_work() { + /// assert_eq!(Context::current().get(), Some(&ValueA("a"))); + /// } + /// + /// let _guard = Context::new().with_value(ValueA("a")).attach(); + /// do_work() + /// ``` + /// + /// [`Span`]: crate::trace::Span + pub fn current() -> Self { + Self::map_current(|cx| cx.clone()) + } + + /// Applies a function to the current context returning its value. + /// + /// This can be used to build higher performing algebraic expressions for + /// optionally creating a new context without the overhead of cloning the + /// current one and dropping it. + /// + /// Note: This function will panic if you attempt to attach another context + /// while the current one is still borrowed. + pub fn map_current(f: impl FnOnce(&Context) -> T) -> T { + CURRENT_CONTEXT.with(|cx| cx.borrow().map_current_cx(f)) + } + + /// Returns a clone of the current thread's context with the given value. + /// + /// This is a more efficient form of `Context::current().with_value(value)` + /// as it avoids the intermediate context clone. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::Context; + /// + /// // Given some value types defined in your application + /// #[derive(Debug, PartialEq)] + /// struct ValueA(&'static str); + /// #[derive(Debug, PartialEq)] + /// struct ValueB(u64); + /// + /// // You can create and attach context with the first value set to "a" + /// let _guard = Context::new().with_value(ValueA("a")).attach(); + /// + /// // And create another context based on the fist with a new value + /// let all_current_and_b = Context::current_with_value(ValueB(42)); + /// + /// // The second context now contains all the current values and the addition + /// assert_eq!(all_current_and_b.get::(), Some(&ValueA("a"))); + /// assert_eq!(all_current_and_b.get::(), Some(&ValueB(42))); + /// ``` + pub fn current_with_value(value: T) -> Self { + Self::map_current(|cx| cx.with_value(value)) + } + + /// Returns a reference to the entry for the corresponding value type. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::Context; + /// + /// // Given some value types defined in your application + /// #[derive(Debug, PartialEq)] + /// struct ValueA(&'static str); + /// #[derive(Debug, PartialEq)] + /// struct MyUser(); + /// + /// let cx = Context::new().with_value(ValueA("a")); + /// + /// // Values can be queried by type + /// assert_eq!(cx.get::(), Some(&ValueA("a"))); + /// + /// // And return none if not yet set + /// assert_eq!(cx.get::(), None); + /// ``` + pub fn get(&self) -> Option<&T> { + self.entries + .as_ref()? + .get(&TypeId::of::())? + .downcast_ref() + } + + /// Returns a copy of the context with the new value included. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::Context; + /// + /// // Given some value types defined in your application + /// #[derive(Debug, PartialEq)] + /// struct ValueA(&'static str); + /// #[derive(Debug, PartialEq)] + /// struct ValueB(u64); + /// + /// // You can create a context with the first value set to "a" + /// let cx_with_a = Context::new().with_value(ValueA("a")); + /// + /// // And create another context based on the fist with a new value + /// let cx_with_a_and_b = cx_with_a.with_value(ValueB(42)); + /// + /// // The first context is still available and unmodified + /// assert_eq!(cx_with_a.get::(), Some(&ValueA("a"))); + /// assert_eq!(cx_with_a.get::(), None); + /// + /// // The second context now contains both values + /// assert_eq!(cx_with_a_and_b.get::(), Some(&ValueA("a"))); + /// assert_eq!(cx_with_a_and_b.get::(), Some(&ValueB(42))); + /// ``` + pub fn with_value(&self, value: T) -> Self { + let entries = if let Some(current_entries) = &self.entries { + let mut inner_entries = (**current_entries).clone(); + inner_entries.insert(TypeId::of::(), Arc::new(value)); + Some(Arc::new(inner_entries)) + } else { + let mut entries = EntryMap::default(); + entries.insert(TypeId::of::(), Arc::new(value)); + Some(Arc::new(entries)) + }; + + Context { + entries, + #[cfg(feature = "trace")] + span: self.span.clone(), + suppress_telemetry: self.suppress_telemetry, + #[cfg(feature = "experimental_context_observer")] + observer_view: None, + } + .with_observer_view() + } + + /// Replaces the current context on this thread with this context. + /// + /// Dropping the returned [`ContextGuard`] will reset the current context to the + /// previous value. + /// + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::Context; + /// + /// #[derive(Debug, PartialEq)] + /// struct ValueA(&'static str); + /// + /// let my_cx = Context::new().with_value(ValueA("a")); + /// + /// // Set the current thread context + /// let cx_guard = my_cx.attach(); + /// assert_eq!(Context::current().get::(), Some(&ValueA("a"))); + /// + /// // Drop the guard to restore the previous context + /// drop(cx_guard); + /// assert_eq!(Context::current().get::(), None); + /// ``` + /// + /// Guards do not need to be explicitly dropped: + /// + /// ``` + /// use opentelemetry::Context; + /// + /// #[derive(Debug, PartialEq)] + /// struct ValueA(&'static str); + /// + /// fn my_function() -> String { + /// // attach a context the duration of this function. + /// let my_cx = Context::new().with_value(ValueA("a")); + /// // NOTE: a variable name after the underscore is **required** or rust + /// // will drop the guard, restoring the previous context _immediately_. + /// let _guard = my_cx.attach(); + /// + /// // anything happening in functions we call can still access my_cx... + /// my_other_function(); + /// + /// // returning from the function drops the guard, exiting the span. + /// return "Hello world".to_owned(); + /// } + /// + /// fn my_other_function() { + /// // ... + /// } + /// ``` + /// Sub-scopes may be created to limit the duration for which the span is + /// entered: + /// + /// ``` + /// use opentelemetry::Context; + /// + /// #[derive(Debug, PartialEq)] + /// struct ValueA(&'static str); + /// + /// let my_cx = Context::new().with_value(ValueA("a")); + /// + /// { + /// let _guard = my_cx.attach(); + /// + /// // the current context can access variables in + /// assert_eq!(Context::current().get::(), Some(&ValueA("a"))); + /// + /// // exiting the scope drops the guard, detaching the context. + /// } + /// + /// // this is back in the default empty context + /// assert_eq!(Context::current().get::(), None); + /// ``` + pub fn attach(self) -> ContextGuard { + let cx_id = CURRENT_CONTEXT.with(|cx| cx.borrow_mut().push(self)); + + ContextGuard { + cx_pos: cx_id, + _marker: PhantomData, + } + } + + /// Returns whether telemetry is suppressed in this context. + #[inline] + pub fn is_telemetry_suppressed(&self) -> bool { + self.suppress_telemetry + } + + /// Returns a new context with telemetry suppression enabled. + pub fn with_telemetry_suppressed(&self) -> Self { + Context { + entries: self.entries.clone(), + #[cfg(feature = "trace")] + span: self.span.clone(), + suppress_telemetry: true, + #[cfg(feature = "experimental_context_observer")] + observer_view: None, + } + .with_observer_view() + } + + /// Enters a scope where telemetry is suppressed. + /// + /// This method is specifically designed for OpenTelemetry components (like Exporters, + /// Processors etc.) to prevent generating recursive or self-referential + /// telemetry data when performing their own operations. + /// + /// Without suppression, we have a telemetry-induced-telemetry situation + /// where, operations like exporting telemetry could generate new telemetry + /// about the export process itself, potentially causing: + /// - Infinite telemetry feedback loops + /// - Excessive resource consumption + /// + /// This method: + /// 1. Takes the current context + /// 2. Creates a new context from current, with `suppress_telemetry` set to `true` + /// 3. Attaches it to the current thread + /// 4. Returns a guard that restores the previous context when dropped + /// + /// OTel SDK components would check `is_current_telemetry_suppressed()` before + /// generating new telemetry, but not end users. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::Context; + /// + /// // Example: Inside an exporter's implementation + /// fn example_export_function() { + /// // Prevent telemetry-generating operations from creating more telemetry + /// let _guard = Context::enter_telemetry_suppressed_scope(); + /// + /// // Verify suppression is active + /// assert_eq!(Context::is_current_telemetry_suppressed(), true); + /// + /// // Here you would normally perform operations that might generate telemetry + /// // but now they won't because the context has suppression enabled + /// } + /// + /// // Demonstrate the function + /// example_export_function(); + /// ``` + pub fn enter_telemetry_suppressed_scope() -> ContextGuard { + Self::map_current(|cx| cx.with_telemetry_suppressed()).attach() + } + + /// Returns whether telemetry is suppressed in the current context. + /// + /// This method is used by OpenTelemetry components to determine whether they should + /// generate new telemetry in the current execution context. It provides a performant + /// way to check the suppression state. + /// + /// End-users generally should not use this method directly, as it is primarily intended for + /// OpenTelemetry SDK components. + /// + /// + #[inline] + pub fn is_current_telemetry_suppressed() -> bool { + Self::map_current(|cx| cx.is_telemetry_suppressed()) + } + + // Initialize `self.observer_view` for a newly created context using the current context + // observer, if set. + #[cfg(feature = "experimental_context_observer")] + fn with_observer_view(mut self) -> Self { + if let Some(observer) = GlobalContextObserver::get() { + self.observer_view = observer.make_view(&self); + } + + self + } + + // No-op when the experimental_context_observer feature is not enabled. + #[cfg(not(feature = "experimental_context_observer"))] + const fn with_observer_view(self) -> Self { + self + } + + /// Returns the observer's view of this context, if any. + #[cfg(feature = "experimental_context_observer")] + #[inline] + pub fn observer_view(&self) -> &Option> { + &self.observer_view + } + + #[cfg(feature = "trace")] + pub(crate) fn current_with_synchronized_span(value: SynchronizedSpan) -> Self { + Self::map_current(|cx| { + Context { + span: Some(Arc::new(value)), + entries: cx.entries.clone(), + suppress_telemetry: cx.suppress_telemetry, + #[cfg(feature = "experimental_context_observer")] + observer_view: None, + } + .with_observer_view() + }) + } + + #[cfg(feature = "trace")] + pub(crate) fn with_synchronized_span(&self, value: SynchronizedSpan) -> Self { + Context { + span: Some(Arc::new(value)), + entries: self.entries.clone(), + suppress_telemetry: self.suppress_telemetry, + #[cfg(feature = "experimental_context_observer")] + observer_view: None, + } + .with_observer_view() + } +} + +impl fmt::Debug for Context { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut dbg = f.debug_struct("Context"); + + #[cfg(feature = "trace")] + let mut entries = self.entries.as_ref().map_or(0, |e| e.len()); + #[cfg(feature = "trace")] + { + if let Some(span) = &self.span { + dbg.field("span", &span.span_context()); + entries += 1; + } else { + dbg.field("span", &"None"); + } + } + #[cfg(not(feature = "trace"))] + let entries = self.entries.as_ref().map_or(0, |e| e.len()); + + dbg.field("entries count", &entries) + .field("suppress_telemetry", &self.suppress_telemetry) + .finish() + } +} + +/// A guard that resets the current context to the prior context when dropped. +#[derive(Debug)] +pub struct ContextGuard { + // The position of the context in the stack. This is used to pop the context. + cx_pos: u16, + // Ensure this type is !Send as it relies on thread locals + _marker: PhantomData<*const ()>, +} + +impl Drop for ContextGuard { + fn drop(&mut self) { + let id = self.cx_pos; + if id > ContextStack::BASE_POS && id < ContextStack::MAX_POS { + // Defer destructors that may call back into Context until after + // the mutable borrow of CURRENT_CONTEXT has been released. + let _to_drop = + CURRENT_CONTEXT.with(|context_stack| context_stack.borrow_mut().pop_id(id)); + } + } +} + +/// With TypeIds as keys, there's no need to hash them. They are already hashes +/// themselves, coming from the compiler. The IdHasher holds the u64 of +/// the TypeId, and then returns it, instead of doing any bit fiddling. +#[derive(Clone, Default, Debug)] +struct IdHasher(u64); + +impl Hasher for IdHasher { + fn write(&mut self, _: &[u8]) { + unreachable!("TypeId calls write_u64"); + } + + #[inline] + fn write_u64(&mut self, id: u64) { + self.0 = id; + } + + #[inline] + fn finish(&self) -> u64 { + self.0 + } +} + +/// A stack for keeping track of the [`Context`] instances that have been attached +/// to a thread. +/// +/// The stack allows for popping of contexts by position, which is used to do out +/// of order dropping of [`ContextGuard`] instances. Only when the top of the +/// stack is popped, the topmost [`Context`] is actually restored. +/// +/// The stack relies on the fact that it is thread local and that the +/// [`ContextGuard`] instances that are constructed using ids from it can't be +/// moved to other threads. That means that the ids are always valid and that +/// they are always within the bounds of the stack. +struct ContextStack { + /// This is the current [`Context`] that is active on this thread, and the top + /// of the [`ContextStack`]. It is always present, and if the `stack` is empty + /// it's an empty [`Context`]. + /// + /// Having this here allows for fast access to the current [`Context`]. + current_cx: Context, + /// A `stack` of the other contexts that have been attached to the thread. + stack: Vec>, + /// Ensure this type is !Send as it relies on thread locals + _marker: PhantomData<*const ()>, +} + +// Fields whose destructors must run after releasing the mutable +// borrow of CURRENT_CONTEXT. +#[derive(Default)] +struct ContextToDrop { + #[cfg(feature = "trace")] + _span: Option>, + _entries: Option>, + #[cfg(feature = "experimental_context_observer")] + _observer_view: Option>, +} + +impl From for ContextToDrop { + fn from(context: Context) -> Self { + Self { + #[cfg(feature = "trace")] + _span: context.span, + _entries: context.entries, + #[cfg(feature = "experimental_context_observer")] + _observer_view: context.observer_view, + } + } +} + +impl ContextStack { + const BASE_POS: u16 = 0; + const MAX_POS: u16 = u16::MAX; + const INITIAL_CAPACITY: usize = 8; + + #[inline(always)] + fn push(&mut self, cx: Context) -> u16 { + // The next id is the length of the `stack`, plus one since we have the + // top of the [`ContextStack`] as the `current_cx`. + let next_id = self.stack.len() + 1; + if next_id < ContextStack::MAX_POS.into() { + #[cfg(feature = "experimental_context_observer")] + if let Some(observer) = GlobalContextObserver::get() { + observer.on_context_enter(&self.current_cx, &cx); + } + + let current_cx = std::mem::replace(&mut self.current_cx, cx); + self.stack.push(Some(current_cx)); + next_id as u16 + } else { + // This is an overflow, log it and ignore it. + otel_warn!( + name: "Context.AttachFailed", + message = format!("Too many contexts. Max limit is {}. \ + Context::current() remains unchanged as this attach failed. \ + Dropping the returned ContextGuard will have no impact on Context::current().", + ContextStack::MAX_POS) + ); + ContextStack::MAX_POS + } + } + + #[inline(always)] + fn pop_id(&mut self, pos: u16) -> ContextToDrop { + if pos == ContextStack::BASE_POS || pos == ContextStack::MAX_POS { + // The empty context is always at the bottom of the [`ContextStack`] + // and cannot be popped, and the overflow position is invalid, so do + // nothing. + otel_warn!( + name: "Context.OutOfOrderDrop", + position = pos, + message = if pos == ContextStack::BASE_POS { + "Attempted to pop the base context which is not allowed" + } else { + "Attempted to pop the overflow position which is not allowed" + } + ); + + return ContextToDrop::default(); + } + let len: u16 = self.stack.len() as u16; + // Are we at the top of the [`ContextStack`]? + if pos == len { + // Shrink the stack if possible to clear out any out of order pops. + while let Some(None) = self.stack.last() { + _ = self.stack.pop(); + } + // Restore the previous context. This will always happen since the + // empty context is always at the bottom of the stack if the + // [`ContextStack`] is not empty. + if let Some(Some(next_cx)) = self.stack.pop() { + #[cfg(feature = "experimental_context_observer")] + if let Some(observer) = GlobalContextObserver::get() { + observer.on_context_exit(&self.current_cx, &next_cx); + } + + // Move fields with potentially reentrant destructors out of the borrow. + let old_cx = std::mem::replace(&mut self.current_cx, next_cx); + return old_cx.into(); + } + + ContextToDrop::default() + } else { + // This is an out of order pop. + if pos >= len { + // This is an invalid id, ignore it. + otel_warn!( + name: "Context.PopOutOfBounds", + position = pos, + stack_length = len, + message = "Attempted to pop beyond the end of the context stack" + ); + return ContextToDrop::default(); + } + // Remove the context and defer dropping its owned fields. + self.stack[pos as usize] + .take() + .map(ContextToDrop::from) + .unwrap_or_default() + } + } + + #[inline(always)] + fn map_current_cx(&self, f: impl FnOnce(&Context) -> T) -> T { + f(&self.current_cx) + } +} + +impl Default for ContextStack { + fn default() -> Self { + ContextStack { + current_cx: Context::default(), + stack: Vec::with_capacity(ContextStack::INITIAL_CAPACITY), + _marker: PhantomData, + } + } +} + +#[cfg(feature = "experimental_context_observer")] +mod context_observer { + use super::*; + + /// An observer trait for monitoring context transitions. + /// + /// Implementors of this trait can observe when a context is entered or exited, allowing for + /// custom logic to be executed during context switches. + /// + /// An observer-specific view can be attached to a [`Context`] via + /// [`Context::observer_view`]. A view is usually a different representation of the context + /// to be published through an alternative channel, but in essence it is arbitrary data computed + /// from the context. + /// + /// # Refcell already borrowed panic + /// + /// [crate::context] maintains thread-local, internal state in a `RefCell`. This implementation + /// detail leaks when the `experimental_context_observer` feature is enabled: + /// [Self::on_context_enter] and [Self::on_context_exit] are called from a point where the cell + /// is currently borrowed. If a [ContextObserver] implementation calls back into a + /// cell-borrowing function from the Context API, typically `Context::current()`, this will + /// result in a `Refcell already borrowed` panic. + /// + /// As general hygiene, with respect to the [Context] API, it is strongly advised to only use + /// simple getters and setters on the `from` and `to` context objects provided to the observer. + /// Do not call `Context::current()`, nor try to manually attach or detach context objects from + /// an observer. + /// + /// If you get Refcell-related panics within the [crate::context] module, this is the most + /// likely cause. + /// + /// # Example + /// + /// ``` + /// # #[cfg(feature = "experimental_context_observer")] + /// # { + /// use opentelemetry::Context; + /// use opentelemetry::context::{ContextObserver, ObserverContextView}; + /// use std::any::Any; + /// use std::sync::Arc; + /// + /// // An observer's own view of the context, carrying arbitrary data. + /// struct MyView { + /// correlation_id: u64, + /// } + /// + /// impl ObserverContextView for MyView { + /// fn as_any(&self) -> &dyn Any { + /// self + /// } + /// } + /// + /// struct Observer; + /// + /// # fn do_something(view: &MyView) { } + /// + /// impl ContextObserver for Observer { + /// fn on_context_enter(&self, from: &Context, to: &Context) { + /// let view = to.observer_view().as_ref().unwrap(); + /// do_something(view.as_any().downcast_ref::().unwrap()); + /// } + /// + /// fn on_context_exit(&self, from: &Context, to: &Context) { + /// let view = to + /// .observer_view() + /// .as_ref() + /// .unwrap() + /// .as_any() + /// .downcast_ref::() + /// .unwrap(); + /// assert_eq!(view.correlation_id, 42); + /// } + /// + /// fn make_view(&self, ctx: &Context) -> Option> { + /// Some(Arc::new(MyView { correlation_id: 42 })) + /// } + /// } + /// # } + /// ``` + pub trait ContextObserver { + /// Called when a context is entered, allowing observers to react to the transition + /// from one context (`from`) to another (`to`). + fn on_context_enter(&self, from: &Context, to: &Context); + + /// Called when a context is exited, allowing observers to react to the transition + /// from one context (`from`) to another (`to`). + fn on_context_exit(&self, from: &Context, to: &Context); + + /// Derive a fresh view for a newly created context. The provided `ctx` has an unitialized + /// view sets to `None`. + /// + /// The default implementation returns `None` for observers that don't use the view. + fn make_view(&self, _ctx: &Context) -> Option> { + None + } + } + + static GLOBAL_CONTEXT_OBSERVER: OnceLock> = + OnceLock::new(); + + /// A global observer for context transitions. + /// + /// This struct provides static methods to set and get a global context observer. + #[derive(Debug)] + pub struct GlobalContextObserver; + + impl GlobalContextObserver { + /// Sets the global context observer, logging a warning if it was already set. + pub fn set(observer: Arc) { + if GLOBAL_CONTEXT_OBSERVER.set(observer).is_err() { + otel_warn!( + name: "GlobalContextObserver.SetFailed", + message = "Global context observer was already set. Ignoring new observer." + ); + } + } + + /// Returns the global context observer. + pub(super) fn get<'a>() -> Option<&'a Arc> { + GLOBAL_CONTEXT_OBSERVER.get() + } + } + + /// A context's observer view of a [Context]. This is an arbitrary data structure. See + /// [ContextObserver]'s documentation for examples. + #[cfg(feature = "experimental_context_observer")] + pub trait ObserverContextView: Any + Send + Sync { + /// Returns this view as a `&dyn Any`, enabling downcasting back to the concrete type. + /// + /// This method is required because this crate's MSRV is below the Rust version (1.86) that + /// stabilized trait upcasting. Without it, callers below 1.86 couldn't upcast `&dyn + /// ObserverContextView` to `&dyn Any` to perform the downcast themselves. + /// + /// This is a work-around for the absence of trait upcasting before Rust 1.86. + /// + /// Implementors should simply return `self`: + /// + /// ```ignore + /// fn as_any(&self) -> &dyn std::any::Any { + /// self + /// } + /// ``` + fn as_any(&self) -> &dyn Any; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + use tokio::time::sleep; + + #[derive(Debug, PartialEq)] + struct ValueA(u64); + #[derive(Debug, PartialEq)] + struct ValueB(u64); + + struct ReadCurrentContextOnDrop; + + impl Drop for ReadCurrentContextOnDrop { + fn drop(&mut self) { + let _ = Context::current(); + } + } + + #[test] + fn context_value_drop_can_access_current_context() { + let guard = Context::new().with_value(ReadCurrentContextOnDrop).attach(); + + drop(guard); + } + + #[test] + fn out_of_order_context_value_drop_can_access_current_context() { + let outer_guard = Context::new().with_value(ReadCurrentContextOnDrop).attach(); + let inner_guard = Context::new().attach(); + + drop(outer_guard); + drop(inner_guard); + } + + #[test] + fn context_immutable() { + // start with Current, which should be an empty context + let cx = Context::current(); + assert_eq!(cx.get::(), None); + assert_eq!(cx.get::(), None); + + // with_value should return a new context, + // leaving the original context unchanged + let cx_new = cx.with_value(ValueA(1)); + + // cx should be unchanged + assert_eq!(cx.get::(), None); + assert_eq!(cx.get::(), None); + + // cx_new should contain the new value + assert_eq!(cx_new.get::(), Some(&ValueA(1))); + + // cx_new should be unchanged + let cx_newer = cx_new.with_value(ValueB(1)); + + // Cx and cx_new are unchanged + assert_eq!(cx.get::(), None); + assert_eq!(cx.get::(), None); + assert_eq!(cx_new.get::(), Some(&ValueA(1))); + assert_eq!(cx_new.get::(), None); + + // cx_newer should contain both values + assert_eq!(cx_newer.get::(), Some(&ValueA(1))); + assert_eq!(cx_newer.get::(), Some(&ValueB(1))); + } + + #[test] + fn nested_contexts() { + let _outer_guard = Context::new().with_value(ValueA(1)).attach(); + + // Only value `a` is set + let current = Context::current(); + assert_eq!(current.get(), Some(&ValueA(1))); + assert_eq!(current.get::(), None); + + { + let _inner_guard = Context::current_with_value(ValueB(42)).attach(); + // Both values are set in inner context + let current = Context::current(); + assert_eq!(current.get(), Some(&ValueA(1))); + assert_eq!(current.get(), Some(&ValueB(42))); + + assert!(Context::map_current(|cx| { + assert_eq!(cx.get(), Some(&ValueA(1))); + assert_eq!(cx.get(), Some(&ValueB(42))); + true + })); + } + + // Resets to only value `a` when inner guard is dropped + let current = Context::current(); + assert_eq!(current.get(), Some(&ValueA(1))); + assert_eq!(current.get::(), None); + + assert!(Context::map_current(|cx| { + assert_eq!(cx.get(), Some(&ValueA(1))); + assert_eq!(cx.get::(), None); + true + })); + } + + #[test] + fn overlapping_contexts() { + let outer_guard = Context::new().with_value(ValueA(1)).attach(); + + // Only value `a` is set + let current = Context::current(); + assert_eq!(current.get(), Some(&ValueA(1))); + assert_eq!(current.get::(), None); + + let inner_guard = Context::current_with_value(ValueB(42)).attach(); + // Both values are set in inner context + let current = Context::current(); + assert_eq!(current.get(), Some(&ValueA(1))); + assert_eq!(current.get(), Some(&ValueB(42))); + + assert!(Context::map_current(|cx| { + assert_eq!(cx.get(), Some(&ValueA(1))); + assert_eq!(cx.get(), Some(&ValueB(42))); + true + })); + + drop(outer_guard); + + // `inner_guard` is still alive so both `ValueA` and `ValueB` should still be accessible + let current = Context::current(); + assert_eq!(current.get(), Some(&ValueA(1))); + assert_eq!(current.get(), Some(&ValueB(42))); + + drop(inner_guard); + + // Both guards are dropped and neither value should be accessible. + let current = Context::current(); + assert_eq!(current.get::(), None); + assert_eq!(current.get::(), None); + } + + #[test] + fn too_many_contexts() { + let mut guards: Vec = Vec::with_capacity(ContextStack::MAX_POS as usize); + let stack_max_pos = ContextStack::MAX_POS as u64; + // Fill the stack up until the last position + for i in 1..stack_max_pos { + let cx_guard = Context::current().with_value(ValueB(i)).attach(); + assert_eq!(Context::current().get(), Some(&ValueB(i))); + assert_eq!(cx_guard.cx_pos, i as u16); + guards.push(cx_guard); + } + // Let's overflow the stack a couple of times + for _ in 0..16 { + let cx_guard = Context::current().with_value(ValueA(1)).attach(); + assert_eq!(cx_guard.cx_pos, ContextStack::MAX_POS); + assert_eq!(Context::current().get::(), None); + assert_eq!(Context::current().get(), Some(&ValueB(stack_max_pos - 1))); + guards.push(cx_guard); + } + // Drop the overflow contexts + for _ in 0..16 { + guards.pop(); + assert_eq!(Context::current().get::(), None); + assert_eq!(Context::current().get(), Some(&ValueB(stack_max_pos - 1))); + } + // Drop one more so we can add a new one + guards.pop(); + assert_eq!(Context::current().get::(), None); + assert_eq!(Context::current().get(), Some(&ValueB(stack_max_pos - 2))); + // Push a new context and see that it works + let cx_guard = Context::current().with_value(ValueA(2)).attach(); + assert_eq!(cx_guard.cx_pos, ContextStack::MAX_POS - 1); + assert_eq!(Context::current().get(), Some(&ValueA(2))); + assert_eq!(Context::current().get(), Some(&ValueB(stack_max_pos - 2))); + guards.push(cx_guard); + // Let's overflow the stack a couple of times again + for _ in 0..16 { + let cx_guard = Context::current().with_value(ValueA(1)).attach(); + assert_eq!(cx_guard.cx_pos, ContextStack::MAX_POS); + assert_eq!(Context::current().get::(), Some(&ValueA(2))); + assert_eq!(Context::current().get(), Some(&ValueB(stack_max_pos - 2))); + guards.push(cx_guard); + } + } + + /// Tests that a new ContextStack is created with the correct initial capacity. + #[test] + fn test_initial_capacity() { + let stack = ContextStack::default(); + assert_eq!(stack.stack.capacity(), ContextStack::INITIAL_CAPACITY); + } + + /// Tests that map_current_cx correctly accesses the current context. + #[test] + fn test_map_current_cx() { + let mut stack = ContextStack::default(); + let test_value = ValueA(42); + stack.current_cx = Context::new().with_value(test_value); + + let result = stack.map_current_cx(|cx| { + assert_eq!(cx.get::(), Some(&ValueA(42))); + true + }); + assert!(result); + } + + /// Tests popping contexts in non-sequential order. + #[test] + fn test_pop_id_out_of_order() { + let mut stack = ContextStack::default(); + + // Push three contexts + let cx1 = Context::new().with_value(ValueA(1)); + let cx2 = Context::new().with_value(ValueA(2)); + let cx3 = Context::new().with_value(ValueA(3)); + + let id1 = stack.push(cx1); + let id2 = stack.push(cx2); + let id3 = stack.push(cx3); + + // Pop middle context first - should not affect current context + stack.pop_id(id2); + assert_eq!(stack.current_cx.get::(), Some(&ValueA(3))); + assert_eq!(stack.stack.len(), 3); // Length unchanged for middle pops + + // Pop last context - should restore previous valid context + stack.pop_id(id3); + assert_eq!(stack.current_cx.get::(), Some(&ValueA(1))); + assert_eq!(stack.stack.len(), 1); + + // Pop first context - should restore to empty state + stack.pop_id(id1); + assert_eq!(stack.current_cx.get::(), None); + assert_eq!(stack.stack.len(), 0); + } + + /// Tests edge cases in context stack operations. IRL these should log + /// warnings, and definitely not panic. + #[test] + fn test_pop_id_edge_cases() { + let mut stack = ContextStack::default(); + + // Test popping BASE_POS - should be no-op + stack.pop_id(ContextStack::BASE_POS); + assert_eq!(stack.stack.len(), 0); + + // Test popping MAX_POS - should be no-op + stack.pop_id(ContextStack::MAX_POS); + assert_eq!(stack.stack.len(), 0); + + // Test popping invalid position - should be no-op + stack.pop_id(1000); + assert_eq!(stack.stack.len(), 0); + + // Test popping from empty stack - should be safe + stack.pop_id(1); + assert_eq!(stack.stack.len(), 0); + } + + /// Tests stack behavior when reaching maximum capacity. + /// Once we push beyond this point, we should end up with a context + /// that points _somewhere_, but mutating it should not affect the current + /// active context. + #[test] + fn test_push_overflow() { + let mut stack = ContextStack::default(); + let max_pos = ContextStack::MAX_POS as usize; + + // Fill stack up to max position + for i in 0..max_pos { + let cx = Context::new().with_value(ValueA(i as u64)); + let id = stack.push(cx); + assert_eq!(id, (i + 1) as u16); + } + + // Try to push beyond capacity + let cx = Context::new().with_value(ValueA(max_pos as u64)); + let id = stack.push(cx); + assert_eq!(id, ContextStack::MAX_POS); + + // Verify current context remains unchanged after overflow + assert_eq!( + stack.current_cx.get::(), + Some(&ValueA((max_pos - 2) as u64)) + ); + } + + /// Tests that: + /// 1. Parent context values are properly propagated to async operations + /// 2. Values added during async operations do not affect parent context + #[tokio::test] + async fn test_async_context_propagation() { + // A nested async operation we'll use to test propagation + async fn nested_operation() { + // Verify we can see the parent context's value + assert_eq!( + Context::current().get::(), + Some(&ValueA(42)), + "Parent context value should be available in async operation" + ); + + // Create new context + let cx_with_both = Context::current() + .with_value(ValueA(43)) // override ValueA + .with_value(ValueB(24)); // Add new ValueB + + // Run nested async operation with both values + async { + // Verify both values are available + assert_eq!( + Context::current().get::(), + Some(&ValueA(43)), + "Parent value should still be available after adding new value" + ); + assert_eq!( + Context::current().get::(), + Some(&ValueB(24)), + "New value should be available in async operation" + ); + + // Do some async work to simulate real-world scenario + sleep(Duration::from_millis(10)).await; + + // Values should still be available after async work + assert_eq!( + Context::current().get::(), + Some(&ValueA(43)), + "Parent value should persist across await points" + ); + assert_eq!( + Context::current().get::(), + Some(&ValueB(24)), + "New value should persist across await points" + ); + } + .with_context(cx_with_both) + .await; + } + + // Set up initial context with ValueA + let parent_cx = Context::new().with_value(ValueA(42)); + + // Create and run async operation with the parent context explicitly propagated + nested_operation().with_context(parent_cx.clone()).await; + + // After async operation completes: + // 1. Parent context should be unchanged + assert_eq!( + parent_cx.get::(), + Some(&ValueA(42)), + "Parent context should be unchanged" + ); + assert_eq!( + parent_cx.get::(), + None, + "Parent context should not see values added in async operation" + ); + + // 2. Current context should be back to default + assert_eq!( + Context::current().get::(), + None, + "Current context should be back to default" + ); + assert_eq!( + Context::current().get::(), + None, + "Current context should not have async operation's values" + ); + } + + /// + /// Tests that unnatural parent->child relationships in nested async + /// operations behave properly. + /// + #[tokio::test] + async fn test_out_of_order_context_detachment_futures() { + // This function returns a future, but doesn't await it + // It will complete before the future that it creates. + async fn create_a_future() -> impl std::future::Future { + // Create a future that will do some work, referencing our current + // context, but don't await it. + async { + assert_eq!(Context::current().get::(), Some(&ValueA(42))); + + // Longer work + sleep(Duration::from_millis(50)).await; + } + .with_context(Context::current()) + } + + // Create our base context + let parent_cx = Context::new().with_value(ValueA(42)); + + // await our nested function, which will create and detach a context + let future = create_a_future().with_context(parent_cx).await; + + // Execute the future. The future that created it is long gone, but this shouldn't + // cause issues. + let _a = future.await; + + // Nothing terrible (e.g., panics!) should happen, and we should definitely not have any + // values attached to our current context that were set in the nested operations. + assert_eq!(Context::current().get::(), None); + assert_eq!(Context::current().get::(), None); + } + + #[test] + fn test_is_telemetry_suppressed() { + // Default context has suppression disabled + let cx = Context::new(); + assert!(!cx.is_telemetry_suppressed()); + + // With suppression enabled + let suppressed = cx.with_telemetry_suppressed(); + assert!(suppressed.is_telemetry_suppressed()); + } + + #[test] + fn test_with_telemetry_suppressed() { + // Start with a normal context + let cx = Context::new(); + assert!(!cx.is_telemetry_suppressed()); + + // Create a suppressed context + let suppressed = cx.with_telemetry_suppressed(); + + // Original should remain unchanged + assert!(!cx.is_telemetry_suppressed()); + + // New context should be suppressed + assert!(suppressed.is_telemetry_suppressed()); + + // Test with values to ensure they're preserved + let cx_with_value = cx.with_value(ValueA(42)); + let suppressed_with_value = cx_with_value.with_telemetry_suppressed(); + + assert!(!cx_with_value.is_telemetry_suppressed()); + assert!(suppressed_with_value.is_telemetry_suppressed()); + assert_eq!(suppressed_with_value.get::(), Some(&ValueA(42))); + } + + #[test] + fn test_enter_telemetry_suppressed_scope() { + // Ensure we start with a clean context + let _reset_guard = Context::new().attach(); + + // Default context should not be suppressed + assert!(!Context::is_current_telemetry_suppressed()); + + // Add an entry to the current context + let cx_with_value = Context::current().with_value(ValueA(42)); + let _guard_with_value = cx_with_value.attach(); + + // Verify the entry is present and context is not suppressed + assert_eq!(Context::current().get::(), Some(&ValueA(42))); + assert!(!Context::is_current_telemetry_suppressed()); + + // Enter a suppressed scope + { + let _guard = Context::enter_telemetry_suppressed_scope(); + + // Verify suppression is active and the entry is still present + assert!(Context::is_current_telemetry_suppressed()); + assert!(Context::current().is_telemetry_suppressed()); + assert_eq!(Context::current().get::(), Some(&ValueA(42))); + } + + // After guard is dropped, should be back to unsuppressed and entry should still be present + assert!(!Context::is_current_telemetry_suppressed()); + assert!(!Context::current().is_telemetry_suppressed()); + assert_eq!(Context::current().get::(), Some(&ValueA(42))); + } + + #[test] + fn test_nested_suppression_scopes() { + // Ensure we start with a clean context + let _reset_guard = Context::new().attach(); + + // Default context should not be suppressed + assert!(!Context::is_current_telemetry_suppressed()); + + // First level suppression + { + let _outer = Context::enter_telemetry_suppressed_scope(); + assert!(Context::is_current_telemetry_suppressed()); + + // Second level. This component is unaware of Suppression, + // and just attaches a new context. Since it is from current, + // it'll already have suppression enabled. + { + let _inner = Context::current().with_value(ValueA(1)).attach(); + assert!(Context::is_current_telemetry_suppressed()); + assert_eq!(Context::current().get::(), Some(&ValueA(1))); + } + + // Another scenario. This component is unaware of Suppression, + // and just attaches a new context, not from Current. Since it is + // not from current it will not have suppression enabled. + { + let _inner = Context::new().with_value(ValueA(1)).attach(); + assert!(!Context::is_current_telemetry_suppressed()); + assert_eq!(Context::current().get::(), Some(&ValueA(1))); + } + + // Still suppressed after inner scope + assert!(Context::is_current_telemetry_suppressed()); + } + + // Back to unsuppressed + assert!(!Context::is_current_telemetry_suppressed()); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + async fn test_async_suppression() { + async fn nested_operation() { + assert!(Context::is_current_telemetry_suppressed()); + + let cx_with_additional_value = Context::current().with_value(ValueB(24)); + + async { + assert_eq!( + Context::current().get::(), + Some(&ValueB(24)), + "Parent value should still be available after adding new value" + ); + assert!(Context::is_current_telemetry_suppressed()); + + // Do some async work to simulate real-world scenario + sleep(Duration::from_millis(10)).await; + + // Values should still be available after async work + assert_eq!( + Context::current().get::(), + Some(&ValueB(24)), + "Parent value should still be available after adding new value" + ); + assert!(Context::is_current_telemetry_suppressed()); + } + .with_context(cx_with_additional_value) + .await; + } + + // Set up suppressed context, but don't attach it to current + let suppressed_parent = Context::new().with_telemetry_suppressed(); + // Current should not be suppressed as we haven't attached it + assert!(!Context::is_current_telemetry_suppressed()); + + // Create and run async operation with the suppressed context explicitly propagated + nested_operation() + .with_context(suppressed_parent.clone()) + .await; + + // After async operation completes: + // Suppression should be active + assert!(suppressed_parent.is_telemetry_suppressed()); + + // Current should still be not suppressed + assert!(!Context::is_current_telemetry_suppressed()); + } +} + +#[cfg(all(test, feature = "experimental_context_observer"))] +mod observer_tests { + use super::*; + use std::cell::{Cell, RefCell}; + use std::sync::Arc; + + #[derive(Debug, PartialEq)] + struct V(u64); + + #[derive(Debug, PartialEq, Clone)] + enum Event { + Enter { from: Option, to: Option }, + Exit { from: Option, to: Option }, + } + + thread_local! { + static EVENTS: RefCell> = const { RefCell::new(Vec::new()) }; + } + + fn value_of(cx: &Context) -> Option { + cx.get::().map(|v| v.0) + } + + // When enabled, on each transition it records the event into a thread-local buffer, and on + // enter it also installs an observer view on the entered context and bumps the thread-local + // `VIEWS_CREATED` counter. See `observer_installs_and_reuses_view`. + // + // The observer is a process-global singleton, so its callbacks fire for context activity on + // *every* thread across the whole test binary once installed. To keep that cheap and isolated, + // all of its work is gated behind the per-thread `ENABLE_TEST_OBSERVER` switch: threads that + // didn't opt in (via `setup()`) get an inert observer. + struct RecordingObserver; + + impl ContextObserver for RecordingObserver { + fn on_context_enter(&self, from: &Context, to: &Context) { + if !ENABLE_TEST_OBSERVER.with(Cell::get) { + return; + } + + EVENTS.with(|e| { + e.borrow_mut().push(Event::Enter { + from: value_of(from), + to: value_of(to), + }) + }); + } + + fn on_context_exit(&self, from: &Context, to: &Context) { + if !ENABLE_TEST_OBSERVER.with(Cell::get) { + return; + } + + EVENTS.with(|e| { + e.borrow_mut().push(Event::Exit { + from: value_of(from), + to: value_of(to), + }) + }); + } + + // On context enter, install the observer's view if this context doesn't have one yet, or reuse + // the existing one otherwise. The view is the context's `V` value (or 0 for a value-less + // context). + fn make_view(&self, cx: &Context) -> Option> { + let correlation_id = value_of(cx).unwrap_or(0); + VIEWS_CREATED.with(|c| c.set(c.get() + 1)); + Some(Arc::new(MyView { correlation_id })) + } + } + + thread_local! { + // Master switch for the observer per thread. Enabled for the duration of a test that cares, + // and left false everywhere else, so we don't slow down tests unrelated to the observer. + // See `setup`. + static ENABLE_TEST_OBSERVER: Cell = const { Cell::new(false) }; + // Counts how many distinct views the observer created on the current thread. + static VIEWS_CREATED: Cell = const { Cell::new(0) }; + } + + // Recovers the correlation id stored in a context's observer view, if any. + fn view_id(cx: &Context) -> Option { + cx.observer_view() + .as_ref() + .and_then(|v| v.as_any().downcast_ref::()) + .map(|v| v.correlation_id) + } + + // Holds the base context guard and disables the observer on this thread when dropped. The + // struct's own `Drop` runs before its fields, so the flag is cleared before the base context is + // popped, which is thus not observed (symmetrically to `setup`, which installs the base context + // before registering/enabling the observer). + struct ObserverTestGuard { + _cx: ContextGuard, + } + + impl Drop for ObserverTestGuard { + fn drop(&mut self) { + ENABLE_TEST_OBSERVER.with(|e| e.set(false)); + } + } + + // Enables the observer on this thread, establishes a clean base context, and clears any + // previously recorded events, so the assertions below only see transitions this test + // triggers. Dropping the returned guard disables the observer again. + fn setup_observer() -> ObserverTestGuard { + // Idempotent across tests: only the first call installs the observer, but every test + // uses the same `RecordingObserver`, so a lost race is harmless. + GlobalContextObserver::set(Arc::new(RecordingObserver)); + ENABLE_TEST_OBSERVER.with(|e| e.set(true)); + let guard = Context::new().attach(); + EVENTS.with(|e| e.borrow_mut().clear()); + ObserverTestGuard { _cx: guard } + } + + #[test] + fn global_observer_records_enter_and_exit() { + let _clean = setup_observer(); + + { + let _g = Context::current().with_value(V(1)).attach(); + // Entering fires immediately: from the clean base (no value) to `V(1)`. + EVENTS.with(|e| { + assert_eq!( + *e.borrow(), + vec![Event::Enter { + from: None, + to: Some(1) + }] + ) + }); + } + + // Dropping the (context) guard restores the base and fires the matching exit event. + EVENTS.with(|e| { + assert_eq!( + *e.borrow(), + vec![ + Event::Enter { + from: None, + to: Some(1) + }, + Event::Exit { + from: Some(1), + to: None + } + ] + ) + }); + } + + #[test] + fn global_observer_records_nested_transitions() { + let _clean = setup_observer(); + + let g1 = Context::current().with_value(V(1)).attach(); + let g2 = Context::current().with_value(V(2)).attach(); + drop(g2); + drop(g1); + + EVENTS.with(|e| { + assert_eq!( + *e.borrow(), + vec![ + Event::Enter { + from: None, + to: Some(1) + }, + Event::Enter { + from: Some(1), + to: Some(2) + }, + Event::Exit { + from: Some(2), + to: Some(1) + }, + Event::Exit { + from: Some(1), + to: None + }, + ] + ) + }); + } + + // An observer view carrying arbitrary data, unrelated to the internal `Context` + // representation. + struct MyView { + correlation_id: u64, + } + + impl ObserverContextView for MyView { + fn as_any(&self) -> &dyn Any { + self + } + } + + // A realistic use of the observer view: the global observer installs its own view on each + // context the first time that context is entered, and reuses it on re-entry. The + // thread-local counter checks that exactly one view is created per distinct context. + #[test] + fn observer_installs_and_reuses_view() { + let _clean = setup_observer(); + + // Reset the counter for the work below. + VIEWS_CREATED.with(|c| c.set(0)); + + // Entering two distinct contexts installs a fresh view for each, derived from its value. + let g1 = Context::current().with_value(V(1)).attach(); + assert_eq!(view_id(&Context::current()), Some(1)); + + let g2 = Context::current().with_value(V(2)).attach(); + assert_eq!(view_id(&Context::current()), Some(2)); + + assert_eq!(VIEWS_CREATED.with(Cell::get), 2); + + // Re-entering an already-observed context must reuse its view, not create a new one. + // `Context::current()` clones the current context, carrying along its already-set view. + let observed = Context::current(); + let g3 = observed.attach(); + + assert_eq!(view_id(&Context::current()), Some(2)); + assert_eq!( + VIEWS_CREATED.with(Cell::get), + 2, + "re-entering an observed context must not create a new view" + ); + + drop(g3); + drop(g2); + drop(g1); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/context/future_ext.rs b/vendor/opentelemetry-rust/opentelemetry/src/context/future_ext.rs new file mode 100644 index 00000000..0321b0be --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/context/future_ext.rs @@ -0,0 +1,108 @@ +use crate::Context; +use futures_core::Stream; +use futures_sink::Sink; +use pin_project_lite::pin_project; +use std::pin::Pin; +use std::task::Context as TaskContext; +use std::task::Poll; +impl FutureExt for T {} + +impl std::future::Future for WithContext { + type Output = T::Output; + + fn poll(self: Pin<&mut Self>, task_cx: &mut TaskContext<'_>) -> Poll { + let this = self.project(); + let _guard = this.otel_cx.clone().attach(); + + this.inner.poll(task_cx) + } +} + +impl Stream for WithContext { + type Item = T::Item; + + fn poll_next(self: Pin<&mut Self>, task_cx: &mut TaskContext<'_>) -> Poll> { + let this = self.project(); + let _guard = this.otel_cx.clone().attach(); + T::poll_next(this.inner, task_cx) + } +} + +pin_project! { + /// A future, stream, or sink that has an associated context. + #[derive(Clone, Debug)] + pub struct WithContext { + #[pin] + inner: T, + otel_cx: Context, + } +} + +impl> Sink for WithContext +where + T: Sink, +{ + type Error = T::Error; + + fn poll_ready( + self: Pin<&mut Self>, + task_cx: &mut TaskContext<'_>, + ) -> Poll> { + let this = self.project(); + let _guard = this.otel_cx.clone().attach(); + T::poll_ready(this.inner, task_cx) + } + + fn start_send(self: Pin<&mut Self>, item: I) -> Result<(), Self::Error> { + let this = self.project(); + let _guard = this.otel_cx.clone().attach(); + T::start_send(this.inner, item) + } + + fn poll_flush( + self: Pin<&mut Self>, + task_cx: &mut TaskContext<'_>, + ) -> Poll> { + let this = self.project(); + let _guard = this.otel_cx.clone().attach(); + T::poll_flush(this.inner, task_cx) + } + + fn poll_close( + self: Pin<&mut Self>, + task_cx: &mut TaskContext<'_>, + ) -> Poll> { + let this = self.project(); + let _enter = this.otel_cx.clone().attach(); + T::poll_close(this.inner, task_cx) + } +} + +/// Extension trait allowing futures, streams, and sinks to be traced with a span. +pub trait FutureExt: Sized { + /// Attaches the provided [`Context`] to this type, returning a `WithContext` + /// wrapper. + /// + /// When the wrapped type is a future, stream, or sink, the attached context + /// will be set as current while it is being polled. + /// + /// [`Context`]: Context + fn with_context(self, otel_cx: Context) -> WithContext { + WithContext { + inner: self, + otel_cx, + } + } + + /// Attaches the current [`Context`] to this type, returning a `WithContext` + /// wrapper. + /// + /// When the wrapped type is a future, stream, or sink, the attached context + /// will be set as the default while it is being polled. + /// + /// [`Context`]: Context + fn with_current_context(self) -> WithContext { + let otel_cx = Context::current(); + self.with_context(otel_cx) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/global/internal_logging.rs b/vendor/opentelemetry-rust/opentelemetry/src/global/internal_logging.rs new file mode 100644 index 00000000..051b94cf --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/global/internal_logging.rs @@ -0,0 +1,227 @@ +#![allow(unused_macros)] +/// +/// **Note**: These macros (`otel_info!`, `otel_warn!`, `otel_debug!`, and `otel_error!`) are intended to be used +/// **internally within OpenTelemetry code** or for **custom exporters, processors and other plugins**. They are not designed +/// for general application logging and should not be used for that purpose. +/// +/// When running tests with `--nocapture`, these macros will print their output to stdout. This is useful for debugging +/// test failures and understanding the flow of operations during testing. +/// +/// Macro for logging informational messages in OpenTelemetry. +/// +/// # Fields: +/// - `name`: The operation or action being logged. +/// - Additional optional key-value pairs can be passed as attributes. +/// +/// # Example: +/// ```rust +/// use opentelemetry::otel_info; +/// otel_info!(name: "sdk_start", version = "1.0.0", schema_url = "http://example.com"); +/// ``` +/// +// TODO: Remove `name` attribute duplication in logging macros below once `tracing::Fmt` supports displaying `name`. +// See issue: https://github.com/tokio-rs/tracing/issues/2774 +#[macro_export] +macro_rules! otel_info { + (name: $name:expr $(,)?) => { + #[cfg(feature = "internal-logs")] + { + $crate::_private::info!( name: $name, target: env!("CARGO_PKG_NAME"), name = $name); + } + + #[cfg(test)] + { + print!("otel_info: name={}\n", $name); + } + + #[cfg(all(not(feature = "internal-logs"), not(test)))] + { + let _ = $name; // Compiler will optimize this out as it's unused. + } + }; + (name: $name:expr, $($key:tt = $value:expr),+ $(,)?) => { + #[cfg(feature = "internal-logs")] + { + $crate::_private::info!(name: $name, target: env!("CARGO_PKG_NAME"), name = $name, $($key = $value),+); + } + + #[cfg(test)] + { + print!("otel_info: name={}", $name); + $( + print!(", {}={}", stringify!($key), $value); + )+ + print!("\n"); + } + + #[cfg(all(not(feature = "internal-logs"), not(test)))] + { + let _ = ($name, $($value),+); // Compiler will optimize this out as it's unused. + } + }; +} + +/// Macro for logging warning messages in OpenTelemetry. +/// +/// # Fields: +/// - `name`: The operation or action being logged. +/// - Additional optional key-value pairs can be passed as attributes. +/// +/// # Example: +/// ```rust +/// use opentelemetry::otel_warn; +/// otel_warn!(name: "export_warning", error_code = 404, version = "1.0.0"); +/// ``` +#[macro_export] +macro_rules! otel_warn { + (name: $name:expr $(,)?) => { + #[cfg(feature = "internal-logs")] + { + $crate::_private::warn!(name: $name, target: env!("CARGO_PKG_NAME"), name = $name); + } + + #[cfg(test)] + { + print!("otel_warn: name={}\n", $name); + } + + #[cfg(all(not(feature = "internal-logs"), not(test)))] + { + let _ = $name; // Compiler will optimize this out as it's unused. + } + }; + (name: $name:expr, $($key:tt = $value:expr),+ $(,)?) => { + #[cfg(feature = "internal-logs")] + { + $crate::_private::warn!(name: $name, + target: env!("CARGO_PKG_NAME"), + name = $name, + $($key = { + $value + }),+, + ) + } + + #[cfg(test)] + { + print!("otel_warn: name={}", $name); + $( + print!(", {}={}", stringify!($key), $value); + )+ + print!("\n"); + } + + #[cfg(all(not(feature = "internal-logs"), not(test)))] + { + let _ = ($name, $($value),+); // Compiler will optimize this out as it's unused. + } + }; +} + +/// Macro for logging debug messages in OpenTelemetry. +/// +/// # Fields: +/// - `name`: The operation or action being logged. +/// - Additional optional key-value pairs can be passed as attributes. +/// +/// # Example: +/// ```rust +/// use opentelemetry::otel_debug; +/// otel_debug!(name: "debug_operation", debug_level = "high", version = "1.0.0"); +/// ``` +#[macro_export] +macro_rules! otel_debug { + (name: $name:expr $(,)?) => { + #[cfg(feature = "internal-logs")] + { + $crate::_private::debug!(name: $name, target: env!("CARGO_PKG_NAME"), name = $name); + } + + #[cfg(test)] + { + print!("otel_debug: name={}\n", $name); + } + + #[cfg(all(not(feature = "internal-logs"), not(test)))] + { + let _ = $name; // Compiler will optimize this out as it's unused. + } + }; + (name: $name:expr, $($key:tt = $value:expr),+ $(,)?) => { + #[cfg(feature = "internal-logs")] + { + $crate::_private::debug!(name: $name, target: env!("CARGO_PKG_NAME"), name = $name, $($key = $value),+); + } + + #[cfg(test)] + { + print!("otel_debug: name={}", $name); + $( + print!(", {}={}", stringify!($key), $value); + )+ + print!("\n"); + } + + #[cfg(all(not(feature = "internal-logs"), not(test)))] + { + let _ = ($name, $($value),+); // Compiler will optimize this out as it's unused. + } + }; +} + +/// Macro for logging error messages in OpenTelemetry. +/// +/// # Fields: +/// - `name`: The operation or action being logged. +/// - Additional optional key-value pairs can be passed as attributes. +/// +/// # Example: +/// ```rust +/// use opentelemetry::otel_error; +/// otel_error!(name: "export_failure", error_code = 500, version = "1.0.0"); +/// ``` +#[macro_export] +macro_rules! otel_error { + (name: $name:expr $(,)?) => { + #[cfg(feature = "internal-logs")] + { + $crate::_private::error!(name: $name, target: env!("CARGO_PKG_NAME"), name = $name); + } + + #[cfg(test)] + { + print!("otel_error: name={}\n", $name); + } + + #[cfg(all(not(feature = "internal-logs"), not(test)))] + { + let _ = $name; // Compiler will optimize this out as it's unused. + } + }; + (name: $name:expr, $($key:tt = $value:expr),+ $(,)?) => { + #[cfg(feature = "internal-logs")] + { + $crate::_private::error!(name: $name, + target: env!("CARGO_PKG_NAME"), + name = $name, + $($key = { + $value + }),+, + ) + } + + #[cfg(test)] + { + print!("otel_error: name={}", $name); + $( + print!(", {}={}", stringify!($key), $value); + )+ + print!("\n"); + } + + #[cfg(all(not(feature = "internal-logs"), not(test)))] + { + let _ = ($name, $($value),+); // Compiler will optimize this out as it's unused. + } + }; +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/global/metrics.rs b/vendor/opentelemetry-rust/opentelemetry/src/global/metrics.rs new file mode 100644 index 00000000..5824e36e --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/global/metrics.rs @@ -0,0 +1,82 @@ +use crate::metrics::{self, Meter, MeterProvider}; +use crate::{otel_error, otel_info, InstrumentationScope}; +use std::sync::{Arc, OnceLock, RwLock}; + +type GlobalMeterProvider = Arc; + +/// The global `MeterProvider` singleton. +static GLOBAL_METER_PROVIDER: OnceLock> = OnceLock::new(); + +#[inline] +fn global_meter_provider() -> &'static RwLock { + GLOBAL_METER_PROVIDER + .get_or_init(|| RwLock::new(Arc::new(crate::metrics::noop::NoopMeterProvider::new()))) +} + +/// Sets the given [`MeterProvider`] instance as the current global meter +/// provider. +/// Libraries should NOT call this function. It is intended for applications/executables. +/// +/// **NOTE:** This function should be called before getting [`Meter`] instances via [`meter()`] or [`meter_with_scope()`]. Otherwise, you could get no-op [`Meter`] instances. +pub fn set_meter_provider

{ + /// next: P, + /// } + /// + /// impl

FilteringSpanProcessor

{ + /// fn new(next: P) -> Self { + /// Self { next } + /// } + /// } + /// + /// impl SpanProcessor for FilteringSpanProcessor

{ + /// fn on_start(&self, span: &mut Span, cx: &Context) { + /// self.next.on_start(span, cx); + /// } + /// + /// fn on_end(&self, span: SpanData) { + /// let should_drop = span.attributes.iter().any(|attribute| { + /// attribute.key.as_str() == "example.drop" + /// && attribute.value == Value::Bool(true) + /// }); + /// + /// if !should_drop { + /// self.next.on_end(span); + /// } + /// } + /// + /// fn force_flush(&self) -> OTelSdkResult { + /// self.next.force_flush() + /// } + /// + /// fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + /// self.next.shutdown_with_timeout(timeout) + /// } + /// + /// fn set_resource(&mut self, resource: &Resource) { + /// self.next.set_resource(resource); + /// } + /// } + /// ``` + /// + /// [`on_start`]: SpanProcessor::on_start + /// [`Context::current()`]: opentelemetry::Context::current + /// + /// TODO - This method should take reference to `SpanData` + fn on_end(&self, span: SpanData); + /// Force the spans lying in the cache to be exported. + fn force_flush(&self) -> OTelSdkResult; + /// Shuts down the processor. Called when SDK is shut down. This is an + /// opportunity for processors to do any cleanup required. + /// + /// Implementation should make sure shutdown can be called multiple times. + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult; + /// shutdown the processor with a default timeout. + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } + /// Set the resource for the span processor. + fn set_resource(&mut self, _resource: &Resource) {} +} + +/// A [SpanProcessor] that passes finished spans to the configured +/// `SpanExporter`, as soon as they are finished, without any batching. This is +/// typically useful for debugging and testing. For scenarios requiring higher +/// performance/throughput, consider using [BatchSpanProcessor]. +/// Spans are exported synchronously +/// in the same thread that emits the log record. +/// When using this processor with the OTLP Exporter, the following exporter +/// features are supported: +/// - `grpc-tonic`: This requires TracerProvider to be created within a tokio +/// runtime. Spans can be emitted from any thread, including tokio runtime +/// threads. +/// - `reqwest-blocking-client`: TracerProvider may be created anywhere, but +/// spans must be emitted from a non-tokio runtime thread. +/// - `reqwest-client`: TracerProvider may be created anywhere, but spans must be +/// emitted from a tokio runtime thread. +/// +/// The OTLP HTTP exporter chooses its default HTTP client from enabled crate +/// features. That choice is not processor-aware. If you enable async HTTP +/// clients such as `reqwest-client` or `hyper-client`, ensure this processor is +/// only used from a thread where those clients can run. +#[derive(Debug)] +pub struct SimpleSpanProcessor { + exporter: Mutex, + is_shutdown: AtomicBool, + + // Self-diagnostics: otel.sdk.processor.span.processed counter, gated behind + // experimental_metrics_bound_instruments. The SimpleSpanProcessor exports + // each span synchronously and has no queue, so the only processor-side drop + // is `already_shutdown`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_success: opentelemetry::metrics::BoundCounter, + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_after_shutdown: opentelemetry::metrics::BoundCounter, +} + +impl SimpleSpanProcessor { + /// Create a new [SimpleSpanProcessor] using the provided exporter. + pub fn new(exporter: T) -> Self { + #[cfg(feature = "experimental_metrics_bound_instruments")] + let (processed_success, processed_after_shutdown) = { + static INSTANCE_COUNTER: AtomicUsize = AtomicUsize::new(0); + let instance_id = INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed); + let component_name = format!("simple_span_processor/{instance_id}"); + + let meter = opentelemetry::global::meter("otel.sdk"); + let counter = meter + .u64_counter("otel.sdk.processor.span.processed") + .with_description( + "The number of spans for which the processing has finished, \ + either successful or failed.", + ) + .with_unit("{span}") + .build(); + + // Attribute values follow the OTel semantic conventions for SDK metrics: + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/otel/sdk-metrics.md#metric-otelsdkprocessorspanprocessed + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/otel.md#otel-component-attributes + let success_attrs = [ + KeyValue::new("otel.component.type", "simple_span_processor"), + KeyValue::new("otel.component.name", component_name.clone()), + ]; + let after_shutdown_attrs = [ + KeyValue::new("error.type", "already_shutdown"), + KeyValue::new("otel.component.type", "simple_span_processor"), + KeyValue::new("otel.component.name", component_name), + ]; + + ( + counter.bind(&success_attrs), + counter.bind(&after_shutdown_attrs), + ) + }; + + Self { + exporter: Mutex::new(exporter), + is_shutdown: AtomicBool::new(false), + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_success, + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_after_shutdown, + } + } +} + +impl SpanProcessor for SimpleSpanProcessor { + fn on_start(&self, _span: &mut Span, _cx: &Context) { + // Ignored + } + + fn on_end(&self, span: SpanData) { + if !span.span_context.is_sampled() { + return; + } + + // noop after shutdown + if self.is_shutdown.load(Ordering::Relaxed) { + // Record the post-shutdown drop in self-diagnostics before returning. + #[cfg(feature = "experimental_metrics_bound_instruments")] + self.processed_after_shutdown.add(1); + otel_warn!( + name: "SimpleSpanProcessor.OnEnd.AfterShutdown", + message = "Spans are being emitted even after Shutdown. This indicates incorrect lifecycle management of TracerProvider in application. Spans will not be exported." + ); + return; + } + + let result = match self.exporter.lock() { + Ok(exporter) => { + // Count the span as processed right before submitting it to the + // exporter, independent of the export outcome, per semconv. + #[cfg(feature = "experimental_metrics_bound_instruments")] + self.processed_success.add(1); + futures_executor::block_on(exporter.export(vec![span])) + } + Err(_) => Err(OTelSdkError::InternalFailure( + "SimpleSpanProcessor mutex poison".into(), + )), + }; + + if let Err(err) = result { + // TODO: check error type, and log `error` only if the error is user-actionable, else log `debug` + otel_debug!( + name: "SimpleProcessor.OnEnd.Error", + reason = format!("{:?}", err) + ); + } + } + + fn force_flush(&self) -> OTelSdkResult { + // Nothing to flush for simple span processor. + Ok(()) + } + + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + self.is_shutdown.store(true, Ordering::Relaxed); + if let Ok(exporter) = self.exporter.lock() { + exporter.shutdown_with_timeout(timeout) + } else { + Err(OTelSdkError::InternalFailure( + "SimpleSpanProcessor mutex poison at shutdown".into(), + )) + } + } + + fn set_resource(&mut self, resource: &Resource) { + if let Ok(mut exporter) = self.exporter.lock() { + exporter.set_resource(resource); + } + } +} + +/// The `BatchSpanProcessor` collects finished spans in a buffer and exports them +/// in batches to the configured `SpanExporter`. This processor is ideal for +/// high-throughput environments, as it minimizes the overhead of exporting spans +/// individually. It uses a **dedicated background thread** to manage and export spans +/// asynchronously, ensuring that the application's main execution flow is not blocked. +/// +/// When using this processor with the OTLP Exporter, the following exporter +/// features are supported: +/// - `grpc-tonic`: This requires `TracerProvider` to be created within a tokio +/// runtime. +/// - `reqwest-blocking-client`: Works with a regular `main` or `tokio::main`. +/// +/// In other words, async HTTP clients like `reqwest-client` and `hyper-client` +/// are not supported by this default processor. The OTLP HTTP exporter chooses +/// its default HTTP client from enabled crate features and cannot tell which +/// processor will drive it. If your dependency graph enables async HTTP client +/// features, either pass an explicit blocking client for this processor or use +/// the experimental async-runtime batch span processor. +/// +/// # Example +/// +/// This example demonstrates how to configure and use the `BatchSpanProcessor` +/// with a custom configuration. Note that a dedicated thread is used internally +/// to manage the export process. +/// +/// ```rust +/// # #[cfg(feature = "testing")] +/// # { +/// use opentelemetry::global; +/// use opentelemetry_sdk::trace::{ +/// BatchSpanProcessor, BatchConfigBuilder, SdkTracerProvider, InMemorySpanExporter, +/// }; +/// use opentelemetry::trace::Tracer as _; +/// use opentelemetry::trace::Span; +/// use std::time::Duration; +/// +/// // Step 1: Create an exporter (e.g., an In-Memory Exporter for demonstration). +/// let exporter = InMemorySpanExporter::default(); +/// +/// // Step 2: Configure the BatchSpanProcessor. +/// let batch_processor = BatchSpanProcessor::builder(exporter) +/// .with_batch_config( +/// BatchConfigBuilder::default() +/// .with_max_queue_size(1024) // Buffer up to 1024 spans. +/// .with_max_export_batch_size(256) // Export in batches of up to 256 spans. +/// .with_scheduled_delay(Duration::from_secs(5)) // Export every 5 seconds. +/// .build(), +/// ) +/// .build(); +/// +/// // Step 3: Set up a TracerProvider with the configured processor. +/// let provider = SdkTracerProvider::builder() +/// .with_span_processor(batch_processor) +/// .build(); +/// global::set_tracer_provider(provider.clone()); +/// +/// // Step 4: Create spans and record operations. +/// let tracer = global::tracer("example-tracer"); +/// let mut span = tracer.start("example-span"); +/// span.end(); // Mark the span as completed. +/// +/// // Step 5: Ensure all spans are flushed before exiting. +/// provider.shutdown(); +/// # } +/// ``` +use std::sync::mpsc::sync_channel; +use std::sync::mpsc::Receiver; +use std::sync::mpsc::RecvTimeoutError; +use std::sync::mpsc::SyncSender; + +/// Messages exchanged between the main thread and the background thread. +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +enum BatchMessage { + //ExportSpan(SpanData), + ExportSpan(Arc), + ForceFlush(SyncSender), + Shutdown(SyncSender), + SetResource(Arc), +} + +/// The `BatchSpanProcessor` collects finished spans in a buffer and exports them +/// in batches to the configured `SpanExporter`. This processor is ideal for +/// high-throughput environments, as it minimizes the overhead of exporting spans +/// individually. It uses a **dedicated background thread** to manage and export spans +/// asynchronously, ensuring that the application's main execution flow is not blocked. +/// +/// This processor supports the following configurations: +/// - **Queue size**: Maximum number of spans that can be buffered. +/// - **Batch size**: Maximum number of spans to include in a single export. +/// - **Scheduled delay**: Frequency at which the batch is exported. +/// +/// When using this processor with the OTLP Exporter, the following exporter +/// features are supported: +/// - `grpc-tonic`: Requires `TracerProvider` to be created within a tokio runtime. +/// - `reqwest-blocking-client`: Works with a regular `main` or `tokio::main`. +/// +/// In other words, async HTTP clients like `reqwest-client` and `hyper-client` +/// are not supported by this default processor. The OTLP HTTP exporter chooses +/// its default HTTP client from enabled crate features and cannot tell which +/// processor will drive it. If your dependency graph enables async HTTP client +/// features, either pass an explicit blocking client for this processor or use +/// the experimental async-runtime batch span processor. +/// +/// `BatchSpanProcessor` buffers spans in memory and exports them in batches. An +/// export is triggered when `max_export_batch_size` is reached or every +/// `scheduled_delay` milliseconds. Users can explicitly trigger an export using +/// the `force_flush` method. Shutdown also triggers an export of all buffered +/// spans and is recommended to be called before the application exits to ensure +/// all buffered spans are exported. +/// +/// **Warning**: When using tokio's current-thread runtime, `shutdown()`, which +/// is a blocking call ,should not be called from your main thread. This can +/// cause deadlock. Instead, call `shutdown()` from a separate thread or use +/// tokio's `spawn_blocking`. +/// +#[derive(Debug)] +pub struct BatchSpanProcessor { + span_sender: SyncSender, // Data channel to store spans + message_sender: SyncSender, // Control channel to store control messages. + handle: Mutex>>, + forceflush_timeout: Duration, + export_span_message_sent: Arc, + current_batch_size: Arc, + max_export_batch_size: usize, + dropped_spans_count: AtomicUsize, + max_queue_size: usize, + + // Self-diagnostics: otel.sdk.processor.span.processed counter, gated behind + // experimental_metrics_bound_instruments so the hot-path `add` is a single + // atomic increment with no per-call attribute resolution. The success count + // is recorded in the worker thread when a batch is submitted to the + // exporter; the drop counts are recorded here at enqueue time. + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_queue_full: opentelemetry::metrics::BoundCounter, + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_after_shutdown: opentelemetry::metrics::BoundCounter, +} + +impl BatchSpanProcessor { + /// Creates a new instance of `BatchSpanProcessor`. + pub fn new( + mut exporter: E, + config: BatchConfig, + //max_queue_size: usize, + //scheduled_delay: Duration, + //shutdown_timeout: Duration, + ) -> Self + where + E: SpanExporter + Send + 'static, + { + let (span_sender, span_receiver) = sync_channel::(config.max_queue_size); + let (message_sender, message_receiver) = sync_channel::(64); // Is this a reasonable bound? + let max_queue_size = config.max_queue_size; + let max_export_batch_size = config.max_export_batch_size; + let current_batch_size = Arc::new(AtomicUsize::new(0)); + let current_batch_size_for_thread = current_batch_size.clone(); + + // Self-diagnostics: create the otel.sdk.processor.span.processed counter. + // Created before the worker thread is spawned so the success counter can + // be moved into the worker and incremented when a batch is exported. + #[cfg(feature = "experimental_metrics_bound_instruments")] + let (processed_success, processed_queue_full, processed_after_shutdown) = { + static INSTANCE_COUNTER: AtomicUsize = AtomicUsize::new(0); + let instance_id = INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed); + let component_name = format!("batching_span_processor/{instance_id}"); + + let meter = opentelemetry::global::meter("otel.sdk"); + let counter = meter + .u64_counter("otel.sdk.processor.span.processed") + .with_description( + "The number of spans for which the processing has finished, \ + either successful or failed.", + ) + .with_unit("{span}") + .build(); + + // Attribute values follow the OTel semantic conventions for SDK metrics: + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/otel/sdk-metrics.md#metric-otelsdkprocessorspanprocessed + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/otel.md#otel-component-attributes + let success_attrs = [ + KeyValue::new("otel.component.type", "batching_span_processor"), + KeyValue::new("otel.component.name", component_name.clone()), + ]; + let queue_full_attrs = [ + KeyValue::new("error.type", "queue_full"), + KeyValue::new("otel.component.type", "batching_span_processor"), + KeyValue::new("otel.component.name", component_name.clone()), + ]; + let after_shutdown_attrs = [ + KeyValue::new("error.type", "already_shutdown"), + KeyValue::new("otel.component.type", "batching_span_processor"), + KeyValue::new("otel.component.name", component_name), + ]; + + ( + counter.bind(&success_attrs), + counter.bind(&queue_full_attrs), + counter.bind(&after_shutdown_attrs), + ) + }; + + let handle = thread::Builder::new() + .name("OpenTelemetry.Traces.BatchProcessor".to_string()) + .spawn(move || { + let _suppress_guard = Context::enter_telemetry_suppressed_scope(); + otel_debug!( + name: "BatchSpanProcessor.ThreadStarted", + interval_in_millisecs = config.scheduled_delay.as_millis(), + max_export_batch_size = config.max_export_batch_size, + max_queue_size = config.max_queue_size, + ); + let mut spans = Vec::with_capacity(config.max_export_batch_size); + let mut last_export_time = Instant::now(); + let current_batch_size = current_batch_size_for_thread; + + // Counts spans for the otel.sdk.processor.span.processed metric; + // a no-op when the self-diagnostics feature is disabled. + #[cfg(feature = "experimental_metrics_bound_instruments")] + let record_processed_success = move |count: u64| processed_success.add(count); + #[cfg(not(feature = "experimental_metrics_bound_instruments"))] + let record_processed_success = |_count: u64| {}; + loop { + let remaining_time_option = config + .scheduled_delay + .checked_sub(last_export_time.elapsed()); + let remaining_time = match remaining_time_option { + Some(remaining_time) => remaining_time, + None => config.scheduled_delay, + }; + match message_receiver.recv_timeout(remaining_time) { + Ok(message) => match message { + BatchMessage::ExportSpan(export_span_message_sent) => { + // Reset the export span message sent flag now it has has been processed. + export_span_message_sent.store(false, Ordering::Relaxed); + otel_debug!( + name: "BatchSpanProcessor.ExportingDueToBatchSize", + ); + let _ = Self::get_spans_and_export( + &span_receiver, + &exporter, + &mut spans, + &mut last_export_time, + ¤t_batch_size, + &config, + &record_processed_success, + ); + } + BatchMessage::ForceFlush(sender) => { + otel_debug!(name: "BatchSpanProcessor.ExportingDueToForceFlush"); + let result = Self::get_spans_and_export( + &span_receiver, + &exporter, + &mut spans, + &mut last_export_time, + ¤t_batch_size, + &config, + &record_processed_success, + ); + let _ = sender.send(result); + } + BatchMessage::Shutdown(sender) => { + otel_debug!(name: "BatchSpanProcessor.ExportingDueToShutdown"); + let result = Self::get_spans_and_export( + &span_receiver, + &exporter, + &mut spans, + &mut last_export_time, + ¤t_batch_size, + &config, + &record_processed_success, + ); + let _ = exporter.shutdown(); + let _ = sender.send(result); + + otel_debug!( + name: "BatchSpanProcessor.ThreadExiting", + reason = "ShutdownRequested" + ); + // + // break out the loop and return from the current background thread. + // + break; + } + BatchMessage::SetResource(resource) => { + exporter.set_resource(&resource); + } + }, + Err(RecvTimeoutError::Timeout) => { + otel_debug!( + name: "BatchSpanProcessor.ExportingDueToTimer", + ); + + let _ = Self::get_spans_and_export( + &span_receiver, + &exporter, + &mut spans, + &mut last_export_time, + ¤t_batch_size, + &config, + &record_processed_success, + ); + } + Err(RecvTimeoutError::Disconnected) => { + // Channel disconnected, only thing to do is break + // out (i.e exit the thread) + otel_debug!( + name: "BatchSpanProcessor.ThreadExiting", + reason = "MessageSenderDisconnected" + ); + break; + } + } + } + otel_debug!( + name: "BatchSpanProcessor.ThreadStopped" + ); + }) + .expect("Failed to spawn thread"); //TODO: Handle thread spawn failure + + Self { + span_sender, + message_sender, + handle: Mutex::new(Some(handle)), + forceflush_timeout: Duration::from_secs(5), // TODO: make this configurable + dropped_spans_count: AtomicUsize::new(0), + max_queue_size, + export_span_message_sent: Arc::new(AtomicBool::new(false)), + current_batch_size, + max_export_batch_size, + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_queue_full, + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_after_shutdown, + } + } + + /// builder + pub fn builder(exporter: E) -> BatchSpanProcessorBuilder + where + E: SpanExporter + Send + 'static, + { + BatchSpanProcessorBuilder { + exporter, + config: BatchConfig::default(), + } + } + + // This method gets up to `max_export_batch_size` amount of spans from the channel and exports them. + // It returns the result of the export operation. + // It expects the spans vec to be empty when it's called. + #[inline] + fn get_spans_and_export( + spans_receiver: &Receiver, + exporter: &E, + spans: &mut Vec, + last_export_time: &mut Instant, + current_batch_size: &AtomicUsize, + config: &BatchConfig, + record_processed_success: &F, + ) -> OTelSdkResult + where + E: SpanExporter + Send + Sync + 'static, + F: Fn(u64), + { + let target = current_batch_size.load(Ordering::Acquire); // `target` is used to determine the stopping criteria for exporting spans. + let mut result = OTelSdkResult::Ok(()); + let mut total_exported_spans: usize = 0; + + while target > 0 && total_exported_spans < target { + let batch_limit = config + .max_export_batch_size + .min(target - total_exported_spans); + + // Get up to the remaining target batch size from the channel and push them to the spans vec + while let Ok(span) = spans_receiver.try_recv() { + spans.push(span); + if spans.len() == batch_limit { + break; + } + } + + let count_of_spans = spans.len(); // Count of spans that will be exported + if count_of_spans == 0 { + break; + } + total_exported_spans += count_of_spans; + + // Count the batch as processed before invoking the exporter, + // regardless of the export outcome. + record_processed_success(count_of_spans as u64); + + result = Self::export_batch_sync(exporter, spans, last_export_time); // This method clears the spans vec after exporting + + current_batch_size.fetch_sub(count_of_spans, Ordering::AcqRel); + } + result + } + + #[allow(clippy::vec_box)] + fn export_batch_sync( + exporter: &E, + batch: &mut Vec, + last_export_time: &mut Instant, + ) -> OTelSdkResult + where + E: SpanExporter + ?Sized, + { + *last_export_time = Instant::now(); + + if batch.is_empty() { + return OTelSdkResult::Ok(()); + } + + // Splitting off batch clears the existing batch capacity, and is ready + // for re-use in the next export. The newly returned vec! from split_off + // is passed to the exporter. + // TODO: Compared to Logs, this requires new allocation for vec for + // every export. See if this can be optimized by + // *not* requiring ownership in the exporter. + let export = exporter.export(batch.split_off(0)); + let export_result = futures_executor::block_on(export); + + match export_result { + Ok(_) => OTelSdkResult::Ok(()), + Err(err) => { + otel_error!( + name: "BatchSpanProcessor.ExportError", + error = format!("{}", err) + ); + OTelSdkResult::Err(err) + } + } + } +} + +impl SpanProcessor for BatchSpanProcessor { + /// Handles span start. + fn on_start(&self, _span: &mut Span, _cx: &Context) { + // Ignored + } + + /// Handles span end. + fn on_end(&self, span: SpanData) { + // Count the span before enqueueing it so that a concurrent + // force_flush()/shutdown() drain never observes an + // enqueued-but-uncounted span and misses it (issue #3453). If the + // send fails, the increment is reverted in the error arms below. + let previous_batch_size = self.current_batch_size.fetch_add(1, Ordering::AcqRel); + let result = self.span_sender.try_send(span); + + // match for result and handle each separately + match result { + Ok(_) => { + // Successfully sent the span to the data channel. + // Check if the current batch size has reached the max export + // batch size. + if previous_batch_size + 1 >= self.max_export_batch_size { + // Check if the a control message for exporting spans is + // already sent to the worker thread. If not, send a control + // message to export spans. `export_span_message_sent` is set + // to false ONLY when the worker thread has processed the + // control message. + + if !self.export_span_message_sent.load(Ordering::Relaxed) { + // This is a cost-efficient check as atomic load + // operations do not require exclusive access to cache + // line. Perform atomic swap to + // `export_span_message_sent` ONLY when the atomic load + // operation above returns false. Atomic + // swap/compare_exchange operations require exclusive + // access to cache line on most processor architectures. + // We could have used compare_exchange as well here, but + // it's more verbose than swap. + if !self.export_span_message_sent.swap(true, Ordering::Relaxed) { + match self.message_sender.try_send(BatchMessage::ExportSpan( + self.export_span_message_sent.clone(), + )) { + Ok(_) => { + // Control message sent successfully. + } + Err(_err) => { + // TODO: Log error If the control message + // could not be sent, reset the + // `export_span_message_sent` flag. + self.export_span_message_sent + .store(false, Ordering::Relaxed); + } + } + } + } + } + } + Err(std::sync::mpsc::TrySendError::Full(_)) => { + // The span never entered the channel; revert the increment. + self.current_batch_size.fetch_sub(1, Ordering::AcqRel); + // Record queue-full drop in self-diagnostics. + #[cfg(feature = "experimental_metrics_bound_instruments")] + self.processed_queue_full.add(1); + // Increment dropped spans count. The first time we have to drop + // a span, emit a warning. + if self.dropped_spans_count.fetch_add(1, Ordering::Relaxed) == 0 { + otel_warn!(name: "BatchSpanProcessor.SpanDroppingStarted", + message = "BatchSpanProcessor dropped a Span due to queue full. No further log will be emitted for further drops until Shutdown. During Shutdown time, a log will be emitted with exact count of total spans dropped."); + } + } + Err(std::sync::mpsc::TrySendError::Disconnected(_)) => { + // The span never entered the channel; revert the increment. + self.current_batch_size.fetch_sub(1, Ordering::AcqRel); + // Record after-shutdown drop in self-diagnostics. + #[cfg(feature = "experimental_metrics_bound_instruments")] + self.processed_after_shutdown.add(1); + // Given background thread is the only receiver, and it's + // disconnected, it indicates the thread is shutdown + otel_warn!( + name: "BatchSpanProcessor.OnEnd.AfterShutdown", + message = "Spans are being emitted even after Shutdown. This indicates incorrect lifecycle management of TracerProvider in application. Spans will not be exported." + ); + } + } + } + + /// Flushes all pending spans. + fn force_flush(&self) -> OTelSdkResult { + let (sender, receiver) = std::sync::mpsc::sync_channel(1); + match self + .message_sender + .try_send(BatchMessage::ForceFlush(sender)) + { + Ok(_) => receiver + .recv_timeout(self.forceflush_timeout) + .map_err(|err| { + if err == std::sync::mpsc::RecvTimeoutError::Timeout { + OTelSdkError::Timeout(self.forceflush_timeout) + } else { + OTelSdkError::InternalFailure(format!("{err}")) + } + })?, + Err(std::sync::mpsc::TrySendError::Full(_)) => { + // If the control message could not be sent, emit a warning. + otel_debug!( + name: "BatchSpanProcessor.ForceFlush.ControlChannelFull", + message = "Control message to flush the worker thread could not be sent as the control channel is full. This can occur if user repeatedly calls force_flush/shutdown without finishing the previous call." + ); + Err(OTelSdkError::InternalFailure("ForceFlush cannot be performed as Control channel is full. This can occur if user repeatedly calls force_flush/shutdown without finishing the previous call.".into())) + } + Err(std::sync::mpsc::TrySendError::Disconnected(_)) => { + // Given background thread is the only receiver, and it's + // disconnected, it indicates the thread is shutdown + otel_debug!( + name: "BatchSpanProcessor.ForceFlush.AlreadyShutdown", + message = "ForceFlush invoked after Shutdown. This will not perform Flush and indicates a incorrect lifecycle management in Application." + ); + + Err(OTelSdkError::AlreadyShutdown) + } + } + } + + /// Shuts down the processor. + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + let dropped_spans = self.dropped_spans_count.load(Ordering::Relaxed); + let max_queue_size = self.max_queue_size; + if dropped_spans > 0 { + otel_warn!( + name: "BatchSpanProcessor.SpansDropped", + dropped_span_count = dropped_spans, + max_queue_size = max_queue_size, + message = "Spans were dropped due to a queue being full. The count represents the total count of spans dropped in the lifetime of this BatchSpanProcessor. Consider increasing the queue size and/or decrease delay between intervals." + ); + } + + let (sender, receiver) = std::sync::mpsc::sync_channel(1); + match self.message_sender.try_send(BatchMessage::Shutdown(sender)) { + Ok(_) => { + receiver + .recv_timeout(timeout) + .map(|_| { + // join the background thread after receiving back the + // shutdown signal + if let Some(handle) = self.handle.lock().unwrap().take() { + handle.join().unwrap(); + } + OTelSdkResult::Ok(()) + }) + .map_err(|err| match err { + std::sync::mpsc::RecvTimeoutError::Timeout => { + otel_error!( + name: "BatchSpanProcessor.Shutdown.Timeout", + message = "BatchSpanProcessor shutdown timing out." + ); + OTelSdkError::Timeout(timeout) + } + _ => { + otel_error!( + name: "BatchSpanProcessor.Shutdown.Error", + error = format!("{}", err) + ); + OTelSdkError::InternalFailure(format!("{err}")) + } + })? + } + Err(std::sync::mpsc::TrySendError::Full(_)) => { + // If the control message could not be sent, emit a warning. + otel_debug!( + name: "BatchSpanProcessor.Shutdown.ControlChannelFull", + message = "Control message to shutdown the worker thread could not be sent as the control channel is full. This can occur if user repeatedly calls force_flush/shutdown without finishing the previous call." + ); + Err(OTelSdkError::InternalFailure("Shutdown cannot be performed as Control channel is full. This can occur if user repeatedly calls force_flush/shutdown without finishing the previous call.".into())) + } + Err(std::sync::mpsc::TrySendError::Disconnected(_)) => { + // Given background thread is the only receiver, and it's + // disconnected, it indicates the thread is shutdown + otel_debug!( + name: "BatchSpanProcessor.Shutdown.AlreadyShutdown", + message = "Shutdown is being invoked more than once. This is noop, but indicates a potential issue in the application's lifecycle management." + ); + + Err(OTelSdkError::AlreadyShutdown) + } + } + } + + /// Set the resource for the processor. + fn set_resource(&mut self, resource: &Resource) { + let resource = Arc::new(resource.clone()); + let _ = self + .message_sender + .try_send(BatchMessage::SetResource(resource)); + } +} + +/// Builder for `BatchSpanProcessorDedicatedThread`. +#[derive(Debug, Default)] +pub struct BatchSpanProcessorBuilder +where + E: SpanExporter + Send + 'static, +{ + exporter: E, + config: BatchConfig, +} + +impl BatchSpanProcessorBuilder +where + E: SpanExporter + Send + 'static, +{ + /// Set the BatchConfig for [BatchSpanProcessorBuilder] + pub fn with_batch_config(self, config: BatchConfig) -> Self { + BatchSpanProcessorBuilder { config, ..self } + } + + /// Build a new instance of `BatchSpanProcessor`. + pub fn build(self) -> BatchSpanProcessor { + BatchSpanProcessor::new(self.exporter, self.config) + } +} + +/// Batch span processor configuration. +/// Use [`BatchConfigBuilder`] to configure your own instance of [`BatchConfig`]. +#[derive(Debug)] +pub struct BatchConfig { + /// The maximum queue size to buffer spans for delayed processing. If the + /// queue gets full it drops the spans. The default value of is 2048. + pub(crate) max_queue_size: usize, + + /// The delay interval in milliseconds between two consecutive processing + /// of batches. The default value is 5 seconds. + pub(crate) scheduled_delay: Duration, + + #[allow(dead_code)] + /// The maximum number of spans to process in a single batch. If there are + /// more than one batch worth of spans then it processes multiple batches + /// of spans one batch after the other without any delay. The default value + /// is 512. + pub(crate) max_export_batch_size: usize, + + #[allow(dead_code)] + /// The maximum duration to export a batch of data. + pub(crate) max_export_timeout: Duration, + + #[allow(dead_code)] + pub(crate) max_concurrent_exports: usize, +} + +impl Default for BatchConfig { + fn default() -> Self { + BatchConfigBuilder::default().build() + } +} + +/// A builder for creating [`BatchConfig`] instances. +#[derive(Debug)] +pub struct BatchConfigBuilder { + max_queue_size: usize, + scheduled_delay: Duration, + max_export_batch_size: usize, + max_export_timeout: Duration, + max_concurrent_exports: usize, +} + +impl Default for BatchConfigBuilder { + /// Create a new [`BatchConfigBuilder`] initialized with default batch config values as per the specs. + /// The values are overriden by environment variables if set. + /// The supported environment variables are: + /// * `OTEL_BSP_MAX_QUEUE_SIZE` + /// * `OTEL_BSP_SCHEDULE_DELAY` + /// * `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` + /// * `OTEL_BSP_EXPORT_TIMEOUT` + /// * `OTEL_BSP_MAX_CONCURRENT_EXPORTS` + /// + /// Note: Programmatic configuration overrides any value set via the environment variable. + fn default() -> Self { + BatchConfigBuilder { + max_queue_size: OTEL_BSP_MAX_QUEUE_SIZE_DEFAULT, + scheduled_delay: OTEL_BSP_SCHEDULE_DELAY_DEFAULT, + max_export_batch_size: OTEL_BSP_MAX_EXPORT_BATCH_SIZE_DEFAULT, + max_export_timeout: OTEL_BSP_EXPORT_TIMEOUT_DEFAULT, + max_concurrent_exports: OTEL_BSP_MAX_CONCURRENT_EXPORTS_DEFAULT, + } + .init_from_env_vars() + } +} + +impl BatchConfigBuilder { + /// Set max_queue_size for [`BatchConfigBuilder`]. + /// It's the maximum queue size to buffer spans for delayed processing. + /// If the queue gets full it will drops the spans. + /// The default value is 2048. + /// + /// Corresponding environment variable: `OTEL_BSP_MAX_QUEUE_SIZE`. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + pub fn with_max_queue_size(mut self, max_queue_size: usize) -> Self { + self.max_queue_size = max_queue_size; + self + } + + /// Set max_export_batch_size for [`BatchConfigBuilder`]. + /// It's the maximum number of spans to process in a single batch. If there are + /// more than one batch worth of spans then it processes multiple batches + /// of spans one batch after the other without any delay. The default value + /// is 512. + /// + /// Corresponding environment variable: `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + pub fn with_max_export_batch_size(mut self, max_export_batch_size: usize) -> Self { + self.max_export_batch_size = max_export_batch_size; + self + } + + #[cfg(feature = "experimental_trace_batch_span_processor_with_async_runtime")] + /// Set max_concurrent_exports for [`BatchConfigBuilder`]. + /// + /// This value is honored by + /// `span_processor_with_async_runtime::BatchSpanProcessor`, where it limits + /// the number of concurrent export tasks. + /// + /// The thread-based `BatchSpanProcessor` exports serially and ignores this + /// setting. + /// + /// Corresponding environment variable: `OTEL_BSP_MAX_CONCURRENT_EXPORTS`. + /// + /// For concurrent exports, enable + /// `experimental_trace_batch_span_processor_with_async_runtime` and use the + /// async-runtime processor. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + pub fn with_max_concurrent_exports(mut self, max_concurrent_exports: usize) -> Self { + self.max_concurrent_exports = max_concurrent_exports; + self + } + + /// Set scheduled_delay_duration for [`BatchConfigBuilder`]. + /// It's the delay interval in milliseconds between two consecutive processing of batches. + /// The default value is 5000 milliseconds. + /// + /// Corresponding environment variable: `OTEL_BSP_SCHEDULE_DELAY`. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + pub fn with_scheduled_delay(mut self, scheduled_delay: Duration) -> Self { + self.scheduled_delay = scheduled_delay; + self + } + + /// Set max_export_timeout for [`BatchConfigBuilder`]. + /// It's the maximum duration to export a batch of data. + /// The The default value is 30000 milliseconds. + /// + /// Corresponding environment variable: `OTEL_BSP_EXPORT_TIMEOUT`. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + #[cfg(feature = "experimental_trace_batch_span_processor_with_async_runtime")] + pub fn with_max_export_timeout(mut self, max_export_timeout: Duration) -> Self { + self.max_export_timeout = max_export_timeout; + self + } + + /// Builds a `BatchConfig` enforcing the following invariants: + /// * `max_export_batch_size` must be less than or equal to `max_queue_size`. + pub fn build(self) -> BatchConfig { + // max export batch size must be less or equal to max queue size. + // we set max export batch size to max queue size if it's larger than max queue size. + let max_export_batch_size = min(self.max_export_batch_size, self.max_queue_size); + + BatchConfig { + max_queue_size: self.max_queue_size, + scheduled_delay: self.scheduled_delay, + max_export_timeout: self.max_export_timeout, + max_concurrent_exports: self.max_concurrent_exports, + max_export_batch_size, + } + } + + fn init_from_env_vars(mut self) -> Self { + if let Some(max_concurrent_exports) = env::var(OTEL_BSP_MAX_CONCURRENT_EXPORTS) + .ok() + .and_then(|max_concurrent_exports| usize::from_str(&max_concurrent_exports).ok()) + { + self.max_concurrent_exports = max_concurrent_exports; + } + + if let Some(max_queue_size) = env::var(OTEL_BSP_MAX_QUEUE_SIZE) + .ok() + .and_then(|queue_size| usize::from_str(&queue_size).ok()) + { + self.max_queue_size = max_queue_size; + } + + if let Some(scheduled_delay) = env::var(OTEL_BSP_SCHEDULE_DELAY) + .ok() + .and_then(|delay| u64::from_str(&delay).ok()) + { + self.scheduled_delay = Duration::from_millis(scheduled_delay); + } + + if let Some(max_export_batch_size) = env::var(OTEL_BSP_MAX_EXPORT_BATCH_SIZE) + .ok() + .and_then(|batch_size| usize::from_str(&batch_size).ok()) + { + self.max_export_batch_size = max_export_batch_size; + } + + // max export batch size must be less or equal to max queue size. + // we set max export batch size to max queue size if it's larger than max queue size. + if self.max_export_batch_size > self.max_queue_size { + self.max_export_batch_size = self.max_queue_size; + } + + if let Some(max_export_timeout) = env::var(OTEL_BSP_EXPORT_TIMEOUT) + .ok() + .and_then(|timeout| u64::from_str(&timeout).ok()) + { + self.max_export_timeout = Duration::from_millis(max_export_timeout); + } + + self + } +} + +#[cfg(all(test, feature = "testing", feature = "trace"))] +mod tests { + // cargo test trace::span_processor::tests:: --features=testing + use super::{ + BatchSpanProcessor, SimpleSpanProcessor, SpanProcessor, OTEL_BSP_EXPORT_TIMEOUT, + OTEL_BSP_MAX_EXPORT_BATCH_SIZE, OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_MAX_QUEUE_SIZE_DEFAULT, + OTEL_BSP_SCHEDULE_DELAY, OTEL_BSP_SCHEDULE_DELAY_DEFAULT, + }; + use crate::error::OTelSdkResult; + use crate::testing::trace::new_test_export_span_data; + use crate::trace::span_processor::{ + OTEL_BSP_EXPORT_TIMEOUT_DEFAULT, OTEL_BSP_MAX_CONCURRENT_EXPORTS, + OTEL_BSP_MAX_CONCURRENT_EXPORTS_DEFAULT, OTEL_BSP_MAX_EXPORT_BATCH_SIZE_DEFAULT, + }; + use crate::trace::InMemorySpanExporterBuilder; + use crate::trace::{BatchConfig, BatchConfigBuilder, SpanEvents, SpanLinks}; + use crate::trace::{SpanData, SpanExporter}; + use opentelemetry::trace::{SpanContext, SpanId, SpanKind, Status}; + use std::fmt::Debug; + use std::time::Duration; + + #[test] + fn simple_span_processor_on_end_calls_export() { + let exporter = InMemorySpanExporterBuilder::new().build(); + let processor = SimpleSpanProcessor::new(exporter.clone()); + let span_data = new_test_export_span_data(); + processor.on_end(span_data.clone()); + assert_eq!(exporter.get_finished_spans().unwrap()[0], span_data); + let _result = processor.shutdown(); + } + + #[test] + fn simple_span_processor_on_end_skips_export_if_not_sampled() { + let exporter = InMemorySpanExporterBuilder::new().build(); + let processor = SimpleSpanProcessor::new(exporter.clone()); + let unsampled = SpanData { + span_context: SpanContext::empty_context(), + parent_span_id: SpanId::INVALID, + parent_span_is_remote: false, + span_kind: SpanKind::Internal, + name: "opentelemetry".into(), + start_time: opentelemetry::time::now(), + end_time: opentelemetry::time::now(), + attributes: Vec::new(), + dropped_attributes_count: 0, + events: SpanEvents::default(), + links: SpanLinks::default(), + status: Status::Unset, + instrumentation_scope: Default::default(), + }; + processor.on_end(unsampled); + assert!(exporter.get_finished_spans().unwrap().is_empty()); + } + + #[test] + fn simple_span_processor_shutdown_calls_shutdown() { + let exporter = InMemorySpanExporterBuilder::new().build(); + let processor = SimpleSpanProcessor::new(exporter.clone()); + let span_data = new_test_export_span_data(); + processor.on_end(span_data.clone()); + assert!(!exporter.get_finished_spans().unwrap().is_empty()); + let _result = processor.shutdown(); + // Assume shutdown is called by ensuring spans are empty in the exporter + assert!(exporter.get_finished_spans().unwrap().is_empty()); + } + + #[test] + fn test_default_const_values() { + assert_eq!(OTEL_BSP_MAX_QUEUE_SIZE, "OTEL_BSP_MAX_QUEUE_SIZE"); + assert_eq!(OTEL_BSP_MAX_QUEUE_SIZE_DEFAULT, 2048); + assert_eq!(OTEL_BSP_SCHEDULE_DELAY, "OTEL_BSP_SCHEDULE_DELAY"); + assert_eq!(OTEL_BSP_SCHEDULE_DELAY_DEFAULT.as_millis(), 5000); + assert_eq!( + OTEL_BSP_MAX_EXPORT_BATCH_SIZE, + "OTEL_BSP_MAX_EXPORT_BATCH_SIZE" + ); + assert_eq!(OTEL_BSP_MAX_EXPORT_BATCH_SIZE_DEFAULT, 512); + assert_eq!(OTEL_BSP_EXPORT_TIMEOUT, "OTEL_BSP_EXPORT_TIMEOUT"); + assert_eq!(OTEL_BSP_EXPORT_TIMEOUT_DEFAULT.as_millis(), 30000); + } + + #[test] + fn test_default_batch_config_adheres_to_specification() { + let env_vars = vec![ + OTEL_BSP_SCHEDULE_DELAY, + OTEL_BSP_EXPORT_TIMEOUT, + OTEL_BSP_MAX_QUEUE_SIZE, + OTEL_BSP_MAX_EXPORT_BATCH_SIZE, + OTEL_BSP_MAX_CONCURRENT_EXPORTS, + ]; + + let config = temp_env::with_vars_unset(env_vars, BatchConfig::default); + + assert_eq!( + config.max_concurrent_exports, + OTEL_BSP_MAX_CONCURRENT_EXPORTS_DEFAULT + ); + assert_eq!(config.scheduled_delay, OTEL_BSP_SCHEDULE_DELAY_DEFAULT); + assert_eq!(config.max_export_timeout, OTEL_BSP_EXPORT_TIMEOUT_DEFAULT); + assert_eq!(config.max_queue_size, OTEL_BSP_MAX_QUEUE_SIZE_DEFAULT); + assert_eq!( + config.max_export_batch_size, + OTEL_BSP_MAX_EXPORT_BATCH_SIZE_DEFAULT + ); + } + + #[test] + fn test_code_based_config_overrides_env_vars() { + let env_vars = vec![ + (OTEL_BSP_EXPORT_TIMEOUT, Some("60000")), + (OTEL_BSP_MAX_CONCURRENT_EXPORTS, Some("5")), + (OTEL_BSP_MAX_EXPORT_BATCH_SIZE, Some("1024")), + (OTEL_BSP_MAX_QUEUE_SIZE, Some("4096")), + (OTEL_BSP_SCHEDULE_DELAY, Some("2000")), + ]; + + temp_env::with_vars(env_vars, || { + let config = BatchConfigBuilder::default() + .with_max_export_batch_size(512) + .with_max_queue_size(2048) + .with_scheduled_delay(Duration::from_millis(1000)); + #[cfg(feature = "experimental_trace_batch_span_processor_with_async_runtime")] + let config = { + config + .with_max_concurrent_exports(10) + .with_max_export_timeout(Duration::from_millis(2000)) + }; + let config = config.build(); + + assert_eq!(config.max_export_batch_size, 512); + assert_eq!(config.max_queue_size, 2048); + assert_eq!(config.scheduled_delay, Duration::from_millis(1000)); + #[cfg(feature = "experimental_trace_batch_span_processor_with_async_runtime")] + { + assert_eq!(config.max_concurrent_exports, 10); + assert_eq!(config.max_export_timeout, Duration::from_millis(2000)); + } + }); + } + + #[test] + fn test_batch_config_configurable_by_env_vars() { + let env_vars = vec![ + (OTEL_BSP_SCHEDULE_DELAY, Some("2000")), + (OTEL_BSP_EXPORT_TIMEOUT, Some("60000")), + (OTEL_BSP_MAX_QUEUE_SIZE, Some("4096")), + (OTEL_BSP_MAX_EXPORT_BATCH_SIZE, Some("1024")), + ]; + + let config = temp_env::with_vars(env_vars, BatchConfig::default); + + assert_eq!(config.scheduled_delay, Duration::from_millis(2000)); + assert_eq!(config.max_export_timeout, Duration::from_millis(60000)); + assert_eq!(config.max_queue_size, 4096); + assert_eq!(config.max_export_batch_size, 1024); + } + + #[test] + fn test_batch_config_max_export_batch_size_validation() { + let env_vars = vec![ + (OTEL_BSP_MAX_QUEUE_SIZE, Some("256")), + (OTEL_BSP_MAX_EXPORT_BATCH_SIZE, Some("1024")), + ]; + + let config = temp_env::with_vars(env_vars, BatchConfig::default); + + assert_eq!(config.max_queue_size, 256); + assert_eq!(config.max_export_batch_size, 256); + assert_eq!(config.scheduled_delay, OTEL_BSP_SCHEDULE_DELAY_DEFAULT); + assert_eq!(config.max_export_timeout, OTEL_BSP_EXPORT_TIMEOUT_DEFAULT); + } + + #[test] + fn test_batch_config_with_fields() { + let batch = BatchConfigBuilder::default() + .with_max_export_batch_size(10) + .with_scheduled_delay(Duration::from_millis(10)) + .with_max_queue_size(10); + #[cfg(feature = "experimental_trace_batch_span_processor_with_async_runtime")] + let batch = { + batch + .with_max_concurrent_exports(10) + .with_max_export_timeout(Duration::from_millis(10)) + }; + let batch = batch.build(); + assert_eq!(batch.max_export_batch_size, 10); + assert_eq!(batch.scheduled_delay, Duration::from_millis(10)); + assert_eq!(batch.max_queue_size, 10); + #[cfg(feature = "experimental_trace_batch_span_processor_with_async_runtime")] + { + assert_eq!(batch.max_concurrent_exports, 10); + assert_eq!(batch.max_export_timeout, Duration::from_millis(10)); + } + } + + // Helper function to create a default test span + fn create_test_span(name: &str) -> SpanData { + SpanData { + span_context: SpanContext::empty_context(), + parent_span_id: SpanId::INVALID, + parent_span_is_remote: false, + span_kind: SpanKind::Internal, + name: name.to_string().into(), + start_time: opentelemetry::time::now(), + end_time: opentelemetry::time::now(), + attributes: Vec::new(), + dropped_attributes_count: 0, + events: SpanEvents::default(), + links: SpanLinks::default(), + status: Status::Unset, + instrumentation_scope: Default::default(), + } + } + + use crate::Resource; + use opentelemetry::{Key, KeyValue, Value}; + use std::{ + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, + time::Instant, + }; + + // Mock exporter to test functionality + #[derive(Debug)] + struct MockSpanExporter { + exported_spans: Arc>>, + exported_resource: Arc>>, + } + + impl MockSpanExporter { + fn new() -> Self { + Self { + exported_spans: Arc::new(Mutex::new(Vec::new())), + exported_resource: Arc::new(Mutex::new(None)), + } + } + } + + impl SpanExporter for MockSpanExporter { + async fn export(&self, batch: Vec) -> OTelSdkResult { + let exported_spans = self.exported_spans.clone(); + exported_spans.lock().unwrap().extend(batch); + Ok(()) + } + + fn shutdown(&self) -> OTelSdkResult { + Ok(()) + } + fn set_resource(&mut self, resource: &Resource) { + let mut exported_resource = self.exported_resource.lock().unwrap(); + *exported_resource = Some(resource.clone()); + } + } + + #[test] + fn batchspanprocessor_handles_on_end() { + let exporter = MockSpanExporter::new(); + let exporter_shared = exporter.exported_spans.clone(); + let config = BatchConfigBuilder::default() + .with_max_queue_size(10) + .with_max_export_batch_size(10) + .with_scheduled_delay(Duration::from_secs(5)) + .build(); + let processor = BatchSpanProcessor::new(exporter, config); + + let test_span = create_test_span("test_span"); + processor.on_end(test_span.clone()); + + // Wait for flush interval to ensure the span is processed + std::thread::sleep(Duration::from_secs(6)); + + let exported_spans = exporter_shared.lock().unwrap(); + assert_eq!(exported_spans.len(), 1); + assert_eq!(exported_spans[0].name, "test_span"); + } + + #[test] + fn batchspanprocessor_force_flush() { + let exporter = MockSpanExporter::new(); + let exporter_shared = exporter.exported_spans.clone(); // Shared access to verify exported spans + let config = BatchConfigBuilder::default() + .with_max_queue_size(10) + .with_max_export_batch_size(10) + .with_scheduled_delay(Duration::from_secs(5)) + .build(); + let processor = BatchSpanProcessor::new(exporter, config); + + // Create a test span and send it to the processor + let test_span = create_test_span("force_flush_span"); + processor.on_end(test_span.clone()); + + // Call force_flush to immediately export the spans + let flush_result = processor.force_flush(); + assert!(flush_result.is_ok(), "Force flush failed unexpectedly"); + + // Verify the exported spans in the mock exporter + let exported_spans = exporter_shared.lock().unwrap(); + assert_eq!( + exported_spans.len(), + 1, + "Unexpected number of exported spans" + ); + assert_eq!(exported_spans[0].name, "force_flush_span"); + } + + #[test] + fn batchspanprocessor_does_not_overdrain_unaccounted_spans() { + let exporter = MockSpanExporter::new(); + let exported_spans = exporter.exported_spans.clone(); + let (sender, receiver) = std::sync::mpsc::sync_channel(4); + let current_batch_size = AtomicUsize::new(1); + let config = BatchConfigBuilder::default() + .with_max_queue_size(4) + .with_max_export_batch_size(4) + .build(); + let mut spans = Vec::with_capacity(config.max_export_batch_size); + let mut last_export_time = Instant::now(); + + sender.send(create_test_span("counted")).unwrap(); + sender.send(create_test_span("unaccounted")).unwrap(); + + let result = BatchSpanProcessor::get_spans_and_export( + &receiver, + &exporter, + &mut spans, + &mut last_export_time, + ¤t_batch_size, + &config, + &|_count: u64| {}, + ); + + assert!(result.is_ok(), "export should succeed"); + assert_eq!( + current_batch_size.load(Ordering::Relaxed), + 0, + "helper should only subtract the counted span" + ); + assert_eq!( + exported_spans.lock().unwrap().len(), + 1, + "helper should export at most the target batch size snapshot" + ); + assert!( + receiver.try_recv().is_ok(), + "one span should remain queued for a later export cycle" + ); + } + + #[test] + fn batchspanprocessor_drain_handles_counted_but_not_yet_enqueued_spans() { + // Since on_end() increments `current_batch_size` before enqueueing + // (issue #3453), a concurrent drain can observe a counter that is + // higher than the channel depth. The drain must export what is + // available, keep the surplus count intact (no underflow), and pick + // the late span up on a later cycle. + let exporter = MockSpanExporter::new(); + let exported_spans = exporter.exported_spans.clone(); + let (sender, receiver) = std::sync::mpsc::sync_channel(4); + // Two spans counted, but only one has landed in the channel so far. + let current_batch_size = AtomicUsize::new(2); + let config = BatchConfigBuilder::default() + .with_max_queue_size(4) + .with_max_export_batch_size(4) + .build(); + let mut spans = Vec::with_capacity(config.max_export_batch_size); + let mut last_export_time = Instant::now(); + + sender.send(create_test_span("landed")).unwrap(); + + let result = BatchSpanProcessor::get_spans_and_export( + &receiver, + &exporter, + &mut spans, + &mut last_export_time, + ¤t_batch_size, + &config, + &|_count: u64| {}, + ); + + assert!(result.is_ok(), "export should succeed"); + assert_eq!( + exported_spans.lock().unwrap().len(), + 1, + "only the span that landed in the channel can be exported" + ); + assert_eq!( + current_batch_size.load(Ordering::Relaxed), + 1, + "the count of the not-yet-enqueued span must survive the drain" + ); + + // The late span lands; a later drain cycle must export it and settle + // the counter back to zero. + sender.send(create_test_span("late")).unwrap(); + let result = BatchSpanProcessor::get_spans_and_export( + &receiver, + &exporter, + &mut spans, + &mut last_export_time, + ¤t_batch_size, + &config, + &|_count: u64| {}, + ); + + assert!(result.is_ok(), "export should succeed"); + assert_eq!( + exported_spans.lock().unwrap().len(), + 2, + "the late span should be exported on the next cycle" + ); + assert_eq!( + current_batch_size.load(Ordering::Relaxed), + 0, + "counter should settle to zero once everything is exported" + ); + } + + #[derive(Debug)] + struct BlockingExporter { + exported_count: Arc, + export_started: std::sync::mpsc::SyncSender<()>, + release: Arc>>, + } + + impl SpanExporter for BlockingExporter { + async fn export(&self, batch: Vec) -> OTelSdkResult { + let _ = self.export_started.try_send(()); + // Block until the test releases the export. + let _ = self.release.lock().unwrap().recv(); + self.exported_count.fetch_add(batch.len(), Ordering::SeqCst); + Ok(()) + } + } + + #[test] + fn batchspanprocessor_on_end_reverts_count_when_queue_full() { + let (started_sender, started_receiver) = std::sync::mpsc::sync_channel(8); + let (release_sender, release_receiver) = std::sync::mpsc::sync_channel(8); + let exported_count = Arc::new(AtomicUsize::new(0)); + let exporter = BlockingExporter { + exported_count: exported_count.clone(), + export_started: started_sender, + release: Arc::new(Mutex::new(release_receiver)), + }; + let config = BatchConfigBuilder::default() + .with_max_queue_size(4) + .with_max_export_batch_size(4) + .with_scheduled_delay(Duration::from_secs(60)) + .build(); + let processor = BatchSpanProcessor::new(exporter, config); + + // Fill the queue to the export threshold; the worker drains all four + // spans and blocks inside export(). + for _ in 0..4 { + processor.on_end(create_test_span("first_batch")); + } + started_receiver + .recv_timeout(Duration::from_secs(5)) + .expect("worker should start exporting the first batch"); + + // While the worker is blocked, refill the queue and overflow it by + // two spans, which must be dropped and their counts reverted. + for _ in 0..4 { + processor.on_end(create_test_span("second_batch")); + } + for _ in 0..2 { + processor.on_end(create_test_span("overflow")); + } + + assert_eq!(processor.dropped_spans_count.load(Ordering::Relaxed), 2); + // 4 spans in-flight in the blocked export (not yet subtracted) plus + // 4 spans queued. Without the queue-full revert this would read 10. + assert_eq!( + processor.current_batch_size.load(Ordering::Relaxed), + 8, + "dropped spans must not remain counted as pending" + ); + + // Release the in-flight export and the one triggered by force_flush. + release_sender.send(()).unwrap(); + release_sender.send(()).unwrap(); + let flush_result = processor.force_flush(); + assert!(flush_result.is_ok(), "force flush failed unexpectedly"); + + assert_eq!( + exported_count.load(Ordering::SeqCst), + 8, + "all spans that entered the queue must be exported" + ); + assert_eq!( + processor.current_batch_size.load(Ordering::Relaxed), + 0, + "counter should settle to zero; a leftover value indicates the \ + queue-full path did not revert its increment" + ); + } + + /// A slow exporter that counts the number of spans received. + /// Used for stress testing the BatchSpanProcessor. + #[derive(Debug)] + struct CountingSpanExporter { + count: Arc, + } + + impl SpanExporter for CountingSpanExporter { + async fn export(&self, batch: Vec) -> OTelSdkResult { + self.count.fetch_add(batch.len(), Ordering::SeqCst); + // Simulate slow export to cause queue buildup and drops + std::thread::sleep(Duration::from_millis(20)); + Ok(()) + } + } + + /// Stress test that verifies all spans are accounted for. + /// With multiple threads pushing spans faster than the exporter can + /// handle, some spans will inevitably be dropped. This test validates: + /// total_spans_sent == spans_received_by_exporter + spans_dropped + #[test] + fn batchspanprocessor_all_spans_accounted_for() { + let count = Arc::new(AtomicUsize::new(0)); + let exporter = CountingSpanExporter { + count: count.clone(), + }; + + let config = BatchConfigBuilder::default() + .with_max_queue_size(2048) + .with_max_export_batch_size(512) + .with_scheduled_delay(Duration::from_millis(5)) + .build(); + + let processor = BatchSpanProcessor::new(exporter, config); + + let total_spans_per_thread = 10_000; + let num_threads = 4; + let total_spans_to_emit = total_spans_per_thread * num_threads; + + std::thread::scope(|s| { + for _ in 0..num_threads { + s.spawn(|| { + for _ in 0..total_spans_per_thread { + processor.on_end(create_test_span("stress test span")); + } + }); + } + }); + + // Shutdown the processor to ensure all buffered spans are flushed + processor.shutdown().unwrap(); + + let spans_received = count.load(Ordering::SeqCst); + let spans_dropped = processor.dropped_spans_count.load(Ordering::SeqCst); + + // The invariant: every span is either received or dropped + assert_eq!( + spans_received + spans_dropped, + total_spans_to_emit, + "Spans unaccounted for! Received: {spans_received}, Dropped: {spans_dropped}, Total emitted: {total_spans_to_emit}" + ); + } + + #[test] + fn batchspanprocessor_shutdown() { + // Setup exporter and processor - following the same pattern as test_batch_shutdown from logs + let exporter = InMemorySpanExporterBuilder::new() + .keep_records_on_shutdown() + .build(); + let processor = BatchSpanProcessor::new(exporter.clone(), BatchConfig::default()); + + let record = create_test_span("test_span"); + + processor.on_end(record); + processor.force_flush().unwrap(); + processor.shutdown().unwrap(); + + // todo: expect to see errors here. How should we assert this? + processor.on_end(create_test_span("after_shutdown_span")); + + assert_eq!(1, exporter.get_finished_spans().unwrap().len()); + assert!(exporter.is_shutdown_called()); + } + + #[test] + fn batchspanprocessor_handles_dropped_spans() { + // This test verifies that BSP drops spans when the queue is full. + + #[derive(Debug)] + struct SlowExporter { + exported_count: Arc, + } + + impl SpanExporter for SlowExporter { + async fn export(&self, batch: Vec) -> OTelSdkResult { + // Simulate slow export + std::thread::sleep(Duration::from_millis(50)); + self.exported_count + .fetch_add(batch.len(), Ordering::Relaxed); + Ok(()) + } + + fn shutdown(&self) -> OTelSdkResult { + Ok(()) + } + + fn set_resource(&mut self, _resource: &Resource) {} + } + + let exported_count = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let exporter = SlowExporter { + exported_count: exported_count.clone(), + }; + + let max_queue_size = 10; + let config = BatchConfigBuilder::default() + .with_max_queue_size(max_queue_size) + .with_max_export_batch_size(5) + .with_scheduled_delay(Duration::from_millis(10)) + .build(); + let processor = BatchSpanProcessor::new(exporter, config); + + // Rapidly send many more spans than the queue can hold + let total_spans_to_send = 100; + for i in 0..total_spans_to_send { + let span = create_test_span(&format!("span_{}", i)); + processor.on_end(span); + } + + // Force flush any remaining spans - this waits for export to complete + let _ = processor.force_flush(); + + let dropped = processor.dropped_spans_count.load(Ordering::Relaxed); + let exported = exported_count.load(Ordering::Relaxed); + + // Verify that dropped + exported = total (every span is accounted for) + assert_eq!( + dropped + exported, + total_spans_to_send, + "dropped ({}) + exported ({}) should equal total sent ({})", + dropped, + exported, + total_spans_to_send + ); + + // With 100 spans sent rapidly and a slow exporter, we should have some drops + assert!( + dropped > 0, + "Expected some spans to be dropped due to full queue. Exported: {}", + exported + ); + } + + #[test] + fn batchspanprocessor_sync_ignores_max_concurrent_exports() { + #[derive(Debug)] + struct TrackingExporter { + active: Arc, + max_inflight: Arc, + export_calls: Arc, + delay: Duration, + } + + impl SpanExporter for TrackingExporter { + async fn export(&self, _batch: Vec) -> OTelSdkResult { + self.export_calls.fetch_add(1, Ordering::SeqCst); + let inflight = self.active.fetch_add(1, Ordering::SeqCst) + 1; + self.max_inflight.fetch_max(inflight, Ordering::SeqCst); + + std::thread::sleep(self.delay); + self.active.fetch_sub(1, Ordering::SeqCst); + Ok(()) + } + } + + let active = Arc::new(AtomicUsize::new(0)); + let max_inflight = Arc::new(AtomicUsize::new(0)); + let export_calls = Arc::new(AtomicUsize::new(0)); + let exporter = TrackingExporter { + active: active.clone(), + max_inflight: max_inflight.clone(), + export_calls: export_calls.clone(), + delay: Duration::from_millis(50), + }; + + let config = BatchConfig { + max_export_batch_size: 1, + max_queue_size: 16, + scheduled_delay: Duration::from_secs(3600), + max_export_timeout: Duration::from_secs(5), + max_concurrent_exports: 4, + }; + + let processor = BatchSpanProcessor::new(exporter, config); + + processor.on_end(new_test_export_span_data()); + processor.on_end(new_test_export_span_data()); + processor.on_end(new_test_export_span_data()); + + processor.force_flush().expect("force flush failed"); + processor.shutdown().expect("shutdown failed"); + + assert_eq!( + export_calls.load(Ordering::SeqCst), + 3, + "expected three exports for three spans with max_export_batch_size=1" + ); + assert_eq!( + max_inflight.load(Ordering::SeqCst), + 1, + "sync BatchSpanProcessor should export serially regardless of max_concurrent_exports" + ); + } + + #[test] + fn validate_span_attributes_exported_correctly() { + let exporter = MockSpanExporter::new(); + let exporter_shared = exporter.exported_spans.clone(); + let config = BatchConfigBuilder::default().build(); + let processor = BatchSpanProcessor::new(exporter, config); + + // Create a span with attributes + let mut span_data = create_test_span("attribute_validation"); + span_data.attributes = vec![ + KeyValue::new("key1", "value1"), + KeyValue::new("key2", "value2"), + ]; + processor.on_end(span_data.clone()); + + // Force flush to export the span + let _ = processor.force_flush(); + + // Validate the exported attributes + let exported_spans = exporter_shared.lock().unwrap(); + assert_eq!(exported_spans.len(), 1); + let exported_span = &exported_spans[0]; + assert!(exported_span + .attributes + .contains(&KeyValue::new("key1", "value1"))); + assert!(exported_span + .attributes + .contains(&KeyValue::new("key2", "value2"))); + } + + #[test] + fn batchspanprocessor_sets_and_exports_with_resource() { + let exporter = MockSpanExporter::new(); + let exporter_shared = exporter.exported_spans.clone(); + let resource_shared = exporter.exported_resource.clone(); + let config = BatchConfigBuilder::default().build(); + let mut processor = BatchSpanProcessor::new(exporter, config); + + // Set a resource for the processor + let resource = Resource::builder_empty() + .with_attributes(vec![KeyValue::new("service.name", "test_service")]) + .build(); + processor.set_resource(&resource); + + // Create a span and send it to the processor + let test_span = create_test_span("resource_test"); + processor.on_end(test_span.clone()); + + // Force flush to ensure the span is exported + let _ = processor.force_flush(); + + // Validate spans are exported + let exported_spans = exporter_shared.lock().unwrap(); + assert_eq!(exported_spans.len(), 1); + + // Validate the resource is correctly set in the exporter + let exported_resource = resource_shared.lock().unwrap(); + assert!(exported_resource.is_some()); + assert_eq!( + exported_resource + .as_ref() + .unwrap() + .get(&Key::new("service.name")), + Some(Value::from("test_service")) + ); + } + + #[tokio::test(flavor = "current_thread")] + async fn test_batch_processor_current_thread_runtime() { + let exporter = MockSpanExporter::new(); + let exporter_shared = exporter.exported_spans.clone(); + + let config = BatchConfigBuilder::default() + .with_max_queue_size(5) + .with_max_export_batch_size(3) + .build(); + + let processor = BatchSpanProcessor::new(exporter, config); + + for _ in 0..4 { + let span = new_test_export_span_data(); + processor.on_end(span); + } + + processor.force_flush().unwrap(); + + let exported_spans = exporter_shared.lock().unwrap(); + assert_eq!(exported_spans.len(), 4); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_batch_processor_multi_thread_count_1_runtime() { + let exporter = MockSpanExporter::new(); + let exporter_shared = exporter.exported_spans.clone(); + + let config = BatchConfigBuilder::default() + .with_max_queue_size(5) + .with_max_export_batch_size(3) + .build(); + + let processor = BatchSpanProcessor::new(exporter, config); + + for _ in 0..4 { + let span = new_test_export_span_data(); + processor.on_end(span); + } + + processor.force_flush().unwrap(); + + let exported_spans = exporter_shared.lock().unwrap(); + assert_eq!(exported_spans.len(), 4); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + async fn test_batch_processor_multi_thread() { + let exporter = MockSpanExporter::new(); + let exporter_shared = exporter.exported_spans.clone(); + + let config = BatchConfigBuilder::default() + .with_max_queue_size(20) + .with_max_export_batch_size(5) + .build(); + + // Create the processor with the thread-safe exporter + let processor = Arc::new(BatchSpanProcessor::new(exporter, config)); + + let mut handles = vec![]; + for _ in 0..10 { + let processor_clone = Arc::clone(&processor); + let handle = tokio::spawn(async move { + let span = new_test_export_span_data(); + processor_clone.on_end(span); + }); + handles.push(handle); + } + + for handle in handles { + handle.await.unwrap(); + } + + processor.force_flush().unwrap(); + + // Verify exported spans + let exported_spans = exporter_shared.lock().unwrap(); + assert_eq!(exported_spans.len(), 10); + } + + /// Sums the values of `otel.sdk.processor.span.processed` data points whose + /// `error.type` attribute equals `error_type` (or that have no `error.type` + /// attribute when `error_type` is `None`). + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn sum_processed_spans( + metric_exporter: &crate::metrics::InMemoryMetricExporter, + error_type: Option<&str>, + ) -> u64 { + use crate::metrics::data::{AggregatedMetrics, MetricData}; + + let metrics = metric_exporter.get_finished_metrics().unwrap(); + let mut total: u64 = 0; + for rm in &metrics { + for sm in &rm.scope_metrics { + for metric in &sm.metrics { + if metric.name == "otel.sdk.processor.span.processed" { + if let AggregatedMetrics::U64(MetricData::Sum(sum)) = &metric.data { + for dp in sum.data_points() { + let dp_error_type = dp + .attributes() + .find(|kv| kv.key.as_str() == "error.type") + .map(|kv| kv.value.as_str().to_string()); + let matches = match error_type { + Some(expected) => dp_error_type.as_deref() == Some(expected), + None => dp_error_type.is_none(), + }; + if matches { + total += dp.value(); + } + } + } + } + } + } + } + total + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + mod self_obs { + use super::*; + + /// Verifies that `otel.sdk.processor.span.processed` counts spans (with no + /// `error.type`) when the processor submits a batch to the exporter. + /// + /// `#[ignore]`d because it mutates process-wide state via + /// `global::set_meter_provider()`. CI runs it in isolation via `test.sh`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + #[ignore] + fn self_diagnostics_counter_records_success() { + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + let span_exporter = InMemorySpanExporterBuilder::new().build(); + let config = BatchConfigBuilder::default() + .with_max_queue_size(256) + .with_max_export_batch_size(64) + .with_scheduled_delay(Duration::from_secs(60)) + .build(); + let processor = BatchSpanProcessor::new(span_exporter, config); + + for _ in 0..10 { + processor.on_end(create_test_span("success")); + } + + // Flush so the batch is submitted to the exporter, which is when the + // counter is incremented. + processor.force_flush().unwrap(); + meter_provider.force_flush().unwrap(); + + let processed = sum_processed_spans(&metric_exporter, None); + assert_eq!( + processed, 10, + "expected 10 processed spans, got {processed}" + ); + + processor.shutdown().unwrap(); + meter_provider.shutdown().unwrap(); + } + + /// Verifies that `otel.sdk.processor.span.processed` records queue-full drops + /// with `error.type = queue_full` when spans overflow the queue while the + /// worker is blocked exporting. + /// + /// `#[ignore]`d because it mutates process-wide state via + /// `global::set_meter_provider()`. CI runs it in isolation via `test.sh`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + #[ignore] + fn self_diagnostics_counter_records_queue_full_drops() { + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + let (started_sender, started_receiver) = std::sync::mpsc::sync_channel(8); + let (release_sender, release_receiver) = std::sync::mpsc::sync_channel(8); + let exported_count = Arc::new(AtomicUsize::new(0)); + let exporter = BlockingExporter { + exported_count: exported_count.clone(), + export_started: started_sender, + release: Arc::new(Mutex::new(release_receiver)), + }; + let config = BatchConfigBuilder::default() + .with_max_queue_size(4) + .with_max_export_batch_size(4) + .with_scheduled_delay(Duration::from_secs(60)) + .build(); + let processor = BatchSpanProcessor::new(exporter, config); + + // Fill the queue to the export threshold; the worker drains all four + // spans and blocks inside export(). + for _ in 0..4 { + processor.on_end(create_test_span("first_batch")); + } + started_receiver + .recv_timeout(Duration::from_secs(5)) + .expect("worker should start exporting the first batch"); + + // While the worker is blocked, refill the queue (4) and overflow it by + // two spans, which must be dropped and counted as queue_full. + for _ in 0..4 { + processor.on_end(create_test_span("second_batch")); + } + for _ in 0..2 { + processor.on_end(create_test_span("overflow")); + } + + // Release the in-flight export and the one triggered by force_flush. + release_sender.send(()).unwrap(); + release_sender.send(()).unwrap(); + processor.force_flush().unwrap(); + meter_provider.force_flush().unwrap(); + + let queue_full = sum_processed_spans(&metric_exporter, Some("queue_full")); + assert_eq!( + queue_full, 2, + "expected 2 queue_full drops, got {queue_full}" + ); + + processor.shutdown().unwrap(); + meter_provider.shutdown().unwrap(); + } + + /// Verifies that `otel.sdk.processor.span.processed` records post-shutdown + /// emits with `error.type = already_shutdown`. + /// + /// `#[ignore]`d because it mutates process-wide state via + /// `global::set_meter_provider()`. CI runs it in isolation via `test.sh`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + #[ignore] + fn self_diagnostics_counter_records_already_shutdown_drops() { + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + let span_exporter = InMemorySpanExporterBuilder::new().build(); + let processor = BatchSpanProcessor::new(span_exporter, BatchConfig::default()); + + // Shut the processor down so the worker thread (the only receiver) + // disconnects; subsequent on_end calls hit the already_shutdown branch. + processor.shutdown().unwrap(); + + for _ in 0..7 { + processor.on_end(create_test_span("after_shutdown")); + } + + meter_provider.force_flush().unwrap(); + + let already_shutdown = sum_processed_spans(&metric_exporter, Some("already_shutdown")); + assert_eq!( + already_shutdown, 7, + "expected 7 already_shutdown drops, got {already_shutdown}" + ); + + meter_provider.shutdown().unwrap(); + } + + /// Verifies that `otel.sdk.processor.span.processed` counts spans (with no + /// `error.type`) when `SimpleSpanProcessor` submits them to the exporter. + /// + /// `#[ignore]`d because it mutates process-wide state via + /// `global::set_meter_provider()`. CI runs it in isolation via `test.sh`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + #[ignore] + fn simple_self_diagnostics_counter_records_success() { + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + let span_exporter = InMemorySpanExporterBuilder::new().build(); + let processor = SimpleSpanProcessor::new(span_exporter); + + for _ in 0..10 { + processor.on_end(new_test_export_span_data()); + } + + meter_provider.force_flush().unwrap(); + + let processed = sum_processed_spans(&metric_exporter, None); + assert_eq!( + processed, 10, + "expected 10 processed spans, got {processed}" + ); + + processor.shutdown().unwrap(); + meter_provider.shutdown().unwrap(); + } + + /// Verifies that `SimpleSpanProcessor` records post-shutdown spans with + /// `error.type = already_shutdown` and does not count them as success. + /// + /// `#[ignore]`d because it mutates process-wide state via + /// `global::set_meter_provider()`. CI runs it in isolation via `test.sh`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + #[ignore] + fn simple_self_diagnostics_counter_records_already_shutdown_drops() { + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + let span_exporter = InMemorySpanExporterBuilder::new().build(); + let processor = SimpleSpanProcessor::new(span_exporter); + + // Shut the processor down; subsequent on_end calls hit the + // already_shutdown branch. + processor.shutdown().unwrap(); + + for _ in 0..7 { + processor.on_end(new_test_export_span_data()); + } + + meter_provider.force_flush().unwrap(); + + let already_shutdown = sum_processed_spans(&metric_exporter, Some("already_shutdown")); + assert_eq!( + already_shutdown, 7, + "expected 7 already_shutdown drops, got {already_shutdown}" + ); + let success = sum_processed_spans(&metric_exporter, None); + assert_eq!( + success, 0, + "post-shutdown spans must not be counted as success, got {success}" + ); + + meter_provider.shutdown().unwrap(); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_processor_with_async_runtime.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_processor_with_async_runtime.rs new file mode 100644 index 00000000..a6676247 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_processor_with_async_runtime.rs @@ -0,0 +1,706 @@ +use crate::error::{OTelSdkError, OTelSdkResult}; +use crate::resource::Resource; +use crate::runtime::{to_interval_stream, RuntimeChannel, TrySend}; +use crate::trace::BatchConfig; +use crate::trace::Span; +use crate::trace::SpanProcessor; +use crate::trace::{SpanData, SpanExporter}; +use futures_channel::oneshot; +use futures_util::{ + future::{self, BoxFuture, Either}, + pin_mut, select, + stream::{self, FusedStream, FuturesUnordered}, + StreamExt as _, +}; +use opentelemetry::Context; +use opentelemetry::{otel_debug, otel_error, otel_warn}; +use std::fmt; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; +use std::time::Duration; +use tokio::sync::RwLock; + +/// A [`SpanProcessor`] that asynchronously buffers finished spans and reports +/// them at a preconfigured interval. +/// +/// Batch span processors need to run a background task to collect and send +/// spans. Different runtimes need different ways to handle the background task. +/// +/// Note: Configuring an opentelemetry `Runtime` that's not compatible with the +/// underlying runtime can cause deadlocks (see tokio section). +/// +/// ### Use with Tokio +/// +/// Tokio currently offers two different schedulers. One is +/// `current_thread_scheduler`, the other is `multiple_thread_scheduler`. Both +/// of them default to use batch span processors to install span exporters. +/// +/// Tokio's `current_thread_scheduler` can cause the program to hang forever if +/// blocking work is scheduled with other tasks in the same runtime. To avoid +/// this, be sure to enable the `rt-tokio-current-thread` feature in this crate +/// if you are using that runtime (e.g. users of actix-web), and blocking tasks +/// will then be scheduled on a different thread. +/// +/// # Examples +/// +/// This processor can be configured with an [`executor`] of your choice to +/// batch and upload spans asynchronously when they end. If you have added a +/// library like [`tokio`], you can pass in their respective +/// `spawn` and `interval` functions to have batching performed in those +/// contexts. +/// +/// ``` +/// # #[cfg(feature="tokio")] +/// # { +/// use opentelemetry::global; +/// use opentelemetry_sdk::{runtime, testing::trace::NoopSpanExporter, trace}; +/// use opentelemetry_sdk::trace::BatchConfigBuilder; +/// use std::time::Duration; +/// use opentelemetry_sdk::trace::span_processor_with_async_runtime::BatchSpanProcessor; +/// +/// #[tokio::main] +/// async fn main() { +/// // Configure your preferred exporter +/// let exporter = NoopSpanExporter::new(); +/// +/// // Create a batch span processor using an exporter and a runtime +/// let batch = BatchSpanProcessor::builder(exporter, runtime::Tokio) +/// .with_batch_config(BatchConfigBuilder::default().with_max_queue_size(4096).build()) +/// .build(); +/// +/// // Then use the `with_batch_exporter` method to have the provider export spans in batches. +/// let provider = trace::SdkTracerProvider::builder() +/// .with_span_processor(batch) +/// .build(); +/// +/// let _ = global::set_tracer_provider(provider); +/// } +/// # } +/// ``` +/// +/// [`executor`]: https://docs.rs/futures/0.3/futures/executor/index.html +/// [`tokio`]: https://tokio.rs +pub struct BatchSpanProcessor { + message_sender: R::Sender, + + // Track dropped spans + dropped_spans_count: AtomicUsize, + + // Track the maximum queue size that was configured for this processor + max_queue_size: usize, +} + +impl fmt::Debug for BatchSpanProcessor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BatchSpanProcessor") + .field("message_sender", &self.message_sender) + .finish() + } +} + +impl SpanProcessor for BatchSpanProcessor { + fn on_start(&self, _span: &mut Span, _cx: &Context) { + // Ignored + } + + fn on_end(&self, span: SpanData) { + if !span.span_context.is_sampled() { + return; + } + + let result = self.message_sender.try_send(BatchMessage::ExportSpan(span)); + + // If the queue is full, and we can't buffer a span + if result.is_err() { + // Increment the number of dropped spans. If this is the first time we've had to drop, + // emit a warning. + if self.dropped_spans_count.fetch_add(1, Ordering::Relaxed) == 0 { + otel_warn!(name: "BatchSpanProcessor.SpanDroppingStarted", + message = "Beginning to drop span messages due to full/internal errors. No further log will be emitted for further drops until Shutdown. During Shutdown time, a log will be emitted with exact count of total spans dropped."); + } + } + } + + fn force_flush(&self) -> OTelSdkResult { + let (res_sender, res_receiver) = oneshot::channel(); + self.message_sender + .try_send(BatchMessage::Flush(Some(res_sender))) + .map_err(|err| { + OTelSdkError::InternalFailure(format!("Failed to send flush message: {err}")) + })?; + + futures_executor::block_on(res_receiver).map_err(|err| { + OTelSdkError::InternalFailure(format!("Flush response channel error: {err}")) + })? + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + let dropped_spans = self.dropped_spans_count.load(Ordering::Relaxed); + let max_queue_size = self.max_queue_size; + if dropped_spans > 0 { + otel_warn!( + name: "BatchSpanProcessor.Shutdown", + dropped_spans = dropped_spans, + max_queue_size = max_queue_size, + message = "Spans were dropped due to a full or closed queue. The count represents the total count of span records dropped in the lifetime of the BatchSpanProcessor. Consider increasing the queue size and/or decrease delay between intervals." + ); + } + + let (res_sender, res_receiver) = oneshot::channel(); + self.message_sender + .try_send(BatchMessage::Shutdown(res_sender)) + .map_err(|err| { + OTelSdkError::InternalFailure(format!("Failed to send shutdown message: {err}")) + })?; + + futures_executor::block_on(res_receiver).map_err(|err| { + OTelSdkError::InternalFailure(format!("Shutdown response channel error: {err}")) + })? + } + + fn set_resource(&mut self, resource: &Resource) { + let resource = Arc::new(resource.clone()); + let _ = self + .message_sender + .try_send(BatchMessage::SetResource(resource)); + } +} + +/// Messages sent between application thread and batch span processor's work thread. +// In this enum the size difference is not a concern because: +// 1. If we wrap SpanData into a pointer, it will add overhead when processing. +// 2. Most of the messages will be ExportSpan. +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +enum BatchMessage { + /// Export spans, usually called when span ends + ExportSpan(SpanData), + /// Flush the current buffer to the backend, it can be triggered by + /// pre configured interval or a call to `force_push` function. + Flush(Option>), + /// Shut down the worker thread, push all spans in buffer to the backend. + Shutdown(oneshot::Sender), + /// Set the resource for the exporter. + SetResource(Arc), +} + +struct BatchSpanProcessorInternal { + spans: Vec, + export_tasks: FuturesUnordered>, + runtime: R, + config: BatchConfig, + // TODO: Redesign the `SpanExporter` trait to use immutable references (`&self`) + // for all methods. This would allow us to remove the `RwLock` and just use `Arc`, + // similar to how `crate::logs::LogExporter` is implemented. + exporter: Arc>, +} + +impl BatchSpanProcessorInternal { + async fn flush(&mut self, res_channel: Option>) { + let export_result = Self::export( + self.spans.split_off(0), + self.exporter.clone(), + self.runtime.clone(), + self.config.max_export_timeout, + ) + .await; + let task = Box::pin(async move { + if let Some(channel) = res_channel { + // If a response channel is provided, attempt to send the export result through it. + if let Err(result) = channel.send(export_result) { + otel_debug!( + name: "BatchSpanProcessor.Flush.SendResultError", + reason = format!("{:?}", result) + ); + } + } else if let Err(err) = export_result { + // If no channel is provided and the export operation encountered an error, + // log the error directly here. + // TODO: Consider returning the status instead of logging it. + otel_error!( + name: "BatchSpanProcessor.Flush.ExportError", + reason = format!("{:?}", err), + message = "Failed during the export process" + ); + } + + Ok(()) + }); + + if self.config.max_concurrent_exports == 1 { + let _ = task.await; + } else { + self.export_tasks.push(task); + while self.export_tasks.next().await.is_some() {} + } + } + + /// Process a single message + /// + /// A return value of false indicates shutdown + async fn process_message(&mut self, message: BatchMessage) -> bool { + match message { + // Span has finished, add to buffer of pending spans. + BatchMessage::ExportSpan(span) => { + self.spans.push(span); + + if self.spans.len() == self.config.max_export_batch_size { + // If concurrent exports are saturated, wait for one to complete. + if !self.export_tasks.is_empty() + && self.export_tasks.len() == self.config.max_concurrent_exports + { + self.export_tasks.next().await; + } + + let batch = self.spans.split_off(0); + let exporter = self.exporter.clone(); + let runtime = self.runtime.clone(); + let max_export_timeout = self.config.max_export_timeout; + + let task = async move { + if let Err(err) = + Self::export(batch, exporter, runtime, max_export_timeout).await + { + otel_error!( + name: "BatchSpanProcessor.Export.Error", + reason = format!("{}", err) + ); + } + + Ok(()) + }; + + // Special case when not using concurrent exports + if self.config.max_concurrent_exports == 1 { + let _ = task.await; + } else { + self.export_tasks.push(Box::pin(task)); + } + } + } + // Span batch interval time reached or a force flush has been invoked, export + // current spans. + // + // This is a hint to ensure that any tasks associated with Spans for which the + // SpanProcessor had already received events prior to the call to ForceFlush + // SHOULD be completed as soon as possible, preferably before returning from + // this method. + // + // In particular, if any SpanProcessor has any associated exporter, it SHOULD + // try to call the exporter's Export with all spans for which this was not + // already done and then invoke ForceFlush on it. The built-in SpanProcessors + // MUST do so. If a timeout is specified (see below), the SpanProcessor MUST + // prioritize honoring the timeout over finishing all calls. It MAY skip or + // abort some or all Export or ForceFlush calls it has made to achieve this + // goal. + // + // NB: `force_flush` is not currently implemented on exporters; the equivalent + // would be waiting for exporter tasks to complete. In the case of + // channel-coupled exporters, they will need a `force_flush` implementation to + // properly block. + BatchMessage::Flush(res_channel) => { + self.flush(res_channel).await; + } + // Stream has terminated or processor is shutdown, return to finish execution. + BatchMessage::Shutdown(ch) => { + self.flush(Some(ch)).await; + let _ = self.exporter.write().await.shutdown(); + return false; + } + // propagate the resource + BatchMessage::SetResource(resource) => { + self.exporter.write().await.set_resource(&resource); + } + } + true + } + + async fn export( + batch: Vec, + exporter: Arc>, + runtime: R, + max_export_timeout: Duration, + ) -> OTelSdkResult { + // Batch size check for flush / shutdown. Those methods may be called + // when there's no work to do. + if batch.is_empty() { + return Ok(()); + } + + let exporter_guard = exporter.read().await; + let export = exporter_guard.export(batch); + let timeout = runtime.delay(max_export_timeout); + + pin_mut!(export); + pin_mut!(timeout); + + match future::select(export, timeout).await { + Either::Left((export_res, _)) => export_res, + Either::Right((_, _)) => Err(OTelSdkError::Timeout(max_export_timeout)), + } + } + + async fn run(mut self, mut messages: impl FusedStream + Unpin) { + loop { + select! { + // FuturesUnordered implements Fuse intelligently such that it + // will become eligible again once new tasks are added to it. + _ = self.export_tasks.next() => { + // An export task completed; do we need to do anything with it? + }, + message = messages.next() => { + match message { + Some(message) => { + if !self.process_message(message).await { + break; + } + }, + None => break, + } + }, + } + } + } +} + +impl BatchSpanProcessor { + pub(crate) fn new(exporter: E, config: BatchConfig, runtime: R) -> Self + where + E: SpanExporter + Send + Sync + 'static, + { + let (message_sender, message_receiver) = + runtime.batch_message_channel(config.max_queue_size); + + let max_queue_size = config.max_queue_size; + + let inner_runtime = runtime.clone(); + // Spawn worker process via user-defined spawn function. + runtime.spawn(async move { + // Timer will take a reference to the current runtime, so its important we do this within the + // runtime.spawn() + let ticker = to_interval_stream(inner_runtime.clone(), config.scheduled_delay) + .skip(1) // The ticker is fired immediately, so we should skip the first one to align with the interval. + .map(|_| BatchMessage::Flush(None)); + let timeout_runtime = inner_runtime.clone(); + + let messages = Box::pin(stream::select(message_receiver, ticker)); + let processor = BatchSpanProcessorInternal { + spans: Vec::new(), + export_tasks: FuturesUnordered::new(), + runtime: timeout_runtime, + config, + exporter: Arc::new(RwLock::new(exporter)), + }; + + processor.run(messages).await + }); + + // Return batch processor with link to worker + BatchSpanProcessor { + message_sender, + dropped_spans_count: AtomicUsize::new(0), + max_queue_size, + } + } + + /// Create a new batch processor builder + pub fn builder(exporter: E, runtime: R) -> BatchSpanProcessorBuilder + where + E: SpanExporter, + { + BatchSpanProcessorBuilder { + exporter, + config: Default::default(), + runtime, + } + } +} + +/// A builder for creating [`BatchSpanProcessor`] instances. +/// +#[derive(Debug)] +pub struct BatchSpanProcessorBuilder { + exporter: E, + config: BatchConfig, + runtime: R, +} + +impl BatchSpanProcessorBuilder +where + E: SpanExporter + 'static, + R: RuntimeChannel, +{ + /// Set the BatchConfig for [BatchSpanProcessorBuilder] + pub fn with_batch_config(self, config: BatchConfig) -> Self { + BatchSpanProcessorBuilder { config, ..self } + } + + /// Build a batch processor + pub fn build(self) -> BatchSpanProcessor { + BatchSpanProcessor::new(self.exporter, self.config, self.runtime) + } +} + +#[cfg(all(test, feature = "testing", feature = "trace"))] +mod tests { + // cargo test trace::span_processor::tests:: --features=testing + use super::{BatchSpanProcessor, SpanProcessor}; + use crate::error::OTelSdkResult; + use crate::runtime; + use crate::testing::trace::{new_test_export_span_data, new_test_exporter}; + use crate::trace::span_processor::{ + OTEL_BSP_EXPORT_TIMEOUT, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, OTEL_BSP_MAX_QUEUE_SIZE, + OTEL_BSP_MAX_QUEUE_SIZE_DEFAULT, OTEL_BSP_SCHEDULE_DELAY, OTEL_BSP_SCHEDULE_DELAY_DEFAULT, + }; + use crate::trace::{BatchConfig, BatchConfigBuilder, InMemorySpanExporterBuilder}; + use crate::trace::{SpanData, SpanExporter}; + use futures_util::Future; + use std::fmt::Debug; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::sync::Arc; + use std::time::Duration; + + struct BlockingExporter { + delay_for: Duration, + delay_fn: D, + } + + impl Debug for BlockingExporter + where + D: Fn(Duration) -> DS + 'static + Send + Sync, + DS: Future + Send + Sync + 'static, + { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("blocking exporter for testing") + } + } + + impl SpanExporter for BlockingExporter + where + D: Fn(Duration) -> DS + 'static + Send + Sync, + DS: Future + Send + Sync + 'static, + { + async fn export(&self, _batch: Vec) -> OTelSdkResult { + (self.delay_fn)(self.delay_for).await; + Ok(()) + } + } + + /// Exporter that records whether two exports overlap in time. + struct TrackingExporter { + /// Artificial delay to keep each export alive for a while. + delay: Duration, + /// Current number of in-flight exports. + active: Arc, + /// Set to true the first time we see overlap. + concurrent_seen: Arc, + } + + impl Debug for TrackingExporter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("tracking exporter") + } + } + + impl SpanExporter for TrackingExporter { + async fn export(&self, _batch: Vec) -> crate::error::OTelSdkResult { + // Increment in-flight counter and note any overlap. + let inflight = self.active.fetch_add(1, Ordering::SeqCst) + 1; + if inflight > 1 { + self.concurrent_seen.store(true, Ordering::SeqCst); + } + + // Keep the export "busy" for a bit. + tokio::time::sleep(self.delay).await; + + // Decrement counter. + self.active.fetch_sub(1, Ordering::SeqCst); + Ok(()) + } + } + + #[test] + fn test_build_batch_span_processor_builder() { + let mut env_vars = vec![ + (OTEL_BSP_MAX_EXPORT_BATCH_SIZE, Some("500")), + (OTEL_BSP_SCHEDULE_DELAY, Some("I am not number")), + (OTEL_BSP_EXPORT_TIMEOUT, Some("2046")), + ]; + temp_env::with_vars(env_vars.clone(), || { + let builder = BatchSpanProcessor::builder( + InMemorySpanExporterBuilder::new().build(), + runtime::Tokio, + ); + // export batch size cannot exceed max queue size + assert_eq!(builder.config.max_export_batch_size, 500); + assert_eq!( + builder.config.scheduled_delay, + OTEL_BSP_SCHEDULE_DELAY_DEFAULT + ); + assert_eq!( + builder.config.max_queue_size, + OTEL_BSP_MAX_QUEUE_SIZE_DEFAULT + ); + assert_eq!( + builder.config.max_export_timeout, + Duration::from_millis(2046) + ); + }); + + env_vars.push((OTEL_BSP_MAX_QUEUE_SIZE, Some("120"))); + + temp_env::with_vars(env_vars, || { + let builder = BatchSpanProcessor::builder( + InMemorySpanExporterBuilder::new().build(), + runtime::Tokio, + ); + assert_eq!(builder.config.max_export_batch_size, 120); + assert_eq!(builder.config.max_queue_size, 120); + }); + } + + #[tokio::test] + async fn test_batch_span_processor() { + let (exporter, mut export_receiver, _shutdown_receiver) = new_test_exporter(); + let config = BatchConfigBuilder::default() + .with_scheduled_delay(Duration::from_secs(60 * 60 * 24)) // set the tick to 24 hours so we know the span must be exported via force_flush + .build(); + let processor = BatchSpanProcessor::new(exporter, config, runtime::TokioCurrentThread); + let handle = tokio::spawn(async move { + loop { + if let Some(span) = export_receiver.recv().await { + assert_eq!(span.span_context, new_test_export_span_data().span_context); + break; + } + } + }); + tokio::time::sleep(Duration::from_secs(1)).await; // skip the first + processor.on_end(new_test_export_span_data()); + let flush_res = processor.force_flush(); + assert!(flush_res.is_ok()); + let _shutdown_result = processor.shutdown(); + + assert!( + tokio::time::timeout(Duration::from_secs(5), handle) + .await + .is_ok(), + "timed out in 5 seconds. force_flush may not export any data when called" + ); + } + + // If `time_out` is `true`, then the export should fail with a timeout. + // Else, the exporter should be able to export within the timeout duration. + async fn timeout_test_tokio(time_out: bool) { + let config = BatchConfig { + max_export_timeout: Duration::from_millis(if time_out { 5 } else { 60 }), + scheduled_delay: Duration::from_secs(60 * 60 * 24), // set the tick to 24 hours so we know the span must be exported via force_flush, + ..Default::default() + }; + let exporter = BlockingExporter { + delay_for: Duration::from_millis(if !time_out { 5 } else { 60 }), + delay_fn: tokio::time::sleep, + }; + let processor = BatchSpanProcessor::new(exporter, config, runtime::TokioCurrentThread); + tokio::time::sleep(Duration::from_secs(1)).await; // skip the first + processor.on_end(new_test_export_span_data()); + let flush_res = processor.force_flush(); + if time_out { + assert!(flush_res.is_err()); + } else { + assert!(flush_res.is_ok()); + } + let shutdown_res = processor.shutdown(); + assert!(shutdown_res.is_ok()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_timeout_tokio_timeout() { + // If time_out is true, then we ask exporter to block for 60s and set timeout to 5s. + // If time_out is false, then we ask the exporter to block for 5s and set timeout to 60s. + // Either way, the test should be finished within 5s. + timeout_test_tokio(true).await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_timeout_tokio_not_timeout() { + timeout_test_tokio(false).await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_concurrent_exports_expected() { + // Shared state for the exporter. + let active = Arc::new(AtomicUsize::new(0)); + let concurrent_seen = Arc::new(AtomicBool::new(false)); + + let exporter = TrackingExporter { + delay: Duration::from_millis(50), + active: active.clone(), + concurrent_seen: concurrent_seen.clone(), + }; + + // Intentionally tiny batch-size so every span forces an export. + let config = BatchConfig { + max_export_batch_size: 1, + max_queue_size: 16, + scheduled_delay: Duration::from_secs(3600), // effectively disabled + max_export_timeout: Duration::from_secs(5), + max_concurrent_exports: 2, // what we want to verify + }; + + // Spawn the processor. + let processor = BatchSpanProcessor::new(exporter, config, runtime::Tokio); + + // Finish three spans in rapid succession. + processor.on_end(new_test_export_span_data()); + processor.on_end(new_test_export_span_data()); + processor.on_end(new_test_export_span_data()); + + // Wait until everything has been exported. + processor.force_flush().expect("force flush failed"); + processor.shutdown().expect("shutdown failed"); + + // Expect at least one period with >1 export in flight. + assert!( + concurrent_seen.load(Ordering::SeqCst), + "exports never overlapped, processor is still serialising them" + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_exports_serial_when_max_concurrent_exports_1() { + let active = Arc::new(AtomicUsize::new(0)); + let concurrent_seen = Arc::new(AtomicBool::new(false)); + + let exporter = TrackingExporter { + delay: Duration::from_millis(50), + active: active.clone(), + concurrent_seen: concurrent_seen.clone(), + }; + + let config = BatchConfig { + max_export_batch_size: 1, + max_queue_size: 16, + scheduled_delay: Duration::from_secs(3600), + max_export_timeout: Duration::from_secs(5), + max_concurrent_exports: 1, // what we want to verify + }; + + let processor = BatchSpanProcessor::new(exporter, config, runtime::Tokio); + + // Finish several spans quickly. + processor.on_end(new_test_export_span_data()); + processor.on_end(new_test_export_span_data()); + processor.on_end(new_test_export_span_data()); + + processor.force_flush().expect("force flush failed"); + processor.shutdown().expect("shutdown failed"); + + // There must never have been more than one export in flight. + assert!( + !concurrent_seen.load(Ordering::SeqCst), + "exports overlapped even though max_concurrent_exports was 1" + ); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/tracer.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/tracer.rs new file mode 100644 index 00000000..8f57b40f --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/tracer.rs @@ -0,0 +1,620 @@ +//! # Tracer +//! +//! The OpenTelemetry library achieves in-process context propagation of +//! `Span`s by way of the `Tracer`. +//! +//! The `Tracer` is responsible for tracking the currently active `Span`, +//! and exposes methods for creating and activating new `Spans`. +//! +//! Docs: +use crate::trace::{ + provider::SdkTracerProvider, + span::{Span, SpanData}, + SamplingDecision, SpanEvents, SpanLimits, SpanLinks, +}; +use opentelemetry::{ + trace::{Span as _, SpanBuilder, SpanContext, SpanKind, Status, TraceContextExt, TraceFlags}, + Context, InstrumentationScope, KeyValue, +}; +use std::fmt; +use std::sync::Arc; + +/// `Tracer` implementation to create and manage spans +#[derive(Clone)] +pub struct SdkTracer { + scope: Arc, + provider: SdkTracerProvider, +} + +impl fmt::Debug for SdkTracer { + /// Formats the `Tracer` using the given formatter. + /// Omitting `provider` here is necessary to avoid cycles. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Tracer") + .field("name", &self.scope.name()) + .field("version", &self.scope.version()) + .finish() + } +} + +impl SdkTracer { + /// Create a new tracer (used internally by `TracerProvider`s). + pub(crate) fn new(scope: InstrumentationScope, provider: SdkTracerProvider) -> Self { + SdkTracer { + scope: Arc::new(scope), + provider, + } + } + + /// TracerProvider associated with this tracer. + pub(crate) fn provider(&self) -> &SdkTracerProvider { + &self.provider + } + + /// Instrumentation scope of this tracer. + pub(crate) fn instrumentation_scope(&self) -> &InstrumentationScope { + &self.scope + } + + fn build_recording_span( + &self, + psc: &SpanContext, + sc: SpanContext, + mut builder: SpanBuilder, + attrs: Vec, + span_limits: SpanLimits, + ) -> Span { + let mut attribute_options = builder.attributes.take().unwrap_or_default(); + for extra_attr in attrs { + attribute_options.push(extra_attr); + } + let span_attributes_limit = span_limits.max_attributes_per_span as usize; + let dropped_attributes_count = attribute_options + .len() + .saturating_sub(span_attributes_limit); + attribute_options.truncate(span_attributes_limit); + let dropped_attributes_count = dropped_attributes_count as u32; + + // Links are available as Option> in the builder + // If it is None, then there are no links to process. + // In that case Span.Links will be default (empty Vec, 0 drop count) + // Otherwise, truncate Vec to keep until limits and use that in Span.Links. + // Store the count of excess links into Span.Links.dropped_count. + // There is no ability today to add Links after Span creation, + // but such a capability will be needed in the future + // once the spec for that stabilizes. + + let spans_links_limit = span_limits.max_links_per_span as usize; + let span_links: SpanLinks = if let Some(mut links) = builder.links.take() { + let dropped_count = links.len().saturating_sub(spans_links_limit); + links.truncate(spans_links_limit); + let link_attributes_limit = span_limits.max_attributes_per_link as usize; + for link in links.iter_mut() { + let dropped_attributes_count = + link.attributes.len().saturating_sub(link_attributes_limit); + link.attributes.truncate(link_attributes_limit); + link.dropped_attributes_count = dropped_attributes_count as u32; + } + SpanLinks { + links, + dropped_count: dropped_count as u32, + } + } else { + SpanLinks::default() + }; + + let SpanBuilder { + name, + start_time, + events, + .. + } = builder; + + let start_time = start_time.unwrap_or_else(opentelemetry::time::now); + let spans_events_limit = span_limits.max_events_per_span as usize; + let span_events: SpanEvents = if let Some(mut events) = events { + let dropped_count = events.len().saturating_sub(spans_events_limit); + events.truncate(spans_events_limit); + let event_attributes_limit = span_limits.max_attributes_per_event as usize; + for event in events.iter_mut() { + let dropped_attributes_count = event + .attributes + .len() + .saturating_sub(event_attributes_limit); + event.attributes.truncate(event_attributes_limit); + event.dropped_attributes_count = dropped_attributes_count as u32; + } + SpanEvents { + events, + dropped_count: dropped_count as u32, + } + } else { + SpanEvents::default() + }; + Span::new( + sc, + Some(SpanData { + parent_span_id: psc.span_id(), + parent_span_is_remote: psc.is_valid() && psc.is_remote(), + span_kind: builder.span_kind.take().unwrap_or(SpanKind::Internal), + name, + start_time, + end_time: start_time, + attributes: attribute_options, + dropped_attributes_count, + events: span_events, + links: span_links, + status: Status::default(), + }), + self.clone(), + span_limits, + ) + } +} + +impl opentelemetry::trace::Tracer for SdkTracer { + /// This implementation of `Tracer` produces `sdk::Span` instances. + type Span = Span; + + /// Starts a span from a `SpanBuilder`. + /// + /// Each span has zero or one parent spans and zero or more child spans, which + /// represent causally related operations. A tree of related spans comprises a + /// trace. A span is said to be a _root span_ if it does not have a parent. Each + /// trace includes a single root span, which is the shared ancestor of all other + /// spans in the trace. + fn build_with_context(&self, builder: SpanBuilder, parent_cx: &Context) -> Self::Span { + if parent_cx.is_telemetry_suppressed() { + return Span::new( + SpanContext::empty_context(), + None, + self.clone(), + SpanLimits::default(), + ); + } + + let provider = self.provider(); + // no point start a span if the tracer provider has already being shutdown + if provider.is_shutdown() { + return Span::new( + SpanContext::empty_context(), + None, + self.clone(), + SpanLimits::default(), + ); + } + + let config = provider.config(); + let span_id = config.id_generator.new_span_id(); + let trace_id; + let mut psc = &SpanContext::empty_context(); + + let parent_span = if parent_cx.has_active_span() { + Some(parent_cx.span()) + } else { + None + }; + + // Build context for sampling decision + if let Some(sc) = parent_span.as_ref().map(|parent| parent.span_context()) { + trace_id = sc.trace_id(); + psc = sc; + } else { + trace_id = config.id_generator.new_trace_id(); + }; + + let samplings_result = config.sampler.should_sample( + Some(parent_cx), + trace_id, + &builder.name, + builder.span_kind.as_ref().unwrap_or(&SpanKind::Internal), + builder.attributes.as_ref().unwrap_or(&Vec::new()), + builder.links.as_deref().unwrap_or(&[]), + ); + + let trace_flags = psc.trace_flags(); + let trace_state = samplings_result.trace_state; + let span_limits = config.span_limits; + // Build optional inner context, `None` if not recording. + let mut span = match samplings_result.decision { + SamplingDecision::RecordAndSample => { + let sc = SpanContext::new( + trace_id, + span_id, + trace_flags.with_sampled(true), + false, + trace_state, + ); + self.build_recording_span( + psc, + sc, + builder, + samplings_result.attributes, + span_limits, + ) + } + SamplingDecision::RecordOnly => { + let sc = SpanContext::new( + trace_id, + span_id, + trace_flags.with_sampled(false), + false, + trace_state, + ); + self.build_recording_span( + psc, + sc, + builder, + samplings_result.attributes, + span_limits, + ) + } + SamplingDecision::Drop => { + let span_context = + SpanContext::new(trace_id, span_id, TraceFlags::default(), false, trace_state); + Span::new(span_context, None, self.clone(), span_limits) + } + }; + + if span.is_recording() { + // Call `on_start` for all processors + for processor in provider.span_processors() { + processor.on_start(&mut span, parent_cx) + } + } + + span + } +} + +#[cfg(all(test, feature = "testing", feature = "trace"))] +mod tests { + use crate::{ + testing::trace::TestSpan, + trace::{Sampler, SamplingDecision, SamplingResult, ShouldSample}, + }; + use opentelemetry::{ + trace::{ + Link, Span, SpanContext, SpanId, SpanKind, TraceContextExt, TraceFlags, TraceId, + TraceState, Tracer, TracerProvider, + }, + Context, KeyValue, + }; + + #[derive(Clone, Debug)] + struct TestSampler {} + + impl ShouldSample for TestSampler { + fn should_sample( + &self, + parent_context: Option<&Context>, + _trace_id: TraceId, + _name: &str, + _span_kind: &SpanKind, + _attributes: &[KeyValue], + _links: &[Link], + ) -> SamplingResult { + let trace_state = parent_context + .unwrap() + .span() + .span_context() + .trace_state() + .clone(); + SamplingResult { + decision: SamplingDecision::RecordAndSample, + attributes: Vec::new(), + trace_state: trace_state.insert("foo", "notbar").unwrap(), + } + } + } + + #[test] + fn allow_sampler_to_change_trace_state() { + // Setup + let sampler = TestSampler {}; + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(sampler) + .build(); + let tracer = tracer_provider.tracer("test"); + let trace_state = TraceState::from_key_value(vec![("foo", "bar")]).unwrap(); + + let parent_context = Context::new().with_span(TestSpan(SpanContext::new( + TraceId::from(128), + SpanId::from(64), + TraceFlags::SAMPLED, + true, + trace_state, + ))); + + // Test sampler should change trace state + let span = tracer.start_with_context("foo", &parent_context); + let span_context = span.span_context(); + let expected = span_context.trace_state(); + assert_eq!(expected.get("foo"), Some("notbar")) + } + + #[test] + fn allow_sampler_to_change_trace_state_boxed() { + // Setup + let sampler: Box = Box::new(TestSampler {}); + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(sampler) + .build(); + let tracer = tracer_provider.tracer("test"); + let trace_state = TraceState::from_key_value(vec![("foo", "bar")]).unwrap(); + + let parent_context = Context::new().with_span(TestSpan(SpanContext::new( + TraceId::from(128), + SpanId::from(64), + TraceFlags::SAMPLED, + true, + trace_state, + ))); + + // Test sampler should change trace state + let span = tracer.start_with_context("foo", &parent_context); + let span_context = span.span_context(); + let expected = span_context.trace_state(); + assert_eq!(expected.get("foo"), Some("notbar")) + } + + #[test] + fn drop_parent_based_children() { + let sampler = Sampler::ParentBased(Box::new(Sampler::AlwaysOn)); + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(sampler) + .build(); + + let context = Context::current_with_span(TestSpan(SpanContext::empty_context())); + let tracer = tracer_provider.tracer("test"); + let span = tracer.start_with_context("must_not_be_sampled", &context); + + assert!(!span.span_context().is_sampled()); + } + + #[test] + fn uses_current_context_for_builders_if_unset() { + let sampler = Sampler::ParentBased(Box::new(Sampler::AlwaysOn)); + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(sampler) + .build(); + let tracer = tracer_provider.tracer("test"); + + let _attached = Context::current_with_span(TestSpan(SpanContext::empty_context())).attach(); + let span = tracer.span_builder("must_not_be_sampled").start(&tracer); + assert!(!span.span_context().is_sampled()); + + let context = Context::map_current(|cx| { + cx.with_remote_span_context(SpanContext::new( + TraceId::from(1), + SpanId::from(1), + TraceFlags::default(), + true, + Default::default(), + )) + }); + let _attached = context.attach(); + let span = tracer.span_builder("must_not_be_sampled").start(&tracer); + + assert!(!span.span_context().is_sampled()); + } + + #[test] + fn in_span_with_context_uses_provided_context() { + use crate::trace::{InMemorySpanExporter, SimpleSpanProcessor}; + + let exporter = InMemorySpanExporter::default(); + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOn) + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + let tracer = tracer_provider.tracer("test"); + + // Create a parent context explicitly + let parent_span = tracer.start("parent"); + let parent_trace_id = parent_span.span_context().trace_id(); + let parent_span_id = parent_span.span_context().span_id(); + let parent_cx = Context::current_with_span(parent_span); + + // Use in_span_with_context with explicit parent context + let mut child_trace_id = None; + let mut child_span_id = None; + let mut executed = false; + + let returned_value = tracer.in_span_with_context("child-span", &parent_cx, |cx| { + let span = cx.span(); + child_trace_id = Some(span.span_context().trace_id()); + child_span_id = Some(span.span_context().span_id()); + executed = true; + "test_result" + }); + + // Verify child span inherited parent's trace_id + assert_eq!(child_trace_id, Some(parent_trace_id)); + // Verify child has a different span_id than parent + assert_ne!(child_span_id, Some(parent_span_id)); + // Verify the closure was executed + assert!(executed); + // Verify return value is passed through + assert_eq!(returned_value, "test_result"); + + // End the parent span to export it + drop(parent_cx); + + // Verify parent-child relationship through exporter + let spans = exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + let parent = spans.iter().find(|s| s.name == "parent").unwrap(); + let child = spans.iter().find(|s| s.name == "child-span").unwrap(); + assert_eq!(child.parent_span_id, parent.span_context.span_id()); + assert_eq!( + child.span_context.trace_id(), + parent.span_context.trace_id() + ); + } + + #[test] + fn in_span_with_builder_uses_current_context() { + use crate::trace::{InMemorySpanExporter, SimpleSpanProcessor}; + + let exporter = InMemorySpanExporter::default(); + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOn) + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + let tracer = tracer_provider.tracer("test"); + + // Create a parent span and attach it to the current context + let parent_span = tracer.start("parent"); + let parent_trace_id = parent_span.span_context().trace_id(); + let parent_span_id = parent_span.span_context().span_id(); + let _attached = Context::current_with_span(parent_span).attach(); + + // Use in_span_with_builder with configured span + let mut child_trace_id = None; + + tracer.in_span_with_builder( + tracer + .span_builder("child") + .with_kind(SpanKind::Client) + .with_attributes(vec![KeyValue::new("test_key", "test_value")]), + |cx| { + let span = cx.span(); + child_trace_id = Some(span.span_context().trace_id()); + }, + ); + + // Verify child span inherited parent's trace_id + assert_eq!(child_trace_id, Some(parent_trace_id)); + + // End the attached context to export the parent span + drop(_attached); + + // Verify parent-child relationship through exporter + let spans = exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + let child = spans.iter().find(|s| s.name == "child").unwrap(); + assert_eq!(child.parent_span_id, parent_span_id); + assert_eq!(child.span_context.trace_id(), parent_trace_id); + } + + #[test] + fn in_span_with_builder_and_context_uses_provided_context() { + use crate::trace::{InMemorySpanExporter, SimpleSpanProcessor}; + + let exporter = InMemorySpanExporter::default(); + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOn) + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + let tracer = tracer_provider.tracer("test"); + + // Create a parent context explicitly + let parent_span = tracer.start("parent"); + let parent_trace_id = parent_span.span_context().trace_id(); + let parent_span_id = parent_span.span_context().span_id(); + let parent_cx = Context::current_with_span(parent_span); + + // Use in_span_with_builder_and_context with explicit parent context + let mut child_trace_id = None; + let mut result = 0; + + let returned_value = tracer.in_span_with_builder_and_context( + tracer + .span_builder("child") + .with_kind(SpanKind::Server) + .with_attributes(vec![ + KeyValue::new("http.method", "GET"), + KeyValue::new("http.url", "/api/test"), + ]), + &parent_cx, + |cx| { + let span = cx.span(); + child_trace_id = Some(span.span_context().trace_id()); + result = 42; + result + }, + ); + + // Verify child span inherited parent's trace_id + assert_eq!(child_trace_id, Some(parent_trace_id)); + // Verify return value is passed through + assert_eq!(returned_value, 42); + assert_eq!(result, 42); + + // End the parent span to export it + drop(parent_cx); + + // Verify parent-child relationship through exporter + let spans = exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + let child = spans.iter().find(|s| s.name == "child").unwrap(); + assert_eq!(child.parent_span_id, parent_span_id); + assert_eq!(child.span_context.trace_id(), parent_trace_id); + } + + #[test] + fn in_span_with_builder_and_context_ignores_active_context() { + use crate::trace::{InMemorySpanExporter, SimpleSpanProcessor}; + + let exporter = InMemorySpanExporter::default(); + let tracer_provider = crate::trace::SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOn) + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + let tracer = tracer_provider.tracer("test"); + + // Create an active context with a specific trace context + let active_span_context = SpanContext::new( + TraceId::from(1u128), + SpanId::from(1u64), + TraceFlags::SAMPLED, + true, + Default::default(), + ); + let active_trace_id = active_span_context.trace_id(); + let active_span_id = active_span_context.span_id(); + let _attached = Context::current_with_span(TestSpan(active_span_context)).attach(); + + // Create a different parent context with a different trace ID to explicitly provide + let provided_span_context = SpanContext::new( + TraceId::from(2u128), + SpanId::from(2u64), + TraceFlags::SAMPLED, + true, + Default::default(), + ); + let provided_trace_id = provided_span_context.trace_id(); + let provided_span_id = provided_span_context.span_id(); + let provided_cx = Context::current_with_span(TestSpan(provided_span_context)); + + // Ensure the two parents have different trace IDs + assert_ne!(active_trace_id, provided_trace_id); + + // Use in_span_with_builder_and_context with explicit parent context + let mut child_trace_id = None; + + tracer.in_span_with_builder_and_context( + tracer.span_builder("child").with_kind(SpanKind::Internal), + &provided_cx, + |cx| { + let span = cx.span(); + child_trace_id = Some(span.span_context().trace_id()); + }, + ); + + // Verify child uses the provided context, NOT the active context + assert_eq!(child_trace_id, Some(provided_trace_id)); + assert_ne!(child_trace_id, Some(active_trace_id)); + + // Verify parent-child relationship through exporter + let spans = exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 1); + let child = &spans[0]; + assert_eq!(child.name, "child"); + assert_eq!(child.parent_span_id, provided_span_id); + assert_eq!(child.span_context.trace_id(), provided_trace_id); + assert_ne!(child.parent_span_id, active_span_id); + assert_ne!(child.span_context.trace_id(), active_trace_id); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/util.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/util.rs new file mode 100644 index 00000000..adad9cc6 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/util.rs @@ -0,0 +1,9 @@ +//! Internal utilities + +/// Helper which wraps `tokio::time::interval` and makes it return a stream +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +pub fn tokio_interval_stream( + period: std::time::Duration, +) -> tokio_stream::wrappers::IntervalStream { + tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(period)) +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/tests/miri_smoke.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/tests/miri_smoke.rs new file mode 100644 index 00000000..3a0c9506 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/tests/miri_smoke.rs @@ -0,0 +1,8 @@ +// Targeted Miri regression check for SDK default resource construction. +// This does not imply general opentelemetry-rust Miri support; see #3554. +#[cfg(feature = "metrics")] +#[test] +fn default_meter_provider_builds() { + let provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder().build(); + drop(provider); +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/tests/otel_default_constants.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/tests/otel_default_constants.rs new file mode 100644 index 00000000..66ba2fd7 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/tests/otel_default_constants.rs @@ -0,0 +1,86 @@ +//! Verifies the spec-defined `OTEL_*` environment variable names and their +//! `OTEL_*_DEFAULT` values are reachable from outside the crate. +//! +//! These live in an integration test rather than a unit test on purpose: an +//! integration test can only see the public API, so it also guards the +//! re-exports in `trace/mod.rs`, `logs/mod.rs`, and `metrics/mod.rs` against a +//! future refactor silently making them private again. +//! +//! See . + +#[cfg(feature = "trace")] +#[test] +fn bsp_constants_are_public_and_match_spec_defaults() { + use opentelemetry_sdk::trace::{ + OTEL_BSP_EXPORT_TIMEOUT, OTEL_BSP_EXPORT_TIMEOUT_DEFAULT, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, + OTEL_BSP_MAX_EXPORT_BATCH_SIZE_DEFAULT, OTEL_BSP_MAX_QUEUE_SIZE, + OTEL_BSP_MAX_QUEUE_SIZE_DEFAULT, OTEL_BSP_SCHEDULE_DELAY, OTEL_BSP_SCHEDULE_DELAY_DEFAULT, + }; + use std::time::Duration; + + assert_eq!(OTEL_BSP_SCHEDULE_DELAY, "OTEL_BSP_SCHEDULE_DELAY"); + assert_eq!( + OTEL_BSP_SCHEDULE_DELAY_DEFAULT, + Duration::from_millis(5_000) + ); + + assert_eq!(OTEL_BSP_MAX_QUEUE_SIZE, "OTEL_BSP_MAX_QUEUE_SIZE"); + assert_eq!(OTEL_BSP_MAX_QUEUE_SIZE_DEFAULT, 2_048); + + assert_eq!( + OTEL_BSP_MAX_EXPORT_BATCH_SIZE, + "OTEL_BSP_MAX_EXPORT_BATCH_SIZE" + ); + assert_eq!(OTEL_BSP_MAX_EXPORT_BATCH_SIZE_DEFAULT, 512); + + assert_eq!(OTEL_BSP_EXPORT_TIMEOUT, "OTEL_BSP_EXPORT_TIMEOUT"); + assert_eq!( + OTEL_BSP_EXPORT_TIMEOUT_DEFAULT, + Duration::from_millis(30_000) + ); +} + +#[cfg(feature = "logs")] +#[test] +fn blrp_constants_are_public_and_match_spec_defaults() { + use opentelemetry_sdk::logs::{ + OTEL_BLRP_EXPORT_TIMEOUT, OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT, + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, OTEL_BLRP_MAX_EXPORT_BATCH_SIZE_DEFAULT, + OTEL_BLRP_MAX_QUEUE_SIZE, OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT, OTEL_BLRP_SCHEDULE_DELAY, + OTEL_BLRP_SCHEDULE_DELAY_DEFAULT, + }; + use std::time::Duration; + + assert_eq!(OTEL_BLRP_SCHEDULE_DELAY, "OTEL_BLRP_SCHEDULE_DELAY"); + assert_eq!( + OTEL_BLRP_SCHEDULE_DELAY_DEFAULT, + Duration::from_millis(1_000) + ); + + assert_eq!(OTEL_BLRP_MAX_QUEUE_SIZE, "OTEL_BLRP_MAX_QUEUE_SIZE"); + assert_eq!(OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT, 2_048); + + assert_eq!( + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, + "OTEL_BLRP_MAX_EXPORT_BATCH_SIZE" + ); + assert_eq!(OTEL_BLRP_MAX_EXPORT_BATCH_SIZE_DEFAULT, 512); + + assert_eq!(OTEL_BLRP_EXPORT_TIMEOUT, "OTEL_BLRP_EXPORT_TIMEOUT"); + assert_eq!( + OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT, + Duration::from_millis(30_000) + ); +} + +#[cfg(feature = "metrics")] +#[test] +fn periodic_reader_constants_are_public_and_match_spec_defaults() { + use opentelemetry_sdk::metrics::{ + OTEL_METRIC_EXPORT_INTERVAL, OTEL_METRIC_EXPORT_INTERVAL_DEFAULT, + }; + use std::time::Duration; + + assert_eq!(OTEL_METRIC_EXPORT_INTERVAL, "OTEL_METRIC_EXPORT_INTERVAL"); + assert_eq!(OTEL_METRIC_EXPORT_INTERVAL_DEFAULT, Duration::from_secs(60)); +} -- 2.54.0

(new_provider: P) +where + P: metrics::MeterProvider + Send + Sync + 'static, +{ + // Try to set the global meter provider. If the RwLock is poisoned, we'll log an error. + let mut global_provider = global_meter_provider().write(); + if let Ok(ref mut provider) = global_provider { + **provider = Arc::new(new_provider); + otel_info!(name: "MeterProvider.GlobalSet", message = "Global meter provider is set. Meters can now be created using global::meter() or global::meter_with_scope()."); + } else { + otel_error!(name: "MeterProvider.GlobalSetFailed", message = "Setting global meter provider failed. Meters created using global::meter() or global::meter_with_scope() will not function. Report this issue in OpenTelemetry repo."); + } +} + +/// Returns an instance of the currently configured global [`MeterProvider`]. +pub fn meter_provider() -> GlobalMeterProvider { + // Try to get the global meter provider. If the RwLock is poisoned, we'll log an error and return a NoopMeterProvider. + let global_provider = global_meter_provider().read(); + if let Ok(provider) = global_provider { + provider.clone() + } else { + otel_error!(name: "MeterProvider.GlobalGetFailed", message = "Getting global meter provider failed. Meters created using global::meter() or global::meter_with_scope() will not function. Report this issue in OpenTelemetry repo."); + Arc::new(crate::metrics::noop::NoopMeterProvider::new()) + } +} + +/// Creates a named [`Meter`] via the currently configured global [`MeterProvider`]. +/// +/// This is a more convenient way of expressing `global::meter_provider().meter(name)`. +/// +/// **NOTE:** Calls to [`meter()`] return a [`Meter`] backed by the global [`MeterProvider`] configured during the method invocation. +/// If the global [`MeterProvider`] is changed after getting [`Meter`] instances from these calls, the [`Meter`] instances returned will not reflect the change. +pub fn meter(name: &'static str) -> Meter { + meter_provider().meter(name) +} + +/// Creates a [`Meter`] with the given instrumentation scope. +/// +/// This is a simpler alternative to `global::meter_provider().meter_with_scope(...)` +/// +/// **NOTE:** Calls to [`meter_with_scope()`] return a [`Meter`] backed by the global [`MeterProvider`] configured during the method invocation. +/// If the global [`MeterProvider`] is changed after getting [`Meter`] instances from these calls, the [`Meter`] instances returned will not reflect the change. +/// +/// # Example +/// +/// ``` +/// use std::sync::Arc; +/// use opentelemetry::global::meter_with_scope; +/// use opentelemetry::InstrumentationScope; +/// use opentelemetry::KeyValue; +/// +/// let scope = InstrumentationScope::builder("io.opentelemetry") +/// .with_version("0.17") +/// .with_schema_url("https://opentelemetry.io/schemas/1.2.0") +/// .with_attributes(vec![(KeyValue::new("key", "value"))]) +/// .build(); +/// +/// let meter = meter_with_scope(scope); +/// ``` +pub fn meter_with_scope(scope: InstrumentationScope) -> Meter { + meter_provider().meter_with_scope(scope) +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/global/mod.rs b/vendor/opentelemetry-rust/opentelemetry/src/global/mod.rs new file mode 100644 index 00000000..182364a1 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/global/mod.rs @@ -0,0 +1,149 @@ +//! Utilities for working with global telemetry primitives +//! +//! ## Global Trace API +//! +//! The global trace API **provides applications access to their configured +//! [`TracerProvider`] instance from anywhere in the codebase**. This allows +//! applications to be less coupled to the specific Open Telemetry SDK while not +//! manually passing references to each part of the code that needs to create +//! [`Span`]s. Additionally, **3rd party middleware** or **library code** can be +//! written against this generic API and not constrain users to a specific +//! implementation choice. +//! +//! ### Usage in Applications +//! +//! Applications configure their tracer either by installing a trace pipeline, +//! or calling [`set_tracer_provider`]. +//! +//! ``` +//! # #[cfg(feature="trace")] +//! # { +//! use opentelemetry::trace::{Tracer, noop::NoopTracerProvider}; +//! use opentelemetry::global; +//! +//! fn init_tracer() { +//! // Swap this no-op provider for your tracing service of choice (jaeger, zipkin, etc) +//! let provider = NoopTracerProvider::new(); +//! +//! // Configure the global `TracerProvider` singleton when your app starts +//! // (there is a no-op default if this is not set by your application) +//! let _ = global::set_tracer_provider(provider); +//! } +//! +//! fn do_something_tracked() { +//! // Then you can get a named tracer instance anywhere in your codebase. +//! let tracer = global::tracer("my-component"); +//! +//! tracer.in_span("doing_work", |cx| { +//! // Traced app logic here... +//! }); +//! } +//! +//! // in main or other app start +//! init_tracer(); +//! do_something_tracked(); +//! # } +//! ``` +//! +//! ### Usage in Libraries +//! +//! ``` +//! # #[cfg(feature="trace")] +//! # { +//! use std::sync::Arc; +//! use opentelemetry::trace::Tracer; +//! use opentelemetry::global; +//! use opentelemetry::InstrumentationScope; +//! +//! pub fn my_traced_library_function() { +//! // End users of your library will configure their global tracer provider +//! // so you can use the global tracer without any setup +//! +//! let scope = InstrumentationScope::builder("my_library-name") +//! .with_version(env!("CARGO_PKG_VERSION")) +//! .with_schema_url("https://opentelemetry.io/schemas/1.17.0") +//! .build(); +//! +//! let tracer = global::tracer_with_scope(scope); +//! +//! tracer.in_span("doing_library_work", |cx| { +//! // Traced library logic here... +//! }); +//! } +//! # } +//! ``` +//! +//! [`TracerProvider`]: crate::trace::TracerProvider +//! [`Span`]: crate::trace::Span +//! +//! ## Global Metrics API +//! +//! The global metrics API **provides applications access to their configured +//! [`MeterProvider`] instance from anywhere in the codebase**. This allows +//! applications to be less coupled to the specific Open Telemetry SDK while not +//! manually passing references to each part of the code that needs to create +//! metric instruments. Additionally, **3rd party middleware** or **library code** can be +//! written against this generic API and not constrain users to a specific +//! implementation choice. +//! +//! ### Usage in Applications and libraries +//! +//! Applications and libraries can obtain meter from the global meter provider, +//! and use the meter to create instruments to emit measurements. +//! +//! ``` +//! # #[cfg(feature="metrics")] +//! # { +//! use opentelemetry::metrics::{Meter}; +//! use opentelemetry::{global, KeyValue}; +//! +//! fn do_something_instrumented() { +//! let meter = global::meter("my-component"); +//! // It is recommended to reuse the same counter instance for the +//! // lifetime of the application +//! let counter = meter.u64_counter("my_counter").build(); +//! +//! // record measurements +//! counter.add(1, &[KeyValue::new("mykey", "myvalue")]); +//! } +//! } +//! ``` +//! +//! ### Usage in Applications +//! Application owners have the responsibility to set the global meter provider. +//! The global meter provider can be set using the [`set_meter_provider`] function. +//! As set_meter_provider takes ownership of the provider, it is recommended to +//! provide a clone of the provider, if the application needs to use the provider +//! later to perform operations like shutdown. +//! ``` +//! # #[cfg(feature="metrics")] +//! # { +//! use opentelemetry::{global, KeyValue}; +//! +//! fn main() { +//! // Set the global meter provider +//! // global::set_meter_provider(my_meter_provider().clone()); +//! } +//! # } +//! ``` +//! +//! [`MeterProvider`]: crate::metrics::MeterProvider +//! [`set_meter_provider`]: crate::global::set_meter_provider + +mod internal_logging; +#[cfg(feature = "metrics")] +mod metrics; +#[cfg(feature = "trace")] +mod propagation; +#[cfg(feature = "trace")] +mod trace; + +#[cfg(feature = "metrics")] +#[cfg_attr(docsrs, doc(cfg(feature = "metrics")))] +pub use metrics::*; +#[cfg(feature = "trace")] +#[cfg_attr(docsrs, doc(cfg(feature = "trace")))] +pub use propagation::*; +#[cfg(feature = "trace")] +#[cfg_attr(docsrs, doc(cfg(feature = "trace")))] +pub use trace::*; diff --git a/vendor/opentelemetry-rust/opentelemetry/src/global/propagation.rs b/vendor/opentelemetry-rust/opentelemetry/src/global/propagation.rs new file mode 100644 index 00000000..4a8a2968 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/global/propagation.rs @@ -0,0 +1,43 @@ +use crate::propagation::TextMapPropagator; +use crate::trace::noop::NoopTextMapPropagator; +use std::sync::{OnceLock, RwLock}; + +/// The current global `TextMapPropagator` propagator. +static GLOBAL_TEXT_MAP_PROPAGATOR: OnceLock>> = + OnceLock::new(); + +/// The global default `TextMapPropagator` propagator. +static DEFAULT_TEXT_MAP_PROPAGATOR: OnceLock = OnceLock::new(); + +/// Ensures the `GLOBAL_TEXT_MAP_PROPAGATOR` is initialized with a `NoopTextMapPropagator`. +#[inline] +fn global_text_map_propagator() -> &'static RwLock> { + GLOBAL_TEXT_MAP_PROPAGATOR.get_or_init(|| RwLock::new(Box::new(NoopTextMapPropagator::new()))) +} + +/// Ensures the `DEFAULT_TEXT_MAP_PROPAGATOR` is initialized. +#[inline] +fn default_text_map_propagator() -> &'static NoopTextMapPropagator { + DEFAULT_TEXT_MAP_PROPAGATOR.get_or_init(NoopTextMapPropagator::new) +} + +/// Sets the given [`TextMapPropagator`] propagator as the current global propagator. +pub fn set_text_map_propagator(propagator: P) { + let _lock = global_text_map_propagator() + .write() + .map(|mut global_propagator| *global_propagator = Box::new(propagator)); +} + +/// Executes a closure with a reference to the current global [`TextMapPropagator`] propagator. +pub fn get_text_map_propagator(mut f: F) -> T +where + F: FnMut(&dyn TextMapPropagator) -> T, +{ + global_text_map_propagator() + .read() + .map(|propagator| f(&**propagator)) + .unwrap_or_else(|_| { + let default_propagator = default_text_map_propagator(); + f(default_propagator as &dyn TextMapPropagator) + }) +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/global/trace.rs b/vendor/opentelemetry-rust/opentelemetry/src/global/trace.rs new file mode 100644 index 00000000..9f8c5069 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/global/trace.rs @@ -0,0 +1,440 @@ +use crate::trace::{noop::NoopTracerProvider, SpanContext, Status}; +use crate::{otel_error, otel_info, InstrumentationScope}; +use crate::{trace, trace::TracerProvider, Context, KeyValue}; +use std::borrow::Cow; +use std::fmt; +use std::sync::{Arc, OnceLock, RwLock}; +use std::time::SystemTime; + +/// Allows a specific [`crate::trace::Span`] to be used generically by [`BoxedSpan`] +/// instances by mirroring the interface and boxing the return types. +pub trait ObjectSafeSpan { + /// An API to record events at a specific time in the context of a given `Span`. + /// + /// Events SHOULD preserve the order in which they're set. This will typically match + /// the ordering of the events' timestamps. + /// + /// Note that the OpenTelemetry project documents certain ["standard event names and + /// keys"](https://github.com/open-telemetry/opentelemetry-specification/tree/v0.5.0/specification/trace/semantic_conventions/README.md) + /// which have prescribed semantic meanings. + fn add_event_with_timestamp( + &mut self, + name: Cow<'static, str>, + timestamp: SystemTime, + attributes: Vec, + ); + + /// Returns the `SpanContext` for the given `Span`. The returned value may be used even after + /// the `Span is finished. The returned value MUST be the same for the entire `Span` lifetime. + fn span_context(&self) -> &SpanContext; + + /// Returns true if this `Span` is recording information like events with the `add_event` + /// operation, attributes using `set_attributes`, status with `set_status`, etc. + /// + /// This flag SHOULD be used to avoid expensive computations of a `Span` attributes or events in + /// case when a `Span` is definitely not recorded. Note that any child span's recording is + /// determined independently from the value of this flag (typically based on the sampled flag of + /// a `TraceFlag` on `SpanContext`). + /// + /// This flag may be true despite the entire trace being sampled out. This allows to record and + /// process information about the individual Span without sending it to the backend. An example + /// of this scenario may be recording and processing of all incoming requests for the processing + /// and building of SLA/SLO latency charts while sending only a subset - sampled spans - to the + /// backend. See also the sampling section of SDK design. + /// + /// Users of the API should only access the `is_recording` property when instrumenting code and + /// never access `SampledFlag` unless used in context propagators. + fn is_recording(&self) -> bool; + + /// An API to set a single `Attribute` where the attribute properties are passed + /// as arguments. To avoid extra allocations some implementations may offer a separate API for + /// each of the possible value types. + /// + /// An `Attribute` is defined as a `KeyValue` pair. + /// + /// Attributes SHOULD preserve the order in which they're set. Setting an attribute + /// with the same key as an existing attribute SHOULD overwrite the existing + /// attribute's value. + /// + /// Note that the OpenTelemetry project documents certain ["standard + /// attributes"](https://github.com/open-telemetry/opentelemetry-specification/tree/v0.5.0/specification/trace/semantic_conventions/README.md) + /// that have prescribed semantic meanings. + fn set_attribute(&mut self, attribute: KeyValue); + + /// Sets the status of the `Span`. `message` MUST be ignored when the status is `OK` or + /// `Unset`. + /// + /// The order of status is `Ok` > `Error` > `Unset`. That's means set the status + /// to `Unset` will always be ignore, set the status to `Error` only works when current + /// status is `Unset`, set the status to `Ok` will be consider final and any further call + /// to this function will be ignore. + fn set_status(&mut self, status: Status); + + /// Updates the `Span`'s name. After this update, any sampling behavior based on the + /// name will depend on the implementation. + /// + /// It is highly discouraged to update the name of a `Span` after its creation. + /// `Span` name is often used to group, filter and identify the logical groups of + /// spans. Often, filtering logic will be implemented before the `Span` creation + /// for performance reasons, and the name update may interfere with this logic. + /// + /// The method name is called `update_name` to differentiate this method from the + /// regular property. It emphasizes that this operation signifies a + /// major change for a `Span` and may lead to re-calculation of sampling or + /// filtering decisions made previously depending on the implementation. + fn update_name(&mut self, new_name: Cow<'static, str>); + + /// Adds a link to this span + /// + fn add_link(&mut self, span_context: SpanContext, attributes: Vec); + + /// Finishes the `Span`. + /// + /// Implementations MUST ignore all subsequent calls to `end` (there might be + /// exceptions when the tracer is streaming events and has no mutable state + /// associated with the Span). + /// + /// Calls to `end` a Span MUST not have any effects on child `Span`s as they may + /// still be running and can be ended later. + /// + /// This API MUST be non-blocking. + fn end(&mut self) { + self.end_with_timestamp(crate::time::now()); + } + + /// Finishes the `Span` with given timestamp + /// + /// For more details, refer to [`Span::end`] + /// + /// [`Span::end`]: trace::Span::end + fn end_with_timestamp(&mut self, timestamp: SystemTime); +} + +impl ObjectSafeSpan for T { + fn add_event_with_timestamp( + &mut self, + name: Cow<'static, str>, + timestamp: SystemTime, + attributes: Vec, + ) { + self.add_event_with_timestamp(name, timestamp, attributes) + } + + fn span_context(&self) -> &SpanContext { + self.span_context() + } + + fn is_recording(&self) -> bool { + self.is_recording() + } + + fn set_attribute(&mut self, attribute: KeyValue) { + self.set_attribute(attribute) + } + + fn set_status(&mut self, status: Status) { + self.set_status(status) + } + + fn update_name(&mut self, new_name: Cow<'static, str>) { + self.update_name(new_name) + } + + fn add_link(&mut self, span_context: SpanContext, attributes: Vec) { + self.add_link(span_context, attributes) + } + + fn end_with_timestamp(&mut self, timestamp: SystemTime) { + self.end_with_timestamp(timestamp) + } +} + +/// Wraps the [`BoxedTracer`]'s [`Span`] so it can be used generically by +/// applications without knowing the underlying type. +/// +/// [`Span`]: crate::trace::Span +pub struct BoxedSpan(Box); + +impl BoxedSpan { + pub(crate) fn new(span: T) -> Self + where + T: ObjectSafeSpan + Send + Sync + 'static, + { + BoxedSpan(Box::new(span)) + } +} + +impl fmt::Debug for BoxedSpan { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("BoxedSpan") + } +} + +impl trace::Span for BoxedSpan { + /// Records events at a specific time in the context of a given `Span`. + /// + /// Note that the OpenTelemetry project documents certain ["standard event names and + /// keys"](https://github.com/open-telemetry/opentelemetry-specification/tree/v0.5.0/specification/trace/semantic_conventions/README.md) + /// which have prescribed semantic meanings. + fn add_event_with_timestamp( + &mut self, + name: T, + timestamp: SystemTime, + attributes: Vec, + ) where + T: Into>, + { + self.0 + .add_event_with_timestamp(name.into(), timestamp, attributes) + } + + /// Returns the `SpanContext` for the given `Span`. + fn span_context(&self) -> &trace::SpanContext { + self.0.span_context() + } + + /// Returns true if this `Span` is recording information like events with the `add_event` + /// operation, attributes using `set_attributes`, status with `set_status`, etc. + fn is_recording(&self) -> bool { + self.0.is_recording() + } + + /// Sets a single `Attribute` where the attribute properties are passed as arguments. + /// + /// Note that the OpenTelemetry project documents certain ["standard + /// attributes"](https://github.com/open-telemetry/opentelemetry-specification/tree/v0.5.0/specification/trace/semantic_conventions/README.md) + /// that have prescribed semantic meanings. + fn set_attribute(&mut self, attribute: KeyValue) { + self.0.set_attribute(attribute) + } + + /// Sets the status of the `Span`. If used, this will override the default `Span` + /// status, which is `Unset`. + fn set_status(&mut self, status: trace::Status) { + self.0.set_status(status) + } + + /// Updates the `Span`'s name. + fn update_name(&mut self, new_name: T) + where + T: Into>, + { + self.0.update_name(new_name.into()) + } + + /// Adds a link to this span + /// + fn add_link(&mut self, span_context: trace::SpanContext, attributes: Vec) { + self.0.add_link(span_context, attributes) + } + + /// Finishes the span with given timestamp. + fn end_with_timestamp(&mut self, timestamp: SystemTime) { + self.0.end_with_timestamp(timestamp); + } +} + +/// Wraps the [`GlobalTracerProvider`]'s [`Tracer`] so it can be used generically by +/// applications without knowing the underlying type. +/// +/// [`Tracer`]: crate::trace::Tracer +/// [`GlobalTracerProvider`]: crate::global::GlobalTracerProvider +pub struct BoxedTracer(Box); + +impl BoxedTracer { + /// Create a `BoxedTracer` from an object-safe tracer. + pub fn new(tracer: Box) -> Self { + BoxedTracer(tracer) + } +} + +impl fmt::Debug for BoxedTracer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("BoxedTracer") + } +} + +impl trace::Tracer for BoxedTracer { + /// Global tracer uses `BoxedSpan`s so that it can be a global singleton, + /// which is not possible if it takes generic type parameters. + type Span = BoxedSpan; + + /// Create a span from a `SpanBuilder` + fn build_with_context(&self, builder: trace::SpanBuilder, parent_cx: &Context) -> Self::Span { + BoxedSpan(self.0.build_with_context_boxed(builder, parent_cx)) + } +} + +/// Allows a specific [`Tracer`] to be used generically by [`BoxedTracer`] +/// instances by mirroring the interface and boxing the return types. +/// +/// [`Tracer`]: crate::trace::Tracer +pub trait ObjectSafeTracer { + /// Returns a trait object so the underlying implementation can be swapped + /// out at runtime. + fn build_with_context_boxed( + &self, + builder: trace::SpanBuilder, + parent_cx: &Context, + ) -> Box; +} + +impl ObjectSafeTracer for T +where + S: trace::Span + Send + Sync + 'static, + T: trace::Tracer, +{ + /// Returns a trait object so the underlying implementation can be swapped + /// out at runtime. + fn build_with_context_boxed( + &self, + builder: trace::SpanBuilder, + parent_cx: &Context, + ) -> Box { + Box::new(self.build_with_context(builder, parent_cx)) + } +} + +/// Allows a specific [`TracerProvider`] to be used generically by the +/// [`GlobalTracerProvider`] by mirroring the interface and boxing the return types. +/// +/// [`TracerProvider`]: crate::trace::TracerProvider +/// [`GlobalTracerProvider`]: crate::global::GlobalTracerProvider +pub trait ObjectSafeTracerProvider { + /// Creates a versioned named tracer instance that is a trait object through the underlying + /// `TracerProvider`. + fn boxed_tracer(&self, scope: InstrumentationScope) -> Box; +} + +impl ObjectSafeTracerProvider for P +where + S: trace::Span + Send + Sync + 'static, + T: trace::Tracer + Send + Sync + 'static, + P: trace::TracerProvider, +{ + /// Return a versioned boxed tracer + fn boxed_tracer(&self, scope: InstrumentationScope) -> Box { + Box::new(self.tracer_with_scope(scope)) + } +} + +/// Represents the globally configured [`TracerProvider`] instance for this +/// application. This allows generic tracing through the returned +/// [`BoxedTracer`] instances. +/// +/// [`TracerProvider`]: crate::trace::TracerProvider +#[derive(Clone)] +pub struct GlobalTracerProvider { + provider: Arc, +} + +impl fmt::Debug for GlobalTracerProvider { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("GlobalTracerProvider") + } +} + +impl GlobalTracerProvider { + /// Create a new GlobalTracerProvider instance from a struct that implements `TracerProvider`. + fn new(provider: P) -> Self + where + S: trace::Span + Send + Sync + 'static, + T: trace::Tracer + Send + Sync + 'static, + P: trace::TracerProvider + Send + Sync + 'static, + { + GlobalTracerProvider { + provider: Arc::new(provider), + } + } +} + +impl trace::TracerProvider for GlobalTracerProvider { + type Tracer = BoxedTracer; + + /// Create a tracer using the global provider. + fn tracer_with_scope(&self, scope: InstrumentationScope) -> Self::Tracer { + BoxedTracer(self.provider.boxed_tracer(scope)) + } +} + +/// The global `Tracer` provider singleton. +static GLOBAL_TRACER_PROVIDER: OnceLock> = OnceLock::new(); + +#[inline] +fn global_tracer_provider() -> &'static RwLock { + GLOBAL_TRACER_PROVIDER + .get_or_init(|| RwLock::new(GlobalTracerProvider::new(NoopTracerProvider::new()))) +} + +/// Returns an instance of the currently configured global [`TracerProvider`] through +/// [`GlobalTracerProvider`]. +/// +/// [`TracerProvider`]: crate::trace::TracerProvider +/// [`GlobalTracerProvider`]: crate::global::GlobalTracerProvider +pub fn tracer_provider() -> GlobalTracerProvider { + // Try to get the global tracer provider. If the RwLock is poisoned, we'll log an error and return a NoopTracerProvider. + let global_provider = global_tracer_provider().read(); + if let Ok(provider) = global_provider { + provider.clone() + } else { + otel_error!(name: "TracerProvider.GlobalGetFailed", message = "Getting global tracer provider failed. Traces created using global::tracer() or global::tracer_with_scope() will not function. Report this issue in OpenTelemetry repo."); + GlobalTracerProvider::new(NoopTracerProvider::new()) + } +} + +/// Creates a named instance of [`Tracer`] via the configured [`GlobalTracerProvider`]. +/// +/// If the name is an empty string, the provider will use a default name. +/// +/// This is a more convenient way of expressing `global::tracer_provider().tracer(name)`. +/// +/// [`Tracer`]: crate::trace::Tracer +pub fn tracer(name: impl Into>) -> BoxedTracer { + tracer_provider().tracer(name.into()) +} + +/// Creates a [`Tracer`] with the given instrumentation scope +/// via the configured [`GlobalTracerProvider`]. +/// +/// This is a simpler alternative to `global::tracer_provider().tracer_with_scope(...)` +/// +/// # Example +/// +/// ``` +/// use std::sync::Arc; +/// use opentelemetry::global::tracer_with_scope; +/// use opentelemetry::InstrumentationScope; +/// use opentelemetry::KeyValue; +/// +/// let scope = InstrumentationScope::builder("io.opentelemetry") +/// .with_version("0.17") +/// .with_schema_url("https://opentelemetry.io/schemas/1.2.0") +/// .with_attributes(vec![(KeyValue::new("key", "value"))]) +/// .build(); +/// +/// let tracer = tracer_with_scope(scope); +/// ``` +/// +/// [`Tracer`]: crate::trace::Tracer +pub fn tracer_with_scope(scope: InstrumentationScope) -> BoxedTracer { + tracer_provider().tracer_with_scope(scope) +} + +/// Sets the given [`TracerProvider`] instance as the current global provider. +/// +/// Libraries should NOT call this function. It is intended for applications/executables. +/// [`TracerProvider`]: crate::trace::TracerProvider +pub fn set_tracer_provider(new_provider: P) +where + S: trace::Span + Send + Sync + 'static, + T: trace::Tracer + Send + Sync + 'static, + P: trace::TracerProvider + Send + Sync + 'static, +{ + let mut global_provider = global_tracer_provider().write(); + if let Ok(ref mut provider) = global_provider { + **provider = GlobalTracerProvider::new(new_provider); + otel_info!(name: "TracerProvider.GlobalSet", message = "Global tracer provider is set. Traces can now be created using global::tracer() or global::tracer_with_scope()."); + } else { + otel_error!(name: "TracerProvider.GlobalSetFailed", message = "Setting global tracer provider failed. Traces created using global::tracer() or global::tracer_with_scope() will not function. Report this issue in OpenTelemetry repo."); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/lib.rs b/vendor/opentelemetry-rust/opentelemetry/src/lib.rs new file mode 100644 index 00000000..12d03cec --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/lib.rs @@ -0,0 +1,305 @@ +//! Implements the [`API`] component of [OpenTelemetry]. +//! +//! *[Supported Rust Versions](#supported-rust-versions)* +//! +//! [`API`]: https://opentelemetry.io/docs/specs/otel/overview/#api +//! [OpenTelemetry]: https://opentelemetry.io/docs/what-is-opentelemetry/ +//! +//! # Getting Started with Traces +//! +//! The [`trace`] module includes types for tracking the progression of a single +//! request while it is handled by services that make up an application. A trace +//! is a tree of [`Span`]s which are objects that represent the work being done +//! by individual services or components involved in a request as it flows +//! through a system. +//! +//! ``` +//! # #[cfg(feature = "trace")] +//! # { +//! use opentelemetry::{global, trace::{Span, Tracer}, KeyValue}; +//! +//! // get a tracer from a provider +//! let tracer = global::tracer("my_service"); +//! +//! // start a new span +//! let mut span = tracer.start("my_span"); +//! +//! // set some attributes +//! span.set_attribute(KeyValue::new("http.client_ip", "83.164.160.102")); +//! +//! // perform some more work... +//! +//! // end or drop the span to export +//! span.end(); +//! # } +//! ``` +//! +//! For a runnable getting-started example, see +//! [`examples/tracing-grpc`](https://github.com/open-telemetry/opentelemetry-rust/tree/main/examples/tracing-grpc), +//! which demonstrates creating spans and propagating trace context across a +//! gRPC client and server. See the +//! [examples](https://github.com/open-telemetry/opentelemetry-rust/tree/main/examples) +//! directory for additional integration patterns. +//! +//! See the [`trace`] module docs for more information on creating and managing +//! spans. +//! +//! [`Span`]: crate::trace::Span +//! +//! # Getting Started with Metrics +//! +//! The [`metrics`] module provides types for recording measurements about a +//! service at runtime. Below are the key steps to report measurements using +//! OpenTelemetry Metrics: +//! +//! 1. **Obtain a Meter:** Get a `Meter` from a `MeterProvider`. +//! 2. **Create Instruments:** Use the `Meter` to create one or more instruments +//! (e.g., counters, histograms). +//! 3. **Record Measurements:** Use the instruments to record measurement values +//! along with optional attributes. +//! +//! For a runnable getting-started example, see +//! [`examples/metrics-basic`](https://github.com/open-telemetry/opentelemetry-rust/tree/main/examples/metrics-basic), +//! which exercises every instrument type. +//! +//! ## How Metrics work in OpenTelemetry +//! In OpenTelemetry, raw measurements recorded using instruments are +//! **aggregated in memory** to form metrics. These aggregated metrics are +//! periodically exported by the [`opentelemetry_sdk`] at fixed intervals (e.g., +//! every 60 seconds) via exporters such as [`opentelemetry-stdout`] or +//! [`opentelemetry-otlp`]. This reduces reporting overhead while ensuring +//! up-to-date data. The aggregation strategy and export interval can be +//! customized in the [`opentelemetry_sdk`] based on your use case. +//! +//! ## Choosing the Right Instrument +//! Selecting the correct instrument is critical for accurately representing +//! your metrics data: +//! +//! - Use **Counters** for values that only increase, such as the number of +//! requests served or errors encountered. +//! - Use **UpDownCounters** for values that can increase or decrease, such as +//! the number of active connections, number of items in a queue etc. +//! - **Gauges:** Use for values that can go up or down and represent the +//! current state, such as CPU usage, temperature etc. +//! - Use **Histograms** for measuring the distribution of a value, such as +//! response times or payload sizes. +//! +//! ### Observable Instruments +//! +//! Counters, UpDownCounters, and Gauges have Observable variants that allow +//! values to be reported through a callback function. Observable instruments +//! are ideal when the metric value is managed elsewhere and needs to be +//! observed by OpenTelemetry instrumentation. The callbacks are automatically +//! invoked by the OpenTelemetry SDK before every export (e.g., every 60 +//! seconds). +//! +//! For example: +//! - An **ObservableCounter** can monitor the number of page faults in a +//! process as reported by the operating system. +//! - An **ObservableUpDownCounter** can monitor the size of an in-memory queue +//! by reporting the size using queue's len() method within the callback +//! function. +//! - An **ObservableGauge** can monitor the CPU temperature by using +//! temperature sensor APIs within the callback function. +//! +//! For detailed guidance, refer to [OpenTelemetry Metrics API - Instrumentation +//! Guidance](https://opentelemetry.io/docs/specs/otel/metrics/supplementary-guidelines/#instrument-selection). +//! +//! ## Best Practices +//! - **Re-use Instruments:** Instruments are designed for +//! reuse. Avoid creating new instruments repeatedly. +//! - **Clone for Sharing:** If the same instrument needs to be used across +//! multiple parts of your code, you can safely clone it to share. +//! +//! ## Example Usage +//! ``` +//! use opentelemetry::{global, KeyValue}; +//! +//! // Get a meter from a provider. +//! let meter = global::meter("my_service"); +//! +//! // Create an instrument (in this case, a Counter). +//! let counter = meter.u64_counter("request.count").build(); +//! +//! // Record a measurement by passing the value and a set of attributes. +//! counter.add(1, &[KeyValue::new("http.client_ip", "83.164.160.102")]); +//! +//! // Create an ObservableCounter and register a callback that reports the measurement. +//! let _observable_counter = meter +//! .u64_observable_counter("bytes_received") +//! .with_callback(|observer| { +//! observer.observe( +//! 100, +//! &[ +//! KeyValue::new("protocol", "udp"), +//! ], +//! ) +//! }) +//! .build(); +//! ``` +//! +//! See the [`metrics`] module docs for more information on creating and +//! managing instruments. +//! +//! +//! # Getting Started with Logs +//! +//! The [`logs`] module contains the Logs Bridge API. It is not intended to be +//! called by application developers directly. It is provided for logging +//! library authors to build log appenders, that bridges existing logging +//! systems with OpenTelemetry. Bridges for +//! [`log`](https://crates.io/crates/log) and +//! [`tracing`](https://crates.io/crates/tracing) libraries are provided via +//! the +//! [`opentelemetry-appender-log`](https://crates.io/crates/opentelemetry-appender-log) +//! and +//! [`opentelemetry-appender-tracing`](https://crates.io/crates/opentelemetry-appender-tracing) +//! crates. +//! +//! For a runnable getting-started example, see +//! [`examples/logs-basic`](https://github.com/open-telemetry/opentelemetry-rust/tree/main/examples/logs-basic), +//! which uses the `tracing` appender to bridge logs to OpenTelemetry. +//! +//! # Crate Feature Flags +//! +//! The following core crate feature flags are available: +//! +//! * `trace`: Includes the trace API. +//! * `metrics`: Includes the metrics API. +//! * `logs`: Includes the logs bridge API. +//! * `internal-logs`: Includes internal logging for the OpenTelemetry library via `tracing`. +//! +//! The default feature flags are ["trace", "metrics", "logs", "internal-logs"]. +//! +//! +//! The following feature flags enable APIs defined in OpenTelemetry specification that is in experimental phase: +//! * `otel_unstable`: Includes unstable APIs. There are no features behind this flag at the moment. +//! +//! # Related Crates +//! +//! In addition to `opentelemetry`, the [`open-telemetry/opentelemetry-rust`] +//! repository contains several additional crates designed to be used with the +//! `opentelemetry` ecosystem. This includes exporters, samplers, as well as +//! utility and adapter crates to assist in propagating context and +//! instrumenting applications. +//! +//! In particular, the following crates are likely to be of interest: +//! +//! - [`opentelemetry_sdk`] provides the OpenTelemetry SDK used to configure providers. +//! - [`opentelemetry-http`] provides an interface for injecting and extracting +//! trace information from [`http`] headers. +//! - [`opentelemetry-otlp`] exporter for sending telemetry in the +//! OTLP format. +//! - [`opentelemetry-stdout`] provides ability to output telemetry to stdout, +//! primarily used for learning/debugging purposes. +//! - [`opentelemetry-prometheus`] provides a pipeline and exporter for sending +//! metrics information to [`Prometheus`]. +//! +//! In addition, there are several other useful crates in the [OTel Rust +//! Contrib +//! repo](https://github.com/open-telemetry/opentelemetry-rust-contrib). A lot +//! of crates maintained outside OpenTelemetry owned repos can be found in the +//! [OpenTelemetry +//! Registry](https://opentelemetry.io/ecosystem/registry/?language=rust). +//! +//! [`http`]: https://crates.io/crates/http +//! [`open-telemetry/opentelemetry-rust`]: https://github.com/open-telemetry/opentelemetry-rust +//! [`opentelemetry_sdk`]: https://crates.io/crates/opentelemetry_sdk +//! [`opentelemetry-stdout`]: https://crates.io/crates/opentelemetry_stdout +//! [`opentelemetry-http`]: https://crates.io/crates/opentelemetry-http +//! [`opentelemetry-otlp`]: https://crates.io/crates/opentelemetry-otlp +//! [`opentelemetry-prometheus`]: https://crates.io/crates/opentelemetry-prometheus +//! [`Prometheus`]: https://prometheus.io +//! +//! # Supported Rust Versions +//! +//! OpenTelemetry is built against the latest stable release. The minimum +//! supported version is 1.70. The current OpenTelemetry version is not +//! guaranteed to build on Rust versions earlier than the minimum supported +//! version. +//! +//! The current stable Rust compiler and the three most recent minor versions +//! before it will always be supported. For example, if the current stable +//! compiler version is 1.49, the minimum supported version will not be +//! increased past 1.46, three minor versions prior. Increasing the minimum +//! supported compiler version is not considered a semver breaking change as +//! long as doing so complies with this policy. +#![warn( + future_incompatible, + missing_debug_implementations, + missing_docs, + nonstandard_style, + rust_2018_idioms, + unreachable_pub, + unused +)] +#![allow(clippy::needless_doctest_main)] +#![cfg_attr(docsrs, feature(doc_cfg), deny(rustdoc::broken_intra_doc_links))] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo.svg" +)] +#![cfg_attr(test, deny(warnings))] + +pub mod global; + +pub mod baggage; + +pub mod context; + +pub use context::{Context, ContextGuard}; + +mod trace_context; +pub use trace_context::{SpanId, TraceFlags, TraceId}; + +mod common; + +#[cfg(any(feature = "testing", test))] +#[doc(hidden)] +pub mod testing; + +pub use common::{ + Array, InstrumentationScope, InstrumentationScopeBuilder, Key, KeyValue, StringValue, Value, +}; + +#[cfg(feature = "metrics")] +#[cfg_attr(docsrs, doc(cfg(feature = "metrics")))] +pub mod metrics; + +#[cfg(feature = "trace")] +#[cfg_attr(docsrs, doc(cfg(feature = "trace")))] +pub mod propagation; + +#[cfg(feature = "trace")] +#[cfg_attr(docsrs, doc(cfg(feature = "trace")))] +pub mod trace; + +#[cfg(feature = "logs")] +#[cfg_attr(docsrs, doc(cfg(feature = "logs")))] +pub mod logs; + +#[doc(hidden)] +#[cfg(any(feature = "metrics", feature = "trace", feature = "logs"))] +pub mod time { + use std::time::SystemTime; + + #[doc(hidden)] + #[cfg(any( + not(target_arch = "wasm32"), + all(target_arch = "wasm32", target_os = "wasi") + ))] + pub fn now() -> SystemTime { + SystemTime::now() + } + + #[doc(hidden)] + #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] + pub fn now() -> SystemTime { + SystemTime::UNIX_EPOCH + std::time::Duration::from_millis(js_sys::Date::now() as u64) + } +} + +#[doc(hidden)] +pub mod _private { + #[cfg(feature = "internal-logs")] + pub use tracing::{debug, error, info, warn}; // re-export +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/logs/logger.rs b/vendor/opentelemetry-rust/opentelemetry/src/logs/logger.rs new file mode 100644 index 00000000..9c398035 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/logs/logger.rs @@ -0,0 +1,62 @@ +use std::borrow::Cow; + +use crate::{logs::LogRecord, InstrumentationScope}; + +use super::Severity; + +/// The interface for emitting [`LogRecord`]s. +pub trait Logger { + /// Specifies the `LogRecord` type associated with this logger. + type LogRecord: LogRecord; + + /// Creates a new log record builder. + fn create_log_record(&self) -> Self::LogRecord; + + /// Emit a [`LogRecord`]. If there is active current thread's [`Context`], + /// the logger will set the record's `TraceContext` to the active trace context, + /// + /// [`Context`]: crate::Context + fn emit(&self, record: Self::LogRecord); + + /// Check if the given log is enabled. + fn event_enabled(&self, level: Severity, target: &str, name: Option<&str>) -> bool; +} + +/// Interfaces that can create [`Logger`] instances. +pub trait LoggerProvider { + /// The [`Logger`] type that this provider will return. + type Logger: Logger; + + /// Returns a new logger with the given instrumentation scope. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::InstrumentationScope; + /// use opentelemetry::logs::LoggerProvider; + /// use opentelemetry_sdk::logs::SdkLoggerProvider; + /// + /// let provider = SdkLoggerProvider::builder().build(); + /// + /// // logger used in applications/binaries + /// let logger = provider.logger("my_app"); + /// + /// // logger used in libraries/crates that optionally includes version and schema url + /// let scope = InstrumentationScope::builder(env!("CARGO_PKG_NAME")) + /// .with_version(env!("CARGO_PKG_VERSION")) + /// .with_schema_url("https://opentelemetry.io/schemas/1.0.0") + /// .build(); + /// + /// let logger = provider.logger_with_scope(scope); + /// ``` + fn logger_with_scope(&self, scope: InstrumentationScope) -> Self::Logger; + + /// Returns a new logger with the given name. + /// + /// The `name` should be the application name or the name of the library + /// providing instrumentation. + fn logger(&self, name: impl Into>) -> Self::Logger { + let scope = InstrumentationScope::builder(name).build(); + self.logger_with_scope(scope) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/logs/mod.rs b/vendor/opentelemetry-rust/opentelemetry/src/logs/mod.rs new file mode 100644 index 00000000..e57684bc --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/logs/mod.rs @@ -0,0 +1,11 @@ +//! # OpenTelemetry Logs Bridge API +/// This API is not intended to be called by application developers directly. +/// It is provided for logging library authors to build log appenders, that +/// bridges existing logging systems with OpenTelemetry. +mod logger; +mod noop; +mod record; + +pub use logger::{Logger, LoggerProvider}; +pub use noop::NoopLoggerProvider; +pub use record::{AnyValue, LogRecord, Severity}; diff --git a/vendor/opentelemetry-rust/opentelemetry/src/logs/noop.rs b/vendor/opentelemetry-rust/opentelemetry/src/logs/noop.rs new file mode 100644 index 00000000..b8498ad8 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/logs/noop.rs @@ -0,0 +1,85 @@ +use std::{borrow::Cow, time::SystemTime}; + +use crate::{ + logs::{AnyValue, LogRecord, Logger, LoggerProvider, Severity}, + InstrumentationScope, Key, +}; + +/// A no-op implementation of a [`LoggerProvider`]. +#[derive(Clone, Debug, Default)] +pub struct NoopLoggerProvider(()); + +impl NoopLoggerProvider { + /// Create a new no-op logger provider. + pub fn new() -> Self { + NoopLoggerProvider(()) + } +} + +impl LoggerProvider for NoopLoggerProvider { + type Logger = NoopLogger; + + fn logger_with_scope(&self, _scope: InstrumentationScope) -> Self::Logger { + NoopLogger(()) + } +} + +#[derive(Debug, Clone, Default)] +/// A no-operation log record that implements the LogRecord trait. +pub struct NoopLogRecord; + +impl LogRecord for NoopLogRecord { + // Implement the LogRecord trait methods with empty bodies. + #[inline] + fn set_event_name(&mut self, _name: &'static str) {} + #[inline] + fn set_timestamp(&mut self, _timestamp: SystemTime) {} + #[inline] + fn set_observed_timestamp(&mut self, _timestamp: SystemTime) {} + #[inline] + fn set_severity_text(&mut self, _text: &'static str) {} + #[inline] + fn set_severity_number(&mut self, _number: Severity) {} + #[inline] + fn set_body(&mut self, _body: AnyValue) {} + #[inline] + fn add_attributes(&mut self, _attributes: I) + where + I: IntoIterator, + K: Into, + V: Into, + { + } + #[inline] + fn add_attribute(&mut self, _key: K, _value: V) + where + K: Into, + V: Into, + { + } + + #[inline] + // Sets the `target` of a record + fn set_target(&mut self, _target: T) + where + T: Into>, + { + } +} + +/// A no-op implementation of a [`Logger`] +#[derive(Clone, Debug)] +pub struct NoopLogger(()); + +impl Logger for NoopLogger { + type LogRecord = NoopLogRecord; + + fn create_log_record(&self) -> Self::LogRecord { + NoopLogRecord {} + } + fn emit(&self, _record: Self::LogRecord) {} + + fn event_enabled(&self, _level: super::Severity, _target: &str, _name: Option<&str>) -> bool { + false + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/logs/record.rs b/vendor/opentelemetry-rust/opentelemetry/src/logs/record.rs new file mode 100644 index 00000000..f0a94bc7 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/logs/record.rs @@ -0,0 +1,230 @@ +use crate::{Key, StringValue}; + +use crate::{SpanId, TraceFlags, TraceId}; + +use std::{borrow::Cow, collections::HashMap, time::SystemTime}; + +/// SDK implemented trait for managing log records +pub trait LogRecord { + /// Sets the `event_name` of a record + fn set_event_name(&mut self, name: &'static str); + + /// Sets the `target` of a record. + /// Currently, both `opentelemetry-appender-tracing` and `opentelemetry-appender-log` create a single logger + /// with a scope that doesn't accurately reflect the component emitting the logs. + /// Exporters MAY use this field to override the `instrumentation_scope.name`. + fn set_target(&mut self, _target: T) + where + T: Into>; + + /// Sets the time when the event occurred measured by the origin clock, i.e. the time at the source. + fn set_timestamp(&mut self, timestamp: SystemTime); + + /// Sets the observed event timestamp. + fn set_observed_timestamp(&mut self, timestamp: SystemTime); + + /// Sets severity as text. + fn set_severity_text(&mut self, text: &'static str); + + /// Sets severity as a numeric value. + fn set_severity_number(&mut self, number: Severity); + + /// Sets the message body of the log. + fn set_body(&mut self, body: AnyValue); + + /// Adds multiple attributes. + fn add_attributes(&mut self, attributes: I) + where + I: IntoIterator, + K: Into, + V: Into; + + /// Adds a single attribute. + fn add_attribute(&mut self, key: K, value: V) + where + K: Into, + V: Into; + + /// Sets the trace context of the log. + fn set_trace_context( + &mut self, + trace_id: TraceId, + span_id: SpanId, + trace_flags: Option, + ) { + let _ = trace_id; + let _ = span_id; + let _ = trace_flags; + } +} + +/// Value types for representing arbitrary values in a log record. +/// Note: The `tracing` and `log` crates only support basic types that can be +/// converted to these core variants: `i64`, `f64`, `StringValue`, and `bool`. +/// Any complex and custom types are supported through their Debug implementation, +/// and converted to String. More complex types (`Bytes`, `ListAny`, and `Map`) are +/// included here to meet specification requirements and are available to support +/// custom appenders that may be implemented for other logging crates. +/// These types allow for handling dynamic data structures, so keep in mind the +/// potential performance overhead of using boxed vectors and maps in appenders. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum AnyValue { + /// An integer value + Int(i64), + /// A double value + Double(f64), + /// A string value + String(StringValue), + /// A boolean value + Boolean(bool), + /// A byte array + Bytes(Box>), + /// An array of `Any` values + ListAny(Box>), + /// A map of string keys to `Any` values, arbitrarily nested. + Map(Box>), +} + +macro_rules! impl_trivial_from { + ($t:ty, $variant:path) => { + impl From<$t> for AnyValue { + fn from(val: $t) -> AnyValue { + $variant(val.into()) + } + } + }; +} + +impl_trivial_from!(i8, AnyValue::Int); +impl_trivial_from!(i16, AnyValue::Int); +impl_trivial_from!(i32, AnyValue::Int); +impl_trivial_from!(i64, AnyValue::Int); + +impl_trivial_from!(u8, AnyValue::Int); +impl_trivial_from!(u16, AnyValue::Int); +impl_trivial_from!(u32, AnyValue::Int); + +impl_trivial_from!(f64, AnyValue::Double); +impl_trivial_from!(f32, AnyValue::Double); + +impl_trivial_from!(String, AnyValue::String); +impl_trivial_from!(Cow<'static, str>, AnyValue::String); +impl_trivial_from!(&'static str, AnyValue::String); +impl_trivial_from!(StringValue, AnyValue::String); + +impl_trivial_from!(bool, AnyValue::Boolean); + +impl From<&[u8]> for AnyValue { + fn from(val: &[u8]) -> AnyValue { + AnyValue::Bytes(Box::new(val.to_vec())) + } +} + +impl> FromIterator for AnyValue { + /// Creates an [`AnyValue::ListAny`] value from a sequence of `Into` values. + fn from_iter>(iter: I) -> Self { + AnyValue::ListAny(Box::new(iter.into_iter().map(Into::into).collect())) + } +} + +impl, V: Into> FromIterator<(K, V)> for AnyValue { + /// Creates an [`AnyValue::Map`] value from a sequence of key-value pairs + /// that can be converted into a `Key` and `AnyValue` respectively. + fn from_iter>(iter: I) -> Self { + AnyValue::Map(Box::new(HashMap::from_iter( + iter.into_iter().map(|(k, v)| (k.into(), v.into())), + ))) + } +} + +/// A normalized severity value. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub enum Severity { + /// TRACE + Trace = 1, + /// TRACE2 + Trace2 = 2, + /// TRACE3 + Trace3 = 3, + /// TRACE4 + Trace4 = 4, + /// DEBUG + Debug = 5, + /// DEBUG2 + Debug2 = 6, + /// DEBUG3 + Debug3 = 7, + /// DEBUG4 + Debug4 = 8, + /// INFO + Info = 9, + /// INFO2 + Info2 = 10, + /// INFO3 + Info3 = 11, + /// INFO4 + Info4 = 12, + /// WARN + Warn = 13, + /// WARN2 + Warn2 = 14, + /// WARN3 + Warn3 = 15, + /// WARN4 + Warn4 = 16, + /// ERROR + Error = 17, + /// ERROR2 + Error2 = 18, + /// ERROR3 + Error3 = 19, + /// ERROR4 + Error4 = 20, + /// FATAL + Fatal = 21, + /// FATAL2 + Fatal2 = 22, + /// FATAL3 + Fatal3 = 23, + /// FATAL4 + Fatal4 = 24, +} + +impl Severity { + /// Return the string representing the short name for the `Severity` + /// value as specified by the OpenTelemetry logs data model. + pub const fn name(&self) -> &'static str { + match &self { + Severity::Trace => "TRACE", + Severity::Trace2 => "TRACE2", + Severity::Trace3 => "TRACE3", + Severity::Trace4 => "TRACE4", + + Severity::Debug => "DEBUG", + Severity::Debug2 => "DEBUG2", + Severity::Debug3 => "DEBUG3", + Severity::Debug4 => "DEBUG4", + + Severity::Info => "INFO", + Severity::Info2 => "INFO2", + Severity::Info3 => "INFO3", + Severity::Info4 => "INFO4", + + Severity::Warn => "WARN", + Severity::Warn2 => "WARN2", + Severity::Warn3 => "WARN3", + Severity::Warn4 => "WARN4", + + Severity::Error => "ERROR", + Severity::Error2 => "ERROR2", + Severity::Error3 => "ERROR3", + Severity::Error4 => "ERROR4", + + Severity::Fatal => "FATAL", + Severity::Fatal2 => "FATAL2", + Severity::Fatal3 => "FATAL3", + Severity::Fatal4 => "FATAL4", + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/counter.rs b/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/counter.rs new file mode 100644 index 00000000..6913e641 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/counter.rs @@ -0,0 +1,101 @@ +use crate::KeyValue; +use core::fmt; +use std::sync::Arc; + +#[cfg(feature = "experimental_metrics_bound_instruments")] +use super::BoundSyncInstrument; +use super::SyncInstrument; + +/// An instrument that records increasing values. +/// +/// [`Counter`] can be cloned to create multiple handles to the same instrument. If a [`Counter`] needs to be shared, +/// users are recommended to clone the [`Counter`] instead of creating duplicate [`Counter`]s for the same metric. Creating +/// duplicate [`Counter`]s for the same metric could lower SDK performance. +#[derive(Clone)] +#[non_exhaustive] +pub struct Counter(Arc + Send + Sync>); + +impl fmt::Debug for Counter +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("Counter<{}>", std::any::type_name::())) + } +} + +impl Counter { + /// Create a new counter. + pub fn new(inner: Arc + Send + Sync>) -> Self { + Counter(inner) + } + + /// Records an increment to the counter. + pub fn add(&self, value: T, attributes: &[KeyValue]) { + self.0.measure(value, attributes) + } + + /// Binds this counter to a fixed set of attributes. + /// + /// Corresponds to the `Bind` capability in the OpenTelemetry spec (status: + /// Development as of spec 1.57.0). + #[cfg(feature = "experimental_metrics_bound_instruments")] + pub fn bind(&self, attributes: &[KeyValue]) -> BoundCounter { + BoundCounter(Arc::from(self.0.bind(attributes))) + } +} + +/// An async instrument that records increasing values. +#[derive(Clone)] +#[non_exhaustive] +pub struct ObservableCounter { + _marker: std::marker::PhantomData, +} + +impl ObservableCounter { + /// Create a new observable counter. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + ObservableCounter { + _marker: std::marker::PhantomData, + } + } +} + +impl fmt::Debug for ObservableCounter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!( + "ObservableCounter<{}>", + std::any::type_name::() + )) + } +} + +/// A counter bound to a fixed set of attributes. +/// +/// Created by calling [`Counter::bind`] with an attribute set. All subsequent +/// [`add`](BoundCounter::add) calls use the pre-resolved attributes, bypassing +/// per-call attribute lookup for significantly better performance. +/// +/// `BoundCounter` can be cloned cheaply to share a single bound state across +/// threads or modules without re-binding. The underlying tracker is reclaimed +/// when the last clone is dropped. +#[cfg(feature = "experimental_metrics_bound_instruments")] +#[derive(Clone)] +#[must_use = "dropping a BoundCounter immediately is a no-op; store it to benefit from pre-bound attributes"] +pub struct BoundCounter(Arc + Send + Sync>); + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl fmt::Debug for BoundCounter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("BoundCounter<{}>", std::any::type_name::())) + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundCounter { + /// Records an increment to the counter using the pre-bound attributes. + pub fn add(&self, value: T) { + self.0.measure(value) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/gauge.rs b/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/gauge.rs new file mode 100644 index 00000000..744b2c65 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/gauge.rs @@ -0,0 +1,104 @@ +use crate::KeyValue; +use core::fmt; +use std::sync::Arc; + +#[cfg(feature = "experimental_metrics_bound_instruments")] +use super::BoundSyncInstrument; +use super::SyncInstrument; + +/// An instrument that records independent values +/// +/// [`Gauge`] can be cloned to create multiple handles to the same instrument. If a [`Gauge`] needs to be shared, +/// users are recommended to clone the [`Gauge`] instead of creating duplicate [`Gauge`]s for the same metric. Creating +/// duplicate [`Gauge`]s for the same metric could lower SDK performance. +#[derive(Clone)] +#[non_exhaustive] +pub struct Gauge(Arc + Send + Sync>); + +impl fmt::Debug for Gauge +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("Gauge<{}>", std::any::type_name::())) + } +} + +impl Gauge { + /// Create a new gauge. + pub fn new(inner: Arc + Send + Sync>) -> Self { + Gauge(inner) + } + + /// Records an independent value. + pub fn record(&self, value: T, attributes: &[KeyValue]) { + self.0.measure(value, attributes) + } + + /// Binds this gauge to a fixed set of attributes. + /// + /// Corresponds to the `Bind` capability in the OpenTelemetry spec (status: + /// Development as of spec 1.57.0). + #[cfg(feature = "experimental_metrics_bound_instruments")] + pub fn bind(&self, attributes: &[KeyValue]) -> BoundGauge { + BoundGauge(Arc::from(self.0.bind(attributes))) + } +} + +/// A gauge bound to a fixed set of attributes. +/// +/// Created by calling [`Gauge::bind`] with an attribute set. All subsequent +/// [`record`](BoundGauge::record) calls use the pre-resolved attributes, bypassing +/// per-call attribute lookup for significantly better performance. +/// +/// `BoundGauge` can be cloned cheaply to share a single bound state across +/// threads or modules without re-binding. The underlying tracker is reclaimed +/// when the last clone is dropped. +#[cfg(feature = "experimental_metrics_bound_instruments")] +#[derive(Clone)] +#[must_use = "dropping a BoundGauge immediately is a no-op; store it to benefit from pre-bound attributes"] +pub struct BoundGauge(Arc + Send + Sync>); + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl fmt::Debug for BoundGauge { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("BoundGauge<{}>", std::any::type_name::())) + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundGauge { + /// Records an independent value using the pre-bound attributes. + pub fn record(&self, value: T) { + self.0.measure(value) + } +} + +/// An async instrument that records independent readings. +#[derive(Clone)] +#[non_exhaustive] +pub struct ObservableGauge { + _marker: std::marker::PhantomData, +} + +impl fmt::Debug for ObservableGauge +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!( + "ObservableGauge<{}>", + std::any::type_name::() + )) + } +} + +impl ObservableGauge { + /// Create a new gauge + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + ObservableGauge { + _marker: std::marker::PhantomData, + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/histogram.rs b/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/histogram.rs new file mode 100644 index 00000000..f64a49c6 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/histogram.rs @@ -0,0 +1,78 @@ +use crate::KeyValue; +use core::fmt; +use std::sync::Arc; + +#[cfg(feature = "experimental_metrics_bound_instruments")] +use super::BoundSyncInstrument; +use super::SyncInstrument; + +/// An instrument that records a distribution of values. +/// +/// [`Histogram`] can be cloned to create multiple handles to the same instrument. If a [`Histogram`] needs to be shared, +/// users are recommended to clone the [`Histogram`] instead of creating duplicate [`Histogram`]s for the same metric. Creating +/// duplicate [`Histogram`]s for the same metric could lower SDK performance. +#[derive(Clone)] +#[non_exhaustive] +pub struct Histogram(Arc + Send + Sync>); + +impl fmt::Debug for Histogram +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("Histogram<{}>", std::any::type_name::())) + } +} + +impl Histogram { + /// Create a new histogram. + pub fn new(inner: Arc + Send + Sync>) -> Self { + Histogram(inner) + } + + /// Adds an additional value to the distribution. + pub fn record(&self, value: T, attributes: &[KeyValue]) { + self.0.measure(value, attributes) + } + + /// Binds this histogram to a fixed set of attributes. + /// + /// Corresponds to the `Bind` capability in the OpenTelemetry spec (status: + /// Development as of spec 1.57.0). + #[cfg(feature = "experimental_metrics_bound_instruments")] + pub fn bind(&self, attributes: &[KeyValue]) -> BoundHistogram { + BoundHistogram(Arc::from(self.0.bind(attributes))) + } +} + +/// A histogram bound to a fixed set of attributes. +/// +/// Created by calling [`Histogram::bind`] with an attribute set. All subsequent +/// [`record`](BoundHistogram::record) calls use the pre-resolved attributes, bypassing +/// per-call attribute lookup for significantly better performance. +/// +/// `BoundHistogram` can be cloned cheaply to share a single bound state across +/// threads or modules without re-binding. The underlying tracker is reclaimed +/// when the last clone is dropped. +#[cfg(feature = "experimental_metrics_bound_instruments")] +#[derive(Clone)] +#[must_use = "dropping a BoundHistogram immediately is a no-op; store it to benefit from pre-bound attributes"] +pub struct BoundHistogram(Arc + Send + Sync>); + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl fmt::Debug for BoundHistogram { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!( + "BoundHistogram<{}>", + std::any::type_name::() + )) + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundHistogram { + /// Records a value in the histogram using the pre-bound attributes. + pub fn record(&self, value: T) { + self.0.measure(value) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/mod.rs b/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/mod.rs new file mode 100644 index 00000000..91bcf707 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/mod.rs @@ -0,0 +1,372 @@ +use gauge::{Gauge, ObservableGauge}; + +use crate::metrics::Meter; +use crate::KeyValue; +use core::fmt; +use std::borrow::Cow; +use std::marker; + +use super::{ + Counter, Histogram, InstrumentProvider, ObservableCounter, ObservableUpDownCounter, + UpDownCounter, +}; + +pub(super) mod counter; +pub(super) mod gauge; +pub(super) mod histogram; +pub(super) mod up_down_counter; + +/// An SDK implemented instrument that records measurements via callback. +pub trait AsyncInstrument: Send + Sync { + /// Observes the state of the instrument. + /// + /// It is only valid to call this within a callback. + fn observe(&self, measurement: T, attributes: &[KeyValue]); +} + +/// An SDK implemented instrument that records measurements synchronously. +pub trait SyncInstrument: Send + Sync { + /// Records a measurement synchronously. + fn measure(&self, measurement: T, attributes: &[KeyValue]); + + /// Binds this instrument to a fixed set of attributes, returning a handle + /// that records measurements without per-call attribute lookup. + /// + /// The default implementation returns a no-op handle so that custom + /// `SyncInstrument` impls that have not opted into bound instruments + /// degrade gracefully rather than panicking on the user's hot path. + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind(&self, _attributes: &[KeyValue]) -> Box + Send + Sync> { + crate::otel_debug!( + name: "SyncInstrument.BindNotImplemented", + message = "bind() called on a SyncInstrument implementation that does not override the default; measurements through the returned handle will be dropped" + ); + Box::new(crate::metrics::noop::NoopBoundSyncInstrument::new()) + } +} + +/// A pre-bound synchronous instrument that records measurements without attributes. +/// Created by calling `bind()` on a `Counter`, `UpDownCounter`, `Histogram`, +/// or `Gauge` with a fixed attribute set. +#[cfg(feature = "experimental_metrics_bound_instruments")] +pub trait BoundSyncInstrument: Send + Sync { + /// Records a measurement. The attributes were fixed at bind time. + fn measure(&self, measurement: T); +} + +/// Configuration for building a Histogram. +#[non_exhaustive] // We expect to add more configuration fields in the future +pub struct HistogramBuilder<'a, T> { + /// Instrument provider is used to create the instrument. + pub instrument_provider: &'a dyn InstrumentProvider, + + /// Name of the Histogram. + pub name: Cow<'static, str>, + + /// Description of the Histogram. + pub description: Option>, + + /// Unit of the Histogram. + pub unit: Option>, + + /// Bucket boundaries for the histogram. + pub boundaries: Option>, + + // boundaries: Vec, + _marker: marker::PhantomData, +} + +impl<'a, T> HistogramBuilder<'a, T> { + /// Create a new instrument builder + pub(crate) fn new(meter: &'a Meter, name: Cow<'static, str>) -> Self { + HistogramBuilder { + instrument_provider: meter.instrument_provider.as_ref(), + name, + description: None, + unit: None, + boundaries: None, + _marker: marker::PhantomData, + } + } + + /// Set the description for this instrument + pub fn with_description>>(mut self, description: S) -> Self { + self.description = Some(description.into()); + self + } + + /// Set the unit for this instrument. + /// + /// Unit is case sensitive(`kb` is not the same as `kB`). + /// + /// Unit must be: + /// - ASCII string + /// - No longer than 63 characters + pub fn with_unit>>(mut self, unit: S) -> Self { + self.unit = Some(unit.into()); + self + } + + /// Set the boundaries for this histogram. + /// + /// Setting boundaries is optional. By default, the boundaries are set to: + /// + /// `[0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, + /// 2500.0, 5000.0, 7500.0, 10000.0]` + /// + /// # Notes + /// - Boundaries must not contain `f64::NAN`, `f64::INFINITY` or + /// `f64::NEG_INFINITY` + /// - Values must be in strictly increasing order (e.g., each value must be + /// greater than the previous). + /// - Boundaries must not contain duplicate values. + /// + /// If invalid boundaries are provided, the instrument will not report + /// measurements. + /// Providing an empty `vec![]` means no bucket information will be + /// calculated. + /// + /// # Warning + /// Using more buckets can improve the accuracy of percentile calculations in backends. + /// However, this comes at a cost, including increased memory, CPU, and network usage. + /// Choose the number of buckets carefully, considering your application's performance + /// and resource requirements. + pub fn with_boundaries(mut self, boundaries: Vec) -> Self { + self.boundaries = Some(boundaries); + self + } +} + +impl HistogramBuilder<'_, Histogram> { + /// Creates a new instrument. + /// + /// Validates the instrument configuration and creates a new instrument. In + /// case of invalid configuration, a no-op instrument is returned + /// and an error is logged using internal logging. + pub fn build(self) -> Histogram { + self.instrument_provider.f64_histogram(self) + } +} + +impl HistogramBuilder<'_, Histogram> { + /// Creates a new instrument. + /// + /// Validates the instrument configuration and creates a new instrument. In + /// case of invalid configuration, a no-op instrument is returned + /// and an error is logged using internal logging. + pub fn build(self) -> Histogram { + self.instrument_provider.u64_histogram(self) + } +} + +/// Configuration for building a sync instrument. +#[non_exhaustive] // We expect to add more configuration fields in the future +pub struct InstrumentBuilder<'a, T> { + /// Instrument provider is used to create the instrument. + pub instrument_provider: &'a dyn InstrumentProvider, + + /// Name of the instrument. + pub name: Cow<'static, str>, + + /// Description of the instrument. + pub description: Option>, + + /// Unit of the instrument. + pub unit: Option>, + + _marker: marker::PhantomData, +} + +impl<'a, T> InstrumentBuilder<'a, T> { + /// Create a new instrument builder + pub(crate) fn new(meter: &'a Meter, name: Cow<'static, str>) -> Self { + InstrumentBuilder { + instrument_provider: meter.instrument_provider.as_ref(), + name, + description: None, + unit: None, + _marker: marker::PhantomData, + } + } + + /// Set the description for this instrument + pub fn with_description>>(mut self, description: S) -> Self { + self.description = Some(description.into()); + self + } + + /// Set the unit for this instrument. + /// + /// Unit is case sensitive(`kb` is not the same as `kB`). + /// + /// Unit must be: + /// - ASCII string + /// - No longer than 63 characters + pub fn with_unit>>(mut self, unit: S) -> Self { + self.unit = Some(unit.into()); + self + } +} + +macro_rules! build_instrument { + ($name:ident, $inst:ty) => { + impl<'a> InstrumentBuilder<'a, $inst> { + #[doc = concat!("Validates the instrument configuration and creates a new `", stringify!($inst), "`.")] + /// In case of invalid configuration, a no-op instrument is returned + /// and an error is logged using internal logging. + pub fn build(self) -> $inst { + self.instrument_provider.$name(self) + } + } + }; +} + +build_instrument!(u64_counter, Counter); +build_instrument!(f64_counter, Counter); +build_instrument!(u64_gauge, Gauge); +build_instrument!(f64_gauge, Gauge); +build_instrument!(i64_gauge, Gauge); +build_instrument!(i64_up_down_counter, UpDownCounter); +build_instrument!(f64_up_down_counter, UpDownCounter); + +impl fmt::Debug for InstrumentBuilder<'_, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InstrumentBuilder") + .field("name", &self.name) + .field("description", &self.description) + .field("unit", &self.unit) + .field("kind", &std::any::type_name::()) + .finish() + } +} + +impl fmt::Debug for HistogramBuilder<'_, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HistogramBuilder") + .field("name", &self.name) + .field("description", &self.description) + .field("unit", &self.unit) + .field("boundaries", &self.boundaries) + .field( + "kind", + &format!("Histogram<{}>", std::any::type_name::()), + ) + .finish() + } +} + +/// A function registered with a [Meter] that makes observations for the +/// instruments it is registered with. +/// +/// The async instrument parameter is used to record measurement observations +/// for these instruments. +/// +/// The function needs to complete in a finite amount of time. +pub type Callback = Box) + Send + Sync>; + +/// Configuration for building an async instrument. +#[must_use = "Callbacks will not be invoked unless you call .build() on this async instrument builder."] +#[non_exhaustive] // We expect to add more configuration fields in the future +pub struct AsyncInstrumentBuilder<'a, I, M> { + /// Instrument provider is used to create the instrument. + pub instrument_provider: &'a dyn InstrumentProvider, + + /// Name of the instrument. + pub name: Cow<'static, str>, + + /// Description of the instrument. + pub description: Option>, + + /// Unit of the instrument. + pub unit: Option>, + + /// Callbacks to be called for this instrument. + pub callbacks: Vec>, + + _inst: marker::PhantomData, +} + +impl<'a, I, M> AsyncInstrumentBuilder<'a, I, M> { + /// Create a new instrument builder + pub(crate) fn new(meter: &'a Meter, name: Cow<'static, str>) -> Self { + AsyncInstrumentBuilder { + instrument_provider: meter.instrument_provider.as_ref(), + name, + description: None, + unit: None, + _inst: marker::PhantomData, + callbacks: Vec::new(), + } + } + + /// Set the description for this instrument + pub fn with_description>>(mut self, description: S) -> Self { + self.description = Some(description.into()); + self + } + + /// Set the unit for this instrument. + /// + /// Unit is case sensitive(`kb` is not the same as `kB`). + /// + /// Unit must be: + /// - ASCII string + /// - No longer than 63 characters + pub fn with_unit>>(mut self, unit: S) -> Self { + self.unit = Some(unit.into()); + self + } + + /// Set the callback to be called for this instrument. + pub fn with_callback(mut self, callback: F) -> Self + where + F: Fn(&dyn AsyncInstrument) + Send + Sync + 'static, + { + self.callbacks.push(Box::new(callback)); + self + } +} + +macro_rules! build_async_instrument { + ($name:ident, $inst:ty, $measurement:ty) => { + impl<'a> AsyncInstrumentBuilder<'a, $inst, $measurement> { + #[doc = concat!("Validates the instrument configuration and creates a new `", stringify!($inst), "`.")] + /// In case of invalid configuration, a no-op instrument is returned + /// and an error is logged using internal logging. + pub fn build(self) -> $inst { + self.instrument_provider.$name(self) + } + } + }; +} + +build_async_instrument!(u64_observable_counter, ObservableCounter, u64); +build_async_instrument!(f64_observable_counter, ObservableCounter, f64); +build_async_instrument!(u64_observable_gauge, ObservableGauge, u64); +build_async_instrument!(f64_observable_gauge, ObservableGauge, f64); +build_async_instrument!(i64_observable_gauge, ObservableGauge, i64); +build_async_instrument!( + i64_observable_up_down_counter, + ObservableUpDownCounter, + i64 +); +build_async_instrument!( + f64_observable_up_down_counter, + ObservableUpDownCounter, + f64 +); + +impl fmt::Debug for AsyncInstrumentBuilder<'_, I, M> +where + I: AsyncInstrument, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InstrumentBuilder") + .field("name", &self.name) + .field("description", &self.description) + .field("unit", &self.unit) + .field("kind", &std::any::type_name::()) + .field("callbacks_len", &self.callbacks.len()) + .finish() + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/up_down_counter.rs b/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/up_down_counter.rs new file mode 100644 index 00000000..9cf05788 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/metrics/instruments/up_down_counter.rs @@ -0,0 +1,110 @@ +use crate::KeyValue; +use core::fmt; +use std::sync::Arc; + +#[cfg(feature = "experimental_metrics_bound_instruments")] +use super::BoundSyncInstrument; +use super::SyncInstrument; + +/// An instrument that records increasing or decreasing values. +/// +/// [`UpDownCounter`] can be cloned to create multiple handles to the same instrument. If a [`UpDownCounter`] needs to be shared, +/// users are recommended to clone the [`UpDownCounter`] instead of creating duplicate [`UpDownCounter`]s for the same metric. Creating +/// duplicate [`UpDownCounter`]s for the same metric could lower SDK performance. +#[derive(Clone)] +#[non_exhaustive] +pub struct UpDownCounter(Arc + Send + Sync>); + +impl fmt::Debug for UpDownCounter +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!( + "UpDownCounter<{}>", + std::any::type_name::() + )) + } +} + +impl UpDownCounter { + /// Create a new up down counter. + pub fn new(inner: Arc + Send + Sync>) -> Self { + UpDownCounter(inner) + } + + /// Records an increment or decrement to the counter. + pub fn add(&self, value: T, attributes: &[KeyValue]) { + self.0.measure(value, attributes) + } + + /// Binds this up-down counter to a fixed set of attributes. + /// + /// Corresponds to the `Bind` capability in the OpenTelemetry spec (status: + /// Development as of spec 1.57.0). + #[cfg(feature = "experimental_metrics_bound_instruments")] + pub fn bind(&self, attributes: &[KeyValue]) -> BoundUpDownCounter { + BoundUpDownCounter(Arc::from(self.0.bind(attributes))) + } +} + +/// An up-down counter bound to a fixed set of attributes. +/// +/// Created by calling [`UpDownCounter::bind`] with an attribute set. All subsequent +/// [`add`](BoundUpDownCounter::add) calls use the pre-resolved attributes, bypassing +/// per-call attribute lookup for significantly better performance. +/// +/// `BoundUpDownCounter` can be cloned cheaply to share a single bound state across +/// threads or modules without re-binding. The underlying tracker is reclaimed +/// when the last clone is dropped. +#[cfg(feature = "experimental_metrics_bound_instruments")] +#[derive(Clone)] +#[must_use = "dropping a BoundUpDownCounter immediately is a no-op; store it to benefit from pre-bound attributes"] +pub struct BoundUpDownCounter(Arc + Send + Sync>); + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl fmt::Debug for BoundUpDownCounter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!( + "BoundUpDownCounter<{}>", + std::any::type_name::() + )) + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundUpDownCounter { + /// Records an increment or decrement using the pre-bound attributes. + pub fn add(&self, value: T) { + self.0.measure(value) + } +} + +/// An async instrument that records increasing or decreasing values. +#[derive(Clone)] +#[non_exhaustive] +pub struct ObservableUpDownCounter { + _marker: std::marker::PhantomData, +} + +impl fmt::Debug for ObservableUpDownCounter +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!( + "ObservableUpDownCounter<{}>", + std::any::type_name::() + )) + } +} + +impl ObservableUpDownCounter { + /// Create a new observable up down counter. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + ObservableUpDownCounter { + _marker: std::marker::PhantomData, + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/metrics/meter.rs b/vendor/opentelemetry-rust/opentelemetry/src/metrics/meter.rs new file mode 100644 index 00000000..0d5729b8 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/metrics/meter.rs @@ -0,0 +1,481 @@ +use core::fmt; +use std::borrow::Cow; +use std::sync::Arc; + +use crate::metrics::{ + AsyncInstrumentBuilder, Gauge, InstrumentBuilder, InstrumentProvider, ObservableCounter, + ObservableGauge, ObservableUpDownCounter, UpDownCounter, +}; +use crate::InstrumentationScope; + +use super::{Counter, Histogram, HistogramBuilder}; + +/// Provides access to named [Meter] instances, for instrumenting an application +/// or crate. +pub trait MeterProvider { + /// Returns a new [Meter] with the provided name and default configuration. + /// + /// A [Meter] should be scoped at most to a single application or crate. The + /// name needs to be unique so it does not collide with other names used by + /// an application, nor other applications. + /// + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{global, metrics::MeterProvider}; + /// use opentelemetry::KeyValue; + /// + /// let provider = global::meter_provider(); + /// + /// // meter used in applications + /// let meter = provider.meter("my_app"); + /// ``` + fn meter(&self, name: &'static str) -> Meter { + let scope = InstrumentationScope::builder(name).build(); + self.meter_with_scope(scope) + } + + /// Returns a new [Meter] with the given instrumentation scope. + /// + /// # Examples + /// + /// ``` + /// use std::sync::Arc; + /// use opentelemetry::InstrumentationScope; + /// use opentelemetry::metrics::MeterProvider; + /// use opentelemetry_sdk::metrics::SdkMeterProvider; + /// + /// let provider = SdkMeterProvider::default(); + /// + /// // meter used in applications/binaries + /// let meter = provider.meter("my_app"); + /// + /// // meter used in libraries/crates that optionally includes version and schema url + /// let scope = InstrumentationScope::builder(env!("CARGO_PKG_NAME")) + /// .with_version(env!("CARGO_PKG_VERSION")) + /// .with_schema_url("https://opentelemetry.io/schemas/1.0.0") + /// .build(); + /// + /// let meter = provider.meter_with_scope(scope); + /// ``` + fn meter_with_scope(&self, scope: InstrumentationScope) -> Meter; +} + +/// Provides the ability to create instruments for recording measurements or +/// accepting callbacks to report measurements. +/// +/// # Instrument Types +/// +/// Instruments are categorized as either synchronous or asynchronous: +/// +/// - **Synchronous Instruments** (e.g., Counter): These are used inline with +/// your application's processing logic. For example, you might use a Counter +/// to record the number of HTTP requests received. +/// +/// - **Asynchronous Instruments** (e.g., ObservableGauge): These allow you to +/// register a callback function that is invoked during export. For instance, +/// you could use an asynchronous gauge to monitor temperature from a sensor +/// every time metrics are exported. +/// +/// # Example Usage +/// +/// ```rust +/// use opentelemetry::{global, KeyValue}; +/// +/// let meter = global::meter("my-meter"); +/// +/// // Synchronous Instruments +/// +/// // u64 Counter +/// let u64_counter = meter.u64_counter("my_u64_counter").build(); +/// u64_counter.add( +/// 10, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ); +/// +/// // f64 Counter +/// let f64_counter = meter.f64_counter("my_f64_counter").build(); +/// f64_counter.add( +/// 3.15, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ); +/// +/// +/// // u64 Observable Counter +/// let _observable_u64_counter = meter +/// .u64_observable_counter("my_observable_u64_counter") +/// .with_description("My observable counter example") +/// .with_unit("myunit") +/// .with_callback(|observer| { +/// observer.observe( +/// 100, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ) +/// }) +/// .build(); +/// +/// // f64 Observable Counter +/// let _observable_f64_counter = meter +/// .f64_observable_counter("my_observable_f64_counter") +/// .with_description("My observable counter example") +/// .with_unit("myunit") +/// .with_callback(|observer| { +/// observer.observe( +/// 100.0, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ) +/// }) +/// .build(); +/// +/// // i64 UpDownCounter +/// let updown_i64_counter = meter.i64_up_down_counter("my_updown_i64_counter").build(); +/// updown_i64_counter.add( +/// -10, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ); +/// +/// // f64 UpDownCounter +/// let updown_f64_counter = meter.f64_up_down_counter("my_updown_f64_counter").build(); +/// updown_f64_counter.add( +/// -10.67, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ); +/// +/// // i64 Observable UpDownCounter +/// let _observable_updown_i64_counter = meter +/// .i64_observable_up_down_counter("my_observable_i64_updown_counter") +/// .with_description("My observable updown counter example") +/// .with_unit("myunit") +/// .with_callback(|observer| { +/// observer.observe( +/// 100, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ) +/// }) +/// .build(); +/// +/// // f64 Observable UpDownCounter +/// let _observable_updown_f64_counter = meter +/// .f64_observable_up_down_counter("my_observable_f64_updown_counter") +/// .with_description("My observable updown counter example") +/// .with_unit("myunit") +/// .with_callback(|observer| { +/// observer.observe( +/// 100.0, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ) +/// }) +/// .build(); +/// +/// // i64 Gauge +/// let gauge = meter.i64_gauge("my_gauge").build(); +/// gauge.record( +/// -10, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ); +/// +/// // u64 Gauge +/// let gauge = meter.u64_gauge("my_gauge").build(); +/// gauge.record( +/// 101, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ); +/// +/// // f64 Gauge +/// let gauge = meter.f64_gauge("my_gauge").build(); +/// gauge.record( +/// 12.5, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ); +/// +/// // u64 Observable Gauge +/// let _observable_u64_gauge = meter +/// .u64_observable_gauge("my_u64_gauge") +/// .with_description("An observable gauge set to 1") +/// .with_unit("myunit") +/// .with_callback(|observer| { +/// observer.observe( +/// 1, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ) +/// }) +/// .build(); +/// +/// // f64 Observable Gauge +/// let _observable_f64_gauge = meter +/// .f64_observable_gauge("my_f64_gauge") +/// .with_description("An observable gauge set to 1.0") +/// .with_unit("myunit") +/// .with_callback(|observer| { +/// observer.observe( +/// 1.0, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ) +/// }) +/// .build(); +/// +/// // i64 Observable Gauge +/// let _observable_i64_gauge = meter +/// .i64_observable_gauge("my_i64_gauge") +/// .with_description("An observable gauge set to 1") +/// .with_unit("myunit") +/// .with_callback(|observer| { +/// observer.observe( +/// 1, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ) +/// }) +/// .build(); +/// +/// // f64 Histogram +/// let f64_histogram = meter.f64_histogram("my_f64_histogram").build(); +/// f64_histogram.record( +/// 10.5, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ); +/// +/// // u64 Histogram +/// let u64_histogram = meter.u64_histogram("my_u64_histogram").build(); +/// u64_histogram.record( +/// 12, +/// &[ +/// KeyValue::new("mykey1", "myvalue1"), +/// KeyValue::new("mykey2", "myvalue2"), +/// ], +/// ); +/// ``` +/// +#[derive(Clone)] +#[non_exhaustive] +pub struct Meter { + pub(crate) instrument_provider: Arc, +} + +impl Meter { + /// Create a new named meter from an instrumentation provider + #[doc(hidden)] + pub fn new(instrument_provider: Arc) -> Self { + Meter { + instrument_provider, + } + } + + /// creates an instrument builder for recording increasing values. + /// + /// [`Counter`] can be cloned to create multiple handles to the same instrument. If a [`Counter`] needs to be shared, + /// users are recommended to clone the [`Counter`] instead of creating duplicate [`Counter`]s for the same metric. Creating + /// duplicate [`Counter`]s for the same metric could lower SDK performance. + pub fn u64_counter( + &self, + name: impl Into>, + ) -> InstrumentBuilder<'_, Counter> { + InstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording increasing values. + /// + /// [`Counter`] can be cloned to create multiple handles to the same instrument. If a [`Counter`] needs to be shared, + /// users are recommended to clone the [`Counter`] instead of creating duplicate [`Counter`]s for the same metric. Creating + /// duplicate [`Counter`]s for the same metric could lower SDK performance. + pub fn f64_counter( + &self, + name: impl Into>, + ) -> InstrumentBuilder<'_, Counter> { + InstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording increasing values via callback. + pub fn u64_observable_counter( + &self, + name: impl Into>, + ) -> AsyncInstrumentBuilder<'_, ObservableCounter, u64> { + AsyncInstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording increasing values via callback. + pub fn f64_observable_counter( + &self, + name: impl Into>, + ) -> AsyncInstrumentBuilder<'_, ObservableCounter, f64> { + AsyncInstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording changes of a value. + /// + /// [`UpDownCounter`] can be cloned to create multiple handles to the same instrument. If a [`UpDownCounter`] needs to be shared, + /// users are recommended to clone the [`UpDownCounter`] instead of creating duplicate [`UpDownCounter`]s for the same metric. Creating + /// duplicate [`UpDownCounter`]s for the same metric could lower SDK performance. + pub fn i64_up_down_counter( + &self, + name: impl Into>, + ) -> InstrumentBuilder<'_, UpDownCounter> { + InstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording changes of a value. + /// + /// [`UpDownCounter`] can be cloned to create multiple handles to the same instrument. If a [`UpDownCounter`] needs to be shared, + /// users are recommended to clone the [`UpDownCounter`] instead of creating duplicate [`UpDownCounter`]s for the same metric. Creating + /// duplicate [`UpDownCounter`]s for the same metric could lower SDK performance. + pub fn f64_up_down_counter( + &self, + name: impl Into>, + ) -> InstrumentBuilder<'_, UpDownCounter> { + InstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording changes of a value via callback. + /// + /// [`UpDownCounter`] can be cloned to create multiple handles to the same instrument. If a [`UpDownCounter`] needs to be shared, + /// users are recommended to clone the [`UpDownCounter`] instead of creating duplicate [`UpDownCounter`]s for the same metric. Creating + /// duplicate [`UpDownCounter`]s for the same metric could lower SDK performance. + pub fn i64_observable_up_down_counter( + &self, + name: impl Into>, + ) -> AsyncInstrumentBuilder<'_, ObservableUpDownCounter, i64> { + AsyncInstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording changes of a value via callback. + pub fn f64_observable_up_down_counter( + &self, + name: impl Into>, + ) -> AsyncInstrumentBuilder<'_, ObservableUpDownCounter, f64> { + AsyncInstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording independent values. + /// + /// [`Gauge`] can be cloned to create multiple handles to the same instrument. If a [`Gauge`] needs to be shared, + /// users are recommended to clone the [`Gauge`] instead of creating duplicate [`Gauge`]s for the same metric. Creating + /// duplicate [`Gauge`]s for the same metric could lower SDK performance. + pub fn u64_gauge( + &self, + name: impl Into>, + ) -> InstrumentBuilder<'_, Gauge> { + InstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording independent values. + /// + /// [`Gauge`] can be cloned to create multiple handles to the same instrument. If a [`Gauge`] needs to be shared, + /// users are recommended to clone the [`Gauge`] instead of creating duplicate [`Gauge`]s for the same metric. Creating + /// duplicate [`Gauge`]s for the same metric could lower SDK performance. + pub fn f64_gauge( + &self, + name: impl Into>, + ) -> InstrumentBuilder<'_, Gauge> { + InstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording independent values. + /// [`Gauge`] can be cloned to create multiple handles to the same instrument. If a [`Gauge`] needs to be shared, + /// users are recommended to clone the [`Gauge`] instead of creating duplicate [`Gauge`]s for the same metric. Creating + /// duplicate [`Gauge`]s for the same metric could lower SDK performance. + pub fn i64_gauge( + &self, + name: impl Into>, + ) -> InstrumentBuilder<'_, Gauge> { + InstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording the current value via callback. + pub fn u64_observable_gauge( + &self, + name: impl Into>, + ) -> AsyncInstrumentBuilder<'_, ObservableGauge, u64> { + AsyncInstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording the current value via callback. + pub fn i64_observable_gauge( + &self, + name: impl Into>, + ) -> AsyncInstrumentBuilder<'_, ObservableGauge, i64> { + AsyncInstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording the current value via callback. + pub fn f64_observable_gauge( + &self, + name: impl Into>, + ) -> AsyncInstrumentBuilder<'_, ObservableGauge, f64> { + AsyncInstrumentBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording a distribution of values. + /// + /// [`Histogram`] can be cloned to create multiple handles to the same instrument. If a [`Histogram`] needs to be shared, + /// users are recommended to clone the [`Histogram`] instead of creating duplicate [`Histogram`]s for the same metric. Creating + /// duplicate [`Histogram`]s for the same metric could lower SDK performance. + pub fn f64_histogram( + &self, + name: impl Into>, + ) -> HistogramBuilder<'_, Histogram> { + HistogramBuilder::new(self, name.into()) + } + + /// creates an instrument builder for recording a distribution of values. + /// + /// [`Histogram`] can be cloned to create multiple handles to the same instrument. If a [`Histogram`] needs to be shared, + /// users are recommended to clone the [`Histogram`] instead of creating duplicate [`Histogram`]s for the same metric. Creating + /// duplicate [`Histogram`]s for the same metric could lower SDK performance. + pub fn u64_histogram( + &self, + name: impl Into>, + ) -> HistogramBuilder<'_, Histogram> { + HistogramBuilder::new(self, name.into()) + } +} + +impl fmt::Debug for Meter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Meter") + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/metrics/mod.rs b/vendor/opentelemetry-rust/opentelemetry/src/metrics/mod.rs new file mode 100644 index 00000000..2c1ccefb --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/metrics/mod.rs @@ -0,0 +1,132 @@ +//! # OpenTelemetry Metrics API + +use std::sync::Arc; + +mod instruments; +mod meter; +pub mod noop; +#[cfg(feature = "experimental_metrics_bound_instruments")] +pub use instruments::{ + counter::BoundCounter, gauge::BoundGauge, histogram::BoundHistogram, + up_down_counter::BoundUpDownCounter, BoundSyncInstrument, +}; +pub use instruments::{ + counter::{Counter, ObservableCounter}, + gauge::{Gauge, ObservableGauge}, + histogram::Histogram, + up_down_counter::{ObservableUpDownCounter, UpDownCounter}, + AsyncInstrument, AsyncInstrumentBuilder, Callback, HistogramBuilder, InstrumentBuilder, + SyncInstrument, +}; +pub use meter::{Meter, MeterProvider}; +pub use noop::NoopMeterProvider; + +/// SDK implemented trait for creating instruments +pub trait InstrumentProvider { + /// creates an instrument for recording increasing values. + fn u64_counter(&self, _builder: InstrumentBuilder<'_, Counter>) -> Counter { + Counter::new(Arc::new(noop::NoopSyncInstrument::new())) + } + + /// creates an instrument for recording increasing values. + fn f64_counter(&self, _builder: InstrumentBuilder<'_, Counter>) -> Counter { + Counter::new(Arc::new(noop::NoopSyncInstrument::new())) + } + + /// creates an instrument for recording increasing values via callback. + fn u64_observable_counter( + &self, + _builder: AsyncInstrumentBuilder<'_, ObservableCounter, u64>, + ) -> ObservableCounter { + ObservableCounter::new() + } + + /// creates an instrument for recording increasing values via callback. + fn f64_observable_counter( + &self, + _builder: AsyncInstrumentBuilder<'_, ObservableCounter, f64>, + ) -> ObservableCounter { + ObservableCounter::new() + } + + /// creates an instrument for recording changes of a value. + fn i64_up_down_counter( + &self, + _builder: InstrumentBuilder<'_, UpDownCounter>, + ) -> UpDownCounter { + UpDownCounter::new(Arc::new(noop::NoopSyncInstrument::new())) + } + + /// creates an instrument for recording changes of a value. + fn f64_up_down_counter( + &self, + _builder: InstrumentBuilder<'_, UpDownCounter>, + ) -> UpDownCounter { + UpDownCounter::new(Arc::new(noop::NoopSyncInstrument::new())) + } + + /// creates an instrument for recording changes of a value. + fn i64_observable_up_down_counter( + &self, + _builder: AsyncInstrumentBuilder<'_, ObservableUpDownCounter, i64>, + ) -> ObservableUpDownCounter { + ObservableUpDownCounter::new() + } + + /// creates an instrument for recording changes of a value via callback. + fn f64_observable_up_down_counter( + &self, + _builder: AsyncInstrumentBuilder<'_, ObservableUpDownCounter, f64>, + ) -> ObservableUpDownCounter { + ObservableUpDownCounter::new() + } + + /// creates an instrument for recording independent values. + fn u64_gauge(&self, _builder: InstrumentBuilder<'_, Gauge>) -> Gauge { + Gauge::new(Arc::new(noop::NoopSyncInstrument::new())) + } + + /// creates an instrument for recording independent values. + fn f64_gauge(&self, _builder: InstrumentBuilder<'_, Gauge>) -> Gauge { + Gauge::new(Arc::new(noop::NoopSyncInstrument::new())) + } + + /// creates an instrument for recording independent values. + fn i64_gauge(&self, _builder: InstrumentBuilder<'_, Gauge>) -> Gauge { + Gauge::new(Arc::new(noop::NoopSyncInstrument::new())) + } + + /// creates an instrument for recording the current value via callback. + fn u64_observable_gauge( + &self, + _builder: AsyncInstrumentBuilder<'_, ObservableGauge, u64>, + ) -> ObservableGauge { + ObservableGauge::new() + } + + /// creates an instrument for recording the current value via callback. + fn i64_observable_gauge( + &self, + _builder: AsyncInstrumentBuilder<'_, ObservableGauge, i64>, + ) -> ObservableGauge { + ObservableGauge::new() + } + + /// creates an instrument for recording the current value via callback. + fn f64_observable_gauge( + &self, + _builder: AsyncInstrumentBuilder<'_, ObservableGauge, f64>, + ) -> ObservableGauge { + ObservableGauge::new() + } + + /// creates an instrument for recording a distribution of values. + fn f64_histogram(&self, _builder: HistogramBuilder<'_, Histogram>) -> Histogram { + Histogram::new(Arc::new(noop::NoopSyncInstrument::new())) + } + + /// creates an instrument for recording a distribution of values. + fn u64_histogram(&self, _builder: HistogramBuilder<'_, Histogram>) -> Histogram { + Histogram::new(Arc::new(noop::NoopSyncInstrument::new())) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/metrics/noop.rs b/vendor/opentelemetry-rust/opentelemetry/src/metrics/noop.rs new file mode 100644 index 00000000..0a1dad5b --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/metrics/noop.rs @@ -0,0 +1,92 @@ +//! # No-op OpenTelemetry Metrics Implementation +//! +//! This implementation is returned as the global Meter if no `MeterProvider` +//! has been set. It is expected to have minimal resource utilization and +//! runtime impact. +use crate::{ + metrics::{InstrumentProvider, Meter, MeterProvider}, + otel_debug, KeyValue, +}; +use std::sync::Arc; + +#[cfg(feature = "experimental_metrics_bound_instruments")] +use super::instruments::BoundSyncInstrument; +use super::instruments::SyncInstrument; + +/// A no-op instance of a `MetricProvider` +#[derive(Debug, Default)] +pub struct NoopMeterProvider { + _private: (), +} + +impl NoopMeterProvider { + /// Create a new no-op meter provider. + pub fn new() -> Self { + NoopMeterProvider { _private: () } + } +} + +impl MeterProvider for NoopMeterProvider { + fn meter_with_scope(&self, scope: crate::InstrumentationScope) -> Meter { + otel_debug!(name: "NoopMeterProvider.MeterCreation", meter_name = scope.name(), message = "Meter was obtained from a NoopMeterProvider. No metrics will be recorded. If global::meter_with_scope()/meter() was used, ensure that a valid MeterProvider is set globally before creating Meter."); + Meter::new(Arc::new(NoopMeter::new())) + } +} + +/// A no-op instance of a `Meter` +#[derive(Debug, Default)] +pub(crate) struct NoopMeter { + _private: (), +} + +impl NoopMeter { + /// Create a new no-op meter core. + pub(crate) fn new() -> Self { + NoopMeter { _private: () } + } +} + +impl InstrumentProvider for NoopMeter {} + +/// A no-op sync instrument +#[derive(Debug, Default)] +pub(crate) struct NoopSyncInstrument { + _private: (), +} + +impl NoopSyncInstrument { + /// Create a new no-op sync instrument + pub(crate) fn new() -> Self { + NoopSyncInstrument { _private: () } + } +} + +impl SyncInstrument for NoopSyncInstrument { + fn measure(&self, _value: T, _attributes: &[KeyValue]) { + // Ignored + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind(&self, _attributes: &[KeyValue]) -> Box + Send + Sync> { + Box::new(NoopBoundSyncInstrument::new()) + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +pub(crate) struct NoopBoundSyncInstrument { + _private: (), +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl NoopBoundSyncInstrument { + pub(crate) fn new() -> Self { + NoopBoundSyncInstrument { _private: () } + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundSyncInstrument for NoopBoundSyncInstrument { + fn measure(&self, _measurement: T) { + // Ignored + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/propagation/composite.rs b/vendor/opentelemetry-rust/opentelemetry/src/propagation/composite.rs new file mode 100644 index 00000000..46b7e674 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/propagation/composite.rs @@ -0,0 +1,286 @@ +//! # Composite Propagator +//! +//! A utility over multiple `Propagator`s to group multiple Propagators from different cross-cutting +//! concerns in order to leverage them as a single entity. +//! +//! Each composite Propagator will implement a specific Propagator type, such as TextMapPropagator, +//! as different Propagator types will likely operate on different data types. +use crate::{ + propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator}, + Context, +}; +use std::collections::HashSet; + +/// Composite propagator for [`TextMapPropagator`]s. +/// +/// A propagator that chains multiple [`TextMapPropagator`] propagators together, +/// injecting or extracting by their respective HTTP header names. +/// +/// Injection and extraction from this propagator will preserve the order of the +/// injectors and extractors passed in during initialization. +/// +/// # Examples +/// +/// ``` +/// use opentelemetry::{ +/// baggage::BaggageExt, +/// propagation::{TextMapPropagator, TextMapCompositePropagator}, +/// +/// trace::{TraceContextExt, Tracer, TracerProvider}, +/// Context, KeyValue, +/// }; +/// use opentelemetry_sdk::propagation::{ +/// BaggagePropagator, TraceContextPropagator, +/// }; +/// use opentelemetry_sdk::trace as sdktrace; +/// use std::collections::HashMap; +/// +/// // First create 1 or more propagators +/// let baggage_propagator = BaggagePropagator::new(); +/// let trace_context_propagator = TraceContextPropagator::new(); +/// +/// // Then create a composite propagator +/// let composite_propagator = TextMapCompositePropagator::new(vec![ +/// Box::new(baggage_propagator), +/// Box::new(trace_context_propagator), +/// ]); +/// +/// // Then for a given implementation of `Injector` +/// let mut injector = HashMap::new(); +/// +/// // And a given span +/// let example_span = sdktrace::SdkTracerProvider::default() +/// .tracer("example-component") +/// .start("span-name"); +/// +/// // with the current context, call inject to add the headers +/// composite_propagator.inject_context( +/// &Context::current_with_span(example_span) +/// .with_baggage(vec![KeyValue::new("test", "example")]), +/// &mut injector, +/// ); +/// +/// // The injector now has both `baggage` and `traceparent` headers +/// assert!(injector.get("baggage").is_some()); +/// assert!(injector.get("traceparent").is_some()); +/// ``` +#[derive(Debug)] +pub struct TextMapCompositePropagator { + propagators: Vec>, + fields: Vec, +} + +impl TextMapCompositePropagator { + /// Constructs a new propagator out of instances of [`TextMapPropagator`]. + /// + /// [`TextMapPropagator`]: TextMapPropagator + pub fn new(propagators: Vec>) -> Self { + let mut fields = HashSet::new(); + for propagator in &propagators { + fields.extend(propagator.fields().map(ToString::to_string)); + } + + TextMapCompositePropagator { + propagators, + fields: fields.into_iter().collect(), + } + } +} + +impl TextMapPropagator for TextMapCompositePropagator { + /// Encodes the values of the `Context` and injects them into the `Injector`. + fn inject_context(&self, context: &Context, injector: &mut dyn Injector) { + for propagator in &self.propagators { + propagator.inject_context(context, injector) + } + } + + /// Retrieves encoded `Context` information using the `Extractor`. If no data was + /// retrieved OR if the retrieved data is invalid, then the current `Context` is + /// returned. + fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context { + self.propagators + .iter() + .fold(cx.clone(), |current_cx, propagator| { + propagator.extract_with_context(¤t_cx, extractor) + }) + } + + fn fields(&self) -> FieldIter<'_> { + FieldIter::new(self.fields.as_slice()) + } +} + +#[cfg(all(test, feature = "trace"))] +mod tests { + use crate::baggage::BaggageExt; + use crate::propagation::TextMapCompositePropagator; + use crate::testing::trace::TestSpan; + use crate::{ + propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator}, + trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState}, + Context, KeyValue, + }; + use std::collections::HashMap; + + /// A test propagator that injects and extracts a single header. + #[derive(Debug)] + struct TestPropagator { + header: &'static str, + fields: Vec, // used by fields method + } + + impl TestPropagator { + #[allow(unreachable_pub)] + pub fn new(header: &'static str) -> Self { + TestPropagator { + header, + fields: vec![header.to_string()], + } + } + } + + impl TextMapPropagator for TestPropagator { + fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) { + let span = cx.span(); + let span_context = span.span_context(); + match self.header { + "span-id" => injector.set(self.header, format!("{:x}", span_context.span_id())), + "baggage" => injector.set(self.header, cx.baggage().to_string()), + _ => {} + } + } + + fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context { + match (self.header, extractor.get(self.header)) { + ("span-id", Some(val)) => cx.with_remote_span_context(SpanContext::new( + TraceId::from(1), + SpanId::from(u64::from_str_radix(val, 16).unwrap()), + TraceFlags::default(), + false, + TraceState::default(), + )), + ("baggage", Some(_)) => cx.with_baggage(vec![KeyValue::new("baggagekey", "value")]), + _ => cx.clone(), + } + } + + fn fields(&self) -> FieldIter<'_> { + FieldIter::new(self.fields.as_slice()) + } + } + + fn setup() -> Context { + let mut cx = Context::default(); + cx = cx.with_span(TestSpan(SpanContext::new( + TraceId::from(1), + SpanId::from(11), + TraceFlags::default(), + true, + TraceState::default(), + ))); + // setup for baggage propagator + cx.with_baggage(vec![KeyValue::new("baggagekey", "value")]) + } + + fn test_data() -> Vec<(&'static str, &'static str)> { + vec![("span-id", "b"), ("baggage", "baggagekey=value")] + } + + #[test] + fn zero_propogators_are_noop() { + // setup + let composite_propagator = TextMapCompositePropagator::new(vec![]); + let cx = setup(); + + let mut injector = HashMap::new(); + composite_propagator.inject_context(&cx, &mut injector); + + assert_eq!(injector.len(), 0); + for (header_name, header_value) in test_data() { + let mut extractor = HashMap::new(); + extractor.insert(header_name.to_string(), header_value.to_string()); + assert_eq!( + composite_propagator + .extract(&extractor) + .span() + .span_context(), + &SpanContext::empty_context() + ); + } + } + + #[test] + fn inject_multiple_propagators() { + let composite_propagator = TextMapCompositePropagator::new(vec![ + Box::new(TestPropagator::new("span-id")), + Box::new(TestPropagator::new("baggage")), + ]); + + let cx = setup(); + let mut injector = HashMap::new(); + composite_propagator.inject_context(&cx, &mut injector); + + for (header_name, header_value) in test_data() { + assert_eq!(injector.get(header_name), Some(&header_value.to_string())); + } + } + + #[test] + fn extract_multiple_propagators() { + let composite_propagator = TextMapCompositePropagator::new(vec![ + Box::new(TestPropagator::new("span-id")), + Box::new(TestPropagator::new("baggage")), + ]); + + let mut extractor = HashMap::new(); + for (header_name, header_value) in test_data() { + extractor.insert(header_name.to_string(), header_value.to_string()); + } + let cx = composite_propagator.extract(&extractor); + assert_eq!( + cx.span().span_context(), + &SpanContext::new( + TraceId::from(1), + SpanId::from(11), + TraceFlags::default(), + false, + TraceState::default(), + ) + ); + assert_eq!(cx.baggage().to_string(), "baggagekey=value",); + } + + #[test] + fn test_get_fields() { + let test_cases = vec![ + // name, header_name, expected_result + // ("single propagator", vec!["span-id"], vec!["span-id"]), + ( + "multiple propagators with order", + vec!["span-id", "baggage"], + vec!["baggage", "span-id"], + ), + ]; + + for test_case in test_cases { + let test_propagators = test_case + .1 + .into_iter() + .map(|name| { + Box::new(TestPropagator::new(name)) as Box + }) + .collect(); + + let composite_propagator = TextMapCompositePropagator::new(test_propagators); + + let mut fields = composite_propagator + .fields() + .map(|s| s.to_string()) + .collect::>(); + fields.sort(); + + assert_eq!(fields, test_case.2); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/propagation/mod.rs b/vendor/opentelemetry-rust/opentelemetry/src/propagation/mod.rs new file mode 100644 index 00000000..7848ffe2 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/propagation/mod.rs @@ -0,0 +1,178 @@ +//! # OpenTelemetry Propagator interface +//! Cross-cutting concerns send their state to the next process using Propagators, which are defined +//! as objects used to read and write context data to and from messages exchanged by the applications. +//! +//! `Propagator`s leverage the [`Context`] to inject and extract data for each cross-cutting concern, +//! such as `TraceContext` and [`Baggage`]. +//! +//! The Propagators API is expected to be leveraged by users writing instrumentation libraries. +//! +//! Currently, the following `Propagator` types are supported: +//! - [`TextMapPropagator`], inject values into and extracts values from carriers as string key/value pairs +//! +//! A binary Propagator type will be added in +//! the future, See [tracking issues](https://github.com/open-telemetry/opentelemetry-specification/issues/437)). +//! +//! `Propagator`s uses [`Injector`] and [`Extractor`] to read and write context data to and from messages. +//! Each specific Propagator type defines its expected carrier type, such as a string map or a byte array. +//! +//! [`Baggage`]: crate::baggage::Baggage +//! [`Context`]: crate::Context + +use std::collections::HashMap; + +pub mod composite; +pub mod text_map_propagator; + +pub use composite::TextMapCompositePropagator; +pub use text_map_propagator::TextMapPropagator; + +/// Injector provides an interface for adding fields from an underlying struct like `HashMap` +pub trait Injector { + /// Add a key and value to the underlying data. + fn set(&mut self, key: &str, value: String); + + #[allow(unused_variables)] + /// Hint to reserve capacity for at least `additional` more entries to be inserted. + fn reserve(&mut self, additional: usize) {} +} + +/// Extractor provides an interface for removing fields from an underlying struct like `HashMap` +pub trait Extractor { + /// Get a value from a key from the underlying data. + fn get(&self, key: &str) -> Option<&str>; + + /// Collect all the keys from the underlying data. + fn keys(&self) -> Vec<&str>; + + /// Get all values from a key from the underlying data. + fn get_all(&self, key: &str) -> Option> { + self.get(key).map(|value| vec![value]) + } +} + +impl Injector for HashMap { + /// Set a key and value in the HashMap. + fn set(&mut self, key: &str, value: String) { + self.insert(key.to_lowercase(), value); + } + + /// Reserves capacity for at least `additional` more entries to be inserted. + fn reserve(&mut self, additional: usize) { + self.reserve(additional); + } +} + +impl Extractor for HashMap { + /// Get a value for a key from the HashMap. + fn get(&self, key: &str) -> Option<&str> { + self.get(&key.to_lowercase()).map(|v| v.as_str()) + } + + /// Collect all the keys from the HashMap. + fn keys(&self) -> Vec<&str> { + self.keys().map(|k| k.as_str()).collect::>() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hash_map_get() { + let mut carrier = HashMap::new(); + carrier.set("headerName", "value".to_string()); + + assert_eq!( + Extractor::get(&carrier, "HEADERNAME"), + Some("value"), + "case insensitive extraction" + ); + } + + #[test] + fn hash_map_get_all() { + let mut carrier = HashMap::new(); + carrier.set("headerName", "value".to_string()); + + assert_eq!( + Extractor::get_all(&carrier, "HEADERNAME"), + Some(vec!["value"]), + "case insensitive get_all extraction" + ); + } + + #[test] + fn hash_map_get_all_missing_key() { + let mut carrier = HashMap::new(); + carrier.set("headerName", "value".to_string()); + + assert_eq!( + Extractor::get_all(&carrier, "missing_key"), + None, + "case insensitive get_all extraction" + ); + } + + #[test] + fn hash_map_keys() { + let mut carrier = HashMap::new(); + carrier.set("headerName1", "value1".to_string()); + carrier.set("headerName2", "value2".to_string()); + + let got = Extractor::keys(&carrier); + assert_eq!(got.len(), 2); + assert!(got.contains(&"headername1")); + assert!(got.contains(&"headername2")); + } + + #[test] + fn hash_map_injector_reserve() { + let mut carrier = HashMap::new(); + + // Test that reserve doesn't panic and works correctly + Injector::reserve(&mut carrier, 10); + + // Verify the HashMap still works after reserve + Injector::set(&mut carrier, "test_key", "test_value".to_string()); + assert_eq!(Extractor::get(&carrier, "test_key"), Some("test_value")); + + // Test reserve with zero capacity + Injector::reserve(&mut carrier, 0); + Injector::set(&mut carrier, "another_key", "another_value".to_string()); + assert_eq!( + Extractor::get(&carrier, "another_key"), + Some("another_value") + ); + + // Test that capacity is actually reserved (at least the requested amount) + let mut new_carrier = HashMap::new(); + Injector::reserve(&mut new_carrier, 5); + let initial_capacity = new_carrier.capacity(); + + // Add some elements and verify capacity doesn't decrease + for i in 0..3 { + Injector::set( + &mut new_carrier, + &format!("key{}", i), + format!("value{}", i), + ); + } + + assert!(new_carrier.capacity() >= initial_capacity); + assert!(new_carrier.capacity() >= 5); + } + + #[test] + fn injector_reserve() { + // Test to have full line coverage of default method + struct TestInjector(); + impl Injector for TestInjector { + fn set(&mut self, _key: &str, _value: String) {} + } + let mut test_injector = TestInjector(); + Injector::reserve(&mut test_injector, 4711); + Injector::set(&mut test_injector, "key", "value".to_string()); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/propagation/text_map_propagator.rs b/vendor/opentelemetry-rust/opentelemetry/src/propagation/text_map_propagator.rs new file mode 100644 index 00000000..c078e494 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/propagation/text_map_propagator.rs @@ -0,0 +1,77 @@ +//! # TextMapPropagator +//! +//! [`TextMapPropagator`] performs the injection and extraction of a cross-cutting concern value as +//! string key/values pairs into carriers that travel in-band across process boundaries. +//! +//! The carrier of propagated data on both the client (injector) and server (extractor) side is +//! usually an HTTP request. +//! +//! In order to increase compatibility, the key/value pairs MUST only consist of US-ASCII characters +//! that make up valid HTTP header fields as per RFC 7230. +use crate::{ + propagation::{Extractor, Injector}, + Context, +}; +use std::fmt::Debug; +use std::slice; + +/// Methods to inject and extract a value as text into injectors and extractors that travel +/// in-band across process boundaries. +pub trait TextMapPropagator: Debug { + /// Properly encodes the values of the current [`Context`] and injects them into + /// the [`Injector`]. + /// + /// [`Context`]: crate::Context + /// [`Injector`]: crate::propagation::Injector + fn inject(&self, injector: &mut dyn Injector) { + Context::map_current(|cx| self.inject_context(cx, injector)) + } + + /// Properly encodes the values of the [`Context`] and injects them into the + /// [`Injector`]. + /// + /// [`Context`]: crate::Context + /// [`Injector`]: crate::propagation::Injector + fn inject_context(&self, cx: &Context, injector: &mut dyn Injector); + + /// Retrieves encoded data using the provided [`Extractor`]. If no data for this + /// format was retrieved OR if the retrieved data is invalid, then the current + /// [`Context`] is returned. + /// + /// [`Context`]: crate::Context + /// [`Injector`]: crate::propagation::Extractor + fn extract(&self, extractor: &dyn Extractor) -> Context { + Context::map_current(|cx| self.extract_with_context(cx, extractor)) + } + + /// Retrieves encoded data using the provided [`Extractor`]. If no data for this + /// format was retrieved OR if the retrieved data is invalid, then the given + /// [`Context`] is returned. + /// + /// [`Context`]: crate::Context + /// [`Injector`]: crate::propagation::Extractor + fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context; + + /// Returns iter of fields used by [`TextMapPropagator`] + /// + fn fields(&self) -> FieldIter<'_>; +} + +/// An iterator over fields of a [`TextMapPropagator`] +/// +#[derive(Debug)] +pub struct FieldIter<'a>(slice::Iter<'a, String>); + +impl<'a> FieldIter<'a> { + /// Create a new `FieldIter` from a slice of propagator fields + pub fn new(fields: &'a [String]) -> Self { + FieldIter(fields.iter()) + } +} + +impl<'a> Iterator for FieldIter<'a> { + type Item = &'a str; + fn next(&mut self) -> Option { + self.0.next().map(|field| field.as_str()) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/testing/mod.rs b/vendor/opentelemetry-rust/opentelemetry/src/testing/mod.rs new file mode 100644 index 00000000..df28ce8d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/testing/mod.rs @@ -0,0 +1,3 @@ +#[cfg(feature = "trace")] +#[cfg_attr(docsrs, doc(cfg(feature = "trace")))] +pub mod trace; diff --git a/vendor/opentelemetry-rust/opentelemetry/src/testing/trace.rs b/vendor/opentelemetry-rust/opentelemetry/src/testing/trace.rs new file mode 100644 index 00000000..c09b0fef --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/testing/trace.rs @@ -0,0 +1,36 @@ +use crate::{ + trace::{Span, SpanContext, Status}, + KeyValue, +}; +use std::borrow::Cow; + +#[derive(Debug)] +pub struct TestSpan(pub SpanContext); + +impl Span for TestSpan { + fn add_event_with_timestamp( + &mut self, + _name: T, + _timestamp: std::time::SystemTime, + _attributes: Vec, + ) where + T: Into>, + { + } + fn span_context(&self) -> &SpanContext { + &self.0 + } + fn is_recording(&self) -> bool { + false + } + fn set_attribute(&mut self, _attribute: KeyValue) {} + fn set_status(&mut self, _status: Status) {} + fn update_name(&mut self, _new_name: T) + where + T: Into>, + { + } + + fn add_link(&mut self, _span_context: SpanContext, _attributes: Vec) {} + fn end_with_timestamp(&mut self, _timestamp: std::time::SystemTime) {} +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/trace/context.rs b/vendor/opentelemetry-rust/opentelemetry/src/trace/context.rs new file mode 100644 index 00000000..386bb048 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/trace/context.rs @@ -0,0 +1,391 @@ +//! Context extensions for tracing +use crate::{ + global, otel_debug, + trace::{Span, SpanContext, Status}, + Context, ContextGuard, KeyValue, +}; +use std::{borrow::Cow, error::Error, sync::Mutex}; + +// Re-export for compatability. This used to be contained here. +pub use crate::context::{FutureExt, WithContext}; + +const NOOP_SPAN: SynchronizedSpan = SynchronizedSpan { + span_context: SpanContext::NONE, + inner: None, +}; + +/// A reference to the currently active span in this context. +#[derive(Debug)] +pub struct SpanRef<'a>(&'a SynchronizedSpan); + +#[derive(Debug)] +pub(crate) struct SynchronizedSpan { + /// Immutable span context + span_context: SpanContext, + /// Mutable span inner that requires synchronization + inner: Option>, +} + +impl SynchronizedSpan { + pub(crate) fn span_context(&self) -> &SpanContext { + &self.span_context + } +} + +impl From for SynchronizedSpan { + fn from(value: SpanContext) -> Self { + Self { + span_context: value, + inner: None, + } + } +} + +impl From for SynchronizedSpan { + fn from(value: T) -> Self { + Self { + span_context: value.span_context().clone(), + inner: Some(Mutex::new(global::BoxedSpan::new(value))), + } + } +} + +impl SpanRef<'_> { + fn with_inner_mut(&self, f: F) { + if let Some(ref inner) = self.0.inner { + match inner.lock() { + Ok(mut locked) => f(&mut locked), + Err(err) => { + otel_debug!( + name: "SpanRef.LockFailed", + message = "Failed to acquire lock for SpanRef: {:?}", + reason = format!("{:?}", err), + span_context = format!("{:?}", self.0.span_context)); + } + } + } + } +} + +impl SpanRef<'_> { + /// Record an event in the context this span. + /// + /// Note that the OpenTelemetry project documents certain "[standard + /// attributes]" that have prescribed semantic meanings and are available via + /// the [opentelemetry_semantic_conventions] crate. + /// + /// [standard attributes]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/trace/semantic_conventions/README.md + /// [opentelemetry_semantic_conventions]: https://docs.rs/opentelemetry-semantic-conventions + pub fn add_event(&self, name: T, attributes: Vec) + where + T: Into>, + { + self.with_inner_mut(|inner| inner.add_event(name, attributes)) + } + + /// Record an error as an event for this span. + /// + /// An additional call to [Span::set_status] is required if the status of the + /// span should be set to error, as this method does not change the span status. + /// + /// If this span is not being recorded then this method does nothing. + pub fn record_error(&self, err: &dyn Error) { + self.with_inner_mut(|inner| inner.record_error(err)) + } + + /// Record an event with a timestamp in the context this span. + /// + /// Note that the OpenTelemetry project documents certain "[standard + /// attributes]" that have prescribed semantic meanings and are available via + /// the [opentelemetry_semantic_conventions] crate. + /// + /// [standard attributes]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/trace/semantic_conventions/README.md + /// [opentelemetry_semantic_conventions]: https://docs.rs/opentelemetry-semantic-conventions + pub fn add_event_with_timestamp( + &self, + name: T, + timestamp: std::time::SystemTime, + attributes: Vec, + ) where + T: Into>, + { + self.with_inner_mut(move |inner| { + inner.add_event_with_timestamp(name, timestamp, attributes) + }) + } + + /// A reference to the [`SpanContext`] for this span. + pub fn span_context(&self) -> &SpanContext { + &self.0.span_context + } + + /// Returns `true` if this span is recording information. + /// + /// Spans will not be recording information after they have ended. + /// + /// This flag may be `true` despite the entire trace being sampled out. This + /// allows recording and processing of information about the individual + /// spans without sending it to the backend. An example of this scenario may + /// be recording and processing of all incoming requests for the processing + /// and building of SLA/SLO latency charts while sending only a subset - + /// sampled spans - to the backend. + pub fn is_recording(&self) -> bool { + self.0 + .inner + .as_ref() + .and_then(|inner| inner.lock().ok().map(|active| active.is_recording())) + .unwrap_or(false) + } + + /// Set an attribute of this span. + /// + /// Setting an attribute with the same key as an existing attribute + /// generally overwrites the existing attribute's value. + /// + /// Note that the OpenTelemetry project documents certain "[standard + /// attributes]" that have prescribed semantic meanings and are available via + /// the [opentelemetry_semantic_conventions] crate. + /// + /// [standard attributes]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/trace/semantic_conventions/README.md + /// [opentelemetry_semantic_conventions]: https://docs.rs/opentelemetry-semantic-conventions + pub fn set_attribute(&self, attribute: crate::KeyValue) { + self.with_inner_mut(move |inner| inner.set_attribute(attribute)) + } + + /// Set multiple attributes of this span. + /// + /// Setting an attribute with the same key as an existing attribute + /// generally overwrites the existing attribute's value. + /// + /// Note that the OpenTelemetry project documents certain "[standard + /// attributes]" that have prescribed semantic meanings and are available via + /// the [opentelemetry_semantic_conventions] crate. + /// + /// [standard attributes]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/trace/semantic_conventions/README.md + /// [opentelemetry_semantic_conventions]: https://docs.rs/opentelemetry-semantic-conventions + pub fn set_attributes(&self, attributes: impl IntoIterator) { + self.with_inner_mut(move |inner| inner.set_attributes(attributes)) + } + + /// Sets the status of this `Span`. + /// + /// If used, this will override the default span status, which is [`Status::Unset`]. + pub fn set_status(&self, status: Status) { + self.with_inner_mut(move |inner| inner.set_status(status)) + } + + /// Updates the span's name. + /// + /// After this update, any sampling behavior based on the name will depend on + /// the implementation. + pub fn update_name(&self, new_name: T) + where + T: Into>, + { + self.with_inner_mut(move |inner| inner.update_name(new_name)) + } + + /// Adds a [`Link`] to another [`SpanContext`]. + /// + /// This method allows linking the current span to another span, identified by + /// its `SpanContext`. Links can be used to connect spans from different traces + /// or within the same trace. Attributes can be attached to the link to provide + /// additional context or metadata. + /// + /// # Arguments + /// + /// * `span_context` - The `SpanContext` of the span to link to. This represents + /// the target span's unique identifiers and trace information. + /// * `attributes` - A vector of `KeyValue` pairs that describe additional + /// attributes of the link. These attributes can include any contextual + /// information relevant to the link between the spans. + /// + /// Note - Any [`Link`] added via this mechanism is not accessible to a `Sampler`. + /// It is recommended to add Links at [`Span`] creation time, rather than adding + /// them afterwards. + /// + /// [`Link`]: crate::trace::Link + pub fn add_link(&self, span_context: SpanContext, attributes: Vec) { + self.with_inner_mut(move |inner| inner.add_link(span_context, attributes)); + } + + /// Signals that the operation described by this span has now ended. + pub fn end(&self) { + self.end_with_timestamp(crate::time::now()); + } + + /// Signals that the operation described by this span ended at the given time. + pub fn end_with_timestamp(&self, timestamp: std::time::SystemTime) { + self.with_inner_mut(move |inner| inner.end_with_timestamp(timestamp)) + } +} + +/// Methods for storing and retrieving trace data in a [`Context`]. +/// +/// See [`Context`] for examples of setting and retrieving the current context. +pub trait TraceContextExt { + /// Returns a clone of the current context with the included [`Span`]. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{global, trace::{TraceContextExt, Tracer}, Context}; + /// + /// let tracer = global::tracer("example"); + /// + /// // build a span + /// let span = tracer.start("parent_span"); + /// + /// // create a new context from the currently active context that includes this span + /// let cx = Context::current_with_span(span); + /// + /// // create a child span by explicitly specifying the parent context + /// let child = tracer.start_with_context("child_span", &cx); + /// # drop(child) + /// ``` + fn current_with_span(span: T) -> Self; + + /// Returns a clone of this context with the included span. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{global, trace::{TraceContextExt, Tracer}, Context}; + /// + /// fn fn_with_passed_in_context(cx: &Context) { + /// let tracer = global::tracer("example"); + /// + /// // build a span + /// let span = tracer.start("parent_span"); + /// + /// // create a new context from the given context that includes the span + /// let cx_with_parent = cx.with_span(span); + /// + /// // create a child span by explicitly specifying the parent context + /// let child = tracer.start_with_context("child_span", &cx_with_parent); + /// # drop(child) + /// } + /// + fn with_span(&self, span: T) -> Self; + + /// Returns a reference to this context's span, or the default no-op span if + /// none has been set. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{trace::TraceContextExt, Context}; + /// + /// // Add an event to the currently active span + /// Context::map_current(|cx| cx.span().add_event("An event!", vec![])); + /// ``` + fn span(&self) -> SpanRef<'_>; + + /// Returns whether or not an active span has been set. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{trace::TraceContextExt, Context}; + /// + /// assert!(!Context::map_current(|cx| cx.has_active_span())); + /// ``` + fn has_active_span(&self) -> bool; + + /// Returns a copy of this context with the span context included. + /// + /// This is useful for building propagators. + fn with_remote_span_context(&self, span_context: crate::trace::SpanContext) -> Self; +} + +impl TraceContextExt for Context { + fn current_with_span(span: T) -> Self { + Context::current_with_synchronized_span(span.into()) + } + + fn with_span(&self, span: T) -> Self { + self.with_synchronized_span(span.into()) + } + + fn span(&self) -> SpanRef<'_> { + if let Some(span) = self.span.as_ref() { + SpanRef(span) + } else { + SpanRef(&NOOP_SPAN) + } + } + + fn has_active_span(&self) -> bool { + self.span.is_some() + } + + fn with_remote_span_context(&self, span_context: crate::trace::SpanContext) -> Self { + self.with_synchronized_span(span_context.into()) + } +} + +/// Mark a given `Span` as active. +/// +/// The `Tracer` MUST provide a way to update its active `Span`, and MAY provide convenience +/// methods to manage a `Span`'s lifetime and the scope in which a `Span` is active. When an +/// active `Span` is made inactive, the previously-active `Span` SHOULD be made active. A `Span` +/// maybe finished (i.e. have a non-null end time) but still be active. A `Span` may be active +/// on one thread after it has been made inactive on another. +/// +/// # Examples +/// +/// ``` +/// use opentelemetry::{global, trace::{Span, Tracer}, KeyValue}; +/// use opentelemetry::trace::{get_active_span, mark_span_as_active}; +/// +/// fn my_function() { +/// let tracer = global::tracer("my-component-a"); +/// // start an active span in one function +/// let span = tracer.start("span-name"); +/// let _guard = mark_span_as_active(span); +/// // anything happening in functions we call can still access the active span... +/// my_other_function(); +/// } +/// +/// fn my_other_function() { +/// // call methods on the current span from +/// get_active_span(|span| { +/// span.add_event("An event!".to_string(), vec![KeyValue::new("happened", true)]); +/// }); +/// } +/// ``` +#[must_use = "Dropping the guard detaches the context."] +pub fn mark_span_as_active(span: T) -> ContextGuard { + let cx = Context::current_with_span(span); + cx.attach() +} + +/// Executes a closure with a reference to this thread's current span. +/// +/// # Examples +/// +/// ``` +/// use opentelemetry::{global, trace::{Span, Tracer}, KeyValue}; +/// use opentelemetry::trace::get_active_span; +/// +/// fn my_function() { +/// // start an active span in one function +/// global::tracer("my-component").in_span("span-name", |_cx| { +/// // anything happening in functions we call can still access the active span... +/// my_other_function(); +/// }) +/// } +/// +/// fn my_other_function() { +/// // call methods on the current span from +/// get_active_span(|span| { +/// span.add_event("An event!", vec![KeyValue::new("happened", true)]); +/// }) +/// } +/// ``` +pub fn get_active_span(f: F) -> T +where + F: FnOnce(SpanRef<'_>) -> T, +{ + Context::map_current(|cx| f(cx.span())) +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/trace/mod.rs b/vendor/opentelemetry-rust/opentelemetry/src/trace/mod.rs new file mode 100644 index 00000000..a38c0ed8 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/trace/mod.rs @@ -0,0 +1,274 @@ +//! API for tracing applications and libraries. +//! +//! The `trace` module includes types for tracking the progression of a single +//! request while it is handled by services that make up an application. A trace +//! is a tree of [`Span`]s which are objects that represent the work being done +//! by individual services or components involved in a request as it flows +//! through a system. This module implements the OpenTelemetry [trace +//! specification]. +//! +//! [trace specification]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md +//! +//! ## Getting Started +//! +//! In application code: +//! +//! ``` +//! use opentelemetry::trace::{Tracer, noop::NoopTracerProvider}; +//! use opentelemetry::global; +//! +//! fn init_tracer() { +//! // Swap this no-op provider for your tracing service of choice (jaeger, zipkin, etc) +//! let provider = NoopTracerProvider::new(); +//! +//! // Configure the global `TracerProvider` singleton when your app starts +//! // (there is a no-op default if this is not set by your application) +//! let _ = global::set_tracer_provider(provider); +//! } +//! +//! fn do_something_tracked() { +//! // Then you can get a named tracer instance anywhere in your codebase. +//! let tracer = global::tracer("my-component"); +//! +//! tracer.in_span("doing_work", |cx| { +//! // Traced app logic here... +//! }); +//! } +//! +//! // in main or other app start +//! init_tracer(); +//! do_something_tracked(); +//! ``` +//! +//! In library code: +//! +//! ``` +//! use opentelemetry::{global, trace::{Span, Tracer, TracerProvider}}; +//! use opentelemetry::InstrumentationScope; +//! use std::sync::Arc; +//! +//! fn my_library_function() { +//! // Use the global tracer provider to get access to the user-specified +//! // tracer configuration +//! let tracer_provider = global::tracer_provider(); +//! +//! // Get a tracer for this library +//! let scope = InstrumentationScope::builder("my_name") +//! .with_version(env!("CARGO_PKG_VERSION")) +//! .with_schema_url("https://opentelemetry.io/schemas/1.17.0") +//! .build(); +//! +//! let tracer = tracer_provider.tracer_with_scope(scope); +//! +//! // Create spans +//! let mut span = tracer.start("doing_work"); +//! +//! // Do work... +//! +//! // End the span +//! span.end(); +//! } +//! ``` +//! +//! ## Overview +//! +//! The tracing API consists of a three main traits: +//! +//! * [`TracerProvider`]s are the entry point of the API. They provide access to +//! `Tracer`s. +//! * [`Tracer`]s are types responsible for creating `Span`s. +//! * [`Span`]s provide the API to trace an operation. +//! +//! ## Working with Async Runtimes +//! +//! Exporting spans often involves sending data over a network or performing +//! other I/O tasks. OpenTelemetry allows you to schedule these tasks using +//! whichever runtime you are already using such as [Tokio]. +//! When using an async runtime it's best to use the batch span processor +//! where the spans will be sent in batches as opposed to being sent once ended, +//! which often ends up being more efficient. +//! +//! [Tokio]: https://tokio.rs +//! +//! ## Managing Active Spans +//! +//! Spans can be marked as "active" for a given [`Context`], and all newly +//! created spans will automatically be children of the currently active span. +//! +//! The active span for a given thread can be managed via [`get_active_span`] +//! and [`mark_span_as_active`]. +//! +//! [`Context`]: crate::Context +//! +//! ``` +//! use opentelemetry::{global, trace::{self, Span, Status, Tracer, TracerProvider}}; +//! +//! fn may_error(rand: f32) { +//! if rand < 0.5 { +//! // Get the currently active span to record additional attributes, +//! // status, etc. +//! trace::get_active_span(|span| { +//! span.set_status(Status::error("value too small")); +//! }); +//! } +//! } +//! +//! // Get a tracer +//! let tracer = global::tracer("my_tracer"); +//! +//! // Create a span +//! let span = tracer.start("parent_span"); +//! +//! // Mark the span as active +//! let active = trace::mark_span_as_active(span); +//! +//! // Any span created here will be a child of `parent_span`... +//! +//! // Drop the guard and the span will no longer be active +//! drop(active) +//! ``` +//! +//! Additionally [`Tracer::in_span`] can be used as shorthand to simplify +//! managing the parent context. +//! +//! ``` +//! use opentelemetry::{global, trace::Tracer}; +//! +//! // Get a tracer +//! let tracer = global::tracer("my_tracer"); +//! +//! // Use `in_span` to create a new span and mark it as the parent, dropping it +//! // at the end of the block. +//! tracer.in_span("parent_span", |cx| { +//! // spans created here will be children of `parent_span` +//! }); +//! ``` +//! +//! #### Async active spans +//! +//! Async spans can be propagated with [`TraceContextExt`] and [`FutureExt`]. +//! +//! ``` +//! use opentelemetry::{Context, global, trace::{FutureExt, TraceContextExt, Tracer}}; +//! +//! async fn some_work() { } +//! # async fn in_an_async_context() { +//! +//! // Get a tracer +//! let tracer = global::tracer("my_tracer"); +//! +//! // Start a span +//! let span = tracer.start("my_span"); +//! +//! // Perform some async work with this span as the currently active parent. +//! some_work().with_context(Context::current_with_span(span)).await; +//! # } +//! ``` + +use std::borrow::Cow; +use std::time; + +pub(crate) mod context; +pub mod noop; +mod span; +mod span_context; +mod tracer; +mod tracer_provider; + +pub use self::{ + context::{ + get_active_span, mark_span_as_active, FutureExt, SpanRef, TraceContextExt, WithContext, + }, + span::{Span, SpanKind, Status}, + span_context::{SpanContext, TraceState}, + tracer::{SpanBuilder, Tracer}, + tracer_provider::TracerProvider, +}; +use crate::KeyValue; +pub use crate::{SpanId, TraceFlags, TraceId}; + +/// Events record things that happened during a [`Span`]'s lifetime. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq)] +pub struct Event { + /// The name of this event. + pub name: Cow<'static, str>, + + /// The time at which this event occurred. + pub timestamp: time::SystemTime, + + /// Attributes that describe this event. + pub attributes: Vec, + + /// The number of attributes that were above the configured limit, and thus + /// dropped. + pub dropped_attributes_count: u32, +} + +impl Event { + /// Create new `Event` + pub fn new>>( + name: T, + timestamp: time::SystemTime, + attributes: Vec, + dropped_attributes_count: u32, + ) -> Self { + Event { + name: name.into(), + timestamp, + attributes, + dropped_attributes_count, + } + } + + /// Create new `Event` with a given name. + pub fn with_name>>(name: T) -> Self { + Event { + name: name.into(), + timestamp: crate::time::now(), + attributes: Vec::new(), + dropped_attributes_count: 0, + } + } +} + +/// Link is the relationship between two Spans. +/// +/// The relationship can be within the same trace or across different traces. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq)] +pub struct Link { + /// The span context of the linked span. + pub span_context: SpanContext, + + /// Attributes that describe this link. + pub attributes: Vec, + + /// The number of attributes that were above the configured limit, and thus + /// dropped. + pub dropped_attributes_count: u32, +} + +impl Link { + /// Create new `Link` + pub fn new( + span_context: SpanContext, + attributes: Vec, + dropped_attributes_count: u32, + ) -> Self { + Link { + span_context, + attributes, + dropped_attributes_count, + } + } + + /// Create new `Link` with given context + pub fn with_context(span_context: SpanContext) -> Self { + Link { + span_context, + attributes: Vec::new(), + dropped_attributes_count: 0, + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/trace/noop.rs b/vendor/opentelemetry-rust/opentelemetry/src/trace/noop.rs new file mode 100644 index 00000000..87196ae7 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/trace/noop.rs @@ -0,0 +1,214 @@ +//! No-op trace implementation +//! +//! This implementation is returned as the global tracer if no `Tracer` +//! has been set. It is also useful for testing purposes as it is intended +//! to have minimal resource utilization and runtime impact. +use crate::{ + propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator}, + trace::{self, TraceContextExt as _}, + Context, InstrumentationScope, KeyValue, +}; +use std::{borrow::Cow, time::SystemTime}; + +/// A no-op instance of a `TracerProvider`. +#[derive(Clone, Debug, Default)] +pub struct NoopTracerProvider { + _private: (), +} + +impl NoopTracerProvider { + /// Create a new no-op tracer provider + pub fn new() -> Self { + NoopTracerProvider { _private: () } + } +} + +impl trace::TracerProvider for NoopTracerProvider { + type Tracer = NoopTracer; + + /// Returns a new `NoopTracer` instance. + fn tracer_with_scope(&self, _scope: InstrumentationScope) -> Self::Tracer { + NoopTracer::new() + } +} + +/// A no-op instance of a `Span`. +#[derive(Clone, Debug)] +pub struct NoopSpan { + span_context: trace::SpanContext, +} + +impl NoopSpan { + /// The default `NoopSpan`, as a constant + pub const DEFAULT: NoopSpan = NoopSpan { + span_context: trace::SpanContext::NONE, + }; +} + +impl trace::Span for NoopSpan { + /// Ignores all events + fn add_event(&mut self, _name: T, _attributes: Vec) + where + T: Into>, + { + // Ignored + } + + /// Ignores all events with timestamps + fn add_event_with_timestamp( + &mut self, + _name: T, + _timestamp: SystemTime, + _attributes: Vec, + ) where + T: Into>, + { + // Ignored + } + + /// Returns an invalid `SpanContext`. + fn span_context(&self) -> &trace::SpanContext { + &self.span_context + } + + /// Returns false, signifying that this span is never recording. + fn is_recording(&self) -> bool { + false + } + + /// Ignores all attributes + fn set_attribute(&mut self, _attribute: KeyValue) { + // Ignored + } + + /// Ignores status + fn set_status(&mut self, _status: trace::Status) { + // Ignored + } + + /// Ignores name updates + fn update_name(&mut self, _new_name: T) + where + T: Into>, + { + // Ignored + } + + fn add_link(&mut self, _span_context: trace::SpanContext, _attributes: Vec) { + // Ignored + } + + /// Ignores `Span` endings + fn end_with_timestamp(&mut self, _timestamp: SystemTime) { + // Ignored + } +} + +/// A no-op instance of a `Tracer`. +#[derive(Clone, Debug, Default)] +pub struct NoopTracer { + _private: (), +} + +impl NoopTracer { + /// Create a new no-op tracer + pub fn new() -> Self { + NoopTracer { _private: () } + } +} + +impl trace::Tracer for NoopTracer { + type Span = NoopSpan; + + /// Builds a `NoopSpan` from a `SpanBuilder`. + /// + /// If the span builder or the context's current span contains a valid span context, it is + /// propagated. + fn build_with_context(&self, _builder: trace::SpanBuilder, parent_cx: &Context) -> Self::Span { + NoopSpan { + span_context: parent_cx.span().span_context().clone(), + } + } +} + +/// A no-op instance of an [`TextMapPropagator`]. +/// +/// [`TextMapPropagator`]: crate::propagation::TextMapPropagator +#[derive(Debug, Default)] +pub struct NoopTextMapPropagator { + _private: (), +} + +impl NoopTextMapPropagator { + /// Create a new noop text map propagator + pub fn new() -> Self { + NoopTextMapPropagator { _private: () } + } +} + +impl TextMapPropagator for NoopTextMapPropagator { + fn inject_context(&self, _cx: &Context, _injector: &mut dyn Injector) { + // ignored + } + + fn extract_with_context(&self, _cx: &Context, _extractor: &dyn Extractor) -> Context { + Context::current() + } + + fn fields(&self) -> FieldIter<'_> { + FieldIter::new(&[]) + } +} + +#[cfg(all(test, feature = "testing", feature = "trace"))] +mod tests { + use super::*; + use crate::testing::trace::TestSpan; + use crate::trace::{Span, TraceState, Tracer}; + + fn valid_span_context() -> trace::SpanContext { + trace::SpanContext::new( + trace::TraceId::from(42), + trace::SpanId::from(42), + trace::TraceFlags::default(), + true, + TraceState::default(), + ) + } + + #[test] + fn noop_tracer_defaults_to_invalid_span() { + let tracer = NoopTracer::new(); + let span = tracer.start_with_context("foo", &Context::new()); + assert!(!span.span_context().is_valid()); + } + + #[test] + fn noop_tracer_propagates_valid_span_context_from_builder() { + let tracer = NoopTracer::new(); + let builder = tracer.span_builder("foo"); + let span = tracer.build_with_context( + builder, + &Context::new().with_span(TestSpan(valid_span_context())), + ); + assert!(span.span_context().is_valid()); + } + + #[test] + fn noop_tracer_propagates_valid_span_context_from_explicitly_specified_context() { + let tracer = NoopTracer::new(); + let cx = Context::new().with_span(NoopSpan { + span_context: valid_span_context(), + }); + let span = tracer.start_with_context("foo", &cx); + assert!(span.span_context().is_valid()); + } + + #[test] + fn noop_tracer_propagates_valid_span_context_from_remote_span_context() { + let tracer = NoopTracer::new(); + let cx = Context::new().with_remote_span_context(valid_span_context()); + let span = tracer.start_with_context("foo", &cx); + assert!(span.span_context().is_valid()); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/trace/span.rs b/vendor/opentelemetry-rust/opentelemetry/src/trace/span.rs new file mode 100644 index 00000000..8b54fffb --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/trace/span.rs @@ -0,0 +1,329 @@ +use crate::{trace::SpanContext, KeyValue}; +use std::borrow::Cow; +use std::error::Error; +use std::time::SystemTime; + +/// The interface for a single operation within a trace. +/// +/// Spans can be nested to form a trace tree. Each trace contains a root span, +/// which typically describes the entire operation and, optionally, one or more +/// sub-spans for its sub-operations. +/// +/// The span `name` concisely identifies the work represented by the span, for +/// example, an RPC method name, a function name, or the name of a subtask or +/// stage within a larger computation. The span name should be the most general +/// string that identifies a (statistically) interesting class of spans, rather +/// than individual span instances while still being human-readable. That is, +/// `"get_user"` is a reasonable name, while `"get_user/314159"`, where `"314159"` is +/// a user ID, is not a good name due to its high cardinality. _Generality_ +/// should be prioritized over _human-readability_. +/// +/// For example, here are potential span names for an endpoint that gets a +/// hypothetical account information: +/// +/// | Span Name | Guidance | +/// | ----------------- | ------------ | +/// | `get` | Too general | +/// | `get_account/42` | Too specific | +/// | `get_account` | Good, and account_id=42 would make a nice Span attribute | +/// | `get_account/{accountId}` | Also good (using the "HTTP route") | +/// +/// The span's start and end timestamps reflect the elapsed real time of the +/// operation. +/// +/// For example, if a span represents a request-response cycle (e.g. HTTP or an +/// RPC), the span should have a start time that corresponds to the start time +/// of the first sub-operation, and an end time of when the final sub-operation +/// is complete. This includes: +/// +/// * receiving the data from the request +/// * parsing of the data (e.g. from a binary or json format) +/// * any middleware or additional processing logic +/// * business logic +/// * construction of the response +/// * sending of the response +/// +/// Child spans (or in some cases events) may be created to represent +/// sub-operations which require more detailed observability. Child spans should +/// measure the timing of the respective sub-operation, and may add additional +/// attributes. +pub trait Span { + /// Record an event in the context this span. + /// + /// Note that the OpenTelemetry project documents certain "[standard + /// attributes]" that have prescribed semantic meanings and are available via + /// the [opentelemetry_semantic_conventions] crate. + /// + /// [standard attributes]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/trace/semantic_conventions/README.md + /// [opentelemetry_semantic_conventions]: https://docs.rs/opentelemetry-semantic-conventions + fn add_event(&mut self, name: T, attributes: Vec) + where + T: Into>, + { + self.add_event_with_timestamp(name, crate::time::now(), attributes) + } + + /// Record an error as an event for this span. + /// + /// An additional call to [Span::set_status] is required if the status of the + /// span should be set to error, as this method does not change the span status. + /// + /// If this span is not being recorded then this method does nothing. + fn record_error(&mut self, err: &dyn Error) { + if self.is_recording() { + let attributes = vec![KeyValue::new("exception.message", err.to_string())]; + self.add_event("exception", attributes); + } + } + + /// Record an event with a timestamp in the context this span. + /// + /// Note that the OpenTelemetry project documents certain "[standard + /// attributes]" that have prescribed semantic meanings and are available via + /// the [opentelemetry_semantic_conventions] crate. + /// + /// [standard attributes]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/trace/semantic_conventions/README.md + /// [opentelemetry_semantic_conventions]: https://docs.rs/opentelemetry-semantic-conventions + fn add_event_with_timestamp( + &mut self, + name: T, + timestamp: SystemTime, + attributes: Vec, + ) where + T: Into>; + + /// A reference to the [`SpanContext`] for this span. + fn span_context(&self) -> &SpanContext; + + /// Returns `true` if this span is recording information. + /// + /// Spans will not be recording information after they have ended. + /// + /// This flag may be `true` despite the entire trace being sampled out. This + /// allows recording and processing of information about the individual + /// spans without sending it to the backend. An example of this scenario may + /// be recording and processing of all incoming requests for the processing + /// and building of SLA/SLO latency charts while sending only a subset - + /// sampled spans - to the backend. + fn is_recording(&self) -> bool; + + /// Set an attribute of this span. + /// + /// Setting an attribute with the same key as an existing attribute + /// results in both being stored as attribute, without any de-duplication + /// performed. + /// + /// Note that the OpenTelemetry project documents certain "[standard + /// attributes]" that have prescribed semantic meanings and are available via + /// the [opentelemetry_semantic_conventions] crate. + /// + /// [standard attributes]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/trace/semantic_conventions/README.md + /// [opentelemetry_semantic_conventions]: https://docs.rs/opentelemetry-semantic-conventions + fn set_attribute(&mut self, attribute: KeyValue); + + /// Set multiple attributes of this span. + /// + /// Setting an attribute with the same key as an existing attribute + /// results in both being stored as attribute, without any de-duplication + /// performed. + /// + /// Note that the OpenTelemetry project documents certain "[standard + /// attributes]" that have prescribed semantic meanings and are available via + /// the [opentelemetry_semantic_conventions] crate. + /// + /// [standard attributes]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/trace/semantic_conventions/README.md + /// [opentelemetry_semantic_conventions]: https://docs.rs/opentelemetry-semantic-conventions + fn set_attributes(&mut self, attributes: impl IntoIterator) { + if self.is_recording() { + for attr in attributes.into_iter() { + self.set_attribute(attr); + } + } + } + + /// Sets the status of this `Span`. + /// + /// If used, this will override the default span status, which is [`Status::Unset`]. + fn set_status(&mut self, status: Status); + + /// Updates the span's name. + /// + /// After this update, any sampling behavior based on the name will depend on + /// the implementation. + fn update_name(&mut self, new_name: T) + where + T: Into>; + + /// Adds [`Link`] to another [`SpanContext`]. + /// + /// This method allows linking the current span to another span, identified by its `SpanContext`. Links can be used + /// to connect spans from different traces or within the same trace. Attributes can be attached to the link to + /// provide additional context or metadata. + /// + /// # Arguments + /// + /// * `span_context` - The `SpanContext` of the span to link to. This represents the target span's unique identifiers + /// and trace information. + /// * `attributes` - A vector of `KeyValue` pairs that describe additional attributes of the link. These attributes + /// can include any contextual information relevant to the link between the spans. + /// + /// [`Link`]: crate::trace::Link + fn add_link(&mut self, span_context: SpanContext, attributes: Vec); + + /// Signals that the operation described by this span has now ended. + fn end(&mut self) { + self.end_with_timestamp(crate::time::now()); + } + + /// Signals that the operation described by this span ended at the given time. + fn end_with_timestamp(&mut self, timestamp: SystemTime); +} + +/// `SpanKind` describes the relationship between the [`Span`], its parents, and +/// its children in a trace. +/// +/// `SpanKind` describes two independent properties that benefit tracing systems +/// during analysis: +/// +/// The first property described by `SpanKind` reflects whether the span is a +/// "logical" remote child or parent. By "logical", we mean that the span is +/// logically a remote child or parent, from the point of view of the library +/// that is being instrumented. Spans with a remote parent are interesting +/// because they are sources of external load. Spans with a remote child are +/// interesting because they reflect a non-local system dependency. +/// +/// The second property described by `SpanKind` reflects whether a child span +/// represents a synchronous call. When a child span is synchronous, the parent +/// is expected to wait for it to complete under ordinary circumstances. It can +/// be useful for tracing systems to know this property, since synchronous spans +/// may contribute to the overall trace latency. Asynchronous scenarios can be +/// remote or local. +/// +/// In order for `SpanKind` to be meaningful, callers should arrange that a +/// single span does not serve more than one purpose. For example, a server-side +/// span should not be used directly as the parent of another remote span. As a +/// simple guideline, instrumentation should create a new span prior to +/// extracting and serializing the SpanContext for a remote call. +/// +/// Note: there are complex scenarios where a `SpanKind::Client` span may have a +/// child that is also logically a `SpanKind::Client` span, or a +/// `SpanKind::Producer` span might have a local child that is a +/// `SpanKind::Client` span, depending on how the various libraries that are +/// providing the functionality are built and instrumented. These scenarios, +/// when they occur, should be detailed in the semantic conventions appropriate +/// to the relevant libraries. +/// +/// To summarize the interpretation of these kinds: +/// +/// | `SpanKind` | Synchronous | Asynchronous | Remote Incoming | Remote Outgoing | +/// |---|---|---|---|---| +/// | `Client` | yes | | | yes | +/// | `Server` | yes | | yes | | +/// | `Producer` | | yes | | maybe | +/// | `Consumer` | | yes | maybe | | +/// | `Internal` | | | | | +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SpanKind { + /// Indicates that the span describes a request to some remote service. This + /// span is usually the parent of a remote `SpanKind::Server` span and does + /// not end until the response is received. + Client, + + /// Indicates that the span covers server-side handling of a synchronous RPC + /// or other remote request. This span is often the child of a remote + /// `SpanKind::Client` span that was expected to wait for a response. + Server, + + /// Indicates that the span describes the initiators of an asynchronous + /// request. This parent span will often end before the corresponding child + /// `SpanKind::Consumer` span, possibly even before the child span starts. + /// + /// In messaging scenarios with batching, tracing individual messages + /// requires a new `SpanKind::Producer` span per message to be created. + Producer, + + /// Indicates that the span describes a child of an asynchronous + /// `SpanKind::Producer` request. + Consumer, + + /// Default value. + /// + /// Indicates that the span represents an internal operation within an + /// application, as opposed to an operations with remote parents or + /// children. + Internal, +} + +/// The status of a [`Span`]. +/// +/// These values form a total order: Ok > Error > Unset. This means that setting +/// `Status::Ok` will override any prior or future attempts to set a status with +/// `Status::Error` or `Status::Unset`. +/// +/// The status should remain unset, except for the following circumstances: +/// +/// Generally, instrumentation libraries should not set the code to +/// `Status::Ok`, unless explicitly configured to do so. Instrumentation +/// libraries should leave the status code as unset unless there is an error. +/// +/// Application developers and operators may set the status code to +/// `Status::Ok`. +/// +/// When span status is set to `Status::Ok` it should be considered final and +/// any further attempts to change it should be ignored. +/// +/// Analysis tools should respond to a `Status::Ok` status by suppressing any +/// errors they would otherwise generate. For example, to suppress noisy errors +/// such as 404s. +/// +/// Only the value of the last call will be recorded, and implementations are +/// free to ignore previous calls. +#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd)] +pub enum Status { + /// The default status. + #[default] + Unset, + + /// The operation contains an error. + Error { + /// The description of the error + description: Cow<'static, str>, + }, + + /// The operation has been validated by an application developer or operator to + /// have completed successfully. + Ok, +} + +impl Status { + /// Create a new error status with a given description. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::trace::Status; + /// + /// // record error with `str` description + /// let error_status = Status::error("something went wrong"); + /// + /// // or with `String` description + /// let error_status = Status::error(format!("too many foos: {}", 42)); + /// # drop(error_status); + /// ``` + pub fn error(description: impl Into>) -> Self { + Status::Error { + description: description.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn status_order() { + assert!(Status::Ok > Status::error("")); + assert!(Status::error("") > Status::Unset); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/trace/span_context.rs b/vendor/opentelemetry-rust/opentelemetry/src/trace/span_context.rs new file mode 100644 index 00000000..91c07e2e --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/trace/span_context.rs @@ -0,0 +1,586 @@ +use crate::{SpanId, TraceFlags, TraceId}; +use std::collections::VecDeque; +use std::hash::Hash; +use std::str::FromStr; +use thiserror::Error; + +/// TraceState carries system-specific configuration data, represented as a list +/// of key-value pairs. TraceState allows multiple tracing systems to +/// participate in the same trace. +/// +/// Please review the [W3C specification] for details on this field. +/// +/// [W3C specification]: https://www.w3.org/TR/trace-context-2/#tracestate-header +#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)] +pub struct TraceState(Option>); + +const MAX_LIST_MEMBERS: usize = 32; + +impl TraceState { + /// The default `TraceState`, as a constant + pub const NONE: TraceState = TraceState(None); + + /// Validates that the given `TraceState` list-member key is valid per the [W3 Spec]. + /// + /// [W3 Spec]: https://www.w3.org/TR/trace-context-2/#key + fn valid_key(key: &str) -> bool { + if key.len() > 256 { + return false; + } + + let allowed_special = |b: u8| b == b'_' || b == b'-' || b == b'*' || b == b'/'; + let mut vendor_start = None; + for (i, &b) in key.as_bytes().iter().enumerate() { + if !(b.is_ascii_lowercase() || b.is_ascii_digit() || allowed_special(b) || b == b'@') { + return false; + } + + if i == 0 && (!b.is_ascii_lowercase() && !b.is_ascii_digit()) { + return false; + } else if b == b'@' { + if vendor_start.is_some() || i + 14 < key.len() { + return false; + } + vendor_start = Some(i); + } else if let Some(start) = vendor_start { + if i == start + 1 && !(b.is_ascii_lowercase() || b.is_ascii_digit()) { + return false; + } + } + } + + true + } + + /// Validates that the given `TraceState` list-member value is valid per the [W3 Spec]. + /// + /// [W3 Spec]: https://www.w3.org/TR/trace-context-2/#value + fn valid_value(value: &str) -> bool { + if value.len() > 256 { + return false; + } + + !(value.contains(',') || value.contains('=')) + } + + /// Creates a new `TraceState` from the given key-value collection, keeping at most the 32 + /// list-members the [W3C specification] allows. Pairs beyond the 32nd are dropped without + /// being validated, so a key or value that would otherwise make this return an `Err` has no + /// effect if it only appears past the limit. + /// + /// [W3C specification]: https://www.w3.org/TR/trace-context-2/#tracestate-header-field-values + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::trace::TraceState; + /// + /// let kvs = vec![("foo", "bar"), ("apple", "banana")]; + /// let trace_state = TraceState::from_key_value(kvs); + /// + /// assert!(trace_state.is_ok()); + /// assert_eq!(trace_state.unwrap().header(), String::from("foo=bar,apple=banana")) + /// ``` + pub fn from_key_value(trace_state: T) -> TraceStateResult + where + T: IntoIterator, + K: ToString, + V: ToString, + { + let ordered_data = trace_state + .into_iter() + .take(MAX_LIST_MEMBERS) + .map(|(key, value)| { + let (key, value) = (key.to_string(), value.to_string()); + if !TraceState::valid_key(key.as_str()) { + return Err(TraceStateError::Key(key)); + } + if !TraceState::valid_value(value.as_str()) { + return Err(TraceStateError::Value(value)); + } + + Ok((key, value)) + }) + .collect::, TraceStateError>>()?; + + if ordered_data.is_empty() { + Ok(TraceState(None)) + } else { + Ok(TraceState(Some(ordered_data))) + } + } + + /// Retrieves a value for a given key from the `TraceState` if it exists. + pub fn get(&self, key: &str) -> Option<&str> { + self.0.as_ref().and_then(|kvs| { + kvs.iter().find_map(|item| { + if item.0.as_str() == key { + Some(item.1.as_str()) + } else { + None + } + }) + }) + } + + /// Inserts the given key-value pair into the `TraceState`. If a value already exists for the + /// given key, this updates the value and updates the value's position. If the key or value are + /// invalid per the [W3 Spec] an `Err` is returned, else a new `TraceState` with the + /// updated key/value is returned. + /// + /// A `TraceState` holds at most the 32 list-members the [W3 Spec] allows. Inserting into a full + /// `TraceState` keeps the inserted pair and drops the last list-member. + /// + /// [W3 Spec]: https://www.w3.org/TR/trace-context-2/#mutating-the-tracestate-field + pub fn insert(&self, key: K, value: V) -> TraceStateResult + where + K: Into, + V: Into, + { + let (key, value) = (key.into(), value.into()); + if !TraceState::valid_key(key.as_str()) { + return Err(TraceStateError::Key(key)); + } + if !TraceState::valid_value(value.as_str()) { + return Err(TraceStateError::Value(value)); + } + + let mut trace_state = self.delete_from_deque(&key); + let kvs = trace_state.0.get_or_insert(VecDeque::with_capacity(1)); + if kvs.len() >= MAX_LIST_MEMBERS { + kvs.truncate(MAX_LIST_MEMBERS - 1); + } + + kvs.push_front((key, value)); + + Ok(trace_state) + } + + /// Removes the given key-value pair from the `TraceState`. If the key is invalid per the + /// [W3 Spec] an `Err` is returned. Else, a new `TraceState` + /// with the removed entry is returned. + /// + /// If the key is not in `TraceState`. The original `TraceState` will be cloned and returned. + /// + /// [W3 Spec]: https://www.w3.org/TR/trace-context-2/#mutating-the-tracestate-field + pub fn delete>(&self, key: K) -> TraceStateResult { + let key = key.into(); + if !TraceState::valid_key(key.as_str()) { + return Err(TraceStateError::Key(key)); + } + + Ok(self.delete_from_deque(&key)) + } + + /// Delete key from trace state's deque. The key MUST be valid + fn delete_from_deque(&self, key: &str) -> TraceState { + let mut owned = self.clone(); + if let Some(kvs) = owned.0.as_mut() { + if let Some(index) = kvs.iter().position(|x| x.0 == key) { + kvs.remove(index); + } + } + owned + } + + /// Creates a new `TraceState` header string, delimiting each key and value with a `=` and each + /// entry with a `,`. + pub fn header(&self) -> String { + self.header_delimited("=", ",") + } + + /// Creates a new `TraceState` header string, with the given key/value delimiter and entry delimiter. + pub fn header_delimited(&self, entry_delimiter: &str, list_delimiter: &str) -> String { + self.0 + .as_ref() + .map(|kvs| { + kvs.iter() + .map(|(key, value)| format!("{key}{entry_delimiter}{value}")) + .collect::>() + .join(list_delimiter) + }) + .unwrap_or_default() + } +} + +impl FromStr for TraceState { + type Err = TraceStateError; + + fn from_str(s: &str) -> Result { + let mut key_value_pairs: Vec<(String, String)> = Vec::new(); + + for list_member in s.split_terminator(',').take(MAX_LIST_MEMBERS) { + match list_member.find('=') { + None => return Err(TraceStateError::List(list_member.to_string())), + Some(separator_index) => { + let (key, value) = list_member.split_at(separator_index); + key_value_pairs + .push((key.to_string(), value.trim_start_matches('=').to_string())); + } + } + } + + TraceState::from_key_value(key_value_pairs) + } +} + +/// Iterator over TraceState key-value pairs as (&str, &str) +#[derive(Debug)] +pub struct TraceStateIter<'a> { + inner: Option>, +} + +impl<'a> Iterator for TraceStateIter<'a> { + type Item = (&'a str, &'a str); + + fn next(&mut self) -> Option { + self.inner + .as_mut()? + .next() + .map(|(key, value)| (key.as_str(), value.as_str())) + } + + fn size_hint(&self) -> (usize, Option) { + match &self.inner { + Some(iter) => iter.size_hint(), + None => (0, Some(0)), + } + } +} + +impl ExactSizeIterator for TraceStateIter<'_> { + fn len(&self) -> usize { + match &self.inner { + Some(iter) => iter.len(), + None => 0, + } + } +} + +impl<'a> IntoIterator for &'a TraceState { + type Item = (&'a str, &'a str); + type IntoIter = TraceStateIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + TraceStateIter { + inner: self.0.as_ref().map(|deque| deque.iter()), + } + } +} + +/// A specialized `Result` type for trace state operations. +type TraceStateResult = Result; + +/// Error returned by `TraceState` operations. +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum TraceStateError { + /// The key is invalid. + /// + /// See for requirement for keys. + #[error("{0} is not a valid key in TraceState, see https://www.w3.org/TR/trace-context-2/#key for more details")] + Key(String), + + /// The value is invalid. + /// + /// See for requirement for values. + #[error("{0} is not a valid value in TraceState, see https://www.w3.org/TR/trace-context-2/#value for more details")] + Value(String), + + /// The list is invalid. + /// + /// See for requirement for list members. + #[error("{0} is not a valid list member in TraceState, see https://www.w3.org/TR/trace-context-2/#list for more details")] + List(String), +} + +/// Immutable portion of a [`Span`] which can be serialized and propagated. +/// +/// This representation conforms to the [W3C TraceContext specification]. +/// +/// Spans that do not have the `sampled` flag set in their [`TraceFlags`] will +/// be ignored by most tracing tools. +/// +/// [`Span`]: crate::trace::Span +/// [W3C TraceContext specification]: https://www.w3.org/TR/trace-context +#[derive(Clone, Debug, PartialEq, Hash, Eq)] +pub struct SpanContext { + trace_id: TraceId, + span_id: SpanId, + trace_flags: TraceFlags, + is_remote: bool, + trace_state: TraceState, +} + +impl SpanContext { + /// An invalid span context + pub const NONE: SpanContext = SpanContext { + trace_id: TraceId::INVALID, + span_id: SpanId::INVALID, + trace_flags: TraceFlags::NOT_SAMPLED, + is_remote: false, + trace_state: TraceState::NONE, + }; + + /// Create an invalid empty span context + pub fn empty_context() -> Self { + SpanContext::NONE + } + + /// Construct a new `SpanContext` + pub fn new( + trace_id: TraceId, + span_id: SpanId, + trace_flags: TraceFlags, + is_remote: bool, + trace_state: TraceState, + ) -> Self { + SpanContext { + trace_id, + span_id, + trace_flags, + is_remote, + trace_state, + } + } + + /// The [`TraceId`] for this span context. + pub fn trace_id(&self) -> TraceId { + self.trace_id + } + + /// The [`SpanId`] for this span context. + pub fn span_id(&self) -> SpanId { + self.span_id + } + + /// Returns details about the trace. + /// + /// Unlike `TraceState` values, these are present in all traces. The current + /// version of the specification only supports a single flag [`TraceFlags::SAMPLED`]. + pub fn trace_flags(&self) -> TraceFlags { + self.trace_flags + } + + /// Returns `true` if the span context has a valid (non-zero) `trace_id` and a + /// valid (non-zero) `span_id`. + pub fn is_valid(&self) -> bool { + self.trace_id != TraceId::INVALID && self.span_id != SpanId::INVALID + } + + /// Returns `true` if the span context was propagated from a remote parent. + pub fn is_remote(&self) -> bool { + self.is_remote + } + + /// Returns `true` if the `sampled` trace flag is set. + /// + /// Spans that are not sampled will be ignored by most tracing tools. + pub fn is_sampled(&self) -> bool { + self.trace_flags.is_sampled() + } + + /// A reference to the span context's [`TraceState`]. + pub fn trace_state(&self) -> &TraceState { + &self.trace_state + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{trace::TraceContextExt, Context}; + + #[rustfmt::skip] + fn trace_state_test_data() -> Vec<(TraceState, &'static str, &'static str)> { + vec![ + (TraceState::from_key_value(vec![("foo", "bar")]).unwrap(), "foo=bar", "foo"), + (TraceState::from_key_value(vec![("foo", ""), ("apple", "banana")]).unwrap(), "foo=,apple=banana", "apple"), + (TraceState::from_key_value(vec![("foo", "bar"), ("apple", "banana")]).unwrap(), "foo=bar,apple=banana", "apple"), + ] + } + + #[test] + fn test_trace_state() { + for test_case in trace_state_test_data() { + assert_eq!(test_case.0.clone().header(), test_case.1); + + let new_key = format!("{}-{}", test_case.0.get(test_case.2).unwrap(), "test"); + + let updated_trace_state = test_case.0.insert(test_case.2, new_key.clone()); + assert!(updated_trace_state.is_ok()); + let updated_trace_state = updated_trace_state.unwrap(); + + let updated = format!("{}={}", test_case.2, new_key); + + let index = updated_trace_state.clone().header().find(&updated); + + assert!(index.is_some()); + assert_eq!(index.unwrap(), 0); + + let deleted_trace_state = updated_trace_state.delete(test_case.2.to_string()); + assert!(deleted_trace_state.is_ok()); + + let deleted_trace_state = deleted_trace_state.unwrap(); + + assert!(deleted_trace_state.get(test_case.2).is_none()); + } + } + + #[test] + fn test_trace_state_key() { + let test_data: Vec<(&'static str, bool)> = vec![ + ("123", true), + ("bar", true), + ("foo@bar", true), + ("foo@0123456789abcdef", false), + ("foo@012345678", true), + ("FOO@BAR", false), + ("你好", false), + ]; + + for (key, expected) in test_data { + assert_eq!(TraceState::valid_key(key), expected, "test key: {key:?}"); + } + } + + #[test] + fn test_trace_state_insert() { + let trace_state = TraceState::from_key_value(vec![("foo", "bar")]).unwrap(); + let inserted_trace_state = trace_state.insert("testkey", "testvalue").unwrap(); + assert!(trace_state.get("testkey").is_none()); // The original state doesn't change + assert_eq!(inserted_trace_state.get("testkey").unwrap(), "testvalue"); // + } + + #[test] + fn test_context_span_debug() { + let cx = Context::current(); + assert_eq!( + format!("{cx:?}"), + "Context { span: \"None\", entries count: 0, suppress_telemetry: false }" + ); + let cx = Context::current().with_remote_span_context(SpanContext::NONE); + assert_eq!( + format!("{cx:?}"), + "Context { \ + span: SpanContext { \ + trace_id: 00000000000000000000000000000000, \ + span_id: 0000000000000000, \ + trace_flags: TraceFlags(0), \ + is_remote: false, \ + trace_state: TraceState(None) \ + }, \ + entries count: 1, suppress_telemetry: false \ + }" + ); + } + + #[test] + fn test_tracestate_iter_empty() { + let ts = TraceState::NONE; + let mut iter = ts.into_iter(); + assert_eq!(iter.next(), None); + assert_eq!(iter.size_hint(), (0, Some(0))); + assert_eq!(iter.len(), 0); + } + + #[test] + fn test_tracestate_iter_single() { + let ts = TraceState::from_key_value(vec![("foo", "bar")]).unwrap(); + let mut iter = ts.into_iter(); + assert_eq!(iter.next(), Some(("foo", "bar"))); + assert_eq!(iter.next(), None); + assert_eq!(iter.size_hint(), (0, Some(0))); + } + + #[test] + fn test_tracestate_iter_multiple() { + let ts = TraceState::from_key_value(vec![("foo", "bar"), ("apple", "banana")]).unwrap(); + let mut iter = ts.into_iter(); + assert_eq!(iter.next(), Some(("foo", "bar"))); + assert_eq!(iter.next(), Some(("apple", "banana"))); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_tracestate_iter_size_hint_and_len() { + let ts = TraceState::from_key_value(vec![("foo", "bar"), ("apple", "banana")]).unwrap(); + let iter = ts.into_iter(); + assert_eq!(iter.size_hint(), (2, Some(2))); + assert_eq!(iter.len(), 2); + } + + #[test] + fn test_tracestate_from_str_keeps_at_most_32_list_members() { + let header = (0..64) + .map(|i| format!("key{i}=value{i}")) + .collect::>() + .join(","); + let trace_state = TraceState::from_str(&header).unwrap(); + + assert_eq!(trace_state.into_iter().count(), 32); + assert_eq!(trace_state.get("key0"), Some("value0")); + assert_eq!(trace_state.get("key31"), Some("value31")); + assert_eq!(trace_state.get("key32"), None); + + let expected_header = (0..32) + .map(|i| format!("key{i}=value{i}")) + .collect::>() + .join(","); + assert_eq!(trace_state.header(), expected_header); + } + #[test] + fn test_tracestate_from_str_ignores_invalid_members_beyond_the_limit() { + let mut members: Vec = (0..32).map(|i| format!("key{i}=value{i}")).collect(); + members.push("invalid-no-separator".to_string()); + let trace_state = TraceState::from_str(&members.join(",")).unwrap(); + + assert_eq!(trace_state.into_iter().count(), 32); + } + #[test] + fn test_tracestate_from_str_rejects_invalid_members_within_the_limit() { + let mut members: Vec = (0..31).map(|i| format!("key{i}=value{i}")).collect(); + members.push("invalid-no-separator".to_string()); + + assert!(TraceState::from_str(&members.join(",")).is_err()); + } + #[test] + fn test_tracestate_from_key_value_keeps_at_most_32_list_members() { + let kvs: Vec<(String, String)> = (0..64) + .map(|i| (format!("key{i}"), format!("value{i}"))) + .collect(); + let trace_state = TraceState::from_key_value(kvs).unwrap(); + + assert_eq!(trace_state.into_iter().count(), 32); + assert_eq!(trace_state.get("key0"), Some("value0")); + assert_eq!(trace_state.get("key32"), None); + } + #[test] + fn test_tracestate_insert_keeps_at_most_32_list_members() { + let mut trace_state = TraceState::default(); + for i in 0..64 { + trace_state = trace_state + .insert(format!("key{i}"), format!("value{i}")) + .unwrap(); + } + + assert_eq!(trace_state.into_iter().count(), 32); + assert_eq!(trace_state.get("key63"), Some("value63")); + assert_eq!(trace_state.get("key32"), Some("value32")); + assert_eq!(trace_state.get("key31"), None); + } + #[test] + fn test_tracestate_insert_of_an_existing_key_evicts_nothing() { + let mut trace_state = TraceState::default(); + for i in 0..32 { + trace_state = trace_state + .insert(format!("key{i}"), format!("value{i}")) + .unwrap(); + } + let updated = trace_state.insert("key20", "updated").unwrap(); + + assert_eq!(updated.into_iter().count(), 32); + assert_eq!(updated.get("key20"), Some("updated")); + assert_eq!(updated.get("key0"), Some("value0")); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/trace/tracer.rs b/vendor/opentelemetry-rust/opentelemetry/src/trace/tracer.rs new file mode 100644 index 00000000..be5afd47 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/trace/tracer.rs @@ -0,0 +1,445 @@ +use crate::{ + trace::{Event, Link, Span, SpanKind, TraceContextExt}, + Context, KeyValue, +}; +use std::borrow::Cow; +use std::time::SystemTime; + +/// The interface for constructing [`Span`]s. +/// +/// ## In Synchronous Code +/// +/// Spans can be created and nested manually: +/// +/// ``` +/// use opentelemetry::{global, trace::{Span, Tracer, TraceContextExt}, Context}; +/// +/// let tracer = global::tracer("my-component"); +/// +/// let parent = tracer.start("foo"); +/// let parent_cx = Context::current_with_span(parent); +/// let mut child = tracer.start_with_context("bar", &parent_cx); +/// +/// // ... +/// +/// child.end(); // explicitly end +/// drop(parent_cx) // or implicitly end on drop +/// ``` +/// +/// Spans can also use the current thread's [`Context`] to track which span is active: +/// +/// ``` +/// use opentelemetry::{global, trace::{SpanKind, Tracer}}; +/// +/// let tracer = global::tracer("my-component"); +/// +/// // Create simple spans with `in_span` +/// tracer.in_span("foo", |_foo_cx| { +/// // parent span is active +/// tracer.in_span("bar", |_bar_cx| { +/// // child span is now the active span and associated with the parent span +/// }); +/// // child has ended, parent now the active span again +/// }); +/// // parent has ended, no active spans +/// ``` +/// +/// Spans can also be marked as active, and the resulting guard allows for +/// greater control over when the span is no longer considered active. +/// +/// ``` +/// use opentelemetry::{global, trace::{Span, Tracer, mark_span_as_active}}; +/// let tracer = global::tracer("my-component"); +/// +/// let parent_span = tracer.start("foo"); +/// let parent_active = mark_span_as_active(parent_span); +/// +/// { +/// let child = tracer.start("bar"); +/// let _child_active = mark_span_as_active(child); +/// +/// // do work in the context of the child span... +/// +/// // exiting the scope drops the guard, child is no longer active +/// } +/// // Parent is active span again +/// +/// // Parent can be dropped manually, or allowed to go out of scope as well. +/// drop(parent_active); +/// +/// // no active span +/// ``` +/// +/// ## In Asynchronous Code +/// +/// If you are instrumenting code that make use of [`std::future::Future`] or +/// async/await, be sure to use the [`FutureExt`] trait. This is needed because +/// the following example _will not_ work: +/// +/// ```no_run +/// # use opentelemetry::{global, trace::{Tracer, mark_span_as_active}}; +/// # let tracer = global::tracer("foo"); +/// # let span = tracer.start("foo-span"); +/// async { +/// // Does not work +/// let _g = mark_span_as_active(span); +/// // ... +/// }; +/// ``` +/// +/// The context guard `_g` will not exit until the future generated by the +/// `async` block is complete. Since futures can be entered and exited +/// _multiple_ times without them completing, the span remains active for as +/// long as the future exists, rather than only when it is polled, leading to +/// very confusing and incorrect output. +/// +/// In order to trace asynchronous code, the [`Future::with_context`] combinator +/// can be used: +/// +/// ``` +/// # async fn run() -> Result<(), ()> { +/// use opentelemetry::{trace::FutureExt, Context}; +/// let cx = Context::current(); +/// +/// let my_future = async { +/// // ... +/// }; +/// +/// my_future +/// .with_context(cx) +/// .await; +/// # Ok(()) +/// # } +/// ``` +/// +/// [`Future::with_context`] attaches a context to the future, ensuring that the +/// context's lifetime is as long as the future's. +/// +/// [`FutureExt`]: crate::trace::FutureExt +/// [`Future::with_context`]: crate::trace::FutureExt::with_context() +/// [`Context`]: crate::Context +pub trait Tracer { + /// The [`Span`] type used by this tracer. + type Span: Span; + + /// Starts a new [`Span`]. + /// + /// By default the currently active `Span` is set as the new `Span`'s parent. + /// + /// Each span has zero or one parent span and zero or more child spans, which + /// represent causally related operations. A tree of related spans comprises a + /// trace. A span is said to be a root span if it does not have a parent. Each + /// trace includes a single root span, which is the shared ancestor of all other + /// spans in the trace. + fn start(&self, name: T) -> Self::Span + where + T: Into>, + { + Context::map_current(|cx| self.start_with_context(name, cx)) + } + + /// Starts a new [`Span`] with a given context. + /// + /// If this context contains a span, the newly created span will be a child of + /// that span. + /// + /// Each span has zero or one parent span and zero or more child spans, which + /// represent causally related operations. A tree of related spans comprises a + /// trace. A span is said to be a root span if it does not have a parent. Each + /// trace includes a single root span, which is the shared ancestor of all other + /// spans in the trace. + fn start_with_context(&self, name: T, parent_cx: &Context) -> Self::Span + where + T: Into>, + { + self.build_with_context(SpanBuilder::from_name(name), parent_cx) + } + + /// Creates a span builder. + /// + /// [`SpanBuilder`]s allow you to specify all attributes of a [`Span`] before + /// the span is started. + fn span_builder(&self, name: T) -> SpanBuilder + where + T: Into>, + { + SpanBuilder::from_name(name) + } + + /// Start a [`Span`] from a [`SpanBuilder`]. + fn build(&self, builder: SpanBuilder) -> Self::Span { + Context::map_current(|cx| self.build_with_context(builder, cx)) + } + + /// Start a span from a [`SpanBuilder`] with a parent context. + fn build_with_context(&self, builder: SpanBuilder, parent_cx: &Context) -> Self::Span; + + /// Start a new span and execute the given closure with reference to the context + /// in which the span is active. + /// + /// This method starts a new span and sets it as the active span for the given + /// function. It then executes the body. It ends the span before returning the + /// execution result. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{global, trace::{Span, Tracer, get_active_span}, KeyValue}; + /// + /// fn my_function() { + /// // start an active span in one function + /// global::tracer("my-component").in_span("span-name", |_cx| { + /// // anything happening in functions we call can still access the active span... + /// my_other_function(); + /// }) + /// } + /// + /// fn my_other_function() { + /// // call methods on the current span from + /// get_active_span(|span| { + /// span.add_event("An event!".to_string(), vec![KeyValue::new("happened", true)]); + /// }) + /// } + /// ``` + fn in_span(&self, name: N, f: F) -> T + where + F: FnOnce(Context) -> T, + N: Into>, + Self::Span: Send + Sync + 'static, + { + self.in_span_with_builder(self.span_builder(name), f) + } + + /// Start a new span with a given parent context and execute the given closure with + /// reference to the context in which the span is active. + /// + /// This method starts a new span as a child of the provided parent context and sets + /// it as the active span for the given function. It then executes the body. It ends + /// the span before returning the execution result. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{global, trace::{Span, Tracer, TraceContextExt}, Context, KeyValue}; + /// + /// fn my_function() { + /// let tracer = global::tracer("my-component"); + /// + /// // Create a parent context + /// let parent = tracer.start("parent-span"); + /// let parent_cx = Context::current_with_span(parent); + /// + /// // start an active span with explicit parent context + /// tracer.in_span_with_context("child-span", &parent_cx, |_cx| { + /// // child span is active here + /// }) + /// } + /// ``` + fn in_span_with_context(&self, name: N, parent_cx: &Context, f: F) -> T + where + F: FnOnce(Context) -> T, + N: Into>, + Self::Span: Send + Sync + 'static, + { + self.in_span_with_builder_and_context(self.span_builder(name), parent_cx, f) + } + + /// Start a new span from a [`SpanBuilder`] and execute the given closure with + /// reference to the context in which the span is active. + /// + /// This method builds and starts a new span from a [`SpanBuilder`] using the current + /// context and sets it as the active span for the given function. It then executes + /// the body. It ends the span before returning the execution result. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{global, trace::{Span, SpanKind, Tracer}, KeyValue}; + /// + /// fn my_function() { + /// let tracer = global::tracer("my-component"); + /// + /// // start an active span with span configuration + /// tracer.in_span_with_builder( + /// tracer.span_builder("span-name") + /// .with_kind(SpanKind::Client) + /// .with_attributes(vec![KeyValue::new("key", "value")]), + /// |_cx| { + /// // span is active here with configured attributes + /// } + /// ) + /// } + /// ``` + fn in_span_with_builder(&self, builder: SpanBuilder, f: F) -> T + where + F: FnOnce(Context) -> T, + Self::Span: Send + Sync + 'static, + { + let span = self.build(builder); + let cx = Context::current_with_span(span); + let _guard = cx.clone().attach(); + f(cx) + } + + /// Start a new span from a [`SpanBuilder`] with a given parent context and execute the + /// given closure with reference to the context in which the span is active. + /// + /// This method builds and starts a new span from a [`SpanBuilder`] as a child of the + /// provided parent context and sets it as the active span for the given function. It + /// then executes the body. It ends the span before returning the execution result. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{global, trace::{Span, SpanKind, Tracer, TraceContextExt}, Context, KeyValue}; + /// + /// fn my_function() { + /// let tracer = global::tracer("my-component"); + /// + /// // Create a parent context + /// let parent = tracer.start("parent-span"); + /// let parent_cx = Context::current_with_span(parent); + /// + /// // start an active span with explicit parent context and span configuration + /// tracer.in_span_with_builder_and_context( + /// tracer.span_builder("child-span") + /// .with_kind(SpanKind::Client) + /// .with_attributes(vec![KeyValue::new("key", "value")]), + /// &parent_cx, + /// |_cx| { + /// // child span is active here with configured attributes + /// } + /// ) + /// } + /// ``` + fn in_span_with_builder_and_context( + &self, + builder: SpanBuilder, + parent_cx: &Context, + f: F, + ) -> T + where + F: FnOnce(Context) -> T, + Self::Span: Send + Sync + 'static, + { + let span = self.build_with_context(builder, parent_cx); + let cx = parent_cx.with_span(span); + let _guard = cx.clone().attach(); + f(cx) + } +} + +/// `SpanBuilder` allows span attributes to be configured before the span +/// has started. +/// +/// ``` +/// use opentelemetry::{ +/// global, +/// trace::{TracerProvider, SpanBuilder, SpanKind, Tracer}, +/// }; +/// +/// let tracer = global::tracer("example-tracer"); +/// +/// // The builder can be used to create a span directly with the tracer +/// let _span = tracer.build(SpanBuilder { +/// name: "example-span-name".into(), +/// span_kind: Some(SpanKind::Server), +/// ..Default::default() +/// }); +/// +/// // Or used with builder pattern +/// let _span = tracer +/// .span_builder("example-span-name") +/// .with_kind(SpanKind::Server) +/// .start(&tracer); +/// ``` +#[derive(Clone, Debug, Default)] +pub struct SpanBuilder { + /// Span kind + pub span_kind: Option, + + /// Span name + pub name: Cow<'static, str>, + + /// Span start time + pub start_time: Option, + + /// Span attributes that are provided at the span creation time. + /// More attributes can be added afterwards. + /// Providing duplicate keys will result in multiple attributes + /// with the same key, as there is no de-duplication performed. + pub attributes: Option>, + + /// Span events + pub events: Option>, + + /// Span Links + pub links: Option>, +} + +/// SpanBuilder methods +impl SpanBuilder { + /// Create a new span builder from a span name + pub fn from_name>>(name: T) -> Self { + SpanBuilder { + name: name.into(), + ..Default::default() + } + } + + /// Assign span kind + pub fn with_kind(self, span_kind: SpanKind) -> Self { + SpanBuilder { + span_kind: Some(span_kind), + ..self + } + } + + /// Assign span start time + pub fn with_start_time>(self, start_time: T) -> Self { + SpanBuilder { + start_time: Some(start_time.into()), + ..self + } + } + + /// Assign span attributes from an iterable. + /// Providing duplicate keys will result in multiple attributes + /// with the same key, as there is no de-duplication performed. + pub fn with_attributes(self, attributes: I) -> Self + where + I: IntoIterator, + { + SpanBuilder { + attributes: Some(attributes.into_iter().collect()), + ..self + } + } + + /// Assign events + pub fn with_events(self, events: Vec) -> Self { + SpanBuilder { + events: Some(events), + ..self + } + } + + /// Assign links + pub fn with_links(self, mut links: Vec) -> Self { + links.retain(|l| l.span_context.is_valid()); + SpanBuilder { + links: Some(links), + ..self + } + } + + /// Builds a span with the given tracer from this configuration. + pub fn start(self, tracer: &T) -> T::Span { + Context::map_current(|cx| tracer.build_with_context(self, cx)) + } + + /// Builds a span with the given tracer from this configuration and parent. + pub fn start_with_context(self, tracer: &T, parent_cx: &Context) -> T::Span { + tracer.build_with_context(self, parent_cx) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/trace/tracer_provider.rs b/vendor/opentelemetry-rust/opentelemetry/src/trace/tracer_provider.rs new file mode 100644 index 00000000..fddebc3b --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/trace/tracer_provider.rs @@ -0,0 +1,58 @@ +use crate::{trace::Tracer, InstrumentationScope}; +use std::borrow::Cow; + +/// Types that can create instances of [`Tracer`]. +/// +/// See the [`global`] module for examples of storing and retrieving tracer +/// provider instances. +/// +/// [`global`]: crate::global +pub trait TracerProvider { + /// The [`Tracer`] type that this provider will return. + type Tracer: Tracer; + + /// Returns a new tracer with the given name. + /// + /// The `name` should be the application name or the name of the library + /// providing instrumentation. If the name is empty, then an + /// implementation-defined default name may be used instead. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{global, trace::TracerProvider}; + /// use opentelemetry::KeyValue; + /// + /// let provider = global::tracer_provider(); + /// + /// // tracer used in applications/binaries + /// let tracer = provider.tracer("my_app"); + /// ``` + fn tracer(&self, name: impl Into>) -> Self::Tracer { + let scope = InstrumentationScope::builder(name).build(); + self.tracer_with_scope(scope) + } + + /// Returns a new versioned tracer with the given instrumentation scope. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{global, InstrumentationScope, trace::TracerProvider}; + /// + /// let provider = global::tracer_provider(); + /// + /// // tracer used in applications/binaries + /// let tracer = provider.tracer("my_app"); + /// + /// // tracer used in libraries/crates that optionally includes version and schema url + /// let scope = + /// InstrumentationScope::builder(env!("CARGO_PKG_NAME")) + /// .with_version(env!("CARGO_PKG_VERSION")) + /// .with_schema_url("https://opentelemetry.io/schemas/1.0.0") + /// .build(); + /// + /// let tracer = provider.tracer_with_scope(scope); + /// ``` + fn tracer_with_scope(&self, scope: InstrumentationScope) -> Self::Tracer; +} diff --git a/vendor/opentelemetry-rust/opentelemetry/src/trace_context.rs b/vendor/opentelemetry-rust/opentelemetry/src/trace_context.rs new file mode 100644 index 00000000..78f59a9a --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry/src/trace_context.rs @@ -0,0 +1,257 @@ +use std::fmt; +use std::hash::Hash; +use std::num::ParseIntError; +use std::ops::{BitAnd, BitOr, Not}; + +/// Flags that can be set on a `SpanContext`. +/// +/// The current version of the specification only supports a single flag +/// [`TraceFlags::SAMPLED`]. +/// +/// See the W3C TraceContext specification's [trace-flags] section for more +/// details. +/// +/// [trace-flags]: https://www.w3.org/TR/trace-context/#trace-flags +#[derive(Clone, Debug, Default, PartialEq, Eq, Copy, Hash)] +pub struct TraceFlags(u8); + +impl TraceFlags { + /// Trace flags with the `sampled` flag set to `0`. + /// + /// Spans that are not sampled will be ignored by most tracing tools. + /// See the `sampled` section of the [W3C TraceContext specification] for details. + /// + /// [W3C TraceContext specification]: https://www.w3.org/TR/trace-context/#sampled-flag + pub const NOT_SAMPLED: TraceFlags = TraceFlags(0x00); + + /// Trace flags with the `sampled` flag set to `1`. + /// + /// Spans that are not sampled will be ignored by most tracing tools. + /// See the `sampled` section of the [W3C TraceContext specification] for details. + /// + /// [W3C TraceContext specification]: https://www.w3.org/TR/trace-context/#sampled-flag + pub const SAMPLED: TraceFlags = TraceFlags(0x01); + + /// Construct new trace flags + pub const fn new(flags: u8) -> Self { + TraceFlags(flags) + } + + /// Returns `true` if the `sampled` flag is set + pub fn is_sampled(&self) -> bool { + (*self & TraceFlags::SAMPLED) == TraceFlags::SAMPLED + } + + /// Returns copy of the current flags with the `sampled` flag set. + pub fn with_sampled(&self, sampled: bool) -> Self { + if sampled { + *self | TraceFlags::SAMPLED + } else { + *self & !TraceFlags::SAMPLED + } + } + + /// Returns the flags as a `u8` + pub fn to_u8(self) -> u8 { + self.0 + } +} + +impl BitAnd for TraceFlags { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self::Output { + Self(self.0 & rhs.0) + } +} + +impl BitOr for TraceFlags { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl Not for TraceFlags { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) + } +} + +impl fmt::LowerHex for TraceFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + +/// A 16-byte value which identifies a given trace. +/// +/// The id is valid if it contains at least one non-zero byte. +#[derive(Clone, PartialEq, Eq, Copy, Hash)] +pub struct TraceId(u128); + +impl TraceId { + /// Invalid trace id + pub const INVALID: TraceId = TraceId(0); + + /// Create a trace id from its representation as a byte array. + pub const fn from_bytes(bytes: [u8; 16]) -> Self { + TraceId(u128::from_be_bytes(bytes)) + } + + /// Return the representation of this trace id as a byte array. + pub const fn to_bytes(self) -> [u8; 16] { + self.0.to_be_bytes() + } + + /// Converts a string in base 16 to a trace id. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::trace::TraceId; + /// + /// assert!(TraceId::from_hex("42").is_ok()); + /// assert!(TraceId::from_hex("58406520a006649127e371903a2de979").is_ok()); + /// + /// assert!(TraceId::from_hex("not_hex").is_err()); + /// ``` + pub fn from_hex(hex: &str) -> Result { + u128::from_str_radix(hex, 16).map(TraceId) + } +} + +impl From for TraceId { + fn from(value: u128) -> Self { + TraceId(value) + } +} + +impl fmt::Debug for TraceId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("{:032x}", self.0)) + } +} + +impl fmt::Display for TraceId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("{:032x}", self.0)) + } +} + +impl fmt::LowerHex for TraceId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + +/// An 8-byte value which identifies a given span. +/// +/// The id is valid if it contains at least one non-zero byte. +#[derive(Clone, PartialEq, Eq, Copy, Hash)] +pub struct SpanId(u64); + +impl SpanId { + /// Invalid span id + pub const INVALID: SpanId = SpanId(0); + + /// Create a span id from its representation as a byte array. + pub const fn from_bytes(bytes: [u8; 8]) -> Self { + SpanId(u64::from_be_bytes(bytes)) + } + + /// Return the representation of this span id as a byte array. + pub const fn to_bytes(self) -> [u8; 8] { + self.0.to_be_bytes() + } + + /// Converts a string in base 16 to a span id. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::trace::SpanId; + /// + /// assert!(SpanId::from_hex("42").is_ok()); + /// assert!(SpanId::from_hex("58406520a0066491").is_ok()); + /// + /// assert!(SpanId::from_hex("not_hex").is_err()); + /// ``` + pub fn from_hex(hex: &str) -> Result { + u64::from_str_radix(hex, 16).map(SpanId) + } +} + +impl From for SpanId { + fn from(value: u64) -> Self { + SpanId(value) + } +} + +impl fmt::Debug for SpanId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("{:016x}", self.0)) + } +} + +impl fmt::Display for SpanId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_fmt(format_args!("{:016x}", self.0)) + } +} + +impl fmt::LowerHex for SpanId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[rustfmt::skip] + fn trace_id_test_data() -> Vec<(TraceId, &'static str, [u8; 16])> { + vec![ + (TraceId(0), "00000000000000000000000000000000", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + (TraceId(42), "0000000000000000000000000000002a", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42]), + (TraceId(126642714606581564793456114182061442190), "5f467fe7bf42676c05e20ba4a90e448e", [95, 70, 127, 231, 191, 66, 103, 108, 5, 226, 11, 164, 169, 14, 68, 142]) + ] + } + + #[rustfmt::skip] + fn span_id_test_data() -> Vec<(SpanId, &'static str, [u8; 8])> { + vec![ + (SpanId(0), "0000000000000000", [0, 0, 0, 0, 0, 0, 0, 0]), + (SpanId(42), "000000000000002a", [0, 0, 0, 0, 0, 0, 0, 42]), + (SpanId(5508496025762705295), "4c721bf33e3caf8f", [76, 114, 27, 243, 62, 60, 175, 143]) + ] + } + + #[test] + fn test_trace_id() { + for test_case in trace_id_test_data() { + assert_eq!(format!("{}", test_case.0), test_case.1); + assert_eq!(format!("{:032x}", test_case.0), test_case.1); + assert_eq!(test_case.0.to_bytes(), test_case.2); + + assert_eq!(test_case.0, TraceId::from_hex(test_case.1).unwrap()); + assert_eq!(test_case.0, TraceId::from_bytes(test_case.2)); + } + } + + #[test] + fn test_span_id() { + for test_case in span_id_test_data() { + assert_eq!(format!("{}", test_case.0), test_case.1); + assert_eq!(format!("{:016x}", test_case.0), test_case.1); + assert_eq!(test_case.0.to_bytes(), test_case.2); + + assert_eq!(test_case.0, SpanId::from_hex(test_case.1).unwrap()); + assert_eq!(test_case.0, SpanId::from_bytes(test_case.2)); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/CHANGELOG.md b/vendor/opentelemetry-rust/opentelemetry_sdk/CHANGELOG.md new file mode 100644 index 00000000..805526ca --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/CHANGELOG.md @@ -0,0 +1,1165 @@ +# Changelog + +## vNext + +- Fixed `Histogram`, `Sum`, `LastValue`, and `PrecomputedSum` aggregators (and + their bound-instrument handles) silently accepting NaN/Infinity + measurements. For `Histogram` and `Sum`, a single NaN measurement + permanently corrupted the cumulative `sum`/total for the rest of the + process's lifetime (`x + NaN == NaN`); `LastValue` and `PrecomputedSum` + self-healed on the next valid measurement but still exported one bad value + in the meantime, and `PrecomputedSum` under delta temporality corrupted two + export cycles. Non-finite measurements are now dropped before recording, + matching `ExponentialHistogram`'s existing behavior. + ([#3656](https://github.com/open-telemetry/opentelemetry-rust/issues/3656)) + +## 0.33.0 + +Released 2026-Sep-18 + +- Publicly export the `OTEL_*`/`OTEL_*_DEFAULT` environment variable name and + default value constants for `BatchSpanProcessor` (`opentelemetry_sdk::trace`), + `BatchLogProcessor` (`opentelemetry_sdk::logs`), and `PeriodicReader` + (`opentelemetry_sdk::metrics`), so downstream configuration systems can read + the SDK's spec-defined defaults programmatically instead of duplicating + them. As part of this, `PeriodicReader`'s previously-private + `DEFAULT_INTERVAL`/`METRIC_EXPORT_INTERVAL_NAME` constants were renamed + to `OTEL_METRIC_EXPORT_INTERVAL_DEFAULT`/`OTEL_METRIC_EXPORT_INTERVAL` to + match the naming convention already used elsewhere. + ([#3623](https://github.com/open-telemetry/opentelemetry-rust/issues/3623)) +- Added SDK self-observability metrics, feature-gated behind + `experimental_metrics_bound_instruments`: `otel.sdk.log.created` counts log + records submitted to the SDK; `otel.sdk.processor.log.processed` and + `otel.sdk.processor.span.processed` count records and spans submitted to an + exporter by batch and simple processors, with `error.type` reporting items + dropped before submission; and `otel.sdk.processor.log.queue.capacity` + reports the configured `BatchLogProcessor` queue capacity. + ([#3514](https://github.com/open-telemetry/opentelemetry-rust/pull/3514), + [#3608](https://github.com/open-telemetry/opentelemetry-rust/pull/3608), + [#3609](https://github.com/open-telemetry/opentelemetry-rust/pull/3609), + [#3611](https://github.com/open-telemetry/opentelemetry-rust/pull/3611)) +- Made `futures-channel`, `futures-executor`, `futures-util`, and `thiserror` + optional, enabling a minimal SDK build. With `default-features = false`, the + SDK's only dependency is the `opentelemetry` API crate. + ([#3593](https://github.com/open-telemetry/opentelemetry-rust/pull/3593)) +- Bound instruments are now available for `Gauge` and `UpDownCounter` via the + new `BoundGauge` and `BoundUpDownCounter` types exposed by the + `opentelemetry` crate. Requires the `experimental_metrics_bound_instruments` + feature. +- Fixed a race in `BatchSpanProcessor` and `BatchLogProcessor` where a + span/log enqueued just before `force_flush()` or `shutdown()` could be + missed by the flush and dropped at shutdown: the pending-item counter is + now incremented before enqueueing (and reverted if the queue is full), so + the worker's counter snapshot can no longer under-count items already in + the queue (#3453). +- Default SDK Resource construction now falls back to `unknown_service` under + Miri instead of calling `std::env::current_exe()`, avoiding an abort in Miri + isolation mode while preserving the normal + `unknown_service:` fallback outside Miri. +- Fixed asynchronous counters (`ObservableCounter`, `ObservableUpDownCounter`) + using delta temporality reporting incorrect deltas when observed attributes + were recorded in an unsorted key order. + +## 0.32.1 + +Released 2026-May-23 + +- `BaggagePropagator` now enforces the W3C Baggage maximum header length + (8192 bytes) and maximum list-member count (64) when extracting an inbound + `baggage` header. Headers exceeding 8192 bytes are dropped at the + propagator boundary; headers with more than 64 list members are + truncated to the first 64 entries. The change keeps the propagator from + parsing attacker-controlled input beyond the W3C limits instead of doing + per-entry parse, decode, and allocation work only to discard the excess + on `Baggage` insert. See https://www.w3.org/TR/baggage/#limits. +- Reverted the `SimpleSpanProcessor` telemetry suppression added in 0.32.0 + (see #3494), which caused a `RefCell already borrowed` panic when a span + was started and dropped inside a `get_active_span` (or `Context::map_current`) + closure. Tracked in #3510. A proper fix for the underlying + `Context::map_current` re-entrancy will be investigated separately, after + which the suppression can be safely re-applied. +- View-provided metric stream `name` (set via `Stream::builder().with_name(...)`) + is no longer validated against the instrument name syntax, per + [spec clarification](https://github.com/open-telemetry/opentelemetry-specification/pull/5094). + `unit` and other stream parameters continue to be validated. + +## 0.32.0 + +Released 2026-May-08 + +- `SimpleSpanProcessor` now suppresses telemetry during export, preventing + telemetry-induced-telemetry feedback loops. This aligns with the existing + behavior in `BatchSpanProcessor` and `SimpleLogProcessor`. +- Removed `SimpleConcurrentLogProcessor` and the `experimental_logs_concurrent_log_processor` + feature flag. The use cases it was designed for (ETW/user_events exporters) are + better served by modeling those exporters as processors directly. +- **Added** `Counter::bind()` and `Histogram::bind()` SDK implementations that + return pre-bound measurement handles (`BoundCounter`, `BoundHistogram`). + Bound instruments resolve the attribute-to-aggregator mapping once at bind time + and cache the result, eliminating per-call HashMap lookups. View attribute + filtering is applied at bind time so the hot path stays free of per-call + attribute processing. Bound and unbound recordings with the same (post-view) + attribute set always aggregate into the same data point, including the empty + attribute set. Bound entries are never evicted during delta collection while + a handle exists — idle cycles produce no export but the tracker persists. If + `bind()` is called at the cardinality limit, the handle binds directly to + the overflow tracker — its writes stay on the same direct (no-lookup) hot + path and consistently land in the `otel.metric.overflow=true` bucket for + the lifetime of the handle. To recover a bound handle after delta collection + frees space, drop the existing handle and call `bind()` again. Gated behind + the `experimental_metrics_bound_instruments` feature flag. Benchmarks show + ~28x speedup for counter operations and ~9x for histograms. +- Delta metrics collection now uses in-place eviction instead of draining the + HashMap on every collect cycle. Stale attribute sets that received no measurements + since the last collection are evicted. Note: recovery from cardinality overflow + now requires 2 collect cycles — the first marks entries as stale, the second + evicts them. +- **Breaking** The SDK `testing` feature is now runtime agnostic. [#3407][3407] + - `TokioSpanExporter` and `new_tokio_test_exporter` have been renamed to `TestSpanExporter` and `new_test_exporter`. + - The following transitive dependencies and features have been removed: `tokio/rt`, `tokio/time`, `tokio/macros`, `tokio/rt-multi-thread`, `tokio-stream`, `experimental_async_runtime` +- Store `InstrumentationScope` in `Arc` internally in `SdkTracer`, making tracer clones cheaper (Arc refcount increment instead of deep copy). +- Add 32-bit platform support by using `portable-atomic` for `AtomicI64` and `AtomicU64` in the metrics module. This enables compilation on 32-bit ARM targets (e.g., `armv5te-unknown-linux-gnueabi`, `armv7-unknown-linux-gnueabihf`). +- `Aggregation` enum and `StreamBuilder::with_aggregation()` are now stable and no longer require the `spec_unstable_metrics_views` feature flag. +- Fix `service.name` Resource attribute fallback to follow OpenTelemetry + specification by using `unknown_service:` format when + service name is not explicitly configured. Previously, it only used + `unknown_service`. +- Fix `SpanExporter::shutdown()` default timeout from 5 nanoseconds to 5 seconds. +- **Breaking** `SpanExporter` trait methods `shutdown`, `shutdown_with_timeout`, and `force_flush` now take `&self` instead of `&mut self` for consistency with `LogExporter` and `PushMetricExporter`. Implementers using interior mutability (e.g., `Mutex`, `AtomicBool`) require no changes. +- Added `Resource::get_ref(&self, key: &Key) -> Option<&Value>` to allow retrieving a reference to a resource value without cloning. +- **Breaking** Removed the following public hidden methods from the `SdkTracer` [#3227][3227]: + - `id_generator`, `should_sample` +- **Breaking** Moved the following SDK sampling types from `opentelemetry::trace` to `opentelemetry_sdk::trace` [#3277][3277]: + - `SamplingDecision`, `SamplingResult` + - These types are SDK implementation details and should be imported from `opentelemetry_sdk::trace` instead. +- `StreamBuilder::build()` now rejects `usize::MAX` as a cardinality limit + with a validation error. [#3506][3506] +- Fix Histogram boundaries being ignored in the presence of views [#3312][3312] +- `TracerProviderBuilder::with_sampler` allows to pass boxed instance of `ShouldSample` [#3313][3313] +- Fix ObservableCounter and ObservableUpDownCounter now correctly report only data points from the current measurement cycle, removing stale attribute combinations that are no longer observed. [#3248][3248] +- Fix panic when `SpanProcessor::on_end` calls `Context::current()` ([#3262][3262]). + - Updated `SpanProcessor::on_end` documentation to clarify that `Context::current()` returns the parent context, not the span's context +- Fix `traceparent` headers with unknown flags (e.g. W3C random-trace-id flag `0x02`) being incorrectly rejected. Unknown flags are now accepted and zeroed out as required by the W3C trace-context spec. [#3435][3435] +- Add public constructors to metrics data types and make SdkLogRecord::new() public. [#2992][2992] +- **Breaking** `InMemoryExporterError` has been removed and replaced by `OTelSdkError`, and a new `JaegerRemoteSamplerBuildError` introduced to replace last uses of `TraceError`. [#3458][3458] +- "spec_unstable_logs_enabled" feature flag is removed. The capability (and the + backing specification) is now stable and is enabled by default. [#3278][3278] + +[3227]: https://github.com/open-telemetry/opentelemetry-rust/pull/3227 +[3277]: https://github.com/open-telemetry/opentelemetry-rust/pull/3277 +[3278]: https://github.com/open-telemetry/opentelemetry-rust/pull/3278 +[3312]: https://github.com/open-telemetry/opentelemetry-rust/pull/3312 +[3248]: https://github.com/open-telemetry/opentelemetry-rust/pull/3248 +[3262]: https://github.com/open-telemetry/opentelemetry-rust/pull/3262 +[3407]: https://github.com/open-telemetry/opentelemetry-rust/pull/3407 +[3435]: https://github.com/open-telemetry/opentelemetry-rust/issues/3435 +[3458]: https://github.com/open-telemetry/opentelemetry-rust/pull/3458 +[3506]: https://github.com/open-telemetry/opentelemetry-rust/pull/3506 + +## 0.31.0 + +Released 2025-Sep-25 + +- Updated `opentelemetry` and `opentelemetry-http` dependencies to version 0.31.0. +- **Feature**: Add span flags support for `isRemote` property in OTLP exporter ([#3153](https://github.com/open-telemetry/opentelemetry-rust/pull/3153)) +- Updated span and link transformations to properly set flags field (0x100 for local, 0x300 for remote) + +- TODO: Placeholder for Span processor related things + - *Fix* SpanProcessor::on_start is no longer called on non recording spans +- **Fix**: Restore true parallel exports in the async-native `BatchSpanProcessor` by honoring `OTEL_BSP_MAX_CONCURRENT_EXPORTS` ([#2959](https://github.com/open-telemetry/opentelemetry-rust/pull/3028)). A regression in [#2685](https://github.com/open-telemetry/opentelemetry-rust/pull/2685) inadvertently awaited the `export()` future directly in `opentelemetry-sdk/src/trace/span_processor_with_async_runtime.rs` instead of spawning it on the runtime, forcing all exports to run sequentially. +- **Feature**: Added `Clone` implementation to `SdkLogger` for API consistency with `SdkTracer` ([#3058](https://github.com/open-telemetry/opentelemetry-rust/issues/3058)). +- **Fix**: batch size accounting in BatchSpanProcessor when queue is full ([#3089](https://github.com/open-telemetry/opentelemetry-rust/pull/3089)). +- **Fix**: Resolved dependency issue where the "logs" feature incorrectly + required the "trace" feature flag + ([#3096](https://github.com/open-telemetry/opentelemetry-rust/issues/3096)). + The logs functionality now operates independently, while automatic correlation + between logs and traces continues to work when the "trace" feature is + explicitly enabled. +- **Fix**: Fix shutdown of `SimpleLogProcessor` and async `BatchLogProcessor`. +- Default implementation of `LogProcessor::shutdown_with_timeout()` will now warn to encourage users to implement proper shutdown. + +## 0.30.0 + +Released 2025-May-23 + +- Updated `opentelemetry` and `opentelemetry-http` dependencies to version 0.30.0. +- It is now possible to add links to a `Span` via the `SpanRef` that you get from + a `Context`. [2959](https://github.com/open-telemetry/opentelemetry-rust/pull/2959) +- **Feature**: Added context based telemetry suppression. [#2868](https://github.com/open-telemetry/opentelemetry-rust/pull/2868) + - `SdkLogger`, `SdkTracer` modified to respect telemetry suppression based on +`Context`. In other words, if the current context has telemetry suppression +enabled, then logs/spans will be ignored. + - The flag is typically set by OTel +components to prevent telemetry from itself being fed back into OTel. + - `BatchLogProcessor`, `BatchSpanProcessor`, and `PeriodicReader` modified to set +the suppression flag in their dedicated thread, so that telemetry generated from +those threads will not be fed back into OTel. + - Similarly, `SimpleLogProcessor` +also modified to suppress telemetry before invoking exporters. + +- **Feature**: Implemented and enabled cardinality capping for Metrics by + default. [#2901](https://github.com/open-telemetry/opentelemetry-rust/pull/2901) + - The default cardinality limit is 2000 and can be customized using Views. + - This feature was previously removed in version 0.28 due to the lack of + configurability but has now been reintroduced with the ability to configure + the limit. + - Fixed the overflow attribute to correctly use the boolean value `true` + instead of the string `"true"`. + [#2878](https://github.com/open-telemetry/opentelemetry-rust/issues/2878) +- The `shutdown_with_timeout` method is added to SpanProcessor, SpanExporter trait and TracerProvider. +- The `shutdown_with_timeout` method is added to LogExporter trait. +- The `shutdown_with_timeout` method is added to LogProvider and LogProcessor trait. +- *Breaking* `MetricError`, `MetricResult` no longer public (except when + `spec_unstable_metrics_views` feature flag is enabled). `OTelSdkResult` should + be used instead, wherever applicable. [#2906](https://github.com/open-telemetry/opentelemetry-rust/pull/2906) +- *Breaking* change, affecting custom `MetricReader` authors: + - The + `shutdown_with_timeout` method is added to `MetricReader` trait. + - `collect` + method on `MetricReader` modified to return `OTelSdkResult`. + [#2905](https://github.com/open-telemetry/opentelemetry-rust/pull/2905) + - `MetricReader` + trait, `ManualReader` struct, `Pipeline` struct, `InstrumentKind` enum moved + behind feature flag "experimental_metrics_custom_reader". + [#2928](https://github.com/open-telemetry/opentelemetry-rust/pull/2928) + +- **Views improvements**: + - Core view functionality is now available by default—users can change the + name, unit, description, and cardinality limit of a metric via views without + enabling the `spec_unstable_metrics_views` feature flag. Advanced view + features, such as custom aggregation or attribute filtering, still require + the `spec_unstable_metrics_views` feature. + - Removed `new_view()` method and `View` trait. Views can now be added by passing + a function with signature `Fn(&Instrument) -> Option` to the `with_view` + method on `MeterProviderBuilder`. +- Introduced a builder pattern for `Stream` creation to use with views: + - Added `StreamBuilder` struct with methods to configure stream properties + - Added `Stream::builder()` method that returns a new `StreamBuilder` + - `StreamBuilder::build()` returns `Result>` enabling + proper validation. + +Example of using views to rename a metric: + +```rust +let view_rename = |i: &Instrument| { + if i.name() == "my_histogram" { + Some( + Stream::builder() + .with_name("my_histogram_renamed") + .build() + .unwrap(), + ) + } else { + None + } +}; + +let provider = SdkMeterProvider::builder() + // add exporters, set resource etc. + .with_view(view_rename) + .build(); +``` + +- *Breaking* `Aggregation` enum moved behind feature flag + "spec_unstable_metrics_views". This was only required when using advanced view + capabilities. + [#2928](https://github.com/open-telemetry/opentelemetry-rust/pull/2928) +- *Breaking* change, affecting custom `PushMetricExporter` authors: + - The `export` method on `PushMetricExporter` now accepts `&ResourceMetrics` + instead of `&mut ResourceMetrics`. + - `ResourceMetrics` no longer exposes `scope_metrics` field, but instead + offers `scope_metrics()` method that returns an iterator over the same. + - `ScopeMetrics` no longer exposes `metrics` field, but instead offers + `metrics()` method that returns an iterator over the same. + - `Sum`, `Gauge`, `Histogram` & `ExponentialHistogram` no longer exposes + `data_points` field, but instead offers `data_points()` method that returns + an iterator over the same. + - `SumDataPoint`, `GaugeDataPoint`, `HistogramDataPoint` & + `ExponentialHistogramDataPoint` no longer exposes `attributes`, `exemplars` + field, but instead offers `attributes()`, and `exemplars()` method that + returns an iterator over the same. + - `Exemplar` no longer exposes `filtered_attributes` field, but instead + offers `filtered_attributes()` method that returns an iterator over + the same. + - `HistogramDataPoint` no longer exposes `bounds` and `bucket_counts`, but + instead offers `bounds()` and `bucket_counts()` methods that returns an + iterator over the same. + - `Metric` no longer exposes `name`, `description`, `unit`, `data` fields, but + instead offers `name()`, `description()`, `unit()`, and `data()` accessor methods. + - `ResourceMetrics` no longer exposes `resource` field, but instead offers + a `resource()` accessor method. + - `ScopeMetrics` no longer exposes `scope` field, but instead offers + a `scope()` accessor method. + +## 0.29.0 + +Released 2025-Mar-21 + +- Update `opentelemetry` dependency to 0.29. +- Update `opentelemetry-http` dependency to 0.29. +- **Breaking**: The `Runtime` trait has been simplified and refined. See the [#2641](https://github.com/open-telemetry/opentelemetry-rust/pull/2641) + for the changes. +- Removed `async-std` support for `Runtime`, as [`async-std` crate is deprecated](https://crates.io/crates/async-std). +- Calls to `MeterProviderBuilder::with_resource`, `TracerProviderBuilder::with_resource`, + `LoggerProviderBuilder::with_resource` are now additive ([#2677](https://github.com/open-telemetry/opentelemetry-rust/pull/2677)). +- Moved `ExportError` trait from `opentelemetry::trace::ExportError` to `opentelemetry_sdk::export::ExportError` +- Moved `TraceError` enum from `opentelemetry::trace::TraceError` to `opentelemetry_sdk::trace::TraceError` +- Moved `TraceResult` type alias from `opentelemetry::trace::TraceResult` to `opentelemetry_sdk::trace::TraceResult` +- **Breaking**: Make `force_flush()` in `PushMetricExporter` synchronous +- **Breaking**: Updated the `SpanExporter` trait method signature: + +```rust + fn export(&mut self, batch: Vec) -> BoxFuture<'static, OTelSdkResult>; +``` + + to + +```rust + fn export( + &mut self, + batch: Vec, +) -> impl std::future::Future + Send; +``` + + This affects anyone who writes custom exporters, as custom implementations of SpanExporter + should now define export as an `async fn`: + +```rust + impl trace::SpanExporter for CustomExporter { + async fn export(&mut self, batch: Vec) -> OTelSdkResult { + // Implementation here + } +} +``` + +- **Breaking** The SpanExporter::export() method no longer requires a mutable reference to self. + Before: + + ```rust + async fn export(&mut self, batch: Vec) -> OTelSdkResult + ``` + + After: + + ```rust + async fn export(&self, batch: Vec) -> OTelSdkResult + ``` + + Custom exporters will need to internally synchronize any mutable state, if applicable. + +- **Breaking** The `shutdown_with_timeout` method is added to MetricExporter trait. This is breaking change for custom `MetricExporter` authors. +- Bug Fix: `BatchLogProcessor` now correctly calls `shutdown` on the exporter + when its `shutdown` is invoked. + +- Reduced some info level logs to debug +- **Breaking** for custom LogProcessor/Exporter authors: Changed `name` + parameter from `&str` to `Option<&str>` in `event_enabled` method on the + `LogProcessor` and `LogExporter` traits. `SdkLogger` no longer passes its + `scope` name but instead passes the incoming `name` when invoking + `event_enabled` on processors. +- **Breaking** for custom LogExporter authors: `shutdown()` method in + `LogExporter` trait no longer requires a mutable ref to `self`. If the exporter + needs to mutate state, it should rely on interior mutability. + [2764](https://github.com/open-telemetry/opentelemetry-rust/pull/2764) +- *Breaking (Affects custom Exporter/Processor authors only)* Removed + `opentelemetry_sdk::logs::error::{LogError, LogResult}`. These were not + intended to be public. If you are authoring custom processor/exporters, use + `opentelemetry_sdk::error::OTelSdkError` and + `opentelemetry_sdk::error::OTelSdkResult`. + [2790](https://github.com/open-telemetry/opentelemetry-rust/pull/2790) +- **Breaking** for custom `LogProcessor` authors: Changed `set_resource` + to require mutable ref. + `fn set_resource(&mut self, _resource: &Resource) {}` +- **Breaking**: InMemoryExporter's return type change. + - `TraceResult>` to `Result, InMemoryExporterError>` + - `MetricResult>` to `Result, InMemoryExporterError>` + - `LogResult>` to `Result, InMemoryExporterError>` + +## 0.28.0 + +Released 2025-Feb-10 + +Note: Due to the large amount of making changes, check [migration guide to +0.28](../docs/migration_0.28.md) for a summary that can help majority users to +quickly migrate. The changelog below is the full list of changes. + +- Update `opentelemetry` dependency to 0.28. +- Update `opentelemetry-http` dependency to 0.28. +- Bump msrv to 1.75.0. +- *Bug fix*: For cumulative temporality, ObservableGauge no longer export + MetricPoints unless measurements were newly reported (in Observable callbacks) + since last export. This bug fixes ensures ObservableGauge behaves as per OTel + Spec. The bug is *not* addressed for other Observable instruments + [#2213](https://github.com/open-telemetry/opentelemetry-rust/issues/2213) +- Upgrade the tracing crate used for internal logging to version 0.1.40 or +later. This is necessary because the internal logging macros utilize the name +field as metadata, a feature introduced in version 0.1.40. +[#2418](https://github.com/open-telemetry/opentelemetry-rust/pull/2418) +- *Feature*: Introduced a new feature flag, + `experimental_metrics_disable_name_validation`, which disables entire + Instrument Name Validation. This is an experimental feature to unblock use + cases requiring currently disallowed characters (eg: Windows Perf Counters). + Use caution when enabling this feature as this breaks guarantees about metric + name. +- Bug fix: Empty Tracer names are retained as-is instead of replacing with + "rust.opentelemetry.io/sdk/tracer" + [#2486](https://github.com/open-telemetry/opentelemetry-rust/pull/2486) +- Update `EnvResourceDetector` to allow resource attribute values containing + equal signs (`"="`). + [#2120](https://github.com/open-telemetry/opentelemetry-rust/pull/2120) +- `ResourceDetector.detect()` no longer supports timeout option. +- *Breaking* Resource.get() modified to require reference to Key instead of + owned. Replace `get(Key::from_static_str("key"))` with + `get(&Key::from_static_str("key"))` +- *Feature*: Add `ResourceBuilder` for an easy way to create new `Resource`s +- *Breaking*: Remove +- `Resource::{new,empty,from_detectors,new_with_defaults,from_schema_url,merge,default}`. + To create Resources you should only use `Resource::builder()` or `Resource::builder_empty()`. See + [#2322](https://github.com/open-telemetry/opentelemetry-rust/pull/2322) for a migration guide. + + Example Usage: + + ```rust + // old + Resource::default().with_attributes([ + KeyValue::new("service.name", "test_service"), + KeyValue::new("key", "value"), + ]); + + // new + Resource::builder() + .with_service_name("test_service") + .with_attribute(KeyValue::new("key", "value")) + .build(); + ``` + +- *Breaking* : + [#2314](https://github.com/open-telemetry/opentelemetry-rust/pull/2314) + - The LogRecord struct has been updated: + - All fields are now pub(crate) instead of pub. + - Getter methods have been introduced to access field values. This change + impacts custom exporter and processor developers by requiring updates to + code that directly accessed LogRecord fields. They must now use the provided + getter methods (e.g., `log_record.event_name()` instead of + `log_record.event_name`). +- *Breaking (Affects custom metric exporter authors only)* `start_time` and + `time` is moved from DataPoints to aggregations (Sum, Gauge, Histogram, + ExpoHistogram) see + [#2377](https://github.com/open-telemetry/opentelemetry-rust/pull/2377) and + [#2411](https://github.com/open-telemetry/opentelemetry-rust/pull/2411), to + reduce memory. +- *Breaking* `start_time` is no longer optional for `Sum` aggregation, see + [#2367](https://github.com/open-telemetry/opentelemetry-rust/pull/2367), but + is still optional for `Gauge` aggregation see + [#2389](https://github.com/open-telemetry/opentelemetry-rust/pull/2389). +- SimpleLogProcessor modified to be generic over `LogExporter` to avoid + dynamic dispatch to invoke exporter. If you were using + `with_simple_exporter` to add `LogExporter` with SimpleLogProcessor, this is + a transparent change. + [#2338](https://github.com/open-telemetry/opentelemetry-rust/pull/2338) +- *Breaking* `opentelemetry::global::shutdown_tracer_provider()` removed from the API, + should now use `tracer_provider.shutdown()` see + [#2369](https://github.com/open-telemetry/opentelemetry-rust/pull/2369) for + a migration example. "Tracer provider" is cheaply clonable, so users are + encouraged to set a clone of it as the global (ex: + `global::set_tracer_provider(provider.clone()))`, so that instrumentations + and other components can obtain tracers from `global::tracer()`. The + tracer_provider must be kept around to call shutdown on it at the end of + application (ex: `tracer_provider.shutdown()`) + +- *Breaking* The LogExporter::export() method no longer requires a mutable + reference to self.: Before: `async fn export(&mut self, _batch: LogBatch<'_>) + -> LogResult<()>` After: `async fn export(&self, _batch: LogBatch<'_>) -> + LogResult<()>` Custom exporters will need to internally synchronize any + mutable state, if applicable. + +- *Breaking* Removed the following deprecated struct: + - logs::LogData - Previously deprecated in version 0.27.1 Migration Guidance: + This structure is no longer utilized within the SDK, and users should not have + dependencies on it. + +- *Breaking* Removed the following deprecated methods: + - `Logger::provider()` : Previously deprecated in version 0.27.1 + - `Logger::instrumentation_scope()` : Previously deprecated in version 0.27.1. + Migration Guidance: - These methods were intended for log appender authors. + Keep the clone of the provider handle, instead of depending on above + methods. +- Rename `opentelemetry_sdk::logs::Builder` to + `opentelemetry_sdk::logs::LoggerProviderBuilder`. +- Rename `opentelemetry_sdk::trace::Builder` to + `opentelemetry_sdk::trace::SdkTracerProviderBuilder`. +- Redesigned PeriodicReader, BatchSpanProcessor, BatchLogProcessor to no longer + require an async runtime. They create its own background thread instead. When + pairing with OTLP, `grpc-tonic` or `reqwest-blocking-client` are the only + supported features (`hyper`, `reqwest` are not supported) These are now + enabled by default and can be migrated to by removing the extra `rt:Runtime` + argument as shown below. + - `PeriodicReader::builder(exporter,runtime::Tokio).build();` to + `PeriodicReader::builder(exporter).build();` + - `.with_batch_exporter(exporter, runtime::Tokio)` to + `.with_batch_exporter(exporter)` + + The new implementation has following limitations: + - Does not work if your application cannot spawn new Thread. + - Does not support `hyper`, `reqwest` HTTP Clients + - Does not support multiple concurrent exports (`with_max_concurrent_exports` + is not supported). This existed only for traces. + + If this applies to you, you can get the old behavior back by following steps + below: + - Enable one or more of the feature flag from below + `experimental_metrics_periodicreader_with_async_runtime` + `experimental_logs_batch_log_processor_with_async_runtime` + `experimental_trace_batch_span_processor_with_async_runtime` + - Use updated namespace; i.e + `periodic_reader_with_async_runtime::PeriodicReader`, + `log_processor_with_async_runtime::BatchLogProcessor` and + `span_processor_with_async_runtime::BatchSpanProcessor` + - Continue using existing features flags `rt-tokio`, + `rt-tokio-current-thread`, or `rt-async-std`. + + As part of the above redesign of PeriodicReader and BatchProcessors, these + components no longer enforce timeout themselves and instead relies on + Exporters to enforce own timeouts. In other words, the following are no longer + supported. + - `with_max_export_timeout`, `with_timeout` methods on `BatchConfigBuilder`, + `PeriodicReaderBuilder` + - `OTEL_BLRP_EXPORT_TIMEOUT`, `OTEL_BSP_EXPORT_TIMEOUT` + + Users are advised to configure timeout on the Exporters itself. For example, + in the OTLP exporter, the export timeout can be configured using: + - Environment variables + - `OTEL_EXPORTER_OTLP_TIMEOUT` + - `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT`, `OTEL_EXPORTER_OTLP_TRACES_TIMEOUT`, + `OTEL_EXPORTER_OTLP_METRICS_TIMEOUT` + - The opentelemetry_otlp API, via `.with_tonic().with_timeout()` or + `.with_http().with_timeout()`. + +- *Breaking* Introduced `experimental_async_runtime` feature for + runtime-specific traits. + - Runtime-specific features (`rt-tokio`, `rt-tokio-current-thread`, and + `rt-async-std`) now depend on the `experimental_async_runtime` feature. + - For most users, no action is required. Enabling runtime features such as + `rt-tokio`, `rt-tokio-current-thread`, or `rt-async-std` will automatically + enable the `experimental_async_runtime` feature. + - If you're implementing a custom runtime, you must explicitly enable the + experimental_async_runtime` feature in your Cargo.toml and implement the + required `Runtime` traits. + +- Removed Metrics Cardinality Limit feature. This was originally introduced in +[#1066](https://github.com/open-telemetry/opentelemetry-rust/pull/1066) with a +hardcoded limit of 2000 and no ability to change it. This feature will be +re-introduced in a future date, along with the ability to change the cardinality +limit. + +- *Breaking* Removed unused `opentelemetry_sdk::Error` enum. +- *Breaking* (Affects custom Exporter authors only) Moved `ExportError` trait + from `opentelemetry::export::ExportError` to `opentelemetry_sdk::ExportError` +- *Breaking (Affects custom SpanExporter, SpanProcessor authors only)*: Rename + namespaces for Span exporter structs/traits before: + `opentelemetry_sdk::export::spans::{ExportResult, SpanData, SpanExporter};` + now: `opentelemetry_sdk::trace::{ExportResult, SpanData, SpanExporter};` + +- *Breaking (Affects custom LogExporter, LogProcessor authors only)*: Rename + namespaces for Log exporter structs/traits. before: + `opentelemetry_sdk::export::logs::{ExportResult, LogBatch, LogExporter};` now: + `opentelemetry_sdk::logs::{ExportResult, LogBatch, LogExporter};` + +- *Breaking* `opentelemetry_sdk::LogRecord::default()` method is removed. The + only way to create log record outside opentelemetry_sdk crate is using + `Logger::create_log_record()` method. + +- *Breaking*: Rename namespaces for InMemoryExporters. (The module is still + under "testing" feature flag) + before: + + ```rust + opentelemetry_sdk::testing::logs::{InMemoryLogExporter, + InMemoryLogExporterBuilder}; + opentelemetry_sdk::testing::trace::{InMemorySpanExporter, + InMemorySpanExporterBuilder}; + opentelemetry_sdk::testing::metrics::{InMemoryMetricExporter, + InMemoryMetricExporterBuilder}; + ``` + + now: + + ```rust + opentelemetry_sdk::logs::{InMemoryLogExporter, InMemoryLogExporterBuilder}; + opentelemetry_sdk::trace::{InMemorySpanExporter, + InMemorySpanExporterBuilder}; + opentelemetry_sdk::metrics::{InMemoryMetricExporter, + InMemoryMetricExporterBuilder}; + ``` + +- *Breaking* Renamed `LoggerProvider`, `Logger` and `LogRecord` to + `SdkLoggerProvider`,`SdkLogger` and `SdkLogRecord` respectively to avoid name + collision with public API types. + [#2612](https://github.com/open-telemetry/opentelemetry-rust/pull/2612) + +- *Breaking* Renamed `TracerProvider` and `Tracer` to `SdkTracerProvider` and + `SdkTracer` to avoid name collision with public API types. `Tracer` is still + type-aliased to `SdkTracer` to keep back-compat with tracing-opentelemetry. + [#2614](https://github.com/open-telemetry/opentelemetry-rust/pull/2614) + +- *Breaking* Providers, Exporters, Processors, and Readers are modified to use a + unified Result type for `export()`, `force_flush()`, and `shutdown()` methods. + All these methods now use `OTelSdkResult` as their return type. Following PRs + show the exact changes: + [2613](https://github.com/open-telemetry/opentelemetry-rust/pull/2613) + [2625](https://github.com/open-telemetry/opentelemetry-rust/pull/2625) + [2604](https://github.com/open-telemetry/opentelemetry-rust/pull/2604) + [2606](https://github.com/open-telemetry/opentelemetry-rust/pull/2606) + [2573](https://github.com/open-telemetry/opentelemetry-rust/pull/2573) + +## 0.27.1 + +Released 2024-Nov-27 + +- **DEPRECATED**: + - `trace::Config` methods are moving onto `TracerProvider` Builder to be consistent with other signals. See for migration guide. + `trace::Config` is scheduled to be removed from public API in `v0.28.0`. + example: + + ```rust + // old + let tracer_provider: TracerProvider = TracerProvider::builder() + .with_config(Config::default().with_resource(Resource::empty())) + .build(); + + // new + let tracer_provider: TracerProvider = TracerProvider::builder() + .with_resource(Resource::empty()) + .build(); + ``` + + - `logs::LogData` struct is deprecated, and scheduled to be removed from public API in `v0.28.0`. + - Bug fix: Empty Meter names are retained as-is instead of replacing with + "rust.opentelemetry.io/sdk/meter" + [#2334](https://github.com/open-telemetry/opentelemetry-rust/pull/2334) + + - Bug fix: Empty Logger names are retained as-is instead of replacing with + "rust.opentelemetry.io/sdk/logger" + [#2316](https://github.com/open-telemetry/opentelemetry-rust/pull/2316) + + - `Logger::provider`: This method is deprecated as of version `0.27.1`. To be removed in `0.28.0`. + - `Logger::instrumentation_scope`: This method is deprecated as of version `0.27.1`. To be removed in `0.28.0` + Migration Guidance: + - These methods are intended for log appenders. Keep the clone of the provider handle, instead of depending on above methods. + + - **Bug Fix:** Validates the `with_boundaries` bucket boundaries used in + Histograms. The boundaries provided by the user must not contain `f64::NAN`, + `f64::INFINITY` or `f64::NEG_INFINITY` and must be sorted in strictly + increasing order, and contain no duplicates. Instruments will not record + measurements if the boundaries are invalid. + [#2351](https://github.com/open-telemetry/opentelemetry-rust/pull/2351) + +- Added `with_periodic_exporter` method to `MeterProviderBuilder`, allowing + users to easily attach an exporter with a PeriodicReader for automatic metric + export. Retained with_reader() for advanced use cases where a custom + MetricReader configuration is needed. + [2597](https://github.com/open-telemetry/opentelemetry-rust/pull/2597) + Example Usage: + + ```rust + SdkMeterProvider::builder() + .with_periodic_exporter(exporter) + .build(); + ``` + + Using a custom PeriodicReader (advanced use case): + + let reader = PeriodicReader::builder(exporter).build(); + SdkMeterProvider::builder() + .with_reader(reader) + .build(); + +## 0.27.0 + +Released 2024-Nov-11 + +- Update `opentelemetry` dependency version to 0.27 +- Update `opentelemetry-http` dependency version to 0.27 + +- Bump MSRV to 1.70 [#2179](https://github.com/open-telemetry/opentelemetry-rust/pull/2179) +- Implement `LogRecord::set_trace_context` for `LogRecord`. Respect any trace context set on a `LogRecord` when emitting through a `Logger`. +- Improved `LoggerProvider` shutdown handling to prevent redundant shutdown calls when `drop` is invoked. [#2195](https://github.com/open-telemetry/opentelemetry-rust/pull/2195) +- When creating new metric instruments by calling `build()`, SDK would return a no-op instrument if the validation fails (eg: Invalid metric name). [#2166](https://github.com/open-telemetry/opentelemetry-rust/pull/2166) +- **BREAKING for Metrics users**: + - **Replaced** + - ([#2217](https://github.com/open-telemetry/opentelemetry-rust/pull/2217)): Removed `{Delta,Cumulative}TemporalitySelector::new()` in favor of directly using `Temporality` enum to simplify the configuration of MetricsExporterBuilder with different temporalities. + - **Renamed** + - ([#2232](https://github.com/open-telemetry/opentelemetry-rust/pull/2232)): The `init` method used to create instruments has been renamed to `build`. + Before: + + ```rust + let counter = meter.u64_counter("my_counter").init(); + ``` + + Now: + + ```rust + let counter = meter.u64_counter("my_counter").build(); + ``` + + - ([#2255](https://github.com/open-telemetry/opentelemetry-rust/pull/2255)): de-pluralize Metric types. + - `PushMetricsExporter` -> `PushMetricExporter` + - `InMemoryMetricsExporter` -> `InMemoryMetricExporter` + - `InMemoryMetricsExporterBuilder` -> `InMemoryMetricExporterBuilder` +- **BREAKING**: [#2220](https://github.com/open-telemetry/opentelemetry-rust/pull/2220) + - Removed `InstrumentationLibrary` re-export and its `Scope` alias, use `opentelemetry::InstrumentationLibrary` instead. + - Unified builders across signals + - Removed deprecated `LoggerProvider::versioned_logger`, `TracerProvider::versioned_tracer` + - Removed `MeterProvider::versioned_meter` + - Replaced these methods with `LoggerProvider::logger_with_scope`, `TracerProvider::logger_with_scope`, `MeterProvider::meter_with_scope` + +- [#2272](https://github.com/open-telemetry/opentelemetry-rust/pull/2272) + - Pin url version to `2.5.2`. The higher version breaks the build refer: [servo/rust-url#992.](https://github.com/servo/rust-url/issues/992) + The `url` crate is used when `jaeger_remote_sampler` feature is enabled. + +- **BREAKING**: [#2266](https://github.com/open-telemetry/opentelemetry-rust/pull/2266) + - Moved `ExportError` trait from `opentelemetry::ExportError` to `opentelemetry_sdk::export::ExportError` + - Moved `LogError` enum from `opentelemetry::logs::LogError` to `opentelemetry_sdk::logs::LogError` + - Moved `LogResult` type alias from `opentelemetry::logs::LogResult` to `opentelemetry_sdk::logs::LogResult` + - Renamed `opentelemetry::metrics::Result` type alias to `opentelemetry::metrics::MetricResult` + - Renamed `opentelemetry::metrics::MetricsError` enum to `opentelemetry::metrics::MetricError` + - Moved `MetricError` enum from `opentelemetry::metrics::MetricError` to `opentelemetry_sdk::metrics::MetricError` + - Moved `MetricResult` type alias from `opentelemetry::metrics::MetricResult` to `opentelemetry_sdk::metrics::MetricResult` + + - Users calling public APIs that return these constructs (e.g, LoggerProvider::shutdown(), MeterProvider::force_flush()) should now import them from the SDK instead of the API. + - Developers creating custom exporters should ensure they import these constructs from the SDK, not the API. + - [2291](https://github.com/open-telemetry/opentelemetry-rust/pull/2291) Rename `logs_level_enabled flag` to `spec_unstable_logs_enabled`. Please enable this updated flag if the feature is needed. This flag will be removed once the feature is stabilized in the specifications. + +- **BREAKING**: `Temporality` enum moved from `opentelemetry_sdk::metrics::data::Temporality` to `opentelemetry_sdk::metrics::Temporality`. + +- **BREAKING**: `Views` are now an opt-in ONLY feature. Please include the feature `spec_unstable_metrics_views` to enable `Views`. It will be stabilized post 1.0 stable release of the SDK. [#2295](https://github.com/open-telemetry/opentelemetry-rust/issues/2295) + +- Added a new `PeriodicReader` implementation (`PeriodicReaderWithOwnThread`) + that does not rely on an async runtime, and instead creates own Thread. This + is under feature flag "experimental_metrics_periodic_reader_no_runtime". The + functionality maybe moved into existing PeriodReader or even removed in the + future. As of today, this cannot be used as-is with OTLP Metric Exporter or + any exporter that require an async runtime. + +## v0.26.0 + +Released 2024-Sep-30 + +- Update `opentelemetry` dependency version to 0.26 +- **BREAKING** Public API changes: + - **Removed**: `SdkMeter` struct [#2113](https://github.com/open-telemetry/opentelemetry-rust/pull/2113). This API is only meant for internal use. + - **Removed**: `AggregationSelector` trait and `DefaultAggregationSelector` struct [#2085](https://github.com/open-telemetry/opentelemetry-rust/pull/2085). This API was unnecessary. The feature to customize aggregation for instruments should be offered by `Views` API. + +- Update `async-std` dependency version to 1.13 +- *Breaking* - Remove support for `MetricProducer` which allowed metrics from + external sources to be sent through OpenTelemetry. + [#2105](https://github.com/open-telemetry/opentelemetry-rust/pull/2105) +- Feature: `SimpleSpanProcessor::new` is now public [#2119](https://github.com/open-telemetry/opentelemetry-rust/pull/2119) +- For Delta Temporality, exporters are not invoked unless there were new + measurements since the last collect/export. + [#2153](https://github.com/open-telemetry/opentelemetry-rust/pull/2153) +- `MeterProvider` modified to not invoke shutdown on `Drop`, if user has already + called `shutdown()`. + [#2156](https://github.com/open-telemetry/opentelemetry-rust/pull/2156) + +## v0.25.0 + +- Update `opentelemetry` dependency version to 0.25 +- Starting with this version, this crate will align with `opentelemetry` crate + on major,minor versions. +- Perf improvements for all metric instruments (except `ExponentialHistogram`) that led to **faster metric updates** and **higher throughput** [#1740](https://github.com/open-telemetry/opentelemetry-rust/pull/1740): + - **Zero allocations when recording measurements**: Once a measurement for a given attribute combination is reported, the SDK would not allocate additional memory for subsequent measurements reported for the same combination. + - **Minimized thread contention**: Threads reporting measurements for the same instrument no longer contest for the same `Mutex`. The internal aggregation data structure now uses a combination of `RwLock` and atomics. Consequently, threads reporting measurements now only have to acquire a read lock. + - **Lock-free floating point updates**: Measurements reported for `f64` based metrics no longer need to acquire a `Mutex` to update the `f64` value. They use a CAS-based loop instead. + +- `opentelemetry_sdk::logs::record::LogRecord` and `opentelemetry_sdk::logs::record::TraceContext` derive from `PartialEq` to facilitate Unit Testing. +- Fixed an issue causing a panic during shutdown when using the + `TokioCurrentThread` in BatchExportProcessor for traces and logs. + [#1964](https://github.com/open-telemetry/opentelemetry-rust/pull/1964) + [#1973](https://github.com/open-telemetry/opentelemetry-rust/pull/1973) +- Fix BatchExportProcessor for traces and logs to trigger first export at the + first interval instead of doing it right away. + [#1970](https://github.com/open-telemetry/opentelemetry-rust/pull/1970) + [#1973](https://github.com/open-telemetry/opentelemetry-rust/pull/1973) + - *Breaking* [#1985](https://github.com/open-telemetry/opentelemetry-rust/pull/1985) + Hide LogRecord attributes Implementation Details from processors and exporters. + The custom exporters and processors can't directly access the `LogData::LogRecord::attributes`, as + these are private to opentelemetry-sdk. Instead, they would now use LogRecord::attributes_iter() + method to access them. +- Fixed various Metric aggregation bug related to + ObservableCounter,UpDownCounter including + [#1517](https://github.com/open-telemetry/opentelemetry-rust/issues/1517). + [#2004](https://github.com/open-telemetry/opentelemetry-rust/pull/2004) +- Fixed a bug related to cumulative aggregation of `Gauge` measurements. + [#1975](https://github.com/open-telemetry/opentelemetry-rust/issues/1975). + [#2021](https://github.com/open-telemetry/opentelemetry-rust/pull/2021) +- Provide default implementation for `event_enabled` method in `LogProcessor` + trait that returns `true` always. +- *Breaking* [#2041](https://github.com/open-telemetry/opentelemetry-rust/pull/2041) + and [#2057](https://github.com/open-telemetry/opentelemetry-rust/pull/2057) + - The Exporter::export() interface is modified as below: + Previous Signature: + + ```rust + async fn export<'a>(&mut self, batch: Vec>) -> LogResult<()>; + ``` + + Updated Signature: + + ```rust + async fn export(&mut self, batch: LogBatch<'_>) -> LogResult<()>; + ``` + + where + + ```rust + pub struct LogBatch<'a> { + + data: &'a [(&'a LogRecord, &'a InstrumentationLibrary)], + } + ``` + + This change enhances performance by reducing unnecessary heap allocations and maintains object safety, allowing for more efficient handling of log records. It also simplifies the processing required by exporters. Exporters no longer need to determine if the LogData is borrowed or owned, as they now work directly with references. As a result, exporters must explicitly create a copy of LogRecord and/or InstrumentationLibrary when needed, as the new interface only provides references to these structures. + +## v0.24.1 + +- Add hidden method to support tracing-opentelemetry + +## v0.24.0 + +- Add "metrics", "logs" to default features. With this, default feature list is + "trace", "metrics" and "logs". +- Add `with_resource` on Builder for LoggerProvider, replacing the `with_config` + method. Instead of using + `.with_config(Config::default().with_resource(RESOURCE::default()))` users + must now use `.with_resource(RESOURCE::default())` to configure Resource on + logger provider. +- Removed dependency on `ordered-float`. +- Removed `XrayIdGenerator`, which was marked deprecated since 0.21.3. Use + [`opentelemetry-aws`](https://crates.io/crates/opentelemetry-aws), version + 0.10.0 or newer. +- Performance Improvement - Counter/UpDownCounter instruments internally use + `RwLock` instead of `Mutex` to reduce contention + +- *Breaking* [1726](https://github.com/open-telemetry/opentelemetry-rust/pull/1726) + Update `LogProcessor::emit()` method to take mutable reference to LogData. This is breaking + change for LogProcessor developers. If the processor needs to invoke the exporter + asynchronously, it should clone the data to ensure it can be safely processed without + lifetime issues. Any changes made to the log data before cloning in this method will be reflected in the next log processor in the chain, as well as to the exporter. +- *Breaking* [1726](https://github.com/open-telemetry/opentelemetry-rust/pull/1726) + Update `LogExporter::export()` method to accept a batch of log data, which can be either a + reference or owned`LogData`. If the exporter needs to process the log data + asynchronously, it should clone the log data to ensure it can be safely processed without + lifetime issues. +- Clean up public methods in SDK. + - [`TracerProvider::span_processors`] and [`TracerProvider::config`] was removed as it's not part of the spec. + - Added `non_exhaustive` annotation to [`trace::Config`]. Marked [`config`] as deprecated since it's only a wrapper for `Config::default` + - Removed [`Tracer::tracer_provider`] and [`Tracer::instrument_libraries`] as it's not part of the spec. + +- *Breaking* [#1830](https://github.com/open-telemetry/opentelemetry-rust/pull/1830/files) [Traces SDK] Improves + performance by sending Resource information to processors (and exporters) once, instead of sending with every log. If you are an author + of Processor, Exporter, the following are *BREAKING* changes. + - Implement `set_resource` method in your custom SpanProcessor, which invokes exporter's `set_resource`. + - Implement `set_resource` method in your custom SpanExporter. This method should save the resource object + in original or serialized format, to be merged with every span event during export. + - `SpanData` doesn't have the resource attributes. The `SpanExporter::export()` method needs to merge it + with the earlier preserved resource before export. + +- *Breaking* [1836](https://github.com/open-telemetry/opentelemetry-rust/pull/1836) `SpanProcessor::shutdown` now takes an immutable reference to self. Any reference can call shutdown on the processor. After the first call to `shutdown` the processor will not process any new spans. + +- *Breaking* [1850] () `LoggerProvider::log_processors()` and `LoggerProvider::resource()` are not public methods anymore. They are only used within the `opentelemetry-sdk` crate. + +- [1857](https://github.com/open-telemetry/opentelemetry-rust/pull/1857) Fixed an issue in Metrics SDK which prevented export errors from being send to global error handler. With the fix, errors occurring during export like OTLP Endpoint unresponsive shows up in stderr by default. + +- [1869](https://github.com/open-telemetry/opentelemetry-rust/pull/1869) Added a `target` field to `LogRecord` structure, populated by `opentelemetry-appender-tracing` and `opentelemetry-appender-log` appenders. + +```rust +async fn export<'a>(&mut self, batch: Vec>) -> LogResult<()>; +``` + +where `LogRecord` within `LogData` now includes: + +```rust +LogData { + LogRecord { + event_name, + target, // newly added + timestamp, + observed_timestamp, + trace_context, + trace_context, + severity_number, + body, + attributes + } + Instrumentation { + name, + version, + schema_url, + version + } +} +``` + +The `LogRecord::target` field contains the actual target/component emitting the logs, while the `Instrumentation::name` contains the name of the OpenTelemetry appender. + +- *Breaking* [#1674](https://github.com/open-telemetry/opentelemetry-rust/pull/1674) Update to `http` v1 types (via `opentelemetry-http` update) +- Update `opentelemetry` dependency version to 0.24 +- Update `opentelemetry-http` dependency version to 0.13 + +## v0.23.0 + +- Fix SimpleSpanProcessor to be consistent with log counterpart. Also removed + dependency on crossbeam-channel. + [1612](https://github.com/open-telemetry/opentelemetry-rust/pull/1612/files) +- [#1422](https://github.com/open-telemetry/opentelemetry-rust/pull/1422) + Fix metrics aggregation bug when using Views to drop attributes. +- [#1766](https://github.com/open-telemetry/opentelemetry-rust/pull/1766) + Fix Metrics PeriodicReader to trigger first collect/export at the first interval + instead of doing it right away. +- [#1623](https://github.com/open-telemetry/opentelemetry-rust/pull/1623) Add Drop implementation for SdkMeterProvider, + which shuts down `MetricReader`s, thereby allowing metrics still in memory to be flushed out. +- *Breaking* [#1624](https://github.com/open-telemetry/opentelemetry-rust/pull/1624) Remove `OsResourceDetector` and + `ProcessResourceDetector` resource detectors, use the + [`opentelemetry-resource-detector`](https://crates.io/crates/opentelemetry-resource-detectors) instead. +- [#1636](https://github.com/open-telemetry/opentelemetry-rust/pull/1636) [Logs SDK] Improves performance by sending + Resource information to processors (and exporters) once, instead of sending with every log. If you are an author + of Processor, Exporter, the following are *BREAKING* changes. + - Implement `set_resource` method in your custom LogProcessor, which invokes exporter's `set_resource`. + - Implement `set_resource` method in your custom LogExporter. This method should save the resource object + in original or serialized format, to be merged with every log event during export. + - `LogData` doesn't have the resource attributes. The `LogExporter::export()` method needs to merge it + with the earlier preserved resource before export. +- Baggage propagation error will be reported to global error handler [#1640](https://github.com/open-telemetry/opentelemetry-rust/pull/1640) +- Improves `shutdown` behavior of `LoggerProvider` and `LogProcessor` [#1643](https://github.com/open-telemetry/opentelemetry-rust/pull/1643). + - `shutdown` can be called by any clone of the `LoggerProvider` without the need of waiting on all `Logger` drops. Thus, `try_shutdown` has been removed. + - `shutdown` methods in `LoggerProvider` and `LogProcessor` now takes a immutable reference + - After `shutdown`, `LoggerProvider` will return noop `Logger` + - After `shutdown`, `LogProcessor` will not process any new logs +- Moving LogRecord implementation to the SDK. [1702](https://github.com/open-telemetry/opentelemetry-rust/pull/1702). + - Relocated `LogRecord` struct to SDK, as an implementation for the trait in the API. +- *Breaking* [#1729](https://github.com/open-telemetry/opentelemetry-rust/pull/1729) + - Update the return type of `TracerProvider.span_processors()` from `&Vec>` to `&[Box]`. + - Update the return type of `LoggerProvider.log_processors()` from `&Vec>` to `&[Box]`. +- Update `opentelemetry` dependency version to 0.23 +- Update `opentelemetry-http` dependency version to 0.12 +- *Breaking* [#1750](https://github.com/open-telemetry/opentelemetry-rust/pull/1729) + - Update the return type of `LoggerProvider.shutdown()` from `Vec>` to `LogResult<()>`. + +## v0.22.1 + +### Fixed + +- [#1576](https://github.com/open-telemetry/opentelemetry-rust/pull/1576) + Fix Span kind is always set to "internal". + +## v0.22.0 + +### Deprecated + +- XrayIdGenerator in the opentelemetry-sdk has been deprecated and moved to version 0.10.0 of the opentelemetry-aws crate. + +### Added + +- [#1410](https://github.com/open-telemetry/opentelemetry-rust/pull/1410) Add experimental synchronous gauge +- [#1471](https://github.com/open-telemetry/opentelemetry-rust/pull/1471) Configure batch log record processor via [`OTEL_BLRP_*`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#batch-logrecord-processor) environment variables and via `OtlpLogPipeline::with_batch_config` +- [#1503](https://github.com/open-telemetry/opentelemetry-rust/pull/1503) Make the documentation for In-Memory exporters visible. + +- [#1526](https://github.com/open-telemetry/opentelemetry-rust/pull/1526) +Performance Improvement : Creating Spans and LogRecords are now faster, by avoiding expensive cloning of `Resource` for every Span/LogRecord. + +### Changed + +- *Breaking* +[#1313](https://github.com/open-telemetry/opentelemetry-rust/pull/1313) +[#1350](https://github.com/open-telemetry/opentelemetry-rust/pull/1350) + Changes how Span links/events are stored to achieve performance gains. See + below for details: + + *Behavior Change*: When enforcing `max_links_per_span`, `max_events_per_span` + from `SpanLimits`, links/events are kept in the first-come order. The previous + "eviction" based approach is no longer performed. + + *Breaking Change Affecting Exporter authors*: + + `SpanData` now stores `links` as `SpanLinks` instead of `EvictedQueue` where + `SpanLinks` is a struct with a `Vec` of links and `dropped_count`. + + `SpanData` now stores `events` as `SpanEvents` instead of `EvictedQueue` where + `SpanEvents` is a struct with a `Vec` of events and `dropped_count`. +- *Breaking* Remove `TextMapCompositePropagator` [#1373](https://github.com/open-telemetry/opentelemetry-rust/pull/1373). Use `TextMapCompositePropagator` in opentelemetry API. + +- [#1375](https://github.com/open-telemetry/opentelemetry-rust/pull/1375/) Fix metric collections during PeriodicReader shutdown +- *Breaking* [#1480](https://github.com/open-telemetry/opentelemetry-rust/pull/1480) Remove fine grained `BatchConfig` configurations from `BatchLogProcessorBuilder` and `BatchSpanProcessorBuilder`. Use `BatchConfigBuilder` to construct a `BatchConfig` instance and pass it using `BatchLogProcessorBuilder::with_batch_config` or `BatchSpanProcessorBuilder::with_batch_config`. +- *Breaking* [#1480](https://github.com/open-telemetry/opentelemetry-rust/pull/1480) Remove mutating functions from `BatchConfig`, use `BatchConfigBuilder` to construct a `BatchConfig` instance. +- *Breaking* [#1495](https://github.com/open-telemetry/opentelemetry-rust/pull/1495) Remove Batch LogRecord&Span Processor configuration via non-standard environment variables. Use the following table to migrate from the no longer supported non-standard environment variables to the standard ones. + +| No longer supported | Standard equivalent | +|---------------------------------|---------------------------| +| OTEL_BLRP_SCHEDULE_DELAY_MILLIS | OTEL_BLRP_SCHEDULE_DELAY | +| OTEL_BLRP_EXPORT_TIMEOUT_MILLIS | OTEL_BLRP_EXPORT_TIMEOUT | +| OTEL_BSP_SCHEDULE_DELAY_MILLIS | OTEL_BSP_SCHEDULE_DELAY | +| OTEL_BSP_EXPORT_TIMEOUT_MILLIS | OTEL_BSP_EXPORT_TIMEOUT | + +- *Breaking* [#1455](https://github.com/open-telemetry/opentelemetry-rust/pull/1455) Make the LoggerProvider Owned + - `Logger` now takes an Owned Logger instead of a `Weak` + - `LoggerProviderInner` is no longer `pub (crate)` + - `Logger.provider()` now returns `&LoggerProvider` instead of an `Option` + +- [#1519](https://github.com/open-telemetry/opentelemetry-rust/pull/1519) Performance improvements + when calling `Counter::add()` and `UpDownCounter::add()` with an empty set of attributes + (e.g. `counter.Add(5, &[])`) + +- *Breaking* Renamed `MeterProvider` and `Meter` to `SdkMeterProvider` and `SdkMeter` respectively to avoid name collision with public API types. [#1328](https://github.com/open-telemetry/opentelemetry-rust/pull/1328) + +### Fixed + +- [#1481](https://github.com/open-telemetry/opentelemetry-rust/pull/1481) Fix error message caused by race condition when using PeriodicReader + +## v0.21.2 + +### Fixed + +- Fix delta aggregation metric reuse. [#1434](https://github.com/open-telemetry/opentelemetry-rust/pull/1434) +- Fix `max_scale` validation of exponential histogram configuration. [#1452](https://github.com/open-telemetry/opentelemetry-rust/pull/1452) + +## v0.21.1 + +### Fixed + +- Fix metric export corruption if gauges have not received a last value. [#1363](https://github.com/open-telemetry/opentelemetry-rust/pull/1363) +- Return consistent `Meter` for a given scope from `MeterProvider`. [#1351](https://github.com/open-telemetry/opentelemetry-rust/pull/1351) + +## v0.21.0 + +### Added + +- Log warning if two instruments have the same name with different [#1266](https://github.com/open-telemetry/opentelemetry-rust/pull/1266) + casing +- Log warning if view is created with empty criteria [#1266](https://github.com/open-telemetry/opentelemetry-rust/pull/1266) +- Add exponential histogram support [#1267](https://github.com/open-telemetry/opentelemetry-rust/pull/1267) +- Add `opentelemetry::sdk::logs::config()` for parity with `opentelemetry::sdk::trace::config()` [#1197](https://github.com/open-telemetry/opentelemetry-rust/pull/1197) + +### Changed + +- Bump MSRV to 1.65 [#1318](https://github.com/open-telemetry/opentelemetry-rust/pull/1318) +- Default Resource (the one used when no other Resource is explicitly provided) now includes `TelemetryResourceDetector`, + populating "telemetry.sdk.*" attributes. + [#1194](https://github.com/open-telemetry/opentelemetry-rust/pull/1194). +- Bump MSRV to 1.64 [#1203](https://github.com/open-telemetry/opentelemetry-rust/pull/1203) +- Add unit/doc tests for MeterProvider [#1220](https://github.com/open-telemetry/opentelemetry-rust/pull/1220) +- Changed dependency from `opentelemetry_api` to `opentelemetry` as the latter + is now the API crate. [#1226](https://github.com/open-telemetry/opentelemetry-rust/pull/1226) +- Add in memory span exporter [#1216](https://github.com/open-telemetry/opentelemetry-rust/pull/1216) +- Add in memory log exporter [#1231](https://github.com/open-telemetry/opentelemetry-rust/pull/1231) +- Add `Sync` bound to the `SpanExporter` and `LogExporter` traits [#1240](https://github.com/open-telemetry/opentelemetry-rust/pull/1240) +- Move `MetricsProducer` config to builders to match other config [#1266](https://github.com/open-telemetry/opentelemetry-rust/pull/1266) +- Return error earlier if readers are shut down [#1266](https://github.com/open-telemetry/opentelemetry-rust/pull/1266) +- Add `/` to valid characters for instrument names [#1269](https://github.com/open-telemetry/opentelemetry-rust/pull/1269) +- Increase instrument name maximum length from 63 to 255 [#1269](https://github.com/open-telemetry/opentelemetry-rust/pull/1269) +- Updated crate documentation and examples. + [#1256](https://github.com/open-telemetry/opentelemetry-rust/issues/1256) +- Replace regex with glob [#1301](https://github.com/open-telemetry/opentelemetry-rust/pull/1301) +- *Breaking* + [#1293](https://github.com/open-telemetry/opentelemetry-rust/issues/1293) + makes few breaking changes with respect to how Span attributes are stored to + achieve performance gains. See below for details: + + *Behavior Change*: + + SDK will no longer perform de-duplication of Span attribute Keys. Please share + [feedback + here](https://github.com/open-telemetry/opentelemetry-rust/issues/1300), if + you are affected. + + *Breaking Change Affecting Exporter authors*: + + `SpanData` now stores `attributes` as `Vec` instead of + `EvictedHashMap`. `SpanData` now expose `dropped_attributes_count` as a + separate field. + + *Breaking Change Affecting Sampler authors*: + + `should_sample` changes `attributes` from `OrderMap` to + `Vec`. +- *Breaking* Move type argument from `RuntimeChannel` to associated types [#1314](https://github.com/open-telemetry/opentelemetry-rust/pull/1314) + +### Removed + +- Remove context from Metric force_flush [#1245](https://github.com/open-telemetry/opentelemetry-rust/pull/1245) +- Remove `logs::BatchMessage` and `trace::BatchMessage` types [#1314](https://github.com/open-telemetry/opentelemetry-rust/pull/1314) + +### Fixed + +- Fix metric instrument name validation to include `_` [#1274](https://github.com/open-telemetry/opentelemetry-rust/pull/1274) + +## v0.20.0 + +### Added + +- Implement cardinality limits for metric streams + [#1066](https://github.com/open-telemetry/opentelemetry-rust/pull/1066). +- Propagate shutdown calls from `PeriodicReader` to metrics exporter + [#1138](https://github.com/open-telemetry/opentelemetry-rust/pull/1138). +- Add in memory metrics exporter #1017 + +### Changed + +- New metrics SDK #1000 +- Use `Cow<'static, str>` instead of `&'static str` #1018 +- Unify trace and logs runtime extensions traits. #1067 + +### Changed + +- Fix EvictedQueue bug when capacity is set to 0 + [#1151](https://github.com/open-telemetry/opentelemetry-rust/pull/1151). + +### Removed + +- Samplers no longer has access to `InstrumentationLibrary` as one of parameters + to `should_sample`. + [#1041](https://github.com/open-telemetry/opentelemetry-rust/pull/1041). +- Synchronous instruments no longer accepts `Context` while reporting + measurements. [#1076](https://github.com/open-telemetry/opentelemetry-rust/pull/1076). +- Don't use CARGO_BIN_NAME for service name #1150 + +### Fixed + +- Wait for exports on the simple span processor's ForceFlush #1030 + +## v0.19.0 + +### Added + +- Add instrument validation to `InstrumentBuilder` [#884](https://github.com/open-telemetry/opentelemetry-rust/pull/884). +- Add `TelemetryResourceDetector` [#899](https://github.com/open-telemetry/opentelemetry-rust/pull/899). +- Add support for instrumentation scope attributes [#1021](https://github.com/open-telemetry/opentelemetry-rust/pull/1021). + +### Changed + +- Update to `opentelemetry_api` v0.19. +- Update to `opentelemetry_http` v0.8. +- Bump MSRV to 1.57 [#953](https://github.com/open-telemetry/opentelemetry-rust/pull/953). +- Fix doc in `ShouldSample` trait [#951](https://github.com/open-telemetry/opentelemetry-rust/pull/951) +- Only run `ParentBased` delegate sampler when there is no parent [#948](https://github.com/open-telemetry/opentelemetry-rust/pull/948). +- Improve `SdkProvidedResourceDetector`'s doc [#964](https://github.com/open-telemetry/opentelemetry-rust/pull/964). +- Update dependencies and bump MSRV to 1.60 [#969](https://github.com/open-telemetry/opentelemetry-rust/pull/969). +- Use CARGO_BIN_NAME as default service name [#991](https://github.com/open-telemetry/opentelemetry-rust/pull/991). + +### Removed + +- Remove `in_memory` settings [#946](https://github.com/open-telemetry/opentelemetry-rust/pull/946). + +## main + +### Changed + +- Update the Number in the SDK API to support min and max. #989 + +## v0.18.0 + +### Changed + +- *BREAKING* `struct`s which implement `ShouldSample` a.k.a Custom Samplers must now + implement `Clone`. This enables (#833) +- SDK split from `opentelemetry` crate diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/Cargo.toml b/vendor/opentelemetry-rust/opentelemetry_sdk/Cargo.toml new file mode 100644 index 00000000..23ed78dd --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/Cargo.toml @@ -0,0 +1,348 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.75.0" +name = "opentelemetry_sdk" +version = "0.33.0" +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "The SDK for the OpenTelemetry metrics collection and distributed tracing framework" +homepage = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-sdk" +readme = "README.md" +license = "Apache-2.0" +repository = "https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-sdk" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = [ + "--cfg", + "docsrs", +] + +[features] +bench_profiling = [] +default = [ + "trace", + "metrics", + "logs", + "internal-logs", +] +experimental_async_runtime = [ + "dep:futures-channel", + "dep:futures-executor", + "dep:futures-util", + "dep:thiserror", +] +experimental_logs_batch_log_processor_with_async_runtime = [ + "logs", + "experimental_async_runtime", +] +experimental_metrics_bound_instruments = [ + "metrics", + "opentelemetry/experimental_metrics_bound_instruments", +] +experimental_metrics_custom_reader = ["metrics"] +experimental_metrics_disable_name_validation = ["metrics"] +experimental_metrics_periodicreader_with_async_runtime = [ + "metrics", + "experimental_async_runtime", +] +experimental_trace_batch_span_processor_with_async_runtime = [ + "tokio/sync", + "trace", + "experimental_async_runtime", +] +internal-logs = ["opentelemetry/internal-logs"] +jaeger_remote_sampler = [ + "trace", + "opentelemetry-http", + "http", + "serde", + "serde_json", + "url", + "experimental_async_runtime", +] +logs = [ + "opentelemetry/logs", + "dep:futures-channel", + "dep:futures-executor", + "dep:futures-util", +] +metrics = [ + "opentelemetry/metrics", + "dep:futures-channel", + "dep:futures-executor", + "dep:futures-util", + "dep:thiserror", +] +rt-tokio = [ + "tokio/rt", + "tokio/time", + "tokio-stream", + "experimental_async_runtime", +] +rt-tokio-current-thread = [ + "tokio/rt", + "tokio/time", + "tokio-stream", + "experimental_async_runtime", +] +spec_unstable_metrics_views = ["metrics"] +testing = [ + "opentelemetry/testing", + "trace", + "metrics", + "logs", + "tokio/sync", +] +trace = [ + "opentelemetry/trace", + "rand", + "percent-encoding", + "dep:futures-channel", + "dep:futures-executor", + "dep:futures-util", + "dep:thiserror", +] + +[lib] +name = "opentelemetry_sdk" +path = "src/lib.rs" +bench = false + +[[test]] +name = "miri_smoke" +path = "tests/miri_smoke.rs" + +[[test]] +name = "otel_default_constants" +path = "tests/otel_default_constants.rs" + +[[bench]] +name = "batch_span_processor" +path = "benches/batch_span_processor.rs" +harness = false +required-features = ["testing"] + +[[bench]] +name = "bound_instruments" +path = "benches/bound_instruments.rs" +harness = false +required-features = [ + "metrics", + "experimental_metrics_custom_reader", + "experimental_metrics_bound_instruments", + "spec_unstable_metrics_views", +] + +[[bench]] +name = "context" +path = "benches/context.rs" +harness = false + +[[bench]] +name = "log" +path = "benches/log.rs" +harness = false +required-features = ["logs"] + +[[bench]] +name = "log_enabled" +path = "benches/log_enabled.rs" +harness = false + +[[bench]] +name = "log_exporter" +path = "benches/log_exporter.rs" +harness = false + +[[bench]] +name = "log_processor" +path = "benches/log_processor.rs" +harness = false + +[[bench]] +name = "metric" +path = "benches/metric.rs" +harness = false +required-features = [ + "metrics", + "spec_unstable_metrics_views", + "experimental_metrics_custom_reader", +] + +[[bench]] +name = "metrics_counter" +path = "benches/metrics_counter.rs" +harness = false +required-features = ["experimental_metrics_custom_reader"] + +[[bench]] +name = "metrics_gauge" +path = "benches/metrics_gauge.rs" +harness = false +required-features = ["experimental_metrics_custom_reader"] + +[[bench]] +name = "metrics_histogram" +path = "benches/metrics_histogram.rs" +harness = false +required-features = ["experimental_metrics_custom_reader"] + +[[bench]] +name = "span" +path = "benches/span.rs" +harness = false + +[[bench]] +name = "span_builder" +path = "benches/span_builder.rs" +harness = false + +[[bench]] +name = "trace" +path = "benches/trace.rs" +harness = false + +[[bench]] +name = "tracer_creation" +path = "benches/tracer_creation.rs" +harness = false + +[dependencies.futures-channel] +version = "0.3" +optional = true + +[dependencies.futures-executor] +version = "0.3" +optional = true + +[dependencies.futures-util] +version = "0.3" +features = [ + "std", + "sink", + "async-await-macro", +] +optional = true +default-features = false + +[dependencies.http] +version = "1.1" +features = ["std"] +optional = true +default-features = false + +[dependencies.opentelemetry] +version = "0.33" +default-features = false + +[dependencies.opentelemetry-http] +version = "0.33" +optional = true +default-features = false + +[dependencies.percent-encoding] +version = "2.0" +optional = true + +[dependencies.rand] +version = "0.9" +features = [ + "std", + "std_rng", + "small_rng", + "os_rng", + "thread_rng", +] +optional = true +default-features = false + +[dependencies.serde] +version = "1.0" +features = [ + "derive", + "rc", +] +optional = true +default-features = false + +[dependencies.serde_json] +version = "1.0" +optional = true + +[dependencies.thiserror] +version = "2" +optional = true +default-features = false + +[dependencies.tokio] +version = "1" +optional = true +default-features = false + +[dependencies.tokio-stream] +version = "0.1" +optional = true + +[dependencies.url] +version = "2.5" +optional = true +default-features = false + +[dev-dependencies.criterion] +version = "0.5" +features = ["html_reports"] + +[dev-dependencies.rstest] +version = "0.26.1" + +[dev-dependencies.temp-env] +version = "0.3.6" + +[dev-dependencies.tokio] +version = "1" +features = [ + "macros", + "rt-multi-thread", +] +default-features = false + +[target.'cfg(not(target_has_atomic = "64"))'.dependencies.portable-atomic] +version = "1" +features = ["fallback"] +default-features = false + +[target.'cfg(not(target_os = "windows"))'.dev-dependencies.pprof] +version = "0.14" +features = [ + "flamegraph", + "criterion", +] + +[lints.clippy.all] +level = "warn" +priority = 1 + +[lints.rust] +edition_2024_expr_fragment_specifier = "allow" +if_let_rescope = "allow" +tail_expr_drop_order = "allow" + +[lints.rust.rust_2024_compatibility] +level = "warn" +priority = -1 diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/README.md b/vendor/opentelemetry-rust/opentelemetry_sdk/README.md new file mode 100644 index 00000000..5a723e88 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/README.md @@ -0,0 +1,115 @@ +# OpenTelemetry Rust SDK + +![OpenTelemetry — An observability framework for cloud-native software.][splash] + +[splash]: https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo-text.png + +This crate contains the [OpenTelemetry](https://opentelemetry.io/) SDK +implementation for Rust. + +[![Crates.io: opentelemetry-sdk](https://img.shields.io/crates/v/opentelemetry_sdk.svg)](https://crates.io/crates/opentelemetry_sdk) +[![Documentation](https://docs.rs/opentelemetry_sdk/badge.svg)](https://docs.rs/opentelemetry_sdk) +[![LICENSE](https://img.shields.io/crates/l/opentelemetry_sdk)](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-sdk/LICENSE) +[![GitHub Actions CI](https://github.com/open-telemetry/opentelemetry-rust/workflows/CI/badge.svg)](https://github.com/open-telemetry/opentelemetry-rust/actions?query=workflow%3ACI+branch%3Amain) +[![codecov](https://codecov.io/gh/open-telemetry/opentelemetry-rust/branch/main/graph/badge.svg)](https://codecov.io/gh/open-telemetry/opentelemetry-rust) +[![Slack](https://img.shields.io/badge/slack-@cncf/otel/rust-brightgreen.svg?logo=slack)](https://cloud-native.slack.com/archives/C03GDP0H023) + +## Overview + +OpenTelemetry is an Observability framework and toolkit designed to create and +manage telemetry data such as traces, metrics, and logs. OpenTelemetry is +vendor- and tool-agnostic, meaning that it can be used with a broad variety of +Observability backends, including open source tools like [Jaeger] and +[Prometheus], as well as commercial offerings. + +OpenTelemetry is *not* an observability backend like Jaeger, Prometheus, or other +commercial vendors. OpenTelemetry is focused on the generation, collection, +management, and export of telemetry. A major goal of OpenTelemetry is that you +can easily instrument your applications or systems, no matter their language, +infrastructure, or runtime environment. Crucially, the storage and visualization +of telemetry is intentionally left to other tools. + +*[Supported Rust Versions](#supported-rust-versions)* + +[Prometheus]: https://prometheus.io +[Jaeger]: https://www.jaegertracing.io + +### What does this crate contain? + +This crate is official SDK implementation of OpenTelemetry encompassing several +aspects of OpenTelemetry, such as context management and propagation, logging, +tracing, and metrics. It follows the [OpenTelemetry +specification](https://github.com/open-telemetry/opentelemetry-specification). +Here's a breakdown of its components: + +- **[Propagators + Implementation](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md):** + While the `opentelemetry` crate contained the API, this crate contains the actual implementation. +- **[Logs SDK](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/sdk.md):** + Implements the Logs SDK specification. +- **[Tracing + SDK](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md):** + Implements the Tracing SDK specification. +- **[Metrics + SDK](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md):** + Implements the Metrics SDK specification. + +This crate lights up the telemetry, by replacing the facade or no-op +implementation from `opentelemetry` crate. In many ways, one can think of +`opentelemetry` as the crate containing the "traits" along with a no-op +implementation, and this (`opentelemetry-sdk`) crate containing a real +implementation to replace the default no-ops. + +This crate defines the telemetry pipeline, and makes telemetry available for +processors etc., but the actual exporting of telemetry requires additional +crates, such as +[opentelemetry-stdout](https://crates.io/crates/opentelemetry-stdout), +[opentelemetry-otlp](https://crates.io/crates/opentelemetry-otlp) etc. + +### Related crates + +Unless you are a plugin (custom Samplers, Processors etc.) author, you will almost always need to use additional +crates along with this. Given this crate has no exporting capability, an +OpenTelemetry Exporter is almost always required. OpenTelemetry provides the following exporters: + +- **[opentelemetry-stdout](https://crates.io/crates/opentelemetry-stdout):** + Prints telemetry to stdout, primarily used for learning/debugging purposes. +- **[opentelemetry-otlp](https://crates.io/crates/opentelemetry-otlp):** Exports + telemetry (logs, metrics and traces) in the [OTLP + format](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/protocol) + to an endpoint accepting OTLP. This could be the [OTel + Collector](https://github.com/open-telemetry/opentelemetry-collector), + telemetry backends like [Jaeger](https://www.jaegertracing.io/), + [Prometheus](https://prometheus.io/docs/prometheus/latest/feature_flags/#otlp-receiver) + or [vendor specific endpoints](https://opentelemetry.io/ecosystem/vendors/). +- **[opentelemetry-prometheus](https://crates.io/crates/opentelemetry-prometheus):** + Exports telemetry (metrics only) to Prometheus following [OpenTelemetry to + Prometheus + specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk_exporters/prometheus.md). + +OpenTelemetry Rust also has a [contrib +repo](https://github.com/open-telemetry/opentelemetry-rust-contrib), where +additional exporters could be found. Check [OpenTelemetry +Registry](https://opentelemetry.io/ecosystem/registry/?language=rust) for +additional exporters and other related components as well. + +## Getting started + +See [docs](https://docs.rs/opentelemetry-sdk). + +## Release Notes + +You can find the release notes (changelog) [here](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-sdk/CHANGELOG.md). + +## Supported Rust Versions + +OpenTelemetry is built against the latest stable release. The minimum supported +version is 1.75.0. The current OpenTelemetry version is NOT guaranteed to build +on Rust versions earlier than the minimum supported version. + +The current stable Rust compiler and the three most recent minor versions +before it will always be supported. For example, if the current stable compiler +version is 1.49, the minimum supported version will not be increased past 1.46, +three minor versions prior. Increasing the minimum supported compiler version +is not considered a semver breaking change as long as doing so complies with +this policy. diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/allowed-external-types.toml b/vendor/opentelemetry-rust/opentelemetry_sdk/allowed-external-types.toml new file mode 100644 index 00000000..11d11b89 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/allowed-external-types.toml @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# This is used with cargo-check-external-types to reduce the surface area of downstream crates from +# the public API. Ideally this can have a few exceptions as possible. +allowed_external_types = [ + "opentelemetry::*", + "async_channel::Receiver", + "async_channel::Sender", + "async_std::stream::interval::Interval", + "futures_channel::oneshot::Sender", # TODO: This is a pre-1.0 crate, we can't easily stabilize with this in the public API + "futures_core::future::BoxFuture", # TODO: This is a pre-1.0 crate, we can't easily stabilize with this in the public API + "futures_core::stream::Stream", # TODO: This is a pre-1.0 crate, we can't easily stabilize with this in the public API + "opentelemetry_http::HttpClient", # TODO: We probably shouldn't be depending on another SDK not in the API from the SDK. + "tokio_stream::wrappers::interval::IntervalStream", # TODO: This is a pre-1.0 crate, we can't easily stabilize with this in the public API + "tokio_stream::wrappers::mpsc_bounded::ReceiverStream", # TODO: This is a pre-1.0 crate, we can't easily stabilize with this in the public API + "tokio::sync::mpsc::bounded::Sender", + "tokio::time::sleep::Sleep", +] diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/batch_span_processor.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/batch_span_processor.rs new file mode 100644 index 00000000..6e90b9a6 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/batch_span_processor.rs @@ -0,0 +1,91 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use opentelemetry::time::now; +use opentelemetry::trace::{ + SpanContext, SpanId, SpanKind, Status, TraceFlags, TraceId, TraceState, +}; +use opentelemetry_sdk::testing::trace::NoopSpanExporter; +use opentelemetry_sdk::trace::SpanData; +use opentelemetry_sdk::trace::{ + BatchConfigBuilder, BatchSpanProcessor, SpanEvents, SpanLinks, SpanProcessor, +}; +use std::sync::Arc; +use tokio::runtime::Runtime; + +fn get_span_data() -> Vec { + (0..200) + .map(|_| SpanData { + span_context: SpanContext::new( + TraceId::from(12), + SpanId::from(12), + TraceFlags::default(), + false, + TraceState::default(), + ), + parent_span_id: SpanId::from(12), + parent_span_is_remote: false, + span_kind: SpanKind::Client, + name: Default::default(), + start_time: now(), + end_time: now(), + attributes: Vec::new(), + dropped_attributes_count: 0, + events: SpanEvents::default(), + links: SpanLinks::default(), + status: Status::Unset, + instrumentation_scope: Default::default(), + }) + .collect::>() +} + +fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("BatchSpanProcessor"); + group.sample_size(50); + + for task_num in [1, 2, 4, 8, 16, 32].iter() { + group.bench_with_input( + BenchmarkId::from_parameter(format!("with {task_num} concurrent task")), + task_num, + |b, &task_num| { + b.iter(|| { + let rt = Runtime::new().unwrap(); + rt.block_on(async move { + let span_processor = BatchSpanProcessor::builder(NoopSpanExporter::new()) + .with_batch_config( + BatchConfigBuilder::default() + .with_max_queue_size(10_000) + .build(), + ) + .build(); + let mut shared_span_processor = Arc::new(span_processor); + let mut handles = Vec::with_capacity(10); + for _ in 0..task_num { + let span_processor = shared_span_processor.clone(); + let spans = get_span_data(); + handles.push(tokio::spawn(async move { + for span in spans { + span_processor.on_end(span); + tokio::task::yield_now().await; + } + })); + } + futures_util::future::join_all(handles).await; + let _ = Arc::::get_mut(&mut shared_span_processor) + .unwrap() + .shutdown(); + }); + }) + }, + ); + } + + group.finish(); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/bound_instruments.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/bound_instruments.rs new file mode 100644 index 00000000..4080d7cd --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/bound_instruments.rs @@ -0,0 +1,321 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use opentelemetry::{metrics::MeterProvider as _, Key, KeyValue}; +use opentelemetry_sdk::metrics::{Instrument, ManualReader, SdkMeterProvider, Stream, Temporality}; + +// Run this benchmark with: +// cargo bench --bench bound_instruments --features metrics,experimental_metrics_custom_reader,experimental_metrics_bound_instruments,spec_unstable_metrics_views +// +// Apple M4 Max, 16 cores (12 performance + 4 efficiency), macOS 15.4 +// +// Results (3 attributes: method, status, path): +// Counter_Unbound_Delta time: [50.20 ns] +// Counter_Bound_Delta time: [ 1.92 ns] ~26x faster +// Counter_Bound_With_View_Delta time: [ 1.82 ns] view filter applied at bind, not on hot path +// Counter_Bound_AtOverflow_Delta time: [ 1.82 ns] bind() at cardinality limit binds directly to the overflow +// tracker — perf parity with a normal bind, no per-call resolution +// UpDownCounter_Unbound_Cumulative time: [53.08 ns] UpDownCounter always uses Cumulative regardless of reader pref +// UpDownCounter_Bound_Cumulative time: [ 1.91 ns] ~28x faster +// Gauge_Unbound_Delta time: [52.82 ns] +// Gauge_Bound_Delta time: [ 1.27 ns] ~42x faster (LastValue store is cheaper than atomic add) +// Histogram_Unbound_Delta time: [60.15 ns] +// Histogram_Bound_Delta time: [ 6.61 ns] ~9.1x faster +// Histogram_Bound_AtOverflow_Delta time: [ 6.58 ns] perf parity with a normal bind +// Counter_Bound_Multithread/2 time: [21.59 µs] (100 adds/thread) +// Counter_Bound_Multithread/4 time: [37.21 µs] (100 adds/thread) +// Counter_Bound_Multithread/8 time: [71.70 µs] (100 adds/thread) +// +// Note: criterion does not fail CI on regression by itself. These numbers are +// reference values for human review; use `cargo criterion --baseline` locally +// if you need automated comparison against a saved baseline. + +fn create_provider(temporality: Temporality) -> SdkMeterProvider { + let reader = ManualReader::builder() + .with_temporality(temporality) + .build(); + SdkMeterProvider::builder().with_reader(reader).build() +} + +fn bench_bound_instruments(c: &mut Criterion) { + let mut group = c.benchmark_group("BoundInstruments"); + group.sample_size(100); + + let attrs = [ + KeyValue::new("method", "GET"), + KeyValue::new("status", "200"), + KeyValue::new("path", "/api/v1/users"), + ]; + + // Counter: Unbound vs Bound (Delta) + { + let provider = create_provider(Temporality::Delta); + let meter = provider.meter("bench"); + let counter = meter.u64_counter("unbound").build(); + group.bench_function("Counter_Unbound_Delta", |b| { + b.iter(|| counter.add(1, &attrs)); + }); + } + + { + let provider = create_provider(Temporality::Delta); + let meter = provider.meter("bench"); + let counter = meter.u64_counter("bound").build(); + let bound = counter.bind(&attrs); + group.bench_function("Counter_Bound_Delta", |b| { + b.iter(|| bound.add(1)); + }); + } + + // Counter: Bound with a View filter — confirms the filter is applied at + // bind() time and the hot path stays free of attribute processing. + { + let view = |i: &opentelemetry_sdk::metrics::Instrument| { + if i.name() == "bound_with_view" { + Stream::builder() + .with_allowed_attribute_keys(vec![ + Key::new("method"), + Key::new("status"), + Key::new("path"), + ]) + .build() + .ok() + } else { + None + } + }; + let reader = ManualReader::builder() + .with_temporality(Temporality::Delta) + .build(); + let provider = SdkMeterProvider::builder() + .with_reader(reader) + .with_view(view) + .build(); + let meter = provider.meter("bench"); + let counter = meter.u64_counter("bound_with_view").build(); + let bound = counter.bind(&attrs); + group.bench_function("Counter_Bound_With_View_Delta", |b| { + b.iter(|| bound.add(1)); + }); + } + + // Counter: Bound at overflow — confirms that binding when the cardinality + // limit is exhausted yields the same hot-path performance as a normal bind + // (writes go directly to the overflow tracker, no per-call resolution). + { + let cardinality_limit = 4; + let view = move |i: &Instrument| { + if i.name() == "bound_at_overflow" { + Stream::builder() + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let reader = ManualReader::builder() + .with_temporality(Temporality::Delta) + .build(); + let provider = SdkMeterProvider::builder() + .with_reader(reader) + .with_view(view) + .build(); + let meter = provider.meter("bench"); + let counter = meter.u64_counter("bound_at_overflow").build(); + // Saturate cardinality with unbound calls so bind() lands in overflow. + for i in 0..cardinality_limit { + counter.add(1, &[KeyValue::new("filler", i as i64)]); + } + let bound = counter.bind(&attrs); + group.bench_function("Counter_Bound_AtOverflow_Delta", |b| { + b.iter(|| bound.add(1)); + }); + } + + // UpDownCounter: Unbound vs Bound (always Cumulative regardless of reader pref) + { + let provider = create_provider(Temporality::Cumulative); + let meter = provider.meter("bench"); + let updown = meter.i64_up_down_counter("unbound_updown").build(); + group.bench_function("UpDownCounter_Unbound_Cumulative", |b| { + b.iter(|| updown.add(1, &attrs)); + }); + } + + { + let provider = create_provider(Temporality::Cumulative); + let meter = provider.meter("bench"); + let updown = meter.i64_up_down_counter("bound_updown").build(); + let bound = updown.bind(&attrs); + group.bench_function("UpDownCounter_Bound_Cumulative", |b| { + b.iter(|| bound.add(1)); + }); + } + + // Gauge: Unbound vs Bound (Delta) + { + let provider = create_provider(Temporality::Delta); + let meter = provider.meter("bench"); + let gauge = meter.u64_gauge("unbound_gauge").build(); + group.bench_function("Gauge_Unbound_Delta", |b| { + b.iter(|| gauge.record(1, &attrs)); + }); + } + + { + let provider = create_provider(Temporality::Delta); + let meter = provider.meter("bench"); + let gauge = meter.u64_gauge("bound_gauge").build(); + let bound = gauge.bind(&attrs); + group.bench_function("Gauge_Bound_Delta", |b| { + b.iter(|| bound.record(1)); + }); + } + + // Histogram: Unbound vs Bound (Delta) + { + let provider = create_provider(Temporality::Delta); + let meter = provider.meter("bench"); + let histogram = meter.f64_histogram("unbound_hist").build(); + group.bench_function("Histogram_Unbound_Delta", |b| { + b.iter(|| histogram.record(1.5, &attrs)); + }); + } + + { + let provider = create_provider(Temporality::Delta); + let meter = provider.meter("bench"); + let histogram = meter.f64_histogram("bound_hist").build(); + let bound = histogram.bind(&attrs); + group.bench_function("Histogram_Bound_Delta", |b| { + b.iter(|| bound.record(1.5)); + }); + } + + // Histogram: Bound at overflow — same property as the counter version. + { + let cardinality_limit = 4; + let view = move |i: &Instrument| { + if i.name() == "bound_hist_at_overflow" { + Stream::builder() + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let reader = ManualReader::builder() + .with_temporality(Temporality::Delta) + .build(); + let provider = SdkMeterProvider::builder() + .with_reader(reader) + .with_view(view) + .build(); + let meter = provider.meter("bench"); + let histogram = meter.f64_histogram("bound_hist_at_overflow").build(); + for i in 0..cardinality_limit { + histogram.record(1.5, &[KeyValue::new("filler", i as i64)]); + } + let bound = histogram.bind(&attrs); + group.bench_function("Histogram_Bound_AtOverflow_Delta", |b| { + b.iter(|| bound.record(1.5)); + }); + } + + // Multi-threaded bound counter + for num_threads in [2, 4, 8] { + let provider = create_provider(Temporality::Delta); + let meter = provider.meter("bench"); + let counter = meter.u64_counter("mt_bound").build(); + let bound = counter.bind(&attrs); + + group.bench_function(format!("Counter_Bound_Multithread/{num_threads}"), |b| { + b.iter(|| { + std::thread::scope(|s| { + for _ in 0..num_threads { + s.spawn(|| { + for _ in 0..100 { + bound.add(1); + } + }); + } + }); + }); + }); + } + + // Multi-threaded bound up-down counter (atomic add on i64) + for num_threads in [2, 4, 8] { + let provider = create_provider(Temporality::Cumulative); + let meter = provider.meter("bench"); + let updown = meter.i64_up_down_counter("mt_bound_updown").build(); + let bound = updown.bind(&attrs); + + group.bench_function( + format!("UpDownCounter_Bound_Multithread/{num_threads}"), + |b| { + b.iter(|| { + std::thread::scope(|s| { + for _ in 0..num_threads { + s.spawn(|| { + for _ in 0..100 { + bound.add(1); + } + }); + } + }); + }); + }, + ); + } + + // Multi-threaded bound histogram (Mutex — expected to show + // contention quickly relative to atomic-based instruments). + for num_threads in [2, 4, 8] { + let provider = create_provider(Temporality::Delta); + let meter = provider.meter("bench"); + let histogram = meter.f64_histogram("mt_bound_hist").build(); + let bound = histogram.bind(&attrs); + + group.bench_function(format!("Histogram_Bound_Multithread/{num_threads}"), |b| { + b.iter(|| { + std::thread::scope(|s| { + for _ in 0..num_threads { + s.spawn(|| { + for _ in 0..100 { + bound.record(1.5); + } + }); + } + }); + }); + }); + } + + // Multi-threaded bound gauge (LastValue: atomic store, no read-modify-write) + for num_threads in [2, 4, 8] { + let provider = create_provider(Temporality::Delta); + let meter = provider.meter("bench"); + let gauge = meter.u64_gauge("mt_bound_gauge").build(); + let bound = gauge.bind(&attrs); + + group.bench_function(format!("Gauge_Bound_Multithread/{num_threads}"), |b| { + b.iter(|| { + std::thread::scope(|s| { + for _ in 0..num_threads { + s.spawn(|| { + for _ in 0..100 { + bound.record(1); + } + }); + } + }); + }); + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_bound_instruments); +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/context.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/context.rs new file mode 100644 index 00000000..c8bbb063 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/context.rs @@ -0,0 +1,193 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use opentelemetry::{ + global::BoxedTracer, + trace::{ + noop::NoopTracer, SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState, + TracerProvider, + }, + Context, ContextGuard, +}; +use opentelemetry_sdk::{ + error::OTelSdkResult, + trace::{Sampler, SdkTracerProvider, SpanData, SpanExporter}, +}; +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +use pprof::criterion::{Output, PProfProfiler}; +use std::fmt::Display; +use std::hint::black_box; + +fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("context"); + for env in [ + Environment::InContext, + Environment::NoContext, + Environment::NoSdk, + ] { + let (_provider, _tracer, _guard) = env.setup(); + + for api in [Api::Alt, Api::Spec] { + let param = format!("{env}/{api}"); + group.bench_function( + BenchmarkId::new("has_active_span", param.clone()), + |b| match api { + Api::Alt => b.iter(has_active_span_alt), + Api::Spec => b.iter(has_active_span_spec), + }, + ); + group.bench_function( + BenchmarkId::new("is_sampled", param.clone()), + |b| match api { + Api::Alt => b.iter(is_sampled_alt), + Api::Spec => b.iter(is_sampled_spec), + }, + ); + group.bench_function(BenchmarkId::new("is_recording", param), |b| match api { + Api::Alt => b.iter(is_recording_alt), + Api::Spec => b.iter(is_recording_spec), + }); + } + } +} + +#[inline(never)] +fn has_active_span_alt() { + let _ = black_box(Context::map_current(TraceContextExt::has_active_span)); +} + +#[inline(never)] +fn has_active_span_spec() { + let _ = black_box(Context::current().has_active_span()); +} + +#[inline(never)] +fn is_sampled_alt() { + let _ = black_box(Context::map_current(|cx| { + cx.span().span_context().is_sampled() + })); +} + +#[inline(never)] +fn is_sampled_spec() { + let _ = black_box(Context::current().span().span_context().is_sampled()); +} + +#[inline(never)] +fn is_recording_alt() { + let _ = black_box(Context::map_current(|cx| cx.span().is_recording())); +} + +#[inline(never)] +fn is_recording_spec() { + let _ = black_box(Context::current().span().is_recording()); +} + +#[derive(Copy, Clone)] +enum Api { + /// An alternative way which may be faster than what the spec recommends. + Alt, + /// The recommended way as proposed by the current opentelemetry specification. + Spec, +} + +impl Api { + const fn as_str(self) -> &'static str { + match self { + Api::Alt => "alt", + Api::Spec => "spec", + } + } +} + +impl Display for Api { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[derive(Copy, Clone)] +enum Environment { + /// There is an active span being sampled in the current context. + InContext, + /// There is no span in context (or there is not context). + NoContext, + /// An SDK has not been configured, so instrumentation should be noop. + NoSdk, +} + +impl Environment { + const fn as_str(self) -> &'static str { + match self { + Environment::InContext => "in-cx", + Environment::NoContext => "no-cx", + Environment::NoSdk => "no-sdk", + } + } + + fn setup(&self) -> (Option, BoxedTracer, Option) { + match self { + Environment::InContext => { + let guard = Context::current() + .with_remote_span_context(SpanContext::new( + TraceId::from(0x09251969), + SpanId::from(0x08171969), + TraceFlags::SAMPLED, + true, + TraceState::default(), + )) + .attach(); + let (provider, tracer) = parent_sampled_tracer(Sampler::AlwaysOff); + (Some(provider), tracer, Some(guard)) + } + Environment::NoContext => { + let (provider, tracer) = parent_sampled_tracer(Sampler::AlwaysOff); + (Some(provider), tracer, None) + } + Environment::NoSdk => (None, BoxedTracer::new(Box::new(NoopTracer::new())), None), + } + } +} + +impl Display for Environment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +fn parent_sampled_tracer(inner_sampler: Sampler) -> (SdkTracerProvider, BoxedTracer) { + let provider = SdkTracerProvider::builder() + .with_sampler(Sampler::ParentBased(Box::new(inner_sampler))) + .with_simple_exporter(NoopExporter) + .build(); + let tracer = provider.tracer(module_path!()); + (provider, BoxedTracer::new(Box::new(tracer))) +} + +#[derive(Debug)] +struct NoopExporter; + +impl SpanExporter for NoopExporter { + async fn export(&self, _spans: Vec) -> OTelSdkResult { + Ok(()) + } +} + +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = criterion_benchmark +} + +#[cfg(any(target_os = "windows", not(feature = "bench_profiling")))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} + +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log.rs new file mode 100644 index 00000000..e70f1420 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log.rs @@ -0,0 +1,340 @@ +//! run with `$ cargo bench --bench log -- --exact ` to run specific test for logs +//! So to run test named "full-log-with-attributes/with-context" you would run `$ cargo bench --bench log -- --exact full-log-with-attributes/with-context` +//! To run all tests for logs you would run `$ cargo bench --bench log` +//! +/* +The benchmark results: +criterion = "0.5.1" +OS: Ubuntu 22.04.3 LTS (5.15.146.1-microsoft-standard-WSL2) +Hardware: AMD EPYC 7763 64-Core Processor - 2.44 GHz, 16vCPUs, +RAM: 64.0 GB +| Test | Average time| +|--------------------------------|-------------| +| Logger_Creation | 30 ns | +| LoggerProvider_Creation | 909 ns | +| Logging_Comparable_To_Appender | 87 ns | +*/ + +use opentelemetry::time::now; +use std::collections::HashMap; + +use criterion::{criterion_group, criterion_main, Criterion}; + +use opentelemetry::logs::{AnyValue, LogRecord as _, Logger, LoggerProvider, Severity}; +use opentelemetry::trace::Tracer; +use opentelemetry::trace::TracerProvider; +use opentelemetry::{InstrumentationScope, Key}; +use opentelemetry_sdk::error::OTelSdkResult; +use opentelemetry_sdk::logs::{LogProcessor, SdkLogRecord, SdkLogger, SdkLoggerProvider}; +use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider}; + +#[derive(Debug)] +struct NoopProcessor; + +impl LogProcessor for NoopProcessor { + fn emit(&self, _data: &mut SdkLogRecord, _scope: &InstrumentationScope) {} + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } +} + +fn log_benchmark_group(c: &mut Criterion, name: &str, f: F) { + let mut group = c.benchmark_group(name); + + group.bench_function("no-context", |b| { + let provider = SdkLoggerProvider::builder() + .with_log_processor(NoopProcessor {}) + .build(); + + let logger = provider.logger("no-context"); + + b.iter(|| f(&logger)); + }); + + group.bench_function("with-context", |b| { + let provider = SdkLoggerProvider::builder() + .with_log_processor(NoopProcessor {}) + .build(); + + let logger = provider.logger("with-context"); + + // setup tracing as well. + let tracer_provider = SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOn) + .build(); + let tracer = tracer_provider.tracer("bench-tracer"); + + // Act + tracer.in_span("bench-span", |_cx| { + b.iter(|| f(&logger)); + }); + }); + + group.finish(); +} + +fn log_provider_creation(c: &mut Criterion) { + c.bench_function("LoggerProvider_Creation", |b| { + b.iter(|| { + let _provider = SdkLoggerProvider::builder() + .with_log_processor(NoopProcessor {}) + .build(); + }); + }); +} + +fn logger_creation(c: &mut Criterion) { + // Provider is created once, outside of the benchmark + let provider = SdkLoggerProvider::builder() + .with_log_processor(NoopProcessor {}) + .build(); + + c.bench_function("Logger_Creation", |b| { + b.iter(|| { + let _logger = provider.logger("benchmark"); + }); + }); +} + +fn logging_comparable_to_appender(c: &mut Criterion) { + let provider = SdkLoggerProvider::builder() + .with_log_processor(NoopProcessor {}) + .build(); + let logger = provider.logger("benchmark"); + + // This mimics the logic from opentelemetry-tracing-appender closely, but + // without the overhead of the tracing layer itself. + c.bench_function("Logging_Comparable_To_Appender", |b| { + b.iter(|| { + let mut log_record = logger.create_log_record(); + let now = now(); + log_record.set_observed_timestamp(now); + log_record.set_target("my-target".to_string()); + log_record.set_event_name("CheckoutFailed"); + log_record.set_severity_number(Severity::Warn); + log_record.set_severity_text("WARN"); + log_record.add_attribute("book_id", "12345"); + log_record.add_attribute("book_title", "Rust Programming Adventures"); + log_record.add_attribute("message", "Unable to process checkout."); + + logger.emit(log_record); + }); + }); +} + +fn criterion_benchmark(c: &mut Criterion) { + logger_creation(c); + log_provider_creation(c); + logging_comparable_to_appender(c); + log_benchmark_group(c, "simple-log", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("simple log".into()); + logger.emit(log_record); + }); + + log_benchmark_group(c, "simple-log-with-int", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("simple log".into()); + log_record.add_attribute("testint", 2); + logger.emit(log_record); + }); + + log_benchmark_group(c, "simple-log-with-double", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("simple log".into()); + log_record.add_attribute("testdouble", 2.2); + logger.emit(log_record); + }); + + log_benchmark_group(c, "simple-log-with-string", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("simple log".into()); + log_record.add_attribute("teststring", "test"); + logger.emit(log_record); + }); + + log_benchmark_group(c, "simple-log-with-bool", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("simple log".into()); + log_record.add_attribute("testbool", AnyValue::Boolean(true)); + logger.emit(log_record); + }); + + let bytes = AnyValue::Bytes(Box::new(vec![25u8, 30u8, 40u8])); + log_benchmark_group(c, "simple-log-with-bytes", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("simple log".into()); + log_record.add_attribute("testbytes", bytes.clone()); + logger.emit(log_record); + }); + + let bytes = AnyValue::Bytes(Box::new(vec![ + 25u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, + 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, + 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, + 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, + 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, + 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, 30u8, 40u8, + ])); + log_benchmark_group(c, "simple-log-with-a-lot-of-bytes", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("simple log".into()); + log_record.add_attribute("testbytes", bytes.clone()); + logger.emit(log_record); + }); + + let vec_any_values = AnyValue::ListAny(Box::new(vec![ + AnyValue::Int(25), + "test".into(), + true.into(), + ])); + log_benchmark_group(c, "simple-log-with-vec-any-value", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("simple log".into()); + log_record.add_attribute("testvec", vec_any_values.clone()); + logger.emit(log_record); + }); + + let vec_any_values = AnyValue::ListAny(Box::new(vec![ + AnyValue::Int(25), + "test".into(), + true.into(), + ])); + let vec_any_values = AnyValue::ListAny(Box::new(vec![ + AnyValue::Int(25), + "test".into(), + true.into(), + vec_any_values, + ])); + log_benchmark_group(c, "simple-log-with-inner-vec-any-value", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("simple log".into()); + log_record.add_attribute("testvec", vec_any_values.clone()); + logger.emit(log_record); + }); + + let map_any_values = AnyValue::Map(Box::new(HashMap::from([ + ("testint".into(), 2.into()), + ("testdouble".into(), 2.2.into()), + ("teststring".into(), "test".into()), + ]))); + log_benchmark_group(c, "simple-log-with-map-any-value", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("simple log".into()); + log_record.add_attribute("testmap", map_any_values.clone()); + logger.emit(log_record); + }); + + let map_any_values = AnyValue::Map(Box::new(HashMap::from([ + ("testint".into(), 2.into()), + ("testdouble".into(), 2.2.into()), + ("teststring".into(), "test".into()), + ]))); + let map_any_values = AnyValue::Map(Box::new(HashMap::from([ + ("testint".into(), 2.into()), + ("testdouble".into(), 2.2.into()), + ("teststring".into(), "test".into()), + ("testmap".into(), map_any_values), + ]))); + log_benchmark_group(c, "simple-log-with-inner-map-any-value", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("simple log".into()); + log_record.add_attribute("testmap", map_any_values.clone()); + logger.emit(log_record); + }); + + log_benchmark_group(c, "long-log", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Gravida in fermentum et sollicitudin ac orci phasellus. Ullamcorper dignissim cras tincidunt lobortis feugiat vivamus at augue. Magna etiam tempor orci eu. Sed tempus urna et pharetra pharetra massa.".into()); + logger.emit(log_record); + }); + + let now = now(); + log_benchmark_group(c, "full-log", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("full log".into()); + log_record.set_timestamp(now); + log_record.set_observed_timestamp(now); + log_record.set_severity_number(Severity::Warn); + log_record.set_severity_text(Severity::Warn.name()); + logger.emit(log_record); + }); + + log_benchmark_group(c, "full-log-with-4-attributes", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("full log".into()); + log_record.set_timestamp(now); + log_record.set_observed_timestamp(now); + log_record.set_severity_number(Severity::Warn); + log_record.set_severity_text(Severity::Warn.name()); + log_record.add_attribute("name", "my-event-name"); + log_record.add_attribute("event.id", 20); + log_record.add_attribute("user.name", "otel"); + log_record.add_attribute("user.email", "otel@opentelemetry.io"); + logger.emit(log_record); + }); + + log_benchmark_group(c, "full-log-with-9-attributes", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("full log".into()); + log_record.set_timestamp(now); + log_record.set_observed_timestamp(now); + log_record.set_severity_number(Severity::Warn); + log_record.set_severity_text(Severity::Warn.name()); + log_record.add_attribute("name", "my-event-name"); + log_record.add_attribute("event.id", 20); + log_record.add_attribute("user.name", "otel"); + log_record.add_attribute("user.email", "otel@opentelemetry.io"); + log_record.add_attribute("code.filename", "log.rs"); + log_record.add_attribute("code.filepath", "opentelemetry_sdk/benches/log.rs"); + log_record.add_attribute("code.lineno", 96); + log_record.add_attribute("code.namespace", "opentelemetry_sdk::benches::log"); + log_record.add_attribute("log.target", "opentelemetry_sdk::benches::log"); + logger.emit(log_record); + }); + + let attributes: Vec<(Key, AnyValue)> = vec![ + ("name".into(), "my-event-name".into()), + ("event-id".into(), 20.into()), + ("user.name".into(), "otel".into()), + ("user.email".into(), "otel@opentelemetry.io".into()), + ("code.filename".into(), "log.rs".into()), + ( + "code.filepath".into(), + "opentelemetry_sdk/benches/log.rs".into(), + ), + ("code.lineno".into(), 96.into()), + ( + "code.namespace".into(), + "opentelemetry_sdk::benches::log".into(), + ), + ( + "log.target".into(), + "opentelemetry_sdk::benches::log".into(), + ), + ]; + log_benchmark_group(c, "full-log-with-attributes", |logger| { + let mut log_record = logger.create_log_record(); + log_record.set_body("full log".into()); + log_record.set_timestamp(now); + log_record.set_observed_timestamp(now); + log_record.set_severity_number(Severity::Warn); + log_record.set_severity_text(Severity::Warn.name()); + log_record.add_attributes(attributes.clone()); + logger.emit(log_record); + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_enabled.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_enabled.rs new file mode 100644 index 00000000..d6b4c3db --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_enabled.rs @@ -0,0 +1,83 @@ +/* + The benchmark results: + criterion = "0.5.1" + Hardware: Apple M4 Pro + Total Number of Cores:   14 (10 performance and 4 efficiency) + | Test | Average time| + |---------------------------------------------|-------------| + | exporter_disabled_simple_processor | 5.3 ns | +*/ + +use criterion::{criterion_group, criterion_main, Criterion}; +use opentelemetry::logs::{Logger, LoggerProvider}; +use opentelemetry_sdk::error::OTelSdkResult; +use opentelemetry_sdk::logs::{ + LogBatch, LogExporter, LogProcessor, SdkLoggerProvider, SimpleLogProcessor, +}; +use opentelemetry_sdk::Resource; +#[cfg(not(target_os = "windows"))] +use pprof::criterion::{Output, PProfProfiler}; + +#[derive(Debug)] +struct NoopExporter; +impl LogExporter for NoopExporter { + async fn export(&self, _: LogBatch<'_>) -> OTelSdkResult { + Ok(()) + } + + #[inline] + fn event_enabled( + &self, + _level: opentelemetry::logs::Severity, + _target: &str, + _name: Option<&str>, + ) -> bool { + false + } + + fn set_resource(&mut self, _: &Resource) {} +} + +fn benchmark_exporter_enabled_false(c: &mut Criterion, name: &str, processor: T) +where + T: LogProcessor + Send + Sync + 'static, +{ + let provider = SdkLoggerProvider::builder() + .with_log_processor(processor) + .build(); + let logger = provider.logger("test_logger"); + + c.bench_function(name, |b| { + b.iter(|| { + std::hint::black_box(logger.event_enabled( + opentelemetry::logs::Severity::Debug, + "target", + Some("name"), + )); + }); + }); +} + +fn criterion_benchmark(c: &mut Criterion) { + let simple = SimpleLogProcessor::new(NoopExporter); + benchmark_exporter_enabled_false(c, "exporter_disabled_simple_processor", simple); +} + +#[cfg(not(target_os = "windows"))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = criterion_benchmark +} +#[cfg(target_os = "windows")] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_exporter.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_exporter.rs new file mode 100644 index 00000000..5b5c8174 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_exporter.rs @@ -0,0 +1,184 @@ +/* + The benchmark results: + criterion = "0.5.1" + OS: Ubuntu 22.04.3 LTS (5.15.146.1-microsoft-standard-WSL2) + Hardware: AMD EPYC 7763 64-Core Processor - 2.44 GHz, 16vCPUs, + RAM: 64.0 GB + | Test | Average time| + |--------------------------------|-------------| + | LogExporterWithFuture | 111 ns | + | LogExporterWithoutFuture | 92 ns | +*/ + +use opentelemetry::time::now; +use opentelemetry_sdk::error::OTelSdkResult; +use std::sync::Mutex; + +use criterion::{criterion_group, criterion_main, Criterion}; + +use opentelemetry::logs::{LogRecord as _, Logger, LoggerProvider, Severity}; +use opentelemetry::InstrumentationScope; +use opentelemetry_sdk::logs::LogBatch; +use opentelemetry_sdk::logs::LogProcessor; +use opentelemetry_sdk::logs::SdkLogRecord; +use opentelemetry_sdk::logs::SdkLoggerProvider; +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +use pprof::criterion::{Output, PProfProfiler}; +use std::fmt::Debug; + +// Run this benchmark with: +// cargo bench --bench log_exporter +pub trait LogExporterWithFuture: Send + Sync + Debug { + fn export(&mut self, batch: LogBatch<'_>) -> impl std::future::Future + Send; +} + +pub trait LogExporterWithoutFuture: Send + Sync + Debug { + fn export(&mut self, batch: LogBatch<'_>); +} + +#[derive(Debug)] +struct NoOpExporterWithFuture {} + +impl LogExporterWithFuture for NoOpExporterWithFuture { + async fn export(&mut self, _batch: LogBatch<'_>) {} +} + +#[derive(Debug)] +struct NoOpExporterWithoutFuture {} +impl LogExporterWithoutFuture for NoOpExporterWithoutFuture { + fn export(&mut self, _batch: LogBatch<'_>) {} +} + +#[derive(Debug)] +struct ExportingProcessorWithFuture { + exporter: Mutex, +} + +impl ExportingProcessorWithFuture { + fn new(exporter: NoOpExporterWithFuture) -> Self { + ExportingProcessorWithFuture { + exporter: Mutex::new(exporter), + } + } +} + +impl LogProcessor for ExportingProcessorWithFuture { + fn emit(&self, record: &mut SdkLogRecord, scope: &InstrumentationScope) { + let mut exporter = self.exporter.lock().expect("lock error"); + let logs = [(record as &SdkLogRecord, scope)]; + futures_executor::block_on(exporter.export(LogBatch::new(&logs))); + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } +} + +#[derive(Debug)] +struct ExportingProcessorWithoutFuture { + exporter: Mutex, +} + +impl ExportingProcessorWithoutFuture { + fn new(exporter: NoOpExporterWithoutFuture) -> Self { + ExportingProcessorWithoutFuture { + exporter: Mutex::new(exporter), + } + } +} + +impl LogProcessor for ExportingProcessorWithoutFuture { + fn emit(&self, record: &mut SdkLogRecord, scope: &InstrumentationScope) { + let logs = [(record as &SdkLogRecord, scope)]; + self.exporter + .lock() + .expect("lock error") + .export(LogBatch::new(&logs)); + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } +} + +fn criterion_benchmark(c: &mut Criterion) { + exporter_with_future(c); + exporter_without_future(c); +} + +fn exporter_with_future(c: &mut Criterion) { + let provider = SdkLoggerProvider::builder() + .with_log_processor(ExportingProcessorWithFuture::new(NoOpExporterWithFuture {})) + .build(); + let logger = provider.logger("benchmark"); + + c.bench_function("LogExporterWithFuture", |b| { + b.iter(|| { + let mut log_record = logger.create_log_record(); + let now = now(); + log_record.set_observed_timestamp(now); + log_record.set_target("my-target".to_string()); + log_record.set_event_name("CheckoutFailed"); + log_record.set_severity_number(Severity::Warn); + log_record.set_severity_text("WARN"); + log_record.add_attribute("book_id", "12345"); + log_record.add_attribute("book_title", "Rust Programming Adventures"); + log_record.add_attribute("message", "Unable to process checkout."); + + logger.emit(log_record); + }); + }); +} + +fn exporter_without_future(c: &mut Criterion) { + let provider = SdkLoggerProvider::builder() + .with_log_processor(ExportingProcessorWithoutFuture::new( + NoOpExporterWithoutFuture {}, + )) + .build(); + let logger = provider.logger("benchmark"); + + c.bench_function("LogExporterWithoutFuture", |b| { + b.iter(|| { + let mut log_record = logger.create_log_record(); + let now = now(); + log_record.set_observed_timestamp(now); + log_record.set_target("my-target".to_string()); + log_record.set_event_name("CheckoutFailed"); + log_record.set_severity_number(Severity::Warn); + log_record.set_severity_text("WARN"); + log_record.add_attribute("book_id", "12345"); + log_record.add_attribute("book_title", "Rust Programming Adventures"); + log_record.add_attribute("message", "Unable to process checkout."); + + logger.emit(log_record); + }); + }); +} + +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = criterion_benchmark +} +#[cfg(any(target_os = "windows", not(feature = "bench_profiling")))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_processor.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_processor.rs new file mode 100644 index 00000000..ce040444 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/log_processor.rs @@ -0,0 +1,180 @@ +/* + The benchmark results: + criterion = "0.5.1" + OS: Ubuntu 22.04.3 LTS (5.15.146.1-microsoft-standard-WSL2) + Hardware: AMD EPYC 7763 64-Core Processor - 2.44 GHz, 16vCPUs, + RAM: 64.0 GB + | Test | Average time| + |---------------------------------------------|-------------| + | log_noop_processor | 134 ns | + | log_cloning_processor | 236 ns | + | log_clone_and_send_to_channel_processor | 403 ns | +*/ + +use opentelemetry::time::now; +use std::{ + sync::{Arc, Mutex}, + thread::sleep, +}; + +use criterion::{criterion_group, criterion_main, Criterion}; +use opentelemetry::{ + logs::{LogRecord as _, Logger, LoggerProvider, Severity}, + InstrumentationScope, +}; +use opentelemetry_sdk::{ + error::OTelSdkResult, + logs::{LogProcessor, SdkLogRecord, SdkLogger, SdkLoggerProvider}, +}; + +// Run this benchmark with: +// cargo bench --bench log_processor + +fn create_log_record(logger: &SdkLogger) -> SdkLogRecord { + let mut log_record = logger.create_log_record(); + let now = now(); + log_record.set_observed_timestamp(now); + log_record.set_target("my-target".to_string()); + log_record.set_event_name("CheckoutFailed"); + log_record.set_severity_number(Severity::Warn); + log_record.set_severity_text("WARN"); + log_record.add_attribute("book_id", "12345"); + log_record.add_attribute("book_title", "Rust Programming Adventures"); + log_record.add_attribute("message", "Unable to process checkout."); + log_record +} + +#[derive(Debug)] +struct NoopProcessor; + +impl LogProcessor for NoopProcessor { + fn emit(&self, _data: &mut SdkLogRecord, _scope: &InstrumentationScope) {} + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } +} + +#[derive(Debug)] +struct CloningProcessor; + +impl LogProcessor for CloningProcessor { + fn emit(&self, data: &mut SdkLogRecord, _scope: &InstrumentationScope) { + let _data_cloned = data.clone(); + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } +} + +#[derive(Debug)] +struct SendToChannelProcessor { + sender: std::sync::mpsc::Sender<(SdkLogRecord, InstrumentationScope)>, + receiver: Arc>>, +} + +impl SendToChannelProcessor { + fn new() -> Self { + let (sender, receiver) = std::sync::mpsc::channel(); + let s = Self { + sender, + receiver: Arc::new(Mutex::new(receiver)), + }; + let receiver_cloned = s.receiver.clone(); + let _ = std::thread::spawn(move || loop { + sleep(std::time::Duration::from_millis(10)); + let data = receiver_cloned.lock().unwrap().recv(); + if data.is_err() { + println!( + "Error receiving log data from channel {0}", + data.err().unwrap() + ); + break; + } + }); + s + } +} + +impl LogProcessor for SendToChannelProcessor { + fn emit(&self, record: &mut SdkLogRecord, scope: &InstrumentationScope) { + let res = self.sender.send((record.clone(), scope.clone())); + if res.is_err() { + println!("Error sending log data to channel {0}", res.err().unwrap()); + } + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } +} + +fn criterion_benchmark(c: &mut Criterion) { + log_noop_processor(c); + log_cloning_processor(c); + log_cloning_and_send_to_channel_processor(c); +} + +fn log_noop_processor(c: &mut Criterion) { + let provider = SdkLoggerProvider::builder() + .with_log_processor(NoopProcessor {}) + .build(); + let logger = provider.logger("benchmark"); + + c.bench_function("log_noop_processor", |b| { + b.iter(|| { + let log_record = create_log_record(&logger); + logger.emit(log_record); + }); + }); +} + +fn log_cloning_processor(c: &mut Criterion) { + let provider = SdkLoggerProvider::builder() + .with_log_processor(CloningProcessor {}) + .build(); + let logger = provider.logger("benchmark"); + + c.bench_function("log_cloning_processor", |b| { + b.iter(|| { + let log_record = create_log_record(&logger); + logger.emit(log_record); + }); + }); +} + +fn log_cloning_and_send_to_channel_processor(c: &mut Criterion) { + let provider = SdkLoggerProvider::builder() + .with_log_processor(SendToChannelProcessor::new()) + .build(); + let logger = provider.logger("benchmark"); + + c.bench_function("log_clone_and_send_to_channel_processor", |b| { + b.iter(|| { + let log_record = create_log_record(&logger); + logger.emit(log_record); + }); + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metric.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metric.rs new file mode 100644 index 00000000..7eaecccb --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metric.rs @@ -0,0 +1,356 @@ +use criterion::{criterion_group, criterion_main, Bencher, Criterion}; +use opentelemetry::{ + metrics::{Counter, Histogram, MeterProvider as _}, + Key, KeyValue, +}; +use opentelemetry_sdk::{ + error::OTelSdkResult, + metrics::{ + data::ResourceMetrics, reader::MetricReader, Aggregation, Instrument, InstrumentKind, + ManualReader, Pipeline, SdkMeterProvider, Stream, Temporality, + }, +}; +use rand::Rng; +use std::sync::{Arc, Weak}; +use std::time::Duration; + +#[derive(Clone, Debug)] +struct SharedReader(Arc); + +impl MetricReader for SharedReader { + fn register_pipeline(&self, pipeline: Weak) { + self.0.register_pipeline(pipeline) + } + + fn collect(&self, rm: &mut ResourceMetrics) -> OTelSdkResult { + self.0.collect(rm) + } + + fn force_flush(&self) -> OTelSdkResult { + self.0.force_flush() + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + self.0.shutdown() + } + + fn temporality(&self, kind: InstrumentKind) -> Temporality { + self.0.temporality(kind) + } +} + +// * Summary * + +// rustc 1.68.0 (2c8cc3432 2023-03-06) +// cargo 1.68.0 (115f34552 2023-02-26), OS=Windows 11 Enterprise +// Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz 2.59 GHz +// 12 logical and 6 physical cores + +// Counter/AddNoAttrs time: [65.406 ns 65.535 ns 65.675 ns] +// Counter/AddNoAttrsDelta time: [65.553 ns 65.761 ns 65.981 ns] +// Counter/AddOneAttr time: [341.55 ns 344.40 ns 347.58 ns] +// Counter/AddOneAttrDelta time: [340.11 ns 342.42 ns 344.89 ns] +// Counter/AddThreeAttr time: [619.01 ns 624.16 ns 630.16 ns] +// Counter/AddThreeAttrDelta +// time: [606.71 ns 611.45 ns 616.66 ns] +// Counter/AddFiveAttr time: [3.7551 µs 3.7813 µs 3.8094 µs] +// Counter/AddFiveAttrDelta +// time: [3.7550 µs 3.7870 µs 3.8266 µs] +// Counter/AddTenAttr time: [4.7684 µs 4.7909 µs 4.8146 µs] +// Counter/AddTenAttrDelta time: [4.7682 µs 4.8152 µs 4.8722 µs] +// Counter/AddInvalidAttr time: [469.31 ns 472.97 ns 476.92 ns] +// Counter/AddSingleUseAttrs +// time: [749.15 ns 805.09 ns 868.03 ns] +// Counter/AddSingleUseInvalid +// time: [693.75 ns 702.65 ns 713.20 ns] +// Counter/AddSingleUseFiltered +// time: [677.00 ns 681.63 ns 686.88 ns] +// Counter/CollectOneAttr time: [659.29 ns 681.20 ns 708.04 ns] +// Counter/CollectTenAttrs time: [3.5048 µs 3.5384 µs 3.5777 µs] +// Histogram/Record0Attrs10bounds +// time: [75.790 ns 77.235 ns 78.825 ns] +// Histogram/Record3Attrs10bounds +// time: [580.88 ns 603.84 ns 628.71 ns] +// Histogram/Record5Attrs10bounds +// time: [3.8539 µs 3.8988 µs 3.9519 µs] +// Histogram/Record7Attrs10bounds +// time: [699.46 ns 720.17 ns 742.24 ns] +// Histogram/Record10Attrs10bounds +// time: [844.95 ns 861.92 ns 880.23 ns] +// Histogram/Record0Attrs49bounds +// time: [75.198 ns 77.081 ns 79.449 ns] +// Histogram/Record3Attrs49bounds +// time: [533.82 ns 540.44 ns 547.30 ns] +// Histogram/Record5Attrs49bounds +// time: [583.01 ns 588.27 ns 593.98 ns] +// Histogram/Record7Attrs49bounds +// time: [645.67 ns 652.03 ns 658.35 ns] +// Histogram/Record10Attrs49bounds +// time: [747.24 ns 755.42 ns 764.37 ns] +// Histogram/Record0Attrs50bounds +// time: [72.023 ns 72.218 ns 72.426 ns] +// Histogram/Record3Attrs50bounds +// time: [530.21 ns 534.23 ns 538.63 ns] +// Histogram/Record5Attrs50bounds +// time: [3.2934 µs 3.3069 µs 3.3228 µs] +// Histogram/Record7Attrs50bounds +// time: [633.88 ns 638.87 ns 644.52 ns] +// Histogram/Record10Attrs50bounds +// time: [759.69 ns 768.42 ns 778.12 ns] +// Histogram/Record0Attrs1000bounds +// time: [75.495 ns 75.942 ns 76.529 ns] +// Histogram/Record3Attrs1000bounds +// time: [542.06 ns 548.37 ns 555.31 ns] +// Histogram/Record5Attrs1000bounds +// time: [3.2935 µs 3.3058 µs 3.3215 µs] +// Histogram/Record7Attrs1000bounds +// time: [643.75 ns 649.05 ns 655.14 ns] +// Histogram/Record10Attrs1000bounds +// time: [726.87 ns 736.52 ns 747.09 ns] +type ViewFn = Box Option + Send + Sync + 'static>; + +fn bench_counter(view: Option, temporality: &str) -> (SharedReader, Counter) { + let rdr = if temporality == "cumulative" { + SharedReader(Arc::new(ManualReader::builder().build())) + } else { + SharedReader(Arc::new( + ManualReader::builder() + .with_temporality(Temporality::Delta) + .build(), + )) + }; + let mut builder = SdkMeterProvider::builder().with_reader(rdr.clone()); + if let Some(view) = view { + builder = builder.with_view(view); + } + let provider = builder.build(); + let cntr = provider.meter("test").u64_counter("hello").build(); + + (rdr, cntr) +} + +fn counters(c: &mut Criterion) { + let (_, cntr) = bench_counter(None, "cumulative"); + let (_, cntr_max) = bench_counter(None, "cumulative"); + + let mut group = c.benchmark_group("Counter"); + group.bench_function("AddNoAttrs", |b| b.iter(|| cntr.add(1, &[]))); + group.bench_function("AddOneAttr", |b| { + b.iter(|| cntr.add(1, &[KeyValue::new("K", "V")])) + }); + group.bench_function("AddThreeAttr", |b| { + b.iter(|| { + cntr.add( + 1, + &[ + KeyValue::new("K2", "V2"), + KeyValue::new("K3", "V3"), + KeyValue::new("K4", "V4"), + ], + ) + }) + }); + group.bench_function("AddFiveAttr", |b| { + b.iter(|| { + cntr.add( + 1, + &[ + KeyValue::new("K5", "V5"), + KeyValue::new("K6", "V6"), + KeyValue::new("K7", "V7"), + KeyValue::new("K8", "V8"), + KeyValue::new("K9", "V9"), + ], + ) + }) + }); + group.bench_function("AddTenAttr", |b| { + b.iter(|| { + cntr.add( + 1, + &[ + KeyValue::new("K10", "V10"), + KeyValue::new("K11", "V11"), + KeyValue::new("K12", "V12"), + KeyValue::new("K13", "V13"), + KeyValue::new("K14", "V14"), + KeyValue::new("K15", "V15"), + KeyValue::new("K16", "V16"), + KeyValue::new("K17", "V17"), + KeyValue::new("K18", "V18"), + KeyValue::new("K19", "V19"), + ], + ) + }) + }); + + const MAX_DATA_POINTS: i64 = 2000; + let mut max_attributes: Vec = Vec::new(); + + for i in 0..MAX_DATA_POINTS - 2 { + max_attributes.push(KeyValue::new(i.to_string(), i)) + } + + group.bench_function("AddOneTillMaxAttr", |b| { + b.iter(|| cntr_max.add(1, &max_attributes)) + }); + + for i in MAX_DATA_POINTS..MAX_DATA_POINTS * 2 { + max_attributes.push(KeyValue::new(i.to_string(), i)) + } + + group.bench_function("AddMaxAttr", |b| { + b.iter(|| cntr_max.add(1, &max_attributes)) + }); + + group.bench_function("AddInvalidAttr", |b| { + b.iter(|| cntr.add(1, &[KeyValue::new("", "V"), KeyValue::new("K", "V")])) + }); + group.bench_function("AddSingleUseAttrs", |b| { + let mut v = 0; + b.iter(|| { + cntr.add(1, &[KeyValue::new("K", v)]); + v += 1; + }) + }); + group.bench_function("AddSingleUseInvalid", |b| { + let mut v = 0; + b.iter(|| { + cntr.add(1, &[KeyValue::new("", v), KeyValue::new("K", v)]); + v += 1; + }) + }); + + let (_, cntr) = bench_counter( + Some(Box::new(|_i: &Instrument| { + Stream::builder() + .with_allowed_attribute_keys([Key::new("K")]) + .build() + .ok() + })), + "cumulative", + ); + + group.bench_function("AddSingleUseFiltered", |b| { + let mut v = 0; + b.iter(|| { + cntr.add(1, &[KeyValue::new("L", v), KeyValue::new("K", v)]); + v += 1; + }) + }); + + let (rdr, cntr) = bench_counter(None, "cumulative"); + let mut rm = ResourceMetrics::default(); + + group.bench_function("CollectOneAttr", |b| { + let mut v = 0; + b.iter(|| { + cntr.add(1, &[KeyValue::new("K", v)]); + let _ = rdr.collect(&mut rm); + v += 1; + }) + }); + + group.bench_function("CollectTenAttrs", |b| { + let mut v = 0; + b.iter(|| { + for i in 0..10 { + cntr.add(1, &[KeyValue::new("K", i)]); + } + let _ = rdr.collect(&mut rm); + v += 1; + }) + }); +} + +const MAX_BOUND: usize = 100000; + +fn bench_histogram(bound_count: usize) -> (SharedReader, Histogram) { + let mut bounds = vec![0; bound_count]; + #[allow(clippy::needless_range_loop)] + for i in 0..bounds.len() { + bounds[i] = i * MAX_BOUND / bound_count + } + + let r = SharedReader(Arc::new(ManualReader::default())); + let builder = SdkMeterProvider::builder() + .with_reader(r.clone()) + .with_view(move |i: &Instrument| { + if i.name().starts_with("histogram_") { + Stream::builder() + .with_aggregation(Aggregation::ExplicitBucketHistogram { + boundaries: bounds.iter().map(|&x| x as f64).collect(), + record_min_max: true, + }) + .build() + .ok() + } else { + None + } + }); + + let mtr = builder.build().meter("test_meter"); + let hist = mtr + .u64_histogram(format!("histogram_{bound_count}")) + .build(); + + (r, hist) +} + +fn histograms(c: &mut Criterion) { + let mut group = c.benchmark_group("Histogram"); + let mut rng = rand::rng(); + + for bound_size in [10, 49, 50, 1000].iter() { + let (_, hist) = bench_histogram(*bound_size); + for attr_size in [0, 3, 5, 7, 10].iter() { + let mut attributes: Vec = Vec::new(); + for i in 0..*attr_size { + attributes.push(KeyValue::new( + format!("K,{bound_size},{attr_size}"), + format!("V,{bound_size},{attr_size},{i}"), + )) + } + let value: u64 = rng.random_range(0..MAX_BOUND).try_into().unwrap(); + group.bench_function(format!("Record{attr_size}Attrs{bound_size}bounds"), |b| { + b.iter(|| hist.record(value, &attributes)) + }); + } + } + group.bench_function("CollectOne", |b| benchmark_collect_histogram(b, 1)); + group.bench_function("CollectFive", |b| benchmark_collect_histogram(b, 5)); + group.bench_function("CollectTen", |b| benchmark_collect_histogram(b, 10)); + group.bench_function("CollectTwentyFive", |b| benchmark_collect_histogram(b, 25)); +} + +fn benchmark_collect_histogram(b: &mut Bencher, n: usize) { + let r = SharedReader(Arc::new(ManualReader::default())); + let mtr = SdkMeterProvider::builder() + .with_reader(r.clone()) + .build() + .meter("sdk/metric/bench/histogram"); + + for i in 0..n { + let h = mtr.u64_histogram(format!("fake_data_{i}")).build(); + h.record(1, &[]); + } + + let mut rm = ResourceMetrics::default(); + + b.iter(|| { + let _ = r.collect(&mut rm); + // TODO - this assertion fails periodically, and breaks + // our bench testing. We should fix it. + // assert_eq!(rm.scope_metrics[0].metrics.len(), n); + }) +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = counters, histograms +} + +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_counter.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_counter.rs new file mode 100644 index 00000000..675c4a4d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_counter.rs @@ -0,0 +1,259 @@ +/* + The benchmark results: + criterion = "0.5.1" + rustc 1.83.0 (90b35a623 2024-11-26) + OS: Ubuntu 22.04.4 LTS (5.15.167.4-microsoft-standard-WSL2) + Hardware: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz 2.79 GHz + RAM: 64.0 GB + | Test | Average time| + |-------------------------------------------------------|-------------| + | Counter_Add_Sorted | 160 ns | + | Counter_Add_Unsorted | 164 ns | + | Counter_Add_Sorted_With_Non_Static_Values | 238 ns | + | Counter_Overflow | 562 ns | + | ThreadLocal_Random_Generator_5 | 37 ns | +*/ + +use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; +use opentelemetry::{ + metrics::{Counter, MeterProvider as _}, + KeyValue, +}; +use opentelemetry_sdk::metrics::{ManualReader, SdkMeterProvider}; +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +use pprof::criterion::{Output, PProfProfiler}; +use rand::{ + rngs::{self}, + Rng, SeedableRng, +}; +use std::cell::RefCell; + +thread_local! { + /// Store random number generator for each thread + static CURRENT_RNG: RefCell = RefCell::new(rngs::SmallRng::from_os_rng()); +} + +static ATTRIBUTE_VALUES: [&str; 10] = [ + "value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8", "value9", + "value10", +]; + +// Run this benchmark with: +// cargo bench --bench metrics_counter +fn create_counter(name: &'static str) -> Counter { + let meter_provider: SdkMeterProvider = SdkMeterProvider::builder() + .with_reader(ManualReader::builder().build()) + .build(); + let meter = meter_provider.meter("benchmarks"); + + println!("Counter_Created"); + meter.u64_counter(name).build() +} + +fn criterion_benchmark(c: &mut Criterion) { + counter_add_sorted(c); + counter_add_unsorted(c); + + let attribute_values: [String; 10] = (1..=10) + .map(|i| format!("value{i}")) + .collect::>() + .try_into() + .expect("Expected a Vec of length 10"); + + counter_add_sorted_with_non_static_values(c, attribute_values); + + counter_overflow(c); + random_generator(c); +} + +fn counter_add_sorted(c: &mut Criterion) { + let counter = create_counter("Counter_Add_Sorted"); + c.bench_function("Counter_Add_Sorted", |b| { + b.iter_batched( + || { + // 4*4*10*10 = 1600 time series. + CURRENT_RNG.with(|rng| { + let mut rng = rng.borrow_mut(); + [ + rng.random_range(0..4), + rng.random_range(0..4), + rng.random_range(0..10), + rng.random_range(0..10), + ] + }) + }, + |rands| { + let index_first_attribute = rands[0]; + let index_second_attribute = rands[1]; + let index_third_attribute = rands[2]; + let index_fourth_attribute = rands[3]; + counter.add( + 1, + &[ + KeyValue::new("attribute1", ATTRIBUTE_VALUES[index_first_attribute]), + KeyValue::new("attribute2", ATTRIBUTE_VALUES[index_second_attribute]), + KeyValue::new("attribute3", ATTRIBUTE_VALUES[index_third_attribute]), + KeyValue::new("attribute4", ATTRIBUTE_VALUES[index_fourth_attribute]), + ], + ); + }, + BatchSize::SmallInput, + ); + }); +} + +fn counter_add_unsorted(c: &mut Criterion) { + let counter = create_counter("Counter_Add_Unsorted"); + c.bench_function("Counter_Add_Unsorted", |b| { + b.iter_batched( + || { + // 4*4*10*10 = 1600 time series. + CURRENT_RNG.with(|rng| { + let mut rng = rng.borrow_mut(); + [ + rng.random_range(0..4), + rng.random_range(0..4), + rng.random_range(0..10), + rng.random_range(0..10), + ] + }) + }, + |rands| { + let index_first_attribute = rands[0]; + let index_second_attribute = rands[1]; + let index_third_attribute = rands[2]; + let index_fourth_attribute = rands[3]; + counter.add( + 1, + &[ + KeyValue::new("attribute2", ATTRIBUTE_VALUES[index_second_attribute]), + KeyValue::new("attribute3", ATTRIBUTE_VALUES[index_third_attribute]), + KeyValue::new("attribute1", ATTRIBUTE_VALUES[index_first_attribute]), + KeyValue::new("attribute4", ATTRIBUTE_VALUES[index_fourth_attribute]), + ], + ); + }, + BatchSize::SmallInput, + ); + }); +} + +fn counter_add_sorted_with_non_static_values(c: &mut Criterion, attribute_values: [String; 10]) { + let counter = create_counter("Counter_Add_Sorted_With_Non_Static_Values"); + c.bench_function("Counter_Add_Sorted_With_Non_Static_Values", |b| { + b.iter_batched( + || { + // 4*4*10*10 = 1600 time series. + CURRENT_RNG.with(|rng| { + let mut rng = rng.borrow_mut(); + [ + rng.random_range(0..4), + rng.random_range(0..4), + rng.random_range(0..10), + rng.random_range(0..10), + ] + }) + }, + |rands| { + let index_first_attribute = rands[0]; + let index_second_attribute = rands[1]; + let index_third_attribute = rands[2]; + let index_fourth_attribute = rands[3]; + counter.add( + 1, + &[ + KeyValue::new( + "attribute1", + attribute_values[index_first_attribute].as_str().to_owned(), + ), + KeyValue::new( + "attribute2", + attribute_values[index_second_attribute].as_str().to_owned(), + ), + KeyValue::new( + "attribute3", + attribute_values[index_third_attribute].as_str().to_owned(), + ), + KeyValue::new( + "attribute4", + attribute_values[index_fourth_attribute].as_str().to_owned(), + ), + ], + ); + }, + BatchSize::SmallInput, + ); + }); +} + +fn counter_overflow(c: &mut Criterion) { + let counter = create_counter("Counter_Overflow"); + // Cause overflow. + for v in 0..2001 { + counter.add(100, &[KeyValue::new("A", v.to_string())]); + } + + c.bench_function("Counter_Overflow", |b| { + b.iter(|| { + // 4*4*10*10 = 1600 time series. + let rands = CURRENT_RNG.with(|rng| { + let mut rng = rng.borrow_mut(); + [ + rng.random_range(0..4), + rng.random_range(0..4), + rng.random_range(0..10), + rng.random_range(0..10), + ] + }); + let index_first_attribute = rands[0]; + let index_second_attribute = rands[1]; + let index_third_attribute = rands[2]; + let index_fourth_attribute = rands[3]; + counter.add( + 1, + &[ + KeyValue::new("attribute2", ATTRIBUTE_VALUES[index_second_attribute]), + KeyValue::new("attribute3", ATTRIBUTE_VALUES[index_third_attribute]), + KeyValue::new("attribute1", ATTRIBUTE_VALUES[index_first_attribute]), + KeyValue::new("attribute4", ATTRIBUTE_VALUES[index_fourth_attribute]), + ], + ); + }); + }); +} + +fn random_generator(c: &mut Criterion) { + c.bench_function("ThreadLocal_Random_Generator_5", |b| { + b.iter(|| { + let __i1 = CURRENT_RNG.with(|rng| { + let mut rng = rng.borrow_mut(); + [ + rng.random_range(0..4), + rng.random_range(0..4), + rng.random_range(0..10), + rng.random_range(0..10), + rng.random_range(0..10), + ] + }); + }); + }); +} + +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = criterion_benchmark +} +#[cfg(any(target_os = "windows", not(feature = "bench_profiling")))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_gauge.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_gauge.rs new file mode 100644 index 00000000..a9f4491b --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_gauge.rs @@ -0,0 +1,88 @@ +/* + The benchmark results: + criterion = "0.5.1" + rustc 1.82.0 (f6e511eec 2024-10-15) + OS: Ubuntu 22.04.4 LTS (5.15.167.4-microsoft-standard-WSL2) + Hardware: AMD EPYC 7763 64-Core Processor - 2.44 GHz, 16vCPUs, + RAM: 64.0 GB + | Test | Average time| + |--------------------------------|-------------| + | Gauge_Add | 187.49 ns | +*/ + +use criterion::{criterion_group, criterion_main, Criterion}; +use opentelemetry::{ + metrics::{Gauge, MeterProvider as _}, + KeyValue, +}; +use opentelemetry_sdk::metrics::{ManualReader, SdkMeterProvider}; +use rand::{ + rngs::{self}, + Rng, SeedableRng, +}; +use std::cell::RefCell; + +thread_local! { + /// Store random number generator for each thread + static CURRENT_RNG: RefCell = RefCell::new(rngs::SmallRng::from_os_rng()); +} + +static ATTRIBUTE_VALUES: [&str; 10] = [ + "value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8", "value9", + "value10", +]; + +// Run this benchmark with: +// cargo bench --bench metrics_gauge +fn create_gauge() -> Gauge { + let meter_provider: SdkMeterProvider = SdkMeterProvider::builder() + .with_reader(ManualReader::builder().build()) + .build(); + let meter = meter_provider.meter("benchmarks"); + + meter.u64_gauge("gauge_bench").build() +} + +fn criterion_benchmark(c: &mut Criterion) { + gauge_record(c); +} + +fn gauge_record(c: &mut Criterion) { + let gauge = create_gauge(); + c.bench_function("Gauge_Add", |b| { + b.iter(|| { + // 4*4*10*10 = 1600 time series. + let rands = CURRENT_RNG.with(|rng| { + let mut rng = rng.borrow_mut(); + [ + rng.random_range(0..4), + rng.random_range(0..4), + rng.random_range(0..10), + rng.random_range(0..10), + ] + }); + let index_first_attribute = rands[0]; + let index_second_attribute = rands[1]; + let index_third_attribute = rands[2]; + let index_fourth_attribute = rands[3]; + gauge.record( + 1, + &[ + KeyValue::new("attribute1", ATTRIBUTE_VALUES[index_first_attribute]), + KeyValue::new("attribute2", ATTRIBUTE_VALUES[index_second_attribute]), + KeyValue::new("attribute3", ATTRIBUTE_VALUES[index_third_attribute]), + KeyValue::new("attribute4", ATTRIBUTE_VALUES[index_fourth_attribute]), + ], + ); + }); + }); +} + +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_histogram.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_histogram.rs new file mode 100644 index 00000000..cfc75927 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/metrics_histogram.rs @@ -0,0 +1,163 @@ +/* + The benchmark results: + criterion = "0.5.1" + rustc 1.83.0 (90b35a623 2024-11-26) + OS: Ubuntu 22.04.4 LTS (5.15.167.4-microsoft-standard-WSL2) + Hardware: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz 2.79 GHz + RAM: 64.0 GB + | Test | Average time| + |-------------------------------------------------------|-------------| + | Histogram_Record | 186.24 ns | + | Histogram_Record_With_Non_Static_Values | 264.70 ns | + +*/ + +use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; +use opentelemetry::{ + metrics::{Histogram, MeterProvider as _}, + KeyValue, +}; +use opentelemetry_sdk::metrics::{ManualReader, SdkMeterProvider}; +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +use pprof::criterion::{Output, PProfProfiler}; +use rand::{ + rngs::{self}, + Rng, SeedableRng, +}; +use std::cell::RefCell; + +thread_local! { + /// Store random number generator for each thread + static CURRENT_RNG: RefCell = RefCell::new(rngs::SmallRng::from_os_rng()); +} + +static ATTRIBUTE_VALUES: [&str; 10] = [ + "value1", "value2", "value3", "value4", "value5", "value6", "value7", "value8", "value9", + "value10", +]; + +// Run this benchmark with: +// cargo bench --bench metrics_histogram +fn create_histogram(name: &'static str) -> Histogram { + let meter_provider: SdkMeterProvider = SdkMeterProvider::builder() + .with_reader(ManualReader::builder().build()) + .build(); + let meter = meter_provider.meter("benchmarks"); + + meter.u64_histogram(name).build() +} + +fn criterion_benchmark(c: &mut Criterion) { + histogram_record(c); + + let attribute_values: [String; 10] = (1..=10) + .map(|i| format!("value{i}")) + .collect::>() + .try_into() + .expect("Expected a Vec of length 10"); + + histogram_record_with_non_static_values(c, attribute_values); +} + +fn histogram_record(c: &mut Criterion) { + let histogram = create_histogram("Histogram_Record"); + c.bench_function("Histogram_Record", |b| { + b.iter_batched( + || { + // 4*4*10*10 = 1600 time series. + CURRENT_RNG.with(|rng| { + let mut rng = rng.borrow_mut(); + [ + rng.random_range(0..4), + rng.random_range(0..4), + rng.random_range(0..10), + rng.random_range(0..10), + ] + }) + }, + |rands| { + let index_first_attribute = rands[0]; + let index_second_attribute = rands[1]; + let index_third_attribute = rands[2]; + let index_fourth_attribute = rands[3]; + histogram.record( + 1, + &[ + KeyValue::new("attribute1", ATTRIBUTE_VALUES[index_first_attribute]), + KeyValue::new("attribute2", ATTRIBUTE_VALUES[index_second_attribute]), + KeyValue::new("attribute3", ATTRIBUTE_VALUES[index_third_attribute]), + KeyValue::new("attribute4", ATTRIBUTE_VALUES[index_fourth_attribute]), + ], + ); + }, + BatchSize::SmallInput, + ); + }); +} + +fn histogram_record_with_non_static_values(c: &mut Criterion, attribute_values: [String; 10]) { + let histogram = create_histogram("Histogram_Record_With_Non_Static_Values"); + c.bench_function("Histogram_Record_With_Non_Static_Values", |b| { + b.iter_batched( + || { + // 4*4*10*10 = 1600 time series. + CURRENT_RNG.with(|rng| { + let mut rng = rng.borrow_mut(); + [ + rng.random_range(0..4), + rng.random_range(0..4), + rng.random_range(0..10), + rng.random_range(0..10), + ] + }) + }, + |rands| { + let index_first_attribute = rands[0]; + let index_second_attribute = rands[1]; + let index_third_attribute = rands[2]; + let index_fourth_attribute = rands[3]; + histogram.record( + 1, + &[ + KeyValue::new( + "attribute1", + attribute_values[index_first_attribute].as_str().to_owned(), + ), + KeyValue::new( + "attribute2", + attribute_values[index_second_attribute].as_str().to_owned(), + ), + KeyValue::new( + "attribute3", + attribute_values[index_third_attribute].as_str().to_owned(), + ), + KeyValue::new( + "attribute4", + attribute_values[index_fourth_attribute].as_str().to_owned(), + ), + ], + ); + }, + BatchSize::SmallInput, + ); + }); +} + +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = criterion_benchmark +} +#[cfg(any(target_os = "windows", not(feature = "bench_profiling")))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/span.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/span.rs new file mode 100644 index 00000000..62a1ee86 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/span.rs @@ -0,0 +1,218 @@ +/* + Span Creation scenarios. + This benchmark measures the performance cost of different + span creation patterns in OpenTelemetry when using On/Off + sampling strategies. + + The benchmark results: + criterion = "0.5.1" + cargo 1.91.1 (ea2d97820 2025-10-10) + Hardware: M4Pro + | Test | Always Sample | Never Sample | + |-------------------------------------------------------|---------------|--------------| + | span-creation-simple | 203.67 ns | 51.316 ns | + | span-creation-span-builder | 209.55 ns | 88.032 ns | + | span-creation-tracer-in-span | 339.52 ns | 150.55 ns | + | span-creation-tracer-in-span-with-builder | 317.44 ns | 170.38 ns | + | span-creation-simple-context-activation | 329.41 ns | 47.681 ns | + | span-creation-span-builder-context-activation | 328.84 ns | 78.021 ns | +*/ + +use criterion::{criterion_group, criterion_main, Criterion}; +use opentelemetry::{ + trace::{mark_span_as_active, Span, SpanBuilder, TraceContextExt, Tracer, TracerProvider}, + Context, KeyValue, +}; +use opentelemetry_sdk::{ + error::OTelSdkResult, + trace::{self as sdktrace, SpanData, SpanExporter}, +}; +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +use pprof::criterion::{Output, PProfProfiler}; + +fn criterion_benchmark(c: &mut Criterion) { + trace_benchmark_group(c, "span-creation-simple", |tracer| { + // Simple span creation + // There is not ability to specify anything other than the name. + // Attributes are set after creation, and automatically gets + // ignored if sampling is unfavorable. + let mut span = tracer.start("span-name"); + if span.is_recording() { + span.set_attribute(KeyValue::new("key1", false)); + span.set_attribute(KeyValue::new("key2", "hello")); + span.set_attribute(KeyValue::new("key3", 123.456)); + span.set_attribute(KeyValue::new("key4", "world")); + span.set_attribute(KeyValue::new("key5", 123)); + } + span.end(); + }); + + trace_benchmark_group(c, "span-creation-span-builder", |tracer| { + // This is similar to the simple span creation, but allows + // attributes and other properties to be set during creation. + // It is slightly slower than the simple span creation due to the fact that + // attributes are collected into a vec! and allocated, even before sampling + // decision is made. + let mut span = tracer + .span_builder("span-name") + .with_attributes([ + KeyValue::new("key1", false), + KeyValue::new("key2", "hello"), + KeyValue::new("key3", 123.456), + ]) + .start(tracer); + if span.is_recording() { + span.set_attribute(KeyValue::new("key4", "world")); + span.set_attribute(KeyValue::new("key5", 123)); + } + span.end(); + }); + + trace_benchmark_group(c, "span-creation-tracer-in-span", |tracer| { + // This is similar to the simple span creation, but also does the job of activating + // the span in the current context. + // It is slower than other approaches of activation due to the fact that + // context activation is done, irrespective of sampling decision. + tracer.in_span("span-name", |ctx| { + let span = ctx.span(); + if span.is_recording() { + span.set_attribute(KeyValue::new("key1", false)); + span.set_attribute(KeyValue::new("key2", "hello")); + span.set_attribute(KeyValue::new("key3", 123.456)); + span.set_attribute(KeyValue::new("key4", "world")); + span.set_attribute(KeyValue::new("key5", 123)); + } + }); + }); + + trace_benchmark_group(c, "span-creation-tracer-in-span-with-builder", |tracer| { + // This is similar to the simple span creation, but also does the job of activating + // the span in the current context. + tracer.in_span_with_builder( + SpanBuilder::from_name("span-name").with_attributes([ + KeyValue::new("key1", false), + KeyValue::new("key2", "hello"), + KeyValue::new("key3", 123.456), + ]), + |ctx| { + let span = ctx.span(); + if span.is_recording() { + span.set_attribute(KeyValue::new("key4", "world")); + span.set_attribute(KeyValue::new("key5", 123)); + } + }, + ); + }); + + trace_benchmark_group(c, "span-creation-simple-context-activation", |tracer| { + // This optimizes by bypassing the context activation + // based on sampling decision, and hence it is faster than the + // tracer.in_span approach. + let mut span = tracer.start("span-name"); + if span.is_recording() { + span.set_attribute(KeyValue::new("key1", false)); + span.set_attribute(KeyValue::new("key2", "hello")); + span.set_attribute(KeyValue::new("key3", 123.456)); + let _guard = mark_span_as_active(span); + Context::map_current(|cx| { + let span_from_context = cx.span(); + span_from_context.set_attribute(KeyValue::new("key4", "world")); + span_from_context.set_attribute(KeyValue::new("key5", 123)); + }); + } + }); + + trace_benchmark_group( + c, + "span-creation-span-builder-context-activation", + |tracer| { + // This optimizes by bypassing the context activation + // based on sampling decision, and hence it is faster than the + // tracer.in_span approach. + let span = tracer + .span_builder("span-name") + .with_attributes([ + KeyValue::new("key1", false), + KeyValue::new("key2", "hello"), + KeyValue::new("key3", 123.456), + ]) + .start(tracer); + if span.is_recording() { + let _guard = mark_span_as_active(span); + Context::map_current(|cx| { + let span_from_context = cx.span(); + span_from_context.set_attribute(KeyValue::new("key4", "world")); + span_from_context.set_attribute(KeyValue::new("key5", 123)); + }); + } + }, + ); + + let provider = sdktrace::SdkTracerProvider::builder() + .with_sampler(sdktrace::Sampler::AlwaysOn) + .with_simple_exporter(VoidExporter) + .build(); + let tracer = provider.tracer("active-parent"); + let parent_cx = Context::new().with_span(tracer.start("parent")); + + c.bench_function("span-creation-with-active-parent", |b| { + b.iter(|| { + let mut span = tracer.start_with_context("span-name", &parent_cx); + span.end(); + }); + }); +} + +#[derive(Debug)] +struct VoidExporter; + +impl SpanExporter for VoidExporter { + async fn export(&self, _spans: Vec) -> OTelSdkResult { + Ok(()) + } +} + +fn trace_benchmark_group(c: &mut Criterion, name: &str, f: F) { + let mut group = c.benchmark_group(name); + + group.bench_function("always-sample", |b| { + let provider = sdktrace::SdkTracerProvider::builder() + .with_sampler(sdktrace::Sampler::AlwaysOn) + .with_simple_exporter(VoidExporter) + .build(); + let always_sample = provider.tracer("always-sample"); + + b.iter(|| f(&always_sample)); + }); + + group.bench_function("never-sample", |b| { + let provider = sdktrace::SdkTracerProvider::builder() + .with_sampler(sdktrace::Sampler::AlwaysOff) + .with_simple_exporter(VoidExporter) + .build(); + let never_sample = provider.tracer("never-sample"); + b.iter(|| f(&never_sample)); + }); + + group.finish(); +} + +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = criterion_benchmark +} + +#[cfg(any(target_os = "windows", not(feature = "bench_profiling")))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/span_builder.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/span_builder.rs new file mode 100644 index 00000000..b407bdd6 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/span_builder.rs @@ -0,0 +1,100 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use opentelemetry::{ + trace::{Span, Tracer, TracerProvider}, + KeyValue, +}; +use opentelemetry_sdk::error::OTelSdkResult; +use opentelemetry_sdk::{ + trace as sdktrace, + trace::{SpanData, SpanExporter}, +}; +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +use pprof::criterion::{Output, PProfProfiler}; + +fn criterion_benchmark(c: &mut Criterion) { + span_builder_benchmark_group(c) +} + +fn span_builder_benchmark_group(c: &mut Criterion) { + let mut group = c.benchmark_group("span_builder"); + group.bench_function("simplest", |b| { + let (_provider, tracer) = not_sampled_provider(); + b.iter(|| { + let mut span = tracer.span_builder("span").start(&tracer); + span.end(); + }) + }); + group.bench_function(BenchmarkId::new("with_attributes", "1"), |b| { + let (_provider, tracer) = not_sampled_provider(); + b.iter(|| { + let mut span = tracer + .span_builder("span") + .with_attributes([KeyValue::new(MAP_KEYS[0], "value")]) + .start(&tracer); + span.end(); + }) + }); + group.bench_function(BenchmarkId::new("with_attributes", "4"), |b| { + let (_provider, tracer) = not_sampled_provider(); + b.iter(|| { + let mut span = tracer + .span_builder("span") + .with_attributes([ + KeyValue::new(MAP_KEYS[0], "value"), + KeyValue::new(MAP_KEYS[1], "value"), + KeyValue::new(MAP_KEYS[2], "value"), + KeyValue::new(MAP_KEYS[3], "value"), + ]) + .start(&tracer); + span.end(); + }) + }); + group.finish(); +} + +fn not_sampled_provider() -> (sdktrace::SdkTracerProvider, sdktrace::SdkTracer) { + let provider = sdktrace::SdkTracerProvider::builder() + .with_sampler(sdktrace::Sampler::AlwaysOff) + .with_simple_exporter(NoopExporter) + .build(); + let tracer = provider.tracer("not-sampled"); + (provider, tracer) +} + +#[derive(Debug)] +struct NoopExporter; + +impl SpanExporter for NoopExporter { + async fn export(&self, _spans: Vec) -> OTelSdkResult { + Ok(()) + } +} + +const MAP_KEYS: [&str; 64] = [ + "key.1", "key.2", "key.3", "key.4", "key.5", "key.6", "key.7", "key.8", "key.9", "key.10", + "key.11", "key.12", "key.13", "key.14", "key.15", "key.16", "key.17", "key.18", "key.19", + "key.20", "key.21", "key.22", "key.23", "key.24", "key.25", "key.26", "key.27", "key.28", + "key.29", "key.30", "key.31", "key.32", "key.33", "key.34", "key.35", "key.36", "key.37", + "key.38", "key.39", "key.40", "key.41", "key.42", "key.43", "key.44", "key.45", "key.46", + "key.47", "key.48", "key.49", "key.50", "key.51", "key.52", "key.53", "key.54", "key.55", + "key.56", "key.57", "key.58", "key.59", "key.60", "key.61", "key.62", "key.63", "key.64", +]; + +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = criterion_benchmark +} +#[cfg(any(target_os = "windows", not(feature = "bench_profiling")))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/trace.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/trace.rs new file mode 100644 index 00000000..dcafa4d5 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/trace.rs @@ -0,0 +1,109 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use opentelemetry::{ + trace::{Span, Tracer, TracerProvider}, + KeyValue, +}; +use opentelemetry_sdk::{ + error::OTelSdkResult, + trace::{self as sdktrace, SpanData, SpanExporter}, +}; +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +use pprof::criterion::{Output, PProfProfiler}; + +fn criterion_benchmark(c: &mut Criterion) { + trace_benchmark_group(c, "start-end-span", |tracer| tracer.start("foo").end()); + + trace_benchmark_group(c, "start-end-span-4-attrs", |tracer| { + let mut span = tracer.start("foo"); + span.set_attribute(KeyValue::new("key1", false)); + span.set_attribute(KeyValue::new("key2", "hello")); + span.set_attribute(KeyValue::new("key4", 123.456)); + span.end(); + }); + + trace_benchmark_group(c, "start-end-span-8-attrs", |tracer| { + let mut span = tracer.start("foo"); + span.set_attribute(KeyValue::new("key1", false)); + span.set_attribute(KeyValue::new("key2", "hello")); + span.set_attribute(KeyValue::new("key4", 123.456)); + span.set_attribute(KeyValue::new("key11", false)); + span.set_attribute(KeyValue::new("key12", "hello")); + span.set_attribute(KeyValue::new("key14", 123.456)); + span.end(); + }); + + trace_benchmark_group(c, "start-end-span-all-attr-types", |tracer| { + let mut span = tracer.start("foo"); + span.set_attribute(KeyValue::new("key1", false)); + span.set_attribute(KeyValue::new("key2", "hello")); + span.set_attribute(KeyValue::new("key3", 123)); + span.set_attribute(KeyValue::new("key5", 123.456)); + span.end(); + }); + + trace_benchmark_group(c, "start-end-span-all-attr-types-2x", |tracer| { + let mut span = tracer.start("foo"); + span.set_attribute(KeyValue::new("key1", false)); + span.set_attribute(KeyValue::new("key2", "hello")); + span.set_attribute(KeyValue::new("key3", 123)); + span.set_attribute(KeyValue::new("key5", 123.456)); + span.set_attribute(KeyValue::new("key11", false)); + span.set_attribute(KeyValue::new("key12", "hello")); + span.set_attribute(KeyValue::new("key13", 123)); + span.set_attribute(KeyValue::new("key15", 123.456)); + span.end(); + }); +} + +#[derive(Debug)] +struct VoidExporter; + +impl SpanExporter for VoidExporter { + async fn export(&self, _spans: Vec) -> OTelSdkResult { + Ok(()) + } +} + +fn trace_benchmark_group(c: &mut Criterion, name: &str, f: F) { + let mut group = c.benchmark_group(name); + + group.bench_function("always-sample", |b| { + let provider = sdktrace::SdkTracerProvider::builder() + .with_sampler(sdktrace::Sampler::AlwaysOn) + .with_simple_exporter(VoidExporter) + .build(); + let always_sample = provider.tracer("always-sample"); + + b.iter(|| f(&always_sample)); + }); + + group.bench_function("never-sample", |b| { + let provider = sdktrace::SdkTracerProvider::builder() + .with_sampler(sdktrace::Sampler::AlwaysOff) + .with_simple_exporter(VoidExporter) + .build(); + let never_sample = provider.tracer("never-sample"); + b.iter(|| f(&never_sample)); + }); + + group.finish(); +} + +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)) + .with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))); + targets = criterion_benchmark +} +#[cfg(any(target_os = "windows", not(feature = "bench_profiling")))] +criterion_group! { + name = benches; + config = Criterion::default() + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/benches/tracer_creation.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/tracer_creation.rs new file mode 100644 index 00000000..9524f3ae --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/benches/tracer_creation.rs @@ -0,0 +1,104 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use opentelemetry::{ + global::{self, BoxedTracer}, + InstrumentationScope, KeyValue, +}; +use opentelemetry_sdk::trace as sdktrace; +use std::hint::black_box; +use std::sync::OnceLock; + +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +use pprof::criterion::{Output, PProfProfiler}; + +/* +Adding results in comments for a quick reference. +Apple M4 Pro + Total Number of Cores: 14 (10 performance and 4 efficiency) + +Tracer_With_Name/new_each_time 20 ns +Tracer_With_Name/reuse_existing 383 ps +Tracer_With_Name_And_Scope_Attrs/new_each_time 63 ns +Tracer_With_Name_And_Scope_Attrs/reuse_existing 385 ps +*/ + +fn get_tracer() -> &'static BoxedTracer { + static TRACER: OnceLock = OnceLock::new(); + TRACER.get_or_init(|| global::tracer("tracer")) +} + +fn get_tracer_with_scope_attrs() -> &'static BoxedTracer { + static TRACER_WITH_ATTRS: OnceLock = OnceLock::new(); + TRACER_WITH_ATTRS.get_or_init(|| { + let scope = InstrumentationScope::builder("tracer") + .with_attributes([KeyValue::new("key", "value")]) + .build(); + global::tracer_with_scope(scope) + }) +} + +fn create_provider() -> sdktrace::SdkTracerProvider { + // Provider is empty, no exporters, no processors etc. + // as the goal is measurement of tracer creation time. + sdktrace::SdkTracerProvider::builder().build() +} + +fn criterion_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("Tracer_With_Name"); + group.bench_function("new_each_time", |b| { + let provider = create_provider(); + global::set_tracer_provider(provider); + b.iter(|| { + black_box(global::tracer("tracer")); + }); + }); + + group.bench_function("reuse_existing", |b| { + let provider = create_provider(); + global::set_tracer_provider(provider); + b.iter(|| { + black_box(get_tracer()); + }); + }); + + group.finish(); + + let mut group = c.benchmark_group("Tracer_With_Name_And_Scope_Attrs"); + group.bench_function("new_each_time", |b| { + let provider = create_provider(); + global::set_tracer_provider(provider); + b.iter(|| { + let scope = InstrumentationScope::builder("tracer") + .with_attributes([KeyValue::new("key", "value")]) + .build(); + black_box(global::tracer_with_scope(scope)); + }); + }); + + group.bench_function("reuse_existing", |b| { + let provider = create_provider(); + global::set_tracer_provider(provider); + b.iter(|| { + black_box(get_tracer_with_scope_attrs()); + }); + }); + group.finish(); +} + +#[cfg(all(not(target_os = "windows"), feature = "bench_profiling"))] +criterion_group! { + name = benches; + config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None))) + .warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} + +#[cfg(any(target_os = "windows", not(feature = "bench_profiling")))] +criterion_group! { + name = benches; + config = Criterion::default().warm_up_time(std::time::Duration::from_secs(1)) + .measurement_time(std::time::Duration::from_secs(2)); + targets = criterion_benchmark +} + +criterion_main!(benches); diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/error.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/error.rs new file mode 100644 index 00000000..47232b9b --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/error.rs @@ -0,0 +1,82 @@ +//! Wrapper for error from trace, logs and metrics part of open telemetry. + +use std::{fmt, result::Result, time::Duration}; + +/// Trait for errors returned by exporters +pub trait ExportError: std::error::Error + Send + Sync + 'static { + /// The name of exporter that returned this error + fn exporter_name(&self) -> &'static str; +} + +#[derive(Debug)] +/// Errors that can occur during SDK operations export(), force_flush() and shutdown(). +pub enum OTelSdkError { + /// Shutdown has already been invoked. + /// + /// While shutdown is idempotent and calling it multiple times has no + /// impact, this error suggests that another part of the application is + /// invoking `shutdown` earlier than intended. Users should review their + /// code to identify unintended or duplicate shutdown calls and ensure it is + /// only triggered once at the correct place. + AlreadyShutdown, + + /// Operation timed out before completing. + /// + /// This does not necessarily indicate a failure—operation may still be + /// complete. If this occurs frequently, consider increasing the timeout + /// duration to allow more time for completion. + Timeout(Duration), + + /// Operation failed due to an internal error. + /// + /// The error message is intended for logging purposes only and should not + /// be used to make programmatic decisions. It is implementation-specific + /// and subject to change without notice. Consumers of this error should not + /// rely on its content beyond logging. + InternalFailure(String), +} + +impl fmt::Display for OTelSdkError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OTelSdkError::AlreadyShutdown => write!(f, "Shutdown already invoked"), + OTelSdkError::Timeout(duration) => { + write!(f, "Operation timed out after {duration:?}") + } + OTelSdkError::InternalFailure(reason) => write!(f, "Operation failed: {reason}"), + } + } +} + +impl std::error::Error for OTelSdkError {} + +#[cfg(any(feature = "testing", test))] +impl From> for OTelSdkError { + fn from(err: std::sync::PoisonError) -> Self { + OTelSdkError::InternalFailure(format!("Mutex poison error: {err}")) + } +} + +/// A specialized `Result` type for Shutdown operations. +pub type OTelSdkResult = Result<(), OTelSdkError>; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_matches_variants() { + assert_eq!( + OTelSdkError::AlreadyShutdown.to_string(), + "Shutdown already invoked" + ); + assert_eq!( + OTelSdkError::Timeout(Duration::from_secs(5)).to_string(), + "Operation timed out after 5s" + ); + assert_eq!( + OTelSdkError::InternalFailure("db unreachable".into()).to_string(), + "Operation failed: db unreachable" + ); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/growable_array.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/growable_array.rs new file mode 100644 index 00000000..525f44e9 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/growable_array.rs @@ -0,0 +1,369 @@ +/// The default max capacity for the stack portion of `GrowableArray`. +const DEFAULT_MAX_INLINE_CAPACITY: usize = 10; +/// The default initial capacity for the vector portion of `GrowableArray`. +const DEFAULT_INITIAL_OVERFLOW_CAPACITY: usize = 5; + +#[derive(Debug, Clone, PartialEq)] +/// A hybrid vector that starts with a fixed-size array and grows dynamically with a vector. +/// +/// `GrowableArray` uses an internal fixed-size array (`inline`) for storing elements until it reaches +/// `MAX_INLINE_CAPACITY`. When this capacity is exceeded, additional elements are stored in a heap-allocated +/// vector (`overflow`). This structure allows for efficient use of stack memory for small numbers of elements, +/// while still supporting dynamic growth. +/// +pub(crate) struct GrowableArray< + T: Default + Clone + PartialEq, + const MAX_INLINE_CAPACITY: usize = DEFAULT_MAX_INLINE_CAPACITY, + const INITIAL_OVERFLOW_CAPACITY: usize = DEFAULT_INITIAL_OVERFLOW_CAPACITY, +> { + inline: [T; MAX_INLINE_CAPACITY], + overflow: Option>, + count: usize, +} + +impl< + T: Default + Clone + PartialEq, + const MAX_INLINE_CAPACITY: usize, + const INITIAL_OVERFLOW_CAPACITY: usize, + > Default for GrowableArray +{ + fn default() -> Self { + Self { + inline: [(); MAX_INLINE_CAPACITY].map(|_| T::default()), + overflow: None, + count: 0, + } + } +} + +impl< + T: Default + Clone + PartialEq, + const MAX_INLINE_CAPACITY: usize, + const INITIAL_OVERFLOW_CAPACITY: usize, + > GrowableArray +{ + /// Creates a new `GrowableArray` with the default initial capacity. + #[allow(dead_code)] + pub(crate) fn new() -> Self { + Self::default() + } + + /// Pushes a value into the `GrowableArray`. + /// + /// If the internal array (`inline`) has reached its capacity (`MAX_INLINE_CAPACITY`), the value is pushed + /// into the heap-allocated vector (`overflow`). Otherwise, it is stored in the array. + #[allow(dead_code)] + #[inline] + pub(crate) fn push(&mut self, value: T) { + if self.count < MAX_INLINE_CAPACITY { + self.inline[self.count] = value; + self.count += 1; + } else { + self.overflow + .get_or_insert_with(|| Vec::with_capacity(INITIAL_OVERFLOW_CAPACITY)) + .push(value); + } + } + + /// Gets a reference to the value at the specified index. + /// + /// Returns `None` if the index is out of bounds. + #[allow(dead_code)] + #[inline] + pub(crate) fn get(&self, index: usize) -> Option<&T> { + if index < self.count { + Some(&self.inline[index]) + } else if let Some(ref overflow) = self.overflow { + overflow.get(index - MAX_INLINE_CAPACITY) + } else { + None + } + } + + /// Returns the number of elements in the `GrowableArray`. + #[allow(dead_code)] + #[inline] + pub(crate) fn len(&self) -> usize { + self.count + self.overflow.as_ref().map_or(0, Vec::len) + } + + /// Returns an iterator over the elements in the `GrowableArray`. + /// + /// The iterator yields elements from the internal array (`initial`) first, followed by elements + /// from the vector (`overflow`) if present. This allows for efficient iteration over both + /// stack-allocated and heap-allocated portions. + /// + #[allow(dead_code)] + #[inline] + pub(crate) fn iter(&self) -> impl Iterator { + let overflow_slice = match &self.overflow { + Some(v) if !v.is_empty() => v.as_slice(), + _ => &[], + }; + self.inline + .iter() + .take(self.count) + .chain(overflow_slice.iter()) + } +} + +// Implement `IntoIterator` for `GrowableArray` +impl IntoIterator + for GrowableArray +{ + type Item = T; + type IntoIter = GrowableArrayIntoIter; + + fn into_iter(self) -> Self::IntoIter { + GrowableArrayIntoIter::::new(self) + } +} + +/// Iterator for consuming a `GrowableArray`. +/// +#[derive(Debug)] +pub(crate) struct GrowableArrayIntoIter< + T: Default + Clone + PartialEq, + const INLINE_CAPACITY: usize, +> { + iter: std::iter::Chain< + std::iter::Take>, + std::vec::IntoIter, + >, +} + +impl + GrowableArrayIntoIter +{ + fn new(source: GrowableArray) -> Self { + Self { + iter: Self::get_iterator(source), + } + } + + fn get_iterator( + source: GrowableArray, + ) -> std::iter::Chain< + std::iter::Take>, + std::vec::IntoIter, + > { + let overflow_vec = match source.overflow { + Some(v) if !v.is_empty() => v, + _ => Vec::new(), + }; + source + .inline + .into_iter() + .take(source.count) + .chain(overflow_vec) + } +} + +impl Iterator + for GrowableArrayIntoIter +{ + type Item = T; + + fn next(&mut self) -> Option { + self.iter.next() + } +} + +#[cfg(test)] +mod tests { + use crate::growable_array::{ + GrowableArray, DEFAULT_INITIAL_OVERFLOW_CAPACITY, DEFAULT_MAX_INLINE_CAPACITY, + }; + use opentelemetry::logs::AnyValue; + use opentelemetry::Key; + + type KeyValuePair = Option<(Key, AnyValue)>; + + #[test] + fn test_push_and_get() { + let mut collection = GrowableArray::::new(); + for i in 0..15 { + collection.push(i); + } + for i in 0..15 { + assert_eq!(collection.get(i), Some(&(i as i32))); + } + } + + #[test] + fn test_len() { + let mut collection = GrowableArray::::new(); + for i in 0..15 { + collection.push(i); + } + assert_eq!(collection.len(), 15); + } + + #[test] + fn test_into_iter() { + let mut collection = GrowableArray::::new(); + for i in 0..15 { + collection.push(i); + } + let mut iter = collection.into_iter(); + for i in 0..15 { + assert_eq!(iter.next(), Some(i)); + } + assert_eq!(iter.next(), None); + } + + #[test] + fn test_ref_iter() { + let mut collection = GrowableArray::::new(); + for i in 0..15 { + collection.push(i); + } + let iter = collection.iter(); + let mut count = 0; + for value in iter { + assert_eq!(*value, count); + count += 1; + } + assert_eq!(count, 15); + } + + #[test] + fn test_key_value_pair_storage_growable_array() { + let mut collection = GrowableArray::::new(); + + let key1 = Key::from("key1"); + let value1 = AnyValue::String("value1".into()); + let key2 = Key::from("key2"); + let value2 = AnyValue::Int(42); + + collection.push(Some((key1.clone(), value1.clone()))); + collection.push(Some((key2.clone(), value2.clone()))); + + assert_eq!( + collection + .get(0) + .and_then(|kv| kv.as_ref().map(|kv| (&kv.0, &kv.1))), + Some((&key1, &value1)) + ); + assert_eq!( + collection + .get(1) + .and_then(|kv| kv.as_ref().map(|kv| (&kv.0, &kv.1))), + Some((&key2, &value2)) + ); + assert_eq!(collection.len(), 2); + + // Test iterating over the key-value pairs + let mut iter = collection.into_iter(); + assert_eq!(iter.next(), Some(Some((key1, value1)))); + assert_eq!(iter.next(), Some(Some((key2, value2)))); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_empty_attributes() { + let collection = GrowableArray::::new(); + assert_eq!(collection.len(), 0); + assert_eq!(collection.get(0), None); + + let mut iter = collection.into_iter(); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_less_than_max_stack_capacity() { + let mut collection = GrowableArray::::new(); + for i in 0..DEFAULT_MAX_INLINE_CAPACITY - 1 { + collection.push(i as i32); + } + assert_eq!(collection.len(), DEFAULT_MAX_INLINE_CAPACITY - 1); + + for i in 0..DEFAULT_MAX_INLINE_CAPACITY - 1 { + assert_eq!(collection.get(i), Some(&(i as i32))); + } + assert_eq!(collection.get(DEFAULT_MAX_INLINE_CAPACITY - 1), None); + assert_eq!(collection.get(DEFAULT_MAX_INLINE_CAPACITY), None); + + let mut iter = collection.into_iter(); + for i in 0..DEFAULT_MAX_INLINE_CAPACITY - 1 { + assert_eq!(iter.next(), Some(i as i32)); + } + assert_eq!(iter.next(), None); + } + + #[test] + fn test_exactly_max_stack_capacity() { + let mut collection = GrowableArray::::new(); + for i in 0..DEFAULT_MAX_INLINE_CAPACITY { + collection.push(i as i32); + } + assert_eq!(collection.len(), DEFAULT_MAX_INLINE_CAPACITY); + + for i in 0..DEFAULT_MAX_INLINE_CAPACITY { + assert_eq!(collection.get(i), Some(&(i as i32))); + } + assert_eq!(collection.get(DEFAULT_MAX_INLINE_CAPACITY), None); + + let mut iter = collection.into_iter(); + for i in 0..DEFAULT_MAX_INLINE_CAPACITY { + assert_eq!(iter.next(), Some(i as i32)); + } + assert_eq!(iter.next(), None); + } + + #[test] + fn test_exceeds_stack_but_not_initial_vec_capacity() { + let mut collection = GrowableArray::::new(); + for i in 0..(DEFAULT_MAX_INLINE_CAPACITY + DEFAULT_INITIAL_OVERFLOW_CAPACITY - 1) { + collection.push(i as i32); + } + assert_eq!( + collection.len(), + DEFAULT_MAX_INLINE_CAPACITY + DEFAULT_INITIAL_OVERFLOW_CAPACITY - 1 + ); + + for i in 0..(DEFAULT_MAX_INLINE_CAPACITY + DEFAULT_INITIAL_OVERFLOW_CAPACITY - 1) { + assert_eq!(collection.get(i), Some(&(i as i32))); + } + assert_eq!( + collection.get(DEFAULT_MAX_INLINE_CAPACITY + DEFAULT_INITIAL_OVERFLOW_CAPACITY - 1), + None + ); + assert_eq!( + collection.get(DEFAULT_MAX_INLINE_CAPACITY + DEFAULT_INITIAL_OVERFLOW_CAPACITY), + None + ); + + let mut iter = collection.into_iter(); + for i in 0..(DEFAULT_MAX_INLINE_CAPACITY + DEFAULT_INITIAL_OVERFLOW_CAPACITY - 1) { + assert_eq!(iter.next(), Some(i as i32)); + } + assert_eq!(iter.next(), None); + } + + #[test] + fn test_exceeds_both_stack_and_initial_vec_capacities() { + let mut collection = GrowableArray::::new(); + for i in 0..(DEFAULT_MAX_INLINE_CAPACITY + DEFAULT_INITIAL_OVERFLOW_CAPACITY + 5) { + collection.push(i as i32); + } + assert_eq!( + collection.len(), + DEFAULT_MAX_INLINE_CAPACITY + DEFAULT_INITIAL_OVERFLOW_CAPACITY + 5 + ); + + for i in 0..(DEFAULT_MAX_INLINE_CAPACITY + DEFAULT_INITIAL_OVERFLOW_CAPACITY + 5) { + assert_eq!(collection.get(i), Some(&(i as i32))); + } + assert_eq!( + collection.get(DEFAULT_MAX_INLINE_CAPACITY + DEFAULT_INITIAL_OVERFLOW_CAPACITY + 5), + None + ); + + let mut iter = collection.into_iter(); + for i in 0..(DEFAULT_MAX_INLINE_CAPACITY + DEFAULT_INITIAL_OVERFLOW_CAPACITY + 5) { + assert_eq!(iter.next(), Some(i as i32)); + } + assert_eq!(iter.next(), None); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/lib.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/lib.rs new file mode 100644 index 00000000..42348d9e --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/lib.rs @@ -0,0 +1,194 @@ +//! Implements the [`SDK`] component of [OpenTelemetry]. +//! +//! *[Supported Rust Versions](#supported-rust-versions)* +//! +//! [`SDK`]: https://opentelemetry.io/docs/specs/otel/overview/#sdk +//! [OpenTelemetry]: https://opentelemetry.io/docs/what-is-opentelemetry/ +//! [msrv]: #supported-rust-versions +//! +//! # Getting Started +//! +//! ```no_run +//! # #[cfg(feature = "trace")] +//! # { +//! use opentelemetry::{global, trace::{Tracer, TracerProvider}}; +//! use opentelemetry_sdk::trace::SdkTracerProvider; +//! +//! fn main() { +//! // Choose an exporter like `opentelemetry_stdout::SpanExporter` +//! # fn example(new_exporter: impl Fn() -> T) { +//! let exporter = new_exporter(); +//! +//! // Create a new trace pipeline that prints to stdout +//! let provider = SdkTracerProvider::builder() +//! .with_simple_exporter(exporter) +//! .build(); +//! let tracer = provider.tracer("readme_example"); +//! +//! tracer.in_span("doing_work", |cx| { +//! // Traced app logic here... +//! }); +//! +//! // Shutdown trace pipeline +//! provider.shutdown().expect("TracerProvider should shutdown successfully") +//! # } +//! } +//! # } +//! ``` +//! +//! See the [examples] directory for different integration patterns. +//! +//! See the API [`trace`] module docs for more information on creating and managing +//! spans. +//! +//! [examples]: https://github.com/open-telemetry/opentelemetry-rust/tree/main/examples +//! [`trace`]: https://docs.rs/opentelemetry/latest/opentelemetry/trace/index.html +//! +//! # Metrics +//! +//! ### Creating instruments and recording measurements +//! +//! ``` +//! # #[cfg(feature = "metrics")] +//! # { +//! use opentelemetry::{global, KeyValue}; +//! +//! // get a meter from a provider +//! let meter = global::meter("my_service"); +//! +//! // create an instrument +//! let counter = meter.u64_counter("my_counter").build(); +//! +//! // record a measurement +//! counter.add(1, &[KeyValue::new("http.client_ip", "83.164.160.102")]); +//! # } +//! ``` +//! +//! See the [examples] directory for different integration patterns. +//! +//! See the API [`metrics`] module docs for more information on creating and +//! managing instruments. +//! +//! [examples]: https://github.com/open-telemetry/opentelemetry-rust/tree/main/examples +//! [`metrics`]: https://docs.rs/opentelemetry/latest/opentelemetry/metrics/index.html +//! +//! ## Environment Variables +//! +//! The SDK respects the following environment variables, as defined by the +//! [OpenTelemetry specification]. Programmatic configuration via builder methods +//! takes precedence over environment variables. +//! +//! [OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/ +//! +//! ### General / Resource +//! +//! | Variable | Description | Default | +//! |---|---|---| +//! | `OTEL_SERVICE_NAME` | Sets the value of the `service.name` resource attribute. Takes priority over `service.name` in `OTEL_RESOURCE_ATTRIBUTES`. | `unknown_service:` | +//! | `OTEL_RESOURCE_ATTRIBUTES` | Key-value pairs to be used as resource attributes. Format: `key1=value1,key2=value2`. | (none) | +//! +//! ### Trace: Sampler +//! +//! | Variable | Description | Default | +//! |---|---|---| +//! | `OTEL_TRACES_SAMPLER` | Sampler to use. Valid values: `always_on`, `always_off`, `traceidratio`, `parentbased_always_on`, `parentbased_always_off`, `parentbased_traceidratio`. | `parentbased_always_on` | +//! | `OTEL_TRACES_SAMPLER_ARG` | Argument for the sampler. Used when `OTEL_TRACES_SAMPLER` is `traceidratio` or `parentbased_traceidratio`. Must be a float between 0.0 and 1.0. | `1.0` | +//! +//! ### Trace: Span Limits +//! +//! | Variable | Description | Default | +//! |---|---|---| +//! | `OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT` | Maximum number of attributes allowed on a span. | `128` | +//! | `OTEL_SPAN_EVENT_COUNT_LIMIT` | Maximum number of events allowed on a span. | `128` | +//! | `OTEL_SPAN_LINK_COUNT_LIMIT` | Maximum number of links allowed on a span. | `128` | +//! +//! ### Trace: Batch Span Processor (BSP) +//! +//! | Variable | Description | Default | +//! |---|---|---| +//! | `OTEL_BSP_SCHEDULE_DELAY` | Delay interval (in milliseconds) between two consecutive exports. | `5000` | +//! | `OTEL_BSP_MAX_QUEUE_SIZE` | Maximum queue size. | `2048` | +//! | `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` | Maximum batch size. Must be less than or equal to `OTEL_BSP_MAX_QUEUE_SIZE`. | `512` | +//! | `OTEL_BSP_MAX_CONCURRENT_EXPORTS` | Maximum number of concurrent exports. Honored by `span_processor_with_async_runtime::BatchSpanProcessor`; thread-based `BatchSpanProcessor` exports serially. For concurrent exports, enable `experimental_trace_batch_span_processor_with_async_runtime` and use the async-runtime processor. | `1` | +//! +//! ### Logs: Batch Log Record Processor (BLRP) +//! +//! | Variable | Description | Default | +//! |---|---|---| +//! | `OTEL_BLRP_SCHEDULE_DELAY` | Delay interval (in milliseconds) between two consecutive exports. | `1000` | +//! | `OTEL_BLRP_MAX_QUEUE_SIZE` | Maximum queue size. | `2048` | +//! | `OTEL_BLRP_MAX_EXPORT_BATCH_SIZE` | Maximum batch size. Must be less than or equal to `OTEL_BLRP_MAX_QUEUE_SIZE`. | `512` | +//! +//! ### Metrics: Periodic Metric Reader +//! +//! | Variable | Description | Default | +//! |---|---|---| +//! | `OTEL_METRIC_EXPORT_INTERVAL` | Interval (in milliseconds) between metrics exports. | `60000` | +//! +//! ## Crate Feature Flags +//! +//! The following feature flags can used to control the telemetry signals to use: +//! +//! * `trace`: Includes the trace SDK (enabled by default). +//! * `metrics`: Includes the metrics SDK (enabled by default). +//! * `logs`: Includes the logs SDK (enabled by default). +//! +//! For `trace` the following feature flags are available: +//! +//! * `jaeger_remote_sampler`: Enables the [Jaeger remote sampler](https://www.jaegertracing.io/docs/1.53/sampling/). +//! +//! +//! Support for recording and exporting telemetry asynchronously and perform +//! metrics aggregation can be added via the following flags: +//! +//! * `experimental_async_runtime`: Enables the experimental `Runtime` trait and related functionality. +//! * `rt-tokio`: Spawn telemetry tasks using [tokio]'s multi-thread runtime. +//! * `rt-tokio-current-thread`: Spawn telemetry tasks on a separate runtime so that the main runtime won't be blocked. +//! +//! [tokio]: https://crates.io/crates/tokio +#![warn( + future_incompatible, + missing_debug_implementations, + missing_docs, + nonstandard_style, + rust_2018_idioms, + unreachable_pub, + unused +)] +#![allow(clippy::needless_doctest_main)] +#![cfg_attr(docsrs, feature(doc_cfg), deny(rustdoc::broken_intra_doc_links))] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/main/assets/logo.svg" +)] +#![cfg_attr(test, deny(warnings))] + +pub(crate) mod growable_array; + +#[cfg(feature = "logs")] +#[cfg_attr(docsrs, doc(cfg(feature = "logs")))] +pub mod logs; +#[cfg(feature = "metrics")] +#[cfg_attr(docsrs, doc(cfg(feature = "metrics")))] +pub mod metrics; +#[cfg(feature = "trace")] +#[cfg_attr(docsrs, doc(cfg(feature = "trace")))] +pub mod propagation; +pub mod resource; +#[cfg(feature = "experimental_async_runtime")] +pub mod runtime; +#[cfg(any(feature = "testing", test))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "testing", test))))] +pub mod testing; + +#[cfg(feature = "trace")] +#[cfg_attr(docsrs, doc(cfg(feature = "trace")))] +pub mod trace; + +#[doc(hidden)] +pub mod util; + +#[doc(inline)] +pub use resource::Resource; + +pub mod error; +pub use error::ExportError; diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/batch_log_processor.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/batch_log_processor.rs new file mode 100644 index 00000000..0175a85d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/batch_log_processor.rs @@ -0,0 +1,1654 @@ +//! # OpenTelemetry Batch Log Processor +//! The `BatchLogProcessor` is one implementation of the `LogProcessor` interface. +//! +//! It buffers log records and sends them to the exporter +//! in batches. This processor is designed for **production use** in high-throughput +//! applications and reduces the overhead of frequent exports by using a background +//! thread for batch processing. +//! +//! ## Diagram +//! +//! ```ascii +//! +-----+---------------+ +-----------------------+ +-------------------+ +//! | | | | | | | +//! | SDK | Logger.emit() +---> (Batch)LogProcessor +---> (OTLPExporter) | +//! +-----+---------------+ +-----------------------+ +-------------------+ +//! ``` + +use crate::error::{OTelSdkError, OTelSdkResult}; +use crate::logs::log_processor::LogProcessor; +use crate::{ + logs::{LogBatch, LogExporter, SdkLogRecord}, + Resource, +}; +use std::sync::mpsc::{self, RecvTimeoutError, SyncSender}; + +use opentelemetry::{otel_debug, otel_error, otel_warn, Context, InstrumentationScope}; + +#[cfg(feature = "experimental_metrics_bound_instruments")] +use opentelemetry::KeyValue; + +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::{cmp::min, env, sync::Mutex}; +use std::{ + fmt::{self, Debug, Formatter}, + str::FromStr, + sync::Arc, + thread, + time::Duration, + time::Instant, +}; + +/// Environment variable for configuring the delay interval (in milliseconds) +/// between two consecutive exports for the [`BatchLogProcessor`]. +pub const OTEL_BLRP_SCHEDULE_DELAY: &str = "OTEL_BLRP_SCHEDULE_DELAY"; +/// Default delay interval between two consecutive exports. +pub const OTEL_BLRP_SCHEDULE_DELAY_DEFAULT: Duration = Duration::from_millis(1_000); +/// Environment variable for configuring the maximum allowed time to export +/// data. +/// +/// This value is honored by +/// `log_processor_with_async_runtime::BatchLogProcessor`. The thread-based +/// [`BatchLogProcessor`] ignores this setting. +pub const OTEL_BLRP_EXPORT_TIMEOUT: &str = "OTEL_BLRP_EXPORT_TIMEOUT"; +/// Default maximum allowed time to export data. +/// +/// See [`OTEL_BLRP_EXPORT_TIMEOUT`] for which processors honor this value. +pub const OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT: Duration = Duration::from_millis(30_000); +/// Environment variable for configuring the maximum queue size for the +/// [`BatchLogProcessor`]. +pub const OTEL_BLRP_MAX_QUEUE_SIZE: &str = "OTEL_BLRP_MAX_QUEUE_SIZE"; +/// Default maximum queue size. +pub const OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT: usize = 2_048; +/// Environment variable for configuring the maximum batch size for the +/// [`BatchLogProcessor`], must be less than or equal to +/// `OTEL_BLRP_MAX_QUEUE_SIZE`. +pub const OTEL_BLRP_MAX_EXPORT_BATCH_SIZE: &str = "OTEL_BLRP_MAX_EXPORT_BATCH_SIZE"; +/// Default maximum batch size. +pub const OTEL_BLRP_MAX_EXPORT_BATCH_SIZE_DEFAULT: usize = 512; + +/// Messages sent between application thread and batch log processor's work thread. +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +enum BatchMessage { + /// This is ONLY sent when the number of logs records in the data channel has reached `max_export_batch_size`. + ExportLog(Arc), + /// ForceFlush flushes the current buffer to the exporter. + ForceFlush(mpsc::SyncSender), + /// Shut down the worker thread, push all logs in buffer to the exporter. + Shutdown(mpsc::SyncSender), + /// Set the resource for the exporter. + SetResource(Arc), +} + +type LogsData = Box<(SdkLogRecord, InstrumentationScope)>; + +/// The `BatchLogProcessor` collects finished logs in a buffer and exports them +/// in batches to the configured `LogExporter`. This processor is ideal for +/// high-throughput environments, as it minimizes the overhead of exporting logs +/// individually. It uses a **dedicated background thread** to manage and export logs +/// asynchronously, ensuring that the application's main execution flow is not blocked. +/// +/// This processor supports the following configurations: +/// - **Queue size**: Maximum number of log records that can be buffered. +/// - **Batch size**: Maximum number of log records to include in a single export. +/// - **Scheduled delay**: Frequency at which the batch is exported. +/// +/// When using this processor with the OTLP Exporter, the following exporter +/// features are supported: +/// - `grpc-tonic`: Requires `LoggerProvider` to be created within a tokio runtime. +/// - `reqwest-blocking-client`: Works with a regular `main` or `tokio::main`. +/// +/// In other words, async HTTP clients like `reqwest-client` and `hyper-client` +/// are not supported by this default processor. The OTLP HTTP exporter chooses +/// its default HTTP client from enabled crate features and cannot tell which +/// processor will drive it. If your dependency graph enables async HTTP client +/// features, either pass an explicit blocking client for this processor or use +/// the experimental async-runtime batch log processor. +/// +/// `BatchLogProcessor` buffers logs in memory and exports them in batches. An +/// export is triggered when `max_export_batch_size` is reached or every +/// `scheduled_delay` milliseconds. Users can explicitly trigger an export using +/// the `force_flush` method. Shutdown also triggers an export of all buffered +/// logs and is recommended to be called before the application exits to ensure +/// all buffered logs are exported. +/// +/// **Warning**: When using tokio's current-thread runtime, `shutdown()`, which +/// is a blocking call ,should not be called from your main thread. This can +/// cause deadlock. Instead, call `shutdown()` from a separate thread or use +/// tokio's `spawn_blocking`. +/// +/// +/// ### Using a BatchLogProcessor: +/// +/// ```rust +/// # #[cfg(feature = "testing")] +/// # { +/// use opentelemetry_sdk::logs::{BatchLogProcessor, BatchConfigBuilder, SdkLoggerProvider}; +/// use opentelemetry::global; +/// use std::time::Duration; +/// use opentelemetry_sdk::logs::InMemoryLogExporter; +/// +/// let exporter = InMemoryLogExporter::default(); // Replace with an actual exporter +/// let processor = BatchLogProcessor::builder(exporter) +/// .with_batch_config( +/// BatchConfigBuilder::default() +/// .with_max_queue_size(2048) +/// .with_max_export_batch_size(512) +/// .with_scheduled_delay(Duration::from_secs(5)) +/// .build(), +/// ) +/// .build(); +/// +/// let provider = SdkLoggerProvider::builder() +/// .with_log_processor(processor) +/// .build(); +/// # } +/// +pub struct BatchLogProcessor { + logs_sender: SyncSender, // Data channel to store log records and instrumentation scopes + message_sender: SyncSender, // Control channel to store control messages for the worker thread + handle: Mutex>>, + forceflush_timeout: Duration, + export_log_message_sent: Arc, + current_batch_size: Arc, + max_export_batch_size: usize, + + // Track dropped logs - we'll log this at shutdown + dropped_logs_count: AtomicUsize, + + // Track the maximum queue size that was configured for this processor + max_queue_size: usize, + + // Self-diagnostics: otel.sdk.processor.log.processed counter. + // Gated behind experimental_metrics_bound_instruments so the hot-path + // `add` is a single atomic increment (~1.8 ns) with no per-call + // attribute resolution. + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_queue_full: opentelemetry::metrics::BoundCounter, + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_after_shutdown: opentelemetry::metrics::BoundCounter, +} + +impl Debug for BatchLogProcessor { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("BatchLogProcessor") + .field("message_sender", &self.message_sender) + .finish() + } +} + +impl LogProcessor for BatchLogProcessor { + fn emit(&self, record: &mut SdkLogRecord, instrumentation: &InstrumentationScope) { + // Count the log record before enqueueing it so that a concurrent + // force_flush()/shutdown() drain never observes an + // enqueued-but-uncounted record and misses it (issue #3453). If the + // send fails, the increment is reverted in the error arms below. + let previous_batch_size = self.current_batch_size.fetch_add(1, Ordering::AcqRel); + let result = self + .logs_sender + .try_send(Box::new((record.clone(), instrumentation.clone()))); + + // match for result and handle each separately + match result { + Ok(_) => { + // Successfully sent the log record to the data channel. + // `processed` success is counted when the batch is submitted to + // the exporter (in the worker thread), not here at enqueue. + // + // Check if the current batch size has reached the max export + // batch size. + if previous_batch_size + 1 >= self.max_export_batch_size { + // Check if the a control message for exporting logs is + // already sent to the worker thread. If not, send a control + // message to export logs. `export_log_message_sent` is set + // to false ONLY when the worker thread has processed the + // control message. + + if !self.export_log_message_sent.load(Ordering::Relaxed) { + // This is a cost-efficient check as atomic load + // operations do not require exclusive access to cache + // line. Perform atomic swap to + // `export_log_message_sent` ONLY when the atomic load + // operation above returns false. Atomic + // swap/compare_exchange operations require exclusive + // access to cache line on most processor architectures. + // We could have used compare_exchange as well here, but + // it's more verbose than swap. + if !self.export_log_message_sent.swap(true, Ordering::Relaxed) { + match self.message_sender.try_send(BatchMessage::ExportLog( + self.export_log_message_sent.clone(), + )) { + Ok(_) => { + // Control message sent successfully. + } + Err(_err) => { + // TODO: Log error If the control message + // could not be sent, reset the + // `export_log_message_sent` flag. + self.export_log_message_sent.store(false, Ordering::Relaxed); + } + } + } + } + } + } + Err(mpsc::TrySendError::Full(_)) => { + // The record never entered the channel; revert the increment. + self.current_batch_size.fetch_sub(1, Ordering::AcqRel); + // Record queue-full drop in self-diagnostics + #[cfg(feature = "experimental_metrics_bound_instruments")] + self.processed_queue_full.add(1); + + // Increment dropped logs count. The first time we have to drop + // a log, emit a warning. + if self.dropped_logs_count.fetch_add(1, Ordering::Relaxed) == 0 { + otel_warn!(name: "BatchLogProcessor.LogDroppingStarted", + message = "BatchLogProcessor dropped a LogRecord due to queue full. No further log will be emitted for further drops until Shutdown. During Shutdown time, a log will be emitted with exact count of total logs dropped."); + } + } + Err(mpsc::TrySendError::Disconnected(_)) => { + // The record never entered the channel; revert the increment. + self.current_batch_size.fetch_sub(1, Ordering::AcqRel); + // Record after-shutdown drop in self-diagnostics + #[cfg(feature = "experimental_metrics_bound_instruments")] + self.processed_after_shutdown.add(1); + + // The following `otel_warn!` may cause an infinite feedback loop of + // 'telemetry-induced-telemetry', potentially causing a stack overflow + let _guard = Context::enter_telemetry_suppressed_scope(); + + // Given background thread is the only receiver, and it's + // disconnected, it indicates the thread is shutdown + otel_warn!( + name: "BatchLogProcessor.Emit.AfterShutdown", + message = "Logs are being emitted even after Shutdown. This indicates incorrect lifecycle management of OTelLoggerProvider in application. Logs will not be exported." + ); + } + } + } + + fn force_flush(&self) -> OTelSdkResult { + let (sender, receiver) = mpsc::sync_channel(1); + match self + .message_sender + .try_send(BatchMessage::ForceFlush(sender)) + { + Ok(_) => receiver + .recv_timeout(self.forceflush_timeout) + .map_err(|err| { + if err == RecvTimeoutError::Timeout { + OTelSdkError::Timeout(self.forceflush_timeout) + } else { + OTelSdkError::InternalFailure(format!("{err}")) + } + })?, + Err(mpsc::TrySendError::Full(_)) => { + // If the control message could not be sent, emit a warning. + otel_debug!( + name: "BatchLogProcessor.ForceFlush.ControlChannelFull", + message = "Control message to flush the worker thread could not be sent as the control channel is full. This can occur if user repeatedily calls force_flush/shutdown without finishing the previous call." + ); + Err(OTelSdkError::InternalFailure("ForceFlush cannot be performed as Control channel is full. This can occur if user repeatedily calls force_flush/shutdown without finishing the previous call.".into())) + } + Err(mpsc::TrySendError::Disconnected(_)) => { + // Given background thread is the only receiver, and it's + // disconnected, it indicates the thread is shutdown + otel_debug!( + name: "BatchLogProcessor.ForceFlush.AlreadyShutdown", + message = "ForceFlush invoked after Shutdown. This will not perform Flush and indicates a incorrect lifecycle management in Application." + ); + + Err(OTelSdkError::AlreadyShutdown) + } + } + } + + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + let dropped_logs = self.dropped_logs_count.load(Ordering::Relaxed); + let max_queue_size = self.max_queue_size; + if dropped_logs > 0 { + otel_warn!( + name: "BatchLogProcessor.LogsDropped", + dropped_logs_count = dropped_logs, + max_queue_size = max_queue_size, + message = "Logs were dropped due to a queue being full. The count represents the total count of log records dropped in the lifetime of this BatchLogProcessor. Consider increasing the queue size and/or decrease delay between intervals." + ); + } + + let (sender, receiver) = mpsc::sync_channel(1); + match self.message_sender.try_send(BatchMessage::Shutdown(sender)) { + Ok(_) => { + receiver + .recv_timeout(timeout) + .map(|_| { + // join the background thread after receiving back the + // shutdown signal + if let Some(handle) = self.handle.lock().unwrap().take() { + handle.join().unwrap(); + } + OTelSdkResult::Ok(()) + }) + .map_err(|err| match err { + RecvTimeoutError::Timeout => { + // TODO: When shutdown times out, log records still + // in the queue or mid-export are silently lost. The + // background thread is not joined and may continue + // running. Consider: (1) recording the lost count + // in the self-diagnostics counter, (2) joining the + // thread with a best-effort wait, or (3) signalling + // the thread to abort the current export. + otel_error!( + name: "BatchLogProcessor.Shutdown.Timeout", + message = "BatchLogProcessor shutdown timing out." + ); + OTelSdkError::Timeout(timeout) + } + _ => { + otel_error!( + name: "BatchLogProcessor.Shutdown.Error", + error = format!("{}", err) + ); + OTelSdkError::InternalFailure(format!("{err}")) + } + })? + } + Err(mpsc::TrySendError::Full(_)) => { + // If the control message could not be sent, emit a warning. + otel_debug!( + name: "BatchLogProcessor.Shutdown.ControlChannelFull", + message = "Control message to shutdown the worker thread could not be sent as the control channel is full. This can occur if user repeatedily calls force_flush/shutdown without finishing the previous call." + ); + Err(OTelSdkError::InternalFailure("Shutdown cannot be performed as Control channel is full. This can occur if user repeatedily calls force_flush/shutdown without finishing the previous call.".into())) + } + Err(mpsc::TrySendError::Disconnected(_)) => { + // Given background thread is the only receiver, and it's + // disconnected, it indicates the thread is shutdown + otel_debug!( + name: "BatchLogProcessor.Shutdown.AlreadyShutdown", + message = "Shutdown is being invoked more than once. This is noop, but indicates a potential issue in the application's lifecycle management." + ); + + Err(OTelSdkError::AlreadyShutdown) + } + } + } + + fn set_resource(&mut self, resource: &Resource) { + let resource = Arc::new(resource.clone()); + let _ = self + .message_sender + .try_send(BatchMessage::SetResource(resource)); + } +} + +impl BatchLogProcessor { + pub(crate) fn new(mut exporter: E, config: BatchConfig) -> Self + where + E: LogExporter + Send + Sync + 'static, + { + let (logs_sender, logs_receiver) = mpsc::sync_channel::(config.max_queue_size); + let (message_sender, message_receiver) = mpsc::sync_channel::(64); // Is this a reasonable bound? + let max_queue_size = config.max_queue_size; + let max_export_batch_size = config.max_export_batch_size; + let current_batch_size = Arc::new(AtomicUsize::new(0)); + let current_batch_size_for_thread = current_batch_size.clone(); + + // Self-diagnostics: create the otel.sdk.processor.log.processed counter. + // Created before the worker thread is spawned so the success counter can + // be moved into the worker and incremented when a batch is exported. + #[cfg(feature = "experimental_metrics_bound_instruments")] + let (processed_success, processed_queue_full, processed_after_shutdown) = { + static INSTANCE_COUNTER: AtomicUsize = AtomicUsize::new(0); + let instance_id = INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed); + let component_name = format!("batching_log_processor/{instance_id}"); + + let meter = opentelemetry::global::meter("otel.sdk"); + let counter = meter + .u64_counter("otel.sdk.processor.log.processed") + .with_description( + "The number of log records for which the processing has finished, \ + either successful or failed.", + ) + .with_unit("{log_record}") + .build(); + + // Self-diagnostics: otel.sdk.processor.log.queue.capacity. A weak + // reference ensures a dropped processor stops reporting, since + // observable callbacks live for the meter provider's lifetime and + // cannot be individually unregistered. + let capacity_attrs = [ + KeyValue::new("otel.component.type", "batching_log_processor"), + KeyValue::new("otel.component.name", component_name.clone()), + ]; + let capacity_state = Arc::downgrade(¤t_batch_size); + let capacity_value = i64::try_from(max_queue_size).unwrap_or(i64::MAX); + let _ = meter + .i64_observable_up_down_counter("otel.sdk.processor.log.queue.capacity") + .with_description( + "The maximum number of log records the queue of a given instance of \ + an SDK log processor can hold.", + ) + .with_unit("{log_record}") + .with_callback(move |observer| { + // The capacity value is constant; this is only a liveness + // guard so a dropped processor stops emitting this + // otherwise-unregisterable callback. `strong_count()` is + // sufficient since the callback never reads the state. + if capacity_state.strong_count() > 0 { + observer.observe(capacity_value, &capacity_attrs); + } + }) + .build(); + + // Attribute values follow the OTel semantic conventions for SDK metrics: + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/otel/sdk-metrics.md#metric-otelsdkprocessorlogprocessed + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/otel.md#otel-component-attributes + let success_attrs = [ + KeyValue::new("otel.component.type", "batching_log_processor"), + KeyValue::new("otel.component.name", component_name.clone()), + ]; + let queue_full_attrs = [ + KeyValue::new("error.type", "queue_full"), + KeyValue::new("otel.component.type", "batching_log_processor"), + KeyValue::new("otel.component.name", component_name.clone()), + ]; + let after_shutdown_attrs = [ + KeyValue::new("error.type", "already_shutdown"), + KeyValue::new("otel.component.type", "batching_log_processor"), + KeyValue::new("otel.component.name", component_name), + ]; + + ( + counter.bind(&success_attrs), + counter.bind(&queue_full_attrs), + counter.bind(&after_shutdown_attrs), + ) + }; + + let handle = thread::Builder::new() + .name("OpenTelemetry.Logs.BatchProcessor".to_string()) + .spawn(move || { + let _suppress_guard = Context::enter_telemetry_suppressed_scope(); + otel_debug!( + name: "BatchLogProcessor.ThreadStarted", + interval_in_millisecs = config.scheduled_delay.as_millis(), + max_export_batch_size = config.max_export_batch_size, + max_queue_size = max_queue_size, + ); + let mut last_export_time = Instant::now(); + let mut logs = Vec::with_capacity(config.max_export_batch_size); + let current_batch_size = current_batch_size_for_thread; + + // Counts records for the otel.sdk.processor.log.processed + // metric; a no-op when the self-diagnostics feature is disabled. + #[cfg(feature = "experimental_metrics_bound_instruments")] + let record_processed_success = move |count: u64| processed_success.add(count); + #[cfg(not(feature = "experimental_metrics_bound_instruments"))] + let record_processed_success = |_count: u64| {}; + + // This method gets up to `max_export_batch_size` amount of logs from the channel and exports them. + // It returns the result of the export operation. + // It expects the logs vec to be empty when it's called. + #[inline] + fn get_logs_and_export( + logs_receiver: &mpsc::Receiver, + exporter: &E, + logs: &mut Vec, + last_export_time: &mut Instant, + current_batch_size: &AtomicUsize, + max_export_size: usize, + record_processed_success: &F, + ) -> OTelSdkResult + where + E: LogExporter + Send + Sync + 'static, + F: Fn(u64), + { + let target = current_batch_size.load(Ordering::Acquire); // `target` is used to determine the stopping criteria for exporting logs. + let mut result = OTelSdkResult::Ok(()); + let mut total_exported_logs: usize = 0; + + while target > 0 && total_exported_logs < target { + let batch_limit = max_export_size.min(target - total_exported_logs); + + // Get up to the remaining target batch size from the channel and push them to the logs vec + while let Ok(log) = logs_receiver.try_recv() { + logs.push(log); + if logs.len() == batch_limit { + break; + } + } + + let count_of_logs = logs.len(); // Count of logs that will be exported + if count_of_logs == 0 { + break; + } + total_exported_logs += count_of_logs; + + // Count the batch as processed before invoking the + // exporter, regardless of the export outcome. + record_processed_success(count_of_logs as u64); + + result = export_batch_sync(exporter, logs, last_export_time); // This method clears the logs vec after exporting + + current_batch_size.fetch_sub(count_of_logs, Ordering::AcqRel); + } + result + } + + loop { + let remaining_time = config + .scheduled_delay + .checked_sub(last_export_time.elapsed()) + .unwrap_or(config.scheduled_delay); + + match message_receiver.recv_timeout(remaining_time) { + Ok(BatchMessage::ExportLog(export_log_message_sent)) => { + // Reset the export log message sent flag now it has has been processed. + export_log_message_sent.store(false, Ordering::Relaxed); + + otel_debug!( + name: "BatchLogProcessor.ExportingDueToBatchSize", + ); + + let _ = get_logs_and_export( + &logs_receiver, + &exporter, + &mut logs, + &mut last_export_time, + ¤t_batch_size, + max_export_batch_size, + &record_processed_success, + ); + } + Ok(BatchMessage::ForceFlush(sender)) => { + otel_debug!(name: "BatchLogProcessor.ExportingDueToForceFlush"); + let result = get_logs_and_export( + &logs_receiver, + &exporter, + &mut logs, + &mut last_export_time, + ¤t_batch_size, + max_export_batch_size, + &record_processed_success, + ); + let _ = sender.send(result); + } + Ok(BatchMessage::Shutdown(sender)) => { + otel_debug!(name: "BatchLogProcessor.ExportingDueToShutdown"); + let result = get_logs_and_export( + &logs_receiver, + &exporter, + &mut logs, + &mut last_export_time, + ¤t_batch_size, + max_export_batch_size, + &record_processed_success, + ); + let _ = exporter.shutdown(); + let _ = sender.send(result); + + otel_debug!( + name: "BatchLogProcessor.ThreadExiting", + reason = "ShutdownRequested" + ); + // + // break out the loop and return from the current background thread. + // + break; + } + Ok(BatchMessage::SetResource(resource)) => { + exporter.set_resource(&resource); + } + Err(RecvTimeoutError::Timeout) => { + otel_debug!( + name: "BatchLogProcessor.ExportingDueToTimer", + ); + + let _ = get_logs_and_export( + &logs_receiver, + &exporter, + &mut logs, + &mut last_export_time, + ¤t_batch_size, + max_export_batch_size, + &record_processed_success, + ); + } + Err(RecvTimeoutError::Disconnected) => { + // Channel disconnected, only thing to do is break + // out (i.e exit the thread) + otel_debug!( + name: "BatchLogProcessor.ThreadExiting", + reason = "MessageSenderDisconnected" + ); + break; + } + } + } + otel_debug!( + name: "BatchLogProcessor.ThreadStopped" + ); + }) + .expect("Thread spawn failed."); //TODO: Handle thread spawn failure + + // Return batch processor with link to worker + BatchLogProcessor { + logs_sender, + message_sender, + handle: Mutex::new(Some(handle)), + forceflush_timeout: Duration::from_secs(5), // TODO: make this configurable + dropped_logs_count: AtomicUsize::new(0), + max_queue_size, + export_log_message_sent: Arc::new(AtomicBool::new(false)), + current_batch_size, + max_export_batch_size, + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_queue_full, + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_after_shutdown, + } + } + + /// Create a new batch processor builder + pub fn builder(exporter: E) -> BatchLogProcessorBuilder + where + E: LogExporter, + { + BatchLogProcessorBuilder { + exporter, + config: Default::default(), + } + } +} + +#[allow(clippy::vec_box)] +fn export_batch_sync( + exporter: &E, + batch: &mut Vec>, + last_export_time: &mut Instant, +) -> OTelSdkResult +where + E: LogExporter + ?Sized, +{ + *last_export_time = Instant::now(); + + if batch.is_empty() { + return OTelSdkResult::Ok(()); + } + + let export = exporter.export(LogBatch::new_with_owned_data(batch.as_slice())); + let export_result = futures_executor::block_on(export); + + // Clear the batch vec after exporting + batch.clear(); + + match export_result { + Ok(_) => OTelSdkResult::Ok(()), + Err(err) => { + otel_error!( + name: "BatchLogProcessor.ExportError", + error = format!("{}", err) + ); + OTelSdkResult::Err(err) + } + } +} + +/// +/// A builder for creating [`BatchLogProcessor`] instances. +/// +#[derive(Debug)] +pub struct BatchLogProcessorBuilder { + exporter: E, + config: BatchConfig, +} + +impl BatchLogProcessorBuilder +where + E: LogExporter + 'static, +{ + /// Set the BatchConfig for [`BatchLogProcessorBuilder`] + pub fn with_batch_config(self, config: BatchConfig) -> Self { + BatchLogProcessorBuilder { config, ..self } + } + + /// Build a batch processor + pub fn build(self) -> BatchLogProcessor { + BatchLogProcessor::new(self.exporter, self.config) + } +} + +/// Batch log processor configuration. +/// Use [`BatchConfigBuilder`] to configure your own instance of [`BatchConfig`]. +#[derive(Debug)] +#[allow(dead_code)] +pub struct BatchConfig { + /// The maximum queue size to buffer logs for delayed processing. If the + /// queue gets full it drops the logs. The default value of is 2048. + pub(crate) max_queue_size: usize, + + /// The delay interval in milliseconds between two consecutive processing + /// of batches. The default value is 1 second. + pub(crate) scheduled_delay: Duration, + + /// The maximum number of logs to process in a single batch. If there are + /// more than one batch worth of logs then it processes multiple batches + /// of logs one batch after the other without any delay. The default value + /// is 512. + pub(crate) max_export_batch_size: usize, + + /// The maximum duration to export a batch of data. + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + pub(crate) max_export_timeout: Duration, +} + +impl Default for BatchConfig { + fn default() -> Self { + BatchConfigBuilder::default().build() + } +} + +/// A builder for creating [`BatchConfig`] instances. +#[derive(Debug)] +pub struct BatchConfigBuilder { + max_queue_size: usize, + scheduled_delay: Duration, + max_export_batch_size: usize, + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + max_export_timeout: Duration, +} + +impl Default for BatchConfigBuilder { + /// Create a new [`BatchConfigBuilder`] initialized with default batch config values as per the specs. + /// The values are overridden by environment variables if set. + /// The supported environment variables are: + /// * `OTEL_BLRP_MAX_QUEUE_SIZE` + /// * `OTEL_BLRP_SCHEDULE_DELAY` + /// * `OTEL_BLRP_MAX_EXPORT_BATCH_SIZE` + /// * `OTEL_BLRP_EXPORT_TIMEOUT` + /// + /// Note: Programmatic configuration overrides any value set via the environment variable. + fn default() -> Self { + BatchConfigBuilder { + max_queue_size: OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT, + scheduled_delay: OTEL_BLRP_SCHEDULE_DELAY_DEFAULT, + max_export_batch_size: OTEL_BLRP_MAX_EXPORT_BATCH_SIZE_DEFAULT, + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + max_export_timeout: OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT, + } + .init_from_env_vars() + } +} + +impl BatchConfigBuilder { + /// Set max_queue_size for [`BatchConfigBuilder`]. + /// It's the maximum queue size to buffer logs for delayed processing. + /// If the queue gets full it will drop the logs. + /// The default value is 2048. + /// + /// Corresponding environment variable: `OTEL_BLRP_MAX_QUEUE_SIZE`. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + pub fn with_max_queue_size(mut self, max_queue_size: usize) -> Self { + self.max_queue_size = max_queue_size; + self + } + + /// Set scheduled_delay for [`BatchConfigBuilder`]. + /// It's the delay interval in milliseconds between two consecutive processing of batches. + /// The default value is 1000 milliseconds. + /// + /// Corresponding environment variable: `OTEL_BLRP_SCHEDULE_DELAY`. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + pub fn with_scheduled_delay(mut self, scheduled_delay: Duration) -> Self { + self.scheduled_delay = scheduled_delay; + self + } + + /// Set max_export_timeout for [`BatchConfigBuilder`]. + /// It's the maximum duration to export a batch of data. + /// The default value is 30000 milliseconds. + /// + /// Corresponding environment variable: `OTEL_BLRP_EXPORT_TIMEOUT`. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + pub fn with_max_export_timeout(mut self, max_export_timeout: Duration) -> Self { + self.max_export_timeout = max_export_timeout; + self + } + + /// Set max_export_batch_size for [`BatchConfigBuilder`]. + /// It's the maximum number of logs to process in a single batch. If there are + /// more than one batch worth of logs then it processes multiple batches + /// of logs one batch after the other without any delay. + /// The default value is 512. + /// + /// Corresponding environment variable: `OTEL_BLRP_MAX_EXPORT_BATCH_SIZE`. + /// + /// Note: Programmatically setting this will override any value set via the environment variable. + pub fn with_max_export_batch_size(mut self, max_export_batch_size: usize) -> Self { + self.max_export_batch_size = max_export_batch_size; + self + } + + /// Builds a `BatchConfig` enforcing the following invariants: + /// * `max_export_batch_size` must be less than or equal to `max_queue_size`. + pub fn build(self) -> BatchConfig { + // max export batch size must be less or equal to max queue size. + // we set max export batch size to max queue size if it's larger than max queue size. + let max_export_batch_size = min(self.max_export_batch_size, self.max_queue_size); + + BatchConfig { + max_queue_size: self.max_queue_size, + scheduled_delay: self.scheduled_delay, + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + max_export_timeout: self.max_export_timeout, + max_export_batch_size, + } + } + + fn init_from_env_vars(mut self) -> Self { + if let Some(max_queue_size) = env::var(OTEL_BLRP_MAX_QUEUE_SIZE) + .ok() + .and_then(|queue_size| usize::from_str(&queue_size).ok()) + { + self.max_queue_size = max_queue_size; + } + + if let Some(max_export_batch_size) = env::var(OTEL_BLRP_MAX_EXPORT_BATCH_SIZE) + .ok() + .and_then(|batch_size| usize::from_str(&batch_size).ok()) + { + self.max_export_batch_size = max_export_batch_size; + } + + if let Some(scheduled_delay) = env::var(OTEL_BLRP_SCHEDULE_DELAY) + .ok() + .and_then(|delay| u64::from_str(&delay).ok()) + { + self.scheduled_delay = Duration::from_millis(scheduled_delay); + } + + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + if let Some(max_export_timeout) = env::var(OTEL_BLRP_EXPORT_TIMEOUT) + .ok() + .and_then(|s| u64::from_str(&s).ok()) + { + self.max_export_timeout = Duration::from_millis(max_export_timeout); + } + + self + } +} + +#[cfg(all(test, feature = "testing", feature = "logs"))] +mod tests { + use super::{ + BatchConfig, BatchConfigBuilder, BatchLogProcessor, OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE_DEFAULT, OTEL_BLRP_MAX_QUEUE_SIZE, + OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT, OTEL_BLRP_SCHEDULE_DELAY, + OTEL_BLRP_SCHEDULE_DELAY_DEFAULT, + }; + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + use super::{OTEL_BLRP_EXPORT_TIMEOUT, OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT}; + use crate::error::OTelSdkResult; + use crate::logs::log_processor::tests::MockLogExporter; + use crate::logs::SdkLogRecord; + use crate::logs::{LogBatch, LogExporter}; + use crate::{ + logs::{InMemoryLogExporter, InMemoryLogExporterBuilder, LogProcessor, SdkLoggerProvider}, + Resource, + }; + use opentelemetry::logs::LogRecord; + use opentelemetry::InstrumentationScope; + use opentelemetry::KeyValue; + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use std::sync::mpsc; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + #[test] + fn test_default_const_values() { + assert_eq!(OTEL_BLRP_SCHEDULE_DELAY, "OTEL_BLRP_SCHEDULE_DELAY"); + assert_eq!(OTEL_BLRP_SCHEDULE_DELAY_DEFAULT.as_millis(), 1_000); + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + assert_eq!(OTEL_BLRP_EXPORT_TIMEOUT, "OTEL_BLRP_EXPORT_TIMEOUT"); + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + assert_eq!(OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT.as_millis(), 30_000); + assert_eq!(OTEL_BLRP_MAX_QUEUE_SIZE, "OTEL_BLRP_MAX_QUEUE_SIZE"); + assert_eq!(OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT, 2_048); + assert_eq!( + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, + "OTEL_BLRP_MAX_EXPORT_BATCH_SIZE" + ); + assert_eq!(OTEL_BLRP_MAX_EXPORT_BATCH_SIZE_DEFAULT, 512); + } + + #[test] + fn test_default_batch_config_adheres_to_specification() { + // The following environment variables are expected to be unset so that their default values are used. + let env_vars = vec![ + OTEL_BLRP_SCHEDULE_DELAY, + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + OTEL_BLRP_EXPORT_TIMEOUT, + OTEL_BLRP_MAX_QUEUE_SIZE, + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, + ]; + + let config = temp_env::with_vars_unset(env_vars, BatchConfig::default); + + assert_eq!(config.scheduled_delay, OTEL_BLRP_SCHEDULE_DELAY_DEFAULT); + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + assert_eq!(config.max_export_timeout, OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT); + assert_eq!(config.max_queue_size, OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT); + assert_eq!( + config.max_export_batch_size, + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE_DEFAULT + ); + } + + #[test] + fn test_code_based_config_overrides_env_vars() { + let env_vars = vec![ + (OTEL_BLRP_SCHEDULE_DELAY, Some("2000")), + (OTEL_BLRP_MAX_QUEUE_SIZE, Some("4096")), + (OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, Some("1024")), + ]; + + temp_env::with_vars(env_vars, || { + let config = BatchConfigBuilder::default() + .with_max_queue_size(2048) + .with_scheduled_delay(Duration::from_millis(1000)) + .with_max_export_batch_size(512) + .build(); + + assert_eq!(config.scheduled_delay, Duration::from_millis(1000)); + assert_eq!(config.max_queue_size, 2048); + assert_eq!(config.max_export_batch_size, 512); + }); + } + + #[test] + fn test_batch_config_configurable_by_env_vars() { + let env_vars = vec![ + (OTEL_BLRP_SCHEDULE_DELAY, Some("2000")), + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + (OTEL_BLRP_EXPORT_TIMEOUT, Some("60000")), + (OTEL_BLRP_MAX_QUEUE_SIZE, Some("4096")), + (OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, Some("1024")), + ]; + + let config = temp_env::with_vars(env_vars, BatchConfig::default); + + assert_eq!(config.scheduled_delay, Duration::from_millis(2000)); + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + assert_eq!(config.max_export_timeout, Duration::from_millis(60000)); + assert_eq!(config.max_queue_size, 4096); + assert_eq!(config.max_export_batch_size, 1024); + } + #[test] + fn test_force_flush_being_called() { + #[derive(Debug, Clone)] + struct MockExporter { + export_called: Arc, + } + impl LogExporter for MockExporter { + async fn export(&self, _batch: LogBatch<'_>) -> OTelSdkResult { + self.export_called.store(true, Ordering::SeqCst); + Ok(()) + } + } + let exporter = MockExporter { + export_called: Arc::new(AtomicBool::new(false)), + }; + let processor = BatchLogProcessor::new(exporter.clone(), BatchConfig::default()); + let scope = opentelemetry::InstrumentationScope::builder("my-crate") + .with_schema_url("https://opentelemetry.io/schemas/1.17.0") + .build(); + processor.emit(&mut SdkLogRecord::new(), &scope); + processor.force_flush().unwrap(); + assert!(exporter.export_called.load(Ordering::SeqCst)); + } + + #[test] + fn test_batch_config_max_export_batch_size_validation() { + let env_vars = vec![ + (OTEL_BLRP_MAX_QUEUE_SIZE, Some("256")), + (OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, Some("1024")), + ]; + + let config = temp_env::with_vars(env_vars, BatchConfig::default); + + assert_eq!(config.max_queue_size, 256); + assert_eq!(config.max_export_batch_size, 256); + assert_eq!(config.scheduled_delay, OTEL_BLRP_SCHEDULE_DELAY_DEFAULT); + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + assert_eq!(config.max_export_timeout, OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT); + } + + #[test] + fn test_batch_config_with_fields() { + let batch_builder = BatchConfigBuilder::default() + .with_max_export_batch_size(1) + .with_scheduled_delay(Duration::from_millis(2)) + .with_max_queue_size(4); + + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + let batch_builder = batch_builder.with_max_export_timeout(Duration::from_millis(3)); + let batch = batch_builder.build(); + + assert_eq!(batch.max_export_batch_size, 1); + assert_eq!(batch.scheduled_delay, Duration::from_millis(2)); + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + assert_eq!(batch.max_export_timeout, Duration::from_millis(3)); + assert_eq!(batch.max_queue_size, 4); + } + + #[test] + fn test_build_batch_log_processor_builder() { + let mut env_vars = vec![ + (OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, Some("500")), + (OTEL_BLRP_SCHEDULE_DELAY, Some("I am not number")), + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + (OTEL_BLRP_EXPORT_TIMEOUT, Some("2046")), + ]; + temp_env::with_vars(env_vars.clone(), || { + let builder = BatchLogProcessor::builder(InMemoryLogExporter::default()); + + assert_eq!(builder.config.max_export_batch_size, 500); + assert_eq!( + builder.config.scheduled_delay, + OTEL_BLRP_SCHEDULE_DELAY_DEFAULT + ); + assert_eq!( + builder.config.max_queue_size, + OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT + ); + + #[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] + assert_eq!( + builder.config.max_export_timeout, + Duration::from_millis(2046) + ); + }); + + env_vars.push((OTEL_BLRP_MAX_QUEUE_SIZE, Some("120"))); + + temp_env::with_vars(env_vars, || { + let builder = BatchLogProcessor::builder(InMemoryLogExporter::default()); + assert_eq!(builder.config.max_export_batch_size, 120); + assert_eq!(builder.config.max_queue_size, 120); + }); + } + + #[test] + fn test_build_batch_log_processor_builder_with_custom_config() { + let expected = BatchConfigBuilder::default() + .with_max_export_batch_size(1) + .with_scheduled_delay(Duration::from_millis(2)) + .with_max_queue_size(4) + .build(); + + let builder = + BatchLogProcessor::builder(InMemoryLogExporter::default()).with_batch_config(expected); + + let actual = &builder.config; + assert_eq!(actual.max_export_batch_size, 1); + assert_eq!(actual.scheduled_delay, Duration::from_millis(2)); + assert_eq!(actual.max_queue_size, 4); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_set_resource_batch_processor() { + let exporter = MockLogExporter { + resource: Arc::new(Mutex::new(None)), + }; + let processor = BatchLogProcessor::new(exporter.clone(), BatchConfig::default()); + let provider = SdkLoggerProvider::builder() + .with_log_processor(processor) + .with_resource( + Resource::builder_empty() + .with_attributes([ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v3"), + KeyValue::new("k3", "v3"), + KeyValue::new("k4", "v4"), + KeyValue::new("k5", "v5"), + ]) + .build(), + ) + .build(); + + provider.force_flush().unwrap(); + + assert_eq!(exporter.get_resource().unwrap().into_iter().count(), 5); + let _ = provider.shutdown(); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_batch_shutdown() { + // assert we will receive an error + // setup + let exporter = InMemoryLogExporterBuilder::default() + .keep_records_on_shutdown() + .build(); + let processor = BatchLogProcessor::new(exporter.clone(), BatchConfig::default()); + + let mut record = SdkLogRecord::new(); + let instrumentation = InstrumentationScope::default(); + + processor.emit(&mut record, &instrumentation); + processor.force_flush().unwrap(); + processor.shutdown().unwrap(); + // todo: expect to see errors here. How should we assert this? + processor.emit(&mut record, &instrumentation); + assert_eq!(1, exporter.get_emitted_logs().unwrap().len()); + assert!(exporter.is_shutdown_called()); + } + + #[tokio::test(flavor = "current_thread")] + async fn test_batch_log_processor_shutdown_under_async_runtime_current_flavor_multi_thread() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = BatchLogProcessor::new(exporter.clone(), BatchConfig::default()); + + processor.shutdown().unwrap(); + } + + #[tokio::test(flavor = "current_thread")] + async fn test_batch_log_processor_shutdown_with_async_runtime_current_flavor_current_thread() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = BatchLogProcessor::new(exporter.clone(), BatchConfig::default()); + processor.shutdown().unwrap(); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_batch_log_processor_shutdown_with_async_runtime_multi_flavor_multi_thread() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = BatchLogProcessor::new(exporter.clone(), BatchConfig::default()); + processor.shutdown().unwrap(); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_batch_log_processor_shutdown_with_async_runtime_multi_flavor_current_thread() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = BatchLogProcessor::new(exporter.clone(), BatchConfig::default()); + processor.shutdown().unwrap(); + } + + #[derive(Debug)] + struct BlockingExporter { + exported_count: Arc, + export_started: mpsc::SyncSender<()>, + release: Arc>>, + } + + impl LogExporter for BlockingExporter { + async fn export(&self, batch: LogBatch<'_>) -> OTelSdkResult { + let _ = self.export_started.try_send(()); + // Block until the test releases the export. + let _ = self.release.lock().unwrap().recv(); + self.exported_count.fetch_add(batch.len(), Ordering::SeqCst); + Ok(()) + } + } + + #[test] + fn test_batch_log_processor_emit_reverts_count_when_queue_full() { + let (started_sender, started_receiver) = mpsc::sync_channel(8); + let (release_sender, release_receiver) = mpsc::sync_channel(8); + let exported_count = Arc::new(AtomicUsize::new(0)); + let exporter = BlockingExporter { + exported_count: exported_count.clone(), + export_started: started_sender, + release: Arc::new(Mutex::new(release_receiver)), + }; + let config = BatchConfigBuilder::default() + .with_max_queue_size(4) + .with_max_export_batch_size(4) + .with_scheduled_delay(Duration::from_secs(60)) + .build(); + let processor = BatchLogProcessor::new(exporter, config); + let instrumentation = InstrumentationScope::default(); + let emit = || { + let mut record = SdkLogRecord::new(); + record.set_body("test log".into()); + processor.emit(&mut record, &instrumentation); + }; + + // Fill the queue to the export threshold; the worker drains all four + // records and blocks inside export(). + for _ in 0..4 { + emit(); + } + started_receiver + .recv_timeout(Duration::from_secs(5)) + .expect("worker should start exporting the first batch"); + + // While the worker is blocked, refill the queue and overflow it by + // two records, which must be dropped and their counts reverted. + for _ in 0..6 { + emit(); + } + + assert_eq!(processor.dropped_logs_count.load(Ordering::Relaxed), 2); + // 4 records in-flight in the blocked export (not yet subtracted) + // plus 4 records queued. Without the queue-full revert this would + // read 10. + assert_eq!( + processor.current_batch_size.load(Ordering::Relaxed), + 8, + "dropped logs must not remain counted as pending" + ); + + // Release the in-flight export and the one triggered by force_flush. + release_sender.send(()).unwrap(); + release_sender.send(()).unwrap(); + let flush_result = processor.force_flush(); + assert!(flush_result.is_ok(), "force flush failed unexpectedly"); + + assert_eq!( + exported_count.load(Ordering::SeqCst), + 8, + "all logs that entered the queue must be exported" + ); + assert_eq!( + processor.current_batch_size.load(Ordering::Relaxed), + 0, + "counter should settle to zero; a leftover value indicates the \ + queue-full path did not revert its increment" + ); + } + + /// A slow exporter that counts the number of logs received. + /// Used for stress testing the BatchLogProcessor. + #[derive(Debug, Clone)] + struct CountingExporter { + count: Arc, + } + + impl CountingExporter { + fn new() -> Self { + CountingExporter { + count: Arc::new(AtomicUsize::new(0)), + } + } + } + + impl LogExporter for CountingExporter { + async fn export(&self, batch: LogBatch<'_>) -> OTelSdkResult { + self.count.fetch_add(batch.len(), Ordering::SeqCst); + // Simulate slow export to cause queue buildup and drops + std::thread::sleep(std::time::Duration::from_millis(20)); + Ok(()) + } + } + + /// Stress test that verifies all logs are accounted for. + /// With multiple threads pushing logs faster than the exporter can handle, + /// some logs will inevitably be dropped. This test validates: + /// total_logs_sent == logs_received_by_exporter + logs_dropped + #[test] + fn test_batch_log_processor_all_logs_accounted_for() { + let exporter = CountingExporter::new(); + let exporter_count = exporter.count.clone(); + + // Configure with small queue to force drops under pressure + let config = BatchConfigBuilder::default() + .with_max_queue_size(2048) + .with_max_export_batch_size(512) + .with_scheduled_delay(Duration::from_millis(5)) + .build(); + + let processor = BatchLogProcessor::new(exporter, config); + + let total_logs_per_thread = 100_000; + let num_threads = 4; + let total_logs_to_emit = total_logs_per_thread * num_threads; + + // Use scoped threads to safely share the processor reference + std::thread::scope(|s| { + for _ in 0..num_threads { + s.spawn(|| { + for _ in 0..total_logs_per_thread { + let mut record = SdkLogRecord::new(); + record.set_body("stress test log".into()); + let instrumentation = InstrumentationScope::default(); + processor.emit(&mut record, &instrumentation); + } + }); + } + }); + + // Shutdown the processor to ensure all buffered logs are flushed + processor.shutdown().unwrap(); + + let logs_received = exporter_count.load(Ordering::SeqCst); + let logs_dropped = processor.dropped_logs_count.load(Ordering::SeqCst); + + // The invariant: every log is either received or dropped + assert_eq!( + logs_received + logs_dropped, + total_logs_to_emit, + "Logs unaccounted for! Received: {}, Dropped: {}, Total emitted: {}", + logs_received, + logs_dropped, + total_logs_to_emit + ); + + // Also verify that some logs were actually dropped (stress test validation) + // If no logs were dropped, the test parameters may need adjustment + assert!( + logs_dropped > 0, + "Expected some logs to be dropped under stress, but none were. \ + Consider reducing queue size or increasing thread count/log volume." + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + mod self_obs { + use super::*; + + /// Verifies that `otel.sdk.processor.log.processed` counter records + /// successful log processing when `experimental_metrics_bound_instruments` + /// is enabled and a real MeterProvider is set as global before creating + /// the processor. + /// + /// This test is `#[ignore]`d because it calls + /// `global::set_meter_provider()` which mutates process-wide state. + /// CI runs it in isolation via `test.sh`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + #[ignore] + fn self_diagnostics_counter_records_success() { + use crate::metrics::data::{AggregatedMetrics, MetricData}; + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + + // Setup a real MeterProvider and set it as global BEFORE creating the + // BatchLogProcessor, so the processor picks up a real meter. + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + let log_exporter = InMemoryLogExporter::default(); + let config = BatchConfigBuilder::default() + .with_max_queue_size(256) + .with_max_export_batch_size(64) + .with_scheduled_delay(Duration::from_secs(60)) + .build(); + let processor = BatchLogProcessor::new(log_exporter, config); + + // Emit 10 logs + let instrumentation = InstrumentationScope::default(); + for _ in 0..10 { + let mut record = SdkLogRecord::new(); + processor.emit(&mut record, &instrumentation); + } + + // Flush so the batch is submitted to the exporter, which is when the + // counter is incremented. + processor.force_flush().unwrap(); + + // Force a metrics collection + meter_provider.force_flush().unwrap(); + + // Find the otel.sdk.processor.log.processed metric and sum all data points + let metrics = metric_exporter.get_finished_metrics().unwrap(); + let mut found = false; + let mut total_value: u64 = 0; + for rm in &metrics { + for sm in &rm.scope_metrics { + for metric in &sm.metrics { + if metric.name == "otel.sdk.processor.log.processed" { + found = true; + if let AggregatedMetrics::U64(MetricData::Sum(sum)) = &metric.data { + for dp in sum.data_points() { + total_value += dp.value(); + } + } + } + } + } + } + + assert!(found, "otel.sdk.processor.log.processed metric not found"); + assert_eq!( + total_value, 10, + "Expected 10 processed logs, got {total_value}" + ); + + processor.shutdown().unwrap(); + meter_provider.shutdown().unwrap(); + } + + /// Verifies `otel.sdk.processor.log.queue.capacity` through a real + /// `SdkLoggerProvider` + `BatchLogProcessor`. The metric reports the + /// configured max queue size with the component identity attributes and + /// stops reporting after the provider (and processor) is dropped. + /// + /// `#[ignore]`d because it mutates process-wide state via + /// `global::set_meter_provider()`. CI runs it in isolation via `test.sh`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + #[ignore] + fn self_diagnostics_queue_capacity() { + use crate::metrics::data::{AggregatedMetrics, MetricData}; + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + let log_exporter = InMemoryLogExporter::default(); + let config = BatchConfigBuilder::default() + .with_max_queue_size(256) + .build(); + let processor = BatchLogProcessor::new(log_exporter, config); + let provider = SdkLoggerProvider::builder() + .with_log_processor(processor) + .build(); + + // Force a metrics collection so the observable callbacks run. This does + // NOT drain the log queue (that only happens on the provider/processor). + meter_provider.force_flush().unwrap(); + + let read = |name: &str| -> Option { + let metrics = metric_exporter.get_finished_metrics().unwrap(); + for rm in &metrics { + for sm in &rm.scope_metrics { + for metric in &sm.metrics { + if metric.name == name { + if let AggregatedMetrics::I64(MetricData::Sum(sum)) = &metric.data { + for dp in sum.data_points() { + let has_component = dp.attributes().any(|kv| { + kv.key.as_str() == "otel.component.type" + && kv.value.as_str() == "batching_log_processor" + }); + if has_component { + return Some(dp.value()); + } + } + } + } + } + } + } + None + }; + + assert_eq!( + read("otel.sdk.processor.log.queue.capacity"), + Some(256), + "queue.capacity should equal the configured max_queue_size" + ); + + // Dropping the provider shuts down and drops the processor, releasing the + // Arc the callback holds a Weak to. A subsequent collection + // must therefore omit the metric because the Weak upgrade fails. + metric_exporter.reset(); + provider.shutdown().unwrap(); + drop(provider); + meter_provider.force_flush().unwrap(); + + assert_eq!( + read("otel.sdk.processor.log.queue.capacity"), + None, + "queue.capacity must stop being reported after the processor is dropped" + ); + + meter_provider.shutdown().unwrap(); + } + + /// Sums the values of `otel.sdk.processor.log.processed` data points whose + /// `error.type` attribute equals `error_type`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn sum_processed_log_records_with_error_type( + metric_exporter: &crate::metrics::InMemoryMetricExporter, + error_type: &str, + ) -> u64 { + use crate::metrics::data::{AggregatedMetrics, MetricData}; + + let metrics = metric_exporter.get_finished_metrics().unwrap(); + let mut total: u64 = 0; + for rm in &metrics { + for sm in &rm.scope_metrics { + for metric in &sm.metrics { + if metric.name == "otel.sdk.processor.log.processed" { + if let AggregatedMetrics::U64(MetricData::Sum(sum)) = &metric.data { + for dp in sum.data_points() { + let matches = dp.attributes().any(|kv| { + kv.key.as_str() == "error.type" + && kv.value.as_str() == error_type + }); + if matches { + total += dp.value(); + } + } + } + } + } + } + } + total + } + + /// Verifies that `otel.sdk.processor.log.processed` records queue-full drops + /// with `error.type = queue_full` when records overflow the queue while the + /// worker is blocked exporting. + /// + /// `#[ignore]`d because it mutates process-wide state via + /// `global::set_meter_provider()`. CI runs it in isolation via `test.sh`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + #[ignore] + fn self_diagnostics_counter_records_queue_full_drops() { + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + let (started_sender, started_receiver) = mpsc::sync_channel(8); + let (release_sender, release_receiver) = mpsc::sync_channel(8); + let exported_count = Arc::new(AtomicUsize::new(0)); + let exporter = BlockingExporter { + exported_count: exported_count.clone(), + export_started: started_sender, + release: Arc::new(Mutex::new(release_receiver)), + }; + let config = BatchConfigBuilder::default() + .with_max_queue_size(4) + .with_max_export_batch_size(4) + .with_scheduled_delay(Duration::from_secs(60)) + .build(); + let processor = BatchLogProcessor::new(exporter, config); + let instrumentation = InstrumentationScope::default(); + let emit = || { + let mut record = SdkLogRecord::new(); + processor.emit(&mut record, &instrumentation); + }; + + // Fill the queue to the export threshold; the worker drains all four + // records and blocks inside export(). + for _ in 0..4 { + emit(); + } + started_receiver + .recv_timeout(Duration::from_secs(5)) + .expect("worker should start exporting the first batch"); + + // While the worker is blocked, refill the queue (4) and overflow it by + // two records, which must be dropped and counted as queue_full. + for _ in 0..6 { + emit(); + } + + // Release the in-flight export and the one triggered by force_flush. + release_sender.send(()).unwrap(); + release_sender.send(()).unwrap(); + processor.force_flush().unwrap(); + + meter_provider.force_flush().unwrap(); + + let queue_full = + sum_processed_log_records_with_error_type(&metric_exporter, "queue_full"); + assert_eq!( + queue_full, 2, + "expected 2 queue_full drops, got {queue_full}" + ); + + processor.shutdown().unwrap(); + meter_provider.shutdown().unwrap(); + } + + /// Verifies that `otel.sdk.processor.log.processed` records post-shutdown + /// emits with `error.type = already_shutdown`. + /// + /// `#[ignore]`d because it mutates process-wide state via + /// `global::set_meter_provider()`. CI runs it in isolation via `test.sh`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + #[ignore] + fn self_diagnostics_counter_records_already_shutdown_drops() { + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + let log_exporter = InMemoryLogExporter::default(); + let processor = BatchLogProcessor::new(log_exporter, BatchConfig::default()); + + // Shut the processor down so the worker thread (the only receiver) + // disconnects; subsequent emits hit the already_shutdown branch. + processor.shutdown().unwrap(); + + let instrumentation = InstrumentationScope::default(); + for _ in 0..7 { + let mut record = SdkLogRecord::new(); + processor.emit(&mut record, &instrumentation); + } + + meter_provider.force_flush().unwrap(); + + let already_shutdown = + sum_processed_log_records_with_error_type(&metric_exporter, "already_shutdown"); + assert_eq!( + already_shutdown, 7, + "expected 7 already_shutdown drops, got {already_shutdown}" + ); + + meter_provider.shutdown().unwrap(); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/export.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/export.rs new file mode 100644 index 00000000..83fb341a --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/export.rs @@ -0,0 +1,162 @@ +//! Log exporters +use crate::error::OTelSdkResult; +use crate::logs::SdkLogRecord; +use crate::Resource; +use opentelemetry::logs::Severity; +use opentelemetry::InstrumentationScope; +use std::fmt::Debug; +use std::time; + +/// A batch of log records to be exported by a `LogExporter`. +/// +/// The `LogBatch` struct holds a collection of log records along with their associated +/// instrumentation scopes. This structure is used to group log records together for efficient +/// export operations. +/// +/// # Type Parameters +/// - `'a`: The lifetime of the references to the log records and instrumentation scopes. +/// +#[derive(Debug)] +pub struct LogBatch<'a> { + data: LogBatchData<'a>, +} + +/// The `LogBatchData` enum represents the data field of a `LogBatch`. +/// It can either be: +/// - A shared reference to a slice of boxed tuples, where each tuple consists of an owned `LogRecord` and an owned `InstrumentationScope`. +/// - Or it can be a shared reference to a slice of tuples, where each tuple consists of a reference to a `LogRecord` and a reference to an `InstrumentationScope`. +#[derive(Debug)] +enum LogBatchData<'a> { + SliceOfOwnedData(&'a [Box<(SdkLogRecord, InstrumentationScope)>]), // Used by BatchProcessor which clones the LogRecords for its own use. + SliceOfBorrowedData(&'a [(&'a SdkLogRecord, &'a InstrumentationScope)]), +} + +impl<'a> LogBatch<'a> { + /// Creates a new instance of `LogBatch`. + /// + /// # Arguments + /// + /// * `data` - A slice of tuples, where each tuple consists of a reference to a `LogRecord` + /// and a reference to an `InstrumentationScope`. These tuples represent the log records + /// and their associated instrumentation scopes to be exported. + /// + /// # Returns + /// + /// A `LogBatch` instance containing the provided log records and instrumentation scopes. + /// + /// Note - this is not a public function, and should not be used directly. This would be + /// made private in the future. + pub fn new(data: &'a [(&'a SdkLogRecord, &'a InstrumentationScope)]) -> LogBatch<'a> { + LogBatch { + data: LogBatchData::SliceOfBorrowedData(data), + } + } + + pub(crate) fn new_with_owned_data( + data: &'a [Box<(SdkLogRecord, InstrumentationScope)>], + ) -> LogBatch<'a> { + LogBatch { + data: LogBatchData::SliceOfOwnedData(data), + } + } +} + +impl LogBatch<'_> { + /// Returns the number of log records in the batch. + #[cfg(test)] + pub(crate) fn len(&self) -> usize { + match &self.data { + LogBatchData::SliceOfOwnedData(data) => data.len(), + LogBatchData::SliceOfBorrowedData(data) => data.len(), + } + } + + /// Returns an iterator over the log records and instrumentation scopes in the batch. + /// + /// Each item yielded by the iterator is a tuple containing references to a `LogRecord` + /// and an `InstrumentationScope`. + /// + /// # Returns + /// + /// An iterator that yields references to the `LogRecord` and `InstrumentationScope` in the batch. + /// + pub fn iter(&self) -> impl Iterator { + LogBatchDataIter { + data: &self.data, + index: 0, + } + } +} + +struct LogBatchDataIter<'a> { + data: &'a LogBatchData<'a>, + index: usize, +} + +impl<'a> Iterator for LogBatchDataIter<'a> { + type Item = (&'a SdkLogRecord, &'a InstrumentationScope); + + fn next(&mut self) -> Option { + match self.data { + LogBatchData::SliceOfOwnedData(data) => { + if self.index < data.len() { + let record = &*data[self.index]; + self.index += 1; + Some((&record.0, &record.1)) + } else { + None + } + } + LogBatchData::SliceOfBorrowedData(data) => { + if self.index < data.len() { + let record = &data[self.index]; + self.index += 1; + Some((record.0, record.1)) + } else { + None + } + } + } + } +} + +/// `LogExporter` defines the interface that log exporters should implement. +pub trait LogExporter: Send + Sync + Debug { + /// Exports a batch of log records and their associated instrumentation scopes. + /// + /// The `export` method is responsible for sending a batch of log records to an external + /// destination. It takes a `LogBatch` as an argument, which contains references to the + /// log records and their corresponding instrumentation scopes. The method returns + /// a `LogResult` indicating the success or failure of the export operation. + /// + /// # Arguments + /// + /// * `batch` - A `LogBatch` containing the log records and instrumentation scopes + /// to be exported. + /// + /// # Returns + /// + /// A `LogResult<()>`, which is a result type indicating either a successful export (with + /// `Ok(())`) or an error (`Err(LogError)`) if the export operation failed. + /// + fn export( + &self, + batch: LogBatch<'_>, + ) -> impl std::future::Future + Send; + /// Shuts down the exporter. + fn shutdown_with_timeout(&self, _timeout: time::Duration) -> OTelSdkResult { + Ok(()) + } + /// Shuts down the exporter with a default timeout. + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(time::Duration::from_secs(5)) + } + + /// Check if logs are enabled. + fn event_enabled(&self, _level: Severity, _target: &str, _name: Option<&str>) -> bool { + // By default, all logs are enabled + true + } + /// Set the resource for the exporter. + fn set_resource(&mut self, _resource: &Resource) {} +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/in_memory_exporter.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/in_memory_exporter.rs new file mode 100644 index 00000000..cb84e693 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/in_memory_exporter.rs @@ -0,0 +1,226 @@ +use crate::error::{OTelSdkError, OTelSdkResult}; +use crate::logs::SdkLogRecord; +use crate::logs::{LogBatch, LogExporter}; +use crate::Resource; +use opentelemetry::InstrumentationScope; +use std::borrow::Cow; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Mutex}; +use std::time; + +/// An in-memory logs exporter that stores logs data in memory. +/// +/// This exporter is useful for testing and debugging purposes. +/// It stores logs in a `Vec`. Logs can be retrieved using +/// `get_emitted_logs` method. +/// +/// # Example +/// ```no_run +///# use opentelemetry_sdk::logs::{BatchLogProcessor, SdkLoggerProvider}; +///# use opentelemetry_sdk::runtime; +///# use opentelemetry_sdk::logs::InMemoryLogExporter; +/// +///# #[tokio::main] +///# async fn main() { +/// // Create an InMemoryLogExporter +/// let exporter: InMemoryLogExporter = InMemoryLogExporter::default(); +/// //Create a LoggerProvider and register the exporter +/// let logger_provider = SdkLoggerProvider::builder() +/// .with_log_processor(BatchLogProcessor::builder(exporter.clone()).build()) +/// .build(); +/// // Setup Log Appenders and emit logs. (Not shown here) +/// logger_provider.force_flush(); +/// let emitted_logs = exporter.get_emitted_logs().unwrap(); +/// for log in emitted_logs { +/// println!("{:?}", log); +/// } +///# } +/// ``` +/// +#[derive(Clone, Debug)] +pub struct InMemoryLogExporter { + logs: Arc>>, + resource: Arc>, + should_reset_on_shutdown: bool, + shutdown_called: Arc, +} + +impl Default for InMemoryLogExporter { + fn default() -> Self { + InMemoryLogExporterBuilder::new().build() + } +} + +/// `OwnedLogData` represents a single log event without resource context. +#[derive(Debug, Clone)] +pub struct OwnedLogData { + /// Log record, which can be borrowed or owned. + pub record: SdkLogRecord, + /// Instrumentation details for the emitter who produced this `LogEvent`. + pub instrumentation: InstrumentationScope, +} + +/// `LogDataWithResource` associates a [`SdkLogRecord`] with a [`Resource`] and +/// [`InstrumentationScope`]. +#[derive(Clone, Debug)] +pub struct LogDataWithResource { + /// Log record + pub record: SdkLogRecord, + /// Instrumentation details for the emitter who produced this `LogRecord`. + pub instrumentation: InstrumentationScope, + /// Resource for the emitter who produced this `LogRecord`. + pub resource: Cow<'static, Resource>, +} + +///Builder for [`InMemoryLogExporter`]. +/// # Example +/// +/// ```no_run +///# use opentelemetry_sdk::logs::{InMemoryLogExporter, InMemoryLogExporterBuilder}; +///# use opentelemetry_sdk::logs::{BatchLogProcessor, SdkLoggerProvider}; +///# use opentelemetry_sdk::runtime; +/// +///# #[tokio::main] +///# async fn main() { +/// //Create an InMemoryLogExporter +/// let exporter: InMemoryLogExporter = InMemoryLogExporterBuilder::default().build(); +/// //Create a LoggerProvider and register the exporter +/// let logger_provider = SdkLoggerProvider::builder() +/// .with_log_processor(BatchLogProcessor::builder(exporter.clone()).build()) +/// .build(); +/// // Setup Log Appenders and emit logs. (Not shown here) +/// logger_provider.force_flush(); +/// let emitted_logs = exporter.get_emitted_logs().unwrap(); +/// for log in emitted_logs { +/// println!("{:?}", log); +/// } +///# } +/// +/// ``` +/// +#[derive(Debug, Clone)] +pub struct InMemoryLogExporterBuilder { + reset_on_shutdown: bool, +} + +impl Default for InMemoryLogExporterBuilder { + fn default() -> Self { + Self::new() + } +} + +impl InMemoryLogExporterBuilder { + /// Creates a new instance of `InMemoryLogExporter`. + /// + pub fn new() -> Self { + Self { + reset_on_shutdown: true, + } + } + + /// Creates a new instance of `InMemoryLogExporter`. + /// + pub fn build(&self) -> InMemoryLogExporter { + InMemoryLogExporter { + logs: Arc::new(Mutex::new(Vec::new())), + resource: Arc::new(Mutex::new(Resource::builder().build())), + should_reset_on_shutdown: self.reset_on_shutdown, + shutdown_called: Arc::new(AtomicBool::new(false)), + } + } + + /// If set, the records will not be [`InMemoryLogExporter::reset`] on shutdown. + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn keep_records_on_shutdown(self) -> Self { + Self { + reset_on_shutdown: false, + } + } +} + +impl InMemoryLogExporter { + /// Returns true if shutdown was called. + pub fn is_shutdown_called(&self) -> bool { + self.shutdown_called + .load(std::sync::atomic::Ordering::Relaxed) + } + + /// Returns the logs emitted via Logger as a vector of `LogDataWithResource`. + /// + /// # Errors + /// + /// Returns an `OTelSdkError`. + /// + /// # Example + /// + /// ``` + /// use opentelemetry_sdk::logs::{InMemoryLogExporter, InMemoryLogExporterBuilder}; + /// + /// let exporter = InMemoryLogExporterBuilder::default().build(); + /// let emitted_logs = exporter.get_emitted_logs().unwrap(); + /// ``` + /// + pub fn get_emitted_logs(&self) -> Result, OTelSdkError> { + let logs_guard = self.logs.lock().map_err(OTelSdkError::from)?; + let resource_guard = self.resource.lock().map_err(OTelSdkError::from)?; + let logs: Vec = logs_guard + .iter() + .map(|log_data| LogDataWithResource { + record: log_data.record.clone(), + resource: Cow::Owned(resource_guard.clone()), + instrumentation: log_data.instrumentation.clone(), + }) + .collect(); + + Ok(logs) + } + /// Clears the internal (in-memory) storage of logs. + /// + /// # Example + /// + /// ``` + /// use opentelemetry_sdk::logs::{InMemoryLogExporter, InMemoryLogExporterBuilder}; + /// + /// let exporter = InMemoryLogExporterBuilder::default().build(); + /// exporter.reset(); + /// ``` + /// + pub fn reset(&self) { + let _ = self + .logs + .lock() + .map(|mut logs_guard| logs_guard.clear()) + .map_err(|e| OTelSdkError::InternalFailure(format!("Failed to reset logs: {e}"))); + } +} + +impl LogExporter for InMemoryLogExporter { + async fn export(&self, batch: LogBatch<'_>) -> OTelSdkResult { + let mut logs_guard = self.logs.lock().map_err(|e| { + OTelSdkError::InternalFailure(format!("Failed to lock logs for export: {e}")) + })?; + for (log_record, instrumentation) in batch.iter() { + let owned_log = OwnedLogData { + record: (*log_record).clone(), + instrumentation: (*instrumentation).clone(), + }; + logs_guard.push(owned_log); + } + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: time::Duration) -> OTelSdkResult { + self.shutdown_called + .store(true, std::sync::atomic::Ordering::Relaxed); + if self.should_reset_on_shutdown { + self.reset(); + } + Ok(()) + } + + fn set_resource(&mut self, resource: &Resource) { + let mut res_guard = self.resource.lock().expect("Resource lock poisoned"); + *res_guard = resource.clone(); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/log_processor.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/log_processor.rs new file mode 100644 index 00000000..5a7fe317 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/log_processor.rs @@ -0,0 +1,240 @@ +//! # OpenTelemetry Log Processor Interface +//! +//! The `LogProcessor` interface provides hooks for log record processing and +//! exporting. Log processors receive `LogRecord`s emitted by the SDK's +//! `Logger` and determine how these records are handled. +//! +//! Built-in log processors are responsible for converting logs to exportable +//! representations and passing them to configured exporters. They can be +//! registered directly with a `LoggerProvider`. +//! +//! ## Types of Log Processors +//! +//! There are currently two types of log processors available in the SDK: +//! - **SimpleLogProcessor**: Forwards log records to the exporter immediately. +//! - **BatchLogProcessor**: Buffers log records and sends them to the exporter in batches. +//! +//! For more information, see simple_log_processor.rs and batch_log_processor.rs. +//! +//! ## Diagram +//! +//! ```ascii +//! +-----+---------------+ +-----------------------+ +-------------------+ +//! | | | | | | | +//! | SDK | Logger.emit() +---> (Simple)LogProcessor +---> LogExporter | +//! | | | | (Batch)LogProcessor +---> (OTLPExporter) | +//! +-----+---------------+ +-----------------------+ +-------------------+ +//! ``` + +use crate::error::OTelSdkResult; +use crate::{logs::SdkLogRecord, Resource}; + +use opentelemetry::logs::Severity; +use opentelemetry::{otel_warn, InstrumentationScope}; + +use std::fmt::Debug; +use std::time::Duration; + +/// The interface for plugging into a [`SdkLogger`]. +/// +/// [`SdkLogger`]: crate::logs::SdkLogger +pub trait LogProcessor: Send + Sync + Debug { + /// Called when a log record is ready to processed and exported. + /// + /// This method receives a mutable reference to `LogRecord`. If the processor + /// needs to handle the export asynchronously, it should clone the data to + /// ensure it can be safely processed without lifetime issues. Any changes + /// made to the log data in this method will be reflected in the next log + /// processor in the chain. + /// + /// # Parameters + /// - `record`: A mutable reference to `LogRecord` representing the log record. + /// - `instrumentation`: The instrumentation scope associated with the log record. + fn emit(&self, data: &mut SdkLogRecord, instrumentation: &InstrumentationScope); + /// Force the logs lying in the cache to be exported. + fn force_flush(&self) -> OTelSdkResult; + /// Shuts down the processor. + /// After shutdown returns the log processor should stop processing any logs. + /// It's up to the implementation on when to drop the LogProcessor. + /// + /// Implementors that manage resources (background threads, network connections, + /// file handles, etc.) should override this method to properly clean up. + /// Processors that wrap other processors should forward the shutdown call to + /// the wrapped processor(s). + /// Simple processors that only transform log data can use the default implementation. + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + // It would have been better to make this method required, but that ship + // sailed when the logs API was declared stable. + otel_warn!( + name: "LogProcessor.DefaultShutdownWithTimeout", + message = "LogProcessor is using default shutdown implementation. If this processor manages background threads, network connections, file handles, or other resources that need cleanup, implement `shutdown_with_timeout()` to properly release them. Simple processors that only transform log data can safely use this default." + ); + Ok(()) + } + /// Shuts down the processor with default timeout. + /// + /// Implementors typically do not need to change this method, and can just + /// implement `shutdown_with_timeout`. + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } + + /// Check if logging is enabled + fn event_enabled(&self, _level: Severity, _target: &str, _name: Option<&str>) -> bool { + // By default, all logs are enabled + true + } + + /// Set the resource for the log processor. + fn set_resource(&mut self, _resource: &Resource) {} +} + +#[cfg(all(test, feature = "testing", feature = "logs"))] +pub(crate) mod tests { + use crate::logs::{LogBatch, LogExporter, SdkLogRecord}; + use crate::Resource; + use crate::{ + error::OTelSdkResult, + logs::{LogProcessor, SdkLoggerProvider}, + }; + use opentelemetry::logs::AnyValue; + use opentelemetry::logs::LogRecord as _; + use opentelemetry::logs::{Logger, LoggerProvider}; + use opentelemetry::{InstrumentationScope, Key}; + use std::sync::{Arc, Mutex}; + + #[derive(Debug, Clone)] + pub(crate) struct MockLogExporter { + pub resource: Arc>>, + } + + impl LogExporter for MockLogExporter { + async fn export(&self, _batch: LogBatch<'_>) -> OTelSdkResult { + Ok(()) + } + + fn set_resource(&mut self, resource: &Resource) { + self.resource + .lock() + .map(|mut res_opt| { + res_opt.replace(resource.clone()); + }) + .expect("mock log exporter shouldn't error when setting resource"); + } + } + + // Implementation specific to the MockLogExporter, not part of the LogExporter trait + impl MockLogExporter { + pub(crate) fn get_resource(&self) -> Option { + (*self.resource).lock().unwrap().clone() + } + } + + #[derive(Debug)] + struct FirstProcessor { + pub(crate) logs: Arc>>, + } + + impl LogProcessor for FirstProcessor { + fn emit(&self, record: &mut SdkLogRecord, instrumentation: &InstrumentationScope) { + // add attribute + record.add_attribute( + Key::from_static_str("processed_by"), + AnyValue::String("FirstProcessor".into()), + ); + // update body + record.body = Some("Updated by FirstProcessor".into()); + + self.logs + .lock() + .unwrap() + .push((record.clone(), instrumentation.clone())); //clone as the LogProcessor is storing the data. + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } + } + + #[derive(Debug)] + struct SecondProcessor { + pub(crate) logs: Arc>>, + } + + impl LogProcessor for SecondProcessor { + fn emit(&self, record: &mut SdkLogRecord, instrumentation: &InstrumentationScope) { + assert!(record.attributes_contains( + &Key::from_static_str("processed_by"), + &AnyValue::String("FirstProcessor".into()) + )); + assert!( + record.body.clone().unwrap() + == AnyValue::String("Updated by FirstProcessor".into()) + ); + self.logs + .lock() + .unwrap() + .push((record.clone(), instrumentation.clone())); + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } + } + + #[test] + fn test_log_data_modification_by_multiple_processors() { + let first_processor_logs = Arc::new(Mutex::new(Vec::new())); + let second_processor_logs = Arc::new(Mutex::new(Vec::new())); + + let first_processor = FirstProcessor { + logs: Arc::clone(&first_processor_logs), + }; + let second_processor = SecondProcessor { + logs: Arc::clone(&second_processor_logs), + }; + + let logger_provider = SdkLoggerProvider::builder() + .with_log_processor(first_processor) + .with_log_processor(second_processor) + .build(); + + let logger = logger_provider.logger("test-logger"); + let mut log_record = logger.create_log_record(); + log_record.body = Some(AnyValue::String("Test log".into())); + + logger.emit(log_record); + + assert_eq!(first_processor_logs.lock().unwrap().len(), 1); + assert_eq!(second_processor_logs.lock().unwrap().len(), 1); + + let first_log = &first_processor_logs.lock().unwrap()[0]; + let second_log = &second_processor_logs.lock().unwrap()[0]; + + assert!(first_log.0.attributes_contains( + &Key::from_static_str("processed_by"), + &AnyValue::String("FirstProcessor".into()) + )); + assert!(second_log.0.attributes_contains( + &Key::from_static_str("processed_by"), + &AnyValue::String("FirstProcessor".into()) + )); + + assert!( + first_log.0.body.clone().unwrap() + == AnyValue::String("Updated by FirstProcessor".into()) + ); + assert!( + second_log.0.body.clone().unwrap() + == AnyValue::String("Updated by FirstProcessor".into()) + ); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/log_processor_with_async_runtime.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/log_processor_with_async_runtime.rs new file mode 100644 index 00000000..34e392d2 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/log_processor_with_async_runtime.rs @@ -0,0 +1,863 @@ +use crate::error::{OTelSdkError, OTelSdkResult}; +use crate::{ + logs::{LogBatch, LogExporter, SdkLogRecord}, + Resource, +}; + +use opentelemetry::{otel_debug, otel_error, otel_warn, InstrumentationScope}; + +use std::{ + fmt::{self, Debug, Formatter}, + sync::Arc, +}; +use std::{ + sync::atomic::{AtomicUsize, Ordering}, + time::Duration, +}; + +use super::{BatchConfig, LogProcessor}; +#[cfg(feature = "experimental_async_runtime")] +use crate::runtime::{to_interval_stream, RuntimeChannel, TrySend}; +use futures_channel::oneshot; +use futures_util::{ + future::{self, Either}, + {pin_mut, stream, StreamExt as _}, +}; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +enum BatchMessage { + /// Export logs, usually called when the log is emitted. + ExportLog((SdkLogRecord, InstrumentationScope)), + /// Flush the current buffer to the backend, it can be triggered by + /// pre configured interval or a call to `force_push` function. + Flush(Option>), + /// Shut down the worker thread, push all logs in buffer to the backend. + Shutdown(oneshot::Sender), + /// Set the resource for the exporter. + SetResource(Arc), +} + +/// A [`LogProcessor`] that asynchronously buffers log records and reports +/// them at a pre-configured interval. +pub struct BatchLogProcessor { + message_sender: R::Sender, + + // Track dropped logs - we'll log this at shutdown + dropped_logs_count: AtomicUsize, + + // Track the maximum queue size that was configured for this processor + max_queue_size: usize, +} + +impl Debug for BatchLogProcessor { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("BatchLogProcessor") + .field("message_sender", &self.message_sender) + .finish() + } +} + +impl LogProcessor for BatchLogProcessor { + fn emit(&self, record: &mut SdkLogRecord, instrumentation: &InstrumentationScope) { + let result = self.message_sender.try_send(BatchMessage::ExportLog(( + record.clone(), + instrumentation.clone(), + ))); + + // TODO - Implement throttling to prevent error flooding when the queue is full or closed. + if result.is_err() { + // Increment dropped logs count. The first time we have to drop a log, + // emit a warning. + if self.dropped_logs_count.fetch_add(1, Ordering::Relaxed) == 0 { + otel_warn!(name: "BatchLogProcessor.LogDroppingStarted", + message = "BatchLogProcessor dropped a LogRecord due to queue full/internal errors. No further log will be emitted for further drops until Shutdown. During Shutdown time, a log will be emitted with exact count of total logs dropped."); + } + } + } + + fn force_flush(&self) -> OTelSdkResult { + let (res_sender, res_receiver) = oneshot::channel(); + self.message_sender + .try_send(BatchMessage::Flush(Some(res_sender))) + .map_err(|err| OTelSdkError::InternalFailure(format!("{err:?}")))?; + + futures_executor::block_on(res_receiver) + .map_err(|err| OTelSdkError::InternalFailure(format!("{err:?}"))) + .and_then(std::convert::identity) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + let dropped_logs = self.dropped_logs_count.load(Ordering::Relaxed); + let max_queue_size = self.max_queue_size; + if dropped_logs > 0 { + otel_warn!( + name: "BatchLogProcessor.LogsDropped", + dropped_logs_count = dropped_logs, + max_queue_size = max_queue_size, + message = "Logs were dropped due to a queue being full or other error. The count represents the total count of log records dropped in the lifetime of this BatchLogProcessor. Consider increasing the queue size and/or decrease delay between intervals." + ); + } + let (res_sender, res_receiver) = oneshot::channel(); + self.message_sender + .try_send(BatchMessage::Shutdown(res_sender)) + .map_err(|err| OTelSdkError::InternalFailure(format!("{err:?}")))?; + + futures_executor::block_on(res_receiver) + .map_err(|err| OTelSdkError::InternalFailure(format!("{err:?}"))) + .and_then(std::convert::identity) + } + + fn set_resource(&mut self, resource: &Resource) { + let resource = Arc::new(resource.clone()); + let _ = self + .message_sender + .try_send(BatchMessage::SetResource(resource)); + } +} + +impl BatchLogProcessor { + pub(crate) fn new(mut exporter: E, config: BatchConfig, runtime: R) -> Self + where + E: LogExporter + Send + Sync + 'static, + { + let (message_sender, message_receiver) = + runtime.batch_message_channel(config.max_queue_size); + let inner_runtime = runtime.clone(); + + // Spawn worker process via user-defined spawn function. + runtime.spawn(async move { + // Timer will take a reference to the current runtime, so its important we do this within the + // runtime.spawn() + let ticker = to_interval_stream(inner_runtime.clone(), config.scheduled_delay) + .skip(1) // The ticker is fired immediately, so we should skip the first one to align with the interval. + .map(|_| BatchMessage::Flush(None)); + + let timeout_runtime = inner_runtime.clone(); + let mut logs = Vec::new(); + let mut messages = Box::pin(stream::select(message_receiver, ticker)); + + while let Some(message) = messages.next().await { + match message { + // Log has finished, add to buffer of pending logs. + BatchMessage::ExportLog(log) => { + logs.push(log); + if logs.len() == config.max_export_batch_size { + let result = export_with_timeout( + config.max_export_timeout, + &mut exporter, + &timeout_runtime, + logs.split_off(0), + ) + .await; + + if let Err(err) = result { + otel_error!( + name: "BatchLogProcessor.Export.Error", + error = format!("{}", err) + ); + } + } + } + // Log batch interval time reached or a force flush has been invoked, export current logs. + BatchMessage::Flush(res_channel) => { + let result = export_with_timeout( + config.max_export_timeout, + &mut exporter, + &timeout_runtime, + logs.split_off(0), + ) + .await; + + if let Some(channel) = res_channel { + if let Err(send_error) = channel.send(result) { + otel_debug!( + name: "BatchLogProcessor.Flush.SendResultError", + error = format!("{:?}", send_error), + ); + } + } + } + // Stream has terminated or processor is shutdown, return to finish execution. + BatchMessage::Shutdown(ch) => { + let result = export_with_timeout( + config.max_export_timeout, + &mut exporter, + &timeout_runtime, + logs.split_off(0), + ) + .await; + + let _ = exporter.shutdown(); //TODO - handle error + + if let Err(send_error) = ch.send(result) { + otel_debug!( + name: "BatchLogProcessor.Shutdown.SendResultError", + error = format!("{:?}", send_error), + ); + } + break; + } + // propagate the resource + BatchMessage::SetResource(resource) => { + exporter.set_resource(&resource); + } + } + } + }); + // Return batch processor with link to worker + BatchLogProcessor { + message_sender, + dropped_logs_count: AtomicUsize::new(0), + max_queue_size: config.max_queue_size, + } + } + + /// Create a new batch processor builder + pub fn builder(exporter: E, runtime: R) -> BatchLogProcessorBuilder + where + E: LogExporter, + { + BatchLogProcessorBuilder { + exporter, + config: Default::default(), + runtime, + } + } +} + +async fn export_with_timeout( + time_out: Duration, + exporter: &mut E, + runtime: &R, + batch: Vec<(SdkLogRecord, InstrumentationScope)>, +) -> OTelSdkResult +where + R: RuntimeChannel, + E: LogExporter + ?Sized, +{ + if batch.is_empty() { + return Ok(()); + } + + // TBD - Can we avoid this conversion as it involves heap allocation with new vector? + let log_vec: Vec<(&SdkLogRecord, &InstrumentationScope)> = batch + .iter() + .map(|log_data| (&log_data.0, &log_data.1)) + .collect(); + let export = exporter.export(LogBatch::new(log_vec.as_slice())); + let timeout = runtime.delay(time_out); + pin_mut!(export); + pin_mut!(timeout); + match future::select(export, timeout).await { + Either::Left((export_res, _)) => export_res, + Either::Right((_, _)) => OTelSdkResult::Err(OTelSdkError::Timeout(time_out)), + } +} + +/// A builder for creating [`BatchLogProcessor`] instances. +/// +#[derive(Debug)] +pub struct BatchLogProcessorBuilder { + exporter: E, + config: BatchConfig, + runtime: R, +} + +impl BatchLogProcessorBuilder +where + E: LogExporter + 'static, + R: RuntimeChannel, +{ + /// Set the BatchConfig for [`BatchLogProcessorBuilder`] + pub fn with_batch_config(self, config: BatchConfig) -> Self { + BatchLogProcessorBuilder { config, ..self } + } + + /// Build a batch processor + pub fn build(self) -> BatchLogProcessor { + BatchLogProcessor::new(self.exporter, self.config, self.runtime) + } +} + +#[cfg(all(test, feature = "testing", feature = "logs"))] +mod tests { + use crate::error::OTelSdkResult; + use crate::logs::batch_log_processor::{ + OTEL_BLRP_EXPORT_TIMEOUT, OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, OTEL_BLRP_MAX_QUEUE_SIZE, + OTEL_BLRP_SCHEDULE_DELAY, + }; + use crate::logs::log_processor_with_async_runtime::BatchLogProcessor; + use crate::logs::InMemoryLogExporterBuilder; + use crate::logs::SdkLogRecord; + use crate::logs::{LogBatch, LogExporter}; + use crate::runtime; + use crate::{ + logs::{ + batch_log_processor::{ + OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT, OTEL_BLRP_MAX_EXPORT_BATCH_SIZE_DEFAULT, + OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT, OTEL_BLRP_SCHEDULE_DELAY_DEFAULT, + }, + BatchConfig, BatchConfigBuilder, InMemoryLogExporter, LogProcessor, SdkLoggerProvider, + SimpleLogProcessor, + }, + Resource, + }; + use opentelemetry::logs::AnyValue; + use opentelemetry::logs::LogRecord; + use opentelemetry::logs::{Logger, LoggerProvider}; + use opentelemetry::KeyValue; + use opentelemetry::{InstrumentationScope, Key}; + use std::sync::{Arc, Mutex}; + use std::time::Duration; + + #[derive(Debug, Clone)] + struct MockLogExporter { + resource: Arc>>, + } + + impl LogExporter for MockLogExporter { + async fn export(&self, _batch: LogBatch<'_>) -> OTelSdkResult { + Ok(()) + } + + fn set_resource(&mut self, resource: &Resource) { + self.resource + .lock() + .map(|mut res_opt| { + res_opt.replace(resource.clone()); + }) + .expect("mock log exporter shouldn't error when setting resource"); + } + } + + // Implementation specific to the MockLogExporter, not part of the LogExporter trait + impl MockLogExporter { + fn get_resource(&self) -> Option { + (*self.resource).lock().unwrap().clone() + } + } + + #[test] + fn test_default_const_values() { + assert_eq!(OTEL_BLRP_SCHEDULE_DELAY, "OTEL_BLRP_SCHEDULE_DELAY"); + assert_eq!(OTEL_BLRP_SCHEDULE_DELAY_DEFAULT.as_millis(), 1_000); + assert_eq!(OTEL_BLRP_EXPORT_TIMEOUT, "OTEL_BLRP_EXPORT_TIMEOUT"); + assert_eq!(OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT.as_millis(), 30_000); + assert_eq!(OTEL_BLRP_MAX_QUEUE_SIZE, "OTEL_BLRP_MAX_QUEUE_SIZE"); + assert_eq!(OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT, 2_048); + assert_eq!( + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, + "OTEL_BLRP_MAX_EXPORT_BATCH_SIZE" + ); + assert_eq!(OTEL_BLRP_MAX_EXPORT_BATCH_SIZE_DEFAULT, 512); + } + + #[test] + fn test_default_batch_config_adheres_to_specification() { + // The following environment variables are expected to be unset so that their default values are used. + let env_vars = vec![ + OTEL_BLRP_SCHEDULE_DELAY, + OTEL_BLRP_EXPORT_TIMEOUT, + OTEL_BLRP_MAX_QUEUE_SIZE, + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, + ]; + + let config = temp_env::with_vars_unset(env_vars, BatchConfig::default); + + assert_eq!(config.scheduled_delay, OTEL_BLRP_SCHEDULE_DELAY_DEFAULT); + assert_eq!(config.max_export_timeout, OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT); + assert_eq!(config.max_queue_size, OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT); + assert_eq!( + config.max_export_batch_size, + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE_DEFAULT + ); + } + + #[test] + fn test_batch_config_configurable_by_env_vars() { + let env_vars = vec![ + (OTEL_BLRP_SCHEDULE_DELAY, Some("2000")), + (OTEL_BLRP_EXPORT_TIMEOUT, Some("60000")), + (OTEL_BLRP_MAX_QUEUE_SIZE, Some("4096")), + (OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, Some("1024")), + ]; + + let config = temp_env::with_vars(env_vars, BatchConfig::default); + + assert_eq!(config.scheduled_delay, Duration::from_millis(2000)); + assert_eq!(config.max_export_timeout, Duration::from_millis(60000)); + assert_eq!(config.max_queue_size, 4096); + assert_eq!(config.max_export_batch_size, 1024); + } + + #[test] + fn test_batch_config_max_export_batch_size_validation() { + let env_vars = vec![ + (OTEL_BLRP_MAX_QUEUE_SIZE, Some("256")), + (OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, Some("1024")), + ]; + + let config = temp_env::with_vars(env_vars, BatchConfig::default); + + assert_eq!(config.max_queue_size, 256); + assert_eq!(config.max_export_batch_size, 256); + assert_eq!(config.scheduled_delay, OTEL_BLRP_SCHEDULE_DELAY_DEFAULT); + assert_eq!(config.max_export_timeout, OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT); + } + + #[test] + fn test_batch_config_with_fields() { + let batch = BatchConfigBuilder::default() + .with_max_export_batch_size(1) + .with_scheduled_delay(Duration::from_millis(2)) + .with_max_export_timeout(Duration::from_millis(3)) + .with_max_queue_size(4) + .build(); + + assert_eq!(batch.max_export_batch_size, 1); + assert_eq!(batch.scheduled_delay, Duration::from_millis(2)); + assert_eq!(batch.max_export_timeout, Duration::from_millis(3)); + assert_eq!(batch.max_queue_size, 4); + } + + #[test] + fn test_build_batch_log_processor_builder() { + let mut env_vars = vec![ + (OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, Some("500")), + (OTEL_BLRP_SCHEDULE_DELAY, Some("I am not number")), + (OTEL_BLRP_EXPORT_TIMEOUT, Some("2046")), + ]; + temp_env::with_vars(env_vars.clone(), || { + let builder = + BatchLogProcessor::builder(InMemoryLogExporter::default(), runtime::Tokio); + + assert_eq!(builder.config.max_export_batch_size, 500); + assert_eq!( + builder.config.scheduled_delay, + OTEL_BLRP_SCHEDULE_DELAY_DEFAULT + ); + assert_eq!( + builder.config.max_queue_size, + OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT + ); + assert_eq!( + builder.config.max_export_timeout, + Duration::from_millis(2046) + ); + }); + + env_vars.push((OTEL_BLRP_MAX_QUEUE_SIZE, Some("120"))); + + temp_env::with_vars(env_vars, || { + let builder = + BatchLogProcessor::builder(InMemoryLogExporter::default(), runtime::Tokio); + assert_eq!(builder.config.max_export_batch_size, 120); + assert_eq!(builder.config.max_queue_size, 120); + }); + } + + #[test] + fn test_build_batch_log_processor_builder_with_custom_config() { + let expected = BatchConfigBuilder::default() + .with_max_export_batch_size(1) + .with_scheduled_delay(Duration::from_millis(2)) + .with_max_export_timeout(Duration::from_millis(3)) + .with_max_queue_size(4) + .build(); + + let builder = BatchLogProcessor::builder(InMemoryLogExporter::default(), runtime::Tokio) + .with_batch_config(expected); + + let actual = &builder.config; + assert_eq!(actual.max_export_batch_size, 1); + assert_eq!(actual.scheduled_delay, Duration::from_millis(2)); + assert_eq!(actual.max_export_timeout, Duration::from_millis(3)); + assert_eq!(actual.max_queue_size, 4); + } + + #[test] + fn test_set_resource_simple_processor() { + let exporter = MockLogExporter { + resource: Arc::new(Mutex::new(None)), + }; + let processor = SimpleLogProcessor::new(exporter.clone()); + let _ = SdkLoggerProvider::builder() + .with_log_processor(processor) + .with_resource( + Resource::builder_empty() + .with_attributes([ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v3"), + KeyValue::new("k3", "v3"), + KeyValue::new("k4", "v4"), + KeyValue::new("k5", "v5"), + ]) + .build(), + ) + .build(); + assert_eq!(exporter.get_resource().unwrap().into_iter().count(), 5); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_set_resource_batch_processor() { + let exporter = MockLogExporter { + resource: Arc::new(Mutex::new(None)), + }; + let processor = + BatchLogProcessor::new(exporter.clone(), BatchConfig::default(), runtime::Tokio); + let provider = SdkLoggerProvider::builder() + .with_log_processor(processor) + .with_resource( + Resource::builder_empty() + .with_attributes([ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v3"), + KeyValue::new("k3", "v3"), + KeyValue::new("k4", "v4"), + KeyValue::new("k5", "v5"), + ]) + .build(), + ) + .build(); + + provider.force_flush().unwrap(); + + assert_eq!(exporter.get_resource().unwrap().into_iter().count(), 5); + let _ = provider.shutdown(); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_batch_shutdown() { + // assert we will receive an error + // setup + let exporter = InMemoryLogExporterBuilder::default() + .keep_records_on_shutdown() + .build(); + let processor = + BatchLogProcessor::new(exporter.clone(), BatchConfig::default(), runtime::Tokio); + + let mut record = SdkLogRecord::new(); + let instrumentation = InstrumentationScope::default(); + + processor.emit(&mut record, &instrumentation); + processor.force_flush().unwrap(); + processor.shutdown().unwrap(); + // todo: expect to see errors here. How should we assert this? + processor.emit(&mut record, &instrumentation); + assert_eq!(1, exporter.get_emitted_logs().unwrap().len()) + } + + #[tokio::test(flavor = "current_thread")] + async fn test_batch_log_processor_shutdown_under_async_runtime_current_flavor_multi_thread() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = BatchLogProcessor::new( + exporter.clone(), + BatchConfig::default(), + runtime::TokioCurrentThread, + ); + + processor.shutdown().unwrap(); + } + + #[tokio::test(flavor = "current_thread")] + #[ignore = "See issue https://github.com/open-telemetry/opentelemetry-rust/issues/1968"] + async fn test_batch_log_processor_with_async_runtime_shutdown_under_async_runtime_current_flavor_multi_thread( + ) { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = BatchLogProcessor::new( + exporter.clone(), + BatchConfig::default(), + runtime::TokioCurrentThread, + ); + + // + // deadlock happens in shutdown with tokio current_thread runtime + // + processor.shutdown().unwrap(); + } + + #[tokio::test(flavor = "current_thread")] + async fn test_batch_log_processor_shutdown_with_async_runtime_current_flavor_current_thread() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = BatchLogProcessor::new( + exporter.clone(), + BatchConfig::default(), + runtime::TokioCurrentThread, + ); + processor.shutdown().unwrap(); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_batch_log_processor_shutdown_with_async_runtime_multi_flavor_multi_thread() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = + BatchLogProcessor::new(exporter.clone(), BatchConfig::default(), runtime::Tokio); + processor.shutdown().unwrap(); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_batch_log_processor_shutdown_with_async_runtime_multi_flavor_current_thread() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = + BatchLogProcessor::new(exporter.clone(), BatchConfig::default(), runtime::Tokio); + processor.shutdown().unwrap(); + } + + #[derive(Debug)] + struct FirstProcessor { + pub(crate) logs: Arc>>, + } + + impl LogProcessor for FirstProcessor { + fn emit(&self, record: &mut SdkLogRecord, instrumentation: &InstrumentationScope) { + // add attribute + record.add_attribute( + Key::from_static_str("processed_by"), + AnyValue::String("FirstProcessor".into()), + ); + // update body + record.body = Some("Updated by FirstProcessor".into()); + + self.logs + .lock() + .unwrap() + .push((record.clone(), instrumentation.clone())); //clone as the LogProcessor is storing the data. + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } + } + + #[derive(Debug)] + struct SecondProcessor { + pub(crate) logs: Arc>>, + } + + impl LogProcessor for SecondProcessor { + fn emit(&self, record: &mut SdkLogRecord, instrumentation: &InstrumentationScope) { + assert!(record.attributes_contains( + &Key::from_static_str("processed_by"), + &AnyValue::String("FirstProcessor".into()) + )); + assert!( + record.body.clone().unwrap() + == AnyValue::String("Updated by FirstProcessor".into()) + ); + self.logs + .lock() + .unwrap() + .push((record.clone(), instrumentation.clone())); + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } + } + #[test] + fn test_log_data_modification_by_multiple_processors() { + let first_processor_logs = Arc::new(Mutex::new(Vec::new())); + let second_processor_logs = Arc::new(Mutex::new(Vec::new())); + + let first_processor = FirstProcessor { + logs: Arc::clone(&first_processor_logs), + }; + let second_processor = SecondProcessor { + logs: Arc::clone(&second_processor_logs), + }; + + let logger_provider = SdkLoggerProvider::builder() + .with_log_processor(first_processor) + .with_log_processor(second_processor) + .build(); + + let logger = logger_provider.logger("test-logger"); + let mut log_record = logger.create_log_record(); + log_record.body = Some(AnyValue::String("Test log".into())); + + logger.emit(log_record); + + assert_eq!(first_processor_logs.lock().unwrap().len(), 1); + assert_eq!(second_processor_logs.lock().unwrap().len(), 1); + + let first_log = &first_processor_logs.lock().unwrap()[0]; + let second_log = &second_processor_logs.lock().unwrap()[0]; + + assert!(first_log.0.attributes_contains( + &Key::from_static_str("processed_by"), + &AnyValue::String("FirstProcessor".into()) + )); + assert!(second_log.0.attributes_contains( + &Key::from_static_str("processed_by"), + &AnyValue::String("FirstProcessor".into()) + )); + + assert!( + first_log.0.body.clone().unwrap() + == AnyValue::String("Updated by FirstProcessor".into()) + ); + assert!( + second_log.0.body.clone().unwrap() + == AnyValue::String("Updated by FirstProcessor".into()) + ); + } + + #[test] + fn test_build_batch_log_processor_builder_rt() { + let mut env_vars = vec![ + (OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, Some("500")), + (OTEL_BLRP_SCHEDULE_DELAY, Some("I am not number")), + (OTEL_BLRP_EXPORT_TIMEOUT, Some("2046")), + ]; + temp_env::with_vars(env_vars.clone(), || { + let builder = + BatchLogProcessor::builder(InMemoryLogExporter::default(), runtime::Tokio); + + assert_eq!(builder.config.max_export_batch_size, 500); + assert_eq!( + builder.config.scheduled_delay, + OTEL_BLRP_SCHEDULE_DELAY_DEFAULT + ); + assert_eq!( + builder.config.max_queue_size, + OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT + ); + assert_eq!( + builder.config.max_export_timeout, + Duration::from_millis(2046) + ); + }); + + env_vars.push((OTEL_BLRP_MAX_QUEUE_SIZE, Some("120"))); + + temp_env::with_vars(env_vars, || { + let builder = + BatchLogProcessor::builder(InMemoryLogExporter::default(), runtime::Tokio); + assert_eq!(builder.config.max_export_batch_size, 120); + assert_eq!(builder.config.max_queue_size, 120); + }); + } + + #[test] + fn test_build_batch_log_processor_builder_rt_with_custom_config() { + let expected = BatchConfigBuilder::default() + .with_max_export_batch_size(1) + .with_scheduled_delay(Duration::from_millis(2)) + .with_max_export_timeout(Duration::from_millis(3)) + .with_max_queue_size(4) + .build(); + + let builder = BatchLogProcessor::builder(InMemoryLogExporter::default(), runtime::Tokio) + .with_batch_config(expected); + + let actual = &builder.config; + assert_eq!(actual.max_export_batch_size, 1); + assert_eq!(actual.scheduled_delay, Duration::from_millis(2)); + assert_eq!(actual.max_export_timeout, Duration::from_millis(3)); + assert_eq!(actual.max_queue_size, 4); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_set_resource_batch_processor_rt() { + let exporter = MockLogExporter { + resource: Arc::new(Mutex::new(None)), + }; + let processor = + BatchLogProcessor::new(exporter.clone(), BatchConfig::default(), runtime::Tokio); + let provider = SdkLoggerProvider::builder() + .with_log_processor(processor) + .with_resource( + Resource::builder_empty() + .with_attributes(vec![ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v3"), + KeyValue::new("k3", "v3"), + KeyValue::new("k4", "v4"), + KeyValue::new("k5", "v5"), + ]) + .build(), + ) + .build(); + provider.force_flush().unwrap(); + assert_eq!(exporter.get_resource().unwrap().into_iter().count(), 5); + let _ = provider.shutdown(); + } + + #[tokio::test(flavor = "multi_thread")] + #[ignore = "See issue https://github.com/open-telemetry/opentelemetry-rust/issues/1968"] + async fn test_batch_shutdown_rt() { + // assert we will receive an error + // setup + let exporter = InMemoryLogExporterBuilder::default() + .keep_records_on_shutdown() + .build(); + let processor = + BatchLogProcessor::new(exporter.clone(), BatchConfig::default(), runtime::Tokio); + + let mut record = SdkLogRecord::new(); + let instrumentation = InstrumentationScope::default(); + + processor.emit(&mut record, &instrumentation); + processor.force_flush().unwrap(); + processor.shutdown().unwrap(); + // todo: expect to see errors here. How should we assert this? + processor.emit(&mut record, &instrumentation); + assert_eq!(1, exporter.get_emitted_logs().unwrap().len()) + } + + #[tokio::test(flavor = "current_thread")] + #[ignore = "See issue https://github.com/open-telemetry/opentelemetry-rust/issues/1968"] + async fn test_batch_log_processor_rt_shutdown_with_async_runtime_current_flavor_multi_thread() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = + BatchLogProcessor::new(exporter.clone(), BatchConfig::default(), runtime::Tokio); + + // + // deadlock happens in shutdown with tokio current_thread runtime + // + processor.shutdown().unwrap(); + } + + #[tokio::test(flavor = "current_thread")] + async fn test_batch_log_processor_rt_shutdown_with_async_runtime_current_flavor_current_thread() + { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = BatchLogProcessor::new( + exporter.clone(), + BatchConfig::default(), + runtime::TokioCurrentThread, + ); + + processor.shutdown().unwrap(); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_batch_log_processor_rt_shutdown_with_async_runtime_multi_flavor_multi_thread() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = + BatchLogProcessor::new(exporter.clone(), BatchConfig::default(), runtime::Tokio); + + processor.shutdown().unwrap(); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_batch_log_processor_rt_shutdown_with_async_runtime_multi_flavor_current_thread() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = BatchLogProcessor::new( + exporter.clone(), + BatchConfig::default(), + runtime::TokioCurrentThread, + ); + + processor.shutdown().unwrap(); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/logger.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/logger.rs new file mode 100644 index 00000000..7148d914 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/logger.rs @@ -0,0 +1,164 @@ +#[cfg(feature = "trace")] +use super::TraceContext; +use super::{SdkLogRecord, SdkLoggerProvider}; +#[cfg(feature = "trace")] +use opentelemetry::trace::TraceContextExt; +use opentelemetry::{Context, InstrumentationScope}; + +use opentelemetry::logs::Severity; +use opentelemetry::time::now; + +#[derive(Debug, Clone)] +/// The object for emitting [`LogRecord`]s. +/// +/// [`LogRecord`]: opentelemetry::logs::LogRecord +pub struct SdkLogger { + scope: InstrumentationScope, + provider: SdkLoggerProvider, + + // Bound is not strictly needed (no attributes), but the semconv is still + // `development` so the metric must be feature-gated; reuse the same + // `experimental_metrics_bound_instruments` flag as the other SDK + // self-observability metrics for consistency. + #[cfg(feature = "experimental_metrics_bound_instruments")] + log_created_counter: opentelemetry::metrics::BoundCounter, +} + +impl SdkLogger { + pub(crate) fn new(scope: InstrumentationScope, provider: SdkLoggerProvider) -> Self { + #[cfg(feature = "experimental_metrics_bound_instruments")] + let log_created_counter = opentelemetry::global::meter("otel.sdk") + .u64_counter("otel.sdk.log.created") + .with_description("The number of log records submitted to the SDK.") + .with_unit("{log_record}") + .build() + .bind(&[]); + SdkLogger { + scope, + provider, + #[cfg(feature = "experimental_metrics_bound_instruments")] + log_created_counter, + } + } +} + +impl opentelemetry::logs::Logger for SdkLogger { + type LogRecord = SdkLogRecord; + + fn create_log_record(&self) -> Self::LogRecord { + SdkLogRecord::new() + } + + /// Emit a `LogRecord`. + fn emit(&self, mut record: Self::LogRecord) { + // Records emitted while telemetry is suppressed are the SDK's own + // internal-operation logs (suppressed to prevent feedback loops). They + // are not application intake, so `otel.sdk.log.created` intentionally + // excludes them (counting them would create a phantom drop signal + // against downstream metrics). + if Context::is_current_telemetry_suppressed() { + return; + } + + // Count every record submitted to the SDK, before any processing, so + // this metric is the top of the delivery funnel: records dropped by + // downstream processing show up as a gap against downstream metrics. + #[cfg(feature = "experimental_metrics_bound_instruments")] + self.log_created_counter.add(1); + + let provider = &self.provider; + let processors = provider.log_processors(); + + //let mut log_record = record; + if record.trace_context.is_none() { + #[cfg(feature = "trace")] + Context::map_current(|cx| { + cx.has_active_span().then(|| { + record.trace_context = Some(TraceContext::from(cx.span().span_context())) + }) + }); + } + if record.observed_timestamp.is_none() { + record.observed_timestamp = Some(now()); + } + + for p in processors { + p.emit(&mut record, &self.scope); + } + } + + #[inline] + fn event_enabled(&self, level: Severity, target: &str, name: Option<&str>) -> bool { + if Context::is_current_telemetry_suppressed() { + return false; + } + // Returns false if there are no log processors. + // Returns true if at least one processor returns true. + self.provider + .log_processors() + .iter() + .any(|processor| processor.event_enabled(level, target, name)) + } +} + +#[cfg(all(test, feature = "experimental_metrics_bound_instruments"))] +mod self_obs { + use crate::logs::SdkLoggerProvider; + use crate::metrics::data::{AggregatedMetrics, MetricData}; + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + use opentelemetry::logs::{Logger, LoggerProvider}; + + fn sum_log_created(exporter: &InMemoryMetricExporter) -> u64 { + let metrics = exporter.get_finished_metrics().unwrap(); + let mut total = 0u64; + for rm in &metrics { + for sm in &rm.scope_metrics { + for metric in &sm.metrics { + if metric.name == "otel.sdk.log.created" { + if let AggregatedMetrics::U64(MetricData::Sum(sum)) = &metric.data { + for dp in sum.data_points() { + total += dp.value(); + } + } + } + } + } + } + total + } + + /// Verifies `otel.sdk.log.created` counts every record submitted to the SDK + /// even when no processors are registered, proving the metric is a + /// pre-processing intake count (the top of the delivery funnel) rather than + /// a post-filter count. + /// + /// `#[ignore]`d because it calls `global::set_meter_provider()`, which + /// mutates process-wide state; CI runs it in isolation via `test.sh`. + #[test] + #[ignore] + fn log_created_counts_intake_without_processors() { + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + // Provider with NO log processors registered. + let logger_provider = SdkLoggerProvider::builder().build(); + let logger = logger_provider.logger("test"); + + for _ in 0..10 { + logger.emit(logger.create_log_record()); + } + + meter_provider.force_flush().unwrap(); + + assert_eq!( + sum_log_created(&metric_exporter), + 10, + "expected 10 records counted at intake regardless of processors" + ); + + meter_provider.shutdown().unwrap(); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/logger_provider.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/logger_provider.rs new file mode 100644 index 00000000..e8765b45 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/logger_provider.rs @@ -0,0 +1,1010 @@ +use super::{BatchLogProcessor, LogProcessor, SdkLogger, SimpleLogProcessor}; +use crate::error::{OTelSdkError, OTelSdkResult}; +use crate::logs::LogExporter; +use crate::Resource; +use opentelemetry::{otel_debug, otel_info, InstrumentationScope}; +use std::time::Duration; +use std::{ + borrow::Cow, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, OnceLock, + }, +}; + +// a no nop logger provider used as placeholder when the provider is shutdown +// TODO - replace it with LazyLock once it is stable +static NOOP_LOGGER_PROVIDER: OnceLock = OnceLock::new(); + +#[inline] +fn noop_logger_provider() -> &'static SdkLoggerProvider { + NOOP_LOGGER_PROVIDER.get_or_init(|| SdkLoggerProvider { + inner: Arc::new(LoggerProviderInner { + processors: Vec::new(), + is_shutdown: AtomicBool::new(true), + }), + }) +} + +#[derive(Debug, Clone)] +/// Handles the creation and coordination of [`Logger`]s. +/// +/// All `Logger`s created by a `SdkLoggerProvider` will share the same +/// [`Resource`] and have their created log records processed by the +/// configured log processors. This is a clonable handle to the `SdkLoggerProvider` +/// itself, and cloning it will create a new reference, not a new instance of a +/// `SdkLoggerProvider`. Dropping the last reference will trigger the shutdown of +/// the provider, ensuring that all remaining logs are flushed and no further +/// logs are processed. Shutdown can also be triggered manually by calling +/// the [`shutdown`](SdkLoggerProvider::shutdown) method. +/// +/// [`Logger`]: opentelemetry::logs::Logger +/// [`Resource`]: crate::Resource +pub struct SdkLoggerProvider { + inner: Arc, +} + +impl opentelemetry::logs::LoggerProvider for SdkLoggerProvider { + type Logger = SdkLogger; + + fn logger(&self, name: impl Into>) -> Self::Logger { + let scope = InstrumentationScope::builder(name).build(); + self.logger_with_scope(scope) + } + + fn logger_with_scope(&self, scope: InstrumentationScope) -> Self::Logger { + // If the provider is shutdown, new logger will refer a no-op logger provider. + if self.inner.is_shutdown.load(Ordering::Relaxed) { + otel_debug!( + name: "LoggerProvider.NoOpLoggerReturned", + logger_name = scope.name(), + ); + return SdkLogger::new(scope, noop_logger_provider().clone()); + } + if scope.name().is_empty() { + otel_info!(name: "LoggerNameEmpty", message = "Logger name is empty; consider providing a meaningful name. Logger will function normally and the provided name will be used as-is."); + }; + otel_debug!( + name: "LoggerProvider.NewLoggerReturned", + logger_name = scope.name(), + ); + SdkLogger::new(scope, self.clone()) + } +} + +impl SdkLoggerProvider { + /// Create a new `LoggerProvider` builder. + pub fn builder() -> LoggerProviderBuilder { + LoggerProviderBuilder::default() + } + + pub(crate) fn log_processors(&self) -> &[Box] { + &self.inner.processors + } + + /// Force flush all remaining logs in log processors and return results. + pub fn force_flush(&self) -> OTelSdkResult { + let result: Vec<_> = self + .log_processors() + .iter() + .map(|processor| processor.force_flush()) + .collect(); + if result.iter().all(|r| r.is_ok()) { + Ok(()) + } else { + Err(OTelSdkError::InternalFailure(format!("errs: {result:?}"))) + } + } + + /// Shuts down this `LoggerProvider` + pub fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + otel_debug!( + name: "LoggerProvider.ShutdownInvokedByUser", + ); + if self + .inner + .is_shutdown + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + { + // propagate the shutdown signal to processors + let result = self.inner.shutdown_with_timeout(timeout); + if result.iter().all(|res| res.is_ok()) { + Ok(()) + } else { + Err(OTelSdkError::InternalFailure(format!( + "Shutdown errors: {:?}", + result + .into_iter() + .filter_map(Result::err) + .collect::>() + ))) + } + } else { + Err(OTelSdkError::AlreadyShutdown) + } + } + + /// Shuts down this `LoggerProvider` with default timeout + pub fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } +} + +#[derive(Debug)] +struct LoggerProviderInner { + processors: Vec>, + is_shutdown: AtomicBool, +} + +impl LoggerProviderInner { + /// Shuts down the `LoggerProviderInner` and returns any errors. + pub(crate) fn shutdown_with_timeout(&self, timeout: Duration) -> Vec { + let mut results = vec![]; + for processor in &self.processors { + let result = processor.shutdown_with_timeout(timeout); + if let Err(err) = &result { + // Log at debug level because: + // - The error is also returned to the user for handling (if applicable) + // - Or the error occurs during `TracerProviderInner::Drop` as part of telemetry shutdown, + // which is non-actionable by the user + otel_debug!(name: "LoggerProvider.ShutdownError", + error = format!("{err}")); + } + results.push(result); + } + results + } + + /// Shuts down the `LoggerProviderInner` with default timeout and returns any errors. + pub(crate) fn shutdown(&self) -> Vec { + self.shutdown_with_timeout(Duration::from_secs(5)) + } +} + +impl Drop for LoggerProviderInner { + fn drop(&mut self) { + if !self.is_shutdown.load(Ordering::Relaxed) { + otel_info!( + name: "LoggerProvider.Drop", + message = "Last reference of LoggerProvider dropped, initiating shutdown." + ); + let _ = self.shutdown(); // errors are handled within shutdown + } else { + otel_debug!( + name: "LoggerProvider.Drop.AlreadyShutdown", + message = "LoggerProvider was already shut down; drop will not attempt shutdown again." + ); + } + } +} + +#[derive(Debug, Default)] +/// Builder for provider attributes. +pub struct LoggerProviderBuilder { + processors: Vec>, + resource: Option, +} + +impl LoggerProviderBuilder { + /// Adds a [SimpleLogProcessor] with the configured exporter to the pipeline. + /// + /// # Arguments + /// + /// * `exporter` - The exporter to be used by the SimpleLogProcessor. + /// + /// # Returns + /// + /// A new `Builder` instance with the SimpleLogProcessor added to the pipeline. + /// + /// Processors are invoked in the order they are added. + pub fn with_simple_exporter(self, exporter: T) -> Self { + let mut processors = self.processors; + processors.push(Box::new(SimpleLogProcessor::new(exporter))); + + LoggerProviderBuilder { processors, ..self } + } + + /// Adds a [BatchLogProcessor] with the configured exporter to the pipeline, + /// using the default [super::BatchConfig]. + /// + /// The following environment variables can be used to configure the batching configuration: + /// + /// * `OTEL_BLRP_SCHEDULE_DELAY` - Corresponds to `with_scheduled_delay`. + /// * `OTEL_BLRP_MAX_QUEUE_SIZE` - Corresponds to `with_max_queue_size`. + /// * `OTEL_BLRP_MAX_EXPORT_BATCH_SIZE` - Corresponds to `with_max_export_batch_size`. + /// + /// # Arguments + /// + /// * `exporter` - The exporter to be used by the `BatchLogProcessor`. + /// + /// # Returns + /// + /// A new `LoggerProviderBuilder` instance with the `BatchLogProcessor` added to the pipeline. + /// + /// Processors are invoked in the order they are added. + pub fn with_batch_exporter(self, exporter: T) -> Self { + let batch = BatchLogProcessor::builder(exporter).build(); + self.with_log_processor(batch) + } + + /// Adds a custom [LogProcessor] to the pipeline. + /// + /// # Arguments + /// + /// * `processor` - The `LogProcessor` to be added. + /// + /// # Returns + /// + /// A new `Builder` instance with the custom `LogProcessor` added to the pipeline. + /// + /// Processors are invoked in the order they are added. + pub fn with_log_processor(self, processor: T) -> Self { + let mut processors = self.processors; + processors.push(Box::new(processor)); + + LoggerProviderBuilder { processors, ..self } + } + + /// Associates a [Resource] with a [SdkLoggerProvider]. + /// + /// This [Resource] represents the entity producing telemetry and is associated + /// with all `Logger`s the [SdkLoggerProvider] will create. + /// + /// By default, if this option is not used, the default [Resource] will be used. + /// + /// When constructing a [Resource], use [`Resource::builder()`] to preserve + /// SDK-provided defaults such as `telemetry.sdk.*` and `service.name`. + /// Using [`Resource::builder_empty()`] will **not** include these attributes. + /// + /// # Example + /// + /// ``` + /// use opentelemetry_sdk::{Resource, logs::SdkLoggerProvider}; + /// use opentelemetry::KeyValue; + /// + /// let provider = SdkLoggerProvider::builder() + /// .with_resource( + /// Resource::builder() + /// .with_service_name("my-service") + /// .with_attributes([KeyValue::new("deployment.environment.name", "production")]) + /// .build(), + /// ) + /// .build(); + /// ``` + /// + /// *Note*: Calls to this method are additive, each call merges the provided + /// resource with the previous one. + /// + /// [`Resource::builder()`]: Resource::builder + /// [`Resource::builder_empty()`]: Resource::builder_empty + pub fn with_resource(self, resource: Resource) -> Self { + let resource = match self.resource { + Some(existing) => Some(existing.merge(&resource)), + None => Some(resource), + }; + + LoggerProviderBuilder { resource, ..self } + } + + /// Create a new provider from this configuration. + pub fn build(self) -> SdkLoggerProvider { + let resource = self.resource.unwrap_or(Resource::builder().build()); + let mut processors = self.processors; + for processor in &mut processors { + processor.set_resource(&resource); + } + + let logger_provider = SdkLoggerProvider { + inner: Arc::new(LoggerProviderInner { + processors, + is_shutdown: AtomicBool::new(false), + }), + }; + + otel_debug!( + name: "LoggerProvider.Built", + ); + logger_provider + } +} + +#[cfg(test)] +mod tests { + #[cfg(feature = "trace")] + use crate::logs::TraceContext; + #[cfg(feature = "trace")] + use crate::trace::SdkTracerProvider; + use crate::{ + logs::{InMemoryLogExporter, LogBatch, SdkLogRecord}, + resource::{ + SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, TELEMETRY_SDK_VERSION, + }, + Resource, + }; + + use super::*; + use opentelemetry::logs::{AnyValue, LogRecord as _, Logger, LoggerProvider}; + #[cfg(feature = "trace")] + use opentelemetry::trace::TraceContextExt; + #[cfg(feature = "trace")] + use opentelemetry::trace::{SpanId, TraceId, Tracer as _, TracerProvider}; + use opentelemetry::{Key, KeyValue, Value}; + use std::fmt::{Debug, Formatter}; + use std::sync::atomic::AtomicU64; + use std::sync::Mutex; + use std::{thread, time}; + + struct ShutdownTestLogProcessor { + is_shutdown: Arc>, + counter: Arc, + } + + impl Debug for ShutdownTestLogProcessor { + fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result { + todo!() + } + } + + impl ShutdownTestLogProcessor { + pub(crate) fn new(counter: Arc) -> Self { + ShutdownTestLogProcessor { + is_shutdown: Arc::new(Mutex::new(false)), + counter, + } + } + } + + impl LogProcessor for ShutdownTestLogProcessor { + fn emit(&self, _data: &mut SdkLogRecord, _scope: &InstrumentationScope) { + self.is_shutdown + .lock() + .map(|is_shutdown| { + if !*is_shutdown { + self.counter + .fetch_add(1, std::sync::atomic::Ordering::SeqCst); + } + }) + .expect("lock poisoned"); + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + self.is_shutdown + .lock() + .map(|mut is_shutdown| *is_shutdown = true) + .expect("lock poisoned"); + Ok(()) + } + } + + #[derive(Debug, Clone)] + struct TestExporterForResource { + resource: Arc>, + } + impl TestExporterForResource { + fn new() -> Self { + TestExporterForResource { + resource: Arc::new(Mutex::new(Resource::empty())), + } + } + + fn resource(&self) -> Resource { + self.resource.lock().unwrap().clone() + } + } + impl LogExporter for TestExporterForResource { + async fn export(&self, _: LogBatch<'_>) -> OTelSdkResult { + Ok(()) + } + + fn set_resource(&mut self, resource: &Resource) { + let mut res = self.resource.lock().unwrap(); + *res = resource.clone(); + } + + fn shutdown_with_timeout(&self, _timeout: time::Duration) -> OTelSdkResult { + Ok(()) + } + } + + #[derive(Debug, Clone)] + struct TestProcessorForResource { + resource: Arc>, + exporter: TestExporterForResource, + } + impl LogProcessor for TestProcessorForResource { + fn emit(&self, _data: &mut SdkLogRecord, _scope: &InstrumentationScope) { + // nothing to do. + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn set_resource(&mut self, resource: &Resource) { + let mut res = self.resource.lock().unwrap(); + *res = resource.clone(); + self.exporter.set_resource(resource); + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + Ok(()) + } + } + impl TestProcessorForResource { + fn new(exporter: TestExporterForResource) -> Self { + TestProcessorForResource { + resource: Arc::new(Mutex::new(Resource::empty())), + exporter, + } + } + fn resource(&self) -> Resource { + self.resource.lock().unwrap().clone() + } + } + + #[test] + fn test_resource_handling_provider_processor_exporter() { + let assert_resource = |processor: &TestProcessorForResource, + exporter: &TestExporterForResource, + resource_key: &'static str, + expect: Option<&'static str>| { + assert_eq!( + processor + .resource() + .get(&Key::from_static_str(resource_key)) + .map(|v| v.to_string()), + expect.map(|s| s.to_string()) + ); + + assert_eq!( + exporter + .resource() + .get(&Key::from_static_str(resource_key)) + .map(|v| v.to_string()), + expect.map(|s| s.to_string()) + ); + }; + let assert_telemetry_resource = + |processor: &TestProcessorForResource, exporter: &TestExporterForResource| { + assert_eq!( + processor.resource().get(&TELEMETRY_SDK_LANGUAGE.into()), + Some(Value::from("rust")) + ); + assert_eq!( + processor.resource().get(&TELEMETRY_SDK_NAME.into()), + Some(Value::from("opentelemetry")) + ); + assert_eq!( + processor.resource().get(&TELEMETRY_SDK_VERSION.into()), + Some(Value::from(env!("CARGO_PKG_VERSION"))) + ); + assert_eq!( + exporter.resource().get(&TELEMETRY_SDK_LANGUAGE.into()), + Some(Value::from("rust")) + ); + assert_eq!( + exporter.resource().get(&TELEMETRY_SDK_NAME.into()), + Some(Value::from("opentelemetry")) + ); + assert_eq!( + exporter.resource().get(&TELEMETRY_SDK_VERSION.into()), + Some(Value::from(env!("CARGO_PKG_VERSION"))) + ); + }; + + // If users didn't provide a resource and there isn't a env var set. Use default one. + temp_env::with_var_unset("OTEL_RESOURCE_ATTRIBUTES", || { + let exporter_with_resource = TestExporterForResource::new(); + let processor_with_resource = + TestProcessorForResource::new(exporter_with_resource.clone()); + let _ = super::SdkLoggerProvider::builder() + .with_log_processor(processor_with_resource.clone()) + .build(); + let service_name = processor_with_resource + .resource() + .get(&Key::from_static_str(SERVICE_NAME)) + .map(|v| v.to_string()) + .unwrap(); + assert!( + service_name.starts_with("unknown_service:opentelemetry_sdk-"), + "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}", + service_name + ); + assert_telemetry_resource(&processor_with_resource, &exporter_with_resource); + }); + + // If user provided a resource, use that. + let exporter_with_resource = TestExporterForResource::new(); + let processor_with_resource = TestProcessorForResource::new(exporter_with_resource.clone()); + let _ = super::SdkLoggerProvider::builder() + .with_resource( + Resource::builder_empty() + .with_service_name("test_service") + .build(), + ) + .with_log_processor(processor_with_resource.clone()) + .build(); + assert_resource( + &processor_with_resource, + &exporter_with_resource, + SERVICE_NAME, + Some("test_service"), + ); + assert_eq!(processor_with_resource.resource().len(), 1); + + // If `OTEL_RESOURCE_ATTRIBUTES` is set, read them automatically + temp_env::with_var( + "OTEL_RESOURCE_ATTRIBUTES", + Some("key1=value1, k2, k3=value2"), + || { + let exporter_with_resource = TestExporterForResource::new(); + let processor_with_resource = + TestProcessorForResource::new(exporter_with_resource.clone()); + let _ = super::SdkLoggerProvider::builder() + .with_log_processor(processor_with_resource.clone()) + .build(); + let service_name = processor_with_resource + .resource() + .get(&Key::from_static_str(SERVICE_NAME)) + .map(|v| v.to_string()) + .unwrap(); + assert!( + service_name.starts_with("unknown_service:opentelemetry_sdk-"), + "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}", + service_name + ); + assert_resource( + &processor_with_resource, + &exporter_with_resource, + "key1", + Some("value1"), + ); + assert_resource( + &processor_with_resource, + &exporter_with_resource, + "k3", + Some("value2"), + ); + assert_telemetry_resource(&processor_with_resource, &exporter_with_resource); + assert_eq!(processor_with_resource.resource().len(), 6); + }, + ); + + // When `OTEL_RESOURCE_ATTRIBUTES` is set and also user provided config + temp_env::with_var( + "OTEL_RESOURCE_ATTRIBUTES", + Some("my-custom-key=env-val,k2=value2"), + || { + let exporter_with_resource = TestExporterForResource::new(); + let processor_with_resource = + TestProcessorForResource::new(exporter_with_resource.clone()); + let _ = super::SdkLoggerProvider::builder() + .with_resource( + Resource::builder() + .with_attributes([ + KeyValue::new("my-custom-key", "my-custom-value"), + KeyValue::new("my-custom-key2", "my-custom-value2"), + ]) + .build(), + ) + .with_log_processor(processor_with_resource.clone()) + .build(); + let service_name = processor_with_resource + .resource() + .get(&Key::from_static_str(SERVICE_NAME)) + .map(|v| v.to_string()) + .unwrap(); + assert!( + service_name.starts_with("unknown_service:opentelemetry_sdk-"), + "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}", + service_name + ); + assert_resource( + &processor_with_resource, + &exporter_with_resource, + "my-custom-key", + Some("my-custom-value"), + ); + assert_resource( + &processor_with_resource, + &exporter_with_resource, + "my-custom-key2", + Some("my-custom-value2"), + ); + assert_resource( + &processor_with_resource, + &exporter_with_resource, + "k2", + Some("value2"), + ); + assert_telemetry_resource(&processor_with_resource, &exporter_with_resource); + assert_eq!(processor_with_resource.resource().len(), 7); + }, + ); + + // If user provided a resource, it takes priority during collision. + let exporter_with_resource = TestExporterForResource::new(); + let processor_with_resource = TestProcessorForResource::new(exporter_with_resource); + let _ = super::SdkLoggerProvider::builder() + .with_resource(Resource::empty()) + .with_log_processor(processor_with_resource.clone()) + .build(); + assert_eq!(processor_with_resource.resource().len(), 0); + } + + #[test] + #[cfg(feature = "trace")] + fn trace_context_test() { + let exporter = InMemoryLogExporter::default(); + + let logger_provider = SdkLoggerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + + let logger = logger_provider.logger("test-logger"); + + let tracer_provider = SdkTracerProvider::builder().build(); + + let tracer = tracer_provider.tracer("test-tracer"); + + tracer.in_span("test-span", |cx| { + let ambient_ctxt = cx.span().span_context().clone(); + let explicit_ctxt = TraceContext { + trace_id: TraceId::from(13), + span_id: SpanId::from(14), + trace_flags: None, + }; + + let mut ambient_ctxt_record = logger.create_log_record(); + ambient_ctxt_record.set_body(AnyValue::String("ambient".into())); + + let mut explicit_ctxt_record = logger.create_log_record(); + explicit_ctxt_record.set_body(AnyValue::String("explicit".into())); + explicit_ctxt_record.set_trace_context( + explicit_ctxt.trace_id, + explicit_ctxt.span_id, + explicit_ctxt.trace_flags, + ); + + logger.emit(ambient_ctxt_record); + logger.emit(explicit_ctxt_record); + + let emitted = exporter.get_emitted_logs().unwrap(); + + assert_eq!( + Some(AnyValue::String("ambient".into())), + emitted[0].record.body + ); + assert_eq!( + ambient_ctxt.trace_id(), + emitted[0].record.trace_context.as_ref().unwrap().trace_id + ); + assert_eq!( + ambient_ctxt.span_id(), + emitted[0].record.trace_context.as_ref().unwrap().span_id + ); + + assert_eq!( + Some(AnyValue::String("explicit".into())), + emitted[1].record.body + ); + assert_eq!( + explicit_ctxt.trace_id, + emitted[1].record.trace_context.as_ref().unwrap().trace_id + ); + assert_eq!( + explicit_ctxt.span_id, + emitted[1].record.trace_context.as_ref().unwrap().span_id + ); + }); + } + + #[test] + fn shutdown_test() { + let counter = Arc::new(AtomicU64::new(0)); + let logger_provider = SdkLoggerProvider::builder() + .with_log_processor(ShutdownTestLogProcessor::new(counter.clone())) + .build(); + + let logger1 = logger_provider.logger("test-logger1"); + let logger2 = logger_provider.logger("test-logger2"); + logger1.emit(logger1.create_log_record()); + logger2.emit(logger1.create_log_record()); + + let logger3 = logger_provider.logger("test-logger3"); + let handle = thread::spawn(move || { + logger3.emit(logger3.create_log_record()); + }); + handle.join().expect("thread panicked"); + + let _ = logger_provider.shutdown(); + logger1.emit(logger1.create_log_record()); + + assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 3); + } + + #[test] + fn shutdown_idempotent_test() { + let counter = Arc::new(AtomicU64::new(0)); + let logger_provider = SdkLoggerProvider::builder() + .with_log_processor(ShutdownTestLogProcessor::new(counter.clone())) + .build(); + + let shutdown_res = logger_provider.shutdown(); + assert!(shutdown_res.is_ok()); + + // Subsequent shutdowns should return an error. + let shutdown_res = logger_provider.shutdown(); + assert!(shutdown_res.is_err()); + + // Subsequent shutdowns should return an error. + let shutdown_res = logger_provider.shutdown(); + assert!(shutdown_res.is_err()); + } + + #[test] + fn global_shutdown_test() { + // cargo test global_shutdown_test --features=testing + + // Arrange + let shutdown_called = Arc::new(Mutex::new(false)); + let flush_called = Arc::new(Mutex::new(false)); + let logger_provider = SdkLoggerProvider::builder() + .with_log_processor(LazyLogProcessor::new( + shutdown_called.clone(), + flush_called.clone(), + )) + .build(); + //set_logger_provider(logger_provider); + let logger1 = logger_provider.logger("test-logger1"); + let logger2 = logger_provider.logger("test-logger2"); + + // Acts + logger1.emit(logger1.create_log_record()); + logger2.emit(logger1.create_log_record()); + + // explicitly calling shutdown on logger_provider. This will + // indeed do the shutdown, even if there are loggers still alive. + let _ = logger_provider.shutdown(); + + // Assert + + // shutdown is called. + assert!(*shutdown_called.lock().unwrap()); + + // flush is never called by the sdk. + assert!(!*flush_called.lock().unwrap()); + } + + #[test] + fn drop_test_with_multiple_providers() { + let shutdown_called = Arc::new(Mutex::new(false)); + let flush_called = Arc::new(Mutex::new(false)); + { + // Create a shared LoggerProviderInner and use it across multiple providers + let shared_inner = Arc::new(LoggerProviderInner { + processors: vec![Box::new(LazyLogProcessor::new( + shutdown_called.clone(), + flush_called.clone(), + ))], + is_shutdown: AtomicBool::new(false), + }); + + { + let logger_provider1 = SdkLoggerProvider { + inner: shared_inner.clone(), + }; + let logger_provider2 = SdkLoggerProvider { + inner: shared_inner.clone(), + }; + + let logger1 = logger_provider1.logger("test-logger1"); + let logger2 = logger_provider2.logger("test-logger2"); + + logger1.emit(logger1.create_log_record()); + logger2.emit(logger1.create_log_record()); + + // LoggerProviderInner should not be dropped yet, since both providers and `shared_inner` + // are still holding a reference. + } + // At this point, both `logger_provider1` and `logger_provider2` are dropped, + // but `shared_inner` still holds a reference, so `LoggerProviderInner` is NOT dropped yet. + } + // Verify shutdown was called during the drop of the shared LoggerProviderInner + assert!(*shutdown_called.lock().unwrap()); + // Verify flush was not called during drop + assert!(!*flush_called.lock().unwrap()); + } + + #[test] + fn drop_after_shutdown_test_with_multiple_providers() { + let shutdown_called = Arc::new(Mutex::new(0)); // Count the number of times shutdown is called + let flush_called = Arc::new(Mutex::new(false)); + + // Create a shared LoggerProviderInner and use it across multiple providers + let shared_inner = Arc::new(LoggerProviderInner { + processors: vec![Box::new(CountingShutdownProcessor::new( + shutdown_called.clone(), + flush_called.clone(), + ))], + is_shutdown: AtomicBool::new(false), + }); + + // Create a scope to test behavior when providers are dropped + { + let logger_provider1 = SdkLoggerProvider { + inner: shared_inner.clone(), + }; + let logger_provider2 = SdkLoggerProvider { + inner: shared_inner.clone(), + }; + + // Explicitly shut down the logger provider + let shutdown_result = logger_provider1.shutdown(); + println!("---->Result: {shutdown_result:?}"); + assert!(shutdown_result.is_ok()); + + // Verify that shutdown was called exactly once + assert_eq!(*shutdown_called.lock().unwrap(), 1); + + // LoggerProvider2 should observe the shutdown state but not trigger another shutdown + let shutdown_result2 = logger_provider2.shutdown(); + assert!(shutdown_result2.is_err()); + + // Both logger providers will be dropped at the end of this scope + } + + // Verify that shutdown was only called once, even after drop + assert_eq!(*shutdown_called.lock().unwrap(), 1); + } + + #[test] + fn test_empty_logger_name() { + let exporter = InMemoryLogExporter::default(); + let logger_provider = SdkLoggerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + let logger = logger_provider.logger(""); + let mut record = logger.create_log_record(); + record.set_body("Testing empty logger name".into()); + logger.emit(record); + + // Create a logger using a scope with an empty name + let scope = InstrumentationScope::builder("").build(); + let scoped_logger = logger_provider.logger_with_scope(scope); + let mut scoped_record = scoped_logger.create_log_record(); + scoped_record.set_body("Testing empty logger scope name".into()); + scoped_logger.emit(scoped_record); + + // Assert: Verify that the emitted logs are processed correctly + let mut emitted_logs = exporter.get_emitted_logs().unwrap(); + assert_eq!(emitted_logs.len(), 2); + let log1 = emitted_logs.remove(0); + // Assert the first log + assert_eq!( + log1.record.body, + Some(AnyValue::String("Testing empty logger name".into())) + ); + assert_eq!(log1.instrumentation.name(), ""); + + // Assert the second log created through the scope + let log2 = emitted_logs.remove(0); + assert_eq!( + log2.record.body, + Some(AnyValue::String("Testing empty logger scope name".into())) + ); + assert_eq!(log1.instrumentation.name(), ""); + } + + #[test] + fn with_resource_multiple_calls_ensure_additive() { + let builder = SdkLoggerProvider::builder() + .with_resource( + Resource::builder_empty() + .with_attributes(vec![KeyValue::new("key1", "value1")]) + .build(), + ) + .with_resource( + Resource::builder_empty() + .with_attributes(vec![KeyValue::new("key2", "value2")]) + .build(), + ) + .with_resource( + Resource::builder_empty() + .with_schema_url(vec![], "http://example.com") + .build(), + ) + .with_resource( + Resource::builder_empty() + .with_attributes(vec![KeyValue::new("key3", "value3")]) + .build(), + ); + + let resource = builder.resource.unwrap(); + + assert_eq!( + resource.get(&Key::from_static_str("key1")), + Some(Value::from("value1")) + ); + assert_eq!( + resource.get(&Key::from_static_str("key2")), + Some(Value::from("value2")) + ); + assert_eq!( + resource.get(&Key::from_static_str("key3")), + Some(Value::from("value3")) + ); + assert_eq!(resource.schema_url(), Some("http://example.com")); + } + + #[derive(Debug)] + pub(crate) struct LazyLogProcessor { + shutdown_called: Arc>, + flush_called: Arc>, + } + + impl LazyLogProcessor { + pub(crate) fn new( + shutdown_called: Arc>, + flush_called: Arc>, + ) -> Self { + LazyLogProcessor { + shutdown_called, + flush_called, + } + } + } + + impl LogProcessor for LazyLogProcessor { + fn emit(&self, _data: &mut SdkLogRecord, _scope: &InstrumentationScope) { + // nothing to do. + } + + fn force_flush(&self) -> OTelSdkResult { + *self.flush_called.lock().unwrap() = true; + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + *self.shutdown_called.lock().unwrap() = true; + Ok(()) + } + } + + #[derive(Debug)] + struct CountingShutdownProcessor { + shutdown_count: Arc>, + flush_called: Arc>, + } + + impl CountingShutdownProcessor { + fn new(shutdown_count: Arc>, flush_called: Arc>) -> Self { + CountingShutdownProcessor { + shutdown_count, + flush_called, + } + } + } + + impl LogProcessor for CountingShutdownProcessor { + fn emit(&self, _data: &mut SdkLogRecord, _scope: &InstrumentationScope) { + // nothing to do + } + + fn force_flush(&self) -> OTelSdkResult { + *self.flush_called.lock().unwrap() = true; + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + let mut count = self.shutdown_count.lock().unwrap(); + *count += 1; + Ok(()) + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/mod.rs new file mode 100644 index 00000000..b977f521 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/mod.rs @@ -0,0 +1,297 @@ +//! # OpenTelemetry Log SDK +mod batch_log_processor; +mod export; +mod log_processor; +mod logger; +mod logger_provider; +pub(crate) mod record; +mod simple_log_processor; + +/// In-Memory log exporter for testing purpose. +#[cfg(any(feature = "testing", test))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "testing", test))))] +pub mod in_memory_exporter; +#[cfg(any(feature = "testing", test))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "testing", test))))] +pub use in_memory_exporter::{InMemoryLogExporter, InMemoryLogExporterBuilder}; + +pub use batch_log_processor::{ + BatchConfig, BatchConfigBuilder, BatchLogProcessor, BatchLogProcessorBuilder, + OTEL_BLRP_EXPORT_TIMEOUT, OTEL_BLRP_EXPORT_TIMEOUT_DEFAULT, OTEL_BLRP_MAX_EXPORT_BATCH_SIZE, + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE_DEFAULT, OTEL_BLRP_MAX_QUEUE_SIZE, + OTEL_BLRP_MAX_QUEUE_SIZE_DEFAULT, OTEL_BLRP_SCHEDULE_DELAY, OTEL_BLRP_SCHEDULE_DELAY_DEFAULT, +}; +pub use export::{LogBatch, LogExporter}; +pub use log_processor::LogProcessor; +pub use logger::SdkLogger; +pub use logger_provider::{LoggerProviderBuilder, SdkLoggerProvider}; +pub use record::{SdkLogRecord, TraceContext}; +pub use simple_log_processor::SimpleLogProcessor; + +#[cfg(feature = "experimental_logs_batch_log_processor_with_async_runtime")] +/// Module for BatchLogProcessor with async runtime. +pub mod log_processor_with_async_runtime; + +#[cfg(all(test, feature = "testing"))] +mod tests { + use super::*; + use crate::error::OTelSdkResult; + use crate::Resource; + use opentelemetry::baggage::BaggageExt; + use opentelemetry::logs::LogRecord; + use opentelemetry::logs::{Logger, LoggerProvider, Severity}; + use opentelemetry::{logs::AnyValue, Key, KeyValue}; + use opentelemetry::{Context, InstrumentationScope}; + use std::borrow::Borrow; + use std::collections::HashMap; + use std::sync::{Arc, Mutex}; + + #[test] + fn logging_sdk_test() { + // Arrange + let resource = Resource::builder_empty() + .with_attributes([ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v2"), + KeyValue::new("k3", "v3"), + KeyValue::new("k4", "v4"), + ]) + .build(); + let exporter: InMemoryLogExporter = InMemoryLogExporter::default(); + let logger_provider = SdkLoggerProvider::builder() + .with_resource(resource.clone()) + .with_log_processor(SimpleLogProcessor::new(exporter.clone())) + .build(); + + // Act + let logger = logger_provider.logger("test-logger"); + let mut log_record = logger.create_log_record(); + log_record.set_severity_number(Severity::Error); + log_record.set_severity_text("Error"); + + // Adding attributes using a vector with explicitly constructed Key and AnyValue objects. + log_record.add_attributes(vec![ + (Key::new("key1"), AnyValue::from("value1")), + (Key::new("key2"), AnyValue::from("value2")), + ]); + + // Adding attributes using an array with explicitly constructed Key and AnyValue objects. + log_record.add_attributes([ + (Key::new("key3"), AnyValue::from("value3")), + (Key::new("key4"), AnyValue::from("value4")), + ]); + + // Adding attributes using a vector with tuple auto-conversion to Key and AnyValue. + log_record.add_attributes(vec![("key5", "value5"), ("key6", "value6")]); + + // Adding attributes using an array with tuple auto-conversion to Key and AnyValue. + log_record.add_attributes([("key7", "value7"), ("key8", "value8")]); + + // Adding Attributes from a HashMap + let mut attributes_map = HashMap::new(); + attributes_map.insert("key9", "value9"); + attributes_map.insert("key10", "value10"); + + log_record.add_attributes(attributes_map); + + logger.emit(log_record); + + // Assert + let exported_logs = exporter + .get_emitted_logs() + .expect("Logs are expected to be exported."); + assert_eq!(exported_logs.len(), 1); + let log = exported_logs + .first() + .expect("Atleast one log is expected to be present."); + assert_eq!(log.instrumentation.name(), "test-logger"); + assert_eq!(log.record.severity_number, Some(Severity::Error)); + assert_eq!(log.record.attributes_len(), 10); + for i in 1..=10 { + assert!(log.record.attributes_contains( + &Key::new(format!("key{i}")), + &AnyValue::String(format!("value{i}").into()) + )); + } + + // validate Resource + assert_eq!(&resource, log.resource.borrow()); + } + + #[test] + fn logger_attributes() { + let exporter: InMemoryLogExporter = InMemoryLogExporter::default(); + let provider = SdkLoggerProvider::builder() + .with_log_processor(SimpleLogProcessor::new(exporter.clone())) + .build(); + + let scope = InstrumentationScope::builder("test_logger") + .with_schema_url("https://opentelemetry.io/schemas/1.0.0") + .with_attributes(vec![(KeyValue::new("test_k", "test_v"))]) + .build(); + + let logger = provider.logger_with_scope(scope); + + let mut log_record = logger.create_log_record(); + log_record.set_severity_number(Severity::Error); + + logger.emit(log_record); + + let mut exported_logs = exporter + .get_emitted_logs() + .expect("Logs are expected to be exported."); + assert_eq!(exported_logs.len(), 1); + let log = exported_logs.remove(0); + assert_eq!(log.record.severity_number, Some(Severity::Error)); + + let instrumentation_scope = log.instrumentation; + assert_eq!(instrumentation_scope.name(), "test_logger"); + assert_eq!( + instrumentation_scope.schema_url(), + Some("https://opentelemetry.io/schemas/1.0.0") + ); + assert!(instrumentation_scope + .attributes() + .eq(&[KeyValue::new("test_k", "test_v")])); + } + + #[derive(Debug)] + struct EnrichWithBaggageProcessor; + impl LogProcessor for EnrichWithBaggageProcessor { + fn emit(&self, data: &mut SdkLogRecord, _instrumentation: &InstrumentationScope) { + Context::map_current(|cx| { + for (kk, vv) in cx.baggage().iter() { + data.add_attribute(kk.clone(), vv.0.clone()); + } + }); + } + + fn force_flush(&self) -> crate::error::OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } + } + #[test] + fn log_and_baggage() { + // Arrange + let exporter: InMemoryLogExporter = InMemoryLogExporter::default(); + let logger_provider = SdkLoggerProvider::builder() + .with_log_processor(EnrichWithBaggageProcessor) + .with_log_processor(SimpleLogProcessor::new(exporter.clone())) + .build(); + + // Act + let logger = logger_provider.logger("test-logger"); + let context_with_baggage = + Context::current_with_baggage(vec![KeyValue::new("key-from-bag", "value-from-bag")]); + let _cx_guard = context_with_baggage.attach(); + let mut log_record = logger.create_log_record(); + log_record.add_attribute("key", "value"); + logger.emit(log_record); + + // Assert + let exported_logs = exporter + .get_emitted_logs() + .expect("Logs are expected to be exported."); + assert_eq!(exported_logs.len(), 1); + let log = exported_logs + .first() + .expect("Atleast one log is expected to be present."); + assert_eq!(log.instrumentation.name(), "test-logger"); + assert_eq!(log.record.attributes_len(), 2); + + // Assert that the log record contains the baggage attribute + // and the attribute added to the log record. + assert!(log + .record + .attributes_contains(&Key::new("key"), &AnyValue::String("value".into()))); + assert!(log.record.attributes_contains( + &Key::new("key-from-bag"), + &AnyValue::String("value-from-bag".into()) + )); + } + + #[test] + fn log_suppression() { + // Arrange + let exporter: InMemoryLogExporter = InMemoryLogExporter::default(); + let logger_provider = SdkLoggerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + + // Act + let logger = logger_provider.logger("test-logger"); + let log_record = logger.create_log_record(); + { + let _suppressed_context = Context::enter_telemetry_suppressed_scope(); + // This log emission should be suppressed and not exported. + logger.emit(log_record); + } + + // Assert + let exported_logs = exporter.get_emitted_logs().expect("this should not fail."); + assert_eq!( + exported_logs.len(), + 0, + "There should be a no logs as log emission is done inside a suppressed context" + ); + } + + #[derive(Debug, Clone)] + struct ReentrantLogProcessor { + logger: Arc>>, + } + + impl ReentrantLogProcessor { + fn new() -> Self { + Self { + logger: Arc::new(Mutex::new(None)), + } + } + + fn set_logger(&self, logger: SdkLogger) { + let mut guard = self.logger.lock().unwrap(); + *guard = Some(logger); + } + } + + impl LogProcessor for ReentrantLogProcessor { + fn emit(&self, _data: &mut SdkLogRecord, _instrumentation: &InstrumentationScope) { + let _suppress = Context::enter_telemetry_suppressed_scope(); + // Without the suppression above, the logger.emit(log_record) below will cause a deadlock, + // as it emits another log, which will attempt to acquire the same lock that is + // already held by itself! + let logger = self.logger.lock().unwrap(); + if let Some(logger) = logger.as_ref() { + let mut log_record = logger.create_log_record(); + log_record.set_severity_number(Severity::Error); + logger.emit(log_record); + } + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> OTelSdkResult { + Ok(()) + } + } + + #[test] + fn processor_internal_log_does_not_deadlock_with_suppression_enabled() { + let processor: ReentrantLogProcessor = ReentrantLogProcessor::new(); + let logger_provider = SdkLoggerProvider::builder() + .with_log_processor(processor.clone()) + .build(); + processor.set_logger(logger_provider.logger("processor-logger")); + + let logger = logger_provider.logger("test-logger"); + let mut log_record = logger.create_log_record(); + log_record.set_severity_number(Severity::Error); + logger.emit(log_record); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/record.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/record.rs new file mode 100644 index 00000000..380e6fb4 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/record.rs @@ -0,0 +1,380 @@ +use crate::growable_array::GrowableArray; +#[cfg(feature = "trace")] +use opentelemetry::trace::SpanContext; +use opentelemetry::{ + logs::{AnyValue, Severity}, + Key, SpanId, TraceFlags, TraceId, +}; +use std::{borrow::Cow, time::SystemTime}; + +// According to a Go-specific study mentioned on https://go.dev/blog/slog, +// up to 5 attributes is the most common case. +const PREALLOCATED_ATTRIBUTE_CAPACITY: usize = 5; + +/// Represents a collection of log record attributes with a predefined capacity. +/// +/// This type uses `GrowableArray` to store key-value pairs of log attributes, where each attribute is an `Option<(Key, AnyValue)>`. +/// The initial attributes are allocated in a fixed-size array of capacity `PREALLOCATED_ATTRIBUTE_CAPACITY`. +/// If more attributes are added beyond this capacity, additional storage is handled by dynamically growing a vector. +pub(crate) type LogRecordAttributes = + GrowableArray, PREALLOCATED_ATTRIBUTE_CAPACITY>; + +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +/// LogRecord represents all data carried by a log record, and +/// is provided to `LogExporter`s as input. +pub struct SdkLogRecord { + /// Event name. Optional as not all the logging API support it. + pub(crate) event_name: Option<&'static str>, + + /// Target of the log record + pub(crate) target: Option>, + + /// Record timestamp + pub(crate) timestamp: Option, + + /// Timestamp for when the record was observed by OpenTelemetry + pub(crate) observed_timestamp: Option, + + /// Trace context for logs associated with spans + pub(crate) trace_context: Option, + + /// The original severity string from the source + pub(crate) severity_text: Option<&'static str>, + + /// The corresponding severity value, normalized + pub(crate) severity_number: Option, + + /// Record body + pub(crate) body: Option, + + /// Additional attributes associated with this record + pub(crate) attributes: LogRecordAttributes, +} + +impl opentelemetry::logs::LogRecord for SdkLogRecord { + fn set_event_name(&mut self, name: &'static str) { + self.event_name = Some(name); + } + + // Sets the `target` of a record + fn set_target(&mut self, _target: T) + where + T: Into>, + { + self.target = Some(_target.into()); + } + + fn set_timestamp(&mut self, timestamp: SystemTime) { + self.timestamp = Some(timestamp); + } + + fn set_observed_timestamp(&mut self, timestamp: SystemTime) { + self.observed_timestamp = Some(timestamp); + } + + fn set_severity_text(&mut self, severity_text: &'static str) { + self.severity_text = Some(severity_text); + } + + fn set_severity_number(&mut self, severity_number: Severity) { + self.severity_number = Some(severity_number); + } + + fn set_body(&mut self, body: AnyValue) { + self.body = Some(body); + } + + fn add_attributes(&mut self, attributes: I) + where + I: IntoIterator, + K: Into, + V: Into, + { + for (key, value) in attributes.into_iter() { + self.add_attribute(key, value); + } + } + + fn add_attribute(&mut self, key: K, value: V) + where + K: Into, + V: Into, + { + self.attributes.push(Some((key.into(), value.into()))); + } + + fn set_trace_context( + &mut self, + trace_id: TraceId, + span_id: SpanId, + trace_flags: Option, + ) { + self.trace_context = Some(TraceContext { + trace_id, + span_id, + trace_flags, + }); + } +} + +impl SdkLogRecord { + /// Creates a new empty `SdkLogRecord`. + pub fn new() -> Self { + SdkLogRecord { + event_name: None, + target: None, + timestamp: None, + observed_timestamp: None, + trace_context: None, + severity_text: None, + severity_number: None, + body: None, + attributes: LogRecordAttributes::default(), + } + } + + /// Returns the event name + #[inline] + pub fn event_name(&self) -> Option<&'static str> { + self.event_name + } + + /// Returns the target + #[inline] + pub fn target(&self) -> Option<&Cow<'static, str>> { + self.target.as_ref() + } + + /// Returns the timestamp + #[inline] + pub fn timestamp(&self) -> Option { + self.timestamp + } + + /// Returns the observed timestamp + #[inline] + pub fn observed_timestamp(&self) -> Option { + self.observed_timestamp + } + + /// Returns the trace context + #[inline] + pub fn trace_context(&self) -> Option<&TraceContext> { + self.trace_context.as_ref() + } + + /// Returns the severity text + #[inline] + pub fn severity_text(&self) -> Option<&'static str> { + self.severity_text + } + + /// Returns the severity number + #[inline] + pub fn severity_number(&self) -> Option { + self.severity_number + } + + /// Returns the body + #[inline] + pub fn body(&self) -> Option<&AnyValue> { + self.body.as_ref() + } + + /// Provides an iterator over the attributes. + #[inline] + pub fn attributes_iter(&self) -> impl Iterator { + self.attributes.iter().filter_map(|opt| opt.as_ref()) + } + + #[allow(dead_code)] + /// Returns the number of attributes in the `LogRecord`. + pub(crate) fn attributes_len(&self) -> usize { + self.attributes.len() + } + + #[allow(dead_code)] + /// Checks if the `LogRecord` contains the specified attribute. + pub(crate) fn attributes_contains(&self, key: &Key, value: &AnyValue) -> bool { + self.attributes + .iter() + .flatten() + .any(|(k, v)| k == key && v == value) + } +} + +impl Default for SdkLogRecord { + fn default() -> Self { + Self::new() + } +} + +/// TraceContext stores the trace context for logs that have an associated +/// span. +#[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] +pub struct TraceContext { + /// Trace id + pub trace_id: TraceId, + /// Span Id + pub span_id: SpanId, + /// Trace flags + pub trace_flags: Option, +} + +#[cfg(feature = "trace")] +impl From<&SpanContext> for TraceContext { + fn from(span_context: &SpanContext) -> Self { + TraceContext { + trace_id: span_context.trace_id(), + span_id: span_context.span_id(), + trace_flags: Some(span_context.trace_flags()), + } + } +} + +#[cfg(all(test, feature = "testing"))] +mod tests { + use super::*; + use opentelemetry::logs::{AnyValue, LogRecord as _, Severity}; + use opentelemetry::time::now; + use std::borrow::Cow; + + #[test] + fn test_set_eventname() { + let mut log_record = SdkLogRecord::new(); + log_record.set_event_name("test_event"); + assert_eq!(log_record.event_name, Some("test_event")); + } + + #[test] + fn test_set_target() { + let mut log_record = SdkLogRecord::new(); + log_record.set_target("foo::bar"); + assert_eq!(log_record.target, Some(Cow::Borrowed("foo::bar"))); + } + + #[test] + fn test_set_timestamp() { + let mut log_record = SdkLogRecord::new(); + let now = now(); + log_record.set_timestamp(now); + assert_eq!(log_record.timestamp, Some(now)); + } + + #[test] + fn test_set_observed_timestamp() { + let mut log_record = SdkLogRecord::new(); + let now = now(); + log_record.set_observed_timestamp(now); + assert_eq!(log_record.observed_timestamp, Some(now)); + } + + #[test] + fn test_set_severity_text() { + let mut log_record = SdkLogRecord::new(); + log_record.set_severity_text("ERROR"); + assert_eq!(log_record.severity_text, Some("ERROR")); + } + + #[test] + fn test_set_severity_number() { + let mut log_record = SdkLogRecord::new(); + let severity_number = Severity::Error; + log_record.set_severity_number(severity_number); + assert_eq!(log_record.severity_number, Some(Severity::Error)); + } + + #[test] + fn test_set_body() { + let mut log_record = SdkLogRecord::new(); + let body = AnyValue::String("Test body".into()); + log_record.set_body(body.clone()); + assert_eq!(log_record.body, Some(body)); + } + + #[test] + fn test_set_attributes() { + let mut log_record = SdkLogRecord::new(); + let attributes = vec![(Key::new("key"), AnyValue::String("value".into()))]; + log_record.add_attributes(attributes.clone()); + for (key, value) in attributes { + assert!(log_record.attributes_contains(&key, &value)); + } + } + + #[test] + fn test_set_attribute() { + let mut log_record = SdkLogRecord::new(); + log_record.add_attribute("key", "value"); + let key = Key::new("key"); + let value = AnyValue::String("value".into()); + assert!(log_record.attributes_contains(&key, &value)); + } + + #[test] + fn compare_trace_context() { + let trace_context = TraceContext { + trace_id: TraceId::from(1), + span_id: SpanId::from(1), + trace_flags: Some(TraceFlags::default()), + }; + + let trace_context_cloned = trace_context.clone(); + + assert_eq!(trace_context, trace_context_cloned); + + let trace_context_different = TraceContext { + trace_id: TraceId::from(2), + span_id: SpanId::from(2), + trace_flags: Some(TraceFlags::default()), + }; + + assert_ne!(trace_context, trace_context_different); + } + + #[test] + fn compare_log_record() { + let mut log_record = SdkLogRecord { + event_name: Some("test_event"), + target: Some(Cow::Borrowed("foo::bar")), + timestamp: Some(now()), + observed_timestamp: Some(now()), + severity_text: Some("ERROR"), + severity_number: Some(Severity::Error), + body: Some(AnyValue::String("Test body".into())), + attributes: LogRecordAttributes::new(), + trace_context: Some(TraceContext { + trace_id: TraceId::from(1), + span_id: SpanId::from(1), + trace_flags: Some(TraceFlags::default()), + }), + }; + log_record.add_attribute(Key::new("key"), AnyValue::String("value".into())); + + let log_record_cloned = log_record.clone(); + + assert_eq!(log_record, log_record_cloned); + + let mut log_record_different = log_record.clone(); + log_record_different.event_name = Some("different_event"); + + assert_ne!(log_record, log_record_different); + } + + #[test] + fn compare_log_record_target_borrowed_eq_owned() { + let log_record_borrowed = SdkLogRecord { + event_name: Some("test_event"), + ..SdkLogRecord::new() + }; + + let log_record_owned = SdkLogRecord { + event_name: Some("test_event"), + ..SdkLogRecord::new() + }; + + assert_eq!(log_record_borrowed, log_record_owned); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/simple_log_processor.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/simple_log_processor.rs new file mode 100644 index 00000000..34088422 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/logs/simple_log_processor.rs @@ -0,0 +1,688 @@ +//! # OpenTelemetry Simple Log Processor +//! The `SimpleLogProcessor` is one implementation of the `LogProcessor` interface. +//! +//! It forwards log records to the exporter immediately after they are emitted +//! (or one exporter after another if applicable). This processor is +//! **synchronous** and is designed for debugging or testing purposes. It is +//! **not suitable for production** environments due to its lack of batching, +//! performance optimizations, or support for high-throughput scenarios. +//! +//! ## Diagram +//! +//! ```ascii +//! +-----+---------------+ +-----------------------+ +-------------------+ +//! | | | | | | | +//! | SDK | Logger.emit() +---> (Simple)LogProcessor +---> LogExporter | +//! +-----+---------------+ +-----------------------+ +-------------------+ +//! ``` + +use crate::error::{OTelSdkError, OTelSdkResult}; +use crate::logs::log_processor::LogProcessor; +use crate::{ + logs::{LogBatch, LogExporter, SdkLogRecord}, + Resource, +}; + +#[cfg(feature = "experimental_metrics_bound_instruments")] +use opentelemetry::KeyValue; +use opentelemetry::{otel_debug, otel_error, otel_warn, Context, InstrumentationScope}; + +use std::fmt::Debug; +use std::sync::atomic::AtomicBool; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use std::sync::atomic::AtomicUsize; +use std::sync::Mutex; +use std::time::Duration; + +/// A [`LogProcessor`] designed for testing and debugging purpose, that immediately +/// exports log records as they are emitted. Log records are exported synchronously +/// in the same thread that emits the log record. +/// When using this processor with the OTLP Exporter, the following exporter +/// features are supported: +/// - `grpc-tonic`: This requires LoggerProvider to be created within a tokio +/// runtime. Logs can be emitted from any thread, including tokio runtime +/// threads. +/// - `reqwest-blocking-client`: LoggerProvider may be created anywhere, but +/// logs must be emitted from a non-tokio runtime thread. +/// - `reqwest-client`: LoggerProvider may be created anywhere, but logs must be +/// emitted from a tokio runtime thread. +/// +/// ## Example +/// +/// ### Using a SimpleLogProcessor +/// +/// ```rust +/// # #[cfg(feature = "testing")] +/// # { +/// use opentelemetry_sdk::logs::{SimpleLogProcessor, SdkLoggerProvider, LogExporter}; +/// use opentelemetry::global; +/// use opentelemetry_sdk::logs::InMemoryLogExporter; +/// +/// let exporter = InMemoryLogExporter::default(); // Replace with an actual exporter +/// let provider = SdkLoggerProvider::builder() +/// .with_simple_exporter(exporter) +/// .build(); +/// # } +/// ``` +/// +#[derive(Debug)] +pub struct SimpleLogProcessor { + exporter: Mutex, + is_shutdown: AtomicBool, + + // Self-diagnostics: otel.sdk.processor.log.processed counter, gated behind + // experimental_metrics_bound_instruments so the hot-path `add` is a single + // atomic increment with no per-call attribute resolution. + // + // The SimpleLogProcessor submits each record to the exporter synchronously + // and has no queue, so the only processor-side drop is `already_shutdown`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_success: opentelemetry::metrics::BoundCounter, + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_after_shutdown: opentelemetry::metrics::BoundCounter, +} + +impl SimpleLogProcessor { + /// Creates a new instance of `SimpleLogProcessor`. + pub fn new(exporter: T) -> Self { + #[cfg(feature = "experimental_metrics_bound_instruments")] + let (processed_success, processed_after_shutdown) = { + static INSTANCE_COUNTER: AtomicUsize = AtomicUsize::new(0); + let instance_id = INSTANCE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let component_name = format!("simple_log_processor/{instance_id}"); + + let meter = opentelemetry::global::meter("otel.sdk"); + let counter = meter + .u64_counter("otel.sdk.processor.log.processed") + .with_description( + "The number of log records for which the processing has finished, \ + either successful or failed.", + ) + .with_unit("{log_record}") + .build(); + + // Attribute values follow the OTel semantic conventions for SDK metrics: + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/otel/sdk-metrics.md#metric-otelsdkprocessorlogprocessed + // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/registry/attributes/otel.md#otel-component-attributes + let success_attrs = [ + KeyValue::new("otel.component.type", "simple_log_processor"), + KeyValue::new("otel.component.name", component_name.clone()), + ]; + let after_shutdown_attrs = [ + KeyValue::new("error.type", "already_shutdown"), + KeyValue::new("otel.component.type", "simple_log_processor"), + KeyValue::new("otel.component.name", component_name), + ]; + + ( + counter.bind(&success_attrs), + counter.bind(&after_shutdown_attrs), + ) + }; + + SimpleLogProcessor { + exporter: Mutex::new(exporter), + is_shutdown: AtomicBool::new(false), + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_success, + #[cfg(feature = "experimental_metrics_bound_instruments")] + processed_after_shutdown, + } + } +} + +impl LogProcessor for SimpleLogProcessor { + fn emit(&self, record: &mut SdkLogRecord, instrumentation: &InstrumentationScope) { + let _suppress_guard = Context::enter_telemetry_suppressed_scope(); + // noop after shutdown + if self.is_shutdown.load(std::sync::atomic::Ordering::Relaxed) { + // Record the post-shutdown drop in self-diagnostics before returning. + #[cfg(feature = "experimental_metrics_bound_instruments")] + self.processed_after_shutdown.add(1); + + // this is a warning, as the user is trying to log after the processor has been shutdown + otel_warn!( + name: "SimpleLogProcessor.Emit.ProcessorShutdown", + ); + return; + } + + let result = match self.exporter.lock() { + Ok(exporter) => { + let log_tuple = &[(record as &SdkLogRecord, instrumentation)]; + // Count the record as processed right before submitting it to + // the exporter, independent of the export outcome, per semconv. + // Matches BatchLogProcessor, which records success before export. + #[cfg(feature = "experimental_metrics_bound_instruments")] + self.processed_success.add(1); + futures_executor::block_on(exporter.export(LogBatch::new(log_tuple))) + } + Err(_) => Err(OTelSdkError::InternalFailure( + "SimpleLogProcessor mutex poison".into(), + )), + }; + // Handle errors with specific static names + match result { + Err(OTelSdkError::InternalFailure(_)) => { + // logging as debug as this is not a user error + otel_debug!( + name: "SimpleLogProcessor.Emit.MutexPoisoning", + ); + } + Err(err) => { + otel_error!( + name: "SimpleLogProcessor.Emit.ExportError", + error = format!("{}",err) + ); + } + _ => {} + } + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + self.is_shutdown + .store(true, std::sync::atomic::Ordering::Relaxed); + if let Ok(exporter) = self.exporter.lock() { + exporter.shutdown_with_timeout(timeout) + } else { + Err(OTelSdkError::InternalFailure( + "SimpleLogProcessor mutex poison at shutdown".into(), + )) + } + } + + fn set_resource(&mut self, resource: &Resource) { + if let Ok(mut exporter) = self.exporter.lock() { + exporter.set_resource(resource); + } + } + + #[inline] + fn event_enabled( + &self, + level: opentelemetry::logs::Severity, + target: &str, + name: Option<&str>, + ) -> bool { + if let Ok(exporter) = self.exporter.lock() { + exporter.event_enabled(level, target, name) + } else { + true + } + } +} + +#[cfg(all(test, feature = "testing", feature = "logs"))] +mod tests { + use crate::logs::log_processor::tests::MockLogExporter; + use crate::logs::{LogBatch, LogExporter, SdkLogRecord, SdkLogger}; + use crate::{ + error::OTelSdkResult, + logs::{InMemoryLogExporterBuilder, LogProcessor, SdkLoggerProvider, SimpleLogProcessor}, + Resource, + }; + use opentelemetry::logs::{LogRecord, Logger, LoggerProvider}; + use opentelemetry::InstrumentationScope; + use opentelemetry::KeyValue; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; + use std::time; + use std::time::Duration; + + #[derive(Debug, Clone)] + struct LogExporterThatRequiresTokio { + export_count: Arc, + } + + impl LogExporterThatRequiresTokio { + /// Creates a new instance of `LogExporterThatRequiresTokio`. + fn new() -> Self { + LogExporterThatRequiresTokio { + export_count: Arc::new(AtomicUsize::new(0)), + } + } + + /// Returns the number of logs stored in the exporter. + fn len(&self) -> usize { + self.export_count.load(Ordering::Acquire) + } + } + + impl LogExporter for LogExporterThatRequiresTokio { + async fn export(&self, batch: LogBatch<'_>) -> OTelSdkResult { + // Simulate minimal dependency on tokio by sleeping asynchronously for a short duration + tokio::time::sleep(Duration::from_millis(50)).await; + + for _ in batch.iter() { + self.export_count.fetch_add(1, Ordering::Acquire); + } + Ok(()) + } + fn shutdown_with_timeout(&self, _timeout: time::Duration) -> OTelSdkResult { + Ok(()) + } + } + + #[test] + fn test_set_resource_simple_processor() { + let exporter = MockLogExporter { + resource: Arc::new(Mutex::new(None)), + }; + let processor = SimpleLogProcessor::new(exporter.clone()); + let _ = SdkLoggerProvider::builder() + .with_log_processor(processor) + .with_resource( + Resource::builder_empty() + .with_attributes([ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v3"), + KeyValue::new("k3", "v3"), + KeyValue::new("k4", "v4"), + KeyValue::new("k5", "v5"), + ]) + .build(), + ) + .build(); + assert_eq!(exporter.get_resource().unwrap().into_iter().count(), 5); + } + + #[test] + fn test_simple_shutdown() { + let exporter = InMemoryLogExporterBuilder::default() + .keep_records_on_shutdown() + .build(); + let processor = SimpleLogProcessor::new(exporter.clone()); + + let mut record: SdkLogRecord = SdkLogRecord::new(); + let instrumentation: InstrumentationScope = Default::default(); + + processor.emit(&mut record, &instrumentation); + + processor.shutdown().unwrap(); + + let is_shutdown = processor + .is_shutdown + .load(std::sync::atomic::Ordering::Relaxed); + assert!(is_shutdown); + + processor.emit(&mut record, &instrumentation); + + assert_eq!(1, exporter.get_emitted_logs().unwrap().len()); + assert!(exporter.is_shutdown_called()); + } + + #[test] + fn test_simple_processor_sync_exporter_without_runtime() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = SimpleLogProcessor::new(exporter.clone()); + + let mut record: SdkLogRecord = SdkLogRecord::new(); + let instrumentation: InstrumentationScope = Default::default(); + + processor.emit(&mut record, &instrumentation); + + assert_eq!(exporter.get_emitted_logs().unwrap().len(), 1); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn test_simple_processor_sync_exporter_with_runtime() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = SimpleLogProcessor::new(exporter.clone()); + + let mut record: SdkLogRecord = SdkLogRecord::new(); + let instrumentation: InstrumentationScope = Default::default(); + + processor.emit(&mut record, &instrumentation); + + assert_eq!(exporter.get_emitted_logs().unwrap().len(), 1); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_simple_processor_sync_exporter_with_multi_thread_runtime() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = Arc::new(SimpleLogProcessor::new(exporter.clone())); + + let mut handles = vec![]; + for _ in 0..10 { + let processor_clone = Arc::clone(&processor); + let handle = tokio::spawn(async move { + let mut record: SdkLogRecord = SdkLogRecord::new(); + let instrumentation: InstrumentationScope = Default::default(); + processor_clone.emit(&mut record, &instrumentation); + }); + handles.push(handle); + } + + for handle in handles { + handle.await.unwrap(); + } + + assert_eq!(exporter.get_emitted_logs().unwrap().len(), 10); + } + + #[tokio::test(flavor = "current_thread")] + async fn test_simple_processor_sync_exporter_with_current_thread_runtime() { + let exporter = InMemoryLogExporterBuilder::default().build(); + let processor = SimpleLogProcessor::new(exporter.clone()); + + let mut record: SdkLogRecord = SdkLogRecord::new(); + let instrumentation: InstrumentationScope = Default::default(); + + processor.emit(&mut record, &instrumentation); + + assert_eq!(exporter.get_emitted_logs().unwrap().len(), 1); + } + + #[test] + fn test_simple_processor_async_exporter_without_runtime() { + // Use `catch_unwind` to catch the panic caused by missing Tokio runtime + let result = std::panic::catch_unwind(|| { + let exporter = LogExporterThatRequiresTokio::new(); + let processor = SimpleLogProcessor::new(exporter.clone()); + + let mut record: SdkLogRecord = SdkLogRecord::new(); + let instrumentation: InstrumentationScope = Default::default(); + + // This will panic because an tokio async operation within exporter without a runtime. + processor.emit(&mut record, &instrumentation); + }); + + // Verify that the panic occurred and check the panic message for the absence of a Tokio runtime + assert!( + result.is_err(), + "The test should fail due to missing Tokio runtime, but it did not." + ); + let panic_payload = result.unwrap_err(); + let panic_message = panic_payload + .downcast_ref::() + .map(|s| s.as_str()) + .or_else(|| panic_payload.downcast_ref::<&str>().copied()) + .unwrap_or("No panic message"); + + assert!( + panic_message.contains("no reactor running") + || panic_message.contains("must be called from the context of a Tokio 1.x runtime"), + "Expected panic message about missing Tokio runtime, but got: {panic_message}" + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] + #[ignore] + // This test demonstrates a potential deadlock scenario in a multi-threaded Tokio runtime. + // It spawns Tokio tasks equal to the number of runtime worker threads (4) to emit log events. + // Each task attempts to acquire a mutex on the exporter in `SimpleLogProcessor::emit`. + // Only one task obtains the lock, while the others are blocked, waiting for its release. + // + // The task holding the lock invokes the LogExporterThatRequiresTokio, which performs an + // asynchronous operation (e.g., network I/O simulated by `tokio::sleep`). This operation + // requires yielding control back to the Tokio runtime to make progress. + // + // However, all worker threads are occupied: + // - One thread is executing the async exporter operation + // - Three threads are blocked waiting for the mutex + // + // This leads to a deadlock as there are no available threads to drive the async operation + // to completion, preventing the mutex from being released. Consequently, neither the blocked + // tasks nor the exporter can proceed. + async fn test_simple_processor_async_exporter_with_all_runtime_worker_threads_blocked() { + let exporter = LogExporterThatRequiresTokio::new(); + let processor = Arc::new(SimpleLogProcessor::new(exporter.clone())); + + let concurrent_emit = 4; // number of worker threads + + let mut handles = vec![]; + // try send `concurrent_emit` events concurrently + for _ in 0..concurrent_emit { + let processor_clone = Arc::clone(&processor); + let handle = tokio::spawn(async move { + let mut record: SdkLogRecord = SdkLogRecord::new(); + let instrumentation: InstrumentationScope = Default::default(); + processor_clone.emit(&mut record, &instrumentation); + }); + handles.push(handle); + } + + // below code won't get executed + for handle in handles { + handle.await.unwrap(); + } + assert_eq!(exporter.len(), concurrent_emit); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + // This test uses a multi-threaded runtime setup with a single worker thread. Note that even + // though only one worker thread is created, it is distinct from the main thread. The processor + // emits a log event, and the exporter performs an async operation that requires the runtime. + // The single worker thread handles this operation without deadlocking, as long as no other + // tasks occupy the runtime. + async fn test_simple_processor_async_exporter_with_runtime() { + let exporter = LogExporterThatRequiresTokio::new(); + let processor = SimpleLogProcessor::new(exporter.clone()); + + let mut record: SdkLogRecord = SdkLogRecord::new(); + let instrumentation: InstrumentationScope = Default::default(); + + processor.emit(&mut record, &instrumentation); + + assert_eq!(exporter.len(), 1); + } + + #[tokio::test(flavor = "multi_thread")] + // This test uses a multi-threaded runtime setup with the default number of worker threads. + // The processor emits a log event, and the exporter, which requires the runtime for its async + // operations, can access one of the available worker threads to complete its task. As there + // are multiple threads, the exporter can proceed without blocking other tasks, ensuring the + // test completes successfully. + async fn test_simple_processor_async_exporter_with_multi_thread_runtime() { + let exporter = LogExporterThatRequiresTokio::new(); + + let processor = SimpleLogProcessor::new(exporter.clone()); + + let mut record: SdkLogRecord = SdkLogRecord::new(); + let instrumentation: InstrumentationScope = Default::default(); + + processor.emit(&mut record, &instrumentation); + + assert_eq!(exporter.len(), 1); + } + + #[tokio::test(flavor = "current_thread")] + #[ignore] + // This test uses a current-thread runtime, where all operations run on the main thread. + // The processor emits a log event while the runtime is blocked using `futures::block_on` + // to complete the export operation. The exporter, which performs an async operation and + // requires the runtime, cannot progress because the main thread is already blocked. + // This results in a deadlock, as the runtime cannot move forward. + async fn test_simple_processor_async_exporter_with_current_thread_runtime() { + let exporter = LogExporterThatRequiresTokio::new(); + + let processor = SimpleLogProcessor::new(exporter.clone()); + + let mut record: SdkLogRecord = SdkLogRecord::new(); + let instrumentation: InstrumentationScope = Default::default(); + + processor.emit(&mut record, &instrumentation); + + assert_eq!(exporter.len(), 1); + } + + #[derive(Debug, Clone)] + struct ReentrantLogExporter { + logger: Arc>>, + } + + impl ReentrantLogExporter { + fn new() -> Self { + Self { + logger: Arc::new(Mutex::new(None)), + } + } + + fn set_logger(&self, logger: SdkLogger) { + let mut guard = self.logger.lock().unwrap(); + *guard = Some(logger); + } + } + + impl LogExporter for ReentrantLogExporter { + async fn export(&self, _batch: LogBatch<'_>) -> OTelSdkResult { + let logger = self.logger.lock().unwrap(); + if let Some(logger) = logger.as_ref() { + let mut log_record = logger.create_log_record(); + log_record.set_severity_number(opentelemetry::logs::Severity::Error); + logger.emit(log_record); + } + + Ok(()) + } + } + + #[test] + fn exporter_internal_log_does_not_deadlock_with_simple_processor() { + // This tests that even when exporter produces logs while + // exporting, it does not deadlock, as SimpleLogProcessor + // activates SuppressGuard before calling the exporter. + let exporter: ReentrantLogExporter = ReentrantLogExporter::new(); + let logger_provider = SdkLoggerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + exporter.set_logger(logger_provider.logger("processor-logger")); + + let logger = logger_provider.logger("test-logger"); + let mut log_record = logger.create_log_record(); + log_record.set_severity_number(opentelemetry::logs::Severity::Error); + logger.emit(log_record); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + mod self_obs { + use super::*; + + /// Sums the values of `otel.sdk.processor.log.processed` data points whose + /// `error.type` attribute equals `error_type` (or that have no `error.type` + /// attribute when `error_type` is `None`). + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn sum_processed_log_records( + metric_exporter: &crate::metrics::InMemoryMetricExporter, + error_type: Option<&str>, + ) -> u64 { + use crate::metrics::data::{AggregatedMetrics, MetricData}; + + let metrics = metric_exporter.get_finished_metrics().unwrap(); + let mut total: u64 = 0; + for rm in &metrics { + for sm in &rm.scope_metrics { + for metric in &sm.metrics { + if metric.name == "otel.sdk.processor.log.processed" { + if let AggregatedMetrics::U64(MetricData::Sum(sum)) = &metric.data { + for dp in sum.data_points() { + let dp_error_type = dp + .attributes() + .find(|kv| kv.key.as_str() == "error.type") + .map(|kv| kv.value.as_str().to_string()); + let matches = match error_type { + Some(expected) => { + dp_error_type.as_deref() == Some(expected) + } + None => dp_error_type.is_none(), + }; + if matches { + total += dp.value(); + } + } + } + } + } + } + } + total + } + + /// Verifies that `otel.sdk.processor.log.processed` counts each record the + /// SimpleLogProcessor submits to the exporter (with no `error.type`), + /// independent of the export outcome. + /// + /// `#[ignore]`d because it mutates process-wide state via + /// `global::set_meter_provider()`. CI runs it in isolation via `test.sh`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + #[ignore] + fn self_diagnostics_counter_records_success() { + use crate::logs::InMemoryLogExporterBuilder; + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + let log_exporter = InMemoryLogExporterBuilder::default().build(); + let processor = SimpleLogProcessor::new(log_exporter); + + let instrumentation = InstrumentationScope::default(); + for _ in 0..10 { + let mut record = SdkLogRecord::new(); + processor.emit(&mut record, &instrumentation); + } + + meter_provider.force_flush().unwrap(); + + let processed = sum_processed_log_records(&metric_exporter, None); + assert_eq!(processed, 10, "expected 10 processed logs, got {processed}"); + + meter_provider.shutdown().unwrap(); + } + + /// Verifies that `otel.sdk.processor.log.processed` records post-shutdown + /// emits with `error.type = already_shutdown`. + /// + /// `#[ignore]`d because it mutates process-wide state via + /// `global::set_meter_provider()`. CI runs it in isolation via `test.sh`. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + #[ignore] + fn self_diagnostics_counter_records_already_shutdown_drops() { + use crate::logs::InMemoryLogExporterBuilder; + use crate::metrics::{InMemoryMetricExporter, SdkMeterProvider}; + + let metric_exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(metric_exporter.clone()) + .build(); + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + let log_exporter = InMemoryLogExporterBuilder::default().build(); + let processor = SimpleLogProcessor::new(log_exporter); + + // Shut the processor down; subsequent emits hit the already_shutdown branch. + processor.shutdown().unwrap(); + + let instrumentation = InstrumentationScope::default(); + for _ in 0..7 { + let mut record = SdkLogRecord::new(); + processor.emit(&mut record, &instrumentation); + } + + meter_provider.force_flush().unwrap(); + + let already_shutdown = + sum_processed_log_records(&metric_exporter, Some("already_shutdown")); + assert_eq!( + already_shutdown, 7, + "expected 7 already_shutdown drops, got {already_shutdown}" + ); + let success = sum_processed_log_records(&metric_exporter, None); + assert_eq!( + success, 0, + "post-shutdown emits must not be counted as success, got {success}" + ); + + meter_provider.shutdown().unwrap(); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/aggregation.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/aggregation.rs new file mode 100644 index 00000000..af9cb02a --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/aggregation.rs @@ -0,0 +1,230 @@ +use std::fmt; + +use crate::metrics::error::{MetricError, MetricResult}; +use crate::metrics::internal::{EXPO_MAX_SCALE, EXPO_MIN_SCALE}; + +/// The way recorded measurements are summarized. +#[derive(Clone, Debug, PartialEq)] +#[non_exhaustive] +pub enum Aggregation { + /// An aggregation that drops all recorded data. + Drop, + + /// An aggregation that uses the default instrument kind selection mapping to + /// select another aggregation. + /// + /// A metric reader can be configured to make an aggregation selection based on + /// instrument kind that differs from the default. This aggregation ensures the + /// default is used. + /// + /// See the [the spec] for information about the default + /// instrument kind selection mapping. + /// + /// [the spec]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.19.0/specification/metrics/sdk.md#default-aggregation + Default, + + /// An aggregation that summarizes a set of measurements as their arithmetic + /// sum. + Sum, + + /// An aggregation that summarizes a set of measurements as the last one made. + LastValue, + + /// An aggregation that summarizes a set of measurements as a histogram with + /// explicitly defined buckets. + ExplicitBucketHistogram { + /// The increasing bucket boundary values. + /// + /// Boundary values define bucket upper bounds. Buckets are exclusive of their + /// lower boundary and inclusive of their upper bound (except at positive + /// infinity). A measurement is defined to fall into the greatest-numbered + /// bucket with a boundary that is greater than or equal to the measurement. As + /// an example, boundaries defined as: + /// + /// vec![0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, + /// 1000.0, 2500.0, 5000.0, 7500.0, 10000.0]; + /// + /// Will define these buckets: + /// + /// (-∞, 0], (0, 5.0], (5.0, 10.0], (10.0, 25.0], (25.0, 50.0], (50.0, + /// 75.0], (75.0, 100.0], (100.0, 250.0], (250.0, 500.0], (500.0, + /// 750.0], (750.0, 1000.0], (1000.0, 2500.0], (2500.0, 5000.0], + /// (5000.0, 7500.0], (7500.0, 10000.0], (10000.0, +∞) + boundaries: Vec, + + /// Indicates whether to not record the min and max of the distribution. + /// + /// By default, these values are recorded. + /// + /// Recording these values for cumulative data is expected to have little + /// value, they will represent the entire life of the instrument instead of + /// just the current collection cycle. It is recommended to set this to + /// `false` for that type of data to avoid computing the low-value + /// instances. + record_min_max: bool, + }, + + /// An aggregation that summarizes a set of measurements as a histogram with + /// bucket widths that grow exponentially. + Base2ExponentialHistogram { + /// The maximum number of buckets to use for the histogram. + max_size: u32, + + /// The maximum resolution scale to use for the histogram. + /// + /// The maximum value is `20`, in which case the maximum number of buckets + /// that can fit within the range of a signed 32-bit integer index could be + /// used. + /// + /// The minimum value is `-10` in which case only two buckets will be used. + max_scale: i8, + + /// Indicates whether to not record the min and max of the distribution. + /// + /// By default, these values are recorded. + /// + /// It is generally not valuable to record min and max for cumulative data + /// as they will represent the entire life of the instrument instead of just + /// the current collection cycle, you can opt out by setting this value to + /// `false` + record_min_max: bool, + }, +} + +impl fmt::Display for Aggregation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // used for stream id comparisons + let name = match self { + Aggregation::Drop => "Drop", + Aggregation::Default => "Default", + Aggregation::Sum => "Sum", + Aggregation::LastValue => "LastValue", + Aggregation::ExplicitBucketHistogram { .. } => "ExplicitBucketHistogram", + Aggregation::Base2ExponentialHistogram { .. } => "Base2ExponentialHistogram", + }; + + f.write_str(name) + } +} + +impl Aggregation { + /// Validate that this aggregation has correct configuration + #[allow(unused)] + pub(crate) fn validate(&self) -> MetricResult<()> { + match self { + Aggregation::Drop => Ok(()), + Aggregation::Default => Ok(()), + Aggregation::Sum => Ok(()), + Aggregation::LastValue => Ok(()), + Aggregation::ExplicitBucketHistogram { boundaries, .. } => { + for x in boundaries.windows(2) { + if x[0] >= x[1] { + return Err(MetricError::Config(format!( + "aggregation: explicit bucket histogram: non-monotonic boundaries: {boundaries:?}", + ))); + } + } + + Ok(()) + } + Aggregation::Base2ExponentialHistogram { max_scale, .. } => { + if *max_scale > EXPO_MAX_SCALE { + return Err(MetricError::Config(format!( + "aggregation: exponential histogram: max scale ({max_scale}) is greater than {}", EXPO_MAX_SCALE + ))); + } + if *max_scale < EXPO_MIN_SCALE { + return Err(MetricError::Config(format!( + "aggregation: exponential histogram: max scale ({max_scale}) is less than {}", EXPO_MIN_SCALE + ))); + } + + Ok(()) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::Aggregation; + use crate::metrics::error::{MetricError, MetricResult}; + use crate::metrics::internal::{EXPO_MAX_SCALE, EXPO_MIN_SCALE}; + + #[test] + fn validate_aggregation() { + struct TestCase { + name: &'static str, + input: Aggregation, + check: Box) -> bool>, + } + let ok = Box::new(|result: MetricResult<()>| result.is_ok()); + let config_error = Box::new(|result| matches!(result, Err(MetricError::Config(_)))); + + let test_cases: Vec = vec![ + TestCase { + name: "base2 histogram with maximum max_scale", + input: Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: EXPO_MAX_SCALE, + record_min_max: true, + }, + check: ok.clone(), + }, + TestCase { + name: "base2 histogram with minimum max_scale", + input: Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: EXPO_MIN_SCALE, + record_min_max: true, + }, + check: ok.clone(), + }, + TestCase { + name: "base2 histogram with max_scale too small", + input: Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: EXPO_MIN_SCALE - 1, + record_min_max: true, + }, + check: config_error.clone(), + }, + TestCase { + name: "base2 histogram with max_scale too big", + input: Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: EXPO_MAX_SCALE + 1, + record_min_max: true, + }, + check: config_error.clone(), + }, + TestCase { + name: "explicit histogram with one boundary", + input: Aggregation::ExplicitBucketHistogram { + boundaries: vec![0.0], + record_min_max: true, + }, + check: ok.clone(), + }, + TestCase { + name: "explicit histogram with monotonic boundaries", + input: Aggregation::ExplicitBucketHistogram { + boundaries: vec![0.0, 2.0, 4.0, 8.0], + record_min_max: true, + }, + check: ok.clone(), + }, + TestCase { + name: "explicit histogram with non-monotonic boundaries", + input: Aggregation::ExplicitBucketHistogram { + boundaries: vec![2.0, 0.0, 4.0, 8.0], + record_min_max: true, + }, + check: config_error.clone(), + }, + ]; + for test in test_cases { + assert!((test.check)(test.input.validate()), "{}", test.name) + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/data/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/data/mod.rs new file mode 100644 index 00000000..09464e7d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/data/mod.rs @@ -0,0 +1,799 @@ +//! Types for delivery of pre-aggregated metric time series data. + +use std::{borrow::Cow, time::SystemTime}; + +use opentelemetry::{InstrumentationScope, KeyValue}; + +use crate::Resource; + +use super::Temporality; + +/// A collection of [ScopeMetrics] and the associated [Resource] that created them. +#[derive(Debug)] +pub struct ResourceMetrics { + /// The entity that collected the metrics. + pub(crate) resource: Resource, + /// The collection of metrics with unique [InstrumentationScope]s. + pub(crate) scope_metrics: Vec, +} + +impl Default for ResourceMetrics { + fn default() -> Self { + Self { + resource: Resource::empty(), + scope_metrics: Vec::new(), + } + } +} + +impl ResourceMetrics { + /// Creates a new [ResourceMetrics] with the given [Resource] and [ScopeMetrics]. + pub fn new(resource: Resource, scope_metrics: Vec) -> Self { + Self { + resource, + scope_metrics, + } + } + + /// Returns a reference to the [Resource] in [ResourceMetrics]. + pub fn resource(&self) -> &Resource { + &self.resource + } + + /// Returns an iterator over the [ScopeMetrics] in [ResourceMetrics]. + pub fn scope_metrics(&self) -> impl Iterator { + self.scope_metrics.iter() + } +} + +/// A collection of metrics produced by a meter. +#[derive(Default, Debug)] +pub struct ScopeMetrics { + /// The [InstrumentationScope] that the meter was created with. + pub(crate) scope: InstrumentationScope, + /// The list of aggregations created by the meter. + pub(crate) metrics: Vec, +} + +impl ScopeMetrics { + /// Creates a new [ScopeMetrics] with the given [InstrumentationScope] and [Metric]s. + pub fn new(scope: InstrumentationScope, metrics: Vec) -> Self { + Self { scope, metrics } + } + + /// Returns a reference to the [InstrumentationScope] in [ScopeMetrics]. + pub fn scope(&self) -> &InstrumentationScope { + &self.scope + } + + /// Returns an iterator over the [Metric]s in [ScopeMetrics]. + pub fn metrics(&self) -> impl Iterator { + self.metrics.iter() + } +} + +/// A collection of one or more aggregated time series from an [Instrument]. +/// +/// [Instrument]: crate::metrics::Instrument +#[derive(Debug)] +pub struct Metric { + /// The name of the instrument that created this data. + pub(crate) name: Cow<'static, str>, + /// The description of the instrument, which can be used in documentation. + pub(crate) description: Cow<'static, str>, + /// The unit in which the instrument reports. + pub(crate) unit: Cow<'static, str>, + /// The aggregated data from an instrument. + pub(crate) data: AggregatedMetrics, +} + +impl Metric { + /// Creates a new [Metric] with the given name, description, unit, and aggregated data. + pub fn new( + name: impl Into>, + description: impl Into>, + unit: impl Into>, + data: AggregatedMetrics, + ) -> Self { + Self { + name: name.into(), + description: description.into(), + unit: unit.into(), + data, + } + } + + /// Returns the name of the instrument that created this data. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the description of the instrument. + pub fn description(&self) -> &str { + &self.description + } + + /// Returns the unit in which the instrument reports. + pub fn unit(&self) -> &str { + &self.unit + } + + /// Returns the aggregated data from the instrument. + pub fn data(&self) -> &AggregatedMetrics { + &self.data + } +} + +/// Aggregated metrics data from an instrument +#[derive(Debug)] +pub enum AggregatedMetrics { + /// All metric data with `f64` value type + F64(MetricData), + /// All metric data with `u64` value type + U64(MetricData), + /// All metric data with `i64` value type + I64(MetricData), +} + +/// Metric data for all types +#[derive(Debug)] +pub enum MetricData { + /// Metric data for Gauge + Gauge(Gauge), + /// Metric data for Sum + Sum(Sum), + /// Metric data for Histogram + Histogram(Histogram), + /// Metric data for ExponentialHistogram + ExponentialHistogram(ExponentialHistogram), +} + +impl From> for AggregatedMetrics { + fn from(value: MetricData) -> Self { + AggregatedMetrics::F64(value) + } +} + +impl From> for AggregatedMetrics { + fn from(value: MetricData) -> Self { + AggregatedMetrics::I64(value) + } +} + +impl From> for AggregatedMetrics { + fn from(value: MetricData) -> Self { + AggregatedMetrics::U64(value) + } +} + +impl From> for MetricData { + fn from(value: Gauge) -> Self { + MetricData::Gauge(value) + } +} + +impl From> for MetricData { + fn from(value: Sum) -> Self { + MetricData::Sum(value) + } +} + +impl From> for MetricData { + fn from(value: Histogram) -> Self { + MetricData::Histogram(value) + } +} + +impl From> for MetricData { + fn from(value: ExponentialHistogram) -> Self { + MetricData::ExponentialHistogram(value) + } +} + +/// DataPoint is a single data point in a time series. +#[derive(Debug, Clone, PartialEq)] +pub struct GaugeDataPoint { + /// Attributes is the set of key value pairs that uniquely identify the + /// time series. + pub(crate) attributes: Vec, + /// The value of this data point. + pub(crate) value: T, + /// The sampled [Exemplar]s collected during the time series. + pub(crate) exemplars: Vec>, +} + +impl GaugeDataPoint { + /// Creates a new [GaugeDataPoint] with the given attributes, value, and exemplars. + pub fn new(attributes: Vec, value: T, exemplars: Vec>) -> Self { + Self { + attributes, + value, + exemplars, + } + } + + /// Returns an iterator over the attributes in [GaugeDataPoint]. + pub fn attributes(&self) -> impl Iterator { + self.attributes.iter() + } + + /// Returns an iterator over the [Exemplar]s in [GaugeDataPoint]. + pub fn exemplars(&self) -> impl Iterator> { + self.exemplars.iter() + } +} + +impl GaugeDataPoint { + /// Returns the value of this data point. + pub fn value(&self) -> T { + self.value + } +} + +/// A measurement of the current value of an instrument. +#[derive(Debug, Clone)] +pub struct Gauge { + /// Represents individual aggregated measurements with unique attributes. + pub(crate) data_points: Vec>, + /// The time when the time series was started. + pub(crate) start_time: Option, + /// The time when the time series was recorded. + pub(crate) time: SystemTime, +} + +impl Gauge { + /// Creates a new [Gauge] with the given data points, start time, and time. + pub fn new( + data_points: Vec>, + start_time: Option, + time: SystemTime, + ) -> Self { + Self { + data_points, + start_time, + time, + } + } + + /// Returns an iterator over the [GaugeDataPoint]s in [Gauge]. + pub fn data_points(&self) -> impl Iterator> { + self.data_points.iter() + } + + /// Returns the time when the time series was started. + pub fn start_time(&self) -> Option { + self.start_time + } + + /// Returns the time when the time series was recorded. + pub fn time(&self) -> SystemTime { + self.time + } +} + +/// DataPoint is a single data point in a time series. +#[derive(Debug, Clone, PartialEq)] +pub struct SumDataPoint { + /// Attributes is the set of key value pairs that uniquely identify the + /// time series. + pub(crate) attributes: Vec, + /// The value of this data point. + pub(crate) value: T, + /// The sampled [Exemplar]s collected during the time series. + pub(crate) exemplars: Vec>, +} + +impl SumDataPoint { + /// Creates a new [SumDataPoint] with the given attributes, value, and exemplars. + pub fn new(attributes: Vec, value: T, exemplars: Vec>) -> Self { + Self { + attributes, + value, + exemplars, + } + } + + /// Returns an iterator over the attributes in [SumDataPoint]. + pub fn attributes(&self) -> impl Iterator { + self.attributes.iter() + } + + /// Returns an iterator over the [Exemplar]s in [SumDataPoint]. + pub fn exemplars(&self) -> impl Iterator> { + self.exemplars.iter() + } +} + +impl SumDataPoint { + /// Returns the value of this data point. + pub fn value(&self) -> T { + self.value + } +} + +/// Represents the sum of all measurements of values from an instrument. +#[derive(Debug, Clone)] +pub struct Sum { + /// Represents individual aggregated measurements with unique attributes. + pub(crate) data_points: Vec>, + /// The time when the time series was started. + pub(crate) start_time: SystemTime, + /// The time when the time series was recorded. + pub(crate) time: SystemTime, + /// Describes if the aggregation is reported as the change from the last report + /// time, or the cumulative changes since a fixed start time. + pub(crate) temporality: Temporality, + /// Whether this aggregation only increases or decreases. + pub(crate) is_monotonic: bool, +} + +impl Sum { + /// Creates a new [Sum] with the given data points, time range, temporality, and monotonicity. + pub fn new( + data_points: Vec>, + start_time: SystemTime, + time: SystemTime, + temporality: Temporality, + is_monotonic: bool, + ) -> Self { + Self { + data_points, + start_time, + time, + temporality, + is_monotonic, + } + } + + /// Returns an iterator over the [SumDataPoint]s in [Sum]. + pub fn data_points(&self) -> impl Iterator> { + self.data_points.iter() + } + + /// Returns the time when the time series was started. + pub fn start_time(&self) -> SystemTime { + self.start_time + } + + /// Returns the time when the time series was recorded. + pub fn time(&self) -> SystemTime { + self.time + } + + /// Returns the temporality describing if the aggregation is reported as the change + /// from the last report time, or the cumulative changes since a fixed start time. + pub fn temporality(&self) -> Temporality { + self.temporality + } + + /// Returns whether this aggregation only increases or decreases. + pub fn is_monotonic(&self) -> bool { + self.is_monotonic + } +} + +/// Represents the histogram of all measurements of values from an instrument. +#[derive(Debug, Clone)] +pub struct Histogram { + /// Individual aggregated measurements with unique attributes. + pub(crate) data_points: Vec>, + /// The time when the time series was started. + pub(crate) start_time: SystemTime, + /// The time when the time series was recorded. + pub(crate) time: SystemTime, + /// Describes if the aggregation is reported as the change from the last report + /// time, or the cumulative changes since a fixed start time. + pub(crate) temporality: Temporality, +} + +impl Histogram { + /// Creates a new [Histogram] with the given data points, time range, and temporality. + pub fn new( + data_points: Vec>, + start_time: SystemTime, + time: SystemTime, + temporality: Temporality, + ) -> Self { + Self { + data_points, + start_time, + time, + temporality, + } + } + + /// Returns an iterator over the [HistogramDataPoint]s in [Histogram]. + pub fn data_points(&self) -> impl Iterator> { + self.data_points.iter() + } + + /// Returns the time when the time series was started. + pub fn start_time(&self) -> SystemTime { + self.start_time + } + + /// Returns the time when the time series was recorded. + pub fn time(&self) -> SystemTime { + self.time + } + + /// Returns the temporality describing if the aggregation is reported as the change + /// from the last report time, or the cumulative changes since a fixed start time. + pub fn temporality(&self) -> Temporality { + self.temporality + } +} + +/// A single histogram data point in a time series. +#[derive(Debug, Clone, PartialEq)] +pub struct HistogramDataPoint { + /// The set of key value pairs that uniquely identify the time series. + pub(crate) attributes: Vec, + /// The number of updates this histogram has been calculated with. + pub(crate) count: u64, + /// The upper bounds of the buckets of the histogram. + /// + /// Because the last boundary is +infinity this one is implied. + pub(crate) bounds: Vec, + /// The count of each of the buckets. + pub(crate) bucket_counts: Vec, + + /// The minimum value recorded. + pub(crate) min: Option, + /// The maximum value recorded. + pub(crate) max: Option, + /// The sum of the values recorded. + pub(crate) sum: T, + + /// The sampled [Exemplar]s collected during the time series. + pub(crate) exemplars: Vec>, +} + +impl HistogramDataPoint { + /// Creates a new [HistogramDataPoint] with the given parameters. + #[allow(clippy::too_many_arguments)] + pub fn new( + attributes: Vec, + count: u64, + bounds: Vec, + bucket_counts: Vec, + min: Option, + max: Option, + sum: T, + exemplars: Vec>, + ) -> Self { + Self { + attributes, + count, + bounds, + bucket_counts, + min, + max, + sum, + exemplars, + } + } + + /// Returns an iterator over the attributes in [HistogramDataPoint]. + pub fn attributes(&self) -> impl Iterator { + self.attributes.iter() + } + + /// Returns an iterator over the exemplars in [HistogramDataPoint]. + pub fn exemplars(&self) -> impl Iterator> { + self.exemplars.iter() + } + + /// Returns an iterator over the bucket boundaries in [HistogramDataPoint]. + pub fn bounds(&self) -> impl Iterator + '_ { + self.bounds.iter().copied() + } + + /// Returns an iterator over the bucket counts in [HistogramDataPoint]. + pub fn bucket_counts(&self) -> impl Iterator + '_ { + self.bucket_counts.iter().copied() + } + + /// Returns the number of updates this histogram has been calculated with. + pub fn count(&self) -> u64 { + self.count + } +} + +impl HistogramDataPoint { + /// Returns the minimum value recorded. + pub fn min(&self) -> Option { + self.min + } + + /// Returns the maximum value recorded. + pub fn max(&self) -> Option { + self.max + } + + /// Returns the sum of the values recorded. + pub fn sum(&self) -> T { + self.sum + } +} + +/// The histogram of all measurements of values from an instrument. +#[derive(Debug, Clone)] +pub struct ExponentialHistogram { + /// The individual aggregated measurements with unique attributes. + pub(crate) data_points: Vec>, + /// When the time series was started. + pub(crate) start_time: SystemTime, + /// The time when the time series was recorded. + pub(crate) time: SystemTime, + /// Describes if the aggregation is reported as the change from the last report + /// time, or the cumulative changes since a fixed start time. + pub(crate) temporality: Temporality, +} + +impl ExponentialHistogram { + /// Returns an iterator over the [ExponentialHistogramDataPoint]s in [ExponentialHistogram]. + pub fn data_points(&self) -> impl Iterator> { + self.data_points.iter() + } + + /// Returns the time when the time series was started. + pub fn start_time(&self) -> SystemTime { + self.start_time + } + + /// Returns the time when the time series was recorded. + pub fn time(&self) -> SystemTime { + self.time + } + + /// Returns the temporality describing if the aggregation is reported as the change + /// from the last report time, or the cumulative changes since a fixed start time. + pub fn temporality(&self) -> Temporality { + self.temporality + } +} + +/// A single exponential histogram data point in a time series. +#[derive(Debug, Clone, PartialEq)] +pub struct ExponentialHistogramDataPoint { + /// The set of key value pairs that uniquely identify the time series. + pub(crate) attributes: Vec, + + /// The number of updates this histogram has been calculated with. + pub(crate) count: usize, + /// The minimum value recorded. + pub(crate) min: Option, + /// The maximum value recorded. + pub(crate) max: Option, + /// The sum of the values recorded. + pub(crate) sum: T, + + /// Describes the resolution of the histogram. + /// + /// Boundaries are located at powers of the base, where: + /// + /// base = 2 ^ (2 ^ -scale) + pub(crate) scale: i8, + + /// The number of values whose absolute value is less than or equal to + /// `zero_threshold`. + /// + /// When `zero_threshold` is `0`, this is the number of values that cannot be + /// expressed using the standard exponential formula as well as values that have + /// been rounded to zero. + pub(crate) zero_count: u64, + + /// The range of positive value bucket counts. + pub(crate) positive_bucket: ExponentialBucket, + /// The range of negative value bucket counts. + pub(crate) negative_bucket: ExponentialBucket, + + /// The width of the zero region. + /// + /// Where the zero region is defined as the closed interval + /// [-zero_threshold, zero_threshold]. + pub(crate) zero_threshold: f64, + + /// The sampled exemplars collected during the time series. + pub(crate) exemplars: Vec>, +} + +impl ExponentialHistogramDataPoint { + /// Returns an iterator over the attributes in [ExponentialHistogramDataPoint]. + pub fn attributes(&self) -> impl Iterator { + self.attributes.iter() + } + + /// Returns an iterator over the exemplars in [ExponentialHistogramDataPoint]. + pub fn exemplars(&self) -> impl Iterator> { + self.exemplars.iter() + } + + /// Returns the number of updates this histogram has been calculated with. + pub fn count(&self) -> usize { + self.count + } + + /// Returns the resolution of the histogram. + pub fn scale(&self) -> i8 { + self.scale + } + + /// Returns the number of values whose absolute value is less than or equal to zero_threshold. + pub fn zero_count(&self) -> u64 { + self.zero_count + } + + /// Returns the range of positive value bucket counts. + pub fn positive_bucket(&self) -> &ExponentialBucket { + &self.positive_bucket + } + + /// Returns the range of negative value bucket counts. + pub fn negative_bucket(&self) -> &ExponentialBucket { + &self.negative_bucket + } + + /// Returns the width of the zero region. + pub fn zero_threshold(&self) -> f64 { + self.zero_threshold + } +} + +impl ExponentialHistogramDataPoint { + /// Returns the minimum value recorded. + pub fn min(&self) -> Option { + self.min + } + + /// Returns the maximum value recorded. + pub fn max(&self) -> Option { + self.max + } + + /// Returns the sum of the values recorded. + pub fn sum(&self) -> T { + self.sum + } +} + +/// A set of bucket counts, encoded in a contiguous array of counts. +#[derive(Debug, Clone, PartialEq)] +pub struct ExponentialBucket { + /// The bucket index of the first entry in the `counts` vec. + pub(crate) offset: i32, + + /// A vec where `counts[i]` carries the count of the bucket at index `offset + i`. + /// + /// `counts[i]` is the count of values greater than base^(offset+i) and less than + /// or equal to base^(offset+i+1). + pub(crate) counts: Vec, +} + +impl ExponentialBucket { + /// Returns the bucket index of the first entry in the counts vec. + pub fn offset(&self) -> i32 { + self.offset + } + + /// Returns an iterator over the counts. + pub fn counts(&self) -> impl Iterator + '_ { + self.counts.iter().copied() + } +} + +/// A measurement sampled from a time series providing a typical example. +#[derive(Debug, Clone, PartialEq)] +pub struct Exemplar { + /// The attributes recorded with the measurement but filtered out of the + /// time series' aggregated data. + pub(crate) filtered_attributes: Vec, + /// The time when the measurement was recorded. + pub(crate) time: SystemTime, + /// The measured value. + pub value: T, + /// The ID of the span that was active during the measurement. + /// + /// If no span was active or the span was not sampled this will be empty. + pub(crate) span_id: [u8; 8], + /// The ID of the trace the active span belonged to during the measurement. + /// + /// If no span was active or the span was not sampled this will be empty. + pub(crate) trace_id: [u8; 16], +} + +impl Exemplar { + /// Returns an iterator over the filtered attributes in [Exemplar]. + pub fn filtered_attributes(&self) -> impl Iterator { + self.filtered_attributes.iter() + } + + /// Returns the time when the measurement was recorded. + pub fn time(&self) -> SystemTime { + self.time + } + + /// Returns the ID of the span that was active during the measurement. + pub fn span_id(&self) -> &[u8; 8] { + &self.span_id + } + + /// Returns the ID of the trace the active span belonged to during the measurement. + pub fn trace_id(&self) -> &[u8; 16] { + &self.trace_id + } +} + +#[cfg(test)] +mod tests { + + use super::{Exemplar, ExponentialHistogramDataPoint, HistogramDataPoint, SumDataPoint}; + + use opentelemetry::time::now; + use opentelemetry::KeyValue; + + #[test] + fn validate_cloning_data_points() { + let data_type = SumDataPoint { + attributes: vec![KeyValue::new("key", "value")], + value: 0u32, + exemplars: vec![Exemplar { + filtered_attributes: vec![], + time: now(), + value: 0u32, + span_id: [0; 8], + trace_id: [0; 16], + }], + }; + assert_eq!(data_type.clone(), data_type); + + let histogram_data_point = HistogramDataPoint { + attributes: vec![KeyValue::new("key", "value")], + count: 0, + bounds: vec![], + bucket_counts: vec![], + min: None, + max: None, + sum: 0u32, + exemplars: vec![Exemplar { + filtered_attributes: vec![], + time: now(), + value: 0u32, + span_id: [0; 8], + trace_id: [0; 16], + }], + }; + assert_eq!(histogram_data_point.clone(), histogram_data_point); + + let exponential_histogram_data_point = ExponentialHistogramDataPoint { + attributes: vec![KeyValue::new("key", "value")], + count: 0, + min: None, + max: None, + sum: 0u32, + scale: 0, + zero_count: 0, + positive_bucket: super::ExponentialBucket { + offset: 0, + counts: vec![], + }, + negative_bucket: super::ExponentialBucket { + offset: 0, + counts: vec![], + }, + zero_threshold: 0.0, + exemplars: vec![Exemplar { + filtered_attributes: vec![], + time: now(), + value: 0u32, + span_id: [0; 8], + trace_id: [0; 16], + }], + }; + assert_eq!( + exponential_histogram_data_point.clone(), + exponential_histogram_data_point + ); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/error.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/error.rs new file mode 100644 index 00000000..cb2e082a --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/error.rs @@ -0,0 +1,28 @@ +use std::result; +use std::sync::PoisonError; +use thiserror::Error; + +/// A specialized `Result` type for metric operations. +pub(crate) type MetricResult = result::Result; + +/// Errors returned by the metrics API. +#[derive(Error, Debug)] +pub(crate) enum MetricError { + /// Other errors not covered by specific cases. + #[error("Metrics error: {0}")] + Other(String), + /// Invalid configuration + #[error("Config error {0}")] + Config(String), + /// Invalid instrument configuration such invalid instrument name, invalid instrument description, invalid instrument unit, etc. + /// See [spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#general-characteristics) + /// for full list of requirements. + #[error("Invalid instrument configuration: {0}")] + InvalidInstrumentConfiguration(&'static str), +} + +impl From> for MetricError { + fn from(err: PoisonError) -> Self { + MetricError::Other(err.to_string()) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/exporter.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/exporter.rs new file mode 100644 index 00000000..c1280db7 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/exporter.rs @@ -0,0 +1,40 @@ +//! Interfaces for exporting metrics + +use crate::error::OTelSdkResult; +use std::time::Duration; + +use crate::metrics::data::ResourceMetrics; + +use super::Temporality; + +/// Exporter handles the delivery of metric data to external receivers. +/// +/// This is the final component in the metric push pipeline. +pub trait PushMetricExporter: Send + Sync + 'static { + /// Export serializes and transmits metric data to a receiver. + /// + /// All retry logic must be contained in this function. The SDK does not + /// implement any retry logic. All errors returned by this function are + /// considered unrecoverable and will be logged. + fn export( + &self, + metrics: &ResourceMetrics, + ) -> impl std::future::Future + Send; + + /// Flushes any metric data held by an exporter. + fn force_flush(&self) -> OTelSdkResult; + + /// Releases any held computational resources. + /// + /// After Shutdown is called, calls to Export will perform no operation and + /// instead will return an error indicating the shutdown state. + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult; + + /// Shutdown with the default timeout of 5 seconds. + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } + + /// Access the [Temporality] of the MetricExporter. + fn temporality(&self) -> Temporality; +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/in_memory_exporter.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/in_memory_exporter.rs new file mode 100644 index 00000000..a25da312 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/in_memory_exporter.rs @@ -0,0 +1,263 @@ +use crate::error::{OTelSdkError, OTelSdkResult}; +use crate::metrics::data::{ + ExponentialHistogram, Gauge, Histogram, MetricData, ResourceMetrics, Sum, +}; +use crate::metrics::exporter::PushMetricExporter; +use crate::metrics::Temporality; +use std::collections::VecDeque; +use std::fmt; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use super::data::{AggregatedMetrics, Metric, ScopeMetrics}; + +/// An in-memory metrics exporter that stores metrics data in memory. +/// +/// This exporter is useful for testing and debugging purposes. It stores +/// metric data in a `VecDeque`. Metrics can be retrieved +/// using the `get_finished_metrics` method. +/// +/// # Panics +/// +/// This exporter may panic +/// - if there's an issue with locking the `metrics` Mutex, such as if the Mutex is poisoned. +/// - the data point recorded is not one of [i64, u64, f64]. This shouldn't happen if used with OpenTelemetry API. +/// +/// # Example +/// +/// ``` +///# use opentelemetry_sdk::metrics; +///# use opentelemetry::{KeyValue}; +///# use opentelemetry::metrics::MeterProvider; +///# use opentelemetry_sdk::metrics::InMemoryMetricExporter; +///# use opentelemetry_sdk::metrics::PeriodicReader; +/// +///# #[tokio::main] +///# async fn main() { +/// // Create an InMemoryMetricExporter +/// let exporter = InMemoryMetricExporter::default(); +/// +/// // Create a MeterProvider and register the exporter +/// let meter_provider = metrics::SdkMeterProvider::builder() +/// .with_reader(PeriodicReader::builder(exporter.clone()).build()) +/// .build(); +/// +/// // Create and record metrics using the MeterProvider +/// let meter = meter_provider.meter("example"); +/// let counter = meter.u64_counter("my_counter").build(); +/// counter.add(1, &[KeyValue::new("key", "value")]); +/// +/// meter_provider.force_flush().unwrap(); +/// +/// // Retrieve the finished metrics from the exporter +/// let finished_metrics = exporter.get_finished_metrics().unwrap(); +/// +/// // Print the finished metrics +/// for resource_metrics in finished_metrics { +/// println!("{:?}", resource_metrics); +/// } +///# } +/// ``` +pub struct InMemoryMetricExporter { + metrics: Arc>>, + temporality: Temporality, +} + +impl Clone for InMemoryMetricExporter { + fn clone(&self) -> Self { + InMemoryMetricExporter { + metrics: self.metrics.clone(), + temporality: self.temporality, + } + } +} + +impl fmt::Debug for InMemoryMetricExporter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InMemoryMetricExporter").finish() + } +} + +impl Default for InMemoryMetricExporter { + fn default() -> Self { + InMemoryMetricExporterBuilder::new().build() + } +} + +/// Builder for [`InMemoryMetricExporter`]. +/// # Example +/// +/// ``` +/// # use opentelemetry_sdk::metrics::{InMemoryMetricExporter, InMemoryMetricExporterBuilder}; +/// +/// let exporter = InMemoryMetricExporterBuilder::new().build(); +/// ``` +pub struct InMemoryMetricExporterBuilder { + temporality: Option, +} + +impl fmt::Debug for InMemoryMetricExporterBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InMemoryMetricExporterBuilder").finish() + } +} + +impl Default for InMemoryMetricExporterBuilder { + fn default() -> Self { + Self::new() + } +} + +impl InMemoryMetricExporterBuilder { + /// Creates a new instance of the `InMemoryMetricExporterBuilder`. + pub fn new() -> Self { + Self { temporality: None } + } + + /// Set the [Temporality] of the exporter. + pub fn with_temporality(mut self, temporality: Temporality) -> Self { + self.temporality = Some(temporality); + self + } + + /// Creates a new instance of the `InMemoryMetricExporter`. + /// + pub fn build(self) -> InMemoryMetricExporter { + InMemoryMetricExporter { + metrics: Arc::new(Mutex::new(VecDeque::new())), + temporality: self.temporality.unwrap_or_default(), + } + } +} + +impl InMemoryMetricExporter { + /// Returns the finished metrics as a vector of `ResourceMetrics`. + /// + /// # Errors + /// + /// Returns an `OTelSdkError`. + /// + /// # Example + /// + /// ``` + /// # use opentelemetry_sdk::metrics::InMemoryMetricExporter; + /// + /// let exporter = InMemoryMetricExporter::default(); + /// let finished_metrics = exporter.get_finished_metrics().unwrap(); + /// ``` + pub fn get_finished_metrics(&self) -> Result, OTelSdkError> { + let metrics = self + .metrics + .lock() + .map(|metrics_guard| metrics_guard.iter().map(Self::clone_metrics).collect()) + .map_err(OTelSdkError::from)?; + Ok(metrics) + } + + /// Clears the internal storage of finished metrics. + /// + /// # Example + /// + /// ``` + /// # use opentelemetry_sdk::metrics::InMemoryMetricExporter; + /// + /// let exporter = InMemoryMetricExporter::default(); + /// exporter.reset(); + /// ``` + pub fn reset(&self) { + let _ = self + .metrics + .lock() + .map(|mut metrics_guard| metrics_guard.clear()); + } + + fn clone_metrics(metric: &ResourceMetrics) -> ResourceMetrics { + ResourceMetrics { + resource: metric.resource.clone(), + scope_metrics: metric + .scope_metrics + .iter() + .map(|scope_metric| ScopeMetrics { + scope: scope_metric.scope.clone(), + metrics: scope_metric + .metrics + .iter() + .map(|metric| Metric { + name: metric.name.clone(), + description: metric.description.clone(), + unit: metric.unit.clone(), + data: Self::clone_data(&metric.data), + }) + .collect(), + }) + .collect(), + } + } + + fn clone_data(data: &AggregatedMetrics) -> AggregatedMetrics { + fn clone_inner(data: &MetricData) -> MetricData { + match data { + MetricData::Gauge(gauge) => Gauge { + data_points: gauge.data_points.clone(), + start_time: gauge.start_time, + time: gauge.time, + } + .into(), + MetricData::Sum(sum) => Sum { + data_points: sum.data_points.clone(), + start_time: sum.start_time, + time: sum.time, + temporality: sum.temporality, + is_monotonic: sum.is_monotonic, + } + .into(), + MetricData::Histogram(histogram) => Histogram { + data_points: histogram.data_points.clone(), + start_time: histogram.start_time, + time: histogram.time, + temporality: histogram.temporality, + } + .into(), + MetricData::ExponentialHistogram(exponential_histogram) => ExponentialHistogram { + data_points: exponential_histogram.data_points.clone(), + start_time: exponential_histogram.start_time, + time: exponential_histogram.time, + temporality: exponential_histogram.temporality, + } + .into(), + } + } + match data { + AggregatedMetrics::F64(metric_data) => AggregatedMetrics::F64(clone_inner(metric_data)), + AggregatedMetrics::U64(metric_data) => AggregatedMetrics::U64(clone_inner(metric_data)), + AggregatedMetrics::I64(metric_data) => AggregatedMetrics::I64(clone_inner(metric_data)), + } + } +} + +impl PushMetricExporter for InMemoryMetricExporter { + async fn export(&self, metrics: &ResourceMetrics) -> OTelSdkResult { + self.metrics + .lock() + .map(|mut metrics_guard| { + metrics_guard.push_back(InMemoryMetricExporter::clone_metrics(metrics)) + }) + .map_err(|_| OTelSdkError::InternalFailure("Failed to lock metrics".to_string())) + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) // In this implementation, flush does nothing + } + + fn shutdown(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + Ok(()) + } + + fn temporality(&self) -> Temporality { + self.temporality + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/instrument.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/instrument.rs new file mode 100644 index 00000000..78c19312 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/instrument.rs @@ -0,0 +1,804 @@ +use std::{borrow::Cow, collections::HashSet, error::Error, sync::Arc}; + +#[cfg(feature = "experimental_metrics_bound_instruments")] +use opentelemetry::metrics::BoundSyncInstrument; +use opentelemetry::{ + metrics::{AsyncInstrument, SyncInstrument}, + InstrumentationScope, Key, KeyValue, +}; + +#[cfg(feature = "experimental_metrics_bound_instruments")] +use crate::metrics::internal::BoundMeasure; +use crate::metrics::{aggregation::Aggregation, internal::Measure}; + +use super::meter::{INSTRUMENT_UNIT_INVALID_CHAR, INSTRUMENT_UNIT_LENGTH}; + +use super::Temporality; + +/// The identifier of a group of instruments that all perform the same function. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub enum InstrumentKind { + /// Identifies a group of instruments that record increasing values synchronously + /// with the code path they are measuring. + Counter, + /// A group of instruments that record increasing and decreasing values + /// synchronously with the code path they are measuring. + UpDownCounter, + /// A group of instruments that record a distribution of values synchronously with + /// the code path they are measuring. + Histogram, + /// A group of instruments that record increasing values in an asynchronous + /// callback. + ObservableCounter, + /// A group of instruments that record increasing and decreasing values in an + /// asynchronous callback. + ObservableUpDownCounter, + + /// a group of instruments that record current value synchronously with + /// the code path they are measuring. + Gauge, + /// + /// a group of instruments that record current values in an asynchronous callback. + ObservableGauge, +} + +impl InstrumentKind { + /// Select the [Temporality] preference based on [InstrumentKind] + /// + /// [exporter-docs]: https://github.com/open-telemetry/opentelemetry-specification/blob/a1c13d59bb7d0fb086df2b3e1eaec9df9efef6cc/specification/metrics/sdk_exporters/otlp.md#additional-configuration + pub(crate) fn temporality_preference(&self, temporality: Temporality) -> Temporality { + match temporality { + Temporality::Cumulative => Temporality::Cumulative, + Temporality::Delta => match self { + Self::Counter + | Self::Histogram + | Self::ObservableCounter + | Self::Gauge + | Self::ObservableGauge => Temporality::Delta, + Self::UpDownCounter | InstrumentKind::ObservableUpDownCounter => { + Temporality::Cumulative + } + }, + Temporality::LowMemory => match self { + Self::Counter | InstrumentKind::Histogram => Temporality::Delta, + Self::ObservableCounter + | Self::Gauge + | Self::ObservableGauge + | Self::UpDownCounter + | Self::ObservableUpDownCounter => Temporality::Cumulative, + }, + } + } +} +/// Describes the properties of an instrument at creation, used for filtering in +/// views. This is utilized in the `with_view` methods on `MeterProviderBuilder` +/// to customize metric output. +/// +/// Users can use a reference to `Instrument` to select which instrument(s) a +/// [Stream] should be applied to. +/// +/// # Example +/// +/// ```rust +/// use opentelemetry_sdk::metrics::{Instrument, Stream}; +/// +/// let my_view_change_cardinality = |i: &Instrument| { +/// if i.name() == "my_second_histogram" { +/// // Note: If Stream is invalid, `build()` will return an error. By +/// // calling `.ok()`, any such error is ignored and treated as if the +/// // view does not match the instrument. If this is not the desired +/// // behavior, consider handling the error explicitly. +/// Stream::builder().with_cardinality_limit(2).build().ok() +/// } else { +/// None +/// } +/// }; +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub struct Instrument { + /// The human-readable identifier of the instrument. + pub(crate) name: Cow<'static, str>, + /// describes the purpose of the instrument. + pub(crate) description: Cow<'static, str>, + /// The functional group of the instrument. + pub(crate) kind: InstrumentKind, + /// Unit is the unit of measurement recorded by the instrument. + pub(crate) unit: Cow<'static, str>, + /// The instrumentation that created the instrument. + pub(crate) scope: InstrumentationScope, +} + +impl Instrument { + /// Instrument name. + pub fn name(&self) -> &str { + self.name.as_ref() + } + + /// Instrument kind. + pub fn kind(&self) -> InstrumentKind { + self.kind + } + + /// Instrument unit. + pub fn unit(&self) -> &str { + self.unit.as_ref() + } + + /// Instrument scope. + pub fn scope(&self) -> &InstrumentationScope { + &self.scope + } +} + +/// A builder for creating Stream objects. +/// +/// # Example +/// +/// ```rust +/// use opentelemetry_sdk::metrics::Stream; +/// +/// let stream = Stream::builder() +/// .with_name("my_stream") +/// .with_description("A custom stream") +/// .with_unit("ms") +/// .with_cardinality_limit(100) +/// .build() +/// .unwrap(); +/// ``` +#[derive(Default, Debug)] +pub struct StreamBuilder { + name: Option>, + description: Option>, + unit: Option>, + aggregation: Option, + allowed_attribute_keys: Option>>, + cardinality_limit: Option, +} + +impl StreamBuilder { + /// Create a new stream builder with default values. + pub(crate) fn new() -> Self { + StreamBuilder::default() + } + + /// Set the stream name. If this is not set, name provide while creating the instrument will be used. + pub fn with_name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + /// Set the stream description. If this is not set, description provided while creating the instrument will be used. + pub fn with_description(mut self, description: impl Into>) -> Self { + self.description = Some(description.into()); + self + } + + /// Set the stream unit. If this is not set, unit provided while creating the instrument will be used. + pub fn with_unit(mut self, unit: impl Into>) -> Self { + self.unit = Some(unit.into()); + self + } + + /// Set the stream aggregation. This is used to customize the aggregation. + /// If not set, the default aggregation based on the instrument kind will be used. + pub fn with_aggregation(mut self, aggregation: Aggregation) -> Self { + self.aggregation = Some(aggregation); + self + } + + #[cfg(feature = "spec_unstable_metrics_views")] + /// Set the stream allowed attribute keys. + /// + /// Any attribute recorded for the stream with a key not in this set will be + /// dropped. If the set is empty, all attributes will be dropped. + /// If this method is not used, all attributes will be kept. + pub fn with_allowed_attribute_keys( + mut self, + attribute_keys: impl IntoIterator, + ) -> Self { + self.allowed_attribute_keys = Some(Arc::new(attribute_keys.into_iter().collect())); + self + } + + /// Set the stream cardinality limit. If this is not set, the default limit of 2000 will be used. + pub fn with_cardinality_limit(mut self, limit: usize) -> Self { + self.cardinality_limit = Some(limit); + self + } + + /// Build a new Stream instance using the configuration in this builder. + /// + /// # Returns + /// + /// A Result containing the new Stream instance or an error if the build failed. + pub fn build(self) -> Result> { + // Note: Per the OpenTelemetry specification, the View-provided stream + // `name` is NOT subject to the instrument name syntax, and the SDK + // MUST NOT validate it against that syntax. This allows Views to be + // used to fix instrumentation mistakes (e.g. renaming an instrument + // whose original name does not satisfy the syntax) and to support + // export targets that have different naming requirements. + // See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view + + // Validate unit if provided + if let Some(unit) = &self.unit { + if unit.len() > super::meter::INSTRUMENT_UNIT_NAME_MAX_LENGTH { + return Err(INSTRUMENT_UNIT_LENGTH.into()); + } + + if unit.contains(|c: char| !c.is_ascii()) { + return Err(INSTRUMENT_UNIT_INVALID_CHAR.into()); + } + } + + // Validate cardinality limit + if let Some(limit) = self.cardinality_limit { + if limit == 0 { + return Err("Cardinality limit must be greater than 0".into()); + } + // Reject usize::MAX because the SDK's internal HashMap capacity + // is sized as `1 + cardinality_limit`, which would overflow. + if limit == usize::MAX { + return Err("Cardinality limit must be less than usize::MAX".into()); + } + } + + // Validate bucket boundaries if using ExplicitBucketHistogram + if let Some(Aggregation::ExplicitBucketHistogram { boundaries, .. }) = &self.aggregation { + validate_bucket_boundaries(boundaries)?; + } + + // Validate parameters if using Base2ExponentialHistogram + if let Some(Aggregation::Base2ExponentialHistogram { + max_size, + max_scale, + .. + }) = &self.aggregation + { + if *max_size == 0 { + return Err("max_size must be greater than 0".into()); + } + if *max_scale < super::internal::EXPO_MIN_SCALE + || *max_scale > super::internal::EXPO_MAX_SCALE + { + return Err(format!( + "max_scale must be between {} and {}", + super::internal::EXPO_MIN_SCALE, + super::internal::EXPO_MAX_SCALE + ) + .into()); + } + } + + Ok(Stream { + name: self.name, + description: self.description, + unit: self.unit, + aggregation: self.aggregation, + allowed_attribute_keys: self.allowed_attribute_keys, + cardinality_limit: self.cardinality_limit, + }) + } +} + +fn validate_bucket_boundaries(boundaries: &[f64]) -> Result<(), String> { + // Validate boundaries do not contain f64::NAN, f64::INFINITY, or f64::NEG_INFINITY + for boundary in boundaries { + if boundary.is_nan() || boundary.is_infinite() { + return Err( + "Bucket boundaries must not contain NaN, Infinity, or -Infinity".to_string(), + ); + } + } + + // validate that buckets are sorted and non-duplicate + for i in 1..boundaries.len() { + if boundaries[i] <= boundaries[i - 1] { + return Err( + "Bucket boundaries must be sorted and not contain any duplicates".to_string(), + ); + } + } + + Ok(()) +} + +/// Describes the stream of data an instrument produces. Used in `with_view` +/// methods on `MeterProviderBuilder` to customize the metric output. +#[derive(Default, Debug)] +pub struct Stream { + /// The human-readable identifier of the stream. + pub(crate) name: Option>, + /// Describes the purpose of the data. + pub(crate) description: Option>, + /// the unit of measurement recorded. + pub(crate) unit: Option>, + /// Aggregation the stream uses for an instrument. + pub(crate) aggregation: Option, + /// An allow-list of attribute keys that will be preserved for the stream. + /// + /// Any attribute recorded for the stream with a key not in this set will be + /// dropped. If the set is empty, all attributes will be dropped, if `None` all + /// attributes will be kept. + pub(crate) allowed_attribute_keys: Option>>, + + /// Cardinality limit for the stream. + pub(crate) cardinality_limit: Option, +} + +impl Stream { + /// Create a new stream builder with default values. + pub fn builder() -> StreamBuilder { + StreamBuilder::new() + } +} + +/// The identifying properties of an instrument. +#[derive(Debug, PartialEq, Eq, Hash)] +pub(crate) struct InstrumentId { + /// The human-readable identifier of the instrument. + pub(crate) name: Cow<'static, str>, + /// Describes the purpose of the data. + pub(crate) description: Cow<'static, str>, + /// Defines the functional group of the instrument. + pub(crate) kind: InstrumentKind, + /// The unit of measurement recorded. + pub(crate) unit: Cow<'static, str>, + /// Number is the underlying data type of the instrument. + pub(crate) number: Cow<'static, str>, +} + +impl InstrumentId { + /// Instrument names are considered case-insensitive ASCII. + /// + /// Standardize the instrument name to always be lowercase so it can be compared + /// via hash. + /// + /// See [naming syntax] for full requirements. + /// + /// [naming syntax]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.21.0/specification/metrics/api.md#instrument-name-syntax + pub(crate) fn normalize(&mut self) { + if self.name.chars().any(|c| c.is_ascii_uppercase()) { + self.name = self.name.to_ascii_lowercase().into(); + } + } +} + +pub(crate) struct ResolvedMeasures { + pub(crate) measures: Vec>>, +} + +impl SyncInstrument for ResolvedMeasures { + fn measure(&self, val: T, attrs: &[KeyValue]) { + for measure in &self.measures { + measure.call(val, attrs) + } + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind(&self, attrs: &[KeyValue]) -> Box + Send + Sync> { + let bound_measures: Vec>> = + self.measures.iter().map(|m| m.bind(attrs)).collect(); + Box::new(ResolvedBoundMeasures { + measures: bound_measures, + }) + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +pub(crate) struct ResolvedBoundMeasures { + measures: Vec>>, +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundSyncInstrument for ResolvedBoundMeasures { + fn measure(&self, val: T) { + for measure in &self.measures { + measure.call(val); + } + } +} + +#[derive(Clone)] +pub(crate) struct Observable { + measures: Vec>>, +} + +impl Observable { + pub(crate) fn new(measures: Vec>>) -> Self { + Self { measures } + } +} + +impl AsyncInstrument for Observable { + fn observe(&self, measurement: T, attrs: &[KeyValue]) { + for measure in &self.measures { + measure.call(measurement, attrs) + } + } +} + +#[cfg(test)] +mod tests { + use super::StreamBuilder; + use crate::metrics::meter::{INSTRUMENT_UNIT_INVALID_CHAR, INSTRUMENT_UNIT_LENGTH}; + + #[test] + fn stream_name_no_validation() { + // Per the OpenTelemetry specification, View-provided stream names are + // NOT subject to the instrument name syntax. The SDK MUST NOT validate + // the View-provided name against that syntax. All names below — which + // would be rejected for direct instrument creation — MUST be accepted + // when supplied via a View/Stream. + let stream_names_all_accepted = vec![ + "validateName", + "_startWithNoneAlphabet", + "utf8char锈", + "a".repeat(255).leak(), + "a".repeat(256).leak(), + "invalid name", + "allow/slash", + "allow_under_score", + "allow.dots.ok", + "", + "\\allow\\slash /sec", + "\\allow\\$$slash /sec", + "Total $ Count", + "\\test\\UsagePercent(Total) > 80%", + "/not / allowed", + ]; + + for name in stream_names_all_accepted { + let result = StreamBuilder::new().with_name(name).build(); + assert!( + result.is_ok(), + "Expected View-provided stream name '{}' to be accepted without validation, but got error: {:?}", + name, + result.err() + ); + } + } + + #[test] + fn stream_unit_validation() { + // (unit, expected error) + let stream_unit_test_cases = vec![ + ( + "0123456789012345678901234567890123456789012345678901234567890123", + INSTRUMENT_UNIT_LENGTH, + ), + ("utf8char锈", INSTRUMENT_UNIT_INVALID_CHAR), + ("kb", ""), + ("Kb/sec", ""), + ("%", ""), + ("", ""), + ]; + + for (unit, expected_error) in stream_unit_test_cases { + // Use a valid name to isolate unit validation + let builder = StreamBuilder::new().with_name("valid_name").with_unit(unit); + + let result = builder.build(); + + if expected_error.is_empty() { + assert!( + result.is_ok(), + "Expected successful build for unit '{}', but got error: {:?}", + unit, + result.err() + ); + } else { + let err = result.err().unwrap(); + let err_str = err.to_string(); + assert!( + err_str == expected_error, + "For unit '{unit}', expected error '{expected_error}', but got '{err_str}'" + ); + } + } + } + + #[test] + fn stream_cardinality_limit_validation() { + // Test zero cardinality limit (invalid) + let builder = StreamBuilder::new() + .with_name("valid_name") + .with_cardinality_limit(0); + + let result = builder.build(); + assert!(result.is_err(), "Expected error for zero cardinality limit"); + assert_eq!( + result.err().unwrap().to_string(), + "Cardinality limit must be greater than 0", + "Expected cardinality limit validation error message" + ); + + // Test usize::MAX (invalid — would overflow internal `1 + limit` capacity) + let builder = StreamBuilder::new() + .with_name("valid_name") + .with_cardinality_limit(usize::MAX); + + let result = builder.build(); + assert!( + result.is_err(), + "Expected error for usize::MAX cardinality limit" + ); + assert_eq!( + result.err().unwrap().to_string(), + "Cardinality limit must be less than usize::MAX", + "Expected cardinality limit usize::MAX error message" + ); + + // Test valid cardinality limits + let valid_limits = vec![1, 10, 100, 1000, usize::MAX - 1]; + for limit in valid_limits { + let builder = StreamBuilder::new() + .with_name("valid_name") + .with_cardinality_limit(limit); + + let result = builder.build(); + assert!( + result.is_ok(), + "Expected successful build for cardinality limit {}, but got error: {:?}", + limit, + result.err() + ); + } + } + + #[test] + fn stream_valid_build() { + // Test with valid configuration + let stream = StreamBuilder::new() + .with_name("valid_name") + .with_description("Valid description") + .with_unit("ms") + .with_cardinality_limit(100) + .build(); + + assert!( + stream.is_ok(), + "Expected valid Stream to be built successfully" + ); + } + + #[test] + fn stream_histogram_bucket_validation() { + use super::Aggregation; + + // Test with valid bucket boundaries + let valid_boundaries = vec![1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0]; + let builder = StreamBuilder::new() + .with_name("valid_histogram") + .with_aggregation(Aggregation::ExplicitBucketHistogram { + boundaries: valid_boundaries.clone(), + record_min_max: true, + }); + + let result = builder.build(); + assert!( + result.is_ok(), + "Expected successful build with valid bucket boundaries" + ); + + // Test with invalid bucket boundaries (NaN and Infinity) + + // Test with NaN + let invalid_nan_boundaries = vec![1.0, 2.0, f64::NAN, 10.0]; + + let builder = StreamBuilder::new() + .with_name("invalid_histogram_nan") + .with_aggregation(Aggregation::ExplicitBucketHistogram { + boundaries: invalid_nan_boundaries, + record_min_max: true, + }); + + let result = builder.build(); + assert!( + result.is_err(), + "Expected error for NaN in bucket boundaries" + ); + assert_eq!( + result.err().unwrap().to_string(), + "Bucket boundaries must not contain NaN, Infinity, or -Infinity", + "Expected correct validation error for NaN" + ); + + // Test with infinity + let invalid_inf_boundaries = vec![1.0, 5.0, f64::INFINITY, 100.0]; + + let builder = StreamBuilder::new() + .with_name("invalid_histogram_inf") + .with_aggregation(Aggregation::ExplicitBucketHistogram { + boundaries: invalid_inf_boundaries, + record_min_max: true, + }); + + let result = builder.build(); + assert!( + result.is_err(), + "Expected error for Infinity in bucket boundaries" + ); + assert_eq!( + result.err().unwrap().to_string(), + "Bucket boundaries must not contain NaN, Infinity, or -Infinity", + "Expected correct validation error for Infinity" + ); + + // Test with negative infinity + let invalid_neg_inf_boundaries = vec![f64::NEG_INFINITY, 5.0, 10.0, 100.0]; + + let builder = StreamBuilder::new() + .with_name("invalid_histogram_neg_inf") + .with_aggregation(Aggregation::ExplicitBucketHistogram { + boundaries: invalid_neg_inf_boundaries, + record_min_max: true, + }); + + let result = builder.build(); + assert!( + result.is_err(), + "Expected error for negative Infinity in bucket boundaries" + ); + assert_eq!( + result.err().unwrap().to_string(), + "Bucket boundaries must not contain NaN, Infinity, or -Infinity", + "Expected correct validation error for negative Infinity" + ); + + // Test with unsorted bucket boundaries + let unsorted_boundaries = vec![1.0, 5.0, 2.0, 10.0]; // 2.0 comes after 5.0, which is incorrect + + let builder = StreamBuilder::new() + .with_name("unsorted_histogram") + .with_aggregation(Aggregation::ExplicitBucketHistogram { + boundaries: unsorted_boundaries, + record_min_max: true, + }); + + let result = builder.build(); + assert!( + result.is_err(), + "Expected error for unsorted bucket boundaries" + ); + assert_eq!( + result.err().unwrap().to_string(), + "Bucket boundaries must be sorted and not contain any duplicates", + "Expected correct validation error for unsorted boundaries" + ); + + // Test with duplicate bucket boundaries + let duplicate_boundaries = vec![1.0, 2.0, 5.0, 5.0, 10.0]; // 5.0 appears twice + + let builder = StreamBuilder::new() + .with_name("duplicate_histogram") + .with_aggregation(Aggregation::ExplicitBucketHistogram { + boundaries: duplicate_boundaries, + record_min_max: true, + }); + + let result = builder.build(); + assert!( + result.is_err(), + "Expected error for duplicate bucket boundaries" + ); + assert_eq!( + result.err().unwrap().to_string(), + "Bucket boundaries must be sorted and not contain any duplicates", + "Expected correct validation error for duplicate boundaries" + ); + } + + #[test] + fn stream_exponential_histogram_validation() { + use super::Aggregation; + use crate::metrics::internal::{EXPO_MAX_SCALE, EXPO_MIN_SCALE}; + + // Test with valid parameters + let builder = StreamBuilder::new() + .with_name("valid_expo_histogram") + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: 10, + record_min_max: true, + }); + + let result = builder.build(); + assert!( + result.is_ok(), + "Expected successful build with valid exponential histogram parameters" + ); + + // Test with max_size = 0 (invalid) + let builder = StreamBuilder::new() + .with_name("invalid_expo_histogram_size") + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 0, + max_scale: 10, + record_min_max: true, + }); + + let result = builder.build(); + assert!(result.is_err(), "Expected error for max_size = 0"); + assert_eq!( + result.err().unwrap().to_string(), + "max_size must be greater than 0", + "Expected correct validation error for max_size = 0" + ); + + // Test with max_scale too high (invalid) + let builder = StreamBuilder::new() + .with_name("invalid_expo_histogram_scale_high") + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: EXPO_MAX_SCALE + 1, + record_min_max: true, + }); + + let result = builder.build(); + assert!( + result.is_err(), + "Expected error for max_scale > EXPO_MAX_SCALE" + ); + assert_eq!( + result.err().unwrap().to_string(), + format!( + "max_scale must be between {} and {}", + EXPO_MIN_SCALE, EXPO_MAX_SCALE + ), + "Expected correct validation error for max_scale too high" + ); + + // Test with max_scale too low (invalid) + let builder = StreamBuilder::new() + .with_name("invalid_expo_histogram_scale_low") + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: EXPO_MIN_SCALE - 1, + record_min_max: true, + }); + + let result = builder.build(); + assert!( + result.is_err(), + "Expected error for max_scale < EXPO_MIN_SCALE" + ); + assert_eq!( + result.err().unwrap().to_string(), + format!( + "max_scale must be between {} and {}", + EXPO_MIN_SCALE, EXPO_MAX_SCALE + ), + "Expected correct validation error for max_scale too low" + ); + + // Test with boundary values of max_scale (valid) + let builder = StreamBuilder::new() + .with_name("valid_expo_histogram_min_scale") + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: EXPO_MIN_SCALE, + record_min_max: true, + }); + + let result = builder.build(); + assert!( + result.is_ok(), + "Expected successful build with max_scale = EXPO_MIN_SCALE" + ); + + let builder = StreamBuilder::new() + .with_name("valid_expo_histogram_max_scale") + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: EXPO_MAX_SCALE, + record_min_max: true, + }); + + let result = builder.build(); + assert!( + result.is_ok(), + "Expected successful build with max_scale = EXPO_MAX_SCALE" + ); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/aggregate.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/aggregate.rs new file mode 100644 index 00000000..18961685 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/aggregate.rs @@ -0,0 +1,494 @@ +use std::{ + marker, + mem::replace, + ops::DerefMut, + sync::{Arc, Mutex}, + time::SystemTime, +}; + +use crate::metrics::{data::AggregatedMetrics, Temporality}; +use opentelemetry::time::now; +use opentelemetry::KeyValue; + +use super::{ + exponential_histogram::ExpoHistogram, histogram::Histogram, last_value::LastValue, + precomputed_sum::PrecomputedSum, sum::Sum, Number, +}; + +/// Receives measurements to be aggregated. +pub(crate) trait Measure: Send + Sync + 'static { + fn call(&self, measurement: T, attrs: &[KeyValue]); + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind(&self, attrs: &[KeyValue]) -> Box>; +} + +/// A pre-bound measurement handle that bypasses attribute lookup. +#[cfg(feature = "experimental_metrics_bound_instruments")] +pub(crate) trait BoundMeasure: Send + Sync + 'static { + fn call(&self, measurement: T); +} + +/// A bound handle that drops every measurement silently. Used when +/// `ValueMap::bind` returns `None` because the trackers `RwLock` is poisoned — +/// an extremely rare degenerate state in which the SDK can no longer aggregate +/// reliably. Returning a noop here mirrors `measure()`'s own poison handling +/// (silent drop) rather than panicking on the user's hot path. +#[cfg(feature = "experimental_metrics_bound_instruments")] +pub(crate) struct NoopBoundMeasure { + _marker: marker::PhantomData, +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl NoopBoundMeasure { + pub(crate) fn new() -> Self { + Self { + _marker: marker::PhantomData, + } + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundMeasure for NoopBoundMeasure { + fn call(&self, _measurement: T) {} +} + +/// Stores the aggregate of measurements into the aggregation and returns the number +/// of aggregate data-points output. +pub(crate) trait ComputeAggregation: Send + Sync + 'static { + /// Compute the new aggregation and store in `dest`. + /// + /// If no initial aggregation exists, `dest` will be `None`, in which case the + /// returned option is expected to contain a new aggregation with the data from + /// the current collection cycle. + fn call(&self, dest: Option<&mut AggregatedMetrics>) -> (usize, Option); +} + +/// Separate `measure` and `collect` functions for an aggregate. +pub(crate) struct AggregateFns { + pub(crate) measure: Arc>, + pub(crate) collect: Arc, +} + +/// Creates aggregate functions out of aggregate instance +impl From for AggregateFns +where + A: Measure + ComputeAggregation, +{ + fn from(value: A) -> Self { + let inst = Arc::new(value); + Self { + measure: inst.clone(), + collect: inst, + } + } +} + +pub(crate) struct AggregateTime { + pub start: SystemTime, + pub current: SystemTime, +} + +/// Initialized [`AggregateTime`] for specific [`Temporality`] +pub(crate) struct AggregateTimeInitiator(Mutex); + +impl AggregateTimeInitiator { + pub(crate) fn delta(&self) -> AggregateTime { + let current_time = now(); + let start_time = self + .0 + .lock() + .map(|mut start| replace(start.deref_mut(), current_time)) + .unwrap_or(current_time); + AggregateTime { + start: start_time, + current: current_time, + } + } + + pub(crate) fn cumulative(&self) -> AggregateTime { + let current_time = now(); + let start_time = self.0.lock().map(|start| *start).unwrap_or(current_time); + AggregateTime { + start: start_time, + current: current_time, + } + } +} + +impl Default for AggregateTimeInitiator { + fn default() -> Self { + Self(Mutex::new(now())) + } +} + +type Filter = Arc bool + Send + Sync>; + +/// Applies filter on provided attribute set +/// No-op, if filter is not set +#[derive(Clone)] +pub(crate) struct AttributeSetFilter { + filter: Option, +} + +impl AttributeSetFilter { + pub(crate) fn new(filter: Option) -> Self { + Self { filter } + } + + pub(crate) fn apply(&self, attrs: &[KeyValue], run: impl FnOnce(&[KeyValue])) { + if let Some(filter) = &self.filter { + let filtered_attrs: Vec = + attrs.iter().filter(|kv| filter(kv)).cloned().collect(); + run(&filtered_attrs); + } else { + run(attrs); + }; + } +} + +/// Builds aggregate functions +pub(crate) struct AggregateBuilder { + /// The temporality used for the returned aggregate functions. + temporality: Temporality, + + /// The attribute filter the aggregate function will use on the input of + /// measurements. + filter: AttributeSetFilter, + + /// Cardinality limit for the metric stream + cardinality_limit: usize, + + _marker: marker::PhantomData, +} + +impl AggregateBuilder { + pub(crate) fn new( + temporality: Temporality, + filter: Option, + cardinality_limit: usize, + ) -> Self { + AggregateBuilder { + temporality, + filter: AttributeSetFilter::new(filter), + cardinality_limit, + _marker: marker::PhantomData, + } + } + + /// Builds a last-value aggregate function input and output. + pub(crate) fn last_value(&self, overwrite_temporality: Option) -> AggregateFns { + LastValue::new( + overwrite_temporality.unwrap_or(self.temporality), + self.filter.clone(), + self.cardinality_limit, + ) + .into() + } + + /// Builds a precomputed sum aggregate function input and output. + pub(crate) fn precomputed_sum(&self, monotonic: bool) -> AggregateFns { + PrecomputedSum::new( + self.temporality, + self.filter.clone(), + monotonic, + self.cardinality_limit, + ) + .into() + } + + /// Builds a sum aggregate function input and output. + pub(crate) fn sum(&self, monotonic: bool) -> AggregateFns { + Sum::new( + self.temporality, + self.filter.clone(), + monotonic, + self.cardinality_limit, + ) + .into() + } + + /// Builds a histogram aggregate function input and output. + pub(crate) fn explicit_bucket_histogram( + &self, + boundaries: Vec, + record_min_max: bool, + record_sum: bool, + ) -> AggregateFns { + Histogram::new( + self.temporality, + self.filter.clone(), + boundaries, + record_min_max, + record_sum, + self.cardinality_limit, + ) + .into() + } + + /// Builds an exponential histogram aggregate function input and output. + pub(crate) fn exponential_bucket_histogram( + &self, + max_size: u32, + max_scale: i8, + record_min_max: bool, + record_sum: bool, + ) -> AggregateFns { + ExpoHistogram::new( + self.temporality, + self.filter.clone(), + max_size, + max_scale, + record_min_max, + record_sum, + self.cardinality_limit, + ) + .into() + } +} + +#[cfg(test)] +mod tests { + use crate::metrics::data::{ + ExponentialBucket, ExponentialHistogram, ExponentialHistogramDataPoint, Gauge, + GaugeDataPoint, Histogram, HistogramDataPoint, MetricData, Sum, SumDataPoint, + }; + use std::vec; + + use super::*; + + const CARDINALITY_LIMIT_DEFAULT: usize = 2000; + + #[test] + fn last_value_aggregation() { + let AggregateFns { measure, collect } = + AggregateBuilder::::new(Temporality::Cumulative, None, CARDINALITY_LIMIT_DEFAULT) + .last_value(None); + let mut a = MetricData::Gauge(Gauge { + data_points: vec![GaugeDataPoint { + attributes: vec![KeyValue::new("a", 1)], + value: 1u64, + exemplars: vec![], + }], + start_time: Some(now()), + time: now(), + }) + .into(); + let new_attributes = [KeyValue::new("b", 2)]; + measure.call(2, &new_attributes[..]); + + let (count, new_agg) = collect.call(Some(&mut a)); + let AggregatedMetrics::U64(MetricData::Gauge(a)) = a else { + unreachable!() + }; + + assert_eq!(count, 1); + assert!(new_agg.is_none()); + assert_eq!(a.data_points.len(), 1); + assert_eq!(a.data_points[0].attributes, new_attributes.to_vec()); + assert_eq!(a.data_points[0].value, 2); + } + + #[test] + fn precomputed_sum_aggregation() { + for temporality in [Temporality::Delta, Temporality::Cumulative] { + let AggregateFns { measure, collect } = + AggregateBuilder::::new(temporality, None, CARDINALITY_LIMIT_DEFAULT) + .precomputed_sum(true); + let mut a = MetricData::Sum(Sum { + data_points: vec![ + SumDataPoint { + attributes: vec![KeyValue::new("a1", 1)], + value: 1u64, + exemplars: vec![], + }, + SumDataPoint { + attributes: vec![KeyValue::new("a2", 1)], + value: 2u64, + exemplars: vec![], + }, + ], + start_time: now(), + time: now(), + temporality: if temporality == Temporality::Delta { + Temporality::Cumulative + } else { + Temporality::Delta + }, + is_monotonic: false, + }) + .into(); + let new_attributes = [KeyValue::new("b", 2)]; + measure.call(3, &new_attributes[..]); + + let (count, new_agg) = collect.call(Some(&mut a)); + let AggregatedMetrics::U64(MetricData::Sum(a)) = a else { + unreachable!() + }; + + assert_eq!(count, 1); + assert!(new_agg.is_none()); + assert_eq!(a.temporality, temporality); + assert!(a.is_monotonic); + assert_eq!(a.data_points.len(), 1); + assert_eq!(a.data_points[0].attributes, new_attributes.to_vec()); + assert_eq!(a.data_points[0].value, 3); + } + } + + #[test] + fn sum_aggregation() { + for temporality in [Temporality::Delta, Temporality::Cumulative] { + let AggregateFns { measure, collect } = + AggregateBuilder::::new(temporality, None, CARDINALITY_LIMIT_DEFAULT) + .sum(true); + let mut a = MetricData::Sum(Sum { + data_points: vec![ + SumDataPoint { + attributes: vec![KeyValue::new("a1", 1)], + value: 1u64, + exemplars: vec![], + }, + SumDataPoint { + attributes: vec![KeyValue::new("a2", 1)], + value: 2u64, + exemplars: vec![], + }, + ], + start_time: now(), + time: now(), + temporality: if temporality == Temporality::Delta { + Temporality::Cumulative + } else { + Temporality::Delta + }, + is_monotonic: false, + }) + .into(); + let new_attributes = [KeyValue::new("b", 2)]; + measure.call(3, &new_attributes[..]); + + let (count, new_agg) = collect.call(Some(&mut a)); + let AggregatedMetrics::U64(MetricData::Sum(a)) = a else { + unreachable!() + }; + + assert_eq!(count, 1); + assert!(new_agg.is_none()); + assert_eq!(a.temporality, temporality); + assert!(a.is_monotonic); + assert_eq!(a.data_points.len(), 1); + assert_eq!(a.data_points[0].attributes, new_attributes.to_vec()); + assert_eq!(a.data_points[0].value, 3); + } + } + + #[test] + fn explicit_bucket_histogram_aggregation() { + for temporality in [Temporality::Delta, Temporality::Cumulative] { + let AggregateFns { measure, collect } = + AggregateBuilder::::new(temporality, None, CARDINALITY_LIMIT_DEFAULT) + .explicit_bucket_histogram(vec![1.0], true, true); + let mut a = MetricData::Histogram(Histogram { + data_points: vec![HistogramDataPoint { + attributes: vec![KeyValue::new("a1", 1)], + count: 2, + bounds: vec![1.0, 2.0], + bucket_counts: vec![0, 1, 1], + min: None, + max: None, + sum: 3u64, + exemplars: vec![], + }], + start_time: now(), + time: now(), + temporality: if temporality == Temporality::Delta { + Temporality::Cumulative + } else { + Temporality::Delta + }, + }) + .into(); + let new_attributes = [KeyValue::new("b", 2)]; + measure.call(3, &new_attributes[..]); + + let (count, new_agg) = collect.call(Some(&mut a)); + let AggregatedMetrics::U64(MetricData::Histogram(a)) = a else { + unreachable!() + }; + + assert_eq!(count, 1); + assert!(new_agg.is_none()); + assert_eq!(a.temporality, temporality); + assert_eq!(a.data_points.len(), 1); + assert_eq!(a.data_points[0].attributes, new_attributes.to_vec()); + assert_eq!(a.data_points[0].count, 1); + assert_eq!(a.data_points[0].bounds, vec![1.0]); + assert_eq!(a.data_points[0].bucket_counts, vec![0, 1]); + assert_eq!(a.data_points[0].min, Some(3)); + assert_eq!(a.data_points[0].max, Some(3)); + assert_eq!(a.data_points[0].sum, 3); + } + } + + #[test] + fn exponential_histogram_aggregation() { + for temporality in [Temporality::Delta, Temporality::Cumulative] { + let AggregateFns { measure, collect } = + AggregateBuilder::::new(temporality, None, CARDINALITY_LIMIT_DEFAULT) + .exponential_bucket_histogram(4, 20, true, true); + let mut a = MetricData::ExponentialHistogram(ExponentialHistogram { + data_points: vec![ExponentialHistogramDataPoint { + attributes: vec![KeyValue::new("a1", 1)], + count: 2, + min: None, + max: None, + sum: 3u64, + scale: 10, + zero_count: 1, + positive_bucket: ExponentialBucket { + offset: 1, + counts: vec![1], + }, + negative_bucket: ExponentialBucket { + offset: 1, + counts: vec![1], + }, + zero_threshold: 1.0, + exemplars: vec![], + }], + start_time: now(), + time: now(), + temporality: if temporality == Temporality::Delta { + Temporality::Cumulative + } else { + Temporality::Delta + }, + }) + .into(); + let new_attributes = [KeyValue::new("b", 2)]; + measure.call(3, &new_attributes[..]); + + let (count, new_agg) = collect.call(Some(&mut a)); + let AggregatedMetrics::U64(MetricData::ExponentialHistogram(a)) = a else { + unreachable!() + }; + + assert_eq!(count, 1); + assert!(new_agg.is_none()); + assert_eq!(a.temporality, temporality); + assert_eq!(a.data_points.len(), 1); + assert_eq!(a.data_points[0].attributes, new_attributes.to_vec()); + assert_eq!(a.data_points[0].count, 1); + assert_eq!(a.data_points[0].min, Some(3)); + assert_eq!(a.data_points[0].max, Some(3)); + assert_eq!(a.data_points[0].sum, 3); + assert_eq!(a.data_points[0].zero_count, 0); + assert_eq!(a.data_points[0].zero_threshold, 0.0); + assert_eq!(a.data_points[0].positive_bucket.offset, 1661953); + assert_eq!(a.data_points[0].positive_bucket.counts, vec![1]); + assert_eq!(a.data_points[0].negative_bucket.offset, 0); + assert!(a.data_points[0].negative_bucket.counts.is_empty()); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/exponential_histogram.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/exponential_histogram.rs new file mode 100644 index 00000000..f1815e58 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/exponential_histogram.rs @@ -0,0 +1,1699 @@ +#[cfg(feature = "experimental_metrics_bound_instruments")] +use std::sync::atomic::Ordering; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use std::sync::Arc; +use std::{f64::consts::LOG2_E, mem::replace, ops::DerefMut, sync::Mutex}; + +use opentelemetry::{otel_debug, KeyValue}; +use std::sync::OnceLock; + +use crate::metrics::{ + data::{self, AggregatedMetrics, MetricData}, + Temporality, +}; + +use super::{ + aggregate::{AggregateTimeInitiator, AttributeSetFilter}, + Aggregator, ComputeAggregation, Measure, Number, ValueMap, +}; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use super::{BoundMeasure, NoopBoundMeasure, TrackerEntry}; + +pub(crate) const EXPO_MAX_SCALE: i8 = 20; +pub(crate) const EXPO_MIN_SCALE: i8 = -10; + +/// Pre-bound exponential-histogram handle: writes go directly to a fixed +/// `TrackerEntry` without per-call attribute lookup. Unlike `BoundHistogramHandle`, +/// no bucket precomputation happens at the call site — `update()` does scale +/// resolution and bin assignment inside the entry's Mutex. The NaN/inf filter +/// from the unbound `call()` path is preserved here. +#[cfg(feature = "experimental_metrics_bound_instruments")] +struct BoundExpoHistogramHandle { + tracker: Arc>>>, +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundMeasure for BoundExpoHistogramHandle { + fn call(&self, measurement: T) { + // Mirror unbound ExpoHistogram::call: ignore NaN and infinity so that + // ExpoHistogramDataPoint::record's invariants are preserved. + if !measurement.into_float().is_finite() { + return; + } + self.tracker.aggregator.update(measurement); + self.tracker.has_been_updated.store(true, Ordering::Release); + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl Drop for BoundExpoHistogramHandle { + fn drop(&mut self) { + self.tracker.bound_count.fetch_sub(1, Ordering::Relaxed); + } +} + +/// A single data point in an exponential histogram. +#[derive(Debug, PartialEq)] +struct ExpoHistogramDataPoint { + max_size: i32, + count: usize, + min: T, + max: T, + sum: T, + scale: i8, + pos_buckets: ExpoBuckets, + neg_buckets: ExpoBuckets, + zero_count: u64, +} + +impl ExpoHistogramDataPoint { + fn new(config: &BucketConfig) -> Self { + ExpoHistogramDataPoint { + max_size: config.max_size, + count: 0, + min: T::max(), + max: T::min(), + sum: T::default(), + scale: config.max_scale, + pos_buckets: ExpoBuckets::default(), + neg_buckets: ExpoBuckets::default(), + zero_count: 0, + } + } +} + +impl ExpoHistogramDataPoint { + /// Adds a new measurement to the histogram. + /// + /// It will rescale the buckets if needed. + fn record(&mut self, v: T) { + self.count += 1; + + if v < self.min { + self.min = v; + } + if v > self.max { + self.max = v; + } + self.sum += v; + + let abs_v = v.into_float().abs(); + + if abs_v == 0.0 { + self.zero_count += 1; + return; + } + + let mut bin = self.get_bin(abs_v); + + let v_is_negative = v < T::default(); + + // If the new bin would make the counts larger than `max_scale`, we need to + // downscale current measurements. + let scale_delta = { + let bucket = if v_is_negative { + &self.neg_buckets + } else { + &self.pos_buckets + }; + + scale_change( + self.max_size, + bin, + bucket.start_bin, + bucket.counts.len() as i32, + ) + }; + if scale_delta > 0 { + if (self.scale - scale_delta as i8) < EXPO_MIN_SCALE { + // With a scale of -10 there is only two buckets for the whole range of f64 values. + // This can only happen if there is a max size of 1. + + // TODO - to check if this should be logged as an error if this is auto-recoverable. + otel_debug!( + name: "ExponentialHistogramDataPoint.Scale.Underflow", + current_scale = self.scale, + scale_delta = scale_delta, + max_size = self.max_size, + min_scale = EXPO_MIN_SCALE, + value = format!("{:?}", v), + message = "The measurement will be dropped due to scale underflow. Check the histogram configuration" + ); + + return; + } + // Downscale + self.scale -= scale_delta as i8; + self.pos_buckets.downscale(scale_delta); + self.neg_buckets.downscale(scale_delta); + + bin = self.get_bin(abs_v); + } + + if v_is_negative { + self.neg_buckets.record(bin) + } else { + self.pos_buckets.record(bin) + } + } + + /// the bin `v` should be recorded into. + fn get_bin(&self, v: f64) -> i32 { + let (frac, exp) = frexp(v); + if self.scale <= 0 { + // With negative scale `frac` is always 1 power of two higher than we want. + let mut correction = 1; + if frac == 0.5 { + // If `v` is an exact power of two, `frac` will be `0.5` and the exp + // will be then be two higher than we want. + correction = 2; + } + return (exp - correction) >> -self.scale; + } + (exp << self.scale) + (frac.ln() * scale_factors()[self.scale as usize]) as i32 - 1 + } +} + +/// The magnitude of the scale change needed to fit bin in the bucket. +/// +/// If no scale change is needed 0 is returned. +fn scale_change(max_size: i32, bin: i32, start_bin: i32, length: i32) -> u32 { + if length == 0 { + // No need to rescale if there are no buckets. + return 0; + } + + let mut low = start_bin; + let mut high = bin; + if start_bin >= bin { + low = bin; + high = start_bin + length - 1; + } + + let mut count = 0u32; + while high - low >= max_size { + low >>= 1; + high >>= 1; + count += 1; + + if count > (EXPO_MAX_SCALE - EXPO_MIN_SCALE) as u32 { + return count; + } + } + + count +} + +// TODO - replace it with LazyLock once it is stable +static SCALE_FACTORS: OnceLock<[f64; 21]> = OnceLock::new(); + +/// returns constants used in calculating the logarithm index. +#[inline] +fn scale_factors() -> &'static [f64; 21] { + SCALE_FACTORS.get_or_init(|| { + [ + LOG2_E * 2f64.powi(0), + LOG2_E * 2f64.powi(1), + LOG2_E * 2f64.powi(2), + LOG2_E * 2f64.powi(3), + LOG2_E * 2f64.powi(4), + LOG2_E * 2f64.powi(5), + LOG2_E * 2f64.powi(6), + LOG2_E * 2f64.powi(7), + LOG2_E * 2f64.powi(8), + LOG2_E * 2f64.powi(9), + LOG2_E * 2f64.powi(10), + LOG2_E * 2f64.powi(11), + LOG2_E * 2f64.powi(12), + LOG2_E * 2f64.powi(13), + LOG2_E * 2f64.powi(14), + LOG2_E * 2f64.powi(15), + LOG2_E * 2f64.powi(16), + LOG2_E * 2f64.powi(17), + LOG2_E * 2f64.powi(18), + LOG2_E * 2f64.powi(19), + LOG2_E * 2f64.powi(20), + ] + }) +} + +/// Breaks the number into a normalized fraction and a base-2 exponent. +/// +/// This impl is necessary as rust removed this functionality from std in +/// +#[inline(always)] +fn frexp(x: f64) -> (f64, i32) { + let mut y = x.to_bits(); + let ee = ((y >> 52) & 0x7ff) as i32; + + if ee == 0 { + if x != 0.0 { + let x1p64 = f64::from_bits(0x43f0000000000000); + let (x, e) = frexp(x * x1p64); + return (x, e - 64); + } + return (x, 0); + } else if ee == 0x7ff { + return (x, 0); + } + + let e = ee - 0x3fe; + y &= 0x800fffffffffffff; + y |= 0x3fe0000000000000; + + (f64::from_bits(y), e) +} + +/// A set of buckets in an exponential histogram. +#[derive(Default, Debug, PartialEq)] +struct ExpoBuckets { + start_bin: i32, + counts: Vec, +} + +impl ExpoBuckets { + /// Increments the count for the given bin, and expands the buckets if needed. + /// + /// Size changes must be done before calling this function. + fn record(&mut self, bin: i32) { + if self.counts.is_empty() { + self.counts = vec![1]; + self.start_bin = bin; + return; + } + + let end_bin = self.start_bin + self.counts.len() as i32 - 1; + + // if the new bin is inside the current range + if bin >= self.start_bin && bin <= end_bin { + self.counts[(bin - self.start_bin) as usize] += 1; + return; + } + + // if the new bin is before the current start, prepend the slots in `self.counts` + if bin < self.start_bin { + let mut zeroes = vec![0; (end_bin - bin + 1) as usize]; + let shift = (self.start_bin - bin) as usize; + zeroes[shift..].copy_from_slice(&self.counts); + self.counts = zeroes; + self.counts[0] = 1; + self.start_bin = bin; + } else if bin > end_bin { + // if the new bin is after the end, initialize the slots up to the new bin + if ((bin - self.start_bin) as usize) < self.counts.capacity() { + self.counts.resize((bin - self.start_bin + 1) as usize, 0); + self.counts[(bin - self.start_bin) as usize] = 1; + return; + } + + self.counts.extend( + std::iter::repeat(0).take((bin - self.start_bin) as usize - self.counts.len() + 1), + ); + self.counts[(bin - self.start_bin) as usize] = 1 + } + } + + /// Shrinks a bucket by a factor of 2*s. + /// + /// It will sum counts into the correct lower resolution bucket. + fn downscale(&mut self, delta: u32) { + // Example + // delta = 2 + // original offset: -6 + // counts: [ 3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + // bins: -6 -5, -4, -3, -2, -1, 0, 1, 2, 3, 4 + // new bins:-2, -2, -1, -1, -1, -1, 0, 0, 0, 0, 1 + // new offset: -2 + // new counts: [4, 14, 30, 10] + + if self.counts.len() <= 1 || delta < 1 { + self.start_bin >>= delta; + return; + } + + let steps = 1 << delta; + let mut offset = self.start_bin % steps; + offset = (offset + steps) % steps; // to make offset positive + for i in 1..self.counts.len() { + let idx = i + offset as usize; + if idx % steps as usize == 0 { + self.counts[idx / steps as usize] = self.counts[i]; + continue; + } + self.counts[idx / steps as usize] += self.counts[i]; + } + + let last_idx = (self.counts.len() as i32 - 1 + offset) / steps; + self.counts = self.counts[..last_idx as usize + 1].to_vec(); + self.start_bin >>= delta; + } +} + +impl Aggregator for Mutex> +where + T: Number, +{ + type InitConfig = BucketConfig; + + type PreComputedValue = T; + + fn create(init: &BucketConfig) -> Self { + Mutex::new(ExpoHistogramDataPoint::new(init)) + } + + fn update(&self, value: T) { + let mut this = match self.lock() { + Ok(guard) => guard, + Err(_) => return, + }; + this.record(value); + } + + fn clone_and_reset(&self, init: &BucketConfig) -> Self { + let mut current = self.lock().unwrap_or_else(|err| err.into_inner()); + let cloned = replace(current.deref_mut(), ExpoHistogramDataPoint::new(init)); + Mutex::new(cloned) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct BucketConfig { + max_size: i32, + max_scale: i8, +} + +/// An aggregator that summarizes a set of measurements as an exponential +/// histogram. +/// +/// Each histogram is scoped by attributes and the aggregation cycle the +/// measurements were made in. +pub(crate) struct ExpoHistogram { + value_map: ValueMap>>, + init_time: AggregateTimeInitiator, + temporality: Temporality, + filter: AttributeSetFilter, + record_sum: bool, + record_min_max: bool, +} + +impl ExpoHistogram { + /// Create a new exponential histogram. + pub(crate) fn new( + temporality: Temporality, + filter: AttributeSetFilter, + max_size: u32, + max_scale: i8, + record_min_max: bool, + record_sum: bool, + cardinality_limit: usize, + ) -> Self { + ExpoHistogram { + value_map: ValueMap::new( + BucketConfig { + max_size: max_size as i32, + max_scale, + }, + cardinality_limit, + ), + init_time: AggregateTimeInitiator::default(), + temporality, + filter, + record_sum, + record_min_max, + } + } + + fn delta(&self, dest: Option<&mut MetricData>) -> (usize, Option>) { + let time = self.init_time.delta(); + + let h = dest.and_then(|d| { + if let MetricData::ExponentialHistogram(hist) = d { + Some(hist) + } else { + None + } + }); + let mut new_agg = if h.is_none() { + Some(data::ExponentialHistogram { + data_points: vec![], + start_time: time.start, + time: time.current, + temporality: Temporality::Delta, + }) + } else { + None + }; + let h = h.unwrap_or_else(|| new_agg.as_mut().expect("present if h is none")); + h.temporality = Temporality::Delta; + h.start_time = time.start; + h.time = time.current; + + let config = *self.value_map.config(); + self.value_map + .collect_and_reset(&mut h.data_points, |attributes, attr| { + let reset = attr.clone_and_reset(&config); + let b = reset.into_inner().unwrap_or_else(|err| err.into_inner()); + data::ExponentialHistogramDataPoint { + attributes, + count: b.count, + min: if self.record_min_max { + Some(b.min) + } else { + None + }, + max: if self.record_min_max { + Some(b.max) + } else { + None + }, + sum: if self.record_sum { b.sum } else { T::default() }, + scale: b.scale, + zero_count: b.zero_count, + positive_bucket: data::ExponentialBucket { + offset: b.pos_buckets.start_bin, + counts: b.pos_buckets.counts, + }, + negative_bucket: data::ExponentialBucket { + offset: b.neg_buckets.start_bin, + counts: b.neg_buckets.counts, + }, + zero_threshold: 0.0, + exemplars: vec![], + } + }); + + (h.data_points.len(), new_agg.map(Into::into)) + } + + fn cumulative(&self, dest: Option<&mut MetricData>) -> (usize, Option>) { + let time = self.init_time.cumulative(); + + let h = dest.and_then(|d| { + if let MetricData::ExponentialHistogram(hist) = d { + Some(hist) + } else { + None + } + }); + let mut new_agg = if h.is_none() { + Some(data::ExponentialHistogram { + data_points: vec![], + start_time: time.start, + time: time.current, + temporality: Temporality::Cumulative, + }) + } else { + None + }; + let h = h.unwrap_or_else(|| new_agg.as_mut().expect("present if h is none")); + h.temporality = Temporality::Cumulative; + h.start_time = time.start; + h.time = time.current; + + self.value_map + .collect_readonly(&mut h.data_points, |attributes, attr| { + let b = attr.lock().unwrap_or_else(|err| err.into_inner()); + data::ExponentialHistogramDataPoint { + attributes, + count: b.count, + min: if self.record_min_max { + Some(b.min) + } else { + None + }, + max: if self.record_min_max { + Some(b.max) + } else { + None + }, + sum: if self.record_sum { b.sum } else { T::default() }, + scale: b.scale, + zero_count: b.zero_count, + positive_bucket: data::ExponentialBucket { + offset: b.pos_buckets.start_bin, + counts: b.pos_buckets.counts.clone(), + }, + negative_bucket: data::ExponentialBucket { + offset: b.neg_buckets.start_bin, + counts: b.neg_buckets.counts.clone(), + }, + zero_threshold: 0.0, + exemplars: vec![], + } + }); + + (h.data_points.len(), new_agg.map(Into::into)) + } +} + +impl Measure for ExpoHistogram +where + T: Number, +{ + fn call(&self, measurement: T, attrs: &[KeyValue]) { + let f_value = measurement.into_float(); + // Ignore NaN and infinity. + // Only makes sense if T is f64, maybe this could be no-op for other cases? + if !f_value.is_finite() { + return; + } + + self.filter.apply(attrs, |filtered| { + self.value_map.measure(measurement, filtered); + }) + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind(&self, attrs: &[KeyValue]) -> Box> { + let mut bound_attrs = Vec::new(); + self.filter.apply(attrs, |filtered| { + bound_attrs = filtered.to_vec(); + }); + match self.value_map.bind(&bound_attrs) { + Some(tracker) => Box::new(BoundExpoHistogramHandle { tracker }), + None => Box::new(NoopBoundMeasure::new()), + } + } +} + +impl ComputeAggregation for ExpoHistogram +where + T: Number, +{ + fn call(&self, dest: Option<&mut AggregatedMetrics>) -> (usize, Option) { + let data = dest.and_then(|d| T::extract_metrics_data_mut(d)); + let (len, new) = match self.temporality { + Temporality::Delta => self.delta(data), + _ => self.cumulative(data), + }; + (len, new.map(T::make_aggregated_metrics)) + } +} + +#[cfg(test)] +mod tests { + use opentelemetry::time::now; + use std::{any::Any, ops::Neg}; + use tests::internal::AggregateFns; + + use crate::metrics::internal::{self, AggregateBuilder}; + + use super::*; + + const CARDINALITY_LIMIT_DEFAULT: usize = 2000; + + #[test] + fn test_expo_histogram_data_point_record() { + run_data_point_record::(); + run_data_point_record_f64(); + run_min_max_sum_f64(); + run_min_max_sum::(); + run_min_max_sum::(); + run_data_point_record::(); + } + + fn run_data_point_record + From>() { + struct TestCase { + max_size: i32, + values: Vec, + expected_buckets: ExpoBuckets, + expected_scale: i8, + } + let test_cases: Vec> = vec![ + TestCase { + max_size: 4, + values: vec![2, 4, 1].into_iter().map(Into::into).collect(), + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![1, 1, 1], + }, + expected_scale: 0, + }, + TestCase { + max_size: 4, + values: vec![4, 4, 4, 2, 16, 1] + .into_iter() + .map(Into::into) + .collect(), + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![1, 4, 1], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![1, 2, 4].into_iter().map(Into::into).collect(), + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![1, 2], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![1, 4, 2].into_iter().map(Into::into).collect(), + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![1, 2], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![2, 4, 1].into_iter().map(Into::into).collect(), + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![1, 2], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![2, 1, 4].into_iter().map(Into::into).collect(), + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![1, 2], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![4, 1, 2].into_iter().map(Into::into).collect(), + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![1, 2], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![4, 2, 1].into_iter().map(Into::into).collect(), + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![1, 2], + }, + expected_scale: -1, + }, + ]; + + for test in test_cases { + let mut dp = ExpoHistogramDataPoint::::new(&BucketConfig { + max_size: test.max_size, + max_scale: 20, + }); + for v in test.values { + dp.record(v); + dp.record(-v); + } + + assert_eq!(test.expected_buckets, dp.pos_buckets, "positive buckets"); + assert_eq!(test.expected_buckets, dp.neg_buckets, "negative buckets"); + assert_eq!(test.expected_scale, dp.scale, "scale"); + } + } + + fn run_min_max_sum_f64() { + struct Expected { + min: f64, + max: f64, + sum: f64, + count: usize, + } + impl Expected { + fn new(min: f64, max: f64, sum: f64, count: usize) -> Self { + Expected { + min, + max, + sum, + count, + } + } + } + struct TestCase { + values: Vec, + expected: Expected, + } + + let test_cases = vec![ + TestCase { + values: vec![2.0, 4.0, 1.0], + expected: Expected::new(1.0, 4.0, 7.0, 3), + }, + TestCase { + values: vec![2.0, 4.0, 1.0, f64::INFINITY], + expected: Expected::new(1.0, 4.0, 7.0, 3), + }, + TestCase { + values: vec![2.0, 4.0, 1.0, -f64::INFINITY], + expected: Expected::new(1.0, 4.0, 7.0, 3), + }, + TestCase { + values: vec![2.0, 4.0, 1.0, f64::NAN], + expected: Expected::new(1.0, 4.0, 7.0, 3), + }, + TestCase { + values: vec![4.0, 4.0, 4.0, 2.0, 16.0, 1.0], + expected: Expected::new(1.0, 16.0, 31.0, 6), + }, + ]; + + for test in test_cases { + let h = ExpoHistogram::new( + Temporality::Cumulative, + AttributeSetFilter::new(None), + 4, + 20, + true, + true, + CARDINALITY_LIMIT_DEFAULT, + ); + for v in test.values { + Measure::call(&h, v, &[]); + } + let dp = h.value_map.no_attribute_tracker.aggregator.lock().unwrap(); + + assert_eq!(test.expected.max, dp.max); + assert_eq!(test.expected.min, dp.min); + assert_eq!(test.expected.sum, dp.sum); + assert_eq!(test.expected.count, dp.count); + } + } + + fn run_min_max_sum>() { + struct Expected { + min: T, + max: T, + sum: T, + count: usize, + } + impl Expected { + fn new(min: T, max: T, sum: T, count: usize) -> Self { + Expected { + min, + max, + sum, + count, + } + } + } + struct TestCase { + values: Vec, + expected: Expected, + } + let test_cases: Vec> = vec![ + TestCase { + values: vec![2, 4, 1].into_iter().map(Into::into).collect(), + expected: Expected::new(1.into(), 4.into(), 7.into(), 3), + }, + TestCase { + values: vec![4, 4, 4, 2, 16, 1] + .into_iter() + .map(Into::into) + .collect(), + expected: Expected::new(1.into(), 16.into(), 31.into(), 6), + }, + ]; + + for test in test_cases { + let h = ExpoHistogram::new( + Temporality::Cumulative, + AttributeSetFilter::new(None), + 4, + 20, + true, + true, + CARDINALITY_LIMIT_DEFAULT, + ); + for v in test.values { + Measure::call(&h, v, &[]); + } + let dp = h.value_map.no_attribute_tracker.aggregator.lock().unwrap(); + + assert_eq!(test.expected.max, dp.max); + assert_eq!(test.expected.min, dp.min); + assert_eq!(test.expected.sum, dp.sum); + assert_eq!(test.expected.count, dp.count); + } + } + + fn run_data_point_record_f64() { + struct TestCase { + max_size: i32, + values: Vec, + expected_buckets: ExpoBuckets, + expected_scale: i8, + } + + let test_cases = vec![ + TestCase { + max_size: 4, + values: vec![2.0, 2.0, 2.0, 1.0, 8.0, 0.5], + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![2, 3, 1], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![1.0, 0.5, 2.0], + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![2, 1], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![1.0, 2.0, 0.5], + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![2, 1], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![2.0, 0.5, 1.0], + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![2, 1], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![2.0, 1.0, 0.5], + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![2, 1], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![0.5, 1.0, 2.0], + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![2, 1], + }, + expected_scale: -1, + }, + TestCase { + max_size: 2, + values: vec![0.5, 2.0, 1.0], + expected_buckets: ExpoBuckets { + start_bin: -1, + counts: vec![2, 1], + }, + expected_scale: -1, + }, + ]; + for test in test_cases { + let mut dp = ExpoHistogramDataPoint::new(&BucketConfig { + max_size: test.max_size, + max_scale: 20, + }); + for v in test.values { + dp.record(v); + dp.record(-v); + } + + assert_eq!(test.expected_buckets, dp.pos_buckets); + assert_eq!(test.expected_buckets, dp.neg_buckets); + assert_eq!(test.expected_scale, dp.scale); + } + } + + #[test] + fn data_point_record_limits() { + // These bins are calculated from the following formula: + // floor( log2( value) * 2^20 ) using an arbitrary precision calculator. + + let cfg = BucketConfig { + max_size: 4, + max_scale: 20, + }; + let mut fdp = ExpoHistogramDataPoint::new(&cfg); + fdp.record(f64::MAX); + + assert_eq!( + fdp.pos_buckets.start_bin, 1073741823, + "start bin does not match for large f64 values", + ); + + let mut fdp = ExpoHistogramDataPoint::new(&cfg); + fdp.record(f64::MIN_POSITIVE); + + assert_eq!( + fdp.pos_buckets.start_bin, -1071644673, + "start bin does not match for small positive values", + ); + + let mut idp = ExpoHistogramDataPoint::new(&cfg); + idp.record(i64::MAX); + + assert_eq!( + idp.pos_buckets.start_bin, 66060287, + "start bin does not match for max i64 values", + ); + } + + #[test] + fn expo_bucket_downscale() { + struct TestCase { + name: &'static str, + bucket: ExpoBuckets, + scale: i8, + want: ExpoBuckets, + } + + let test_cases = vec![ + TestCase { + name: "Empty bucket", + bucket: ExpoBuckets { + start_bin: 0, + counts: vec![], + }, + scale: 3, + want: ExpoBuckets { + start_bin: 0, + counts: vec![], + }, + }, + TestCase { + name: "1 size bucket", + bucket: ExpoBuckets { + start_bin: 50, + counts: vec![7], + }, + scale: 4, + want: ExpoBuckets { + start_bin: 3, + counts: vec![7], + }, + }, + TestCase { + name: "zero scale", + bucket: ExpoBuckets { + start_bin: 50, + counts: vec![7, 5], + }, + scale: 0, + want: ExpoBuckets { + start_bin: 50, + counts: vec![7, 5], + }, + }, + TestCase { + name: "aligned bucket scale 1", + bucket: ExpoBuckets { + start_bin: 0, + counts: vec![1, 2, 3, 4, 5, 6], + }, + scale: 1, + want: ExpoBuckets { + start_bin: 0, + counts: vec![3, 7, 11], + }, + }, + TestCase { + name: "aligned bucket scale 2", + bucket: ExpoBuckets { + start_bin: 0, + counts: vec![1, 2, 3, 4, 5, 6], + }, + scale: 2, + want: ExpoBuckets { + start_bin: 0, + counts: vec![10, 11], + }, + }, + TestCase { + name: "aligned bucket scale 3", + bucket: ExpoBuckets { + start_bin: 0, + counts: vec![1, 2, 3, 4, 5, 6], + }, + scale: 3, + want: ExpoBuckets { + start_bin: 0, + counts: vec![21], + }, + }, + TestCase { + name: "unaligned bucket scale 1", + bucket: ExpoBuckets { + start_bin: 5, + counts: vec![1, 2, 3, 4, 5, 6], + }, // This is equivalent to [0,0,0,0,0,1,2,3,4,5,6] + scale: 1, + want: ExpoBuckets { + start_bin: 2, + counts: vec![1, 5, 9, 6], + }, // This is equivalent to [0,0,1,5,9,6] + }, + TestCase { + name: "unaligned bucket scale 2", + bucket: ExpoBuckets { + start_bin: 7, + counts: vec![1, 2, 3, 4, 5, 6], + }, // This is equivalent to [0,0,0,0,0,0,0,1,2,3,4,5,6] + scale: 2, + want: ExpoBuckets { + start_bin: 1, + counts: vec![1, 14, 6], + }, // This is equivalent to [0,1,14,6] + }, + TestCase { + name: "unaligned bucket scale 3", + bucket: ExpoBuckets { + start_bin: 3, + counts: vec![1, 2, 3, 4, 5, 6], + }, // This is equivalent to [0,0,0,1,2,3,4,5,6] + scale: 3, + want: ExpoBuckets { + start_bin: 0, + counts: vec![15, 6], + }, // This is equivalent to [0,15,6] + }, + TestCase { + name: "unaligned bucket scale 1", + bucket: ExpoBuckets { + start_bin: 1, + counts: vec![1, 0, 1], + }, + scale: 1, + want: ExpoBuckets { + start_bin: 0, + counts: vec![1, 1], + }, + }, + TestCase { + name: "negative start_bin", + bucket: ExpoBuckets { + start_bin: -1, + counts: vec![1, 0, 3], + }, + scale: 1, + want: ExpoBuckets { + start_bin: -1, + counts: vec![1, 3], + }, + }, + TestCase { + name: "negative start_bin 2", + bucket: ExpoBuckets { + start_bin: -4, + counts: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + }, + scale: 1, + want: ExpoBuckets { + start_bin: -2, + counts: vec![3, 7, 11, 15, 19], + }, + }, + ]; + for mut test in test_cases { + test.bucket.downscale(test.scale as u32); + assert_eq!(test.want, test.bucket, "{}", test.name); + } + } + + #[test] + fn expo_bucket_record() { + struct TestCase { + name: &'static str, + bucket: ExpoBuckets, + bin: i32, + want: ExpoBuckets, + } + + let test_cases = vec![ + TestCase { + name: "Empty bucket creates first count", + bucket: ExpoBuckets { + start_bin: 0, + counts: vec![], + }, + bin: -5, + want: ExpoBuckets { + start_bin: -5, + counts: vec![1], + }, + }, + TestCase { + name: "Bin is in the bucket", + bucket: ExpoBuckets { + start_bin: 3, + counts: vec![1, 2, 3, 4, 5, 6], + }, + bin: 5, + want: ExpoBuckets { + start_bin: 3, + counts: vec![1, 2, 4, 4, 5, 6], + }, + }, + TestCase { + name: "Bin is before the start of the bucket", + bucket: ExpoBuckets { + start_bin: 1, + counts: vec![1, 2, 3, 4, 5, 6], + }, + bin: -2, + want: ExpoBuckets { + start_bin: -2, + counts: vec![1, 0, 0, 1, 2, 3, 4, 5, 6], + }, + }, + TestCase { + name: "Bin is after the end of the bucket", + bucket: ExpoBuckets { + start_bin: -2, + counts: vec![1, 2, 3, 4, 5, 6], + }, + bin: 4, + want: ExpoBuckets { + start_bin: -2, + counts: vec![1, 2, 3, 4, 5, 6, 1], + }, + }, + ]; + + for mut test in test_cases { + test.bucket.record(test.bin); + assert_eq!(test.want, test.bucket, "{}", test.name); + } + } + + #[test] + fn scale_change_rescaling() { + struct Args { + bin: i32, + start_bin: i32, + length: i32, + max_size: i32, + } + struct TestCase { + name: &'static str, + args: Args, + want: u32, + } + let test_cases = vec![ + TestCase { + name: "if length is 0, no rescale is needed", + // [] -> [5] length 1 + args: Args { + bin: 5, + start_bin: 0, + length: 0, + max_size: 4, + }, + want: 0, + }, + TestCase { + name: "if bin is between start, and the end, no rescale needed", + // [-1, ..., 8] length 10 -> [-1, ..., 5, ..., 8] length 10 + args: Args { + bin: 5, + start_bin: -1, + length: 10, + max_size: 20, + }, + want: 0, + }, + TestCase { + name: "if [bin,... end].len() > max_size, rescale needed", + // [8,9,10] length 3 -> [5, ..., 10] length 6 + args: Args { + bin: 5, + start_bin: 8, + length: 3, + max_size: 5, + }, + want: 1, + }, + TestCase { + name: "if [start, ..., bin].len() > max_size, rescale needed", + // [2,3,4] length 3 -> [2, ..., 7] length 6 + args: Args { + bin: 7, + start_bin: 2, + length: 3, + max_size: 5, + }, + want: 1, + }, + TestCase { + name: "if [start, ..., bin].len() > max_size, rescale needed", + // [2,3,4] length 3 -> [2, ..., 7] length 12 + args: Args { + bin: 13, + start_bin: 2, + length: 3, + max_size: 5, + }, + want: 2, + }, + TestCase { + name: "It should not hang if it will never be able to rescale", + args: Args { + bin: 1, + start_bin: -1, + length: 1, + max_size: 1, + }, + want: 31, + }, + ]; + + for test in test_cases { + let got = scale_change( + test.args.max_size, + test.args.bin, + test.args.start_bin, + test.args.length, + ); + assert_eq!(got, test.want, "incorrect scale change, {}", test.name); + } + } + + #[test] + fn sub_normal() { + let want = ExpoHistogramDataPoint { + max_size: 4, + count: 3, + min: f64::MIN_POSITIVE, + max: f64::MIN_POSITIVE, + sum: 3.0 * f64::MIN_POSITIVE, + + scale: 20, + pos_buckets: ExpoBuckets { + start_bin: -1071644673, + counts: vec![3], + }, + neg_buckets: ExpoBuckets { + start_bin: 0, + counts: vec![], + }, + zero_count: 0, + }; + + let mut ehdp = ExpoHistogramDataPoint::new(&BucketConfig { + max_size: 4, + max_scale: 20, + }); + ehdp.record(f64::MIN_POSITIVE); + ehdp.record(f64::MIN_POSITIVE); + ehdp.record(f64::MIN_POSITIVE); + + assert_eq!(want, ehdp); + } + + #[test] + fn hist_aggregations() { + hist_aggregation::(); + hist_aggregation::(); + hist_aggregation::(); + } + + fn hist_aggregation>() { + let max_size = 4; + let max_scale = 20; + let record_min_max = true; + let record_sum = true; + + #[allow(clippy::type_complexity)] + struct TestCase { + name: &'static str, + build: Box AggregateFns>, + input: Vec>, + want: data::ExponentialHistogram, + want_count: usize, + } + let test_cases: Vec> = vec![ + TestCase { + name: "Delta Single", + build: Box::new(move || { + AggregateBuilder::new(Temporality::Delta, None, CARDINALITY_LIMIT_DEFAULT) + .exponential_bucket_histogram( + max_size, + max_scale, + record_min_max, + record_sum, + ) + }), + input: vec![vec![4, 4, 4, 2, 16, 1] + .into_iter() + .map(Into::into) + .collect()], + want: data::ExponentialHistogram { + temporality: Temporality::Delta, + data_points: vec![data::ExponentialHistogramDataPoint { + attributes: vec![], + count: 6, + min: Some(1.into()), + max: Some(16.into()), + sum: 31.into(), + scale: -1, + positive_bucket: data::ExponentialBucket { + offset: -1, + counts: vec![1, 4, 1], + }, + negative_bucket: data::ExponentialBucket { + offset: 0, + counts: vec![], + }, + exemplars: vec![], + zero_threshold: 0.0, + zero_count: 0, + }], + start_time: now(), + time: now(), + }, + want_count: 1, + }, + TestCase { + name: "Cumulative Single", + build: Box::new(move || { + internal::AggregateBuilder::new( + Temporality::Cumulative, + None, + CARDINALITY_LIMIT_DEFAULT, + ) + .exponential_bucket_histogram( + max_size, + max_scale, + record_min_max, + record_sum, + ) + }), + input: vec![vec![4, 4, 4, 2, 16, 1] + .into_iter() + .map(Into::into) + .collect()], + want: data::ExponentialHistogram { + temporality: Temporality::Cumulative, + data_points: vec![data::ExponentialHistogramDataPoint { + attributes: vec![], + count: 6, + min: Some(1.into()), + max: Some(16.into()), + sum: 31.into(), + scale: -1, + positive_bucket: data::ExponentialBucket { + offset: -1, + counts: vec![1, 4, 1], + }, + negative_bucket: data::ExponentialBucket { + offset: 0, + counts: vec![], + }, + exemplars: vec![], + zero_threshold: 0.0, + zero_count: 0, + }], + start_time: now(), + time: now(), + }, + want_count: 1, + }, + TestCase { + name: "Delta Multiple", + build: Box::new(move || { + internal::AggregateBuilder::new( + Temporality::Delta, + None, + CARDINALITY_LIMIT_DEFAULT, + ) + .exponential_bucket_histogram( + max_size, + max_scale, + record_min_max, + record_sum, + ) + }), + input: vec![ + vec![2, 3, 8].into_iter().map(Into::into).collect(), + vec![4, 4, 4, 2, 16, 1] + .into_iter() + .map(Into::into) + .collect(), + ], + want: data::ExponentialHistogram { + temporality: Temporality::Delta, + data_points: vec![data::ExponentialHistogramDataPoint { + attributes: vec![], + count: 6, + min: Some(1.into()), + max: Some(16.into()), + sum: 31.into(), + scale: -1, + positive_bucket: data::ExponentialBucket { + offset: -1, + counts: vec![1, 4, 1], + }, + negative_bucket: data::ExponentialBucket { + offset: 0, + counts: vec![], + }, + exemplars: vec![], + zero_threshold: 0.0, + zero_count: 0, + }], + start_time: now(), + time: now(), + }, + want_count: 1, + }, + TestCase { + name: "Cumulative Multiple ", + build: Box::new(move || { + internal::AggregateBuilder::new( + Temporality::Cumulative, + None, + CARDINALITY_LIMIT_DEFAULT, + ) + .exponential_bucket_histogram( + max_size, + max_scale, + record_min_max, + record_sum, + ) + }), + input: vec![ + vec![2, 3, 8].into_iter().map(Into::into).collect(), + vec![4, 4, 4, 2, 16, 1] + .into_iter() + .map(Into::into) + .collect(), + ], + want: data::ExponentialHistogram { + temporality: Temporality::Cumulative, + data_points: vec![data::ExponentialHistogramDataPoint { + count: 9, + min: Some(1.into()), + max: Some(16.into()), + sum: 44.into(), + scale: -1, + positive_bucket: data::ExponentialBucket { + offset: -1, + counts: vec![1, 6, 2], + }, + attributes: vec![], + negative_bucket: data::ExponentialBucket { + offset: 0, + counts: vec![], + }, + exemplars: vec![], + zero_threshold: 0.0, + zero_count: 0, + }], + start_time: now(), + time: now(), + }, + want_count: 1, + }, + ]; + + for test in test_cases { + let AggregateFns { measure, collect } = (test.build)(); + + let mut got = T::make_aggregated_metrics(MetricData::ExponentialHistogram( + data::ExponentialHistogram:: { + data_points: vec![], + start_time: now(), + time: now(), + temporality: Temporality::Delta, + }, + )); + let mut count = 0; + for n in test.input { + for v in n { + measure.call(v, &[]) + } + count = collect.call(Some(&mut got)).0 + } + + assert_aggregation_eq( + &MetricData::ExponentialHistogram(test.want), + T::extract_metrics_data_ref(&got).unwrap(), + test.name, + ); + assert_eq!(test.want_count, count, "{}", test.name); + } + } + + fn assert_aggregation_eq( + a: &MetricData, + b: &MetricData, + test_name: &'static str, + ) { + match (a, b) { + (MetricData::Gauge(a), MetricData::Gauge(b)) => { + assert_eq!( + a.data_points.len(), + b.data_points.len(), + "{test_name} gauge counts" + ); + for (a, b) in a.data_points.iter().zip(b.data_points.iter()) { + assert_gauge_data_points_eq(a, b, "mismatching gauge data points", test_name); + } + } + (MetricData::Sum(a), MetricData::Sum(b)) => { + assert_eq!( + a.temporality, b.temporality, + "{test_name} mismatching sum temporality" + ); + assert_eq!( + a.is_monotonic, b.is_monotonic, + "{test_name} mismatching sum monotonicity", + ); + assert_eq!( + a.data_points.len(), + b.data_points.len(), + "{test_name} sum counts" + ); + for (a, b) in a.data_points.iter().zip(b.data_points.iter()) { + assert_sum_data_points_eq(a, b, "mismatching sum data points", test_name); + } + } + (MetricData::Histogram(a), MetricData::Histogram(b)) => { + assert_eq!( + a.temporality, b.temporality, + "{test_name}: mismatching hist temporality" + ); + assert_eq!( + a.data_points.len(), + b.data_points.len(), + "{test_name} hist counts" + ); + for (a, b) in a.data_points.iter().zip(b.data_points.iter()) { + assert_hist_data_points_eq(a, b, "mismatching hist data points", test_name); + } + } + (MetricData::ExponentialHistogram(a), MetricData::ExponentialHistogram(b)) => { + assert_eq!( + a.temporality, b.temporality, + "{test_name} mismatching hist temporality" + ); + assert_eq!( + a.data_points.len(), + b.data_points.len(), + "{test_name} hist counts" + ); + for (a, b) in a.data_points.iter().zip(b.data_points.iter()) { + assert_exponential_hist_data_points_eq( + a, + b, + "mismatching hist data points", + test_name, + ); + } + } + _ => { + assert_eq!( + a.type_id(), + b.type_id(), + "{test_name} Aggregation types not equal" + ); + } + } + } + + fn assert_sum_data_points_eq( + a: &data::SumDataPoint, + b: &data::SumDataPoint, + message: &'static str, + test_name: &'static str, + ) { + assert_eq!( + a.attributes, b.attributes, + "{test_name}: {message} attributes" + ); + assert_eq!(a.value, b.value, "{test_name}: {message} value"); + } + + fn assert_gauge_data_points_eq( + a: &data::GaugeDataPoint, + b: &data::GaugeDataPoint, + message: &'static str, + test_name: &'static str, + ) { + assert_eq!( + a.attributes, b.attributes, + "{test_name}: {message} attributes" + ); + assert_eq!(a.value, b.value, "{test_name}: {message} value"); + } + + fn assert_hist_data_points_eq( + a: &data::HistogramDataPoint, + b: &data::HistogramDataPoint, + message: &'static str, + test_name: &'static str, + ) { + assert_eq!( + a.attributes, b.attributes, + "{test_name}: {message} attributes" + ); + assert_eq!(a.count, b.count, "{test_name}: {message} count"); + assert_eq!(a.bounds, b.bounds, "{test_name}: {message} bounds"); + assert_eq!( + a.bucket_counts, b.bucket_counts, + "{test_name}: {message} bucket counts" + ); + assert_eq!(a.min, b.min, "{test_name}: {message} min"); + assert_eq!(a.max, b.max, "{test_name}: {message} max"); + assert_eq!(a.sum, b.sum, "{test_name}: {message} sum"); + } + + fn assert_exponential_hist_data_points_eq( + a: &data::ExponentialHistogramDataPoint, + b: &data::ExponentialHistogramDataPoint, + message: &'static str, + test_name: &'static str, + ) { + assert_eq!( + a.attributes, b.attributes, + "{test_name}: {message} attributes" + ); + assert_eq!(a.count, b.count, "{test_name}: {message} count"); + assert_eq!(a.min, b.min, "{test_name}: {message} min"); + assert_eq!(a.max, b.max, "{test_name}: {message} max"); + assert_eq!(a.sum, b.sum, "{test_name}: {message} sum"); + + assert_eq!(a.scale, b.scale, "{test_name}: {message} scale"); + assert_eq!(a.zero_count, b.zero_count, "{test_name}: {message} zeros"); + + assert_eq!( + a.positive_bucket, b.positive_bucket, + "{test_name}: {message} pos" + ); + assert_eq!( + a.negative_bucket, b.negative_bucket, + "{test_name}: {message} neg" + ); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/histogram.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/histogram.rs new file mode 100644 index 00000000..309e7605 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/histogram.rs @@ -0,0 +1,406 @@ +use std::mem::replace; +use std::ops::DerefMut; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use std::sync::atomic::Ordering; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use std::sync::Arc; +use std::sync::Mutex; + +use crate::metrics::data::{self, MetricData}; +use crate::metrics::data::{AggregatedMetrics, HistogramDataPoint}; +use crate::metrics::Temporality; +use opentelemetry::KeyValue; + +use super::aggregate::{AggregateTimeInitiator, AttributeSetFilter}; +use super::{Aggregator, ComputeAggregation, Measure, Number, ValueMap}; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use super::{BoundMeasure, NoopBoundMeasure, TrackerEntry}; + +impl Aggregator for Mutex> +where + T: Number, +{ + type InitConfig = usize; + /// Value and bucket index + type PreComputedValue = (T, usize); + + fn update(&self, (value, index): (T, usize)) { + let mut buckets = self.lock().unwrap_or_else(|err| err.into_inner()); + + buckets.total += value; + buckets.count += 1; + if !buckets.counts.is_empty() { + buckets.counts[index] += 1; + } + + if value < buckets.min { + buckets.min = value; + } + if value > buckets.max { + buckets.max = value + } + } + + fn create(count: &usize) -> Self { + Mutex::new(Buckets::::new(*count)) + } + + fn clone_and_reset(&self, count: &usize) -> Self { + let mut current = self.lock().unwrap_or_else(|err| err.into_inner()); + Mutex::new(replace(current.deref_mut(), Buckets::new(*count))) + } +} + +#[derive(Default)] +struct Buckets { + counts: Vec, + count: u64, + total: T, + min: T, + max: T, +} + +impl Buckets { + /// returns buckets with `n` bins. + fn new(n: usize) -> Buckets { + Buckets { + counts: vec![0; n], + min: T::max(), + max: T::min(), + ..Default::default() + } + } +} + +/// Pre-bound histogram handle: writes go directly to a fixed `TrackerEntry` +/// without per-call attribute lookup. The `tracker` is either a dedicated entry +/// for the bound attribute set, or — if bind() hit the cardinality limit — the +/// shared overflow tracker. +#[cfg(feature = "experimental_metrics_bound_instruments")] +struct BoundHistogramHandle { + tracker: Arc>>>, + bounds: Vec, +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundMeasure for BoundHistogramHandle { + fn call(&self, measurement: T) { + let f = measurement.into_float(); + // Mirror unbound Histogram::call: ignore NaN and infinity so `sum` and + // bucket counts can't be permanently corrupted. + if !f.is_finite() { + return; + } + let index = self.bounds.partition_point(|&x| x < f); + self.tracker.aggregator.update((measurement, index)); + self.tracker.has_been_updated.store(true, Ordering::Release); + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl Drop for BoundHistogramHandle { + fn drop(&mut self) { + self.tracker.bound_count.fetch_sub(1, Ordering::Relaxed); + } +} + +/// Summarizes a set of measurements as a histogram with explicitly defined +/// buckets. +pub(crate) struct Histogram { + value_map: ValueMap>>, + init_time: AggregateTimeInitiator, + temporality: Temporality, + filter: AttributeSetFilter, + bounds: Vec, + record_min_max: bool, + record_sum: bool, +} + +impl Histogram { + pub(crate) fn new( + temporality: Temporality, + filter: AttributeSetFilter, + bounds: Vec, + record_min_max: bool, + record_sum: bool, + cardinality_limit: usize, + ) -> Self { + let buckets_count = if bounds.is_empty() { + 0 + } else { + bounds.len() + 1 + }; + + Histogram { + value_map: ValueMap::new(buckets_count, cardinality_limit), + init_time: AggregateTimeInitiator::default(), + temporality, + filter, + bounds, + record_min_max, + record_sum, + } + } + + fn delta(&self, dest: Option<&mut MetricData>) -> (usize, Option>) { + let time = self.init_time.delta(); + + let h = dest.and_then(|d| { + if let MetricData::Histogram(hist) = d { + Some(hist) + } else { + None + } + }); + let mut new_agg = if h.is_none() { + Some(data::Histogram { + data_points: vec![], + start_time: time.start, + time: time.current, + temporality: Temporality::Delta, + }) + } else { + None + }; + let h = h.unwrap_or_else(|| new_agg.as_mut().expect("present if h is none")); + h.temporality = Temporality::Delta; + h.start_time = time.start; + h.time = time.current; + + let buckets_count = *self.value_map.config(); + self.value_map + .collect_and_reset(&mut h.data_points, |attributes, aggr| { + let reset = aggr.clone_and_reset(&buckets_count); + let b = reset.into_inner().unwrap_or_else(|err| err.into_inner()); + HistogramDataPoint { + attributes, + count: b.count, + bounds: self.bounds.clone(), + bucket_counts: b.counts, + sum: if self.record_sum { + b.total + } else { + T::default() + }, + min: if self.record_min_max { + Some(b.min) + } else { + None + }, + max: if self.record_min_max { + Some(b.max) + } else { + None + }, + exemplars: vec![], + } + }); + + (h.data_points.len(), new_agg.map(Into::into)) + } + + fn cumulative(&self, dest: Option<&mut MetricData>) -> (usize, Option>) { + let time = self.init_time.cumulative(); + let h = dest.and_then(|d| { + if let MetricData::Histogram(hist) = d { + Some(hist) + } else { + None + } + }); + let mut new_agg = if h.is_none() { + Some(data::Histogram { + data_points: vec![], + start_time: time.start, + time: time.current, + temporality: Temporality::Cumulative, + }) + } else { + None + }; + let h = h.unwrap_or_else(|| new_agg.as_mut().expect("present if h is none")); + h.temporality = Temporality::Cumulative; + h.start_time = time.start; + h.time = time.current; + + self.value_map + .collect_readonly(&mut h.data_points, |attributes, aggr| { + let b = aggr.lock().unwrap_or_else(|err| err.into_inner()); + HistogramDataPoint { + attributes, + count: b.count, + bounds: self.bounds.clone(), + bucket_counts: b.counts.clone(), + sum: if self.record_sum { + b.total + } else { + T::default() + }, + min: if self.record_min_max { + Some(b.min) + } else { + None + }, + max: if self.record_min_max { + Some(b.max) + } else { + None + }, + exemplars: vec![], + } + }); + + (h.data_points.len(), new_agg.map(Into::into)) + } +} + +impl Measure for Histogram +where + T: Number, +{ + fn call(&self, measurement: T, attrs: &[KeyValue]) { + let f = measurement.into_float(); + // Ignore NaN and infinity: `x < NaN` is always false, so NaN would land + // in bucket 0 and, via `Buckets::total += value`, permanently corrupt + // `sum` since `x + NaN == NaN` for any `x`. + if !f.is_finite() { + return; + } + // This search will return an index in the range `[0, bounds.len()]`, where + // it will return `bounds.len()` if value is greater than the last element + // of `bounds`. This aligns with the buckets in that the length of buckets + // is `bounds.len()+1`, with the last bucket representing: + // `(bounds[bounds.len()-1], +∞)`. + let index = self.bounds.partition_point(|&x| x < f); + + self.filter.apply(attrs, |filtered| { + self.value_map.measure((measurement, index), filtered); + }) + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind(&self, attrs: &[KeyValue]) -> Box> { + let mut bound_attrs = Vec::new(); + self.filter.apply(attrs, |filtered| { + bound_attrs = filtered.to_vec(); + }); + match self.value_map.bind(&bound_attrs) { + Some(tracker) => Box::new(BoundHistogramHandle { + tracker, + bounds: self.bounds.clone(), + }), + // Trackers RwLock is poisoned — return a noop handle so writes + // silently drop, mirroring `measure()`'s own poison handling. + None => Box::new(NoopBoundMeasure::new()), + } + } +} + +impl ComputeAggregation for Histogram +where + T: Number, +{ + fn call(&self, dest: Option<&mut AggregatedMetrics>) -> (usize, Option) { + let data = dest.and_then(|d| T::extract_metrics_data_mut(d)); + let (len, new) = match self.temporality { + Temporality::Delta => self.delta(data), + _ => self.cumulative(data), + }; + (len, new.map(T::make_aggregated_metrics)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_buckets_are_selected_correctly() { + let hist = Histogram::::new( + Temporality::Cumulative, + AttributeSetFilter::new(None), + vec![1.0, 3.0, 6.0], + false, + false, + 2000, + ); + for v in 1..11 { + Measure::call(&hist, v, &[]); + } + let (count, dp) = ComputeAggregation::call(&hist, None); + let dp = dp.unwrap(); + let AggregatedMetrics::I64(MetricData::Histogram(dp)) = dp else { + unreachable!() + }; + assert_eq!(count, 1); + assert_eq!(dp.data_points[0].count, 10); + assert_eq!(dp.data_points[0].bucket_counts.len(), 4); + assert_eq!(dp.data_points[0].bucket_counts[0], 1); // 1 + assert_eq!(dp.data_points[0].bucket_counts[1], 2); // 2, 3 + assert_eq!(dp.data_points[0].bucket_counts[2], 3); // 4, 5, 6 + assert_eq!(dp.data_points[0].bucket_counts[3], 4); // 7, 8, 9, 10 + } + + #[test] + fn nan_and_infinity_are_ignored() { + let hist = Histogram::::new( + Temporality::Cumulative, + AttributeSetFilter::new(None), + vec![1.0, 3.0, 6.0], + true, + true, + 2000, + ); + Measure::call(&hist, 1.0, &[]); + Measure::call(&hist, f64::NAN, &[]); + Measure::call(&hist, f64::INFINITY, &[]); + Measure::call(&hist, f64::NEG_INFINITY, &[]); + Measure::call(&hist, 2.0, &[]); + + let (count, dp) = ComputeAggregation::call(&hist, None); + let dp = dp.unwrap(); + let AggregatedMetrics::F64(MetricData::Histogram(dp)) = dp else { + unreachable!() + }; + assert_eq!(count, 1); + assert_eq!(dp.data_points[0].count, 2); + assert_eq!(dp.data_points[0].sum, 3.0); + assert!(!dp.data_points[0].sum.is_nan()); + } +} + +#[cfg(all(test, feature = "experimental_metrics_bound_instruments"))] +mod bound_tests { + use super::*; + + #[test] + fn bound_nan_and_infinity_are_ignored() { + let hist = Histogram::::new( + Temporality::Cumulative, + AttributeSetFilter::new(None), + vec![1.0, 3.0, 6.0], + true, + true, + 2000, + ); + let attrs = [KeyValue::new("k", "v")]; + let bound = Measure::bind(&hist, &attrs); + + bound.call(1.0); + bound.call(f64::NAN); + bound.call(f64::INFINITY); + bound.call(f64::NEG_INFINITY); + bound.call(2.0); + + let (count, dp) = ComputeAggregation::call(&hist, None); + let dp = dp.unwrap(); + let AggregatedMetrics::F64(MetricData::Histogram(dp)) = dp else { + unreachable!() + }; + assert_eq!(count, 1); + assert_eq!(dp.data_points[0].count, 2); + assert_eq!(dp.data_points[0].sum, 3.0); + assert!(!dp.data_points[0].sum.is_nan()); + assert_eq!(dp.data_points[0].attributes, attrs.to_vec()); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/last_value.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/last_value.rs new file mode 100644 index 00000000..4b0bcb93 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/last_value.rs @@ -0,0 +1,330 @@ +use crate::metrics::{ + data::{self, AggregatedMetrics, GaugeDataPoint, MetricData}, + Temporality, +}; +use opentelemetry::KeyValue; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use std::sync::atomic::Ordering; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use std::sync::Arc; + +use super::{ + aggregate::{AggregateTimeInitiator, AttributeSetFilter}, + Aggregator, AtomicTracker, AtomicallyUpdate, ComputeAggregation, Measure, Number, ValueMap, +}; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use super::{BoundMeasure, NoopBoundMeasure, TrackerEntry}; + +/// Pre-bound gauge/last-value handle: writes go directly to a fixed +/// `TrackerEntry` without per-call attribute lookup. The `tracker` is either +/// a dedicated entry for the bound attribute set, or — if bind() hit the +/// cardinality limit — the shared overflow tracker. +#[cfg(feature = "experimental_metrics_bound_instruments")] +struct BoundLastValueHandle { + tracker: Arc>>, +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundMeasure for BoundLastValueHandle { + fn call(&self, measurement: T) { + // Mirror unbound LastValue::call: ignore NaN and infinity so a bad + // measurement can't be exported before the next valid one overwrites it. + if !measurement.into_float().is_finite() { + return; + } + self.tracker.aggregator.update(measurement); + self.tracker.has_been_updated.store(true, Ordering::Release); + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl Drop for BoundLastValueHandle { + fn drop(&mut self) { + self.tracker.bound_count.fetch_sub(1, Ordering::Relaxed); + } +} + +/// this is reused by PrecomputedSum +pub(crate) struct Assign +where + T: AtomicallyUpdate, +{ + pub(crate) value: T::AtomicTracker, +} + +impl Aggregator for Assign +where + T: Number, +{ + type InitConfig = (); + type PreComputedValue = T; + + fn create(_init: &()) -> Self { + Self { + value: T::new_atomic_tracker(T::default()), + } + } + + fn update(&self, value: T) { + self.value.store(value) + } + + fn clone_and_reset(&self, _: &()) -> Self { + Self { + value: T::new_atomic_tracker(self.value.get_and_reset_value()), + } + } +} + +/// Summarizes a set of measurements as the last one made. +pub(crate) struct LastValue { + value_map: ValueMap>, + init_time: AggregateTimeInitiator, + temporality: Temporality, + filter: AttributeSetFilter, +} + +impl LastValue { + pub(crate) fn new( + temporality: Temporality, + filter: AttributeSetFilter, + cardinality_limit: usize, + ) -> Self { + LastValue { + value_map: ValueMap::new((), cardinality_limit), + init_time: AggregateTimeInitiator::default(), + temporality, + filter, + } + } + + pub(crate) fn delta(&self, dest: Option<&mut MetricData>) -> (usize, Option>) { + let time = self.init_time.delta(); + + let s_data = dest.and_then(|d| { + if let MetricData::Gauge(gauge) = d { + Some(gauge) + } else { + None + } + }); + let mut new_agg = if s_data.is_none() { + Some(data::Gauge { + data_points: vec![], + start_time: Some(time.start), + time: time.current, + }) + } else { + None + }; + let s_data = s_data.unwrap_or_else(|| new_agg.as_mut().expect("present if s_data is none")); + s_data.start_time = Some(time.start); + s_data.time = time.current; + + self.value_map + .collect_and_reset(&mut s_data.data_points, |attributes, aggr| GaugeDataPoint { + attributes, + value: aggr.value.get_and_reset_value(), + exemplars: vec![], + }); + + (s_data.data_points.len(), new_agg.map(Into::into)) + } + + pub(crate) fn cumulative( + &self, + dest: Option<&mut MetricData>, + ) -> (usize, Option>) { + let time = self.init_time.cumulative(); + let s_data = dest.and_then(|d| { + if let MetricData::Gauge(gauge) = d { + Some(gauge) + } else { + None + } + }); + let mut new_agg = if s_data.is_none() { + Some(data::Gauge { + data_points: vec![], + start_time: Some(time.start), + time: time.current, + }) + } else { + None + }; + let s_data = s_data.unwrap_or_else(|| new_agg.as_mut().expect("present if s_data is none")); + + s_data.start_time = Some(time.start); + s_data.time = time.current; + + self.value_map + .collect_readonly(&mut s_data.data_points, |attributes, aggr| GaugeDataPoint { + attributes, + value: aggr.value.get_value(), + exemplars: vec![], + }); + + (s_data.data_points.len(), new_agg.map(Into::into)) + } +} + +impl Measure for LastValue +where + T: Number, +{ + fn call(&self, measurement: T, attrs: &[KeyValue]) { + // Ignore NaN and infinity: `Assign::update` overwrites unconditionally, + // so a bad value would still be exported once before the next valid + // measurement replaces it. + if !measurement.into_float().is_finite() { + return; + } + self.filter.apply(attrs, |filtered| { + self.value_map.measure(measurement, filtered); + }) + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind(&self, attrs: &[KeyValue]) -> Box> { + let mut bound_attrs = Vec::new(); + self.filter.apply(attrs, |filtered| { + bound_attrs = filtered.to_vec(); + }); + match self.value_map.bind(&bound_attrs) { + Some(tracker) => Box::new(BoundLastValueHandle { tracker }), + None => Box::new(NoopBoundMeasure::new()), + } + } +} + +impl ComputeAggregation for LastValue +where + T: Number, +{ + fn call(&self, dest: Option<&mut AggregatedMetrics>) -> (usize, Option) { + let data = dest.and_then(|d| T::extract_metrics_data_mut(d)); + let (len, new) = match self.temporality { + Temporality::Delta => self.delta(data), + _ => self.cumulative(data), + }; + (len, new.map(T::make_aggregated_metrics)) + } +} + +#[cfg(test)] +mod nan_guard_tests { + use super::*; + use crate::metrics::data::{AggregatedMetrics, MetricData}; + + #[test] + fn nan_and_infinity_are_ignored() { + let last_value = + LastValue::::new(Temporality::Cumulative, AttributeSetFilter::new(None), 2000); + Measure::call(&last_value, 1.0, &[]); + Measure::call(&last_value, f64::NAN, &[]); + Measure::call(&last_value, f64::INFINITY, &[]); + Measure::call(&last_value, f64::NEG_INFINITY, &[]); + + let (count, dp) = ComputeAggregation::call(&last_value, None); + let dp = dp.unwrap(); + let AggregatedMetrics::F64(MetricData::Gauge(dp)) = dp else { + unreachable!() + }; + assert_eq!(count, 1); + assert_eq!(dp.data_points[0].value, 1.0); + } +} + +#[cfg(all(test, feature = "experimental_metrics_bound_instruments"))] +mod tests { + use super::*; + use crate::metrics::data::{AggregatedMetrics, Gauge, MetricData}; + + fn extract_gauge(agg: AggregatedMetrics) -> Gauge { + match agg { + AggregatedMetrics::U64(MetricData::Gauge(g)) => g, + _ => panic!("expected u64 Gauge"), + } + } + + /// Direct unit coverage for `LastValue::bind`. Sync `Gauge::bind()` is not yet + /// exposed in the public API, so the only callers of this code path today + /// are Views that remap an instrument to `Aggregation::LastValue`. This test + /// constructs the aggregator directly and exercises the bound handle through + /// the `Measure` / `BoundMeasure` traits to keep the impl honest. + #[test] + fn bind_writes_through_bound_handle() { + let last_value = + LastValue::::new(Temporality::Cumulative, AttributeSetFilter::new(None), 100); + let attrs = [KeyValue::new("k", "v")]; + let bound = Measure::bind(&last_value, &attrs); + + bound.call(7); + bound.call(42); // overwrites previous value (LastValue semantics) + + let (count, agg) = ComputeAggregation::call(&last_value, None); + assert_eq!(count, 1); + let gauge = extract_gauge(agg.expect("aggregation produced")); + assert_eq!(gauge.data_points.len(), 1); + assert_eq!(gauge.data_points[0].value, 42); + assert_eq!(gauge.data_points[0].attributes, attrs.to_vec()); + } + + #[test] + fn bound_handle_drop_decrements_bound_count() { + let last_value = + LastValue::::new(Temporality::Delta, AttributeSetFilter::new(None), 100); + let attrs = [KeyValue::new("k", "v")]; + + let bound = Measure::bind(&last_value, &attrs); + bound.call(5); + + // While the handle exists, the entry's bound_count is 1. + let trackers = last_value.value_map.trackers.read().unwrap(); + let entry = trackers + .values() + .next() + .expect("entry should exist after bind+call"); + assert_eq!( + entry.bound_count.load(Ordering::Relaxed), + 1, + "bound_count should reflect a live handle" + ); + drop(trackers); + + drop(bound); + + let trackers = last_value.value_map.trackers.read().unwrap(); + let entry = trackers + .values() + .next() + .expect("entry should still exist post-drop"); + assert_eq!( + entry.bound_count.load(Ordering::Relaxed), + 0, + "bound_count should drop to 0 after handle drops" + ); + } + + #[test] + fn bound_nan_and_infinity_are_ignored() { + let last_value = + LastValue::::new(Temporality::Cumulative, AttributeSetFilter::new(None), 2000); + let attrs = [KeyValue::new("k", "v")]; + let bound = Measure::bind(&last_value, &attrs); + + bound.call(1.0); + bound.call(f64::NAN); + bound.call(f64::INFINITY); + bound.call(f64::NEG_INFINITY); + + let (count, dp) = ComputeAggregation::call(&last_value, None); + let dp = dp.unwrap(); + let AggregatedMetrics::F64(MetricData::Gauge(dp)) = dp else { + unreachable!() + }; + assert_eq!(count, 1); + assert_eq!(dp.data_points[0].value, 1.0); + assert_eq!(dp.data_points[0].attributes, attrs.to_vec()); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/mod.rs new file mode 100644 index 00000000..6bb4900f --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/mod.rs @@ -0,0 +1,922 @@ +mod aggregate; +mod exponential_histogram; +mod histogram; +mod last_value; +mod precomputed_sum; +mod sum; + +use core::fmt; +#[cfg(not(target_has_atomic = "64"))] +use portable_atomic::{AtomicI64, AtomicU64}; +use std::collections::{HashMap, HashSet}; +use std::ops::{Add, AddAssign, Sub}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +#[cfg(target_has_atomic = "64")] +use std::sync::atomic::{AtomicI64, AtomicU64}; +use std::sync::{Arc, OnceLock, RwLock}; + +pub(crate) use aggregate::{AggregateBuilder, AggregateFns, ComputeAggregation, Measure}; +#[cfg(feature = "experimental_metrics_bound_instruments")] +pub(crate) use aggregate::{BoundMeasure, NoopBoundMeasure}; +pub(crate) use exponential_histogram::{EXPO_MAX_SCALE, EXPO_MIN_SCALE}; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use opentelemetry::otel_debug; +use opentelemetry::{otel_warn, KeyValue}; + +use super::data::{AggregatedMetrics, MetricData}; + +// TODO Replace it with LazyLock once it is stable +pub(crate) static STREAM_OVERFLOW_ATTRIBUTES: OnceLock> = OnceLock::new(); + +#[inline] +fn stream_overflow_attributes() -> &'static Vec { + STREAM_OVERFLOW_ATTRIBUTES.get_or_init(|| vec![KeyValue::new("otel.metric.overflow", true)]) +} + +pub(crate) trait Aggregator { + /// A static configuration that is needed in order to initialize aggregator. + /// E.g. bucket_size at creation time . + type InitConfig; + + /// Some aggregators can do some computations before updating aggregator. + /// This helps to reduce contention for aggregators because it makes + /// [`Aggregator::update`] as short as possible. + type PreComputedValue; + + /// Called everytime a new attribute-set is stored. + fn create(init: &Self::InitConfig) -> Self; + + /// Called for each measurement. + fn update(&self, value: Self::PreComputedValue); + + /// Return current value and reset this instance + fn clone_and_reset(&self, init: &Self::InitConfig) -> Self; +} + +/// Wraps an aggregator with status tracking for delta collection and bound instruments. +/// +/// `has_been_updated` tracks whether the aggregator received measurements since the last +/// collection cycle. This enables in-place delta collection: only updated entries are exported, +/// and stale unbound entries are evicted to prevent unbounded memory growth. +/// +/// `bound_count` tracks how many bound instrument handles reference this entry. Entries with +/// bound_count > 0 are never evicted from the map, even if they had no updates in a cycle +/// (they simply produce no export). This ensures bound handles always point to a live tracker. +pub(crate) struct TrackerEntry { + pub(crate) aggregator: A, + pub(crate) has_been_updated: AtomicBool, + pub(crate) bound_count: AtomicUsize, +} + +impl TrackerEntry { + fn new(config: &A::InitConfig) -> Self { + TrackerEntry { + aggregator: A::create(config), + has_been_updated: AtomicBool::new(false), + bound_count: AtomicUsize::new(0), + } + } +} + +/// Map from attribute sets to their aggregator tracker entries. +type TrackerMap = HashMap, Arc>>; + +/// The storage for sums. +/// +/// This structure is parametrized by an `Operation` that indicates how +/// updates to the underlying value trackers should be performed. +pub(crate) struct ValueMap +where + A: Aggregator, +{ + /// Trackers store the values associated with different attribute sets. + trackers: RwLock>, + + /// Number of different attribute set stored in the `trackers` map. + count: AtomicUsize, + /// Tracker for values with no attributes attached. + no_attribute_tracker: Arc>, + /// Configuration for an Aggregator + config: A::InitConfig, + cardinality_limit: usize, +} + +impl ValueMap +where + A: Aggregator, +{ + pub(crate) fn config(&self) -> &A::InitConfig { + &self.config + } + + fn new(config: A::InitConfig, cardinality_limit: usize) -> Self { + ValueMap { + trackers: RwLock::new(HashMap::with_capacity(1 + cardinality_limit)), + no_attribute_tracker: Arc::new(TrackerEntry::new(&config)), + count: AtomicUsize::new(0), + config, + cardinality_limit, + } + } + + /// Checks whether aggregator has hit cardinality limit for metric streams + fn is_under_cardinality_limit(&self) -> bool { + self.count.load(Ordering::SeqCst) < self.cardinality_limit + } + + fn measure(&self, value: A::PreComputedValue, attributes: &[KeyValue]) { + if attributes.is_empty() { + self.no_attribute_tracker.aggregator.update(value); + self.no_attribute_tracker + .has_been_updated + .store(true, Ordering::Release); + return; + } + + let Ok(trackers) = self.trackers.read() else { + return; + }; + + // Try to retrieve and update the tracker with the attributes in the provided order first + if let Some(tracker) = trackers.get(attributes) { + tracker.aggregator.update(value); + tracker.has_been_updated.store(true, Ordering::Release); + return; + } + + // Try to retrieve and update the tracker with the attributes sorted. + let sorted_attrs = sort_and_dedup(attributes); + if let Some(tracker) = trackers.get(sorted_attrs.as_slice()) { + tracker.aggregator.update(value); + tracker.has_been_updated.store(true, Ordering::Release); + return; + } + + // Give up the read lock before acquiring the write lock. + drop(trackers); + + let Ok(mut trackers) = self.trackers.write() else { + return; + }; + + // Recheck both the provided and sorted orders after acquiring the write lock + // in case another thread has pushed an update in the meantime. + if let Some(tracker) = trackers.get(attributes) { + tracker.aggregator.update(value); + tracker.has_been_updated.store(true, Ordering::Release); + } else if let Some(tracker) = trackers.get(sorted_attrs.as_slice()) { + tracker.aggregator.update(value); + tracker.has_been_updated.store(true, Ordering::Release); + } else if self.is_under_cardinality_limit() { + let new_tracker = Arc::new(TrackerEntry::::new(&self.config)); + new_tracker.aggregator.update(value); + new_tracker.has_been_updated.store(true, Ordering::Release); + + // Insert tracker with the attributes in the provided and sorted orders + trackers.insert(attributes.to_vec(), new_tracker.clone()); + trackers.insert(sorted_attrs, new_tracker); + + self.count.fetch_add(1, Ordering::SeqCst); + } else if let Some(overflow_value) = trackers.get(stream_overflow_attributes().as_slice()) { + overflow_value.aggregator.update(value); + overflow_value + .has_been_updated + .store(true, Ordering::Release); + } else { + let new_tracker = TrackerEntry::::new(&self.config); + new_tracker.aggregator.update(value); + new_tracker.has_been_updated.store(true, Ordering::Release); + trackers.insert(stream_overflow_attributes().clone(), Arc::new(new_tracker)); + } + } + + /// Resolves attributes and returns a cached Arc for bound instruments. + /// The caller can then call `tracker.aggregator.update()` directly, bypassing the + /// full lookup path on subsequent measurements. + /// + /// When the cardinality limit has been reached, the returned tracker is the + /// overflow tracker (the same one unbound `measure()` calls write to at + /// overflow), preserving the bind() perf contract — every subsequent + /// `bound.add()` call is a direct write, regardless of cardinality state. + /// The handle remains permanently bound to overflow for its lifetime; + /// to recover after space frees up, drop and re-bind. + /// + /// Returns `None` only if the trackers RwLock is poisoned, in which case + /// the caller should produce a noop bound handle so measurements are + /// silently dropped rather than panicking on the user's hot path. + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind(&self, attributes: &[KeyValue]) -> Option>> { + if attributes.is_empty() { + self.no_attribute_tracker + .bound_count + .fetch_add(1, Ordering::Relaxed); + return Some(Arc::clone(&self.no_attribute_tracker)); + } + + let sorted_attrs = sort_and_dedup(attributes); + self.bind_attrs(attributes, sorted_attrs) + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind_attrs( + &self, + original: &[KeyValue], + sorted_attrs: Vec, + ) -> Option>> { + // Fast path: read lock lookup using the canonical (sorted) key. + if let Ok(trackers) = self.trackers.read() { + if let Some(tracker) = trackers.get(sorted_attrs.as_slice()) { + tracker.bound_count.fetch_add(1, Ordering::Relaxed); + return Some(Arc::clone(tracker)); + } + } + + // Slow path: write lock, insert if missing. + let Ok(mut trackers) = self.trackers.write() else { + // Lock poisoned — caller will produce a noop bound handle. + return None; + }; + + // Recheck after acquiring write lock. + if let Some(tracker) = trackers.get(sorted_attrs.as_slice()) { + tracker.bound_count.fetch_add(1, Ordering::Relaxed); + return Some(Arc::clone(tracker)); + } + + if self.is_under_cardinality_limit() { + let new_tracker = Arc::new(TrackerEntry::::new(&self.config)); + new_tracker.bound_count.fetch_add(1, Ordering::Relaxed); + // Insert with both the original and sorted orderings so subsequent + // unbound measure() calls hit the fast path regardless of attr order. + // Mirrors `measure()`'s insert pattern. + if original != sorted_attrs.as_slice() { + trackers.insert(original.to_vec(), new_tracker.clone()); + } + trackers.insert(sorted_attrs, new_tracker.clone()); + self.count.fetch_add(1, Ordering::SeqCst); + Some(new_tracker) + } else { + // Over cardinality limit — bind directly to the overflow tracker so + // the bound handle keeps its perf contract (no per-call lookup) and + // its writes land predictably in the overflow bucket. This matches + // the spec SHOULD that the SDK pre-resolve aggregator state at bind + // time, and the spec MUST that bound recordings behave identically + // to unbound recordings (which themselves route to overflow once + // cardinality is exhausted). See open-telemetry/opentelemetry-specification#5050. + // + // The overflow tracker is created lazily here if it doesn't exist + // yet — mirrors the lazy creation in `measure()` (line above where + // overflow is inserted on first overflowing measurement). + let overflow_tracker = trackers + .entry(stream_overflow_attributes().clone()) + .or_insert_with(|| Arc::new(TrackerEntry::::new(&self.config))) + .clone(); + overflow_tracker.bound_count.fetch_add(1, Ordering::Relaxed); + otel_debug!( + name: "BoundInstrument.CardinalityOverflow", + message = "bind() called at cardinality limit, attributing to overflow bucket" + ); + Some(overflow_tracker) + } + } + + /// Iterate through all attribute sets and populate `DataPoints` in readonly mode. + /// This is used for synchronous instruments (Counter, Histogram, etc.) in Cumulative temporality mode, + /// where attribute sets persist across collection cycles and [`ValueMap`] is not cleared. + pub(crate) fn collect_readonly(&self, dest: &mut Vec, mut map_fn: MapFn) + where + MapFn: FnMut(Vec, &A) -> Res, + { + prepare_data(dest, self.count.load(Ordering::SeqCst)); + if self + .no_attribute_tracker + .has_been_updated + .load(Ordering::Acquire) + { + dest.push(map_fn(vec![], &self.no_attribute_tracker.aggregator)); + } + + let Ok(trackers) = self.trackers.read() else { + return; + }; + + let mut seen = HashSet::new(); + for (attrs, tracker) in trackers.iter() { + if seen.insert(Arc::as_ptr(tracker)) { + dest.push(map_fn(attrs.clone(), &tracker.aggregator)); + } + } + } + + /// Iterate through all attribute sets in-place, populate `DataPoints` and reset. + /// Only entries updated since the last collection (tracked via `has_been_updated`) + /// are exported. Stale unbound entries are evicted to prevent unbounded memory growth. + /// Bound entries (bound_count > 0) are never evicted — they persist until explicitly + /// unbound, but produce no export when they have no updates. + /// + /// Used for synchronous instruments (Counter, Histogram, etc.) in Delta temporality mode. + pub(crate) fn collect_and_reset(&self, dest: &mut Vec, mut map_fn: MapFn) + where + MapFn: FnMut(Vec, &A) -> Res, + { + prepare_data(dest, self.count.load(Ordering::SeqCst)); + if self + .no_attribute_tracker + .has_been_updated + .swap(false, Ordering::AcqRel) + { + dest.push(map_fn(vec![], &self.no_attribute_tracker.aggregator)); + } + + let overflow_attrs = stream_overflow_attributes(); + let mut stale_entries: Vec>> = Vec::new(); + + { + let Ok(trackers) = self.trackers.read() else { + return; + }; + + let mut seen = HashSet::new(); + for (attrs, tracker) in trackers.iter() { + if seen.insert(Arc::as_ptr(tracker)) { + if tracker.has_been_updated.swap(false, Ordering::Acquire) { + dest.push(map_fn(attrs.clone(), &tracker.aggregator)); + } else if attrs.as_slice() != overflow_attrs.as_slice() + && tracker.bound_count.load(Ordering::Relaxed) == 0 + { + // Stale and not bound — candidate for eviction + stale_entries.push(Arc::clone(tracker)); + } + } + } + // Read lock released here + } + + if !stale_entries.is_empty() { + if let Ok(mut trackers) = self.trackers.write() { + // Re-check under write lock to avoid TOCTOU race: a measure() or bind() call + // between dropping the read lock and acquiring the write lock could have + // updated an entry or bound a handle to one we marked as stale. + stale_entries.retain(|entry| { + !entry.has_been_updated.load(Ordering::Acquire) + && entry.bound_count.load(Ordering::Acquire) == 0 + }); + + if !stale_entries.is_empty() { + let stale_pointers: HashSet<*const TrackerEntry> = + stale_entries.iter().map(Arc::as_ptr).collect(); + trackers.retain(|_, tracker| !stale_pointers.contains(&Arc::as_ptr(tracker))); + self.count.fetch_sub(stale_entries.len(), Ordering::SeqCst); + } + } + } + } + + /// Iterate through all attribute sets, populate `DataPoints` and reset by draining the map. + /// This is used for asynchronous instruments (Observable/PrecomputedSum) in both Delta and + /// Cumulative temporality modes, where map clearing is needed for staleness detection. + pub(crate) fn drain_and_reset(&self, dest: &mut Vec, mut map_fn: MapFn) + where + MapFn: FnMut(Vec, A) -> Res, + { + prepare_data(dest, self.count.load(Ordering::SeqCst)); + if self + .no_attribute_tracker + .has_been_updated + .swap(false, Ordering::AcqRel) + { + dest.push(map_fn( + vec![], + self.no_attribute_tracker + .aggregator + .clone_and_reset(&self.config), + )); + } + + let old_trackers = { + let Ok(mut trackers) = self.trackers.write() else { + otel_warn!(name: "MeterProvider.InternalError", message = "Metric collection failed. Report this issue in OpenTelemetry repo.", details ="ValueMap trackers lock poisoned"); + return; + }; + self.count.store(0, Ordering::SeqCst); + std::mem::take(&mut *trackers) + // Write lock released here + }; + + let mut seen = HashSet::new(); + for (attrs, tracker) in old_trackers { + if seen.insert(Arc::as_ptr(&tracker)) { + dest.push(map_fn( + attrs, + tracker.aggregator.clone_and_reset(&self.config), + )); + } + } + } +} + +/// Clear and allocate exactly required amount of space for all attribute-sets +fn prepare_data(data: &mut Vec, list_len: usize) { + data.clear(); + let total_len = list_len + 2; // to account for no_attributes case + overflow state + if total_len > data.capacity() { + data.reserve_exact(total_len - data.capacity()); + } +} + +fn sort_and_dedup(attributes: &[KeyValue]) -> Vec { + // Use newly allocated vec here as incoming attributes are immutable so + // cannot sort/de-dup in-place. TODO: This allocation can be avoided by + // leveraging a ThreadLocal vec. + let mut sorted = attributes.to_vec(); + sorted.sort_unstable_by(|a, b| a.key.cmp(&b.key)); + sorted.dedup_by(|a, b| a.key == b.key); + sorted +} + +/// Marks a type that can have a value added and retrieved atomically. Required since +/// different types have different backing atomic mechanisms +pub(crate) trait AtomicTracker: Sync + Send + 'static { + fn store(&self, _value: T); + fn add(&self, _value: T); + fn get_value(&self) -> T; + fn get_and_reset_value(&self) -> T; +} + +/// Marks a type that can have an atomic tracker generated for it +pub(crate) trait AtomicallyUpdate { + type AtomicTracker: AtomicTracker; + fn new_atomic_tracker(init: T) -> Self::AtomicTracker; +} + +pub(crate) trait AggregatedMetricsAccess: Sized { + /// This function is used in tests. + #[allow(unused)] + fn extract_metrics_data_ref(data: &AggregatedMetrics) -> Option<&MetricData>; + fn extract_metrics_data_mut(data: &mut AggregatedMetrics) -> Option<&mut MetricData>; + fn make_aggregated_metrics(data: MetricData) -> AggregatedMetrics; +} + +pub(crate) trait Number: + Add + + AddAssign + + Sub + + PartialOrd + + fmt::Debug + + Clone + + Copy + + PartialEq + + Default + + Send + + Sync + + 'static + + AtomicallyUpdate + + AggregatedMetricsAccess +{ + fn min() -> Self; + fn max() -> Self; + + fn into_float(self) -> f64; +} + +impl Number for i64 { + fn min() -> Self { + i64::MIN + } + + fn max() -> Self { + i64::MAX + } + + fn into_float(self) -> f64 { + // May have precision loss at high values + self as f64 + } +} +impl Number for u64 { + fn min() -> Self { + u64::MIN + } + + fn max() -> Self { + u64::MAX + } + + fn into_float(self) -> f64 { + // May have precision loss at high values + self as f64 + } +} +impl Number for f64 { + fn min() -> Self { + f64::MIN + } + + fn max() -> Self { + f64::MAX + } + + fn into_float(self) -> f64 { + self + } +} + +impl AggregatedMetricsAccess for i64 { + fn make_aggregated_metrics(data: MetricData) -> AggregatedMetrics { + AggregatedMetrics::I64(data) + } + + fn extract_metrics_data_ref(data: &AggregatedMetrics) -> Option<&MetricData> { + if let AggregatedMetrics::I64(data) = data { + Some(data) + } else { + None + } + } + + fn extract_metrics_data_mut(data: &mut AggregatedMetrics) -> Option<&mut MetricData> { + if let AggregatedMetrics::I64(data) = data { + Some(data) + } else { + None + } + } +} + +impl AggregatedMetricsAccess for u64 { + fn make_aggregated_metrics(data: MetricData) -> AggregatedMetrics { + AggregatedMetrics::U64(data) + } + + fn extract_metrics_data_ref(data: &AggregatedMetrics) -> Option<&MetricData> { + if let AggregatedMetrics::U64(data) = data { + Some(data) + } else { + None + } + } + + fn extract_metrics_data_mut(data: &mut AggregatedMetrics) -> Option<&mut MetricData> { + if let AggregatedMetrics::U64(data) = data { + Some(data) + } else { + None + } + } +} + +impl AggregatedMetricsAccess for f64 { + fn make_aggregated_metrics(data: MetricData) -> AggregatedMetrics { + AggregatedMetrics::F64(data) + } + + fn extract_metrics_data_ref(data: &AggregatedMetrics) -> Option<&MetricData> { + if let AggregatedMetrics::F64(data) = data { + Some(data) + } else { + None + } + } + + fn extract_metrics_data_mut(data: &mut AggregatedMetrics) -> Option<&mut MetricData> { + if let AggregatedMetrics::F64(data) = data { + Some(data) + } else { + None + } + } +} + +impl AtomicTracker for AtomicU64 { + fn store(&self, value: u64) { + self.store(value, Ordering::Relaxed); + } + + fn add(&self, value: u64) { + self.fetch_add(value, Ordering::Relaxed); + } + + fn get_value(&self) -> u64 { + self.load(Ordering::Relaxed) + } + + fn get_and_reset_value(&self) -> u64 { + self.swap(0, Ordering::Relaxed) + } +} + +impl AtomicallyUpdate for u64 { + type AtomicTracker = AtomicU64; + + fn new_atomic_tracker(init: u64) -> Self::AtomicTracker { + AtomicU64::new(init) + } +} + +impl AtomicTracker for AtomicI64 { + fn store(&self, value: i64) { + self.store(value, Ordering::Relaxed); + } + + fn add(&self, value: i64) { + self.fetch_add(value, Ordering::Relaxed); + } + + fn get_value(&self) -> i64 { + self.load(Ordering::Relaxed) + } + + fn get_and_reset_value(&self) -> i64 { + self.swap(0, Ordering::Relaxed) + } +} + +impl AtomicallyUpdate for i64 { + type AtomicTracker = AtomicI64; + + fn new_atomic_tracker(init: i64) -> Self::AtomicTracker { + AtomicI64::new(init) + } +} + +pub(crate) struct F64AtomicTracker { + inner: AtomicU64, // Floating points don't have true atomics, so we need to use the their binary representation to perform atomic operations +} + +impl F64AtomicTracker { + fn new(init: f64) -> Self { + let value_as_u64 = init.to_bits(); + F64AtomicTracker { + inner: AtomicU64::new(value_as_u64), + } + } +} + +impl AtomicTracker for F64AtomicTracker { + fn store(&self, value: f64) { + let value_as_u64 = value.to_bits(); + self.inner.store(value_as_u64, Ordering::Relaxed); + } + + fn add(&self, value: f64) { + let mut current_value_as_u64 = self.inner.load(Ordering::Relaxed); + + loop { + let current_value = f64::from_bits(current_value_as_u64); + let new_value = current_value + value; + let new_value_as_u64 = new_value.to_bits(); + match self.inner.compare_exchange( + current_value_as_u64, + new_value_as_u64, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + // Succeeded in updating the value + Ok(_) => return, + + // Some other thread changed the value before this thread could update it. + // Read the latest value again and try to swap it with the recomputed `new_value_as_u64`. + Err(v) => current_value_as_u64 = v, + } + } + } + + fn get_value(&self) -> f64 { + let value_as_u64 = self.inner.load(Ordering::Relaxed); + f64::from_bits(value_as_u64) + } + + fn get_and_reset_value(&self) -> f64 { + let zero_as_u64 = 0.0_f64.to_bits(); + let value = self.inner.swap(zero_as_u64, Ordering::Relaxed); + f64::from_bits(value) + } +} + +impl AtomicallyUpdate for f64 { + type AtomicTracker = F64AtomicTracker; + + fn new_atomic_tracker(init: f64) -> Self::AtomicTracker { + F64AtomicTracker::new(init) + } +} + +#[cfg(test)] +mod tests { + use crate::metrics::internal::last_value::Assign; + + use super::*; + + // Test helpers that return boxed trait objects to avoid method shadowing + // from portable-atomic's inherent methods + fn new_u64_tracker(init: u64) -> Box> { + Box::new(u64::new_atomic_tracker(init)) + } + + fn new_i64_tracker(init: i64) -> Box> { + Box::new(i64::new_atomic_tracker(init)) + } + + #[test] + fn can_store_u64_atomic_value() { + let atomic = new_u64_tracker(0); + + let value = atomic.get_value(); + assert_eq!(value, 0); + + atomic.store(25); + let value = atomic.get_value(); + assert_eq!(value, 25); + } + + #[test] + fn can_add_and_get_u64_atomic_value() { + let atomic = new_u64_tracker(0); + atomic.add(15); + atomic.add(10); + + let value = atomic.get_value(); + assert_eq!(value, 25); + } + + #[test] + fn can_reset_u64_atomic_value() { + let atomic = new_u64_tracker(0); + atomic.add(15); + + let value = atomic.get_and_reset_value(); + let value2 = atomic.get_value(); + + assert_eq!(value, 15, "Incorrect first value"); + assert_eq!(value2, 0, "Incorrect second value"); + } + + #[test] + fn can_store_i64_atomic_value() { + let atomic = new_i64_tracker(0); + + let value = atomic.get_value(); + assert_eq!(value, 0); + + atomic.store(-25); + let value = atomic.get_value(); + assert_eq!(value, -25); + + atomic.store(25); + let value = atomic.get_value(); + assert_eq!(value, 25); + } + + #[test] + fn can_add_and_get_i64_atomic_value() { + let atomic = new_i64_tracker(0); + atomic.add(15); + atomic.add(-10); + + let value = atomic.get_value(); + assert_eq!(value, 5); + } + + #[test] + fn can_reset_i64_atomic_value() { + let atomic = new_i64_tracker(0); + atomic.add(15); + + let value = atomic.get_and_reset_value(); + let value2 = atomic.get_value(); + + assert_eq!(value, 15, "Incorrect first value"); + assert_eq!(value2, 0, "Incorrect second value"); + } + + #[test] + fn can_store_f64_atomic_value() { + let atomic = f64::new_atomic_tracker(0.0); + let atomic_tracker = &atomic as &dyn AtomicTracker; + + let value = atomic.get_value(); + assert_eq!(value, 0.0); + + atomic_tracker.store(-15.5); + let value = atomic.get_value(); + assert!(f64::abs(-15.5 - value) < 0.0001); + + atomic_tracker.store(25.7); + let value = atomic.get_value(); + assert!(f64::abs(25.7 - value) < 0.0001); + } + + #[test] + fn can_add_and_get_f64_atomic_value() { + let atomic = f64::new_atomic_tracker(0.0); + atomic.add(15.3); + atomic.add(10.4); + + let value = atomic.get_value(); + + assert!(f64::abs(25.7 - value) < 0.0001); + } + + #[test] + fn can_reset_f64_atomic_value() { + let atomic = f64::new_atomic_tracker(0.0); + atomic.add(15.5); + + let value = atomic.get_and_reset_value(); + let value2 = atomic.get_value(); + + assert!(f64::abs(15.5 - value) < 0.0001, "Incorrect first value"); + assert!(f64::abs(0.0 - value2) < 0.0001, "Incorrect second value"); + } + + #[test] + fn stale_entry_evicts_both_unsorted_and_sorted_keys() { + // ValueMap stores two HashMap keys per attribute set: one for the insertion + // order and one for the sorted (canonical) order, both pointing to the same + // Arc. This test verifies that stale eviction removes *both* + // keys so no zombie entries remain in the map. + let value_map = ValueMap::>::new((), 10); + + // Insert with attributes deliberately in non-sorted order. + // measure() inserts two keys: + // - unsorted: [("b", ...), ("a", ...)] + // - sorted: [("a", ...), ("b", ...)] + // both pointing to the same Arc. + let attrs = vec![KeyValue::new("b", 1_i64), KeyValue::new("a", 2_i64)]; + value_map.measure(1_i64, attrs.as_slice()); + + { + let trackers = value_map.trackers.read().unwrap(); + assert_eq!( + trackers.len(), + 2, + "should have 2 HashMap keys (unsorted + sorted) for one logical attr-set" + ); + } + assert_eq!(value_map.count.load(Ordering::SeqCst), 1); + + // First collect: entry was updated, so it is exported and has_been_updated is reset. + let mut dest: Vec> = Vec::new(); + value_map.collect_and_reset(&mut dest, |attrs, _| attrs); + assert_eq!(dest.len(), 1, "first collect should export the entry"); + + // Second collect: entry was not updated since last collect, so it is stale. + // Both HashMap keys (unsorted + sorted) must be evicted. + dest.clear(); + value_map.collect_and_reset(&mut dest, |attrs, _| attrs); + assert_eq!(dest.len(), 0, "stale entry should not be exported"); + + { + let trackers = value_map.trackers.read().unwrap(); + assert_eq!( + trackers.len(), + 0, + "both HashMap keys (unsorted + sorted) must be evicted for the stale entry" + ); + } + assert_eq!( + value_map.count.load(Ordering::SeqCst), + 0, + "count should reach 0 after eviction" + ); + } + + /// When the trackers `RwLock` is poisoned, `bind()` cannot safely insert or + /// look up entries, so it returns `None` and the caller (Sum/Histogram/etc.) + /// hands back a `NoopBoundMeasure`. This is a defensive branch that fires + /// on degenerate states (a thread panicked while holding the write lock) + /// and is unreachable through normal traffic. The test induces poisoning + /// explicitly so the branch keeps coverage. + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[test] + fn bind_returns_none_when_trackers_lock_is_poisoned() { + let value_map = ValueMap::>::new((), 100); + + // Poison the trackers RwLock by panicking inside a write guard. + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _guard = value_map.trackers.write().unwrap(); + panic!("intentional poison"); + })); + + assert!( + value_map.trackers.is_poisoned(), + "trackers lock must be poisoned for this test to be meaningful" + ); + + // Empty attrs use the no_attribute_tracker fast path and never touch + // the poisoned lock — they should still succeed. + assert!( + value_map.bind(&[]).is_some(), + "bind(&[]) must succeed even with poisoned lock; uses no_attribute_tracker" + ); + + // Non-empty attrs go through bind_attrs which needs the trackers lock. + // The read-lock try succeeds (only writes poison, but read on poisoned + // can also fail) — fall through to write lock which fails poisoned. + let result = value_map.bind(&[KeyValue::new("k", 1_i64)]); + assert!( + result.is_none(), + "bind() with non-empty attrs must return None on poisoned lock" + ); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/precomputed_sum.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/precomputed_sum.rs new file mode 100644 index 00000000..85742a1b --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/precomputed_sum.rs @@ -0,0 +1,393 @@ +use opentelemetry::KeyValue; + +use crate::metrics::data::{self, AggregatedMetrics, MetricData, SumDataPoint}; +use crate::metrics::Temporality; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use std::sync::atomic::Ordering; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use std::sync::Arc; + +use super::aggregate::{AggregateTimeInitiator, AttributeSetFilter}; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use super::Aggregator; +use super::{last_value::Assign, sort_and_dedup, AtomicTracker, Number, ValueMap}; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use super::{BoundMeasure, NoopBoundMeasure, TrackerEntry}; +use super::{ComputeAggregation, Measure}; +use std::{collections::HashMap, sync::Mutex}; + +/// Pre-bound precomputed-sum handle. Writes go directly to a fixed +/// `TrackerEntry`. PrecomputedSum is used by asynchronous instruments +/// (ObservableCounter / ObservableUpDownCounter), which do not expose `bind()` +/// to user code, so this impl exists to satisfy the `Measure` trait and is +/// not reachable via the public API today. The implementation mirrors +/// `BoundLastValueHandle` since both share the `Assign` aggregator. +#[cfg(feature = "experimental_metrics_bound_instruments")] +struct BoundPrecomputedSumHandle { + tracker: Arc>>, +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundMeasure for BoundPrecomputedSumHandle { + fn call(&self, measurement: T) { + // Mirror unbound PrecomputedSum::call: ignore NaN and infinity so a bad + // value can't get cached in `reported` and poison the delta computed + // against the next valid measurement. + if !measurement.into_float().is_finite() { + return; + } + self.tracker.aggregator.update(measurement); + self.tracker.has_been_updated.store(true, Ordering::Release); + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl Drop for BoundPrecomputedSumHandle { + fn drop(&mut self) { + self.tracker.bound_count.fetch_sub(1, Ordering::Relaxed); + } +} + +/// Summarizes a set of pre-computed sums as their arithmetic sum. +pub(crate) struct PrecomputedSum { + value_map: ValueMap>, + init_time: AggregateTimeInitiator, + temporality: Temporality, + filter: AttributeSetFilter, + monotonic: bool, + reported: Mutex, T>>, +} + +impl PrecomputedSum { + pub(crate) fn new( + temporality: Temporality, + filter: AttributeSetFilter, + monotonic: bool, + cardinality_limit: usize, + ) -> Self { + PrecomputedSum { + value_map: ValueMap::new((), cardinality_limit), + init_time: AggregateTimeInitiator::default(), + temporality, + filter, + monotonic, + reported: Mutex::new(Default::default()), + } + } + + pub(crate) fn delta(&self, dest: Option<&mut MetricData>) -> (usize, Option>) { + let time = self.init_time.delta(); + + let s_data = dest.and_then(|d| { + if let MetricData::Sum(sum) = d { + Some(sum) + } else { + None + } + }); + let mut new_agg = if s_data.is_none() { + Some(data::Sum { + data_points: vec![], + start_time: time.start, + time: time.current, + temporality: Temporality::Delta, + is_monotonic: self.monotonic, + }) + } else { + None + }; + let s_data = s_data.unwrap_or_else(|| new_agg.as_mut().expect("present if s_data is none")); + s_data.start_time = time.start; + s_data.time = time.current; + s_data.temporality = Temporality::Delta; + s_data.is_monotonic = self.monotonic; + + let mut reported = match self.reported.lock() { + Ok(r) => r, + Err(_) => return (0, None), + }; + let mut new_reported = HashMap::with_capacity(reported.len()); + + self.value_map + .drain_and_reset(&mut s_data.data_points, |attributes, aggr| { + let value = aggr.value.get_value(); + // Canonicalize before caching in `reported`; otherwise the same + // attributes in a different order are cached as a distinct key. + let lookup_attributes = sort_and_dedup(&attributes); + let delta = value - *reported.get(&lookup_attributes).unwrap_or(&T::default()); + new_reported.insert(lookup_attributes, value); + SumDataPoint { + attributes, + value: delta, + exemplars: vec![], + } + }); + + *reported = new_reported; + drop(reported); // drop before values guard is dropped + + (s_data.data_points.len(), new_agg.map(Into::into)) + } + + pub(crate) fn cumulative( + &self, + dest: Option<&mut MetricData>, + ) -> (usize, Option>) { + let time = self.init_time.cumulative(); + + let s_data = dest.and_then(|d| { + if let MetricData::Sum(sum) = d { + Some(sum) + } else { + None + } + }); + let mut new_agg = if s_data.is_none() { + Some(data::Sum { + data_points: vec![], + start_time: time.start, + time: time.current, + temporality: Temporality::Cumulative, + is_monotonic: self.monotonic, + }) + } else { + None + }; + let s_data = s_data.unwrap_or_else(|| new_agg.as_mut().expect("present if s_data is none")); + s_data.start_time = time.start; + s_data.time = time.current; + s_data.temporality = Temporality::Cumulative; + s_data.is_monotonic = self.monotonic; + + // Use drain_and_reset to remove stale attributes (not observed in current callback) + // For cumulative, report absolute values (no delta calculation needed) + self.value_map + .drain_and_reset(&mut s_data.data_points, |attributes, aggr| SumDataPoint { + attributes, + value: aggr.value.get_value(), + exemplars: vec![], + }); + + (s_data.data_points.len(), new_agg.map(Into::into)) + } +} + +impl Measure for PrecomputedSum +where + T: Number, +{ + fn call(&self, measurement: T, attrs: &[KeyValue]) { + // Ignore NaN and infinity: a cached NaN in `reported` poisons the delta + // computed against the *next* valid measurement too + // (`valid_val - NaN == NaN`), so a single bad value corrupts two cycles. + if !measurement.into_float().is_finite() { + return; + } + self.filter.apply(attrs, |filtered| { + self.value_map.measure(measurement, filtered); + }) + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind(&self, attrs: &[KeyValue]) -> Box> { + let mut bound_attrs = Vec::new(); + self.filter.apply(attrs, |filtered| { + bound_attrs = filtered.to_vec(); + }); + match self.value_map.bind(&bound_attrs) { + Some(tracker) => Box::new(BoundPrecomputedSumHandle { tracker }), + None => Box::new(NoopBoundMeasure::new()), + } + } +} + +impl ComputeAggregation for PrecomputedSum +where + T: Number, +{ + fn call(&self, dest: Option<&mut AggregatedMetrics>) -> (usize, Option) { + let data = dest.and_then(|d| T::extract_metrics_data_mut(d)); + let (len, new) = match self.temporality { + Temporality::Delta => self.delta(data), + _ => self.cumulative(data), + }; + (len, new.map(T::make_aggregated_metrics)) + } +} + +#[cfg(test)] +mod nan_guard_tests { + use super::*; + use crate::metrics::data::{AggregatedMetrics, MetricData}; + + #[test] + fn nan_and_infinity_are_ignored() { + let pre_sum = PrecomputedSum::::new( + Temporality::Cumulative, + AttributeSetFilter::new(None), + true, + 2000, + ); + Measure::call(&pre_sum, 1.0, &[]); + Measure::call(&pre_sum, f64::NAN, &[]); + Measure::call(&pre_sum, f64::INFINITY, &[]); + Measure::call(&pre_sum, f64::NEG_INFINITY, &[]); + + let (count, dp) = ComputeAggregation::call(&pre_sum, None); + let dp = dp.unwrap(); + let AggregatedMetrics::F64(MetricData::Sum(dp)) = dp else { + unreachable!() + }; + assert_eq!(count, 1); + assert_eq!(dp.data_points[0].value, 1.0); + } + + #[test] + fn nan_in_delta_temporality_leaves_baseline_cleared() { + // Under Delta temporality, dropping a NaN measurement leaves `value_map` + // unobserved for that collection cycle. When `drain_and_reset` runs, + // `new_reported` does not include the attribute set, which clears the + // baseline in `reported`. Consequently, a subsequent valid observation + // calculates delta against 0 rather than the previous valid value (100), + // re-counting the earlier observation. + // + // This mirrors the behavior of missing/skipped observations in async instruments + // today. Baseline recovery across unobserved cycles without unbounded memory + // growth (stale-attribute eviction) is tracked separately. + let pre_sum = PrecomputedSum::::new( + Temporality::Delta, + AttributeSetFilter::new(None), + true, + 2000, + ); + + // 1. Initial valid measurement: 100.0 + Measure::call(&pre_sum, 100.0, &[]); + let (count, dp) = ComputeAggregation::call(&pre_sum, None); + let dp = dp.unwrap(); + let AggregatedMetrics::F64(MetricData::Sum(dp)) = dp else { + unreachable!() + }; + assert_eq!(count, 1); + assert_eq!(dp.data_points[0].value, 100.0); + + // 2. Bad measurement: NaN (dropped by the guard) + Measure::call(&pre_sum, f64::NAN, &[]); + let (count, dp) = ComputeAggregation::call(&pre_sum, None); + // Since NaN was ignored, no points were updated; collection returns count 0 + assert_eq!(count, 0); + let dp = dp.unwrap(); + let AggregatedMetrics::F64(MetricData::Sum(dp)) = dp else { + unreachable!() + }; + assert!(dp.data_points.is_empty()); + + // 3. Next valid measurement: 110.0 + Measure::call(&pre_sum, 110.0, &[]); + let (count, dp) = ComputeAggregation::call(&pre_sum, None); + let dp = dp.unwrap(); + let AggregatedMetrics::F64(MetricData::Sum(dp)) = dp else { + unreachable!() + }; + assert_eq!(count, 1); + // Document current behavior: baseline was cleared when cycle 2 had no observation, + // so delta is 110.0 - 0.0 = 110.0 rather than 110.0 - 100.0 = 10.0. + assert_eq!(dp.data_points[0].value, 110.0); + } +} + +#[cfg(all(test, feature = "experimental_metrics_bound_instruments"))] +mod tests { + use super::*; + use crate::metrics::data::{AggregatedMetrics, MetricData, Sum}; + use std::sync::atomic::Ordering; + + fn extract_sum(agg: AggregatedMetrics) -> Sum { + match agg { + AggregatedMetrics::U64(MetricData::Sum(s)) => s, + _ => panic!("expected u64 Sum"), + } + } + + /// PrecomputedSum is used by ObservableCounter / ObservableUpDownCounter, which + /// do not expose `bind()` to user code. The `Measure::bind` impl exists so the + /// trait is uniform across all aggregators (and future Observable bind() + /// extensions are mechanical). This test exercises the impl directly so the + /// otherwise-unreachable code path stays honest. + #[test] + fn bind_writes_through_bound_handle() { + let pre_sum = PrecomputedSum::::new( + Temporality::Cumulative, + AttributeSetFilter::new(None), + true, + 100, + ); + let attrs = [KeyValue::new("k", "v")]; + let bound = Measure::bind(&pre_sum, &attrs); + + bound.call(99); // PrecomputedSum semantics: each call assigns the absolute value + + let (count, agg) = ComputeAggregation::call(&pre_sum, None); + assert_eq!(count, 1); + let sum = extract_sum(agg.expect("aggregation produced")); + assert_eq!(sum.data_points.len(), 1); + assert_eq!(sum.data_points[0].value, 99); + assert_eq!(sum.data_points[0].attributes, attrs.to_vec()); + } + + #[test] + fn bound_handle_drop_decrements_bound_count() { + let pre_sum = PrecomputedSum::::new( + Temporality::Delta, + AttributeSetFilter::new(None), + true, + 100, + ); + let attrs = [KeyValue::new("k", "v")]; + let bound = Measure::bind(&pre_sum, &attrs); + bound.call(5); + + let trackers = pre_sum.value_map.trackers.read().unwrap(); + let entry = trackers + .values() + .next() + .expect("entry should exist after bind+call"); + assert_eq!(entry.bound_count.load(Ordering::Relaxed), 1); + drop(trackers); + + drop(bound); + + let trackers = pre_sum.value_map.trackers.read().unwrap(); + let entry = trackers + .values() + .next() + .expect("entry should still exist post-drop"); + assert_eq!(entry.bound_count.load(Ordering::Relaxed), 0); + } + + #[test] + fn bound_nan_and_infinity_are_ignored() { + let pre_sum = PrecomputedSum::::new( + Temporality::Cumulative, + AttributeSetFilter::new(None), + true, + 2000, + ); + let attrs = [KeyValue::new("k", "v")]; + let bound = Measure::bind(&pre_sum, &attrs); + + bound.call(1.0); + bound.call(f64::NAN); + bound.call(f64::INFINITY); + bound.call(f64::NEG_INFINITY); + + let (count, dp) = ComputeAggregation::call(&pre_sum, None); + let dp = dp.unwrap(); + let AggregatedMetrics::F64(MetricData::Sum(dp)) = dp else { + unreachable!() + }; + assert_eq!(count, 1); + assert_eq!(dp.data_points[0].value, 1.0); + assert_eq!(dp.data_points[0].attributes, attrs.to_vec()); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/sum.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/sum.rs new file mode 100644 index 00000000..6a755d23 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/internal/sum.rs @@ -0,0 +1,288 @@ +use crate::metrics::data::{self, AggregatedMetrics, MetricData, SumDataPoint}; +use crate::metrics::Temporality; +use opentelemetry::KeyValue; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use std::sync::atomic::Ordering; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use std::sync::Arc; + +use super::aggregate::{AggregateTimeInitiator, AttributeSetFilter}; +use super::{Aggregator, AtomicTracker, ComputeAggregation, Measure, Number}; +use super::{AtomicallyUpdate, ValueMap}; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use super::{BoundMeasure, NoopBoundMeasure, TrackerEntry}; + +struct Increment +where + T: AtomicallyUpdate, +{ + value: T::AtomicTracker, +} + +impl Aggregator for Increment +where + T: Number, +{ + type InitConfig = (); + type PreComputedValue = T; + + fn create(_init: &()) -> Self { + Self { + value: T::new_atomic_tracker(T::default()), + } + } + + fn update(&self, value: T) { + self.value.add(value) + } + + fn clone_and_reset(&self, _: &()) -> Self { + Self { + value: T::new_atomic_tracker(self.value.get_and_reset_value()), + } + } +} + +/// Pre-bound counter handle: writes go directly to a fixed `TrackerEntry` without +/// per-call attribute lookup. The `tracker` is either a dedicated entry for the +/// bound attribute set, or — if bind() hit the cardinality limit — the shared +/// overflow tracker. Either way, `call()` is a single atomic increment and a +/// release store; no map lookup, no lock acquisition. +#[cfg(feature = "experimental_metrics_bound_instruments")] +struct BoundSumHandle { + tracker: Arc>>, +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundMeasure for BoundSumHandle { + fn call(&self, measurement: T) { + // Mirror unbound Sum::call: ignore NaN and infinity so the cumulative + // total can't be permanently corrupted (`x + NaN == NaN`). + if !measurement.into_float().is_finite() { + return; + } + self.tracker.aggregator.update(measurement); + self.tracker.has_been_updated.store(true, Ordering::Release); + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl Drop for BoundSumHandle { + fn drop(&mut self) { + self.tracker.bound_count.fetch_sub(1, Ordering::Relaxed); + } +} + +/// Summarizes a set of measurements made as their arithmetic sum. +pub(crate) struct Sum { + value_map: ValueMap>, + init_time: AggregateTimeInitiator, + temporality: Temporality, + filter: AttributeSetFilter, + monotonic: bool, +} + +impl Sum { + /// Returns an aggregator that summarizes a set of measurements as their + /// arithmetic sum. + /// + /// Each sum is scoped by attributes and the aggregation cycle the measurements + /// were made in. + pub(crate) fn new( + temporality: Temporality, + filter: AttributeSetFilter, + monotonic: bool, + cardinality_limit: usize, + ) -> Self { + Sum { + value_map: ValueMap::new((), cardinality_limit), + init_time: AggregateTimeInitiator::default(), + temporality, + filter, + monotonic, + } + } + + pub(crate) fn delta(&self, dest: Option<&mut MetricData>) -> (usize, Option>) { + let time = self.init_time.delta(); + let s_data = dest.and_then(|d| { + if let MetricData::Sum(sum) = d { + Some(sum) + } else { + None + } + }); + let mut new_agg = if s_data.is_none() { + Some(data::Sum { + data_points: vec![], + start_time: time.start, + time: time.current, + temporality: Temporality::Delta, + is_monotonic: self.monotonic, + }) + } else { + None + }; + let s_data = s_data.unwrap_or_else(|| new_agg.as_mut().expect("present if s_data is none")); + s_data.start_time = time.start; + s_data.time = time.current; + s_data.temporality = Temporality::Delta; + s_data.is_monotonic = self.monotonic; + + self.value_map + .collect_and_reset(&mut s_data.data_points, |attributes, aggr| SumDataPoint { + attributes, + value: aggr.value.get_and_reset_value(), + exemplars: vec![], + }); + + (s_data.data_points.len(), new_agg.map(Into::into)) + } + + pub(crate) fn cumulative( + &self, + dest: Option<&mut MetricData>, + ) -> (usize, Option>) { + let time = self.init_time.cumulative(); + let s_data = dest.and_then(|d| { + if let MetricData::Sum(sum) = d { + Some(sum) + } else { + None + } + }); + let mut new_agg = if s_data.is_none() { + Some(data::Sum { + data_points: vec![], + start_time: time.start, + time: time.current, + temporality: Temporality::Cumulative, + is_monotonic: self.monotonic, + }) + } else { + None + }; + let s_data = s_data.unwrap_or_else(|| new_agg.as_mut().expect("present if s_data is none")); + + s_data.start_time = time.start; + s_data.time = time.current; + s_data.temporality = Temporality::Cumulative; + s_data.is_monotonic = self.monotonic; + + self.value_map + .collect_readonly(&mut s_data.data_points, |attributes, aggr| SumDataPoint { + attributes, + value: aggr.value.get_value(), + exemplars: vec![], + }); + + (s_data.data_points.len(), new_agg.map(Into::into)) + } +} + +impl Measure for Sum +where + T: Number, +{ + fn call(&self, measurement: T, attrs: &[KeyValue]) { + // Ignore NaN and infinity: `F64AtomicTracker::add`'s CAS loop computes + // `current + NaN == NaN`, which would permanently poison the cumulative + // total for the rest of the process's lifetime. + if !measurement.into_float().is_finite() { + return; + } + self.filter.apply(attrs, |filtered| { + self.value_map.measure(measurement, filtered); + }) + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind(&self, attrs: &[KeyValue]) -> Box> { + let mut bound_attrs = Vec::new(); + self.filter.apply(attrs, |filtered| { + bound_attrs = filtered.to_vec(); + }); + match self.value_map.bind(&bound_attrs) { + Some(tracker) => Box::new(BoundSumHandle { tracker }), + // Trackers RwLock is poisoned — return a noop handle so writes + // silently drop, mirroring `measure()`'s own poison handling. + None => Box::new(NoopBoundMeasure::new()), + } + } +} + +impl ComputeAggregation for Sum +where + T: Number, +{ + fn call(&self, dest: Option<&mut AggregatedMetrics>) -> (usize, Option) { + let data = dest.and_then(|d| T::extract_metrics_data_mut(d)); + let (len, new) = match self.temporality { + Temporality::Delta => self.delta(data), + _ => self.cumulative(data), + }; + (len, new.map(T::make_aggregated_metrics)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nan_and_infinity_are_ignored() { + let sum = Sum::::new( + Temporality::Cumulative, + AttributeSetFilter::new(None), + false, + 2000, + ); + Measure::call(&sum, 1.0, &[]); + Measure::call(&sum, f64::NAN, &[]); + Measure::call(&sum, f64::INFINITY, &[]); + Measure::call(&sum, f64::NEG_INFINITY, &[]); + Measure::call(&sum, 2.0, &[]); + + let (count, dp) = ComputeAggregation::call(&sum, None); + let dp = dp.unwrap(); + let AggregatedMetrics::F64(MetricData::Sum(dp)) = dp else { + unreachable!() + }; + assert_eq!(count, 1); + assert_eq!(dp.data_points[0].value, 3.0); + assert!(!dp.data_points[0].value.is_nan()); + } +} + +#[cfg(all(test, feature = "experimental_metrics_bound_instruments"))] +mod bound_tests { + use super::*; + + #[test] + fn bound_nan_and_infinity_are_ignored() { + let sum = Sum::::new( + Temporality::Cumulative, + AttributeSetFilter::new(None), + false, + 2000, + ); + let attrs = [KeyValue::new("k", "v")]; + let bound = Measure::bind(&sum, &attrs); + + bound.call(1.0); + bound.call(f64::NAN); + bound.call(f64::INFINITY); + bound.call(f64::NEG_INFINITY); + bound.call(2.0); + + let (count, dp) = ComputeAggregation::call(&sum, None); + let dp = dp.unwrap(); + let AggregatedMetrics::F64(MetricData::Sum(dp)) = dp else { + unreachable!() + }; + assert_eq!(count, 1); + assert_eq!(dp.data_points[0].value, 3.0); + assert!(!dp.data_points[0].value.is_nan()); + assert_eq!(dp.data_points[0].attributes, attrs.to_vec()); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/manual_reader.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/manual_reader.rs new file mode 100644 index 00000000..32932381 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/manual_reader.rs @@ -0,0 +1,163 @@ +use opentelemetry::otel_debug; +use std::time::Duration; +use std::{ + fmt, + sync::{Mutex, Weak}, +}; + +use crate::{ + error::{OTelSdkError, OTelSdkResult}, + metrics::Temporality, +}; + +use super::{ + data::ResourceMetrics, + pipeline::Pipeline, + reader::{MetricReader, SdkProducer}, +}; + +/// A simple [MetricReader] that allows an application to read metrics on demand. +/// +/// See [ManualReaderBuilder] for configuration options. +/// +/// # Example +/// +/// ``` +/// use opentelemetry_sdk::metrics::ManualReader; +/// +/// // can specify additional reader configuration +/// let reader = ManualReader::builder().build(); +/// # drop(reader) +/// ``` +pub struct ManualReader { + inner: Mutex, + temporality: Temporality, +} + +impl Default for ManualReader { + fn default() -> Self { + ManualReader::builder().build() + } +} + +impl fmt::Debug for ManualReader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ManualReader") + } +} + +#[derive(Debug)] +struct ManualReaderInner { + sdk_producer: Option>, + is_shutdown: bool, +} + +impl ManualReader { + /// Configuration for this reader + pub fn builder() -> ManualReaderBuilder { + ManualReaderBuilder::default() + } + + /// A [MetricReader] which is directly called to collect metrics. + pub(crate) fn new(temporality: Temporality) -> Self { + ManualReader { + inner: Mutex::new(ManualReaderInner { + sdk_producer: None, + is_shutdown: false, + }), + temporality, + } + } +} + +impl MetricReader for ManualReader { + /// Register a pipeline which enables the caller to read metrics from the SDK + /// on demand. + fn register_pipeline(&self, pipeline: Weak) { + let _ = self.inner.lock().map(|mut inner| { + // Only register once. If producer is already set, do nothing. + if inner.sdk_producer.is_none() { + inner.sdk_producer = Some(pipeline); + } else { + otel_debug!( + name: "ManualReader.DuplicateRegistration", + message = "The pipeline is already registered to the Reader. Registering pipeline multiple times is not allowed."); + } + }); + } + + /// Gathers all metrics from the SDK, calling any + /// callbacks necessary and returning the results. + /// + /// Returns an error if called after shutdown. + fn collect(&self, rm: &mut ResourceMetrics) -> OTelSdkResult { + let inner = self + .inner + .lock() + .map_err(|_| OTelSdkError::InternalFailure("Failed to lock pipeline".into()))?; + + match &inner.sdk_producer.as_ref().and_then(|w| w.upgrade()) { + Some(producer) => producer.produce(rm)?, + None => { + return Err(OTelSdkError::InternalFailure( + "reader is shut down or not registered".into(), + )) + } + }; + + Ok(()) + } + + /// ForceFlush is a no-op, it always returns nil. + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + /// Closes any connections and frees any resources used by the reader. + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + let mut inner = self + .inner + .lock() + .map_err(|e| OTelSdkError::InternalFailure(format!("Failed to acquire lock: {e}")))?; + + // Any future call to collect will now return an error. + inner.sdk_producer = None; + inner.is_shutdown = true; + + Ok(()) + } + + fn temporality(&self, kind: super::InstrumentKind) -> Temporality { + kind.temporality_preference(self.temporality) + } +} + +/// Configuration for a [ManualReader] +#[derive(Default)] +pub struct ManualReaderBuilder { + temporality: Temporality, +} + +impl fmt::Debug for ManualReaderBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ManualReaderBuilder") + } +} + +impl ManualReaderBuilder { + /// New manual builder configuration + pub fn new() -> Self { + Default::default() + } + + /// Set the [Temporality] of the exporter. + pub fn with_temporality(mut self, temporality: Temporality) -> Self { + self.temporality = temporality; + self + } + + /// Create a new [ManualReader] from this configuration. + pub fn build(self) -> ManualReader { + ManualReader::new(self.temporality) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/meter.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/meter.rs new file mode 100644 index 00000000..34256faf --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/meter.rs @@ -0,0 +1,803 @@ +#![allow(dead_code)] +use core::fmt; +use std::{borrow::Cow, sync::Arc}; + +use opentelemetry::{ + metrics::{ + AsyncInstrumentBuilder, Counter, Gauge, Histogram, HistogramBuilder, InstrumentBuilder, + InstrumentProvider, ObservableCounter, ObservableGauge, ObservableUpDownCounter, + UpDownCounter, + }, + otel_error, InstrumentationScope, +}; + +use crate::metrics::{ + error::{MetricError, MetricResult}, + instrument::{Instrument, InstrumentKind, Observable, ResolvedMeasures}, + internal::{self, Number}, + pipeline::{Pipelines, Resolver}, +}; + +use super::noop::NoopSyncInstrument; + +// maximum length of instrument name +pub(crate) const INSTRUMENT_NAME_MAX_LENGTH: usize = 255; +// maximum length of instrument unit name +pub(crate) const INSTRUMENT_UNIT_NAME_MAX_LENGTH: usize = 63; +// Characters allowed in instrument name +pub(crate) const INSTRUMENT_NAME_ALLOWED_NON_ALPHANUMERIC_CHARS: [char; 4] = ['_', '.', '-', '/']; + +// name validation error strings +pub(crate) const INSTRUMENT_NAME_EMPTY: &str = "name must be non-empty"; +pub(crate) const INSTRUMENT_NAME_LENGTH: &str = "name must be less than 256 characters"; +pub(crate) const INSTRUMENT_NAME_INVALID_CHAR: &str = + "characters in name must be ASCII and belong to the alphanumeric characters, '_', '.', '-' and '/'"; +pub(crate) const INSTRUMENT_NAME_FIRST_ALPHABETIC: &str = + "name must start with an alphabetic character"; + +// unit validation error strings +pub(crate) const INSTRUMENT_UNIT_LENGTH: &str = "unit must be less than 64 characters"; +pub(crate) const INSTRUMENT_UNIT_INVALID_CHAR: &str = "characters in unit must be ASCII"; + +/// Handles the creation and coordination of all metric instruments. +/// +/// A meter represents a single instrumentation scope; all metric telemetry +/// produced by an instrumentation scope will use metric instruments from a +/// single meter. +/// +/// See the [Meter API] docs for usage. +/// +/// [Meter API]: opentelemetry::metrics::Meter +pub(crate) struct SdkMeter { + scope: InstrumentationScope, + pipes: Arc, + u64_resolver: Resolver, + i64_resolver: Resolver, + f64_resolver: Resolver, +} + +impl SdkMeter { + pub(crate) fn new(scope: InstrumentationScope, pipes: Arc) -> Self { + let view_cache = Default::default(); + + SdkMeter { + scope, + pipes: Arc::clone(&pipes), + u64_resolver: Resolver::new(Arc::clone(&pipes), Arc::clone(&view_cache)), + i64_resolver: Resolver::new(Arc::clone(&pipes), Arc::clone(&view_cache)), + f64_resolver: Resolver::new(pipes, view_cache), + } + } + + fn create_counter( + &self, + builder: InstrumentBuilder<'_, Counter>, + resolver: &InstrumentResolver<'_, T>, + ) -> Counter + where + T: Number, + { + let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); + if let Err(err) = validation_result { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Measurements from this Counter will be ignored.", + reason = format!("{}", err) + ); + return Counter::new(Arc::new(NoopSyncInstrument::new())); + } + + match resolver + .lookup( + InstrumentKind::Counter, + builder.name.clone(), + builder.description, + builder.unit, + None, + ) + .map(|i| Counter::new(Arc::new(i))) + { + Ok(counter) => counter, + Err(err) => { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Measurements from this Counter will be ignored.", + reason = format!("{}", err) + ); + Counter::new(Arc::new(NoopSyncInstrument::new())) + } + } + } + + fn create_observable_counter( + &self, + builder: AsyncInstrumentBuilder<'_, ObservableCounter, T>, + resolver: &InstrumentResolver<'_, T>, + ) -> ObservableCounter + where + T: Number, + { + let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); + if let Err(err) = validation_result { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Callbacks for this ObservableCounter will not be invoked.", + reason = format!("{}", err)); + return ObservableCounter::new(); + } + + match resolver.measures( + InstrumentKind::ObservableCounter, + builder.name.clone(), + builder.description, + builder.unit, + None, + ) { + Ok(ms) => { + if ms.is_empty() { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Callbacks for this ObservableCounter will not be invoked. Check View Configuration." + ); + return ObservableCounter::new(); + } + + let observable = Arc::new(Observable::new(ms)); + + for callback in builder.callbacks { + let cb_inst = Arc::clone(&observable); + self.pipes + .register_callback(move || callback(cb_inst.as_ref())); + } + + ObservableCounter::new() + } + Err(err) => { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Callbacks for this ObservableCounter will not be invoked.", + reason = format!("{}", err)); + ObservableCounter::new() + } + } + } + + fn create_observable_updown_counter( + &self, + builder: AsyncInstrumentBuilder<'_, ObservableUpDownCounter, T>, + resolver: &InstrumentResolver<'_, T>, + ) -> ObservableUpDownCounter + where + T: Number, + { + let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); + if let Err(err) = validation_result { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Callbacks for this ObservableUpDownCounter will not be invoked.", + reason = format!("{}", err)); + return ObservableUpDownCounter::new(); + } + + match resolver.measures( + InstrumentKind::ObservableUpDownCounter, + builder.name.clone(), + builder.description, + builder.unit, + None, + ) { + Ok(ms) => { + if ms.is_empty() { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Callbacks for this ObservableUpDownCounter will not be invoked. Check View Configuration." + ); + return ObservableUpDownCounter::new(); + } + + let observable = Arc::new(Observable::new(ms)); + + for callback in builder.callbacks { + let cb_inst = Arc::clone(&observable); + self.pipes + .register_callback(move || callback(cb_inst.as_ref())); + } + + ObservableUpDownCounter::new() + } + Err(err) => { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Callbacks for this ObservableUpDownCounter will not be invoked.", + reason = format!("{}", err)); + ObservableUpDownCounter::new() + } + } + } + + fn create_observable_gauge( + &self, + builder: AsyncInstrumentBuilder<'_, ObservableGauge, T>, + resolver: &InstrumentResolver<'_, T>, + ) -> ObservableGauge + where + T: Number, + { + let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); + if let Err(err) = validation_result { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Callbacks for this ObservableGauge will not be invoked.", + reason = format!("{}", err)); + return ObservableGauge::new(); + } + + match resolver.measures( + InstrumentKind::ObservableGauge, + builder.name.clone(), + builder.description, + builder.unit, + None, + ) { + Ok(ms) => { + if ms.is_empty() { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Callbacks for this ObservableGauge will not be invoked. Check View Configuration." + ); + return ObservableGauge::new(); + } + + let observable = Arc::new(Observable::new(ms)); + + for callback in builder.callbacks { + let cb_inst = Arc::clone(&observable); + self.pipes + .register_callback(move || callback(cb_inst.as_ref())); + } + + ObservableGauge::new() + } + Err(err) => { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Callbacks for this ObservableGauge will not be invoked.", + reason = format!("{}", err)); + ObservableGauge::new() + } + } + } + + fn create_updown_counter( + &self, + builder: InstrumentBuilder<'_, UpDownCounter>, + resolver: &InstrumentResolver<'_, T>, + ) -> UpDownCounter + where + T: Number, + { + let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); + if let Err(err) = validation_result { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Measurements from this UpDownCounter will be ignored.", + reason = format!("{}", err) + ); + return UpDownCounter::new(Arc::new(NoopSyncInstrument::new())); + } + + match resolver + .lookup( + InstrumentKind::UpDownCounter, + builder.name.clone(), + builder.description, + builder.unit, + None, + ) + .map(|i| UpDownCounter::new(Arc::new(i))) + { + Ok(updown_counter) => updown_counter, + Err(err) => { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Measurements from this UpDownCounter will be ignored.", + reason = format!("{}", err) + ); + UpDownCounter::new(Arc::new(NoopSyncInstrument::new())) + } + } + } + + fn create_gauge( + &self, + builder: InstrumentBuilder<'_, Gauge>, + resolver: &InstrumentResolver<'_, T>, + ) -> Gauge + where + T: Number, + { + let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); + if let Err(err) = validation_result { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Measurements from this Gauge will be ignored.", + reason = format!("{}", err) + ); + return Gauge::new(Arc::new(NoopSyncInstrument::new())); + } + + match resolver + .lookup( + InstrumentKind::Gauge, + builder.name.clone(), + builder.description, + builder.unit, + None, + ) + .map(|i| Gauge::new(Arc::new(i))) + { + Ok(gauge) => gauge, + Err(err) => { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Measurements from this Gauge will be ignored.", + reason = format!("{}", err) + ); + Gauge::new(Arc::new(NoopSyncInstrument::new())) + } + } + } + + fn create_histogram( + &self, + builder: HistogramBuilder<'_, Histogram>, + resolver: &InstrumentResolver<'_, T>, + ) -> Histogram + where + T: Number, + { + let validation_result = validate_instrument_config(builder.name.as_ref(), &builder.unit); + if let Err(err) = validation_result { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Measurements from this Histogram will be ignored.", + reason = format!("{}", err) + ); + return Histogram::new(Arc::new(NoopSyncInstrument::new())); + } + + if let Some(ref boundaries) = builder.boundaries { + let validation_result = validate_bucket_boundaries(boundaries); + if let Err(err) = validation_result { + // TODO: Include the buckets too in the error message. + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Measurements from this Histogram will be ignored.", + reason = format!("{}", err) + ); + return Histogram::new(Arc::new(NoopSyncInstrument::new())); + } + } + + match resolver + .lookup( + InstrumentKind::Histogram, + builder.name.clone(), + builder.description, + builder.unit, + builder.boundaries, + ) + .map(|i| Histogram::new(Arc::new(i))) + { + Ok(histogram) => histogram, + Err(err) => { + otel_error!( + name: "InstrumentCreationFailed", + meter_name = self.scope.name(), + instrument_name = builder.name.as_ref(), + message = "Measurements from this Histogram will be ignored.", + reason = format!("{}", err) + ); + Histogram::new(Arc::new(NoopSyncInstrument::new())) + } + } + } +} + +#[doc(hidden)] +impl InstrumentProvider for SdkMeter { + fn u64_counter(&self, builder: InstrumentBuilder<'_, Counter>) -> Counter { + let resolver = InstrumentResolver::new(self, &self.u64_resolver); + self.create_counter(builder, &resolver) + } + + fn f64_counter(&self, builder: InstrumentBuilder<'_, Counter>) -> Counter { + let resolver = InstrumentResolver::new(self, &self.f64_resolver); + self.create_counter(builder, &resolver) + } + + fn u64_observable_counter( + &self, + builder: AsyncInstrumentBuilder<'_, ObservableCounter, u64>, + ) -> ObservableCounter { + let resolver = InstrumentResolver::new(self, &self.u64_resolver); + self.create_observable_counter(builder, &resolver) + } + + fn f64_observable_counter( + &self, + builder: AsyncInstrumentBuilder<'_, ObservableCounter, f64>, + ) -> ObservableCounter { + let resolver = InstrumentResolver::new(self, &self.f64_resolver); + self.create_observable_counter(builder, &resolver) + } + + fn i64_up_down_counter( + &self, + builder: InstrumentBuilder<'_, UpDownCounter>, + ) -> UpDownCounter { + let resolver = InstrumentResolver::new(self, &self.i64_resolver); + self.create_updown_counter(builder, &resolver) + } + + fn f64_up_down_counter( + &self, + builder: InstrumentBuilder<'_, UpDownCounter>, + ) -> UpDownCounter { + let resolver = InstrumentResolver::new(self, &self.f64_resolver); + self.create_updown_counter(builder, &resolver) + } + + fn i64_observable_up_down_counter( + &self, + builder: AsyncInstrumentBuilder<'_, ObservableUpDownCounter, i64>, + ) -> ObservableUpDownCounter { + let resolver = InstrumentResolver::new(self, &self.i64_resolver); + self.create_observable_updown_counter(builder, &resolver) + } + + fn f64_observable_up_down_counter( + &self, + builder: AsyncInstrumentBuilder<'_, ObservableUpDownCounter, f64>, + ) -> ObservableUpDownCounter { + let resolver = InstrumentResolver::new(self, &self.f64_resolver); + self.create_observable_updown_counter(builder, &resolver) + } + + fn u64_gauge(&self, builder: InstrumentBuilder<'_, Gauge>) -> Gauge { + let resolver = InstrumentResolver::new(self, &self.u64_resolver); + self.create_gauge(builder, &resolver) + } + + fn f64_gauge(&self, builder: InstrumentBuilder<'_, Gauge>) -> Gauge { + let resolver = InstrumentResolver::new(self, &self.f64_resolver); + self.create_gauge(builder, &resolver) + } + + fn i64_gauge(&self, builder: InstrumentBuilder<'_, Gauge>) -> Gauge { + let resolver = InstrumentResolver::new(self, &self.i64_resolver); + self.create_gauge(builder, &resolver) + } + + fn u64_observable_gauge( + &self, + builder: AsyncInstrumentBuilder<'_, ObservableGauge, u64>, + ) -> ObservableGauge { + let resolver = InstrumentResolver::new(self, &self.u64_resolver); + self.create_observable_gauge(builder, &resolver) + } + + fn i64_observable_gauge( + &self, + builder: AsyncInstrumentBuilder<'_, ObservableGauge, i64>, + ) -> ObservableGauge { + let resolver = InstrumentResolver::new(self, &self.i64_resolver); + self.create_observable_gauge(builder, &resolver) + } + + fn f64_observable_gauge( + &self, + builder: AsyncInstrumentBuilder<'_, ObservableGauge, f64>, + ) -> ObservableGauge { + let resolver = InstrumentResolver::new(self, &self.f64_resolver); + self.create_observable_gauge(builder, &resolver) + } + + fn f64_histogram(&self, builder: HistogramBuilder<'_, Histogram>) -> Histogram { + let resolver = InstrumentResolver::new(self, &self.f64_resolver); + self.create_histogram(builder, &resolver) + } + + fn u64_histogram(&self, builder: HistogramBuilder<'_, Histogram>) -> Histogram { + let resolver = InstrumentResolver::new(self, &self.u64_resolver); + self.create_histogram(builder, &resolver) + } +} + +fn validate_instrument_config(name: &str, unit: &Option>) -> MetricResult<()> { + validate_instrument_name(name).and_then(|_| validate_instrument_unit(unit)) +} + +fn validate_bucket_boundaries(boundaries: &[f64]) -> MetricResult<()> { + // Validate boundaries do not contain f64::NAN, f64::INFINITY, or f64::NEG_INFINITY + for boundary in boundaries { + if boundary.is_nan() || boundary.is_infinite() { + return Err(MetricError::InvalidInstrumentConfiguration( + "Bucket boundaries must not contain NaN, +Inf, or -Inf", + )); + } + } + + // validate that buckets are sorted and non-duplicate + for i in 1..boundaries.len() { + if boundaries[i] <= boundaries[i - 1] { + return Err(MetricError::InvalidInstrumentConfiguration( + "Bucket boundaries must be sorted and non-duplicate", + )); + } + } + + Ok(()) +} + +#[cfg(feature = "experimental_metrics_disable_name_validation")] +fn validate_instrument_name(_name: &str) -> MetricResult<()> { + // No name restrictions when name validation is disabled + Ok(()) +} + +#[cfg(not(feature = "experimental_metrics_disable_name_validation"))] +fn validate_instrument_name(name: &str) -> MetricResult<()> { + if name.is_empty() { + return Err(MetricError::InvalidInstrumentConfiguration( + INSTRUMENT_NAME_EMPTY, + )); + } + if name.len() > INSTRUMENT_NAME_MAX_LENGTH { + return Err(MetricError::InvalidInstrumentConfiguration( + INSTRUMENT_NAME_LENGTH, + )); + } + + if name.starts_with(|c: char| !c.is_ascii_alphabetic()) { + return Err(MetricError::InvalidInstrumentConfiguration( + INSTRUMENT_NAME_FIRST_ALPHABETIC, + )); + } + if name.contains(|c: char| { + !c.is_ascii_alphanumeric() && !INSTRUMENT_NAME_ALLOWED_NON_ALPHANUMERIC_CHARS.contains(&c) + }) { + return Err(MetricError::InvalidInstrumentConfiguration( + INSTRUMENT_NAME_INVALID_CHAR, + )); + } + Ok(()) +} + +fn validate_instrument_unit(unit: &Option>) -> MetricResult<()> { + if let Some(unit) = unit { + if unit.len() > INSTRUMENT_UNIT_NAME_MAX_LENGTH { + return Err(MetricError::InvalidInstrumentConfiguration( + INSTRUMENT_UNIT_LENGTH, + )); + } + if unit.contains(|c: char| !c.is_ascii()) { + return Err(MetricError::InvalidInstrumentConfiguration( + INSTRUMENT_UNIT_INVALID_CHAR, + )); + } + } + Ok(()) +} + +impl fmt::Debug for SdkMeter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Meter").field("scope", &self.scope).finish() + } +} + +/// Provides all OpenTelemetry instruments. +struct InstrumentResolver<'a, T> { + meter: &'a SdkMeter, + resolve: &'a Resolver, +} + +impl<'a, T> InstrumentResolver<'a, T> +where + T: Number, +{ + fn new(meter: &'a SdkMeter, resolve: &'a Resolver) -> Self { + InstrumentResolver { meter, resolve } + } + + /// lookup returns the resolved measures. + fn lookup( + &self, + kind: InstrumentKind, + name: Cow<'static, str>, + description: Option>, + unit: Option>, + boundaries: Option>, + ) -> MetricResult> { + let aggregators = self.measures(kind, name, description, unit, boundaries)?; + Ok(ResolvedMeasures { + measures: aggregators, + }) + } + + fn measures( + &self, + kind: InstrumentKind, + name: Cow<'static, str>, + description: Option>, + unit: Option>, + boundaries: Option>, + ) -> MetricResult>>> { + let inst = Instrument { + name, + description: description.unwrap_or_default(), + unit: unit.unwrap_or_default(), + kind, + scope: self.meter.scope.clone(), + }; + + self.resolve.measures(inst, boundaries) + } +} + +#[allow(unused_imports)] +#[cfg(test)] +mod tests { + use std::borrow::Cow; + + use crate::metrics::error::MetricError; + + use super::{ + validate_instrument_name, validate_instrument_unit, INSTRUMENT_NAME_EMPTY, + INSTRUMENT_NAME_FIRST_ALPHABETIC, INSTRUMENT_NAME_INVALID_CHAR, INSTRUMENT_NAME_LENGTH, + INSTRUMENT_UNIT_INVALID_CHAR, INSTRUMENT_UNIT_LENGTH, + }; + + #[test] + #[cfg(not(feature = "experimental_metrics_disable_name_validation"))] + fn instrument_name_validation() { + // (name, expected error) + let instrument_name_test_cases = vec![ + ("validateName", ""), + ("_startWithNoneAlphabet", INSTRUMENT_NAME_FIRST_ALPHABETIC), + ("utf8char锈", INSTRUMENT_NAME_INVALID_CHAR), + ("a".repeat(255).leak(), ""), + ("a".repeat(256).leak(), INSTRUMENT_NAME_LENGTH), + ("invalid name", INSTRUMENT_NAME_INVALID_CHAR), + ("allow/slash", ""), + ("allow_under_score", ""), + ("allow.dots.ok", ""), + ("", INSTRUMENT_NAME_EMPTY), + ("\\allow\\slash /sec", INSTRUMENT_NAME_FIRST_ALPHABETIC), + ("\\allow\\$$slash /sec", INSTRUMENT_NAME_FIRST_ALPHABETIC), + ("Total $ Count", INSTRUMENT_NAME_INVALID_CHAR), + ( + "\\test\\UsagePercent(Total) > 80%", + INSTRUMENT_NAME_FIRST_ALPHABETIC, + ), + ("/not / allowed", INSTRUMENT_NAME_FIRST_ALPHABETIC), + ]; + for (name, expected_error) in instrument_name_test_cases { + let assert = |result: Result<_, MetricError>| { + if expected_error.is_empty() { + assert!(result.is_ok()); + } else { + assert!(matches!( + result.unwrap_err(), + MetricError::InvalidInstrumentConfiguration(msg) if msg == expected_error + )); + } + }; + + assert(validate_instrument_name(name).map(|_| ())); + } + } + + #[test] + #[cfg(feature = "experimental_metrics_disable_name_validation")] + fn instrument_name_validation_disabled() { + // (name, expected error) + let instrument_name_test_cases = vec![ + ("validateName", ""), + ("_startWithNoneAlphabet", ""), + ("utf8char锈", ""), + ("a".repeat(255).leak(), ""), + ("a".repeat(256).leak(), ""), + ("invalid name", ""), + ("allow/slash", ""), + ("allow_under_score", ""), + ("allow.dots.ok", ""), + ("", ""), + ("\\allow\\slash /sec", ""), + ("\\allow\\$$slash /sec", ""), + ("Total $ Count", ""), + ("\\test\\UsagePercent(Total) > 80%", ""), + ("/not / allowed", ""), + ]; + for (name, expected_error) in instrument_name_test_cases { + let assert = |result: Result<_, MetricError>| { + if expected_error.is_empty() { + assert!(result.is_ok()); + } else { + assert!(matches!( + result.unwrap_err(), + MetricError::InvalidInstrumentConfiguration(msg) if msg == expected_error + )); + } + }; + + assert(validate_instrument_name(name).map(|_| ())); + } + } + + #[test] + fn instrument_unit_validation() { + // (unit, expected error) + let instrument_unit_test_cases = vec![ + ( + "0123456789012345678901234567890123456789012345678901234567890123", + INSTRUMENT_UNIT_LENGTH, + ), + ("utf8char锈", INSTRUMENT_UNIT_INVALID_CHAR), + ("kb", ""), + ("Kb/sec", ""), + ("%", ""), + ("", ""), + ]; + + for (unit, expected_error) in instrument_unit_test_cases { + let assert = |result: Result<_, MetricError>| { + if expected_error.is_empty() { + assert!(result.is_ok()); + } else { + assert!(matches!( + result.unwrap_err(), + MetricError::InvalidInstrumentConfiguration(msg) if msg == expected_error + )); + } + }; + let unit: Option> = Some(unit.into()); + + assert(validate_instrument_unit(&unit).map(|_| ())); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/meter_provider.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/meter_provider.rs new file mode 100644 index 00000000..27f7ca9c --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/meter_provider.rs @@ -0,0 +1,781 @@ +use core::fmt; +use opentelemetry::{ + metrics::{Meter, MeterProvider}, + otel_debug, otel_error, otel_info, InstrumentationScope, +}; +use std::time::Duration; +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, +}; + +use crate::error::OTelSdkResult; +use crate::Resource; + +use super::{ + exporter::PushMetricExporter, meter::SdkMeter, noop::NoopMeter, + periodic_reader::PeriodicReader, pipeline::Pipelines, reader::MetricReader, view::View, + Instrument, Stream, +}; + +/// Handles the creation and coordination of [Meter]s. +/// +/// All `Meter`s created by a `MeterProvider` will be associated with the same +/// [Resource], have the same views applied to them, and have their produced +/// metric telemetry passed to the configured `MetricReader`s. This is a +/// clonable handle to the MeterProvider implementation itself, and cloning it +/// will create a new reference, not a new instance of a MeterProvider. Dropping +/// the last reference to it will trigger shutdown of the provider. Shutdown can +/// also be triggered manually by calling the `shutdown` method. +/// [Meter]: opentelemetry::metrics::Meter +#[derive(Clone, Debug)] +pub struct SdkMeterProvider { + inner: Arc, +} + +#[derive(Debug)] +struct SdkMeterProviderInner { + pipes: Arc, + meters: Mutex>>, + shutdown_invoked: AtomicBool, +} + +impl Default for SdkMeterProvider { + fn default() -> Self { + SdkMeterProvider::builder().build() + } +} + +impl SdkMeterProvider { + /// Return default [MeterProviderBuilder] + pub fn builder() -> MeterProviderBuilder { + MeterProviderBuilder::default() + } + + /// Flushes all pending telemetry. + /// + /// There is no guaranteed that all telemetry be flushed or all resources have + /// been released on error. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::{global, Context}; + /// use opentelemetry_sdk::metrics::SdkMeterProvider; + /// + /// fn init_metrics() -> SdkMeterProvider { + /// // Setup metric pipelines with readers + views, default has no + /// // readers so nothing is exported. + /// let provider = SdkMeterProvider::default(); + /// + /// // Set provider to be used as global meter provider + /// let _ = global::set_meter_provider(provider.clone()); + /// + /// provider + /// } + /// + /// fn main() -> Result<(), Box> { + /// let provider = init_metrics(); + /// + /// // create instruments + record measurements + /// + /// // force all instruments to flush + /// provider.force_flush()?; + /// + /// // record more measurements.. + /// + /// // shutdown ensures any cleanup required by the provider is done, + /// // and also invokes shutdown on the readers. + /// provider.shutdown()?; + /// + /// Ok(()) + /// } + /// ``` + pub fn force_flush(&self) -> OTelSdkResult { + self.inner.force_flush() + } + + /// Shuts down the meter provider flushing all pending telemetry and releasing + /// any held computational resources. + /// + /// This call is idempotent. The first call will perform all flush and releasing + /// operations. Subsequent calls will perform no action and will return an error + /// stating this. + /// + /// Measurements made by instruments from meters this MeterProvider created will + /// not be exported after Shutdown is called. + /// + /// There is no guaranteed that all telemetry be flushed or all resources have + /// been released on error. + pub fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + otel_debug!( + name: "MeterProvider.Shutdown", + message = "User initiated shutdown of MeterProvider." + ); + self.inner.shutdown() + } + + /// shutdown with default timeout + pub fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } +} + +impl SdkMeterProviderInner { + fn force_flush(&self) -> OTelSdkResult { + if self + .shutdown_invoked + .load(std::sync::atomic::Ordering::Relaxed) + { + Err(crate::error::OTelSdkError::AlreadyShutdown) + } else { + self.pipes.force_flush() + } + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + if self + .shutdown_invoked + .swap(true, std::sync::atomic::Ordering::SeqCst) + { + // If the previous value was true, shutdown was already invoked. + Err(crate::error::OTelSdkError::AlreadyShutdown) + } else { + self.pipes.shutdown() + } + } + + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } +} + +impl Drop for SdkMeterProviderInner { + fn drop(&mut self) { + // If user has already shutdown the provider manually by calling + // shutdown(), then we don't need to call shutdown again. + if self.shutdown_invoked.load(Ordering::Relaxed) { + otel_debug!( + name: "MeterProvider.Drop.AlreadyShutdown", + message = "MeterProvider was already shut down; drop will not attempt shutdown again." + ); + } else { + otel_info!( + name: "MeterProvider.Drop", + message = "Last reference of MeterProvider dropped, initiating shutdown." + ); + if let Err(err) = self.shutdown() { + otel_error!( + name: "MeterProvider.Drop.ShutdownFailed", + message = "Shutdown attempt failed during drop of MeterProvider.", + reason = format!("{}", err) + ); + } else { + otel_debug!( + name: "MeterProvider.Drop.ShutdownCompleted", + ); + } + } + } +} + +impl MeterProvider for SdkMeterProvider { + fn meter(&self, name: &'static str) -> Meter { + let scope = InstrumentationScope::builder(name).build(); + self.meter_with_scope(scope) + } + + fn meter_with_scope(&self, scope: InstrumentationScope) -> Meter { + if self.inner.shutdown_invoked.load(Ordering::Relaxed) { + otel_debug!( + name: "MeterProvider.NoOpMeterReturned", + meter_name = scope.name(), + ); + return Meter::new(Arc::new(NoopMeter::new())); + } + + if scope.name().is_empty() { + otel_info!(name: "MeterNameEmpty", message = "Meter name is empty; consider providing a meaningful name. Meter will function normally and the provided name will be used as-is."); + }; + + if let Ok(mut meters) = self.inner.meters.lock() { + if let Some(existing_meter) = meters.get(&scope) { + otel_debug!( + name: "MeterProvider.ExistingMeterReturned", + meter_name = scope.name(), + ); + Meter::new(existing_meter.clone()) + } else { + let new_meter = Arc::new(SdkMeter::new(scope.clone(), self.inner.pipes.clone())); + meters.insert(scope.clone(), new_meter.clone()); + otel_debug!( + name: "MeterProvider.NewMeterCreated", + meter_name = scope.name(), + ); + Meter::new(new_meter) + } + } else { + otel_debug!( + name: "MeterProvider.NoOpMeterReturned", + meter_name = scope.name(), + ); + Meter::new(Arc::new(NoopMeter::new())) + } + } +} + +/// Configuration options for a [MeterProvider]. +#[derive(Default)] +pub struct MeterProviderBuilder { + resource: Option, + readers: Vec>, + views: Vec>, +} + +impl MeterProviderBuilder { + /// Associates a [Resource] with a [MeterProvider]. + /// + /// This [Resource] represents the entity producing telemetry and is associated + /// with all [Meter]s the [MeterProvider] will create. + /// + /// By default, if this option is not used, the default [Resource] will be used. + /// + /// When constructing a [Resource], use [`Resource::builder()`] to preserve + /// SDK-provided defaults such as `telemetry.sdk.*` and `service.name`. + /// Using [`Resource::builder_empty()`] will **not** include these attributes. + /// + /// # Example + /// + /// ``` + /// use opentelemetry_sdk::{Resource, metrics::SdkMeterProvider}; + /// use opentelemetry::KeyValue; + /// + /// let provider = SdkMeterProvider::builder() + /// .with_resource( + /// Resource::builder() + /// .with_service_name("my-service") + /// .with_attributes([KeyValue::new("deployment.environment.name", "production")]) + /// .build(), + /// ) + /// .build(); + /// ``` + /// + /// *Note*: Calls to this method are additive, each call merges the provided + /// resource with the previous one. + /// + /// [Meter]: opentelemetry::metrics::Meter + /// [`Resource::builder()`]: Resource::builder + /// [`Resource::builder_empty()`]: Resource::builder_empty + pub fn with_resource(mut self, resource: Resource) -> Self { + self.resource = match self.resource { + Some(existing) => Some(existing.merge(&resource)), + None => Some(resource), + }; + + self + } + + /// Associates a `MetricReader` with a [MeterProvider]. + /// [`MeterProviderBuilder::with_periodic_exporter()`] can be used to add a PeriodicReader which is + /// the most common use case. + /// + /// A [MeterProvider] will export no metrics without a `MetricReader` + /// added. + pub fn with_reader(mut self, reader: T) -> Self { + self.readers.push(Box::new(reader)); + self + } + + /// Adds a [`PushMetricExporter`] to the [`MeterProvider`] and configures it + /// to export metrics at **fixed** intervals (60 seconds) using a + /// [`PeriodicReader`]. + /// + /// To customize the export interval, set the + /// **"OTEL_METRIC_EXPORT_INTERVAL"** environment variable (in + /// milliseconds). + /// + /// Most users should use this method to attach an exporter. Advanced users + /// who need finer control over the export process can use + /// [`crate::metrics::PeriodicReaderBuilder`] to configure a custom reader and attach it + /// using [`MeterProviderBuilder::with_reader()`]. + pub fn with_periodic_exporter(mut self, exporter: T) -> Self + where + T: PushMetricExporter, + { + let reader = PeriodicReader::builder(exporter).build(); + self.readers.push(Box::new(reader)); + self + } + + /// Adds a view to the [MeterProvider]. + /// + /// Views allow you to customize how metrics are aggregated, renamed, or + /// otherwise transformed before export, without modifying instrument + /// definitions in your application or library code. + /// + /// You can pass any function or closure matching the signature + /// `Fn(&Instrument) -> Option + Send + Sync + 'static`. The + /// function receives a reference to the [`Instrument`] and can return an + /// [`Option`] of [`Stream`] to specify how matching instruments should be + /// exported. Returning `None` means the view does not apply to the given + /// instrument, and the default behavior will be used. + /// + /// + /// # Examples + /// + /// Renaming a metric: + /// + /// ``` + /// # use opentelemetry_sdk::metrics::{Stream, Instrument}; + /// let view_rename = |i: &Instrument| { + /// if i.name() == "my_counter" { + /// Some(Stream::builder().with_name("my_counter_renamed").build().expect("Stream should be valid")) + /// } else { + /// None + /// } + /// }; + /// # let builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); + /// # let _builder = + /// builder.with_view(view_rename); + /// ``` + /// + /// Setting a cardinality limit to control resource usage: + /// + /// ``` + /// # use opentelemetry_sdk::metrics::{Stream, Instrument}; + /// let view_change_cardinality = |i: &Instrument| { + /// if i.name() == "my_counter" { + /// Some( + /// Stream::builder() + /// .with_cardinality_limit(100).build().expect("Stream should be valid"), + /// ) + /// } else { + /// None + /// } + /// }; + /// # let builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); + /// # let _builder = + /// builder.with_view(view_change_cardinality); + /// ``` + /// + /// Silently ignoring Stream build errors: + /// + /// ``` + /// # use opentelemetry_sdk::metrics::{Stream, Instrument}; + /// let my_view_change_cardinality = |i: &Instrument| { + /// if i.name() == "my_second_histogram" { + /// // Note: If Stream is invalid, build() will return `Error` variant. + /// // By calling `.ok()`, any such error is ignored and treated as if the view does not match + /// // the instrument. + /// // If this is not the desired behavior, consider handling the error explicitly. + /// Stream::builder().with_cardinality_limit(0).build().ok() + /// } else { + /// None + /// } + /// }; + /// # let builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder(); + /// # let _builder = + /// builder.with_view(my_view_change_cardinality); + /// ``` + /// + /// If no views are added, the [MeterProvider] uses the default view. + /// + /// [`Instrument`]: crate::metrics::Instrument + /// [`Stream`]: crate::metrics::Stream + /// [`Option`]: core::option::Option + pub fn with_view(mut self, view: T) -> Self + where + T: Fn(&Instrument) -> Option + Send + Sync + 'static, + { + self.views.push(Arc::new(view)); + self + } + + /// Construct a new [MeterProvider] with this configuration. + pub fn build(self) -> SdkMeterProvider { + otel_debug!( + name: "MeterProvider.Building", + builder = format!("{:?}", &self), + ); + + let meter_provider = SdkMeterProvider { + inner: Arc::new(SdkMeterProviderInner { + pipes: Arc::new(Pipelines::new( + self.resource.unwrap_or(Resource::builder().build()), + self.readers, + self.views, + )), + meters: Default::default(), + shutdown_invoked: AtomicBool::new(false), + }), + }; + + otel_debug!( + name: "MeterProvider.Built", + ); + meter_provider + } +} + +impl fmt::Debug for MeterProviderBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MeterProviderBuilder") + .field("resource", &self.resource) + .field("readers", &self.readers) + .field("views", &self.views.len()) + .finish() + } +} +#[cfg(all(test, feature = "testing"))] +mod tests { + use crate::error::OTelSdkError; + use crate::metrics::SdkMeterProvider; + use crate::resource::{ + SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, TELEMETRY_SDK_VERSION, + }; + use crate::testing::metrics::metric_reader::TestMetricReader; + use crate::Resource; + use opentelemetry::metrics::MeterProvider; + use opentelemetry::{global, InstrumentationScope}; + use opentelemetry::{Key, KeyValue, Value}; + use std::env; + + #[test] + fn test_meter_provider_resource() { + let assert_resource = |provider: &super::SdkMeterProvider, + resource_key: &'static str, + expect: Option<&'static str>| { + assert_eq!( + provider.inner.pipes.0[0] + .resource + .get(&Key::from_static_str(resource_key)) + .map(|v| v.to_string()), + expect.map(|s| s.to_string()) + ); + }; + let assert_telemetry_resource = |provider: &super::SdkMeterProvider| { + assert_eq!( + provider.inner.pipes.0[0] + .resource + .get(&TELEMETRY_SDK_LANGUAGE.into()), + Some(Value::from("rust")) + ); + assert_eq!( + provider.inner.pipes.0[0] + .resource + .get(&TELEMETRY_SDK_NAME.into()), + Some(Value::from("opentelemetry")) + ); + assert_eq!( + provider.inner.pipes.0[0] + .resource + .get(&TELEMETRY_SDK_VERSION.into()), + Some(Value::from(env!("CARGO_PKG_VERSION"))) + ); + }; + + // If users didn't provide a resource and there isn't a env var set. Use default one. + temp_env::with_var_unset("OTEL_RESOURCE_ATTRIBUTES", || { + let reader = TestMetricReader::new(); + let default_meter_provider = super::SdkMeterProvider::builder() + .with_reader(reader) + .build(); + let service_name = default_meter_provider.inner.pipes.0[0] + .resource + .get(&Key::from_static_str(SERVICE_NAME)) + .map(|v| v.to_string()) + .unwrap(); + assert!( + service_name.starts_with("unknown_service:opentelemetry_sdk-"), + "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}", + service_name + ); + assert_telemetry_resource(&default_meter_provider); + }); + + // If user provided a resource, use that. + let reader2 = TestMetricReader::new(); + let custom_meter_provider = super::SdkMeterProvider::builder() + .with_reader(reader2) + .with_resource( + Resource::builder_empty() + .with_service_name("test_service") + .build(), + ) + .build(); + assert_resource(&custom_meter_provider, SERVICE_NAME, Some("test_service")); + assert_eq!(custom_meter_provider.inner.pipes.0[0].resource.len(), 1); + + temp_env::with_var( + "OTEL_RESOURCE_ATTRIBUTES", + Some("key1=value1, k2, k3=value2"), + || { + // If `OTEL_RESOURCE_ATTRIBUTES` is set, read them automatically + let reader3 = TestMetricReader::new(); + let env_resource_provider = super::SdkMeterProvider::builder() + .with_reader(reader3) + .build(); + let service_name = env_resource_provider.inner.pipes.0[0] + .resource + .get(&Key::from_static_str(SERVICE_NAME)) + .map(|v| v.to_string()) + .unwrap(); + assert!( + service_name.starts_with("unknown_service:opentelemetry_sdk-"), + "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}", + service_name + ); + assert_resource(&env_resource_provider, "key1", Some("value1")); + assert_resource(&env_resource_provider, "k3", Some("value2")); + assert_telemetry_resource(&env_resource_provider); + assert_eq!(env_resource_provider.inner.pipes.0[0].resource.len(), 6); + }, + ); + + // When `OTEL_RESOURCE_ATTRIBUTES` is set and also user provided config + temp_env::with_var( + "OTEL_RESOURCE_ATTRIBUTES", + Some("my-custom-key=env-val,k2=value2"), + || { + let reader4 = TestMetricReader::new(); + let user_provided_resource_config_provider = super::SdkMeterProvider::builder() + .with_reader(reader4) + .with_resource( + Resource::builder() + .with_attributes([ + KeyValue::new("my-custom-key", "my-custom-value"), + KeyValue::new("my-custom-key2", "my-custom-value2"), + ]) + .build(), + ) + .build(); + let service_name = user_provided_resource_config_provider.inner.pipes.0[0] + .resource + .get(&Key::from_static_str(SERVICE_NAME)) + .map(|v| v.to_string()) + .unwrap(); + assert!( + service_name.starts_with("unknown_service:opentelemetry_sdk-"), + "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}", + service_name + ); + assert_resource( + &user_provided_resource_config_provider, + "my-custom-key", + Some("my-custom-value"), + ); + assert_resource( + &user_provided_resource_config_provider, + "my-custom-key2", + Some("my-custom-value2"), + ); + assert_resource( + &user_provided_resource_config_provider, + "k2", + Some("value2"), + ); + assert_telemetry_resource(&user_provided_resource_config_provider); + assert_eq!( + user_provided_resource_config_provider.inner.pipes.0[0] + .resource + .len(), + 7 + ); + }, + ); + + // If user provided a resource, it takes priority during collision. + let reader5 = TestMetricReader::new(); + let no_service_name = super::SdkMeterProvider::builder() + .with_reader(reader5) + .with_resource(Resource::empty()) + .build(); + + assert_eq!(no_service_name.inner.pipes.0[0].resource.len(), 0) + } + + #[test] + fn test_meter_provider_shutdown() { + let reader = TestMetricReader::new(); + let provider = super::SdkMeterProvider::builder() + .with_reader(reader.clone()) + .build(); + global::set_meter_provider(provider.clone()); + assert!(!reader.is_shutdown()); + // create a meter and an instrument + let meter = global::meter("test"); + let counter = meter.u64_counter("test_counter").build(); + // no need to drop a meter for meter_provider shutdown + let shutdown_res = provider.shutdown(); + assert!(shutdown_res.is_ok()); + + // shutdown once more should return an error + let shutdown_res = provider.shutdown(); + assert!(matches!(shutdown_res, Err(OTelSdkError::AlreadyShutdown))); + + assert!(shutdown_res.is_err()); + assert!(reader.is_shutdown()); + // TODO Fix: the instrument is still available, and can be used. + // While the reader is shutdown, and no collect is happening + counter.add(1, &[]); + } + #[test] + fn test_shutdown_invoked_on_last_drop() { + let reader = TestMetricReader::new(); + let provider = super::SdkMeterProvider::builder() + .with_reader(reader.clone()) + .build(); + let clone1 = provider.clone(); + let clone2 = provider.clone(); + + // Initially, shutdown should not be called + assert!(!reader.is_shutdown()); + + // Drop the first clone + drop(clone1); + assert!(!reader.is_shutdown()); + + // Drop the second clone + drop(clone2); + assert!(!reader.is_shutdown()); + + // Drop the last original provider + drop(provider); + // Now the shutdown should be invoked + assert!(reader.is_shutdown()); + } + + #[test] + fn same_meter_reused_same_scope() { + let provider = super::SdkMeterProvider::builder().build(); + let _meter1 = provider.meter("test"); + let _meter2 = provider.meter("test"); + assert_eq!(provider.inner.meters.lock().unwrap().len(), 1); + + let scope = InstrumentationScope::builder("test") + .with_version("1.0.0") + .with_schema_url("http://example.com") + .build(); + + let _meter3 = provider.meter_with_scope(scope.clone()); + let _meter4 = provider.meter_with_scope(scope.clone()); + let _meter5 = provider.meter_with_scope(scope); + assert_eq!(provider.inner.meters.lock().unwrap().len(), 2); + + // these are different meters because meter names are case sensitive + let make_scope = |name| { + InstrumentationScope::builder(name) + .with_version("1.0.0") + .with_schema_url("http://example.com") + .build() + }; + + let _meter6 = provider.meter_with_scope(make_scope("ABC")); + let _meter7 = provider.meter_with_scope(make_scope("Abc")); + let _meter8 = provider.meter_with_scope(make_scope("abc")); + + assert_eq!(provider.inner.meters.lock().unwrap().len(), 5); + } + + #[test] + fn same_meter_reused_same_scope_attributes() { + let meter_provider = super::SdkMeterProvider::builder().build(); + let make_scope = |attributes| { + InstrumentationScope::builder("test.meter") + .with_version("v0.1.0") + .with_schema_url("http://example.com") + .with_attributes(attributes) + .build() + }; + + let _meter1 = + meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")])); + let _meter2 = + meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")])); + + assert_eq!(meter_provider.inner.meters.lock().unwrap().len(), 1); + + // these are identical because InstrumentScope ignores the order of attributes + let _meter3 = meter_provider.meter_with_scope(make_scope(vec![ + KeyValue::new("key1", "value1"), + KeyValue::new("key2", "value2"), + ])); + let _meter4 = meter_provider.meter_with_scope(make_scope(vec![ + KeyValue::new("key2", "value2"), + KeyValue::new("key1", "value1"), + ])); + + assert_eq!(meter_provider.inner.meters.lock().unwrap().len(), 2); + } + + #[test] + fn different_meter_different_attributes() { + let meter_provider = super::SdkMeterProvider::builder().build(); + let make_scope = |attributes| { + InstrumentationScope::builder("test.meter") + .with_version("v0.1.0") + .with_schema_url("http://example.com") + .with_attributes(attributes) + .build() + }; + + let _meter1 = meter_provider.meter_with_scope(make_scope(vec![])); + // _meter2 and _meter3, and _meter4 are different because attribute is case sensitive + let _meter2 = + meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key1", "value1")])); + let _meter3 = + meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("Key1", "value1")])); + let _meter4 = + meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key1", "Value1")])); + let _meter5 = meter_provider.meter_with_scope(make_scope(vec![ + KeyValue::new("key1", "value1"), + KeyValue::new("key2", "value2"), + ])); + + assert_eq!(meter_provider.inner.meters.lock().unwrap().len(), 5); + } + + #[test] + fn with_resource_multiple_calls_ensure_additive() { + let builder = SdkMeterProvider::builder() + .with_resource( + Resource::builder_empty() + .with_attributes(vec![KeyValue::new("key1", "value1")]) + .build(), + ) + .with_resource( + Resource::builder_empty() + .with_attributes(vec![KeyValue::new("key2", "value2")]) + .build(), + ) + .with_resource( + Resource::builder_empty() + .with_schema_url(vec![], "http://example.com") + .build(), + ) + .with_resource( + Resource::builder_empty() + .with_attributes(vec![KeyValue::new("key3", "value3")]) + .build(), + ); + + let resource = builder.resource.unwrap(); + + assert_eq!( + resource.get(&Key::from_static_str("key1")), + Some(Value::from("value1")) + ); + assert_eq!( + resource.get(&Key::from_static_str("key2")), + Some(Value::from("value2")) + ); + assert_eq!( + resource.get(&Key::from_static_str("key3")), + Some(Value::from("value3")) + ); + assert_eq!(resource.schema_url(), Some("http://example.com")); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/mod.rs new file mode 100644 index 00000000..ba102609 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/mod.rs @@ -0,0 +1,6745 @@ +//! The crust of the OpenTelemetry metrics SDK. +//! +//! ## Configuration +//! +//! The metrics SDK configuration is stored with each [SdkMeterProvider]. +//! Configuration for [Resource]s, views, and `ManualReader` or +//! [PeriodicReader] instances can be specified. +//! +//! ### Example +//! +//! ``` +//! use opentelemetry::global; +//! use opentelemetry::KeyValue; +//! use opentelemetry_sdk::{metrics::SdkMeterProvider, Resource}; +//! +//! // Generate SDK configuration, resource, views, etc +//! let resource = Resource::builder().build(); // default attributes about the current process +//! +//! // Create a meter provider with the desired config +//! let meter_provider = SdkMeterProvider::builder().with_resource(resource).build(); +//! global::set_meter_provider(meter_provider.clone()); +//! +//! // Use the meter provider to create meter instances +//! let meter = global::meter("my_app"); +//! +//! // Create instruments scoped to the meter +//! let counter = meter +//! .u64_counter("power_consumption") +//! .with_unit("kWh") +//! .build(); +//! +//! // use instruments to record measurements +//! counter.add(10, &[KeyValue::new("rate", "standard")]); +//! +//! // shutdown the provider at the end of the application to ensure any metrics not yet +//! // exported are flushed. +//! meter_provider.shutdown().unwrap(); +//! ``` +//! +//! [Resource]: crate::Resource + +#[allow(unreachable_pub)] +#[allow(unused)] +pub(crate) mod aggregation; +pub mod data; +mod error; +pub mod exporter; +pub(crate) mod instrument; +pub(crate) mod internal; +#[cfg(feature = "experimental_metrics_custom_reader")] +pub(crate) mod manual_reader; +pub(crate) mod meter; +mod meter_provider; +pub(crate) mod noop; +pub(crate) mod periodic_reader; +#[cfg(feature = "experimental_metrics_periodicreader_with_async_runtime")] +/// Module for periodic reader with async runtime. +pub mod periodic_reader_with_async_runtime; +pub(crate) mod pipeline; +#[cfg(feature = "experimental_metrics_custom_reader")] +pub mod reader; +#[cfg(not(feature = "experimental_metrics_custom_reader"))] +pub(crate) mod reader; +pub(crate) mod view; + +/// In-Memory metric exporter for testing purpose. +#[cfg(any(feature = "testing", test))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "testing", test))))] +pub mod in_memory_exporter; +#[cfg(any(feature = "testing", test))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "testing", test))))] +pub use in_memory_exporter::{InMemoryMetricExporter, InMemoryMetricExporterBuilder}; + +pub use aggregation::*; +#[cfg(feature = "experimental_metrics_custom_reader")] +pub use manual_reader::*; +pub use meter_provider::*; +pub use periodic_reader::*; +#[cfg(feature = "experimental_metrics_custom_reader")] +pub use pipeline::Pipeline; + +pub use instrument::{Instrument, InstrumentKind, Stream, StreamBuilder}; + +use std::hash::Hash; +use std::str::FromStr; + +/// Defines the window that an aggregation was calculated over. +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum Temporality { + /// A measurement interval that continues to expand forward in time from a + /// starting point. + /// + /// New measurements are added to all previous measurements since a start time. + #[default] + Cumulative, + + /// A measurement interval that resets each cycle. + /// + /// Measurements from one cycle are recorded independently, measurements from + /// other cycles do not affect them. + Delta, + + /// Configures Synchronous Counter and Histogram instruments to use + /// Delta aggregation temporality, which allows them to shed memory + /// following a cardinality explosion, thus use less memory. + LowMemory, +} + +impl FromStr for Temporality { + type Err = (); + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "cumulative" => Ok(Temporality::Cumulative), + "delta" => Ok(Temporality::Delta), + "lowmemory" => Ok(Temporality::LowMemory), + _ => Err(()), + } + } +} + +#[cfg(all(test, feature = "testing"))] +mod tests { + #[cfg(feature = "experimental_metrics_bound_instruments")] + use self::data::ExponentialHistogramDataPoint; + use self::data::{HistogramDataPoint, MetricData, ScopeMetrics, SumDataPoint}; + use super::internal::Number; + use super::*; + use crate::metrics::data::ResourceMetrics; + use crate::metrics::internal::AggregatedMetricsAccess; + use crate::metrics::InMemoryMetricExporter; + use crate::metrics::InMemoryMetricExporterBuilder; + use data::GaugeDataPoint; + use opentelemetry::metrics::{Counter, Meter, UpDownCounter}; + use opentelemetry::InstrumentationScope; + use opentelemetry::Value; + use opentelemetry::{metrics::MeterProvider as _, KeyValue}; + use rand::{rngs, Rng, SeedableRng}; + use std::cmp::{max, min}; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::{Arc, Mutex}; + use std::thread; + use std::time::Duration; + + use rstest::rstest; + + // Run all tests in this mod + // cargo test metrics::tests --features=testing,spec_unstable_metrics_views + // Note for all tests from this point onwards in this mod: + // "multi_thread" tokio flavor must be used else flush won't + // be able to make progress! + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + #[cfg(not(feature = "experimental_metrics_disable_name_validation"))] + async fn invalid_instrument_config_noops() { + // Run this test with stdout enabled to see output. + // cargo test invalid_instrument_config_noops --features=testing,spec_unstable_metrics_views -- --nocapture + let invalid_instrument_names = vec![ + "_startWithNoneAlphabet", + "utf8char锈", + "a".repeat(256).leak(), + "invalid name", + ]; + for name in invalid_instrument_names { + let test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.meter().u64_counter(name).build(); + counter.add(1, &[]); + + let up_down_counter = test_context.meter().i64_up_down_counter(name).build(); + up_down_counter.add(1, &[]); + + let gauge = test_context.meter().f64_gauge(name).build(); + gauge.record(1.9, &[]); + + let histogram = test_context.meter().f64_histogram(name).build(); + histogram.record(1.0, &[]); + + let _observable_counter = test_context + .meter() + .u64_observable_counter(name) + .with_callback(move |observer| { + observer.observe(1, &[]); + }) + .build(); + + let _observable_gauge = test_context + .meter() + .f64_observable_gauge(name) + .with_callback(move |observer| { + observer.observe(1.0, &[]); + }) + .build(); + + let _observable_up_down_counter = test_context + .meter() + .i64_observable_up_down_counter(name) + .with_callback(move |observer| { + observer.observe(1, &[]); + }) + .build(); + + test_context.flush_metrics(); + + // As instrument name is invalid, no metrics should be exported + test_context.check_no_metrics(); + } + + let invalid_bucket_boundaries = vec![ + vec![1.0, 1.0], // duplicate boundaries + vec![1.0, 2.0, 3.0, 2.0], // duplicate non consequent boundaries + vec![1.0, 2.0, 3.0, 4.0, 2.5], // unsorted boundaries + vec![1.0, 2.0, 3.0, f64::INFINITY, 4.0], // boundaries with positive infinity + vec![1.0, 2.0, 3.0, f64::NAN], // boundaries with NaNs + vec![f64::NEG_INFINITY, 2.0, 3.0], // boundaries with negative infinity + ]; + for bucket_boundaries in invalid_bucket_boundaries { + let test_context = TestContext::new(Temporality::Cumulative); + let histogram = test_context + .meter() + .f64_histogram("test") + .with_boundaries(bucket_boundaries) + .build(); + histogram.record(1.9, &[]); + test_context.flush_metrics(); + + // As bucket boundaries provided via advisory params are invalid, + // no metrics should be exported + test_context.check_no_metrics(); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + #[cfg(feature = "experimental_metrics_disable_name_validation")] + async fn valid_instrument_config_with_feature_experimental_metrics_disable_name_validation() { + // Run this test with stdout enabled to see output. + // cargo test valid_instrument_config_with_feature_experimental_metrics_disable_name_validation --all-features -- --nocapture + let invalid_instrument_names = vec![ + "_startWithNoneAlphabet", + "utf8char锈", + "", + "a".repeat(256).leak(), + "\\allow\\slash /sec", + "\\allow\\$$slash /sec", + "Total $ Count", + "\\test\\UsagePercent(Total) > 80%", + "invalid name", + ]; + for name in invalid_instrument_names { + let test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.meter().u64_counter(name).build(); + counter.add(1, &[]); + + let up_down_counter = test_context.meter().i64_up_down_counter(name).build(); + up_down_counter.add(1, &[]); + + let gauge = test_context.meter().f64_gauge(name).build(); + gauge.record(1.9, &[]); + + let histogram = test_context.meter().f64_histogram(name).build(); + histogram.record(1.0, &[]); + + let _observable_counter = test_context + .meter() + .u64_observable_counter(name) + .with_callback(move |observer| { + observer.observe(1, &[]); + }) + .build(); + + let _observable_gauge = test_context + .meter() + .f64_observable_gauge(name) + .with_callback(move |observer| { + observer.observe(1.0, &[]); + }) + .build(); + + let _observable_up_down_counter = test_context + .meter() + .i64_observable_up_down_counter(name) + .with_callback(move |observer| { + observer.observe(1, &[]); + }) + .build(); + + test_context.flush_metrics(); + + // As instrument name are valid because of the feature flag, metrics should be exported + let resource_metrics = test_context + .exporter + .get_finished_metrics() + .expect("metrics expected to be exported"); + + assert!(!resource_metrics.is_empty(), "metrics should be exported"); + } + + // Ensuring that the Histograms with invalid bucket boundaries are not exported + // when using the feature flag + let invalid_bucket_boundaries = vec![ + vec![1.0, 1.0], // duplicate boundaries + vec![1.0, 2.0, 3.0, 2.0], // duplicate non consequent boundaries + vec![1.0, 2.0, 3.0, 4.0, 2.5], // unsorted boundaries + vec![1.0, 2.0, 3.0, f64::INFINITY, 4.0], // boundaries with positive infinity + vec![1.0, 2.0, 3.0, f64::NAN], // boundaries with NaNs + vec![f64::NEG_INFINITY, 2.0, 3.0], // boundaries with negative infinity + ]; + for bucket_boundaries in invalid_bucket_boundaries { + let test_context = TestContext::new(Temporality::Cumulative); + let histogram = test_context + .meter() + .f64_histogram("test") + .with_boundaries(bucket_boundaries) + .build(); + histogram.record(1.9, &[]); + test_context.flush_metrics(); + + // As bucket boundaries provided via advisory params are invalid, + // no metrics should be exported + test_context.check_no_metrics(); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_aggregation_delta() { + // Run this test with stdout enabled to see output. + // cargo test counter_aggregation_delta --features=testing -- --nocapture + counter_aggregation_helper(Temporality::Delta); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_aggregation_cumulative() { + // Run this test with stdout enabled to see output. + // cargo test counter_aggregation_cumulative --features=testing -- --nocapture + counter_aggregation_helper(Temporality::Cumulative); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_aggregation_no_attributes_cumulative() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.u64_counter("test", "my_counter", None); + + counter.add(50, &[]); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 1, "Expected only one data point"); + assert!(sum.is_monotonic, "Should produce monotonic."); + assert_eq!( + sum.temporality, + Temporality::Cumulative, + "Should produce cumulative" + ); + + let data_point = &sum.data_points[0]; + assert!(data_point.attributes.is_empty(), "Non-empty attribute set"); + assert_eq!(data_point.value, 50, "Unexpected data point value"); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_aggregation_no_attributes_delta() { + let mut test_context = TestContext::new(Temporality::Delta); + let counter = test_context.u64_counter("test", "my_counter", None); + + counter.add(50, &[]); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 1, "Expected only one data point"); + assert!(sum.is_monotonic, "Should produce monotonic."); + assert_eq!(sum.temporality, Temporality::Delta, "Should produce delta"); + + let data_point = &sum.data_points[0]; + assert!(data_point.attributes.is_empty(), "Non-empty attribute set"); + assert_eq!(data_point.value, 50, "Unexpected data point value"); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_aggregation_overflow_delta() { + counter_aggregation_overflow_helper(Temporality::Delta); + counter_aggregation_overflow_helper_custom_limit(Temporality::Delta); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_aggregation_overflow_cumulative() { + counter_aggregation_overflow_helper(Temporality::Cumulative); + counter_aggregation_overflow_helper_custom_limit(Temporality::Cumulative); + } + + #[rstest] + #[case(Temporality::Delta, true)] + #[case(Temporality::Delta, false)] + #[case(Temporality::Cumulative, true)] + #[case(Temporality::Cumulative, false)] + fn counter_aggregation(#[case] temporality: Temporality, #[case] start_sorted: bool) { + // Run this test with stdout enabled to see output. + // cargo test counter_aggregation_attribute --features=testing -- --nocapture + counter_aggregation_attribute_order_helper(temporality, start_sorted); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn histogram_aggregation_cumulative() { + // Run this test with stdout enabled to see output. + // cargo test histogram_aggregation_cumulative --features=testing -- --nocapture + histogram_aggregation_helper(Temporality::Cumulative); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn histogram_aggregation_delta() { + // Run this test with stdout enabled to see output. + // cargo test histogram_aggregation_delta --features=testing -- --nocapture + histogram_aggregation_helper(Temporality::Delta); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn histogram_aggregation_with_custom_bounds() { + // Run this test with stdout enabled to see output. + // cargo test histogram_aggregation_with_custom_bounds --features=testing -- --nocapture + histogram_aggregation_with_custom_bounds_helper(Temporality::Delta); + histogram_aggregation_with_custom_bounds_helper(Temporality::Cumulative); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn histogram_aggregation_with_empty_bounds() { + // Run this test with stdout enabled to see output. + // cargo test histogram_aggregation_with_empty_bounds --features=testing -- --nocapture + histogram_aggregation_with_empty_bounds_helper(Temporality::Delta); + histogram_aggregation_with_empty_bounds_helper(Temporality::Cumulative); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn histogram_aggregation_with_custom_bounds_and_view() { + // Run this test with stdout enabled to see output. + // cargo test histogram_aggregation_with_custom_bounds_and_view --features=testing -- --nocapture + histogram_aggregation_with_custom_bounds_and_view_helper(Temporality::Delta); + histogram_aggregation_with_custom_bounds_and_view_helper(Temporality::Cumulative); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn exponential_histogram_aggregation_with_view() { + // Run this test with stdout enabled to see output. + // cargo test exponential_histogram_aggregation_with_view --features=testing -- --nocapture + exponential_histogram_aggregation_with_view_helper(Temporality::Delta); + exponential_histogram_aggregation_with_view_helper(Temporality::Cumulative); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn updown_counter_aggregation_cumulative() { + // Run this test with stdout enabled to see output. + // cargo test updown_counter_aggregation_cumulative --features=testing -- --nocapture + updown_counter_aggregation_helper(Temporality::Cumulative); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn updown_counter_aggregation_delta() { + // Run this test with stdout enabled to see output. + // cargo test updown_counter_aggregation_delta --features=testing -- --nocapture + updown_counter_aggregation_helper(Temporality::Delta); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn gauge_aggregation() { + // Run this test with stdout enabled to see output. + // cargo test gauge_aggregation --features=testing -- --nocapture + + // Gauge should use last value aggregation regardless of the aggregation temporality used. + gauge_aggregation_helper(Temporality::Delta); + gauge_aggregation_helper(Temporality::Cumulative); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn observable_gauge_aggregation() { + // Run this test with stdout enabled to see output. + // cargo test observable_gauge_aggregation --features=testing -- --nocapture + + // Gauge should use last value aggregation regardless of the aggregation temporality used. + observable_gauge_aggregation_helper(Temporality::Delta, false); + observable_gauge_aggregation_helper(Temporality::Delta, true); + observable_gauge_aggregation_helper(Temporality::Cumulative, false); + observable_gauge_aggregation_helper(Temporality::Cumulative, true); + } + + #[rstest] + #[case(Temporality::Cumulative, 10, false)] + #[case(Temporality::Cumulative, 10, true)] + #[case(Temporality::Delta, 10, false)] + #[case(Temporality::Delta, 10, true)] + #[case(Temporality::Cumulative, 0, false)] + #[case(Temporality::Cumulative, 0, true)] + #[case(Temporality::Delta, 0, false)] + #[case(Temporality::Delta, 0, true)] + fn observable_counter_aggregation( + #[case] temporality: Temporality, + #[case] increment: u64, + #[case] is_empty_attributes: bool, + ) { + // Run this test with stdout enabled to see output. + // cargo test observable_counter_aggregation --features=testing -- --nocapture + observable_counter_aggregation_helper(temporality, 100, increment, 4, is_empty_attributes); + } + + fn observable_counter_aggregation_helper( + temporality: Temporality, + start: u64, + increment: u64, + length: u64, + is_empty_attributes: bool, + ) { + // Arrange + let mut test_context = TestContext::new(temporality); + let attributes = if is_empty_attributes { + vec![] + } else { + vec![KeyValue::new("key1", "value1")] + }; + // The Observable counter reports values[0], values[1],....values[n] on each flush. + let values: Vec = (0..length).map(|i| start + i * increment).collect(); + println!("Testing with observable values: {values:?}"); + let values = Arc::new(values); + let values_clone = values.clone(); + let i = Arc::new(Mutex::new(0)); + let _observable_counter = test_context + .meter() + .u64_observable_counter("my_observable_counter") + .with_unit("my_unit") + .with_callback(move |observer| { + let mut index = i.lock().unwrap(); + if *index < values.len() { + observer.observe(values[*index], &attributes); + *index += 1; + } + }) + .build(); + + for (iter, v) in values_clone.iter().enumerate() { + test_context.flush_metrics(); + let MetricData::Sum(sum) = + test_context.get_aggregation::("my_observable_counter", None) + else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 1); + assert!(sum.is_monotonic, "Counter should produce monotonic."); + if let Temporality::Cumulative = temporality { + assert_eq!( + sum.temporality, + Temporality::Cumulative, + "Should produce cumulative" + ); + } else { + assert_eq!(sum.temporality, Temporality::Delta, "Should produce delta"); + } + + // find and validate datapoint + let data_point = if is_empty_attributes { + &sum.data_points[0] + } else { + find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected") + }; + + if let Temporality::Cumulative = temporality { + // Cumulative counter should have the value as is. + assert_eq!(data_point.value, *v); + } else { + // Delta counter should have the increment value. + // Except for the first value which should be the start value. + if iter == 0 { + assert_eq!(data_point.value, start); + } else { + assert_eq!(data_point.value, increment); + } + } + + test_context.reset_metrics(); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn observable_counter_delta_attribute_order_changes() { + let mut test_context = TestContext::new(Temporality::Delta); + + let observations = Arc::new(Mutex::new(std::collections::VecDeque::from([ + ( + 100, + vec![ + KeyValue::new("A", "a"), + KeyValue::new("B", "b"), + KeyValue::new("C", "c"), + ], + ), + ( + 150, + vec![ + KeyValue::new("B", "b"), + KeyValue::new("A", "a"), + KeyValue::new("C", "c"), + ], + ), + ( + 175, + vec![ + KeyValue::new("C", "c"), + KeyValue::new("B", "b"), + KeyValue::new("A", "a"), + ], + ), + ]))); + let observations_clone = observations.clone(); + + let _observable_counter = test_context + .meter() + .u64_observable_counter("my_observable_counter") + .with_callback(move |observer| { + let mut observations = observations_clone.lock().unwrap(); + if let Some((value, attributes)) = observations.pop_front() { + observer.observe(value, &attributes); + } + }) + .build(); + + for expected in [100, 50, 25] { + test_context.flush_metrics(); + + let MetricData::Sum(sum) = + test_context.get_aggregation::("my_observable_counter", None) + else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 1); + assert_eq!(sum.temporality, Temporality::Delta); + + let data_point = &sum.data_points[0]; + assert_eq!(data_point.attributes.len(), 3); + assert!(data_point + .attributes + .iter() + .any(|kv| kv.key.as_str() == "A" && kv.value.as_str() == "a")); + assert!(data_point + .attributes + .iter() + .any(|kv| kv.key.as_str() == "B" && kv.value.as_str() == "b")); + assert!(data_point + .attributes + .iter() + .any(|kv| kv.key.as_str() == "C" && kv.value.as_str() == "c")); + assert_eq!(data_point.value, expected); + + test_context.reset_metrics(); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn observable_counter_delta_attribute_set_reappears_after_gap() { + // Run this test with stdout enabled to see output. + // cargo test observable_counter_delta_attribute_set_reappears_after_gap --features=testing -- --nocapture + + // This test verifies the behavior when an attribute set is not reported + // for one collection cycle and then reappears. + // See: https://github.com/open-telemetry/opentelemetry-specification/issues/4861 + // + // Scenario (Observable Counter with Delta temporality): + // | Collection | Callback Reports | Expected Delta Export | + // |------------|-------------------|------------------------| + // | 1 | A=100, B=50 | A=100, B=50 | + // | 2 | A=150 (B missing) | A=50 (B not exported) | + // | 3 | A=200, B=80 | A=50, B=80 | + // + // Current implementation: When B reappears, its delta is calculated from zero + // (fresh start), not from the last known value. This is Option 1 from the spec issue. + + let mut test_context = TestContext::new(Temporality::Delta); + + // Shared state for callback: (collection_cycle, value_a, value_b_option) + // value_b_option is None when B should not be reported + let callback_state = Arc::new(Mutex::new((0u32, 0u64, Option::::None))); + let callback_state_clone = callback_state.clone(); + + let _observable_counter = test_context + .meter() + .u64_observable_counter("my_observable_counter") + .with_callback(move |observer| { + let state = callback_state_clone.lock().unwrap(); + let (_cycle, value_a, value_b_option) = *state; + + observer.observe(value_a, &[KeyValue::new("key", "A")]); + if let Some(value_b) = value_b_option { + observer.observe(value_b, &[KeyValue::new("key", "B")]); + } + }) + .build(); + + // Collection 1: A=100, B=50 + { + *callback_state.lock().unwrap() = (1, 100, Some(50)); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = + test_context.get_aggregation::("my_observable_counter", None) + else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 2); + assert_eq!(sum.temporality, Temporality::Delta); + + let dp_a = find_sum_datapoint_with_key_value(&sum.data_points, "key", "A") + .expect("datapoint for A expected"); + let dp_b = find_sum_datapoint_with_key_value(&sum.data_points, "key", "B") + .expect("datapoint for B expected"); + + // First collection: delta = value - 0 + assert_eq!( + dp_a.value, 100, + "A's delta should be 100 (first collection)" + ); + assert_eq!(dp_b.value, 50, "B's delta should be 50 (first collection)"); + + test_context.reset_metrics(); + } + + // Collection 2: A=150, B missing + { + *callback_state.lock().unwrap() = (2, 150, None); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = + test_context.get_aggregation::("my_observable_counter", None) + else { + unreachable!() + }; + + // Only A should be exported, B is not observed so not exported (per spec) + assert_eq!( + sum.data_points.len(), + 1, + "Only A should be exported when B is not observed" + ); + + let dp_a = find_sum_datapoint_with_key_value(&sum.data_points, "key", "A") + .expect("datapoint for A expected"); + assert_eq!(dp_a.value, 50, "A's delta should be 50 (150 - 100)"); + + test_context.reset_metrics(); + } + + // Collection 3: A=200, B=80 (B reappears) + { + *callback_state.lock().unwrap() = (3, 200, Some(80)); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = + test_context.get_aggregation::("my_observable_counter", None) + else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 2); + + let dp_a = find_sum_datapoint_with_key_value(&sum.data_points, "key", "A") + .expect("datapoint for A expected"); + let dp_b = find_sum_datapoint_with_key_value(&sum.data_points, "key", "B") + .expect("datapoint for B expected"); + + assert_eq!(dp_a.value, 50, "A's delta should be 50 (200 - 150)"); + + // B reappears after a gap. Current implementation uses "delta from zero" (Option 1). + // This means B's delta = 80 - 0 = 80, not 80 - 50 = 30. + // See: https://github.com/open-telemetry/opentelemetry-specification/issues/4861 + // TODO: Watch for spec clarification on this behavior. + assert_eq!( + dp_b.value, 80, + "B's delta should be 80 (fresh start after gap, not 30 from last known value)" + ); + + test_context.reset_metrics(); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn empty_meter_name_retained() { + async fn meter_name_retained_helper( + meter: Meter, + provider: SdkMeterProvider, + exporter: InMemoryMetricExporter, + ) { + // Act + let counter = meter.u64_counter("my_counter").build(); + + counter.add(10, &[]); + provider.force_flush().unwrap(); + + // Assert + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!( + resource_metrics[0].scope_metrics[0].metrics.len() == 1, + "There should be a single metric" + ); + let meter_name = resource_metrics[0].scope_metrics[0].scope.name(); + assert_eq!(meter_name, ""); + } + + let exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .build(); + + // Test Meter creation in 2 ways, both with empty string as meter name + let meter1 = meter_provider.meter(""); + meter_name_retained_helper(meter1, meter_provider.clone(), exporter.clone()).await; + + let meter_scope = InstrumentationScope::builder("").build(); + let meter2 = meter_provider.meter_with_scope(meter_scope); + meter_name_retained_helper(meter2, meter_provider, exporter).await; + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_duplicate_instrument_merge() { + // Arrange + let exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let counter = meter + .u64_counter("my_counter") + .with_unit("my_unit") + .with_description("my_description") + .build(); + + let counter_duplicated = meter + .u64_counter("my_counter") + .with_unit("my_unit") + .with_description("my_description") + .build(); + + let attribute = vec![KeyValue::new("key1", "value1")]; + counter.add(10, &attribute); + counter_duplicated.add(5, &attribute); + + meter_provider.force_flush().unwrap(); + + // Assert + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!( + resource_metrics[0].scope_metrics[0].metrics.len() == 1, + "There should be single metric merging duplicate instruments" + ); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + assert_eq!(metric.name, "my_counter"); + assert_eq!(metric.unit, "my_unit"); + let MetricData::Sum(sum) = u64::extract_metrics_data_ref(&metric.data) + .expect("Sum aggregation expected for Counter instruments by default") + else { + unreachable!() + }; + + // Expecting 1 time-series. + assert_eq!(sum.data_points.len(), 1); + + let datapoint = &sum.data_points[0]; + assert_eq!(datapoint.value, 15); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_duplicate_instrument_different_meter_no_merge() { + // Arrange + let exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .build(); + + // Act + let meter1 = meter_provider.meter("test.meter1"); + let meter2 = meter_provider.meter("test.meter2"); + let counter1 = meter1 + .u64_counter("my_counter") + .with_unit("my_unit") + .with_description("my_description") + .build(); + + let counter2 = meter2 + .u64_counter("my_counter") + .with_unit("my_unit") + .with_description("my_description") + .build(); + + let attribute = vec![KeyValue::new("key1", "value1")]; + counter1.add(10, &attribute); + counter2.add(5, &attribute); + + meter_provider.force_flush().unwrap(); + + // Assert + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!( + resource_metrics[0].scope_metrics.len() == 2, + "There should be 2 separate scope" + ); + assert!( + resource_metrics[0].scope_metrics[0].metrics.len() == 1, + "There should be single metric for the scope" + ); + assert!( + resource_metrics[0].scope_metrics[1].metrics.len() == 1, + "There should be single metric for the scope" + ); + + let scope1 = find_scope_metric(&resource_metrics[0].scope_metrics, "test.meter1"); + let scope2 = find_scope_metric(&resource_metrics[0].scope_metrics, "test.meter2"); + + if let Some(scope1) = scope1 { + let metric1 = &scope1.metrics[0]; + assert_eq!(metric1.name, "my_counter"); + assert_eq!(metric1.unit, "my_unit"); + assert_eq!(metric1.description, "my_description"); + let MetricData::Sum(sum1) = u64::extract_metrics_data_ref(&metric1.data) + .expect("Sum aggregation expected for Counter instruments by default") + else { + unreachable!() + }; + + // Expecting 1 time-series. + assert_eq!(sum1.data_points.len(), 1); + + let datapoint1 = &sum1.data_points[0]; + assert_eq!(datapoint1.value, 10); + } else { + panic!("No MetricScope found for 'test.meter1'"); + } + + if let Some(scope2) = scope2 { + let metric2 = &scope2.metrics[0]; + assert_eq!(metric2.name, "my_counter"); + assert_eq!(metric2.unit, "my_unit"); + assert_eq!(metric2.description, "my_description"); + + let MetricData::Sum(sum2) = u64::extract_metrics_data_ref(&metric2.data) + .expect("Sum aggregation expected for Counter instruments by default") + else { + unreachable!() + }; + + // Expecting 1 time-series. + assert_eq!(sum2.data_points.len(), 1); + + let datapoint2 = &sum2.data_points[0]; + assert_eq!(datapoint2.value, 5); + } else { + panic!("No MetricScope found for 'test.meter2'"); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn instrumentation_scope_identity_test() { + // Arrange + let exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .build(); + + // Act + // Meters are identical. + // Hence there should be a single metric stream output for this test. + let make_scope = |attributes| { + InstrumentationScope::builder("test.meter") + .with_version("v0.1.0") + .with_schema_url("http://example.com") + .with_attributes(attributes) + .build() + }; + + let meter1 = + meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")])); + let meter2 = + meter_provider.meter_with_scope(make_scope(vec![KeyValue::new("key", "value1")])); + + let counter1 = meter1 + .u64_counter("my_counter") + .with_unit("my_unit") + .with_description("my_description") + .build(); + + let counter2 = meter2 + .u64_counter("my_counter") + .with_unit("my_unit") + .with_description("my_description") + .build(); + + let attribute = vec![KeyValue::new("key1", "value1")]; + counter1.add(10, &attribute); + counter2.add(5, &attribute); + + meter_provider.force_flush().unwrap(); + + // Assert + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + println!("resource_metrics: {resource_metrics:?}"); + assert!( + resource_metrics[0].scope_metrics.len() == 1, + "There should be a single scope as the meters are identical" + ); + assert!( + resource_metrics[0].scope_metrics[0].metrics.len() == 1, + "There should be single metric for the scope as instruments are identical" + ); + + let scope = &resource_metrics[0].scope_metrics[0].scope; + assert_eq!(scope.name(), "test.meter"); + assert_eq!(scope.version(), Some("v0.1.0")); + assert_eq!(scope.schema_url(), Some("http://example.com")); + + // This is validating current behavior, but it is not guaranteed to be the case in the future, + // as this is a user error and SDK reserves right to change this behavior. + assert!(scope.attributes().eq(&[KeyValue::new("key", "value1")])); + + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + assert_eq!(metric.name, "my_counter"); + assert_eq!(metric.unit, "my_unit"); + assert_eq!(metric.description, "my_description"); + + let MetricData::Sum(sum) = u64::extract_metrics_data_ref(&metric.data) + .expect("Sum aggregation expected for Counter instruments by default") + else { + unreachable!() + }; + + // Expecting 1 time-series. + assert_eq!(sum.data_points.len(), 1); + + let datapoint = &sum.data_points[0]; + assert_eq!(datapoint.value, 15); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn histogram_aggregation_with_invalid_aggregation_should_proceed_as_if_view_not_exist() { + // Run this test with stdout enabled to see output. + // cargo test histogram_aggregation_with_invalid_aggregation_should_proceed_as_if_view_not_exist --features=testing -- --nocapture + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "test_histogram" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::ExplicitBucketHistogram { + boundaries: vec![0.9, 1.9, 1.2, 1.3, 1.4, 1.5], // invalid boundaries + record_min_max: false, + }) + .with_name("test_histogram_renamed") + .with_unit("test_unit_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let histogram = meter + .f64_histogram("test_histogram") + .with_unit("test_unit") + .build(); + + histogram.record(1.5, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + assert_eq!( + metric.name, "test_histogram", + "View rename should be ignored and original name retained." + ); + assert_eq!( + metric.unit, "test_unit", + "View rename of unit should be ignored and original unit retained." + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_with_lastvalue_aggregation_uses_default() { + // LastValue aggregation is only valid for Gauge instruments. + // When applied to a Counter via a view, the view is ignored and + // the default aggregation (Sum) is used per the spec: + // "proceed as if the View did not match" + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_counter" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::LastValue) + .with_name("my_counter_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let counter = meter.u64_counter("my_counter").build(); + counter.add(10, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - view is ignored, default aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // Original name is used (view rename is ignored) + assert_eq!( + metric.name, "my_counter", + "View rename should be ignored due to incompatible aggregation." + ); + // Default Sum aggregation is used + assert!( + matches!( + &metric.data, + data::AggregatedMetrics::U64(data::MetricData::Sum(_)) + ), + "Counter should use default Sum aggregation when LastValue is incompatible." + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn gauge_with_sum_aggregation_uses_default() { + // Sum aggregation is not valid for Gauge instruments. + // When applied to a Gauge via a view, the view is ignored and + // the default aggregation (LastValue/Gauge) is used per the spec: + // "proceed as if the View did not match" + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_gauge" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::Sum) + .with_name("my_gauge_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let gauge = meter.f64_gauge("my_gauge").build(); + gauge.record(42.0, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - view is ignored, default aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // Original name is used (view rename is ignored) + assert_eq!( + metric.name, "my_gauge", + "View rename should be ignored due to incompatible aggregation." + ); + // Default Gauge (LastValue) aggregation is used + assert!( + matches!( + &metric.data, + data::AggregatedMetrics::F64(data::MetricData::Gauge(_)) + ), + "Gauge should use default LastValue aggregation when Sum is incompatible." + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn updowncounter_with_lastvalue_aggregation_uses_default() { + // LastValue aggregation is only valid for Gauge instruments. + // When applied to an UpDownCounter via a view, the view is ignored and + // the default aggregation (Sum) is used per the spec: + // "proceed as if the View did not match" + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_updown_counter" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::LastValue) + .with_name("my_updown_counter_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let counter = meter.i64_up_down_counter("my_updown_counter").build(); + counter.add(-5, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - view is ignored, default aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // Original name is used (view rename is ignored) + assert_eq!( + metric.name, "my_updown_counter", + "View rename should be ignored due to incompatible aggregation." + ); + // Default Sum aggregation is used + assert!( + matches!( + &metric.data, + data::AggregatedMetrics::I64(data::MetricData::Sum(_)) + ), + "UpDownCounter should use default Sum aggregation when LastValue is incompatible." + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn histogram_with_lastvalue_aggregation_uses_default() { + // LastValue aggregation is only valid for Gauge instruments. + // When applied to a Histogram via a view, the view is ignored and + // the default aggregation (ExplicitBucketHistogram) is used per the spec: + // "proceed as if the View did not match" + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_histogram" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::LastValue) + .with_name("my_histogram_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let histogram = meter.f64_histogram("my_histogram").build(); + histogram.record(42.0, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - view is ignored, default aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // Original name is used (view rename is ignored) + assert_eq!( + metric.name, "my_histogram", + "View rename should be ignored due to incompatible aggregation." + ); + // Default Histogram aggregation is used + assert!( + matches!( + &metric.data, + data::AggregatedMetrics::F64(data::MetricData::Histogram(_)) + ), + "Histogram should use default ExplicitBucketHistogram aggregation when LastValue is incompatible." + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn observable_gauge_with_sum_aggregation_uses_default() { + // Sum aggregation is not valid for Observable Gauge instruments. + // When applied to an Observable Gauge via a view, the view is ignored and + // the default aggregation (LastValue/Gauge) is used per the spec: + // "proceed as if the View did not match" + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_observable_gauge" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::Sum) + .with_name("my_observable_gauge_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let _observable_gauge = meter + .f64_observable_gauge("my_observable_gauge") + .with_callback(|observer| { + observer.observe(42.0, &[KeyValue::new("key1", "value1")]); + }) + .build(); + meter_provider.force_flush().unwrap(); + + // Assert - view is ignored, default aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // Original name is used (view rename is ignored) + assert_eq!( + metric.name, "my_observable_gauge", + "View rename should be ignored due to incompatible aggregation." + ); + // Default Gauge (LastValue) aggregation is used + assert!( + matches!( + &metric.data, + data::AggregatedMetrics::F64(data::MetricData::Gauge(_)) + ), + "Observable Gauge should use default LastValue aggregation when Sum is incompatible." + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn observable_counter_with_lastvalue_aggregation_uses_default() { + // LastValue aggregation is only valid for Gauge instruments. + // When applied to an Observable Counter via a view, the view is ignored and + // the default aggregation (Sum) is used per the spec: + // "proceed as if the View did not match" + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_observable_counter" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::LastValue) + .with_name("my_observable_counter_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let _observable_counter = meter + .u64_observable_counter("my_observable_counter") + .with_callback(|observer| { + observer.observe(100, &[KeyValue::new("key1", "value1")]); + }) + .build(); + meter_provider.force_flush().unwrap(); + + // Assert - view is ignored, default aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // Original name is used (view rename is ignored) + assert_eq!( + metric.name, "my_observable_counter", + "View rename should be ignored due to incompatible aggregation." + ); + // Default Sum aggregation is used + assert!( + matches!( + &metric.data, + data::AggregatedMetrics::U64(data::MetricData::Sum(_)) + ), + "Observable Counter should use default Sum aggregation when LastValue is incompatible." + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn observable_updowncounter_with_lastvalue_aggregation_uses_default() { + // LastValue aggregation is only valid for Gauge instruments. + // When applied to an Observable UpDownCounter via a view, the view is ignored and + // the default aggregation (Sum) is used per the spec: + // "proceed as if the View did not match" + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_observable_updowncounter" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::LastValue) + .with_name("my_observable_updowncounter_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let _observable_updowncounter = meter + .i64_observable_up_down_counter("my_observable_updowncounter") + .with_callback(|observer| { + observer.observe(-50, &[KeyValue::new("key1", "value1")]); + }) + .build(); + meter_provider.force_flush().unwrap(); + + // Assert - view is ignored, default aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // Original name is used (view rename is ignored) + assert_eq!( + metric.name, "my_observable_updowncounter", + "View rename should be ignored due to incompatible aggregation." + ); + // Default Sum aggregation is used + assert!( + matches!( + &metric.data, + data::AggregatedMetrics::I64(data::MetricData::Sum(_)) + ), + "Observable UpDownCounter should use default Sum aggregation when LastValue is incompatible." + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn histogram_with_sum_aggregation_is_valid() { + // Sum aggregation is valid for Histogram instruments. + // When applied via a view, the aggregation should change from + // ExplicitBucketHistogram to Sum. + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_histogram" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::Sum) + .with_name("my_histogram_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let histogram = meter.f64_histogram("my_histogram").build(); + histogram.record(10.0, &[KeyValue::new("key1", "value1")]); + histogram.record(20.0, &[KeyValue::new("key1", "value1")]); + histogram.record(30.0, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - view is applied, Sum aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // View rename is applied + assert_eq!( + metric.name, "my_histogram_renamed", + "View rename should be applied for compatible aggregation." + ); + // Sum aggregation is used instead of default Histogram + let MetricData::Sum(sum) = f64::extract_metrics_data_ref(&metric.data) + .expect("Sum aggregation expected when view specifies Sum") + else { + panic!("Expected Sum aggregation for Histogram with Sum view"); + }; + assert_eq!(sum.data_points.len(), 1); + assert_eq!(sum.data_points[0].value, 60.0); // 10 + 20 + 30 + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn gauge_with_histogram_aggregation_is_valid() { + // Histogram aggregation is valid for Gauge instruments. + // When applied via a view, the aggregation should change from + // LastValue to ExplicitBucketHistogram. + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_gauge" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::ExplicitBucketHistogram { + boundaries: vec![5.0, 10.0, 25.0, 50.0], + record_min_max: true, + }) + .with_name("my_gauge_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let gauge = meter.f64_gauge("my_gauge").build(); + gauge.record(3.0, &[KeyValue::new("key1", "value1")]); + gauge.record(7.0, &[KeyValue::new("key1", "value1")]); + gauge.record(15.0, &[KeyValue::new("key1", "value1")]); + gauge.record(30.0, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - view is applied, Histogram aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // View rename is applied + assert_eq!( + metric.name, "my_gauge_renamed", + "View rename should be applied for compatible aggregation." + ); + // Histogram aggregation is used instead of default LastValue + let MetricData::Histogram(histogram) = f64::extract_metrics_data_ref(&metric.data) + .expect("Histogram aggregation expected when view specifies Histogram") + else { + panic!("Expected Histogram aggregation for Gauge with Histogram view"); + }; + assert_eq!(histogram.data_points.len(), 1); + let dp = &histogram.data_points[0]; + assert_eq!(dp.count, 4); + assert_eq!(dp.sum, 55.0); // 3 + 7 + 15 + 30 + assert_eq!(dp.min, Some(3.0)); + assert_eq!(dp.max, Some(30.0)); + // Bucket boundaries: [5.0, 10.0, 25.0, 50.0] + // Values: 3.0 (bucket 0), 7.0 (bucket 1), 15.0 (bucket 2), 30.0 (bucket 3) + assert_eq!(dp.bucket_counts, vec![1, 1, 1, 1, 0]); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_with_histogram_aggregation_is_valid() { + // Histogram aggregation is valid for Counter instruments. + // When applied via a view, the aggregation should change from + // Sum to ExplicitBucketHistogram. + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_counter" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::ExplicitBucketHistogram { + boundaries: vec![5.0, 10.0, 25.0, 50.0], + record_min_max: true, + }) + .with_name("my_counter_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let counter = meter.u64_counter("my_counter").build(); + counter.add(3, &[KeyValue::new("key1", "value1")]); + counter.add(7, &[KeyValue::new("key1", "value1")]); + counter.add(15, &[KeyValue::new("key1", "value1")]); + counter.add(30, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - view is applied, Histogram aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // View rename is applied + assert_eq!( + metric.name, "my_counter_renamed", + "View rename should be applied for compatible aggregation." + ); + // Histogram aggregation is used instead of default Sum + let MetricData::Histogram(histogram) = u64::extract_metrics_data_ref(&metric.data) + .expect("Histogram aggregation expected when view specifies Histogram") + else { + panic!("Expected Histogram aggregation for Counter with Histogram view"); + }; + assert_eq!(histogram.data_points.len(), 1); + let dp = &histogram.data_points[0]; + assert_eq!(dp.count, 4); + assert_eq!(dp.sum, 55); // 3 + 7 + 15 + 30 + assert_eq!(dp.min, Some(3)); + assert_eq!(dp.max, Some(30)); + // Bucket boundaries: [5.0, 10.0, 25.0, 50.0] + // Values: 3 (bucket 0), 7 (bucket 1), 15 (bucket 2), 30 (bucket 3) + assert_eq!(dp.bucket_counts, vec![1, 1, 1, 1, 0]); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn updowncounter_with_histogram_aggregation_is_valid() { + // Histogram aggregation is valid for UpDownCounter instruments. + // When applied via a view, the aggregation should change from + // Sum to ExplicitBucketHistogram. + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_updowncounter" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::ExplicitBucketHistogram { + boundaries: vec![0.0, 10.0, 20.0, 50.0], + record_min_max: true, + }) + .with_name("my_updowncounter_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let updowncounter = meter.i64_up_down_counter("my_updowncounter").build(); + updowncounter.add(-5, &[KeyValue::new("key1", "value1")]); + updowncounter.add(15, &[KeyValue::new("key1", "value1")]); + updowncounter.add(25, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - view is applied, Histogram aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // View rename is applied + assert_eq!( + metric.name, "my_updowncounter_renamed", + "View rename should be applied for compatible aggregation." + ); + // Histogram aggregation is used instead of default Sum + let MetricData::Histogram(histogram) = i64::extract_metrics_data_ref(&metric.data) + .expect("Histogram aggregation expected when view specifies Histogram") + else { + panic!("Expected Histogram aggregation for UpDownCounter with Histogram view"); + }; + assert_eq!(histogram.data_points.len(), 1); + let dp = &histogram.data_points[0]; + assert_eq!(dp.count, 3); + // Note: Sum is not recorded for UpDownCounter histogram per the spec + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_with_exponential_histogram_aggregation_is_valid() { + // ExponentialHistogram aggregation is valid for Counter instruments. + // When applied via a view, the aggregation should change from + // Sum to Base2ExponentialHistogram. + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_counter" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: 20, + record_min_max: true, + }) + .with_name("my_counter_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let counter = meter.u64_counter("my_counter").build(); + counter.add(5, &[KeyValue::new("key1", "value1")]); + counter.add(10, &[KeyValue::new("key1", "value1")]); + counter.add(20, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - view is applied, ExponentialHistogram aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // View rename is applied + assert_eq!( + metric.name, "my_counter_renamed", + "View rename should be applied for compatible aggregation." + ); + // ExponentialHistogram aggregation is used instead of default Sum + let MetricData::ExponentialHistogram(exp_hist) = + u64::extract_metrics_data_ref(&metric.data) + .expect("ExponentialHistogram aggregation expected when view specifies it") + else { + panic!("Expected ExponentialHistogram aggregation for Counter with ExponentialHistogram view"); + }; + assert_eq!(exp_hist.data_points.len(), 1); + let dp = &exp_hist.data_points[0]; + assert_eq!(dp.count(), 3); + assert_eq!(dp.sum(), 35); // 5 + 10 + 20 + assert_eq!(dp.min(), Some(5)); + assert_eq!(dp.max(), Some(20)); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn gauge_with_exponential_histogram_aggregation_is_valid() { + // ExponentialHistogram aggregation is valid for Gauge instruments. + // When applied via a view, the aggregation should change from + // LastValue to Base2ExponentialHistogram. + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_gauge" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: 20, + record_min_max: true, + }) + .with_name("my_gauge_renamed") + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let gauge = meter.f64_gauge("my_gauge").build(); + gauge.record(2.5, &[KeyValue::new("key1", "value1")]); + gauge.record(7.5, &[KeyValue::new("key1", "value1")]); + gauge.record(15.0, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - view is applied, ExponentialHistogram aggregation is used + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + // View rename is applied + assert_eq!( + metric.name, "my_gauge_renamed", + "View rename should be applied for compatible aggregation." + ); + // ExponentialHistogram aggregation is used instead of default LastValue + let MetricData::ExponentialHistogram(exp_hist) = + f64::extract_metrics_data_ref(&metric.data) + .expect("ExponentialHistogram aggregation expected when view specifies it") + else { + panic!("Expected ExponentialHistogram aggregation for Gauge with ExponentialHistogram view"); + }; + assert_eq!(exp_hist.data_points.len(), 1); + let dp = &exp_hist.data_points[0]; + assert_eq!(dp.count(), 3); + assert_eq!(dp.sum(), 25.0); // 2.5 + 7.5 + 15.0 + assert_eq!(dp.min(), Some(2.5)); + assert_eq!(dp.max(), Some(15.0)); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_with_drop_aggregation_is_dropped() { + // Run this test with stdout enabled to see output. + // cargo test counter_with_drop_aggregation_is_dropped --features=testing -- --nocapture + + // When a view matches an instrument and specifies Aggregation::Drop, + // the instrument should be dropped and no metrics should be exported. + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_counter_to_drop" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::Drop) + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let counter = meter.u64_counter("my_counter_to_drop").build(); + counter.add(10, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - no metrics should be exported because the view drops the instrument + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics result expected"); + assert!( + resource_metrics.is_empty() + || resource_metrics[0].scope_metrics.is_empty() + || resource_metrics[0].scope_metrics[0].metrics.is_empty(), + "No metrics should be exported when view uses Aggregation::Drop. Got: {:?}", + resource_metrics + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn histogram_with_drop_aggregation_is_dropped() { + // Run this test with stdout enabled to see output. + // cargo test histogram_with_drop_aggregation_is_dropped --features=testing -- --nocapture + + // When a view matches a histogram and specifies Aggregation::Drop, + // the instrument should be dropped and no metrics should be exported. + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_histogram_to_drop" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::Drop) + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let histogram = meter.f64_histogram("my_histogram_to_drop").build(); + histogram.record(42.0, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - no metrics should be exported because the view drops the instrument + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics result expected"); + assert!( + resource_metrics.is_empty() + || resource_metrics[0].scope_metrics.is_empty() + || resource_metrics[0].scope_metrics[0].metrics.is_empty(), + "No metrics should be exported when view uses Aggregation::Drop. Got: {:?}", + resource_metrics + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn gauge_with_drop_aggregation_is_dropped() { + // Run this test with stdout enabled to see output. + // cargo test gauge_with_drop_aggregation_is_dropped --features=testing -- --nocapture + + // When a view matches a gauge and specifies Aggregation::Drop, + // the instrument should be dropped and no metrics should be exported. + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_gauge_to_drop" { + Stream::builder() + .with_aggregation(aggregation::Aggregation::Drop) + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let gauge = meter.f64_gauge("my_gauge_to_drop").build(); + gauge.record(42.0, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - no metrics should be exported because the view drops the instrument + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics result expected"); + assert!( + resource_metrics.is_empty() + || resource_metrics[0].scope_metrics.is_empty() + || resource_metrics[0].scope_metrics[0].metrics.is_empty(), + "No metrics should be exported when view uses Aggregation::Drop. Got: {:?}", + resource_metrics + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_with_drop_aggregation_and_rename_should_still_drop() { + // Run this test with stdout enabled to see output. + // cargo test counter_with_drop_aggregation_and_rename_should_still_drop --features=testing -- --nocapture + + // When a view matches and specifies Aggregation::Drop, the instrument should be + // dropped even if the view also specifies other customizations (like name). + // The name customization is meaningless for a dropped instrument, but the Drop + // intent should still be honored. + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name == "my_counter" { + Stream::builder() + .with_name("dropped_counter") // Meaningless but shouldn't break Drop + .with_aggregation(aggregation::Aggregation::Drop) + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let counter = meter.u64_counter("my_counter").build(); + counter.add(10, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert - no metrics should be exported because the view drops the instrument + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics result expected"); + assert!( + resource_metrics.is_empty() + || resource_metrics[0].scope_metrics.is_empty() + || resource_metrics[0].scope_metrics[0].metrics.is_empty(), + "No metrics should be exported when view uses Aggregation::Drop, even with rename. Got: {:?}", + resource_metrics + ); + } + + #[cfg(feature = "spec_unstable_metrics_views")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + #[ignore = "Spatial aggregation is not yet implemented."] + async fn spatial_aggregation_when_view_drops_attributes_observable_counter() { + // cargo test metrics::tests::spatial_aggregation_when_view_drops_attributes_observable_counter --features=testing + + // Arrange + let exporter = InMemoryMetricExporter::default(); + // View drops all attributes. + let view = |i: &Instrument| { + if i.name == "my_observable_counter" { + Stream::builder() + .with_allowed_attribute_keys(vec![]) + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let _observable_counter = meter + .u64_observable_counter("my_observable_counter") + .with_callback(|observer| { + observer.observe( + 100, + &[ + KeyValue::new("statusCode", "200"), + KeyValue::new("verb", "get"), + ], + ); + + observer.observe( + 100, + &[ + KeyValue::new("statusCode", "200"), + KeyValue::new("verb", "post"), + ], + ); + + observer.observe( + 100, + &[ + KeyValue::new("statusCode", "500"), + KeyValue::new("verb", "get"), + ], + ); + }) + .build(); + + meter_provider.force_flush().unwrap(); + + // Assert + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + assert_eq!(metric.name, "my_observable_counter",); + + let MetricData::Sum(sum) = u64::extract_metrics_data_ref(&metric.data) + .expect("Sum aggregation expected for ObservableCounter instruments by default") + else { + unreachable!() + }; + + // Expecting 1 time-series only, as the view drops all attributes resulting + // in a single time-series. + // This is failing today, due to lack of support for spatial aggregation. + assert_eq!(sum.data_points.len(), 1); + + // find and validate the single datapoint + let data_point = &sum.data_points[0]; + assert_eq!(data_point.value, 300); + } + + #[cfg(feature = "spec_unstable_metrics_views")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn spatial_aggregation_when_view_drops_attributes_counter() { + // cargo test spatial_aggregation_when_view_drops_attributes_counter --features=testing + + // Arrange + let exporter = InMemoryMetricExporter::default(); + // View drops all attributes. + let view = |i: &Instrument| { + if i.name == "my_counter" { + Some( + Stream::builder() + .with_allowed_attribute_keys(vec![]) + .build() + .unwrap(), + ) + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let counter = meter.u64_counter("my_counter").build(); + + // Normally, this would generate 3 time-series, but since the view + // drops all attributes, we expect only 1 time-series. + counter.add( + 10, + [ + KeyValue::new("statusCode", "200"), + KeyValue::new("verb", "Get"), + ] + .as_ref(), + ); + + counter.add( + 10, + [ + KeyValue::new("statusCode", "500"), + KeyValue::new("verb", "Get"), + ] + .as_ref(), + ); + + counter.add( + 10, + [ + KeyValue::new("statusCode", "200"), + KeyValue::new("verb", "Post"), + ] + .as_ref(), + ); + + meter_provider.force_flush().unwrap(); + + // Assert + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert!(!resource_metrics.is_empty()); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + assert_eq!(metric.name, "my_counter",); + + let MetricData::Sum(sum) = u64::extract_metrics_data_ref(&metric.data) + .expect("Sum aggregation expected for Counter instruments by default") + else { + unreachable!() + }; + + // Expecting 1 time-series only, as the view drops all attributes resulting + // in a single time-series. + // This is failing today, due to lack of support for spatial aggregation. + assert_eq!(sum.data_points.len(), 1); + // find and validate the single datapoint + let data_point = &sum.data_points[0]; + assert_eq!(data_point.value, 30); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn no_attr_cumulative_up_down_counter() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.i64_up_down_counter("test", "my_counter", Some("my_unit")); + + counter.add(50, &[]); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = + test_context.get_aggregation::("my_counter", Some("my_unit")) + else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 1, "Expected only one data point"); + assert!(!sum.is_monotonic, "Should not produce monotonic."); + assert_eq!( + sum.temporality, + Temporality::Cumulative, + "Should produce cumulative" + ); + + let data_point = &sum.data_points[0]; + assert!(data_point.attributes.is_empty(), "Non-empty attribute set"); + assert_eq!(data_point.value, 50, "Unexpected data point value"); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn no_attr_up_down_counter_always_cumulative() { + let mut test_context = TestContext::new(Temporality::Delta); + let counter = test_context.i64_up_down_counter("test", "my_counter", Some("my_unit")); + + counter.add(50, &[]); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = + test_context.get_aggregation::("my_counter", Some("my_unit")) + else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 1, "Expected only one data point"); + assert!(!sum.is_monotonic, "Should not produce monotonic."); + assert_eq!( + sum.temporality, + Temporality::Cumulative, + "Should produce Cumulative due to UpDownCounter temporality_preference" + ); + + let data_point = &sum.data_points[0]; + assert!(data_point.attributes.is_empty(), "Non-empty attribute set"); + assert_eq!(data_point.value, 50, "Unexpected data point value"); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn no_attr_cumulative_counter_value_added_after_export() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.u64_counter("test", "my_counter", None); + + counter.add(50, &[]); + test_context.flush_metrics(); + let _ = test_context.get_aggregation::("my_counter", None); + test_context.reset_metrics(); + + counter.add(5, &[]); + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 1, "Expected only one data point"); + assert!(sum.is_monotonic, "Should produce monotonic."); + assert_eq!( + sum.temporality, + Temporality::Cumulative, + "Should produce cumulative" + ); + + let data_point = &sum.data_points[0]; + assert!(data_point.attributes.is_empty(), "Non-empty attribute set"); + assert_eq!(data_point.value, 55, "Unexpected data point value"); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn no_attr_delta_counter_value_reset_after_export() { + let mut test_context = TestContext::new(Temporality::Delta); + let counter = test_context.u64_counter("test", "my_counter", None); + + counter.add(50, &[]); + test_context.flush_metrics(); + let _ = test_context.get_aggregation::("my_counter", None); + test_context.reset_metrics(); + + counter.add(5, &[]); + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 1, "Expected only one data point"); + assert!(sum.is_monotonic, "Should produce monotonic."); + assert_eq!( + sum.temporality, + Temporality::Delta, + "Should produce cumulative" + ); + + let data_point = &sum.data_points[0]; + assert!(data_point.attributes.is_empty(), "Non-empty attribute set"); + assert_eq!(data_point.value, 5, "Unexpected data point value"); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn second_delta_export_does_not_give_no_attr_value_if_add_not_called() { + let mut test_context = TestContext::new(Temporality::Delta); + let counter = test_context.u64_counter("test", "my_counter", None); + + counter.add(50, &[]); + test_context.flush_metrics(); + let _ = test_context.get_aggregation::("my_counter", None); + test_context.reset_metrics(); + + counter.add(50, &[KeyValue::new("a", "b")]); + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + let no_attr_data_point = sum.data_points.iter().find(|x| x.attributes.is_empty()); + + assert!( + no_attr_data_point.is_none(), + "Expected no data points with no attributes" + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn delta_memory_efficiency_test() { + // Run this test with stdout enabled to see output. + // cargo test delta_memory_efficiency_test --features=testing -- --nocapture + + // Arrange + let mut test_context = TestContext::new(Temporality::Delta); + let counter = test_context.u64_counter("test", "my_counter", None); + + // Act + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + + counter.add(1, &[KeyValue::new("key1", "value2")]); + counter.add(1, &[KeyValue::new("key1", "value2")]); + counter.add(1, &[KeyValue::new("key1", "value2")]); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + // Expecting 2 time-series. + assert_eq!(sum.data_points.len(), 2); + + // find and validate key1=value1 datapoint + let data_point1 = find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 5); + + // find and validate key1=value2 datapoint + let data_point1 = find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value2") + .expect("datapoint with key1=value2 expected"); + assert_eq!(data_point1.value, 3); + + test_context.exporter.reset(); + // flush again, and validate that nothing is flushed + // as delta temporality. + test_context.flush_metrics(); + + let resource_metrics = test_context + .exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + println!("resource_metrics: {resource_metrics:?}"); + assert!(resource_metrics.is_empty(), "No metrics should be exported as no new measurements were recorded since last collect."); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_multithreaded() { + // Run this test with stdout enabled to see output. + // cargo test counter_multithreaded --features=testing -- --nocapture + + counter_multithreaded_aggregation_helper(Temporality::Delta); + counter_multithreaded_aggregation_helper(Temporality::Cumulative); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn counter_f64_multithreaded() { + // Run this test with stdout enabled to see output. + // cargo test counter_f64_multithreaded --features=testing -- --nocapture + + counter_f64_multithreaded_aggregation_helper(Temporality::Delta); + counter_f64_multithreaded_aggregation_helper(Temporality::Cumulative); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn histogram_multithreaded() { + // Run this test with stdout enabled to see output. + // cargo test histogram_multithreaded --features=testing -- --nocapture + + histogram_multithreaded_aggregation_helper(Temporality::Delta); + histogram_multithreaded_aggregation_helper(Temporality::Cumulative); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn histogram_f64_multithreaded() { + // Run this test with stdout enabled to see output. + // cargo test histogram_f64_multithreaded --features=testing -- --nocapture + + histogram_f64_multithreaded_aggregation_helper(Temporality::Delta); + histogram_f64_multithreaded_aggregation_helper(Temporality::Cumulative); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn synchronous_instruments_cumulative_with_gap_in_measurements() { + // Run this test with stdout enabled to see output. + // cargo test synchronous_instruments_cumulative_with_gap_in_measurements --features=testing -- --nocapture + + synchronous_instruments_cumulative_with_gap_in_measurements_helper("counter"); + synchronous_instruments_cumulative_with_gap_in_measurements_helper("updown_counter"); + synchronous_instruments_cumulative_with_gap_in_measurements_helper("histogram"); + synchronous_instruments_cumulative_with_gap_in_measurements_helper("gauge"); + } + + fn synchronous_instruments_cumulative_with_gap_in_measurements_helper( + instrument_name: &'static str, + ) { + let mut test_context = TestContext::new(Temporality::Cumulative); + let attributes = &[KeyValue::new("key1", "value1")]; + + // Create instrument and emit measurements + match instrument_name { + "counter" => { + let counter = test_context.meter().u64_counter("test_counter").build(); + counter.add(5, &[]); + counter.add(10, attributes); + } + "updown_counter" => { + let updown_counter = test_context + .meter() + .i64_up_down_counter("test_updowncounter") + .build(); + updown_counter.add(15, &[]); + updown_counter.add(20, attributes); + } + "histogram" => { + let histogram = test_context.meter().u64_histogram("test_histogram").build(); + histogram.record(25, &[]); + histogram.record(30, attributes); + } + "gauge" => { + let gauge = test_context.meter().u64_gauge("test_gauge").build(); + gauge.record(35, &[]); + gauge.record(40, attributes); + } + _ => panic!("Incorrect instrument kind provided"), + }; + + test_context.flush_metrics(); + + // Test the first export + assert_correct_export(&mut test_context, instrument_name); + + // Reset and export again without making any measurements + test_context.reset_metrics(); + + test_context.flush_metrics(); + + // Test that latest export has the same data as the previous one + assert_correct_export(&mut test_context, instrument_name); + + fn assert_correct_export(test_context: &mut TestContext, instrument_name: &'static str) { + match instrument_name { + "counter" => { + let MetricData::Sum(sum) = + test_context.get_aggregation::("test_counter", None) + else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 2); + let zero_attribute_datapoint = + find_sum_datapoint_with_no_attributes(&sum.data_points) + .expect("datapoint with no attributes expected"); + assert_eq!(zero_attribute_datapoint.value, 5); + let data_point1 = + find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 10); + } + "updown_counter" => { + let MetricData::Sum(sum) = + test_context.get_aggregation::("test_updowncounter", None) + else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 2); + let zero_attribute_datapoint = + find_sum_datapoint_with_no_attributes(&sum.data_points) + .expect("datapoint with no attributes expected"); + assert_eq!(zero_attribute_datapoint.value, 15); + let data_point1 = + find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 20); + } + "histogram" => { + let MetricData::Histogram(histogram_data) = + test_context.get_aggregation::("test_histogram", None) + else { + unreachable!() + }; + assert_eq!(histogram_data.data_points.len(), 2); + let zero_attribute_datapoint = + find_histogram_datapoint_with_no_attributes(&histogram_data.data_points) + .expect("datapoint with no attributes expected"); + assert_eq!(zero_attribute_datapoint.count, 1); + assert_eq!(zero_attribute_datapoint.sum, 25); + assert_eq!(zero_attribute_datapoint.min, Some(25)); + assert_eq!(zero_attribute_datapoint.max, Some(25)); + let data_point1 = find_histogram_datapoint_with_key_value( + &histogram_data.data_points, + "key1", + "value1", + ) + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.count, 1); + assert_eq!(data_point1.sum, 30); + assert_eq!(data_point1.min, Some(30)); + assert_eq!(data_point1.max, Some(30)); + } + "gauge" => { + let MetricData::Gauge(gauge_data) = + test_context.get_aggregation::("test_gauge", None) + else { + unreachable!() + }; + assert_eq!(gauge_data.data_points.len(), 2); + let zero_attribute_datapoint = + find_gauge_datapoint_with_no_attributes(&gauge_data.data_points) + .expect("datapoint with no attributes expected"); + assert_eq!(zero_attribute_datapoint.value, 35); + let data_point1 = find_gauge_datapoint_with_key_value( + &gauge_data.data_points, + "key1", + "value1", + ) + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 40); + } + _ => panic!("Incorrect instrument kind provided"), + } + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn asynchronous_instruments_cumulative_data_points_only_from_last_measurement() { + // Run this test with stdout enabled to see output. + // cargo test asynchronous_instruments_cumulative_data_points_only_from_last_measurement --features=testing -- --nocapture + + asynchronous_instruments_cumulative_data_points_only_from_last_measurement_helper("gauge"); + asynchronous_instruments_cumulative_data_points_only_from_last_measurement_helper( + "counter", + ); + asynchronous_instruments_cumulative_data_points_only_from_last_measurement_helper( + "updown_counter", + ); + } + + #[test] + fn view_test_rename() { + test_view_customization( + |i| { + if i.name == "my_counter" { + Some( + Stream::builder() + .with_name("my_counter_renamed") + .build() + .unwrap(), + ) + } else { + None + } + }, + "my_counter_renamed", + "my_unit", + "my_description", + ) + } + + #[test] + fn view_test_change_unit() { + test_view_customization( + |i| { + if i.name == "my_counter" { + Some(Stream::builder().with_unit("my_unit_new").build().unwrap()) + } else { + None + } + }, + "my_counter", + "my_unit_new", + "my_description", + ) + } + + #[test] + fn view_test_change_description() { + test_view_customization( + |i| { + if i.name == "my_counter" { + Some( + Stream::builder() + .with_description("my_description_new") + .build() + .unwrap(), + ) + } else { + None + } + }, + "my_counter", + "my_unit", + "my_description_new", + ) + } + + #[test] + fn view_test_change_name_unit() { + test_view_customization( + |i| { + if i.name == "my_counter" { + Some( + Stream::builder() + .with_name("my_counter_renamed") + .with_unit("my_unit_new") + .build() + .unwrap(), + ) + } else { + None + } + }, + "my_counter_renamed", + "my_unit_new", + "my_description", + ) + } + + #[test] + fn view_test_change_name_unit_desc() { + test_view_customization( + |i| { + if i.name == "my_counter" { + Some( + Stream::builder() + .with_name("my_counter_renamed") + .with_unit("my_unit_new") + .with_description("my_description_new") + .build() + .unwrap(), + ) + } else { + None + } + }, + "my_counter_renamed", + "my_unit_new", + "my_description_new", + ) + } + + #[test] + fn view_test_match_unit() { + test_view_customization( + |i| { + if i.unit == "my_unit" { + Some(Stream::builder().with_unit("my_unit_new").build().unwrap()) + } else { + None + } + }, + "my_counter", + "my_unit_new", + "my_description", + ) + } + + #[test] + fn view_test_match_none() { + test_view_customization( + |i| { + if i.name == "not_expected_to_match" { + Some(Stream::builder().build().unwrap()) + } else { + None + } + }, + "my_counter", + "my_unit", + "my_description", + ) + } + + #[test] + fn view_test_match_multiple() { + test_view_customization( + |i| { + if i.name == "my_counter" && i.unit == "my_unit" { + Some( + Stream::builder() + .with_name("my_counter_renamed") + .build() + .unwrap(), + ) + } else { + None + } + }, + "my_counter_renamed", + "my_unit", + "my_description", + ) + } + + /// Helper function to test view customizations + fn test_view_customization( + view_function: F, + expected_name: &str, + expected_unit: &str, + expected_description: &str, + ) where + F: Fn(&Instrument) -> Option + Send + Sync + 'static, + { + // Run this test with stdout enabled to see output. + // cargo test view_test_* --all-features -- --nocapture + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view_function) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let counter = meter + .f64_counter("my_counter") + .with_unit("my_unit") + .with_description("my_description") + .build(); + + counter.add(1.5, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert_eq!(resource_metrics.len(), 1); + assert_eq!(resource_metrics[0].scope_metrics.len(), 1); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + assert_eq!( + metric.name, expected_name, + "Expected name: {expected_name}." + ); + assert_eq!( + metric.unit, expected_unit, + "Expected unit: {expected_unit}." + ); + assert_eq!( + metric.description, expected_description, + "Expected description: {expected_description}." + ); + } + + // Following are just a basic set of advanced View tests - Views bring a lot + // of permutations and combinations, and we need + // to expand coverage for more scenarios in future. + // It is best to first split this file into multiple files + // based on scenarios (eg: regular aggregation, cardinality, views, view_advanced, etc) + // and then add more tests for each of the scenarios. + #[test] + fn test_view_single_instrument_multiple_stream() { + // Run this test with stdout enabled to see output. + // cargo test test_view_multiple_stream --all-features + + // Each of the views match the instrument name "my_counter" and create a + // new stream with a different name. In other words, View can be used to + // create multiple streams for the same instrument. + + let view1 = |i: &Instrument| { + if i.name() == "my_counter" { + Some(Stream::builder().with_name("my_counter_1").build().unwrap()) + } else { + None + } + }; + + let view2 = |i: &Instrument| { + if i.name() == "my_counter" { + Some(Stream::builder().with_name("my_counter_2").build().unwrap()) + } else { + None + } + }; + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view1) + .with_view(view2) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let counter = meter.f64_counter("my_counter").build(); + + counter.add(1.5, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert_eq!(resource_metrics.len(), 1); + assert_eq!(resource_metrics[0].scope_metrics.len(), 1); + let metrics = &resource_metrics[0].scope_metrics[0].metrics; + assert_eq!(metrics.len(), 2); + assert_eq!(metrics[0].name, "my_counter_1"); + assert_eq!(metrics[1].name, "my_counter_2"); + } + + #[test] + fn test_view_multiple_instrument_single_stream() { + // Run this test with stdout enabled to see output. + // cargo test test_view_multiple_instrument_single_stream --all-features + + // The view matches the instrument name "my_counter1" and "my_counter1" + // and create a single new stream for both. In other words, View can be used to + // "merge" multiple instruments into a single stream. + let view = |i: &Instrument| { + if i.name() == "my_counter1" || i.name() == "my_counter2" { + Some(Stream::builder().with_name("my_counter").build().unwrap()) + } else { + None + } + }; + + // Arrange + let exporter = InMemoryMetricExporter::default(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + // Act + let meter = meter_provider.meter("test"); + let counter1 = meter.f64_counter("my_counter1").build(); + let counter2 = meter.f64_counter("my_counter2").build(); + + counter1.add(1.5, &[KeyValue::new("key1", "value1")]); + counter2.add(1.5, &[KeyValue::new("key1", "value1")]); + meter_provider.force_flush().unwrap(); + + // Assert + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + assert_eq!(resource_metrics.len(), 1); + assert_eq!(resource_metrics[0].scope_metrics.len(), 1); + let metrics = &resource_metrics[0].scope_metrics[0].metrics; + assert_eq!(metrics.len(), 1); + assert_eq!(metrics[0].name, "my_counter"); + // TODO: Assert that the data points are aggregated correctly. + } + + fn asynchronous_instruments_cumulative_data_points_only_from_last_measurement_helper( + instrument_name: &'static str, + ) { + let mut test_context = TestContext::new(Temporality::Cumulative); + let attributes = Arc::new([KeyValue::new("key1", "value1")]); + + // Create instrument and emit measurements once + match instrument_name { + "counter" => { + let has_run = AtomicBool::new(false); + let _observable_counter = test_context + .meter() + .u64_observable_counter("test_counter") + .with_callback(move |observer| { + if !has_run.load(Ordering::SeqCst) { + observer.observe(5, &[]); + observer.observe(10, &*attributes.clone()); + has_run.store(true, Ordering::SeqCst); + } + }) + .build(); + } + "updown_counter" => { + let has_run = AtomicBool::new(false); + let _observable_up_down_counter = test_context + .meter() + .i64_observable_up_down_counter("test_updowncounter") + .with_callback(move |observer| { + if !has_run.load(Ordering::SeqCst) { + observer.observe(15, &[]); + observer.observe(20, &*attributes.clone()); + has_run.store(true, Ordering::SeqCst); + } + }) + .build(); + } + "gauge" => { + let has_run = AtomicBool::new(false); + let _observable_gauge = test_context + .meter() + .u64_observable_gauge("test_gauge") + .with_callback(move |observer| { + if !has_run.load(Ordering::SeqCst) { + observer.observe(25, &[]); + observer.observe(30, &*attributes.clone()); + has_run.store(true, Ordering::SeqCst); + } + }) + .build(); + } + _ => panic!("Incorrect instrument kind provided"), + }; + + test_context.flush_metrics(); + + // Test the first export + assert_correct_export(&mut test_context, instrument_name); + + // Reset and export again without making any measurements + test_context.reset_metrics(); + + test_context.flush_metrics(); + + test_context.check_no_metrics(); + + fn assert_correct_export(test_context: &mut TestContext, instrument_name: &'static str) { + match instrument_name { + "counter" => { + let MetricData::Sum(sum) = + test_context.get_aggregation::("test_counter", None) + else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 2); + assert!(sum.is_monotonic); + let zero_attribute_datapoint = + find_sum_datapoint_with_no_attributes(&sum.data_points) + .expect("datapoint with no attributes expected"); + assert_eq!(zero_attribute_datapoint.value, 5); + let data_point1 = + find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 10); + } + "updown_counter" => { + let MetricData::Sum(sum) = + test_context.get_aggregation::("test_updowncounter", None) + else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 2); + assert!(!sum.is_monotonic); + let zero_attribute_datapoint = + find_sum_datapoint_with_no_attributes(&sum.data_points) + .expect("datapoint with no attributes expected"); + assert_eq!(zero_attribute_datapoint.value, 15); + let data_point1 = + find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 20); + } + "gauge" => { + let MetricData::Gauge(gauge_data) = + test_context.get_aggregation::("test_gauge", None) + else { + unreachable!() + }; + assert_eq!(gauge_data.data_points.len(), 2); + let zero_attribute_datapoint = + find_gauge_datapoint_with_no_attributes(&gauge_data.data_points) + .expect("datapoint with no attributes expected"); + assert_eq!(zero_attribute_datapoint.value, 25); + let data_point1 = find_gauge_datapoint_with_key_value( + &gauge_data.data_points, + "key1", + "value1", + ) + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 30); + } + _ => panic!("Incorrect instrument kind provided"), + } + } + } + + fn counter_multithreaded_aggregation_helper(temporality: Temporality) { + // Arrange + let mut test_context = TestContext::new(temporality); + let counter = Arc::new(test_context.u64_counter("test", "my_counter", None)); + + for i in 0..10 { + thread::scope(|s| { + s.spawn(|| { + counter.add(1, &[]); + + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + + // Test concurrent collection by forcing half of the update threads to `force_flush` metrics and sleep for some time. + if i % 2 == 0 { + test_context.flush_metrics(); + thread::sleep(Duration::from_millis(i)); // Make each thread sleep for some time duration for better testing + } + + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + }); + }); + } + + test_context.flush_metrics(); + + // Assert + // We invoke `test_context.flush_metrics()` six times. + let sums = test_context + .get_from_multiple_aggregations::("my_counter", None, 6) + .into_iter() + .map(|data| { + if let MetricData::Sum(sum) = data { + sum + } else { + unreachable!() + } + }) + .collect::>(); + + let mut sum_zero_attributes = 0; + let mut sum_key1_value1 = 0; + sums.iter().for_each(|sum| { + assert_eq!(sum.data_points.len(), 2); // Expecting 1 time-series. + assert!(sum.is_monotonic, "Counter should produce monotonic."); + assert_eq!(sum.temporality, temporality); + + if temporality == Temporality::Delta { + sum_zero_attributes += sum.data_points[0].value; + sum_key1_value1 += sum.data_points[1].value; + } else { + sum_zero_attributes = sum.data_points[0].value; + sum_key1_value1 = sum.data_points[1].value; + }; + }); + + assert_eq!(sum_zero_attributes, 10); + assert_eq!(sum_key1_value1, 50); // Each of the 10 update threads record measurements summing up to 5. + } + + fn counter_f64_multithreaded_aggregation_helper(temporality: Temporality) { + // Arrange + let mut test_context = TestContext::new(temporality); + let counter = Arc::new(test_context.meter().f64_counter("test_counter").build()); + + for i in 0..10 { + thread::scope(|s| { + s.spawn(|| { + counter.add(1.23, &[]); + + counter.add(1.23, &[KeyValue::new("key1", "value1")]); + counter.add(1.23, &[KeyValue::new("key1", "value1")]); + counter.add(1.23, &[KeyValue::new("key1", "value1")]); + + // Test concurrent collection by forcing half of the update threads to `force_flush` metrics and sleep for some time. + if i % 2 == 0 { + test_context.flush_metrics(); + thread::sleep(Duration::from_millis(i)); // Make each thread sleep for some time duration for better testing + } + + counter.add(1.23, &[KeyValue::new("key1", "value1")]); + counter.add(1.23, &[KeyValue::new("key1", "value1")]); + }); + }); + } + + test_context.flush_metrics(); + + // Assert + // We invoke `test_context.flush_metrics()` six times. + let sums = test_context + .get_from_multiple_aggregations::("test_counter", None, 6) + .into_iter() + .map(|data| { + if let MetricData::Sum(sum) = data { + sum + } else { + unreachable!() + } + }) + .collect::>(); + + let mut sum_zero_attributes = 0.0; + let mut sum_key1_value1 = 0.0; + sums.iter().for_each(|sum| { + assert_eq!(sum.data_points.len(), 2); // Expecting 1 time-series. + assert!(sum.is_monotonic, "Counter should produce monotonic."); + assert_eq!(sum.temporality, temporality); + + if temporality == Temporality::Delta { + sum_zero_attributes += sum.data_points[0].value; + sum_key1_value1 += sum.data_points[1].value; + } else { + sum_zero_attributes = sum.data_points[0].value; + sum_key1_value1 = sum.data_points[1].value; + }; + }); + + assert!(f64::abs(12.3 - sum_zero_attributes) < 0.0001); + assert!(f64::abs(61.5 - sum_key1_value1) < 0.0001); // Each of the 10 update threads record measurements 5 times = 10 * 5 * 1.23 = 61.5 + } + + fn histogram_multithreaded_aggregation_helper(temporality: Temporality) { + // Arrange + let mut test_context = TestContext::new(temporality); + let histogram = Arc::new(test_context.meter().u64_histogram("test_histogram").build()); + + for i in 0..10 { + thread::scope(|s| { + s.spawn(|| { + histogram.record(1, &[]); + histogram.record(4, &[]); + + histogram.record(5, &[KeyValue::new("key1", "value1")]); + histogram.record(7, &[KeyValue::new("key1", "value1")]); + histogram.record(18, &[KeyValue::new("key1", "value1")]); + + // Test concurrent collection by forcing half of the update threads to `force_flush` metrics and sleep for some time. + if i % 2 == 0 { + test_context.flush_metrics(); + thread::sleep(Duration::from_millis(i)); // Make each thread sleep for some time duration for better testing + } + + histogram.record(35, &[KeyValue::new("key1", "value1")]); + histogram.record(35, &[KeyValue::new("key1", "value1")]); + }); + }); + } + + test_context.flush_metrics(); + + // Assert + // We invoke `test_context.flush_metrics()` six times. + let histograms = test_context + .get_from_multiple_aggregations::("test_histogram", None, 6) + .into_iter() + .map(|data| { + if let MetricData::Histogram(hist) = data { + hist + } else { + unreachable!() + } + }) + .collect::>(); + + let ( + mut sum_zero_attributes, + mut count_zero_attributes, + mut min_zero_attributes, + mut max_zero_attributes, + ) = (0, 0, u64::MAX, u64::MIN); + let (mut sum_key1_value1, mut count_key1_value1, mut min_key1_value1, mut max_key1_value1) = + (0, 0, u64::MAX, u64::MIN); + + let mut bucket_counts_zero_attributes = vec![0; 16]; // There are 16 buckets for the default configuration + let mut bucket_counts_key1_value1 = vec![0; 16]; + + histograms.iter().for_each(|histogram| { + assert_eq!(histogram.data_points.len(), 2); // Expecting 1 time-series. + assert_eq!(histogram.temporality, temporality); + + let data_point_zero_attributes = + find_histogram_datapoint_with_no_attributes(&histogram.data_points).unwrap(); + let data_point_key1_value1 = + find_histogram_datapoint_with_key_value(&histogram.data_points, "key1", "value1") + .unwrap(); + + if temporality == Temporality::Delta { + sum_zero_attributes += data_point_zero_attributes.sum; + sum_key1_value1 += data_point_key1_value1.sum; + + count_zero_attributes += data_point_zero_attributes.count; + count_key1_value1 += data_point_key1_value1.count; + + min_zero_attributes = + min(min_zero_attributes, data_point_zero_attributes.min.unwrap()); + min_key1_value1 = min(min_key1_value1, data_point_key1_value1.min.unwrap()); + + max_zero_attributes = + max(max_zero_attributes, data_point_zero_attributes.max.unwrap()); + max_key1_value1 = max(max_key1_value1, data_point_key1_value1.max.unwrap()); + + assert_eq!(data_point_zero_attributes.bucket_counts.len(), 16); + assert_eq!(data_point_key1_value1.bucket_counts.len(), 16); + + for (i, _) in data_point_zero_attributes.bucket_counts.iter().enumerate() { + bucket_counts_zero_attributes[i] += data_point_zero_attributes.bucket_counts[i]; + } + + for (i, _) in data_point_key1_value1.bucket_counts.iter().enumerate() { + bucket_counts_key1_value1[i] += data_point_key1_value1.bucket_counts[i]; + } + } else { + sum_zero_attributes = data_point_zero_attributes.sum; + sum_key1_value1 = data_point_key1_value1.sum; + + count_zero_attributes = data_point_zero_attributes.count; + count_key1_value1 = data_point_key1_value1.count; + + min_zero_attributes = data_point_zero_attributes.min.unwrap(); + min_key1_value1 = data_point_key1_value1.min.unwrap(); + + max_zero_attributes = data_point_zero_attributes.max.unwrap(); + max_key1_value1 = data_point_key1_value1.max.unwrap(); + + assert_eq!(data_point_zero_attributes.bucket_counts.len(), 16); + assert_eq!(data_point_key1_value1.bucket_counts.len(), 16); + + bucket_counts_zero_attributes.clone_from(&data_point_zero_attributes.bucket_counts); + bucket_counts_key1_value1.clone_from(&data_point_key1_value1.bucket_counts); + }; + }); + + // Default buckets: + // (-∞, 0], (0, 5.0], (5.0, 10.0], (10.0, 25.0], (25.0, 50.0], (50.0, 75.0], (75.0, 100.0], (100.0, 250.0], (250.0, 500.0], + // (500.0, 750.0], (750.0, 1000.0], (1000.0, 2500.0], (2500.0, 5000.0], (5000.0, 7500.0], (7500.0, 10000.0], (10000.0, +∞). + + assert_eq!(count_zero_attributes, 20); // Each of the 10 update threads record two measurements. + assert_eq!(sum_zero_attributes, 50); // Each of the 10 update threads record measurements summing up to 5. + assert_eq!(min_zero_attributes, 1); + assert_eq!(max_zero_attributes, 4); + + for (i, count) in bucket_counts_zero_attributes.iter().enumerate() { + match i { + 1 => assert_eq!(*count, 20), // For each of the 10 update threads, both the recorded values 1 and 4 fall under the bucket (0, 5]. + _ => assert_eq!(*count, 0), + } + } + + assert_eq!(count_key1_value1, 50); // Each of the 10 update threads record 5 measurements. + assert_eq!(sum_key1_value1, 1000); // Each of the 10 update threads record measurements summing up to 100 (5 + 7 + 18 + 35 + 35). + assert_eq!(min_key1_value1, 5); + assert_eq!(max_key1_value1, 35); + + for (i, count) in bucket_counts_key1_value1.iter().enumerate() { + match i { + 1 => assert_eq!(*count, 10), // For each of the 10 update threads, the recorded value 5 falls under the bucket (0, 5]. + 2 => assert_eq!(*count, 10), // For each of the 10 update threads, the recorded value 7 falls under the bucket (5, 10]. + 3 => assert_eq!(*count, 10), // For each of the 10 update threads, the recorded value 18 falls under the bucket (10, 25]. + 4 => assert_eq!(*count, 20), // For each of the 10 update threads, the recorded value 35 (recorded twice) falls under the bucket (25, 50]. + _ => assert_eq!(*count, 0), + } + } + } + + fn histogram_f64_multithreaded_aggregation_helper(temporality: Temporality) { + // Arrange + let mut test_context = TestContext::new(temporality); + let histogram = Arc::new(test_context.meter().f64_histogram("test_histogram").build()); + + for i in 0..10 { + thread::scope(|s| { + s.spawn(|| { + histogram.record(1.5, &[]); + histogram.record(4.6, &[]); + + histogram.record(5.0, &[KeyValue::new("key1", "value1")]); + histogram.record(7.3, &[KeyValue::new("key1", "value1")]); + histogram.record(18.1, &[KeyValue::new("key1", "value1")]); + + // Test concurrent collection by forcing half of the update threads to `force_flush` metrics and sleep for some time. + if i % 2 == 0 { + test_context.flush_metrics(); + thread::sleep(Duration::from_millis(i)); // Make each thread sleep for some time duration for better testing + } + + histogram.record(35.1, &[KeyValue::new("key1", "value1")]); + histogram.record(35.1, &[KeyValue::new("key1", "value1")]); + }); + }); + } + + test_context.flush_metrics(); + + // Assert + // We invoke `test_context.flush_metrics()` six times. + let histograms = test_context + .get_from_multiple_aggregations::("test_histogram", None, 6) + .into_iter() + .map(|data| { + if let MetricData::Histogram(hist) = data { + hist + } else { + unreachable!() + } + }) + .collect::>(); + + let ( + mut sum_zero_attributes, + mut count_zero_attributes, + mut min_zero_attributes, + mut max_zero_attributes, + ) = (0.0, 0, f64::MAX, f64::MIN); + let (mut sum_key1_value1, mut count_key1_value1, mut min_key1_value1, mut max_key1_value1) = + (0.0, 0, f64::MAX, f64::MIN); + + let mut bucket_counts_zero_attributes = vec![0; 16]; // There are 16 buckets for the default configuration + let mut bucket_counts_key1_value1 = vec![0; 16]; + + histograms.iter().for_each(|histogram| { + assert_eq!(histogram.data_points.len(), 2); // Expecting 1 time-series. + assert_eq!(histogram.temporality, temporality); + + let data_point_zero_attributes = + find_histogram_datapoint_with_no_attributes(&histogram.data_points).unwrap(); + let data_point_key1_value1 = + find_histogram_datapoint_with_key_value(&histogram.data_points, "key1", "value1") + .unwrap(); + + if temporality == Temporality::Delta { + sum_zero_attributes += data_point_zero_attributes.sum; + sum_key1_value1 += data_point_key1_value1.sum; + + count_zero_attributes += data_point_zero_attributes.count; + count_key1_value1 += data_point_key1_value1.count; + + min_zero_attributes = + min_zero_attributes.min(data_point_zero_attributes.min.unwrap()); + min_key1_value1 = min_key1_value1.min(data_point_key1_value1.min.unwrap()); + + max_zero_attributes = + max_zero_attributes.max(data_point_zero_attributes.max.unwrap()); + max_key1_value1 = max_key1_value1.max(data_point_key1_value1.max.unwrap()); + + assert_eq!(data_point_zero_attributes.bucket_counts.len(), 16); + assert_eq!(data_point_key1_value1.bucket_counts.len(), 16); + + for (i, _) in data_point_zero_attributes.bucket_counts.iter().enumerate() { + bucket_counts_zero_attributes[i] += data_point_zero_attributes.bucket_counts[i]; + } + + for (i, _) in data_point_key1_value1.bucket_counts.iter().enumerate() { + bucket_counts_key1_value1[i] += data_point_key1_value1.bucket_counts[i]; + } + } else { + sum_zero_attributes = data_point_zero_attributes.sum; + sum_key1_value1 = data_point_key1_value1.sum; + + count_zero_attributes = data_point_zero_attributes.count; + count_key1_value1 = data_point_key1_value1.count; + + min_zero_attributes = data_point_zero_attributes.min.unwrap(); + min_key1_value1 = data_point_key1_value1.min.unwrap(); + + max_zero_attributes = data_point_zero_attributes.max.unwrap(); + max_key1_value1 = data_point_key1_value1.max.unwrap(); + + assert_eq!(data_point_zero_attributes.bucket_counts.len(), 16); + assert_eq!(data_point_key1_value1.bucket_counts.len(), 16); + + bucket_counts_zero_attributes.clone_from(&data_point_zero_attributes.bucket_counts); + bucket_counts_key1_value1.clone_from(&data_point_key1_value1.bucket_counts); + }; + }); + + // Default buckets: + // (-∞, 0], (0, 5.0], (5.0, 10.0], (10.0, 25.0], (25.0, 50.0], (50.0, 75.0], (75.0, 100.0], (100.0, 250.0], (250.0, 500.0], + // (500.0, 750.0], (750.0, 1000.0], (1000.0, 2500.0], (2500.0, 5000.0], (5000.0, 7500.0], (7500.0, 10000.0], (10000.0, +∞). + + assert_eq!(count_zero_attributes, 20); // Each of the 10 update threads record two measurements. + assert!(f64::abs(61.0 - sum_zero_attributes) < 0.0001); // Each of the 10 update threads record measurements summing up to 6.1 (1.5 + 4.6) + assert_eq!(min_zero_attributes, 1.5); + assert_eq!(max_zero_attributes, 4.6); + + for (i, count) in bucket_counts_zero_attributes.iter().enumerate() { + match i { + 1 => assert_eq!(*count, 20), // For each of the 10 update threads, both the recorded values 1.5 and 4.6 fall under the bucket (0, 5.0]. + _ => assert_eq!(*count, 0), + } + } + + assert_eq!(count_key1_value1, 50); // Each of the 10 update threads record 5 measurements. + assert!(f64::abs(1006.0 - sum_key1_value1) < 0.0001); // Each of the 10 update threads record measurements summing up to 100.4 (5.0 + 7.3 + 18.1 + 35.1 + 35.1). + assert_eq!(min_key1_value1, 5.0); + assert_eq!(max_key1_value1, 35.1); + + for (i, count) in bucket_counts_key1_value1.iter().enumerate() { + match i { + 1 => assert_eq!(*count, 10), // For each of the 10 update threads, the recorded value 5.0 falls under the bucket (0, 5.0]. + 2 => assert_eq!(*count, 10), // For each of the 10 update threads, the recorded value 7.3 falls under the bucket (5.0, 10.0]. + 3 => assert_eq!(*count, 10), // For each of the 10 update threads, the recorded value 18.1 falls under the bucket (10.0, 25.0]. + 4 => assert_eq!(*count, 20), // For each of the 10 update threads, the recorded value 35.1 (recorded twice) falls under the bucket (25.0, 50.0]. + _ => assert_eq!(*count, 0), + } + } + } + + fn histogram_aggregation_helper(temporality: Temporality) { + // Arrange + let mut test_context = TestContext::new(temporality); + let histogram = test_context.meter().u64_histogram("my_histogram").build(); + + // Act + let mut rand = rngs::SmallRng::from_os_rng(); + let values_kv1 = (0..50) + .map(|_| rand.random_range(0..100)) + .collect::>(); + for value in values_kv1.iter() { + histogram.record(*value, &[KeyValue::new("key1", "value1")]); + } + + let values_kv2 = (0..30) + .map(|_| rand.random_range(0..100)) + .collect::>(); + for value in values_kv2.iter() { + histogram.record(*value, &[KeyValue::new("key1", "value2")]); + } + + test_context.flush_metrics(); + + // Assert + let MetricData::Histogram(histogram_data) = + test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + // Expecting 2 time-series. + assert_eq!(histogram_data.data_points.len(), 2); + if let Temporality::Cumulative = temporality { + assert_eq!( + histogram_data.temporality, + Temporality::Cumulative, + "Should produce cumulative" + ); + } else { + assert_eq!( + histogram_data.temporality, + Temporality::Delta, + "Should produce delta" + ); + } + + // find and validate key1=value2 datapoint + let data_point1 = + find_histogram_datapoint_with_key_value(&histogram_data.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.count, values_kv1.len() as u64); + assert_eq!(data_point1.sum, values_kv1.iter().sum::()); + assert_eq!(data_point1.min.unwrap(), *values_kv1.iter().min().unwrap()); + assert_eq!(data_point1.max.unwrap(), *values_kv1.iter().max().unwrap()); + + let data_point2 = + find_histogram_datapoint_with_key_value(&histogram_data.data_points, "key1", "value2") + .expect("datapoint with key1=value2 expected"); + assert_eq!(data_point2.count, values_kv2.len() as u64); + assert_eq!(data_point2.sum, values_kv2.iter().sum::()); + assert_eq!(data_point2.min.unwrap(), *values_kv2.iter().min().unwrap()); + assert_eq!(data_point2.max.unwrap(), *values_kv2.iter().max().unwrap()); + + // Reset and report more measurements + test_context.reset_metrics(); + for value in values_kv1.iter() { + histogram.record(*value, &[KeyValue::new("key1", "value1")]); + } + + for value in values_kv2.iter() { + histogram.record(*value, &[KeyValue::new("key1", "value2")]); + } + + test_context.flush_metrics(); + + let MetricData::Histogram(histogram_data) = + test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + assert_eq!(histogram_data.data_points.len(), 2); + let data_point1 = + find_histogram_datapoint_with_key_value(&histogram_data.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + if temporality == Temporality::Cumulative { + assert_eq!(data_point1.count, 2 * (values_kv1.len() as u64)); + assert_eq!(data_point1.sum, 2 * (values_kv1.iter().sum::())); + assert_eq!(data_point1.min.unwrap(), *values_kv1.iter().min().unwrap()); + assert_eq!(data_point1.max.unwrap(), *values_kv1.iter().max().unwrap()); + } else { + assert_eq!(data_point1.count, values_kv1.len() as u64); + assert_eq!(data_point1.sum, values_kv1.iter().sum::()); + assert_eq!(data_point1.min.unwrap(), *values_kv1.iter().min().unwrap()); + assert_eq!(data_point1.max.unwrap(), *values_kv1.iter().max().unwrap()); + } + + let data_point1 = + find_histogram_datapoint_with_key_value(&histogram_data.data_points, "key1", "value2") + .expect("datapoint with key1=value1 expected"); + if temporality == Temporality::Cumulative { + assert_eq!(data_point1.count, 2 * (values_kv2.len() as u64)); + assert_eq!(data_point1.sum, 2 * (values_kv2.iter().sum::())); + assert_eq!(data_point1.min.unwrap(), *values_kv2.iter().min().unwrap()); + assert_eq!(data_point1.max.unwrap(), *values_kv2.iter().max().unwrap()); + } else { + assert_eq!(data_point1.count, values_kv2.len() as u64); + assert_eq!(data_point1.sum, values_kv2.iter().sum::()); + assert_eq!(data_point1.min.unwrap(), *values_kv2.iter().min().unwrap()); + assert_eq!(data_point1.max.unwrap(), *values_kv2.iter().max().unwrap()); + } + } + + fn histogram_aggregation_with_custom_bounds_helper(temporality: Temporality) { + let mut test_context = TestContext::new(temporality); + let histogram = test_context + .meter() + .u64_histogram("test_histogram") + .with_boundaries(vec![1.0, 2.5, 5.5]) + .build(); + histogram.record(1, &[KeyValue::new("key1", "value1")]); + histogram.record(2, &[KeyValue::new("key1", "value1")]); + histogram.record(3, &[KeyValue::new("key1", "value1")]); + histogram.record(4, &[KeyValue::new("key1", "value1")]); + histogram.record(5, &[KeyValue::new("key1", "value1")]); + + test_context.flush_metrics(); + + // Assert + let MetricData::Histogram(histogram_data) = + test_context.get_aggregation::("test_histogram", None) + else { + unreachable!() + }; + // Expecting 2 time-series. + assert_eq!(histogram_data.data_points.len(), 1); + if let Temporality::Cumulative = temporality { + assert_eq!( + histogram_data.temporality, + Temporality::Cumulative, + "Should produce cumulative" + ); + } else { + assert_eq!( + histogram_data.temporality, + Temporality::Delta, + "Should produce delta" + ); + } + + // find and validate key1=value1 datapoint + let data_point = + find_histogram_datapoint_with_key_value(&histogram_data.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + + assert_eq!(data_point.count, 5); + assert_eq!(data_point.sum, 15); + + // Check the bucket counts + // -∞ to 1.0: 1 + // 1.0 to 2.5: 1 + // 2.5 to 5.5: 3 + // 5.5 to +∞: 0 + + assert_eq!(vec![1.0, 2.5, 5.5], data_point.bounds); + assert_eq!(vec![1, 1, 3, 0], data_point.bucket_counts); + } + + fn histogram_aggregation_with_empty_bounds_helper(temporality: Temporality) { + let mut test_context = TestContext::new(temporality); + let histogram = test_context + .meter() + .u64_histogram("test_histogram") + .with_boundaries(vec![]) + .build(); + histogram.record(1, &[KeyValue::new("key1", "value1")]); + histogram.record(2, &[KeyValue::new("key1", "value1")]); + histogram.record(3, &[KeyValue::new("key1", "value1")]); + histogram.record(4, &[KeyValue::new("key1", "value1")]); + histogram.record(5, &[KeyValue::new("key1", "value1")]); + + test_context.flush_metrics(); + + // Assert + let MetricData::Histogram(histogram_data) = + test_context.get_aggregation::("test_histogram", None) + else { + unreachable!() + }; + // Expecting 1 time-series. + assert_eq!(histogram_data.data_points.len(), 1); + if let Temporality::Cumulative = temporality { + assert_eq!( + histogram_data.temporality, + Temporality::Cumulative, + "Should produce cumulative" + ); + } else { + assert_eq!( + histogram_data.temporality, + Temporality::Delta, + "Should produce delta" + ); + } + + // find and validate key1=value1 datapoint + let data_point = + find_histogram_datapoint_with_key_value(&histogram_data.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + + assert_eq!(data_point.count, 5); + assert_eq!(data_point.sum, 15); + assert!(data_point.bounds.is_empty()); + assert!(data_point.bucket_counts.is_empty()); + } + + fn histogram_aggregation_with_custom_bounds_and_view_helper(temporality: Temporality) { + for specify_boundaries_in_view in [false, true] { + let view = move |_: &Instrument| { + let mut builder = Stream::builder(); + if specify_boundaries_in_view { + builder = builder.with_aggregation(Aggregation::ExplicitBucketHistogram { + boundaries: vec![1.5, 4.2, 6.7], + record_min_max: true, + }); + } + Some(builder.build().unwrap()) + }; + let mut test_context = TestContext::new_with_view(temporality, view); + let histogram = test_context + .meter() + .u64_histogram("test_histogram") + .with_boundaries(vec![1.0, 2.5, 5.5]) + .build(); + histogram.record(1, &[KeyValue::new("key1", "value1")]); + histogram.record(2, &[KeyValue::new("key1", "value1")]); + histogram.record(3, &[KeyValue::new("key1", "value1")]); + histogram.record(4, &[KeyValue::new("key1", "value1")]); + histogram.record(5, &[KeyValue::new("key1", "value1")]); + + test_context.flush_metrics(); + + let MetricData::Histogram(histogram_data) = + test_context.get_aggregation::("test_histogram", None) + else { + unreachable!() + }; + assert_eq!(histogram_data.data_points.len(), 1); + if let Temporality::Cumulative = temporality { + assert_eq!( + histogram_data.temporality, + Temporality::Cumulative, + "Should produce cumulative" + ); + } else { + assert_eq!( + histogram_data.temporality, + Temporality::Delta, + "Should produce delta" + ); + } + + // find and validate key1=value1 datapoint + let data_point = find_histogram_datapoint_with_key_value( + &histogram_data.data_points, + "key1", + "value1", + ) + .expect("datapoint with key1=value1 expected"); + + assert_eq!(data_point.count, 5); + assert_eq!(data_point.sum, 15); + + // Check the bucket counts + if specify_boundaries_in_view { + // If boundaries are specified in the view, they should take precedence + assert_eq!(vec![1.5, 4.2, 6.7], data_point.bounds); + assert_eq!(vec![1, 3, 1, 0], data_point.bucket_counts); + } else { + // If boundaries are not specified in the view, the ones from the instrument + // should be used + assert_eq!(vec![1.0, 2.5, 5.5], data_point.bounds); + assert_eq!(vec![1, 1, 3, 0], data_point.bucket_counts); + } + } + } + + fn exponential_histogram_aggregation_with_view_helper(temporality: Temporality) { + // Arrange: Create a view that converts a regular histogram to Base2ExponentialHistogram + let view = |i: &Instrument| { + if i.name == "test_histogram" { + Some( + Stream::builder() + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: 20, + record_min_max: true, + }) + .build() + .unwrap(), + ) + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(temporality, view); + let histogram = test_context.meter().f64_histogram("test_histogram").build(); + + // Act: Record some values + histogram.record(1.0, &[KeyValue::new("key1", "value1")]); + histogram.record(2.0, &[KeyValue::new("key1", "value1")]); + histogram.record(3.0, &[KeyValue::new("key1", "value1")]); + histogram.record(4.0, &[KeyValue::new("key1", "value1")]); + histogram.record(5.0, &[KeyValue::new("key1", "value1")]); + + test_context.flush_metrics(); + + // Assert: Verify we get an ExponentialHistogram instead of regular Histogram + let exponential_histogram_data = + test_context.get_aggregation::("test_histogram", None); + let MetricData::ExponentialHistogram(exp_hist) = exponential_histogram_data else { + panic!( + "Expected ExponentialHistogram aggregation, got {:?}", + exponential_histogram_data + ); + }; + + assert_eq!(exp_hist.data_points.len(), 1); + if let Temporality::Cumulative = temporality { + assert_eq!( + exp_hist.temporality, + Temporality::Cumulative, + "Should produce cumulative" + ); + } else { + assert_eq!( + exp_hist.temporality, + Temporality::Delta, + "Should produce delta" + ); + } + + // Validate the data point + let data_point = &exp_hist.data_points[0]; + assert_eq!(data_point.count(), 5); + assert_eq!(data_point.sum(), 15.0); + assert_eq!(data_point.min(), Some(1.0)); + assert_eq!(data_point.max(), Some(5.0)); + + // Validate exponential histogram specific fields + // Scale should be within valid range (-10 to 20) + let scale = data_point.scale(); + assert!( + (-10..=20).contains(&scale), + "Scale {} should be within valid range [-10, 20]", + scale + ); + + // zero_count should be 0 since we only recorded positive values > 0 + assert_eq!( + data_point.zero_count(), + 0, + "zero_count should be 0 for positive values" + ); + + // Positive bucket should have counts (we recorded positive values) + let positive_bucket = data_point.positive_bucket(); + let positive_counts: Vec = positive_bucket.counts().collect(); + let total_positive_count: u64 = positive_counts.iter().sum(); + assert_eq!( + total_positive_count, 5, + "Total count in positive buckets should equal number of recorded values" + ); + + // Negative bucket should be empty (we only recorded positive values) + let negative_bucket = data_point.negative_bucket(); + let negative_counts: Vec = negative_bucket.counts().collect(); + let total_negative_count: u64 = negative_counts.iter().sum(); + assert_eq!( + total_negative_count, 0, + "Negative bucket should be empty for positive-only values" + ); + + // Verify the attribute is present + let attrs: Vec<_> = data_point.attributes().collect(); + assert_eq!(attrs.len(), 1); + assert_eq!(attrs[0].key.as_str(), "key1"); + + // Reset and report more measurements to verify Delta vs Cumulative behavior + test_context.reset_metrics(); + histogram.record(10.0, &[KeyValue::new("key1", "value1")]); + histogram.record(20.0, &[KeyValue::new("key1", "value1")]); + histogram.record(30.0, &[KeyValue::new("key1", "value1")]); + + test_context.flush_metrics(); + + // Assert second collect + let exponential_histogram_data = + test_context.get_aggregation::("test_histogram", None); + let MetricData::ExponentialHistogram(exp_hist) = exponential_histogram_data else { + panic!( + "Expected ExponentialHistogram aggregation, got {:?}", + exponential_histogram_data + ); + }; + + assert_eq!(exp_hist.data_points.len(), 1); + let data_point = &exp_hist.data_points[0]; + + if temporality == Temporality::Cumulative { + // Cumulative: values accumulate (5 original + 3 new = 8 count, 15 + 60 = 75 sum) + assert_eq!(data_point.count(), 8); + assert_eq!(data_point.sum(), 75.0); + assert_eq!(data_point.min(), Some(1.0)); // min from first batch + assert_eq!(data_point.max(), Some(30.0)); // max from second batch + } else { + // Delta: only new values (3 count, 60 sum) + assert_eq!(data_point.count(), 3); + assert_eq!(data_point.sum(), 60.0); + assert_eq!(data_point.min(), Some(10.0)); + assert_eq!(data_point.max(), Some(30.0)); + } + + // Verify positive bucket counts match the count + let positive_bucket = data_point.positive_bucket(); + let positive_counts: Vec = positive_bucket.counts().collect(); + let total_positive_count: u64 = positive_counts.iter().sum(); + assert_eq!( + total_positive_count, + data_point.count() as u64, + "Total count in positive buckets should equal count" + ); + } + + fn gauge_aggregation_helper(temporality: Temporality) { + // Arrange + let mut test_context = TestContext::new(temporality); + let gauge = test_context.meter().i64_gauge("my_gauge").build(); + + // Act + gauge.record(1, &[KeyValue::new("key1", "value1")]); + gauge.record(2, &[KeyValue::new("key1", "value1")]); + gauge.record(1, &[KeyValue::new("key1", "value1")]); + gauge.record(3, &[KeyValue::new("key1", "value1")]); + gauge.record(4, &[KeyValue::new("key1", "value1")]); + + gauge.record(11, &[KeyValue::new("key1", "value2")]); + gauge.record(13, &[KeyValue::new("key1", "value2")]); + gauge.record(6, &[KeyValue::new("key1", "value2")]); + + test_context.flush_metrics(); + + // Assert + let MetricData::Gauge(gauge_data_point) = + test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + // Expecting 2 time-series. + assert_eq!(gauge_data_point.data_points.len(), 2); + + // find and validate key1=value2 datapoint + let data_point1 = + find_gauge_datapoint_with_key_value(&gauge_data_point.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 4); + + let data_point1 = + find_gauge_datapoint_with_key_value(&gauge_data_point.data_points, "key1", "value2") + .expect("datapoint with key1=value2 expected"); + assert_eq!(data_point1.value, 6); + + // Reset and report more measurements + test_context.reset_metrics(); + gauge.record(1, &[KeyValue::new("key1", "value1")]); + gauge.record(2, &[KeyValue::new("key1", "value1")]); + gauge.record(11, &[KeyValue::new("key1", "value1")]); + gauge.record(3, &[KeyValue::new("key1", "value1")]); + gauge.record(41, &[KeyValue::new("key1", "value1")]); + + gauge.record(34, &[KeyValue::new("key1", "value2")]); + gauge.record(12, &[KeyValue::new("key1", "value2")]); + gauge.record(54, &[KeyValue::new("key1", "value2")]); + + test_context.flush_metrics(); + + let MetricData::Gauge(gauge) = test_context.get_aggregation::("my_gauge", None) else { + unreachable!() + }; + assert_eq!(gauge.data_points.len(), 2); + let data_point1 = find_gauge_datapoint_with_key_value(&gauge.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 41); + + let data_point1 = find_gauge_datapoint_with_key_value(&gauge.data_points, "key1", "value2") + .expect("datapoint with key1=value2 expected"); + assert_eq!(data_point1.value, 54); + } + + fn observable_gauge_aggregation_helper(temporality: Temporality, use_empty_attributes: bool) { + // Arrange + let mut test_context = TestContext::new(temporality); + let _observable_gauge = test_context + .meter() + .i64_observable_gauge("test_observable_gauge") + .with_callback(move |observer| { + if use_empty_attributes { + observer.observe(1, &[]); + } + observer.observe(4, &[KeyValue::new("key1", "value1")]); + observer.observe(5, &[KeyValue::new("key2", "value2")]); + }) + .build(); + + test_context.flush_metrics(); + + // Assert + let MetricData::Gauge(gauge) = + test_context.get_aggregation::("test_observable_gauge", None) + else { + unreachable!() + }; + // Expecting 2 time-series. + let expected_time_series_count = if use_empty_attributes { 3 } else { 2 }; + assert_eq!(gauge.data_points.len(), expected_time_series_count); + + if use_empty_attributes { + // find and validate zero attribute datapoint + let zero_attribute_datapoint = + find_gauge_datapoint_with_no_attributes(&gauge.data_points) + .expect("datapoint with no attributes expected"); + assert_eq!(zero_attribute_datapoint.value, 1); + } + + // find and validate key1=value1 datapoint + let data_point1 = find_gauge_datapoint_with_key_value(&gauge.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 4); + + // find and validate key2=value2 datapoint + let data_point2 = find_gauge_datapoint_with_key_value(&gauge.data_points, "key2", "value2") + .expect("datapoint with key2=value2 expected"); + assert_eq!(data_point2.value, 5); + + // Reset and report more measurements + test_context.reset_metrics(); + + test_context.flush_metrics(); + + let MetricData::Gauge(gauge) = + test_context.get_aggregation::("test_observable_gauge", None) + else { + unreachable!() + }; + assert_eq!(gauge.data_points.len(), expected_time_series_count); + + if use_empty_attributes { + let zero_attribute_datapoint = + find_gauge_datapoint_with_no_attributes(&gauge.data_points) + .expect("datapoint with no attributes expected"); + assert_eq!(zero_attribute_datapoint.value, 1); + } + + let data_point1 = find_gauge_datapoint_with_key_value(&gauge.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 4); + + let data_point2 = find_gauge_datapoint_with_key_value(&gauge.data_points, "key2", "value2") + .expect("datapoint with key2=value2 expected"); + assert_eq!(data_point2.value, 5); + } + + fn counter_aggregation_helper(temporality: Temporality) { + // Arrange + let mut test_context = TestContext::new(temporality); + let counter = test_context.u64_counter("test", "my_counter", None); + + // Act + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + + counter.add(1, &[KeyValue::new("key1", "value2")]); + counter.add(1, &[KeyValue::new("key1", "value2")]); + counter.add(1, &[KeyValue::new("key1", "value2")]); + + test_context.flush_metrics(); + + // Assert + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + // Expecting 2 time-series. + assert_eq!(sum.data_points.len(), 2); + assert!(sum.is_monotonic, "Counter should produce monotonic."); + if let Temporality::Cumulative = temporality { + assert_eq!( + sum.temporality, + Temporality::Cumulative, + "Should produce cumulative" + ); + } else { + assert_eq!(sum.temporality, Temporality::Delta, "Should produce delta"); + } + + // find and validate key1=value2 datapoint + let data_point1 = find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 5); + + let data_point1 = find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value2") + .expect("datapoint with key1=value2 expected"); + assert_eq!(data_point1.value, 3); + + // Reset and report more measurements + test_context.reset_metrics(); + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + + counter.add(1, &[KeyValue::new("key1", "value2")]); + counter.add(1, &[KeyValue::new("key1", "value2")]); + counter.add(1, &[KeyValue::new("key1", "value2")]); + + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 2); + let data_point1 = find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + if temporality == Temporality::Cumulative { + assert_eq!(data_point1.value, 10); + } else { + assert_eq!(data_point1.value, 5); + } + + let data_point1 = find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value2") + .expect("datapoint with key1=value2 expected"); + if temporality == Temporality::Cumulative { + assert_eq!(data_point1.value, 6); + } else { + assert_eq!(data_point1.value, 3); + } + } + + fn counter_aggregation_overflow_helper(temporality: Temporality) { + // Arrange + let mut test_context = TestContext::new(temporality); + let counter = test_context.u64_counter("test", "my_counter", None); + + // Act + // Record measurements with A:0, A:1,.......A:1999, which just fits in the 2000 limit + for v in 0..2000 { + counter.add(100, &[KeyValue::new("A", v.to_string())]); + } + + // Empty attributes is specially treated and does not count towards the limit. + counter.add(3, &[]); + counter.add(3, &[]); + + // All of the below will now go into overflow. + counter.add(100, &[KeyValue::new("A", "foo")]); + counter.add(100, &[KeyValue::new("A", "another")]); + counter.add(100, &[KeyValue::new("A", "yet_another")]); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + // Expecting 2002 metric points. (2000 + 1 overflow + Empty attributes) + assert_eq!(sum.data_points.len(), 2002); + + let data_point = + find_overflow_sum_datapoint(&sum.data_points).expect("overflow point expected"); + assert_eq!(data_point.value, 300); + + // let empty_attrs_data_point = &sum.data_points[0]; + let empty_attrs_data_point = find_sum_datapoint_with_no_attributes(&sum.data_points) + .expect("Empty attributes point expected"); + assert!( + empty_attrs_data_point.attributes.is_empty(), + "Non-empty attribute set" + ); + assert_eq!( + empty_attrs_data_point.value, 6, + "Empty attributes value should be 3+3=6" + ); + + // Phase 2 - for delta temporality, collect_and_reset uses in-place eviction: + // the first collect marks entries as not-updated, and the second collect evicts + // those still-stale entries. We need an extra flush to trigger that eviction + // before adding new measurements that should fit under the cardinality limit. + test_context.reset_metrics(); + if temporality == Temporality::Delta { + test_context.flush_metrics(); + test_context.reset_metrics(); + } + // The following should be aggregated normally for Delta, + // and should go into overflow for Cumulative. + counter.add(100, &[KeyValue::new("A", "foo")]); + counter.add(100, &[KeyValue::new("A", "another")]); + counter.add(100, &[KeyValue::new("A", "yet_another")]); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + if temporality == Temporality::Delta { + assert_eq!(sum.data_points.len(), 3); + + let data_point = find_sum_datapoint_with_key_value(&sum.data_points, "A", "foo") + .expect("point expected"); + assert_eq!(data_point.value, 100); + + let data_point = find_sum_datapoint_with_key_value(&sum.data_points, "A", "another") + .expect("point expected"); + assert_eq!(data_point.value, 100); + + let data_point = + find_sum_datapoint_with_key_value(&sum.data_points, "A", "yet_another") + .expect("point expected"); + assert_eq!(data_point.value, 100); + } else { + // For cumulative, overflow should still be there, and new points should not be added. + assert_eq!(sum.data_points.len(), 2002); + let data_point = + find_overflow_sum_datapoint(&sum.data_points).expect("overflow point expected"); + assert_eq!(data_point.value, 600); + + let data_point = find_sum_datapoint_with_key_value(&sum.data_points, "A", "foo"); + assert!(data_point.is_none(), "point should not be present"); + + let data_point = find_sum_datapoint_with_key_value(&sum.data_points, "A", "another"); + assert!(data_point.is_none(), "point should not be present"); + + let data_point = + find_sum_datapoint_with_key_value(&sum.data_points, "A", "yet_another"); + assert!(data_point.is_none(), "point should not be present"); + } + } + + fn counter_aggregation_overflow_helper_custom_limit(temporality: Temporality) { + // Arrange + let cardinality_limit = 2300; + let view_change_cardinality = move |i: &Instrument| { + if i.name == "my_counter" { + Some( + Stream::builder() + .with_name("my_counter") + .with_cardinality_limit(cardinality_limit) + .build() + .unwrap(), + ) + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(temporality, view_change_cardinality); + let counter = test_context.u64_counter("test", "my_counter", None); + + // Act + // Record measurements with A:0, A:1,.......A:cardinality_limit, which just fits in the cardinality_limit + for v in 0..cardinality_limit { + counter.add(100, &[KeyValue::new("A", v.to_string())]); + } + + // Empty attributes is specially treated and does not count towards the limit. + counter.add(3, &[]); + counter.add(3, &[]); + + // All of the below will now go into overflow. + counter.add(100, &[KeyValue::new("A", "foo")]); + counter.add(100, &[KeyValue::new("A", "another")]); + counter.add(100, &[KeyValue::new("A", "yet_another")]); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + // Expecting (cardinality_limit + 1 overflow + Empty attributes) data points. + assert_eq!(sum.data_points.len(), cardinality_limit + 1 + 1); + + let data_point = + find_overflow_sum_datapoint(&sum.data_points).expect("overflow point expected"); + assert_eq!(data_point.value, 300); + + // let empty_attrs_data_point = &sum.data_points[0]; + let empty_attrs_data_point = find_sum_datapoint_with_no_attributes(&sum.data_points) + .expect("Empty attributes point expected"); + assert!( + empty_attrs_data_point.attributes.is_empty(), + "Non-empty attribute set" + ); + assert_eq!( + empty_attrs_data_point.value, 6, + "Empty attributes value should be 3+3=6" + ); + + // Phase 2 - for delta temporality, collect_and_reset uses in-place eviction: + // the first collect marks entries as not-updated, and the second collect evicts + // those still-stale entries. We need an extra flush to trigger that eviction + // before adding new measurements that should fit under the cardinality limit. + test_context.reset_metrics(); + if temporality == Temporality::Delta { + test_context.flush_metrics(); + test_context.reset_metrics(); + } + // The following should be aggregated normally for Delta, + // and should go into overflow for Cumulative. + counter.add(100, &[KeyValue::new("A", "foo")]); + counter.add(100, &[KeyValue::new("A", "another")]); + counter.add(100, &[KeyValue::new("A", "yet_another")]); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + if temporality == Temporality::Delta { + assert_eq!(sum.data_points.len(), 3); + + let data_point = find_sum_datapoint_with_key_value(&sum.data_points, "A", "foo") + .expect("point expected"); + assert_eq!(data_point.value, 100); + + let data_point = find_sum_datapoint_with_key_value(&sum.data_points, "A", "another") + .expect("point expected"); + assert_eq!(data_point.value, 100); + + let data_point = + find_sum_datapoint_with_key_value(&sum.data_points, "A", "yet_another") + .expect("point expected"); + assert_eq!(data_point.value, 100); + } else { + // For cumulative, overflow should still be there, and new points should not be added. + assert_eq!(sum.data_points.len(), cardinality_limit + 1 + 1); + let data_point = + find_overflow_sum_datapoint(&sum.data_points).expect("overflow point expected"); + assert_eq!(data_point.value, 600); + + let data_point = find_sum_datapoint_with_key_value(&sum.data_points, "A", "foo"); + assert!(data_point.is_none(), "point should not be present"); + + let data_point = find_sum_datapoint_with_key_value(&sum.data_points, "A", "another"); + assert!(data_point.is_none(), "point should not be present"); + + let data_point = + find_sum_datapoint_with_key_value(&sum.data_points, "A", "yet_another"); + assert!(data_point.is_none(), "point should not be present"); + } + } + + fn counter_aggregation_attribute_order_helper(temporality: Temporality, start_sorted: bool) { + // Arrange + let mut test_context = TestContext::new(temporality); + let counter = test_context.u64_counter("test", "my_counter", None); + + // Act + // Add the same set of attributes in different order. (they are expected + // to be treated as same attributes) + // start with sorted order + if start_sorted { + counter.add( + 1, + &[ + KeyValue::new("A", "a"), + KeyValue::new("B", "b"), + KeyValue::new("C", "c"), + ], + ); + } else { + counter.add( + 1, + &[ + KeyValue::new("A", "a"), + KeyValue::new("C", "c"), + KeyValue::new("B", "b"), + ], + ); + } + + counter.add( + 1, + &[ + KeyValue::new("A", "a"), + KeyValue::new("C", "c"), + KeyValue::new("B", "b"), + ], + ); + counter.add( + 1, + &[ + KeyValue::new("B", "b"), + KeyValue::new("A", "a"), + KeyValue::new("C", "c"), + ], + ); + counter.add( + 1, + &[ + KeyValue::new("B", "b"), + KeyValue::new("C", "c"), + KeyValue::new("A", "a"), + ], + ); + counter.add( + 1, + &[ + KeyValue::new("C", "c"), + KeyValue::new("B", "b"), + KeyValue::new("A", "a"), + ], + ); + counter.add( + 1, + &[ + KeyValue::new("C", "c"), + KeyValue::new("A", "a"), + KeyValue::new("B", "b"), + ], + ); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + // Expecting 1 time-series. + assert_eq!(sum.data_points.len(), 1); + + // validate the sole datapoint + let data_point1 = &sum.data_points[0]; + assert_eq!(data_point1.value, 6); + } + + fn updown_counter_aggregation_helper(temporality: Temporality) { + // Arrange + let mut test_context = TestContext::new(temporality); + let counter = test_context.i64_up_down_counter("test", "my_updown_counter", None); + + // Act + counter.add(10, &[KeyValue::new("key1", "value1")]); + counter.add(-1, &[KeyValue::new("key1", "value1")]); + counter.add(-5, &[KeyValue::new("key1", "value1")]); + counter.add(0, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + + counter.add(10, &[KeyValue::new("key1", "value2")]); + counter.add(0, &[KeyValue::new("key1", "value2")]); + counter.add(-3, &[KeyValue::new("key1", "value2")]); + + test_context.flush_metrics(); + + // Assert + let MetricData::Sum(sum) = test_context.get_aggregation::("my_updown_counter", None) + else { + unreachable!() + }; + // Expecting 2 time-series. + assert_eq!(sum.data_points.len(), 2); + assert!( + !sum.is_monotonic, + "UpDownCounter should produce non-monotonic." + ); + assert_eq!( + sum.temporality, + Temporality::Cumulative, + "Should produce Cumulative for UpDownCounter" + ); + + // find and validate key1=value2 datapoint + let data_point1 = find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 5); + + let data_point1 = find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value2") + .expect("datapoint with key1=value2 expected"); + assert_eq!(data_point1.value, 7); + + // Reset and report more measurements + test_context.reset_metrics(); + counter.add(10, &[KeyValue::new("key1", "value1")]); + counter.add(-1, &[KeyValue::new("key1", "value1")]); + counter.add(-5, &[KeyValue::new("key1", "value1")]); + counter.add(0, &[KeyValue::new("key1", "value1")]); + counter.add(1, &[KeyValue::new("key1", "value1")]); + + counter.add(10, &[KeyValue::new("key1", "value2")]); + counter.add(0, &[KeyValue::new("key1", "value2")]); + counter.add(-3, &[KeyValue::new("key1", "value2")]); + + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_updown_counter", None) + else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 2); + let data_point1 = find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value1") + .expect("datapoint with key1=value1 expected"); + assert_eq!(data_point1.value, 10); + + let data_point1 = find_sum_datapoint_with_key_value(&sum.data_points, "key1", "value2") + .expect("datapoint with key1=value2 expected"); + assert_eq!(data_point1.value, 14); + } + + fn find_sum_datapoint_with_key_value<'a, T>( + data_points: &'a [SumDataPoint], + key: &str, + value: &str, + ) -> Option<&'a SumDataPoint> { + data_points.iter().find(|&datapoint| { + datapoint + .attributes + .iter() + .any(|kv| kv.key.as_str() == key && kv.value.as_str() == value) + }) + } + + fn find_overflow_sum_datapoint(data_points: &[SumDataPoint]) -> Option<&SumDataPoint> { + data_points.iter().find(|&datapoint| { + datapoint.attributes.iter().any(|kv| { + kv.key.as_str() == "otel.metric.overflow" && kv.value == Value::Bool(true) + }) + }) + } + + fn find_gauge_datapoint_with_key_value<'a, T>( + data_points: &'a [GaugeDataPoint], + key: &str, + value: &str, + ) -> Option<&'a GaugeDataPoint> { + data_points.iter().find(|&datapoint| { + datapoint + .attributes + .iter() + .any(|kv| kv.key.as_str() == key && kv.value.as_str() == value) + }) + } + + fn find_sum_datapoint_with_no_attributes( + data_points: &[SumDataPoint], + ) -> Option<&SumDataPoint> { + data_points + .iter() + .find(|&datapoint| datapoint.attributes.is_empty()) + } + + fn find_gauge_datapoint_with_no_attributes( + data_points: &[GaugeDataPoint], + ) -> Option<&GaugeDataPoint> { + data_points + .iter() + .find(|&datapoint| datapoint.attributes.is_empty()) + } + + fn find_histogram_datapoint_with_key_value<'a, T>( + data_points: &'a [HistogramDataPoint], + key: &str, + value: &str, + ) -> Option<&'a HistogramDataPoint> { + data_points.iter().find(|&datapoint| { + datapoint + .attributes + .iter() + .any(|kv| kv.key.as_str() == key && kv.value.as_str() == value) + }) + } + + fn find_histogram_datapoint_with_no_attributes( + data_points: &[HistogramDataPoint], + ) -> Option<&HistogramDataPoint> { + data_points + .iter() + .find(|&datapoint| datapoint.attributes.is_empty()) + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn find_overflow_histogram_datapoint( + data_points: &[HistogramDataPoint], + ) -> Option<&HistogramDataPoint> { + data_points.iter().find(|&datapoint| { + datapoint.attributes.iter().any(|kv| { + kv.key.as_str() == "otel.metric.overflow" && kv.value == Value::Bool(true) + }) + }) + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn find_overflow_exponential_histogram_datapoint( + data_points: &[ExponentialHistogramDataPoint], + ) -> Option<&ExponentialHistogramDataPoint> { + data_points.iter().find(|&datapoint| { + datapoint.attributes.iter().any(|kv| { + kv.key.as_str() == "otel.metric.overflow" && kv.value == Value::Bool(true) + }) + }) + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn find_overflow_gauge_datapoint( + data_points: &[GaugeDataPoint], + ) -> Option<&GaugeDataPoint> { + data_points.iter().find(|&datapoint| { + datapoint.attributes.iter().any(|kv| { + kv.key.as_str() == "otel.metric.overflow" && kv.value == Value::Bool(true) + }) + }) + } + + fn find_scope_metric<'a>( + metrics: &'a [ScopeMetrics], + name: &'a str, + ) -> Option<&'a ScopeMetrics> { + metrics + .iter() + .find(|&scope_metric| scope_metric.scope.name() == name) + } + + struct TestContext { + exporter: InMemoryMetricExporter, + meter_provider: SdkMeterProvider, + + // Saving this on the test context for lifetime simplicity + resource_metrics: Vec, + } + + impl TestContext { + fn new(temporality: Temporality) -> Self { + let exporter = InMemoryMetricExporterBuilder::new().with_temporality(temporality); + let exporter = exporter.build(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .build(); + + TestContext { + exporter, + meter_provider, + resource_metrics: vec![], + } + } + + fn new_with_view(temporality: Temporality, view: T) -> Self + where + T: Fn(&Instrument) -> Option + Send + Sync + 'static, + { + let exporter = InMemoryMetricExporterBuilder::new().with_temporality(temporality); + let exporter = exporter.build(); + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + + TestContext { + exporter, + meter_provider, + resource_metrics: vec![], + } + } + + fn u64_counter( + &self, + meter_name: &'static str, + counter_name: &'static str, + unit: Option<&'static str>, + ) -> Counter { + let meter = self.meter_provider.meter(meter_name); + let mut counter_builder = meter.u64_counter(counter_name); + if let Some(unit_name) = unit { + counter_builder = counter_builder.with_unit(unit_name); + } + counter_builder.build() + } + + fn i64_up_down_counter( + &self, + meter_name: &'static str, + counter_name: &'static str, + unit: Option<&'static str>, + ) -> UpDownCounter { + let meter = self.meter_provider.meter(meter_name); + let mut updown_counter_builder = meter.i64_up_down_counter(counter_name); + if let Some(unit_name) = unit { + updown_counter_builder = updown_counter_builder.with_unit(unit_name); + } + updown_counter_builder.build() + } + + fn meter(&self) -> Meter { + self.meter_provider.meter("test") + } + + fn flush_metrics(&self) { + self.meter_provider.force_flush().unwrap(); + } + + fn reset_metrics(&self) { + self.exporter.reset(); + } + + fn check_no_metrics(&self) { + let resource_metrics = self + .exporter + .get_finished_metrics() + .expect("metrics expected to be exported"); // TODO: Need to fix InMemoryMetricExporter to return None. + + assert!(resource_metrics.is_empty(), "no metrics should be exported"); + } + + fn get_aggregation( + &mut self, + counter_name: &str, + unit_name: Option<&str>, + ) -> &MetricData { + self.resource_metrics = self + .exporter + .get_finished_metrics() + .expect("metrics expected to be exported"); + + assert!( + !self.resource_metrics.is_empty(), + "no metrics were exported" + ); + + assert!( + self.resource_metrics.len() == 1, + "Expected single resource metrics." + ); + let resource_metric = self + .resource_metrics + .first() + .expect("This should contain exactly one resource metric, as validated above."); + + assert!( + !resource_metric.scope_metrics.is_empty(), + "No scope metrics in latest export" + ); + assert!(!resource_metric.scope_metrics[0].metrics.is_empty()); + + let metric = &resource_metric.scope_metrics[0].metrics[0]; + assert_eq!(metric.name, counter_name); + if let Some(expected_unit) = unit_name { + assert_eq!(metric.unit, expected_unit); + } + + T::extract_metrics_data_ref(&metric.data) + .expect("Failed to cast aggregation to expected type") + } + + fn get_from_multiple_aggregations( + &mut self, + counter_name: &str, + unit_name: Option<&str>, + invocation_count: usize, + ) -> Vec<&MetricData> { + self.resource_metrics = self + .exporter + .get_finished_metrics() + .expect("metrics expected to be exported"); + + assert!( + !self.resource_metrics.is_empty(), + "no metrics were exported" + ); + + assert_eq!( + self.resource_metrics.len(), + invocation_count, + "Expected collect to be called {invocation_count} times" + ); + + let result = self + .resource_metrics + .iter() + .map(|resource_metric| { + assert!( + !resource_metric.scope_metrics.is_empty(), + "An export with no scope metrics occurred" + ); + + assert!(!resource_metric.scope_metrics[0].metrics.is_empty()); + + let metric = &resource_metric.scope_metrics[0].metrics[0]; + assert_eq!(metric.name, counter_name); + + if let Some(expected_unit) = unit_name { + assert_eq!(metric.unit, expected_unit); + } + + let aggregation = T::extract_metrics_data_ref(&metric.data) + .expect("Failed to cast aggregation to expected type"); + aggregation + }) + .collect::>(); + + result + } + } + + #[test] + fn parse_valid_temporality_values() { + assert_eq!( + "cumulative".parse::(), + Ok(Temporality::Cumulative) + ); + assert_eq!("delta".parse::(), Ok(Temporality::Delta)); + assert_eq!( + "lowmemory".parse::(), + Ok(Temporality::LowMemory) + ); + } + + #[test] + fn parse_temporality_case_insensitive() { + assert_eq!( + "Cumulative".parse::(), + Ok(Temporality::Cumulative) + ); + assert_eq!("DELTA".parse::(), Ok(Temporality::Delta)); + assert_eq!( + "LowMemory".parse::(), + Ok(Temporality::LowMemory) + ); + assert_eq!( + "LOWMEMORY".parse::(), + Ok(Temporality::LowMemory) + ); + } + + #[test] + fn parse_invalid_temporality_returns_err() { + assert!("unknown".parse::().is_err()); + assert!("".parse::().is_err()); + assert!("cumulativ".parse::().is_err()); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_cumulative() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.u64_counter("test", "my_counter", None); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = counter.bind(&attrs); + + bound.add(10); + bound.add(20); + bound.add(30); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 1, "Expected one data point"); + assert!(sum.is_monotonic); + assert_eq!(sum.temporality, Temporality::Cumulative); + + let data_point = &sum.data_points[0]; + assert_eq!(data_point.value, 60); + assert_eq!( + data_point.attributes, + vec![KeyValue::new("key1", "bound_value")] + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_delta() { + let mut test_context = TestContext::new(Temporality::Delta); + let counter = test_context.u64_counter("test", "my_counter", None); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = counter.bind(&attrs); + + bound.add(50); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + assert_eq!(sum.temporality, Temporality::Delta); + assert_eq!(sum.data_points.len(), 1); + assert_eq!(sum.data_points[0].value, 50); + + // After delta collect, add more and collect again + test_context.reset_metrics(); + bound.add(25); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 1); + assert_eq!( + sum.data_points[0].value, 25, + "Delta should reset between collections" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_histogram_cumulative() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let histogram = test_context + .meter() + .f64_histogram("my_histogram") + .with_boundaries(vec![5.0, 10.0, 25.0, 50.0]) + .build(); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = histogram.bind(&attrs); + + bound.record(1.0); + bound.record(7.5); + bound.record(15.0); + bound.record(30.0); + test_context.flush_metrics(); + + let MetricData::Histogram(histogram_data) = + test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + + assert_eq!(histogram_data.data_points.len(), 1); + assert_eq!(histogram_data.temporality, Temporality::Cumulative); + + let dp = &histogram_data.data_points[0]; + assert_eq!(dp.count, 4); + assert_eq!(dp.sum, 53.5); + assert_eq!(dp.min.unwrap(), 1.0); + assert_eq!(dp.max.unwrap(), 30.0); + assert_eq!(dp.attributes, vec![KeyValue::new("key1", "bound_value")]); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_matches_unbound() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.u64_counter("test", "my_counter", None); + let attrs = vec![KeyValue::new("key1", "shared")]; + let bound = counter.bind(&attrs); + + // Mix bound and unbound additions to the same attribute set + counter.add(10, &attrs); + bound.add(20); + counter.add(30, &attrs); + bound.add(40); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + assert_eq!( + sum.data_points.len(), + 1, + "Bound and unbound should share the same data point" + ); + assert_eq!(sum.data_points[0].value, 100); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_delta_no_update_no_export() { + let mut test_context = TestContext::new(Temporality::Delta); + let counter = test_context.u64_counter("test", "my_counter", None); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = counter.bind(&attrs); + + // Cycle 1: add and collect + bound.add(10); + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 1); + assert_eq!(sum.data_points[0].value, 10); + + // Cycle 2: no add, collect — should export nothing + test_context.reset_metrics(); + test_context.flush_metrics(); + let resource_metrics = test_context + .exporter + .get_finished_metrics() + .expect("metrics export should succeed"); + assert!( + resource_metrics.is_empty(), + "Bound handle with no updates should not export" + ); + + // Cycle 3: add again — handle is still alive, produces fresh delta + test_context.reset_metrics(); + bound.add(5); + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 1); + assert_eq!( + sum.data_points[0].value, 5, + "Bound handle should produce fresh delta after quiet cycle" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_at_overflow_attributes_to_overflow_bucket() { + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_counter" { + Stream::builder() + .with_name("my_counter") + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Delta, view); + let counter = test_context.u64_counter("test", "my_counter", None); + + // Fill to cardinality limit with unbound calls + for v in 0..cardinality_limit { + counter.add(1, &[KeyValue::new("A", v.to_string())]); + } + + // bind() at overflow — handle binds directly to the overflow tracker + let overflow_attrs = vec![KeyValue::new("A", "overflow_bind")]; + let bound = counter.bind(&overflow_attrs); + bound.add(42); + + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + // Expect: cardinality_limit unique + 1 overflow = cardinality_limit + 1 + assert_eq!( + sum.data_points.len(), + cardinality_limit + 1, + "Expected {} unique + 1 overflow data points", + cardinality_limit + ); + + // The bound handle's value should appear in the overflow bucket + let overflow_dp = + find_overflow_sum_datapoint(&sum.data_points).expect("overflow point expected"); + assert_eq!( + overflow_dp.value, 42, + "Bound-at-overflow data should go to overflow bucket" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_overflow_recovery_after_delta_eviction() { + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_counter" { + Stream::builder() + .with_name("my_counter") + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Delta, view); + let counter = test_context.u64_counter("test", "my_counter", None); + + // Fill to cardinality limit with unbound calls (these are one-shot, not bound) + for v in 0..cardinality_limit { + counter.add(1, &[KeyValue::new("A", v.to_string())]); + } + + // Collect cycle 1: exports the 3 unique entries, then evicts them (no new updates) + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), cardinality_limit); + + // Cycle 2: no unbound adds, so the stale entries get evicted. + // Space is now open. A new bind() should get a dedicated tracker. + test_context.reset_metrics(); + test_context.flush_metrics(); // triggers eviction of stale entries + + let new_attrs = vec![KeyValue::new("A", "recovered")]; + let bound = counter.bind(&new_attrs); + bound.add(99); + + test_context.reset_metrics(); + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + // The bound handle should have a dedicated tracker, NOT overflow + assert_eq!( + sum.data_points.len(), + 1, + "Only the bound entry should be exported" + ); + let dp = find_sum_datapoint_with_key_value(&sum.data_points, "A", "recovered") + .expect("should find dedicated data point for recovered attrs"); + assert_eq!( + dp.value, 99, + "Bound handle after recovery should have dedicated tracker" + ); + assert!( + find_overflow_sum_datapoint(&sum.data_points).is_none(), + "Should not have overflow — bind() after eviction should get a dedicated tracker" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_multiple_overflow_handles_share_overflow_bucket() { + let cardinality_limit = 2; + let view = move |i: &Instrument| { + if i.name() == "my_counter" { + Stream::builder() + .with_name("my_counter") + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Delta, view); + let counter = test_context.u64_counter("test", "my_counter", None); + + // Fill to limit + counter.add(1, &[KeyValue::new("A", "0")]); + counter.add(1, &[KeyValue::new("A", "1")]); + + // Bind two distinct attribute sets at overflow + let bound_a = counter.bind(&[KeyValue::new("A", "overflow_a")]); + let bound_b = counter.bind(&[KeyValue::new("A", "overflow_b")]); + + bound_a.add(10); + bound_b.add(20); + bound_a.add(5); + + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + let overflow_dp = + find_overflow_sum_datapoint(&sum.data_points).expect("overflow point expected"); + assert_eq!( + overflow_dp.value, 35, + "All overflow-bound measurements should accumulate in overflow bucket" + ); + + // Cycle 2: bound handles still work after delta collect + test_context.reset_metrics(); + bound_a.add(7); + bound_b.add(3); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + let overflow_dp = + find_overflow_sum_datapoint(&sum.data_points).expect("overflow point expected"); + assert_eq!( + overflow_dp.value, 10, + "Overflow-bound handles should continue working across delta cycles" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_overflow_persists_across_eviction_cycles() { + // Once a bind() lands in overflow, the handle's writes must continue + // landing in overflow for the lifetime of the handle — even after + // delta eviction frees space. This is the predictability guarantee: + // a user inspecting their data should see the bound handle's + // attribution as a single, stable bucket. The recovery story is + // explicit (drop and re-bind), not implicit (silent self-healing). + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_counter" { + Stream::builder() + .with_name("my_counter") + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Delta, view); + let counter = test_context.u64_counter("test", "my_counter", None); + + // Cycle 1: fill cardinality with unbound calls, then bind at overflow. + for v in 0..cardinality_limit { + counter.add(1, &[KeyValue::new("A", v.to_string())]); + } + let stuck_attrs = vec![KeyValue::new("A", "stuck_in_overflow")]; + let bound = counter.bind(&stuck_attrs); + bound.add(10); + + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + let overflow_dp = find_overflow_sum_datapoint(&sum.data_points) + .expect("cycle 1: bound write at overflow should land in overflow bucket"); + assert_eq!(overflow_dp.value, 10); + + // Cycle 2: no calls. The 3 unbound entries become stale and are evicted, + // freeing all of the cardinality budget. + test_context.reset_metrics(); + test_context.flush_metrics(); + + // Cycle 3: the SAME bound handle is used again. Even though space is + // available, its writes must still land in overflow — the handle is + // permanently bound to overflow, not silently re-resolved. + test_context.reset_metrics(); + bound.add(99); + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + let overflow_dp = find_overflow_sum_datapoint(&sum.data_points) + .expect("cycle 3: bound write must still land in overflow even after space frees up"); + assert_eq!( + overflow_dp.value, 99, + "Bound-at-overflow handle must keep writing to overflow even after delta eviction" + ); + assert!( + find_sum_datapoint_with_key_value(&sum.data_points, "A", "stuck_in_overflow").is_none(), + "Bound-at-overflow handle must not silently self-heal to a dedicated tracker" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_histogram_delta() { + let mut test_context = TestContext::new(Temporality::Delta); + let histogram = test_context + .meter() + .f64_histogram("my_histogram") + .with_boundaries(vec![5.0, 10.0, 25.0, 50.0]) + .build(); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = histogram.bind(&attrs); + + // Cycle 1: record and collect + bound.record(3.0); + bound.record(12.0); + test_context.flush_metrics(); + + let MetricData::Histogram(hist) = test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + assert_eq!(hist.temporality, Temporality::Delta); + assert_eq!(hist.data_points.len(), 1); + assert_eq!(hist.data_points[0].count, 2); + assert_eq!(hist.data_points[0].sum, 15.0); + + // Cycle 2: delta resets, new values + test_context.reset_metrics(); + bound.record(40.0); + test_context.flush_metrics(); + + let MetricData::Histogram(hist) = test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + assert_eq!(hist.data_points.len(), 1); + assert_eq!( + hist.data_points[0].count, 1, + "Delta should reset count between collections" + ); + assert_eq!( + hist.data_points[0].sum, 40.0, + "Delta should reset sum between collections" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_histogram_matches_unbound() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let histogram = test_context + .meter() + .f64_histogram("my_histogram") + .with_boundaries(vec![10.0, 50.0]) + .build(); + let attrs = vec![KeyValue::new("key1", "shared")]; + let bound = histogram.bind(&attrs); + + // Mix bound and unbound recordings to the same attribute set + histogram.record(5.0, &attrs); + bound.record(15.0); + histogram.record(25.0, &attrs); + bound.record(35.0); + test_context.flush_metrics(); + + let MetricData::Histogram(hist) = test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + + assert_eq!( + hist.data_points.len(), + 1, + "Bound and unbound should share the same data point" + ); + assert_eq!(hist.data_points[0].count, 4); + assert_eq!(hist.data_points[0].sum, 80.0); + assert_eq!(hist.data_points[0].min.unwrap(), 5.0); + assert_eq!(hist.data_points[0].max.unwrap(), 35.0); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_histogram_delta_no_update_no_export() { + let mut test_context = TestContext::new(Temporality::Delta); + let histogram = test_context + .meter() + .f64_histogram("my_histogram") + .with_boundaries(vec![10.0]) + .build(); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = histogram.bind(&attrs); + + // Cycle 1: record and collect + bound.record(5.0); + test_context.flush_metrics(); + let MetricData::Histogram(hist) = test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + assert_eq!(hist.data_points.len(), 1); + assert_eq!(hist.data_points[0].count, 1); + + // Cycle 2: no recordings — should export nothing + test_context.reset_metrics(); + test_context.flush_metrics(); + let resource_metrics = test_context + .exporter + .get_finished_metrics() + .expect("metrics export should succeed"); + assert!( + resource_metrics.is_empty(), + "Bound histogram with no updates should not export" + ); + + // Cycle 3: record again — handle still alive + test_context.reset_metrics(); + bound.record(20.0); + test_context.flush_metrics(); + let MetricData::Histogram(hist) = test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + assert_eq!(hist.data_points.len(), 1); + assert_eq!( + hist.data_points[0].count, 1, + "Fresh delta after quiet cycle" + ); + assert_eq!(hist.data_points[0].sum, 20.0); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_histogram_at_overflow_attributes_to_overflow_bucket() { + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_histogram" { + Stream::builder() + .with_name("my_histogram") + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Delta, view); + let histogram = test_context + .meter() + .f64_histogram("my_histogram") + .with_boundaries(vec![10.0, 50.0]) + .build(); + + // Fill to cardinality limit with unbound calls + for v in 0..cardinality_limit { + histogram.record(1.0, &[KeyValue::new("A", v.to_string())]); + } + + // bind() at overflow — handle binds directly to the overflow tracker + let overflow_attrs = vec![KeyValue::new("A", "overflow_bind")]; + let bound = histogram.bind(&overflow_attrs); + bound.record(42.0); + + test_context.flush_metrics(); + let MetricData::Histogram(hist) = test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + + assert_eq!( + hist.data_points.len(), + cardinality_limit + 1, + "Expected {} unique + 1 overflow data points", + cardinality_limit + ); + + let overflow_dp = + find_overflow_histogram_datapoint(&hist.data_points).expect("overflow point expected"); + assert_eq!( + overflow_dp.sum, 42.0, + "Bound-at-overflow data should go to overflow bucket" + ); + assert_eq!(overflow_dp.count, 1); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_histogram_overflow_persists_across_eviction_cycles() { + // Mirror of bound_counter_overflow_persists_across_eviction_cycles for + // histograms: a bound-at-overflow handle must keep landing in overflow + // even after delta eviction frees space, for the lifetime of the handle. + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_histogram" { + Stream::builder() + .with_name("my_histogram") + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Delta, view); + let histogram = test_context + .meter() + .f64_histogram("my_histogram") + .with_boundaries(vec![10.0, 50.0]) + .build(); + + // Cycle 1: fill cardinality with unbound calls, then bind at overflow. + for v in 0..cardinality_limit { + histogram.record(1.0, &[KeyValue::new("A", v.to_string())]); + } + let stuck_attrs = vec![KeyValue::new("A", "stuck_in_overflow")]; + let bound = histogram.bind(&stuck_attrs); + bound.record(15.0); + + test_context.flush_metrics(); + let MetricData::Histogram(hist) = test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + let overflow_dp = find_overflow_histogram_datapoint(&hist.data_points) + .expect("cycle 1: bound write at overflow should land in overflow bucket"); + assert_eq!(overflow_dp.sum, 15.0); + + // Cycle 2: no calls. Stale unbound entries get evicted, freeing space. + test_context.reset_metrics(); + test_context.flush_metrics(); + + // Cycle 3: same bound handle. Even though space is free, writes must + // still land in overflow. + test_context.reset_metrics(); + bound.record(99.0); + test_context.flush_metrics(); + let MetricData::Histogram(hist) = test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + let overflow_dp = find_overflow_histogram_datapoint(&hist.data_points) + .expect("cycle 3: bound write must still land in overflow even after space frees up"); + assert_eq!( + overflow_dp.sum, 99.0, + "Bound-at-overflow histogram must keep writing to overflow even after delta eviction" + ); + assert_eq!(overflow_dp.count, 1); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_exponential_histogram_delta() { + // Histogram configured with Base2ExponentialHistogram aggregation goes + // through ExpoHistogram internally. Verify bind() returns a handle + // whose direct writes accumulate correctly. + let view = |i: &Instrument| { + if i.name() == "my_histogram" { + Stream::builder() + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: 20, + record_min_max: true, + }) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Delta, view); + let histogram = test_context.meter().f64_histogram("my_histogram").build(); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = histogram.bind(&attrs); + + bound.record(2.0); + bound.record(4.0); + bound.record(8.0); + // NaN/inf must be filtered just like the unbound path. + bound.record(f64::NAN); + bound.record(f64::INFINITY); + + test_context.flush_metrics(); + let MetricData::ExponentialHistogram(hist) = + test_context.get_aggregation::("my_histogram", None) + else { + panic!("expected ExponentialHistogram aggregation"); + }; + assert_eq!(hist.data_points.len(), 1); + let dp = &hist.data_points[0]; + assert_eq!(dp.count, 3, "NaN and infinity should be dropped"); + assert_eq!(dp.sum, 14.0); + assert_eq!(dp.min, Some(2.0)); + assert_eq!(dp.max, Some(8.0)); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_exponential_histogram_at_overflow_attributes_to_overflow_bucket() { + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_histogram" { + Stream::builder() + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: 20, + record_min_max: true, + }) + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Delta, view); + let histogram = test_context.meter().f64_histogram("my_histogram").build(); + + for v in 0..cardinality_limit { + histogram.record(1.0, &[KeyValue::new("A", v.to_string())]); + } + + // bind() at overflow — handle binds directly to the overflow tracker + let overflow_attrs = vec![KeyValue::new("A", "overflow_bind")]; + let bound = histogram.bind(&overflow_attrs); + bound.record(42.0); + + test_context.flush_metrics(); + let MetricData::ExponentialHistogram(hist) = + test_context.get_aggregation::("my_histogram", None) + else { + panic!("expected ExponentialHistogram aggregation"); + }; + let overflow_dp = find_overflow_exponential_histogram_datapoint(&hist.data_points) + .expect("overflow point expected"); + assert_eq!( + overflow_dp.sum, 42.0, + "Bound-at-overflow data should go to overflow bucket" + ); + assert_eq!(overflow_dp.count, 1); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_exponential_histogram_overflow_persists_across_eviction_cycles() { + // Same predictability invariant the counter/histogram tests assert, + // verified for the exponential histogram aggregator path. + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_histogram" { + Stream::builder() + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: 20, + record_min_max: true, + }) + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Delta, view); + let histogram = test_context.meter().f64_histogram("my_histogram").build(); + + for v in 0..cardinality_limit { + histogram.record(1.0, &[KeyValue::new("A", v.to_string())]); + } + let stuck_attrs = vec![KeyValue::new("A", "stuck_in_overflow")]; + let bound = histogram.bind(&stuck_attrs); + bound.record(15.0); + + test_context.flush_metrics(); + let MetricData::ExponentialHistogram(hist) = + test_context.get_aggregation::("my_histogram", None) + else { + panic!("expected ExponentialHistogram aggregation"); + }; + let overflow_dp = find_overflow_exponential_histogram_datapoint(&hist.data_points) + .expect("cycle 1: bound write at overflow should land in overflow bucket"); + assert_eq!(overflow_dp.sum, 15.0); + + // Cycle 2: evict stale entries, freeing space. + test_context.reset_metrics(); + test_context.flush_metrics(); + + // Cycle 3: bound handle keeps writing to overflow. + test_context.reset_metrics(); + bound.record(99.0); + test_context.flush_metrics(); + let MetricData::ExponentialHistogram(hist) = + test_context.get_aggregation::("my_histogram", None) + else { + panic!("expected ExponentialHistogram aggregation"); + }; + let overflow_dp = find_overflow_exponential_histogram_datapoint(&hist.data_points) + .expect("cycle 3: bound write must still land in overflow even after space frees up"); + assert_eq!( + overflow_dp.sum, 99.0, + "Bound-at-overflow ExpoHistogram must keep writing to overflow even after delta eviction" + ); + assert_eq!(overflow_dp.count, 1); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_drop_enables_eviction() { + let mut test_context = TestContext::new(Temporality::Delta); + let counter = test_context.u64_counter("test", "my_counter", None); + let attrs = vec![KeyValue::new("key1", "ephemeral")]; + + { + let bound = counter.bind(&attrs); + bound.add(100); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) + else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 1); + assert_eq!(sum.data_points[0].value, 100); + // bound drops here + } + + // Cycle 2: no updates, bound handle dropped — entry becomes stale and evictable + test_context.reset_metrics(); + test_context.flush_metrics(); + + // Cycle 3: the stale entry should have been evicted, so a new unbound add + // should be the only data point + test_context.reset_metrics(); + counter.add(1, &[KeyValue::new("key1", "new_entry")]); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + // Only the new entry should be present — the old "ephemeral" entry was evicted + assert_eq!(sum.data_points.len(), 1); + let dp = find_sum_datapoint_with_key_value(&sum.data_points, "key1", "new_entry") + .expect("new_entry should be present"); + assert_eq!(dp.value, 1); + assert!( + find_sum_datapoint_with_key_value(&sum.data_points, "key1", "ephemeral").is_none(), + "Dropped bound entry should have been evicted" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_multiple_handles_same_attrs() { + let mut test_context = TestContext::new(Temporality::Delta); + let counter = test_context.u64_counter("test", "my_counter", None); + let attrs = vec![KeyValue::new("key1", "shared")]; + + let bound1 = counter.bind(&attrs); + let bound2 = counter.bind(&attrs); + + bound1.add(10); + bound2.add(20); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + assert_eq!( + sum.data_points.len(), + 1, + "Multiple handles to same attrs should share data point" + ); + assert_eq!(sum.data_points[0].value, 30); + + // Drop one handle — entry should NOT be evicted + drop(bound1); + test_context.reset_metrics(); + test_context.flush_metrics(); // idle cycle, but bound2 still holds it + + // bound2 still works + test_context.reset_metrics(); + bound2.add(5); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 1); + assert_eq!( + sum.data_points[0].value, 5, + "Entry should persist while any handle is alive" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_empty_attributes() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.u64_counter("test", "my_counter", None); + let bound = counter.bind(&[]); + + bound.add(10); + bound.add(30); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 1); + assert_eq!(sum.data_points[0].value, 40); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_empty_attributes_shares_with_unbound() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.u64_counter("test", "my_counter", None); + let bound = counter.bind(&[]); + + // Mix bound and unbound calls with empty attributes — they must share + // the same data point (both route to no_attribute_tracker). + counter.add(10, &[]); + bound.add(20); + counter.add(30, &[]); + bound.add(40); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + + assert_eq!( + sum.data_points.len(), + 1, + "Bound and unbound with empty attributes must share the same data point" + ); + assert_eq!(sum.data_points[0].value, 100); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_histogram_empty_attributes_shares_with_unbound() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let histogram = test_context + .meter() + .u64_histogram("my_histogram") + .with_boundaries(vec![5.0, 10.0, 25.0]) + .build(); + let bound = histogram.bind(&[]); + + histogram.record(3, &[]); + bound.record(7); + histogram.record(20, &[]); + test_context.flush_metrics(); + + let MetricData::Histogram(hist) = test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + + assert_eq!( + hist.data_points.len(), + 1, + "Bound and unbound with empty attributes must share the same data point" + ); + let dp = &hist.data_points[0]; + assert!(dp.attributes.is_empty()); + assert_eq!(dp.count, 3); + assert_eq!(dp.sum, 30); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[cfg(feature = "spec_unstable_metrics_views")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_view_filters_attributes_at_bind_time() { + use opentelemetry::Key; + + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name() == "my_counter" { + Stream::builder() + .with_allowed_attribute_keys(vec![Key::new("k1"), Key::new("k2")]) + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + let meter = meter_provider.meter("test"); + let counter = meter.u64_counter("my_counter").build(); + + // bind with k3 included — view should drop it at bind time + let bound = counter.bind(&[ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v2"), + KeyValue::new("k3", "v3"), + ]); + bound.add(10); + bound.add(20); + + // unbound call with a *different* k3 value: after view filtering both + // bound and unbound must collapse into the same data point. + counter.add( + 7, + &[ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v2"), + KeyValue::new("k3", "different"), + ], + ); + + meter_provider.force_flush().unwrap(); + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + let data::AggregatedMetrics::U64(MetricData::Sum(sum)) = &metric.data else { + unreachable!() + }; + + assert_eq!( + sum.data_points.len(), + 1, + "view should filter k3, leaving bound+unbound to aggregate together" + ); + assert_eq!(sum.data_points[0].value, 37); + let attrs = &sum.data_points[0].attributes; + assert_eq!(attrs.len(), 2); + assert!(attrs.iter().any(|kv| kv.key.as_str() == "k1")); + assert!(attrs.iter().any(|kv| kv.key.as_str() == "k2")); + assert!(!attrs.iter().any(|kv| kv.key.as_str() == "k3")); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[cfg(feature = "spec_unstable_metrics_views")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_histogram_view_filters_attributes_at_bind_time() { + use opentelemetry::Key; + + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name() == "my_hist" { + Stream::builder() + .with_allowed_attribute_keys(vec![Key::new("k1"), Key::new("k2")]) + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + let meter = meter_provider.meter("test"); + let histogram = meter + .u64_histogram("my_hist") + .with_boundaries(vec![5.0, 10.0, 25.0]) + .build(); + + let bound = histogram.bind(&[ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v2"), + KeyValue::new("k3", "v3"), + ]); + bound.record(3); + bound.record(20); + histogram.record( + 7, + &[ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v2"), + KeyValue::new("k3", "different"), + ], + ); + + meter_provider.force_flush().unwrap(); + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + let data::AggregatedMetrics::U64(MetricData::Histogram(hist)) = &metric.data else { + unreachable!() + }; + + assert_eq!( + hist.data_points.len(), + 1, + "view should filter k3, leaving bound+unbound to aggregate together" + ); + let dp = &hist.data_points[0]; + assert_eq!(dp.count, 3); + assert_eq!(dp.sum, 30); + assert_eq!(dp.attributes.len(), 2); + assert!(dp.attributes.iter().any(|kv| kv.key.as_str() == "k1")); + assert!(dp.attributes.iter().any(|kv| kv.key.as_str() == "k2")); + assert!(!dp.attributes.iter().any(|kv| kv.key.as_str() == "k3")); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_counter_at_overflow_attributes_to_overflow_bucket_cumulative() { + // Cumulative: cardinality only grows, never evicts. A bind() at the + // limit lands in overflow and accumulates there forever. Verifies the + // bound handle's cumulative writes converge in the overflow bucket + // across multiple collection cycles. + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_counter" { + Stream::builder() + .with_name("my_counter") + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Cumulative, view); + let counter = test_context.u64_counter("test", "my_counter", None); + + for v in 0..cardinality_limit { + counter.add(1, &[KeyValue::new("A", v.to_string())]); + } + let bound = counter.bind(&[KeyValue::new("A", "overflow_bind")]); + bound.add(10); + + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + let overflow_dp = find_overflow_sum_datapoint(&sum.data_points) + .expect("cycle 1: overflow point expected"); + assert_eq!(overflow_dp.value, 10); + + // Cycle 2: cumulative state accumulates internally; reset the exporter + // so the assertion sees a single export rather than two appended ones. + test_context.reset_metrics(); + bound.add(7); + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_counter", None) else { + unreachable!() + }; + let overflow_dp = find_overflow_sum_datapoint(&sum.data_points) + .expect("cycle 2: overflow point expected"); + assert_eq!( + overflow_dp.value, 17, + "cumulative overflow-bound writes must accumulate" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_histogram_at_overflow_attributes_to_overflow_bucket_cumulative() { + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_histogram" { + Stream::builder() + .with_name("my_histogram") + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Cumulative, view); + let histogram = test_context + .meter() + .f64_histogram("my_histogram") + .with_boundaries(vec![10.0, 50.0]) + .build(); + + for v in 0..cardinality_limit { + histogram.record(1.0, &[KeyValue::new("A", v.to_string())]); + } + let bound = histogram.bind(&[KeyValue::new("A", "overflow_bind")]); + bound.record(20.0); + + test_context.flush_metrics(); + let MetricData::Histogram(hist) = test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + let overflow_dp = find_overflow_histogram_datapoint(&hist.data_points) + .expect("cycle 1: overflow point expected"); + assert_eq!(overflow_dp.sum, 20.0); + assert_eq!(overflow_dp.count, 1); + + // Cycle 2: cumulative accumulates internally; reset exporter to see a + // single fresh export rather than two appended ones. + test_context.reset_metrics(); + bound.record(30.0); + test_context.flush_metrics(); + let MetricData::Histogram(hist) = test_context.get_aggregation::("my_histogram", None) + else { + unreachable!() + }; + let overflow_dp = find_overflow_histogram_datapoint(&hist.data_points) + .expect("cycle 2: overflow point expected"); + assert_eq!( + overflow_dp.sum, 50.0, + "cumulative overflow-bound writes must accumulate" + ); + assert_eq!(overflow_dp.count, 2); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_exponential_histogram_at_overflow_attributes_to_overflow_bucket_cumulative() { + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_histogram" { + Stream::builder() + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: 20, + record_min_max: true, + }) + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Cumulative, view); + let histogram = test_context.meter().f64_histogram("my_histogram").build(); + + for v in 0..cardinality_limit { + histogram.record(1.0, &[KeyValue::new("A", v.to_string())]); + } + let bound = histogram.bind(&[KeyValue::new("A", "overflow_bind")]); + bound.record(20.0); + + test_context.flush_metrics(); + let MetricData::ExponentialHistogram(hist) = + test_context.get_aggregation::("my_histogram", None) + else { + panic!("expected ExponentialHistogram aggregation"); + }; + let overflow_dp = find_overflow_exponential_histogram_datapoint(&hist.data_points) + .expect("cycle 1: overflow point expected"); + assert_eq!(overflow_dp.sum, 20.0); + assert_eq!(overflow_dp.count, 1); + + // Cycle 2: cumulative accumulates internally; reset exporter to see a + // single fresh export rather than two appended ones. + test_context.reset_metrics(); + bound.record(30.0); + test_context.flush_metrics(); + let MetricData::ExponentialHistogram(hist) = + test_context.get_aggregation::("my_histogram", None) + else { + panic!("expected ExponentialHistogram aggregation"); + }; + let overflow_dp = find_overflow_exponential_histogram_datapoint(&hist.data_points) + .expect("cycle 2: overflow point expected"); + assert_eq!( + overflow_dp.sum, 50.0, + "cumulative overflow-bound writes must accumulate" + ); + assert_eq!(overflow_dp.count, 2); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_exponential_histogram_view_filters_attributes_at_bind_time() { + use opentelemetry::Key; + + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name() == "my_hist" { + Stream::builder() + .with_aggregation(Aggregation::Base2ExponentialHistogram { + max_size: 160, + max_scale: 20, + record_min_max: true, + }) + .with_allowed_attribute_keys(vec![Key::new("k1"), Key::new("k2")]) + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + let meter = meter_provider.meter("test"); + let histogram = meter.f64_histogram("my_hist").build(); + + let bound = histogram.bind(&[ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v2"), + KeyValue::new("k3", "v3"), + ]); + bound.record(3.0); + bound.record(20.0); + // Unbound call with a different k3: after view filtering, bound and unbound + // must collapse into the same exponential histogram data point. + histogram.record( + 7.0, + &[ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v2"), + KeyValue::new("k3", "different"), + ], + ); + + meter_provider.force_flush().unwrap(); + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + let data::AggregatedMetrics::F64(MetricData::ExponentialHistogram(hist)) = &metric.data + else { + panic!("expected ExponentialHistogram aggregation"); + }; + + assert_eq!( + hist.data_points.len(), + 1, + "view should filter k3, leaving bound+unbound to aggregate together" + ); + let dp = &hist.data_points[0]; + assert_eq!(dp.count, 3); + assert_eq!(dp.sum, 30.0); + assert_eq!(dp.attributes.len(), 2); + assert!(dp.attributes.iter().any(|kv| kv.key.as_str() == "k1")); + assert!(dp.attributes.iter().any(|kv| kv.key.as_str() == "k2")); + assert!(!dp.attributes.iter().any(|kv| kv.key.as_str() == "k3")); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_gauge_cumulative() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let gauge = test_context.meter().u64_gauge("my_gauge").build(); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = gauge.bind(&attrs); + + bound.record(10); + bound.record(20); + bound.record(30); + test_context.flush_metrics(); + + let MetricData::Gauge(gauge_data) = test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + + assert_eq!(gauge_data.data_points.len(), 1, "Expected one data point"); + let dp = &gauge_data.data_points[0]; + assert_eq!(dp.value, 30, "Gauge should report the last recorded value"); + assert_eq!(dp.attributes, vec![KeyValue::new("key1", "bound_value")]); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_gauge_delta() { + let mut test_context = TestContext::new(Temporality::Delta); + let gauge = test_context.meter().u64_gauge("my_gauge").build(); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = gauge.bind(&attrs); + + bound.record(50); + bound.record(75); + test_context.flush_metrics(); + + let MetricData::Gauge(gauge_data) = test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + assert_eq!(gauge_data.data_points.len(), 1); + assert_eq!(gauge_data.data_points[0].value, 75); + + // Next cycle: a new record produces the new last value. + test_context.reset_metrics(); + bound.record(25); + test_context.flush_metrics(); + + let MetricData::Gauge(gauge_data) = test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + assert_eq!(gauge_data.data_points.len(), 1); + assert_eq!( + gauge_data.data_points[0].value, 25, + "Delta gauge should report only the latest value since the last collection" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_gauge_matches_unbound() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let gauge = test_context.meter().u64_gauge("my_gauge").build(); + let attrs = vec![KeyValue::new("key1", "shared")]; + let bound = gauge.bind(&attrs); + + // Bound and unbound writes target the same data point. The final + // exported value is whichever write happened last. + gauge.record(10, &attrs); + bound.record(20); + gauge.record(30, &attrs); + bound.record(40); + test_context.flush_metrics(); + + let MetricData::Gauge(gauge_data) = test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + + assert_eq!( + gauge_data.data_points.len(), + 1, + "Bound and unbound should share the same data point" + ); + assert_eq!( + gauge_data.data_points[0].value, 40, + "Last write (via bound) should win" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_gauge_delta_no_update_no_export() { + let mut test_context = TestContext::new(Temporality::Delta); + let gauge = test_context.meter().u64_gauge("my_gauge").build(); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = gauge.bind(&attrs); + + // Cycle 1: record and collect. + bound.record(10); + test_context.flush_metrics(); + let MetricData::Gauge(gauge_data) = test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + assert_eq!(gauge_data.data_points.len(), 1); + assert_eq!(gauge_data.data_points[0].value, 10); + + // Cycle 2: no record - should export nothing. + test_context.reset_metrics(); + test_context.flush_metrics(); + let resource_metrics = test_context + .exporter + .get_finished_metrics() + .expect("metrics export should succeed"); + assert!( + resource_metrics.is_empty(), + "Bound gauge with no updates should not export" + ); + + // Cycle 3: record again - handle still alive. + test_context.reset_metrics(); + bound.record(99); + test_context.flush_metrics(); + let MetricData::Gauge(gauge_data) = test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + assert_eq!(gauge_data.data_points.len(), 1); + assert_eq!( + gauge_data.data_points[0].value, 99, + "Bound handle should produce fresh value after quiet cycle" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_gauge_at_overflow_attributes_to_overflow_bucket() { + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_gauge" { + Stream::builder() + .with_name("my_gauge") + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Delta, view); + let gauge = test_context.meter().u64_gauge("my_gauge").build(); + + // Fill to cardinality limit with unbound calls. + for v in 0..cardinality_limit { + gauge.record(1, &[KeyValue::new("A", v.to_string())]); + } + + // bind() at overflow - handle binds directly to the overflow tracker. + let overflow_attrs = vec![KeyValue::new("A", "overflow_bind")]; + let bound = gauge.bind(&overflow_attrs); + bound.record(42); + + test_context.flush_metrics(); + let MetricData::Gauge(gauge_data) = test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + + assert_eq!( + gauge_data.data_points.len(), + cardinality_limit + 1, + "Expected {} unique + 1 overflow data points", + cardinality_limit + ); + + let overflow_dp = find_overflow_gauge_datapoint(&gauge_data.data_points) + .expect("overflow point expected"); + assert_eq!( + overflow_dp.value, 42, + "Bound-at-overflow data should go to overflow bucket" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_gauge_multiple_handles_same_attrs() { + let mut test_context = TestContext::new(Temporality::Delta); + let gauge = test_context.meter().u64_gauge("my_gauge").build(); + let attrs = vec![KeyValue::new("key1", "shared")]; + + let bound1 = gauge.bind(&attrs); + let bound2 = gauge.bind(&attrs); + + bound1.record(10); + bound2.record(20); + test_context.flush_metrics(); + + let MetricData::Gauge(gauge_data) = test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + assert_eq!( + gauge_data.data_points.len(), + 1, + "Multiple handles to same attrs should share data point" + ); + assert_eq!(gauge_data.data_points[0].value, 20); + + // Drop one handle - entry should NOT be evicted while the other handle is alive. + drop(bound1); + test_context.reset_metrics(); + test_context.flush_metrics(); + + test_context.reset_metrics(); + bound2.record(5); + test_context.flush_metrics(); + + let MetricData::Gauge(gauge_data) = test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + assert_eq!(gauge_data.data_points.len(), 1); + assert_eq!( + gauge_data.data_points[0].value, 5, + "Entry should persist while any handle is alive" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_gauge_empty_attributes_shares_with_unbound() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let gauge = test_context.meter().u64_gauge("my_gauge").build(); + let bound = gauge.bind(&[]); + + // Mix bound and unbound calls with empty attributes - they must share + // the same data point. Last write wins. + gauge.record(10, &[]); + bound.record(20); + gauge.record(30, &[]); + bound.record(40); + test_context.flush_metrics(); + + let MetricData::Gauge(gauge_data) = test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + + assert_eq!( + gauge_data.data_points.len(), + 1, + "Bound and unbound with empty attributes must share the same data point" + ); + let dp = &gauge_data.data_points[0]; + assert!(dp.attributes.is_empty()); + assert_eq!(dp.value, 40); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[cfg(feature = "spec_unstable_metrics_views")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_gauge_view_filters_attributes_at_bind_time() { + use opentelemetry::Key; + + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name() == "my_gauge" { + Stream::builder() + .with_allowed_attribute_keys(vec![Key::new("k1"), Key::new("k2")]) + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + let meter = meter_provider.meter("test"); + let gauge = meter.u64_gauge("my_gauge").build(); + + // bind with k3 included - view should drop it at bind time. + let bound = gauge.bind(&[ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v2"), + KeyValue::new("k3", "v3"), + ]); + bound.record(10); + bound.record(20); + + // Unbound call with a *different* k3 value: after view filtering both + // bound and unbound must collapse into the same data point. + gauge.record( + 7, + &[ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v2"), + KeyValue::new("k3", "different"), + ], + ); + + meter_provider.force_flush().unwrap(); + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + let data::AggregatedMetrics::U64(MetricData::Gauge(gauge_data)) = &metric.data else { + unreachable!() + }; + + assert_eq!( + gauge_data.data_points.len(), + 1, + "view should filter k3, leaving bound+unbound to share the same data point" + ); + assert_eq!( + gauge_data.data_points[0].value, 7, + "Last write (the unbound record) should win" + ); + let attrs = &gauge_data.data_points[0].attributes; + assert_eq!(attrs.len(), 2); + assert!(attrs.iter().any(|kv| kv.key.as_str() == "k1")); + assert!(attrs.iter().any(|kv| kv.key.as_str() == "k2")); + assert!(!attrs.iter().any(|kv| kv.key.as_str() == "k3")); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_gauge_drop_enables_eviction() { + let mut test_context = TestContext::new(Temporality::Delta); + let gauge = test_context.meter().u64_gauge("my_gauge").build(); + let attrs = vec![KeyValue::new("key1", "ephemeral")]; + + { + let bound = gauge.bind(&attrs); + bound.record(100); + test_context.flush_metrics(); + + let MetricData::Gauge(gauge_data) = + test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + assert_eq!(gauge_data.data_points.len(), 1); + assert_eq!(gauge_data.data_points[0].value, 100); + // bound drops here + } + + // Cycle 2: no updates, bound handle dropped - entry becomes stale and evictable. + test_context.reset_metrics(); + test_context.flush_metrics(); + + // Cycle 3: the stale entry should have been evicted, so a new unbound + // record should be the only data point. + test_context.reset_metrics(); + gauge.record(1, &[KeyValue::new("key1", "new_entry")]); + test_context.flush_metrics(); + + let MetricData::Gauge(gauge_data) = test_context.get_aggregation::("my_gauge", None) + else { + unreachable!() + }; + + assert_eq!(gauge_data.data_points.len(), 1); + let dp = find_gauge_datapoint_with_key_value(&gauge_data.data_points, "key1", "new_entry") + .expect("new_entry should be present"); + assert_eq!(dp.value, 1); + assert!( + find_gauge_datapoint_with_key_value(&gauge_data.data_points, "key1", "ephemeral") + .is_none(), + "Dropped bound entry should have been evicted" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_updown_counter_cumulative() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.i64_up_down_counter("test", "my_updown", None); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = counter.bind(&attrs); + + // UpDownCounter supports both positive and negative values. + bound.add(50); + bound.add(-20); + bound.add(30); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_updown", None) else { + unreachable!() + }; + + assert_eq!(sum.data_points.len(), 1, "Expected one data point"); + assert!( + !sum.is_monotonic, + "UpDownCounter must not produce monotonic Sums" + ); + assert_eq!(sum.temporality, Temporality::Cumulative); + assert_eq!(sum.data_points[0].value, 60); + assert_eq!( + sum.data_points[0].attributes, + vec![KeyValue::new("key1", "bound_value")] + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_updown_counter_always_cumulative_even_when_delta_requested() { + // UpDownCounter always uses Cumulative temporality regardless of the + // reader's preference. Confirm the bound API observes the same rule. + let mut test_context = TestContext::new(Temporality::Delta); + let counter = test_context.i64_up_down_counter("test", "my_updown", None); + let attrs = vec![KeyValue::new("key1", "bound_value")]; + let bound = counter.bind(&attrs); + + bound.add(50); + bound.add(-10); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_updown", None) else { + unreachable!() + }; + assert_eq!( + sum.temporality, + Temporality::Cumulative, + "UpDownCounter must produce Cumulative regardless of reader preference" + ); + assert!(!sum.is_monotonic); + assert_eq!(sum.data_points.len(), 1); + assert_eq!(sum.data_points[0].value, 40); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_updown_counter_matches_unbound() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.i64_up_down_counter("test", "my_updown", None); + let attrs = vec![KeyValue::new("key1", "shared")]; + let bound = counter.bind(&attrs); + + // Mix bound and unbound additions to the same attribute set. + counter.add(10, &attrs); + bound.add(-20); + counter.add(30, &attrs); + bound.add(40); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_updown", None) else { + unreachable!() + }; + + assert_eq!( + sum.data_points.len(), + 1, + "Bound and unbound should share the same data point" + ); + assert!(!sum.is_monotonic); + assert_eq!(sum.data_points[0].value, 60); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_updown_counter_at_overflow_attributes_to_overflow_bucket() { + let cardinality_limit = 3; + let view = move |i: &Instrument| { + if i.name() == "my_updown" { + Stream::builder() + .with_name("my_updown") + .with_cardinality_limit(cardinality_limit) + .build() + .ok() + } else { + None + } + }; + let mut test_context = TestContext::new_with_view(Temporality::Cumulative, view); + let counter = test_context.i64_up_down_counter("test", "my_updown", None); + + // Fill to cardinality limit with unbound calls. + for v in 0..cardinality_limit { + counter.add(1, &[KeyValue::new("A", v.to_string())]); + } + + // bind() at overflow - handle binds directly to the overflow tracker. + let overflow_attrs = vec![KeyValue::new("A", "overflow_bind")]; + let bound = counter.bind(&overflow_attrs); + bound.add(42); + bound.add(-2); + + test_context.flush_metrics(); + let MetricData::Sum(sum) = test_context.get_aggregation::("my_updown", None) else { + unreachable!() + }; + + assert_eq!( + sum.data_points.len(), + cardinality_limit + 1, + "Expected {} unique + 1 overflow data points", + cardinality_limit + ); + + let overflow_dp = + find_overflow_sum_datapoint(&sum.data_points).expect("overflow point expected"); + assert_eq!( + overflow_dp.value, 40, + "Bound-at-overflow data should go to overflow bucket" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_updown_counter_multiple_handles_same_attrs() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.i64_up_down_counter("test", "my_updown", None); + let attrs = vec![KeyValue::new("key1", "shared")]; + + let bound1 = counter.bind(&attrs); + let bound2 = counter.bind(&attrs); + + bound1.add(10); + bound2.add(-3); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_updown", None) else { + unreachable!() + }; + assert_eq!( + sum.data_points.len(), + 1, + "Multiple handles to same attrs should share data point" + ); + assert_eq!(sum.data_points[0].value, 7); + + // Drop one handle. Cumulative state still includes its contributions + // and the surviving handle continues to write to the same tracker. + drop(bound1); + test_context.reset_metrics(); + bound2.add(-5); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_updown", None) else { + unreachable!() + }; + assert_eq!(sum.data_points.len(), 1); + assert_eq!( + sum.data_points[0].value, 2, + "Cumulative tracker should retain prior bound contributions" + ); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_updown_counter_empty_attributes_shares_with_unbound() { + let mut test_context = TestContext::new(Temporality::Cumulative); + let counter = test_context.i64_up_down_counter("test", "my_updown", None); + let bound = counter.bind(&[]); + + // Mix bound and unbound calls with empty attributes - they must share + // the same data point (both route to no_attribute_tracker). + counter.add(10, &[]); + bound.add(-3); + counter.add(30, &[]); + bound.add(-2); + test_context.flush_metrics(); + + let MetricData::Sum(sum) = test_context.get_aggregation::("my_updown", None) else { + unreachable!() + }; + + assert_eq!( + sum.data_points.len(), + 1, + "Bound and unbound with empty attributes must share the same data point" + ); + assert!(sum.data_points[0].attributes.is_empty()); + assert_eq!(sum.data_points[0].value, 35); + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + #[cfg(feature = "spec_unstable_metrics_views")] + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn bound_updown_counter_view_filters_attributes_at_bind_time() { + use opentelemetry::Key; + + let exporter = InMemoryMetricExporter::default(); + let view = |i: &Instrument| { + if i.name() == "my_updown" { + Stream::builder() + .with_allowed_attribute_keys(vec![Key::new("k1"), Key::new("k2")]) + .build() + .ok() + } else { + None + } + }; + let meter_provider = SdkMeterProvider::builder() + .with_periodic_exporter(exporter.clone()) + .with_view(view) + .build(); + let meter = meter_provider.meter("test"); + let counter = meter.i64_up_down_counter("my_updown").build(); + + // bind with k3 included - view should drop it at bind time. + let bound = counter.bind(&[ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v2"), + KeyValue::new("k3", "v3"), + ]); + bound.add(10); + bound.add(-3); + + // unbound call with a *different* k3 value: after view filtering both + // bound and unbound must collapse into the same data point. + counter.add( + 7, + &[ + KeyValue::new("k1", "v1"), + KeyValue::new("k2", "v2"), + KeyValue::new("k3", "different"), + ], + ); + + meter_provider.force_flush().unwrap(); + let resource_metrics = exporter + .get_finished_metrics() + .expect("metrics are expected to be exported."); + let metric = &resource_metrics[0].scope_metrics[0].metrics[0]; + let data::AggregatedMetrics::I64(MetricData::Sum(sum)) = &metric.data else { + unreachable!() + }; + + assert_eq!( + sum.data_points.len(), + 1, + "view should filter k3, leaving bound+unbound to aggregate together" + ); + assert!(!sum.is_monotonic); + assert_eq!(sum.data_points[0].value, 14); + let attrs = &sum.data_points[0].attributes; + assert_eq!(attrs.len(), 2); + assert!(attrs.iter().any(|kv| kv.key.as_str() == "k1")); + assert!(attrs.iter().any(|kv| kv.key.as_str() == "k2")); + assert!(!attrs.iter().any(|kv| kv.key.as_str() == "k3")); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/noop.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/noop.rs new file mode 100644 index 00000000..62b9dfe1 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/noop.rs @@ -0,0 +1,57 @@ +#[cfg(feature = "experimental_metrics_bound_instruments")] +use opentelemetry::metrics::BoundSyncInstrument; +use opentelemetry::{ + metrics::{InstrumentProvider, SyncInstrument}, + KeyValue, +}; + +/// A no-op instance of a `Meter` +#[derive(Debug, Default)] +pub(crate) struct NoopMeter { + _private: (), +} + +impl NoopMeter { + /// Create a new no-op meter core. + pub(crate) fn new() -> Self { + NoopMeter { _private: () } + } +} + +impl InstrumentProvider for NoopMeter {} + +/// A no-op sync instrument +#[derive(Debug, Default)] +pub(crate) struct NoopSyncInstrument { + _private: (), +} + +impl NoopSyncInstrument { + /// Create a new no-op sync instrument + pub(crate) fn new() -> Self { + NoopSyncInstrument { _private: () } + } +} + +impl SyncInstrument for NoopSyncInstrument { + fn measure(&self, _value: T, _attributes: &[KeyValue]) { + // Ignored + } + + #[cfg(feature = "experimental_metrics_bound_instruments")] + fn bind(&self, _attributes: &[KeyValue]) -> Box + Send + Sync> { + Box::new(NoopBoundSyncInstrument { _private: () }) + } +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +struct NoopBoundSyncInstrument { + _private: (), +} + +#[cfg(feature = "experimental_metrics_bound_instruments")] +impl BoundSyncInstrument for NoopBoundSyncInstrument { + fn measure(&self, _measurement: T) { + // Ignored + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/periodic_reader.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/periodic_reader.rs new file mode 100644 index 00000000..86f8cfab --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/periodic_reader.rs @@ -0,0 +1,1006 @@ +use std::{ + env, fmt, + sync::{ + mpsc::{self, Receiver, Sender}, + Arc, Mutex, Weak, + }, + thread, + time::{Duration, Instant}, +}; + +use opentelemetry::{otel_debug, otel_error, otel_info, otel_warn, Context}; + +use crate::{ + error::{OTelSdkError, OTelSdkResult}, + metrics::{exporter::PushMetricExporter, reader::SdkProducer}, + Resource, +}; + +use super::{ + data::ResourceMetrics, instrument::InstrumentKind, pipeline::Pipeline, reader::MetricReader, + Temporality, +}; + +/// Environment variable for configuring the delay interval (in milliseconds) +/// between two consecutive exports for [PeriodicReader]. +pub const OTEL_METRIC_EXPORT_INTERVAL: &str = "OTEL_METRIC_EXPORT_INTERVAL"; + +/// Default delay interval between two consecutive exports for [PeriodicReader]. +pub const OTEL_METRIC_EXPORT_INTERVAL_DEFAULT: Duration = Duration::from_secs(60); + +/// Configuration options for [PeriodicReader]. +#[derive(Debug)] +pub struct PeriodicReaderBuilder { + interval: Duration, + exporter: E, +} + +impl PeriodicReaderBuilder +where + E: PushMetricExporter, +{ + fn new(exporter: E) -> Self { + let interval = env::var(OTEL_METRIC_EXPORT_INTERVAL) + .ok() + .and_then(|v| v.parse().map(Duration::from_millis).ok()) + .unwrap_or(OTEL_METRIC_EXPORT_INTERVAL_DEFAULT); + + PeriodicReaderBuilder { interval, exporter } + } + + /// Configures the intervening time between exports for a [PeriodicReader]. + /// + /// This option overrides any value set for the `OTEL_METRIC_EXPORT_INTERVAL` + /// environment variable. + /// + /// If this option is not used or `interval` is equal to zero, 60 seconds is + /// used as the default. + pub fn with_interval(mut self, interval: Duration) -> Self { + if !interval.is_zero() { + self.interval = interval; + } + self + } + + /// Create a [PeriodicReader] with the given config. + pub fn build(self) -> PeriodicReader { + PeriodicReader::new(self.exporter, self.interval) + } +} + +/// A `MetricReader` that periodically collects and exports metrics at a configurable interval. +/// +/// By default, [`PeriodicReader`] collects and exports metrics every **60 seconds**. +/// The time taken for export is **not** included in the interval. Use [`PeriodicReaderBuilder`] +/// to customize the interval. +/// +/// [`PeriodicReader`] spawns a background thread to handle metric collection and export. +/// This thread remains active until [`shutdown()`] is called. +/// +/// ## Collection Process +/// "Collection" refers to gathering aggregated metrics from the SDK's internal storage. +/// During this phase, callbacks from observable instruments are also triggered. +/// +/// [`PeriodicReader`] does **not** enforce a timeout for collection. If an +/// observable callback takes too long, it may delay the next collection cycle. +/// If a callback never returns, it **will stall** all metric collection (and exports) +/// indefinitely. +/// +/// ## Exporter Compatibility +/// When used with the [`OTLP Exporter`](https://docs.rs/opentelemetry-otlp), the following +/// transport options are supported: +/// +/// - **`grpc-tonic`**: Requires [`MeterProvider`] to be initialized within a `tokio` runtime. +/// - **`reqwest-blocking-client`**: Works with both a standard (`main`) function and `tokio::main`. +/// +/// Async HTTP clients such as `reqwest-client` and `hyper-client` are not +/// supported by this default reader. The OTLP HTTP exporter chooses its default +/// HTTP client from enabled crate features and cannot tell which reader will +/// drive it. If your dependency graph enables async HTTP client features, either +/// pass an explicit blocking client for this reader or use the experimental +/// async-runtime periodic reader. +/// +/// [`PeriodicReader`] does **not** enforce a timeout for exports either. Instead, +/// the configured exporter is responsible for enforcing timeouts. If an export operation +/// never returns, [`PeriodicReader`] will **stop exporting new metrics**, stalling +/// metric collection. +/// +/// ## Manual Export & Shutdown +/// Users can manually trigger an export via [`force_flush()`]. Calling [`shutdown()`] +/// exports any remaining metrics and should be done before application exit to ensure +/// all data is sent. +/// +/// **Warning**: If using **tokio’s current-thread runtime**, calling [`shutdown()`] +/// from the main thread may cause a deadlock. To prevent this, call [`shutdown()`] +/// from a separate thread or use tokio's `spawn_blocking`. +/// +/// [`PeriodicReader`]: crate::metrics::PeriodicReader +/// [`PeriodicReaderBuilder`]: crate::metrics::PeriodicReaderBuilder +/// [`MeterProvider`]: crate::metrics::SdkMeterProvider +/// [`shutdown()`]: crate::metrics::SdkMeterProvider::shutdown +/// [`force_flush()`]: crate::metrics::SdkMeterProvider::force_flush +/// +/// # Example +/// +/// ```no_run +/// use opentelemetry_sdk::metrics::PeriodicReader; +/// # fn example(get_exporter: impl Fn() -> E) +/// # where +/// # E: opentelemetry_sdk::metrics::exporter::PushMetricExporter, +/// # { +/// +/// let exporter = get_exporter(); // set up a push exporter +/// +/// let reader = PeriodicReader::builder(exporter).build(); +/// # drop(reader); +/// # } +/// ``` +pub struct PeriodicReader { + inner: Arc>, +} + +impl Clone for PeriodicReader { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +impl PeriodicReader { + /// Configuration options for a periodic reader with own thread + pub fn builder(exporter: E) -> PeriodicReaderBuilder { + PeriodicReaderBuilder::new(exporter) + } + + fn new(exporter: E, interval: Duration) -> Self { + let (message_sender, message_receiver): (Sender, Receiver) = + mpsc::channel(); + let exporter_arc = Arc::new(exporter); + let reader = PeriodicReader { + inner: Arc::new(PeriodicReaderInner { + message_sender, + producer: Mutex::new(None), + exporter: exporter_arc.clone(), + }), + }; + let cloned_reader = reader.clone(); + + let mut rm = ResourceMetrics { + resource: Resource::empty(), + scope_metrics: Vec::new(), + }; + + let result_thread_creation = thread::Builder::new() + .name("OpenTelemetry.Metrics.PeriodicReader".to_string()) + .spawn(move || { + let _suppress_guard = Context::enter_telemetry_suppressed_scope(); + let mut interval_start = Instant::now(); + let mut remaining_interval = interval; + otel_debug!( + name: "PeriodReaderThreadStarted", + interval_in_millisecs = interval.as_millis(), + ); + loop { + otel_debug!( + name: "PeriodReaderThreadLoopAlive", message = "Next export will happen after interval, unless flush or shutdown is triggered.", interval_in_millisecs = remaining_interval.as_millis() + ); + match message_receiver.recv_timeout(remaining_interval) { + Ok(Message::Flush(response_sender)) => { + otel_debug!( + name: "PeriodReaderThreadExportingDueToFlush" + ); + let export_result = cloned_reader.collect_and_export(&mut rm); + otel_debug!( + name: "PeriodReaderInvokedExport", + export_result = format!("{:?}", export_result) + ); + + // If response_sender is disconnected, we can't send + // the result back. This occurs when the thread that + // initiated flush gave up due to timeout. + // Gracefully handle that with internal logs. The + // internal errors are of Info level, as this is + // useful for user to know whether the flush was + // successful or not, when flush() itself merely + // tells that it timed out. + + if export_result.is_err() { + if response_sender.send(false).is_err() { + otel_debug!( + name: "PeriodReader.Flush.ResponseSendError", + message = "PeriodicReader's flush has failed, but unable to send this info back to caller. + This occurs when the caller has timed out waiting for the response. If you see this occuring frequently, consider increasing the flush timeout." + ); + } + } else if response_sender.send(true).is_err() { + otel_debug!( + name: "PeriodReader.Flush.ResponseSendError", + message = "PeriodicReader's flush has completed successfully, but unable to send this info back to caller. + This occurs when the caller has timed out waiting for the response. If you see this occuring frequently, consider increasing the flush timeout." + ); + } + + // Adjust the remaining interval after the flush + let elapsed = interval_start.elapsed(); + if elapsed < interval { + remaining_interval = interval - elapsed; + otel_debug!( + name: "PeriodReaderThreadAdjustingRemainingIntervalAfterFlush", + remaining_interval = remaining_interval.as_secs() + ); + } else { + otel_debug!( + name: "PeriodReaderThreadAdjustingExportAfterFlush", + ); + // Reset the interval if the flush finishes after the expected export time + // effectively missing the normal export. + // Should we attempt to do the missed export immediately? + // Or do the next export at the next interval? + // Currently this attempts the next export immediately. + // i.e calling Flush can affect the regularity. + interval_start = Instant::now(); + remaining_interval = Duration::ZERO; + } + } + Ok(Message::Shutdown(response_sender)) => { + // Perform final export and break out of loop and exit the thread + otel_debug!(name: "PeriodReaderThreadExportingDueToShutdown"); + let export_result = cloned_reader.collect_and_export(&mut rm); + otel_debug!( + name: "PeriodReaderInvokedExport", + export_result = format!("{:?}", export_result) + ); + let shutdown_result = exporter_arc.shutdown(); + otel_debug!( + name: "PeriodReaderInvokedExporterShutdown", + shutdown_result = format!("{:?}", shutdown_result) + ); + + // If response_sender is disconnected, we can't send + // the result back. This occurs when the thread that + // initiated shutdown gave up due to timeout. + // Gracefully handle that with internal logs and + // continue with shutdown (i.e exit thread) The + // internal errors are of Info level, as this is + // useful for user to know whether the shutdown was + // successful or not, when shutdown() itself merely + // tells that it timed out. + if export_result.is_err() || shutdown_result.is_err() { + if response_sender.send(false).is_err() { + otel_info!( + name: "PeriodReaderThreadShutdown.ResponseSendError", + message = "PeriodicReader's shutdown has failed, but unable to send this info back to caller. + This occurs when the caller has timed out waiting for the response. If you see this occuring frequently, consider increasing the shutdown timeout." + ); + } + } else if response_sender.send(true).is_err() { + otel_debug!( + name: "PeriodReaderThreadShutdown.ResponseSendError", + message = "PeriodicReader completed its shutdown, but unable to send this info back to caller. + This occurs when the caller has timed out waiting for the response. If you see this occuring frequently, consider increasing the shutdown timeout." + ); + } + + otel_debug!( + name: "PeriodReaderThreadExiting", + reason = "ShutdownRequested" + ); + break; + } + Err(mpsc::RecvTimeoutError::Timeout) => { + let export_start = Instant::now(); + otel_debug!( + name: "PeriodReaderThreadExportingDueToTimer" + ); + + let export_result = cloned_reader.collect_and_export(&mut rm); + otel_debug!( + name: "PeriodReaderInvokedExport", + export_result = format!("{:?}", export_result) + ); + + let time_taken_for_export = export_start.elapsed(); + if time_taken_for_export > interval { + otel_debug!( + name: "PeriodReaderThreadExportTookLongerThanInterval" + ); + // if export took longer than interval, do the + // next export immediately. + // Alternatively, we could skip the next export + // and wait for the next interval. + // Or enforce that export timeout is less than interval. + // What is the desired behavior? + interval_start = Instant::now(); + remaining_interval = Duration::ZERO; + } else { + remaining_interval = interval - time_taken_for_export; + interval_start = Instant::now(); + } + } + Err(mpsc::RecvTimeoutError::Disconnected) => { + // Channel disconnected, only thing to do is break + // out (i.e exit the thread) + otel_debug!( + name: "PeriodReaderThreadExiting", + reason = "MessageSenderDisconnected" + ); + break; + } + } + } + otel_debug!( + name: "PeriodReaderThreadStopped" + ); + }); + + // TODO: Should we fail-fast here and bubble up the error to user? + #[allow(unused_variables)] + if let Err(e) = result_thread_creation { + otel_error!( + name: "PeriodReaderThreadStartError", + message = "Failed to start PeriodicReader thread. Metrics will not be exported.", + error = format!("{:?}", e) + ); + } + reader + } + + fn collect_and_export(&self, rm: &mut ResourceMetrics) -> OTelSdkResult { + self.inner.collect_and_export(rm) + } +} + +impl fmt::Debug for PeriodicReader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PeriodicReader").finish() + } +} + +struct PeriodicReaderInner { + exporter: Arc, + message_sender: mpsc::Sender, + producer: Mutex>>, +} + +impl PeriodicReaderInner { + fn register_pipeline(&self, producer: Weak) { + let mut inner = self.producer.lock().expect("lock poisoned"); + *inner = Some(producer); + } + + fn temporality(&self, _kind: InstrumentKind) -> Temporality { + self.exporter.temporality() + } + + fn collect(&self, rm: &mut ResourceMetrics) -> OTelSdkResult { + let producer = self.producer.lock().expect("lock poisoned"); + if let Some(p) = producer.as_ref() { + p.upgrade() + .ok_or(OTelSdkError::AlreadyShutdown)? + .produce(rm)?; + Ok(()) + } else { + otel_warn!( + name: "PeriodReader.MeterProviderNotRegistered", + message = "PeriodicReader is not registered with MeterProvider. Metrics will not be collected. \ + This occurs when a periodic reader is created but not associated with a MeterProvider \ + by calling `.with_reader(reader)` on MeterProviderBuilder." + ); + Err(OTelSdkError::InternalFailure( + "MeterProvider is not registered".into(), + )) + } + } + + fn collect_and_export(&self, rm: &mut ResourceMetrics) -> OTelSdkResult { + let current_time = Instant::now(); + let collect_result = self.collect(rm); + let time_taken_for_collect = current_time.elapsed(); + + #[allow(clippy::question_mark)] + if let Err(e) = collect_result { + otel_warn!( + name: "PeriodReaderCollectError", + error = format!("{:?}", e) + ); + return Err(OTelSdkError::InternalFailure(e.to_string())); + } + + if rm.scope_metrics.is_empty() { + otel_debug!(name: "NoMetricsCollected"); + return Ok(()); + } + + let metrics_count = rm.scope_metrics.iter().fold(0, |count, scope_metrics| { + count + scope_metrics.metrics.len() + }); + otel_debug!(name: "PeriodicReaderMetricsCollected", count = metrics_count, time_taken_in_millis = time_taken_for_collect.as_millis()); + + // Relying on futures executor to execute async call. + // TODO: Pass timeout to exporter + futures_executor::block_on(self.exporter.export(rm)) + } + + fn force_flush(&self) -> OTelSdkResult { + // TODO: Better message for this scenario. + // Flush and Shutdown called from 2 threads Flush check shutdown + // flag before shutdown thread sets it. Both threads attempt to send + // message to the same channel. Case1: Flush thread sends message first, + // shutdown thread sends message next. Flush would succeed, as + // background thread won't process shutdown message until flush + // triggered export is done. Case2: Shutdown thread sends message first, + // flush thread sends message next. Shutdown would succeed, as + // background thread would process shutdown message first. The + // background exits so it won't receive the flush message. ForceFlush + // returns Failure, but we could indicate specifically that shutdown has + // completed. TODO is to see if this message can be improved. + + let (response_tx, response_rx) = mpsc::channel(); + self.message_sender + .send(Message::Flush(response_tx)) + .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?; + + if let Ok(response) = response_rx.recv() { + // TODO: call exporter's force_flush method. + if response { + Ok(()) + } else { + Err(OTelSdkError::InternalFailure("Failed to flush".into())) + } + } else { + Err(OTelSdkError::InternalFailure("Failed to flush".into())) + } + } + + fn shutdown(&self) -> OTelSdkResult { + // TODO: See if this is better to be created upfront. + let (response_tx, response_rx) = mpsc::channel(); + self.message_sender + .send(Message::Shutdown(response_tx)) + .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?; + + // TODO: Make this timeout configurable. + match response_rx.recv_timeout(Duration::from_secs(5)) { + Ok(response) => { + if response { + Ok(()) + } else { + Err(OTelSdkError::InternalFailure("Failed to shutdown".into())) + } + } + Err(mpsc::RecvTimeoutError::Timeout) => { + Err(OTelSdkError::Timeout(Duration::from_secs(5))) + } + Err(mpsc::RecvTimeoutError::Disconnected) => { + Err(OTelSdkError::InternalFailure("Failed to shutdown".into())) + } + } + } +} + +#[derive(Debug)] +enum Message { + Flush(Sender), + Shutdown(Sender), +} + +impl MetricReader for PeriodicReader { + fn register_pipeline(&self, pipeline: Weak) { + self.inner.register_pipeline(pipeline); + } + + fn collect(&self, rm: &mut ResourceMetrics) -> OTelSdkResult { + self.inner.collect(rm) + } + + fn force_flush(&self) -> OTelSdkResult { + self.inner.force_flush() + } + + // TODO: Offer an async version of shutdown so users can await the shutdown + // completion, and avoid blocking the thread. The default shutdown on drop + // can still use blocking call. If user already explicitly called shutdown, + // drop won't call shutdown again. + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + self.inner.shutdown() + } + + /// To construct a [MetricReader][metric-reader] when setting up an SDK, + /// The output temporality (optional), a function of instrument kind. + /// This function SHOULD be obtained from the exporter. + /// + /// If not configured, the Cumulative temporality SHOULD be used. + /// + /// [metric-reader]: https://github.com/open-telemetry/opentelemetry-specification/blob/0a78571045ca1dca48621c9648ec3c832c3c541c/specification/metrics/sdk.md#metricreader + fn temporality(&self, kind: InstrumentKind) -> Temporality { + kind.temporality_preference(self.inner.temporality(kind)) + } +} + +#[cfg(all(test, feature = "testing"))] +mod tests { + use super::PeriodicReader; + use crate::{ + error::{OTelSdkError, OTelSdkResult}, + metrics::{ + data::ResourceMetrics, exporter::PushMetricExporter, reader::MetricReader, + InMemoryMetricExporter, SdkMeterProvider, Temporality, + }, + Resource, + }; + use opentelemetry::metrics::MeterProvider; + use std::{ + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + mpsc, Arc, + }, + time::Duration, + }; + + // use below command to run all tests + // cargo test metrics::periodic_reader::tests --features=testing,spec_unstable_metrics_views -- --nocapture + + #[derive(Debug, Clone)] + struct MetricExporterThatFailsOnlyOnFirst { + count: Arc, + } + + impl Default for MetricExporterThatFailsOnlyOnFirst { + fn default() -> Self { + MetricExporterThatFailsOnlyOnFirst { + count: Arc::new(AtomicUsize::new(0)), + } + } + } + + impl MetricExporterThatFailsOnlyOnFirst { + fn get_count(&self) -> usize { + self.count.load(Ordering::Relaxed) + } + } + + impl PushMetricExporter for MetricExporterThatFailsOnlyOnFirst { + async fn export(&self, _metrics: &ResourceMetrics) -> OTelSdkResult { + if self.count.fetch_add(1, Ordering::Relaxed) == 0 { + Err(OTelSdkError::InternalFailure("export failed".into())) + } else { + Ok(()) + } + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + Ok(()) + } + + fn temporality(&self) -> Temporality { + Temporality::Cumulative + } + } + + #[derive(Debug, Clone, Default)] + struct MockMetricExporter { + is_shutdown: Arc, + } + + impl PushMetricExporter for MockMetricExporter { + async fn export(&self, _metrics: &ResourceMetrics) -> OTelSdkResult { + Ok(()) + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + self.is_shutdown.store(true, Ordering::Relaxed); + Ok(()) + } + + fn temporality(&self) -> Temporality { + Temporality::Cumulative + } + } + + #[test] + fn collection_triggered_by_interval_multiple() { + // Arrange + let interval = std::time::Duration::from_millis(1); + let exporter = InMemoryMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone()) + .with_interval(interval) + .build(); + let i = Arc::new(AtomicUsize::new(0)); + let i_clone = i.clone(); + + // Act + let meter_provider = SdkMeterProvider::builder().with_reader(reader).build(); + let meter = meter_provider.meter("test"); + let _counter = meter + .u64_observable_counter("testcounter") + .with_callback(move |_| { + i_clone.fetch_add(1, Ordering::Relaxed); + }) + .build(); + + // Sleep for a duration 5X (plus liberal buffer to account for potential + // CI slowness) the interval to ensure multiple collection. + // Not a fan of such tests, but this seems to be the only way to test + // if periodic reader is doing its job. + // TODO: Decide if this should be ignored in CI + std::thread::sleep(interval * 5 * 20); + + // Assert + assert!(i.load(Ordering::Relaxed) >= 5); + } + + #[test] + fn shutdown_repeat() { + // Arrange + let exporter = InMemoryMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone()).build(); + + let meter_provider = SdkMeterProvider::builder().with_reader(reader).build(); + let result = meter_provider.shutdown(); + assert!(result.is_ok()); + + // calling shutdown again should return Err + let result = meter_provider.shutdown(); + assert!(result.is_err()); + assert!(matches!(result, Err(OTelSdkError::AlreadyShutdown))); + + // calling shutdown again should return Err + let result = meter_provider.shutdown(); + assert!(result.is_err()); + assert!(matches!(result, Err(OTelSdkError::AlreadyShutdown))); + } + + #[test] + fn flush_after_shutdown() { + // Arrange + let exporter = InMemoryMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone()).build(); + + let meter_provider = SdkMeterProvider::builder().with_reader(reader).build(); + let result = meter_provider.force_flush(); + assert!(result.is_ok()); + + let result = meter_provider.shutdown(); + assert!(result.is_ok()); + + // calling force_flush after shutdown should return Err + let result = meter_provider.force_flush(); + assert!(result.is_err()); + } + + #[test] + fn flush_repeat() { + // Arrange + let exporter = InMemoryMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone()).build(); + + let meter_provider = SdkMeterProvider::builder().with_reader(reader).build(); + let result = meter_provider.force_flush(); + assert!(result.is_ok()); + + // calling force_flush again should return Ok + let result = meter_provider.force_flush(); + assert!(result.is_ok()); + } + + #[test] + fn periodic_reader_without_pipeline() { + // Arrange + let exporter = InMemoryMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone()).build(); + + let rm = &mut ResourceMetrics { + resource: Resource::empty(), + scope_metrics: Vec::new(), + }; + // Pipeline is not registered, so collect should return an error + let result = reader.collect(rm); + assert!(result.is_err()); + + // Pipeline is not registered, so flush should return an error + let result = reader.force_flush(); + assert!(result.is_err()); + + // Adding reader to meter provider should register the pipeline + // TODO: This part might benefit from a different design. + let meter_provider = SdkMeterProvider::builder() + .with_reader(reader.clone()) + .build(); + + // Now collect and flush should succeed + let result = reader.collect(rm); + assert!(result.is_ok()); + + let result = meter_provider.force_flush(); + assert!(result.is_ok()); + } + + #[test] + fn exporter_failures_are_handled() { + // create a mock exporter that fails 1st time and succeeds 2nd time + // Validate using this exporter that periodic reader can handle exporter failure + // and continue to export metrics. + // Arrange + let interval = std::time::Duration::from_millis(10); + let exporter = MetricExporterThatFailsOnlyOnFirst::default(); + let reader = PeriodicReader::builder(exporter.clone()) + .with_interval(interval) + .build(); + + let meter_provider = SdkMeterProvider::builder().with_reader(reader).build(); + let meter = meter_provider.meter("test"); + let counter = meter.u64_counter("sync_counter").build(); + counter.add(1, &[]); + let _obs_counter = meter + .u64_observable_counter("testcounter") + .with_callback(move |observer| { + observer.observe(1, &[]); + }) + .build(); + + // Sleep for a duration much longer than the interval to trigger + // multiple exports, including failures. + // Not a fan of such tests, but this seems to be the + // only way to test if periodic reader is doing its job. TODO: Decide if + // this should be ignored in CI + std::thread::sleep(Duration::from_millis(500)); + + // Assert that atleast 2 exports are attempted given the 1st one fails. + assert!(exporter.get_count() >= 2); + } + + #[test] + fn shutdown_passed_to_exporter() { + // Arrange + let exporter = MockMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone()).build(); + + let meter_provider = SdkMeterProvider::builder().with_reader(reader).build(); + let meter = meter_provider.meter("test"); + let counter = meter.u64_counter("sync_counter").build(); + counter.add(1, &[]); + + // shutdown the provider, which should call shutdown on periodic reader + // which in turn should call shutdown on exporter. + let result = meter_provider.shutdown(); + assert!(result.is_ok()); + assert!(exporter.is_shutdown.load(Ordering::Relaxed)); + } + + #[test] + fn collection() { + collection_triggered_by_interval_helper(); + collection_triggered_by_flush_helper(); + collection_triggered_by_shutdown_helper(); + collection_triggered_by_drop_helper(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn collection_from_tokio_multi_with_one_worker() { + collection_triggered_by_interval_helper(); + collection_triggered_by_flush_helper(); + collection_triggered_by_shutdown_helper(); + collection_triggered_by_drop_helper(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn collection_from_tokio_with_two_worker() { + collection_triggered_by_interval_helper(); + collection_triggered_by_flush_helper(); + collection_triggered_by_shutdown_helper(); + collection_triggered_by_drop_helper(); + } + + #[tokio::test(flavor = "current_thread")] + async fn collection_from_tokio_current() { + collection_triggered_by_interval_helper(); + collection_triggered_by_flush_helper(); + collection_triggered_by_shutdown_helper(); + collection_triggered_by_drop_helper(); + } + + fn collection_triggered_by_interval_helper() { + collection_helper(|_| { + // Sleep for a duration longer than the interval to ensure at least one collection + // Not a fan of such tests, but this seems to be the only way to test + // if periodic reader is doing its job. + // TODO: Decide if this should be ignored in CI + std::thread::sleep(Duration::from_millis(500)); + }); + } + + fn collection_triggered_by_flush_helper() { + collection_helper(|meter_provider| { + meter_provider.force_flush().expect("flush should succeed"); + }); + } + + fn collection_triggered_by_shutdown_helper() { + collection_helper(|meter_provider| { + meter_provider.shutdown().expect("shutdown should succeed"); + }); + } + + fn collection_triggered_by_drop_helper() { + collection_helper(|meter_provider| { + drop(meter_provider); + }); + } + + fn collection_helper(trigger: fn(SdkMeterProvider)) { + // Arrange + let exporter = InMemoryMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone()).build(); + let (sender, receiver) = mpsc::channel(); + + let meter_provider = SdkMeterProvider::builder().with_reader(reader).build(); + let meter = meter_provider.meter("test"); + let _counter = meter + .u64_observable_counter("testcounter") + .with_callback(move |observer| { + observer.observe(1, &[]); + sender.send(()).expect("channel should still be open"); + }) + .build(); + + // Act + trigger(meter_provider); + + // Assert + receiver + .recv_timeout(Duration::ZERO) + .expect("message should be available in channel, indicating a collection occurred, which should trigger observable callback"); + + let exported_metrics = exporter + .get_finished_metrics() + .expect("this should not fail"); + assert!( + !exported_metrics.is_empty(), + "Metrics should be available in exporter." + ); + } + + async fn some_async_function() -> u64 { + // No dependency on any particular async runtime. + std::thread::sleep(std::time::Duration::from_millis(1)); + 1 + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn async_inside_observable_callback_from_tokio_multi_with_one_worker() { + async_inside_observable_callback_helper(); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn async_inside_observable_callback_from_tokio_multi_with_two_worker() { + async_inside_observable_callback_helper(); + } + + #[tokio::test(flavor = "current_thread")] + async fn async_inside_observable_callback_from_tokio_current_thread() { + async_inside_observable_callback_helper(); + } + + #[test] + fn async_inside_observable_callback_from_regular_main() { + async_inside_observable_callback_helper(); + } + + fn async_inside_observable_callback_helper() { + let interval = std::time::Duration::from_millis(10); + let exporter = InMemoryMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone()) + .with_interval(interval) + .build(); + + let meter_provider = SdkMeterProvider::builder().with_reader(reader).build(); + let meter = meter_provider.meter("test"); + let _gauge = meter + .u64_observable_gauge("my_observable_gauge") + .with_callback(|observer| { + // using futures_executor::block_on intentionally and avoiding + // any particular async runtime. + let value = futures_executor::block_on(some_async_function()); + observer.observe(value, &[]); + }) + .build(); + + meter_provider.force_flush().expect("flush should succeed"); + let exported_metrics = exporter + .get_finished_metrics() + .expect("this should not fail"); + assert!( + !exported_metrics.is_empty(), + "Metrics should be available in exporter." + ); + } + + async fn some_tokio_async_function() -> u64 { + // Tokio specific async function + tokio::time::sleep(Duration::from_millis(1)).await; + 1 + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + + async fn tokio_async_inside_observable_callback_from_tokio_multi_with_one_worker() { + tokio_async_inside_observable_callback_helper(true); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn tokio_async_inside_observable_callback_from_tokio_multi_with_two_worker() { + tokio_async_inside_observable_callback_helper(true); + } + + #[tokio::test(flavor = "current_thread")] + #[ignore] //TODO: Investigate if this can be fixed. + async fn tokio_async_inside_observable_callback_from_tokio_current_thread() { + tokio_async_inside_observable_callback_helper(true); + } + + #[test] + fn tokio_async_inside_observable_callback_from_regular_main() { + tokio_async_inside_observable_callback_helper(false); + } + + fn tokio_async_inside_observable_callback_helper(use_current_tokio_runtime: bool) { + let exporter = InMemoryMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone()).build(); + + let meter_provider = SdkMeterProvider::builder().with_reader(reader).build(); + let meter = meter_provider.meter("test"); + + if use_current_tokio_runtime { + let rt = tokio::runtime::Handle::current().clone(); + let _gauge = meter + .u64_observable_gauge("my_observable_gauge") + .with_callback(move |observer| { + // call tokio specific async function from here + let value = rt.block_on(some_tokio_async_function()); + observer.observe(value, &[]); + }) + .build(); + // rt here is a reference to the current tokio runtime. + // Dropping it occurs when the tokio::main itself ends. + } else { + let rt = tokio::runtime::Runtime::new().unwrap(); + let _gauge = meter + .u64_observable_gauge("my_observable_gauge") + .with_callback(move |observer| { + // call tokio specific async function from here + let value = rt.block_on(some_tokio_async_function()); + observer.observe(value, &[]); + }) + .build(); + // rt is not dropped here as it is moved to the closure, + // and is dropped only when MeterProvider itself is dropped. + // This works when called from normal main. + }; + + meter_provider.force_flush().expect("flush should succeed"); + let exported_metrics = exporter + .get_finished_metrics() + .expect("this should not fail"); + assert!( + !exported_metrics.is_empty(), + "Metrics should be available in exporter." + ); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/periodic_reader_with_async_runtime.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/periodic_reader_with_async_runtime.rs new file mode 100644 index 00000000..9e94701a --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/periodic_reader_with_async_runtime.rs @@ -0,0 +1,538 @@ +use std::{ + env, fmt, mem, + sync::{Arc, Mutex, Weak}, + time::Duration, +}; + +use futures_channel::{mpsc, oneshot}; +use futures_util::{ + future::{self, Either}, + pin_mut, + stream::{self, FusedStream}, + StreamExt, +}; +use opentelemetry::{otel_debug, otel_error}; + +use crate::runtime::{to_interval_stream, Runtime}; +use crate::{ + error::{OTelSdkError, OTelSdkResult}, + metrics::{exporter::PushMetricExporter, reader::SdkProducer}, + Resource, +}; + +use super::{ + data::ResourceMetrics, instrument::InstrumentKind, pipeline::Pipeline, reader::MetricReader, +}; + +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); +const DEFAULT_INTERVAL: Duration = Duration::from_secs(60); + +const METRIC_EXPORT_INTERVAL_NAME: &str = "OTEL_METRIC_EXPORT_INTERVAL"; +const METRIC_EXPORT_TIMEOUT_NAME: &str = "OTEL_METRIC_EXPORT_TIMEOUT"; + +/// Configuration options for [PeriodicReader]. +/// +/// A periodic reader is a [MetricReader] that collects and exports metric data +/// to the exporter at a defined interval. +/// +/// By default, the returned [MetricReader] will collect and export data every +/// 60 seconds, and will cancel export attempts that exceed 30 seconds. The +/// export time is not counted towards the interval between attempts. +/// +/// The [collect] method of the returned [MetricReader] continues to gather and +/// return metric data to the user. It will not automatically send that data to +/// the exporter outside of the predefined interval. +/// +/// [collect]: MetricReader::collect +#[derive(Debug)] +pub struct PeriodicReaderBuilder { + interval: Duration, + timeout: Duration, + exporter: E, + runtime: RT, +} + +impl PeriodicReaderBuilder +where + E: PushMetricExporter, + RT: Runtime, +{ + fn new(exporter: E, runtime: RT) -> Self { + let interval = env::var(METRIC_EXPORT_INTERVAL_NAME) + .ok() + .and_then(|v| v.parse().map(Duration::from_millis).ok()) + .unwrap_or(DEFAULT_INTERVAL); + let timeout = env::var(METRIC_EXPORT_TIMEOUT_NAME) + .ok() + .and_then(|v| v.parse().map(Duration::from_millis).ok()) + .unwrap_or(DEFAULT_TIMEOUT); + + PeriodicReaderBuilder { + interval, + timeout, + exporter, + runtime, + } + } + + /// Configures the intervening time between exports for a [PeriodicReader]. + /// + /// This option overrides any value set for the `OTEL_METRIC_EXPORT_INTERVAL` + /// environment variable. + /// + /// If this option is not used or `interval` is equal to zero, 60 seconds is + /// used as the default. + pub fn with_interval(mut self, interval: Duration) -> Self { + if !interval.is_zero() { + self.interval = interval; + } + self + } + + /// Configures the time a [PeriodicReader] waits for an export to complete + /// before canceling it. + /// + /// This option overrides any value set for the `OTEL_METRIC_EXPORT_TIMEOUT` + /// environment variable. + /// + /// If this option is not used or `timeout` is equal to zero, 30 seconds is used + /// as the default. + pub fn with_timeout(mut self, timeout: Duration) -> Self { + if !timeout.is_zero() { + self.timeout = timeout; + } + self + } + + /// Create a [PeriodicReader] with the given config. + pub fn build(self) -> PeriodicReader { + let (message_sender, message_receiver) = mpsc::channel(256); + + let worker = move |reader: &PeriodicReader| { + let runtime = self.runtime.clone(); + let reader = reader.clone(); + self.runtime.spawn(async move { + let ticker = to_interval_stream(runtime.clone(), self.interval) + .skip(1) // The ticker is fired immediately, so we should skip the first one to align with the interval. + .map(|_| Message::Export); + let messages = Box::pin(stream::select(message_receiver, ticker)); + PeriodicReaderWorker { + reader, + timeout: self.timeout, + runtime, + rm: ResourceMetrics { + resource: Resource::empty(), + scope_metrics: Vec::new(), + }, + } + .run(messages) + .await + }); + }; + + otel_debug!( + name: "PeriodicReader.BuildCompleted", + message = "Periodic reader built.", + interval_in_secs = self.interval.as_secs(), + temporality = format!("{:?}", self.exporter.temporality()), + ); + + PeriodicReader { + exporter: Arc::new(self.exporter), + inner: Arc::new(Mutex::new(PeriodicReaderInner { + message_sender, + is_shutdown: false, + sdk_producer_or_worker: ProducerOrWorker::Worker(Box::new(worker)), + })), + } + } +} + +/// A [MetricReader] that continuously collects and exports metric data at a set +/// interval. +/// +/// By default it will collect and export data every 60 seconds, and will cancel +/// export attempts that exceed 30 seconds. The export time is not counted +/// towards the interval between attempts. +/// +/// The [collect] method of the returned continues to gather and +/// return metric data to the user. It will not automatically send that data to +/// the exporter outside of the predefined interval. +/// +/// The [runtime] can be selected based on feature flags set for this crate. +/// +/// The exporter can be any exporter that implements [PushMetricExporter] such +/// as [opentelemetry-otlp]. Exporters that require a runtime should be used +/// with a compatible [runtime]. +/// +/// [collect]: MetricReader::collect +/// [runtime]: crate::runtime +/// [opentelemetry-otlp]: https://docs.rs/opentelemetry-otlp/latest/opentelemetry_otlp/ +/// +/// # Example +/// +/// ```no_run +/// use opentelemetry_sdk::metrics::periodic_reader_with_async_runtime::PeriodicReader; +/// # fn example(get_exporter: impl Fn() -> E, get_runtime: impl Fn() -> R) +/// # where +/// # E: opentelemetry_sdk::metrics::exporter::PushMetricExporter, +/// # R: opentelemetry_sdk::runtime::Runtime, +/// # { +/// +/// let exporter = get_exporter(); // set up a push exporter like OTLP +/// let runtime = get_runtime(); // select runtime: e.g. opentelemetry_sdk:runtime::Tokio +/// +/// let reader = PeriodicReader::builder(exporter, runtime).build(); +/// # drop(reader); +/// # } +/// ``` +pub struct PeriodicReader { + exporter: Arc, + inner: Arc>>, +} + +impl Clone for PeriodicReader { + fn clone(&self) -> Self { + Self { + exporter: Arc::clone(&self.exporter), + inner: Arc::clone(&self.inner), + } + } +} + +impl PeriodicReader { + /// Configuration options for a periodic reader + pub fn builder(exporter: E, runtime: RT) -> PeriodicReaderBuilder + where + RT: Runtime, + { + PeriodicReaderBuilder::new(exporter, runtime) + } +} + +impl fmt::Debug for PeriodicReader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PeriodicReader").finish() + } +} + +struct PeriodicReaderInner { + message_sender: mpsc::Sender, + is_shutdown: bool, + sdk_producer_or_worker: ProducerOrWorker, +} + +#[derive(Debug)] +enum Message { + Export, + Flush(oneshot::Sender), + Shutdown(oneshot::Sender), +} + +enum ProducerOrWorker { + Producer(Weak), + #[allow(clippy::type_complexity)] + Worker(Box) + Send + Sync>), +} + +struct PeriodicReaderWorker { + reader: PeriodicReader, + timeout: Duration, + runtime: RT, + rm: ResourceMetrics, +} + +impl PeriodicReaderWorker { + async fn collect_and_export(&mut self) -> OTelSdkResult { + self.reader + .collect(&mut self.rm) + .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?; + if self.rm.scope_metrics.is_empty() { + otel_debug!( + name: "PeriodicReaderWorker.NoMetricsToExport", + ); + // No metrics to export. + return Ok(()); + } + + otel_debug!( + name: "PeriodicReaderWorker.InvokeExporter", + message = "Calling exporter's export method with collected metrics.", + count = self.rm.scope_metrics.len(), + ); + let export = self.reader.exporter.export(&self.rm); + let timeout = self.runtime.delay(self.timeout); + pin_mut!(export); + pin_mut!(timeout); + + match future::select(export, timeout).await { + Either::Left((res, _)) => { + res // return the status of export. + } + Either::Right(_) => Err(OTelSdkError::Timeout(self.timeout)), + } + } + + async fn process_message(&mut self, message: Message) -> bool { + match message { + Message::Export => { + otel_debug!( + name: "PeriodicReader.ExportTriggered", + message = "Export message received.", + ); + if let Err(err) = self.collect_and_export().await { + otel_error!( + name: "PeriodicReader.ExportFailed", + message = "Failed to export metrics", + reason = format!("{}", err)); + } + } + Message::Flush(ch) => { + otel_debug!( + name: "PeriodicReader.ForceFlushCalled", + message = "Flush message received.", + ); + let res = self.collect_and_export().await; + if let Err(send_error) = ch.send(res) { + otel_debug!( + name: "PeriodicReader.Flush.SendResultError", + message = "Failed to send flush result.", + reason = format!("{:?}", send_error), + ); + } + } + Message::Shutdown(ch) => { + otel_debug!( + name: "PeriodicReader.ShutdownCalled", + message = "Shutdown message received", + ); + let res = self.collect_and_export().await; + let _ = self.reader.exporter.shutdown(); + if let Err(send_error) = + ch.send(res.map_err(|e| OTelSdkError::InternalFailure(e.to_string()))) + { + otel_debug!( + name: "PeriodicReader.Shutdown.SendResultError", + message = "Failed to send shutdown result", + reason = format!("{:?}", send_error), + ); + } + return false; + } + } + + true + } + + async fn run(mut self, mut messages: impl FusedStream + Unpin) { + while let Some(message) = messages.next().await { + if !self.process_message(message).await { + break; + } + } + } +} + +impl MetricReader for PeriodicReader { + fn register_pipeline(&self, pipeline: Weak) { + let mut inner = match self.inner.lock() { + Ok(guard) => guard, + Err(_) => return, + }; + + let worker = match &mut inner.sdk_producer_or_worker { + ProducerOrWorker::Producer(_) => { + // Only register once. If producer is already set, do nothing. + otel_debug!(name: "PeriodicReader.DuplicateRegistration", + message = "duplicate registration found, did not register periodic reader."); + return; + } + ProducerOrWorker::Worker(w) => mem::replace(w, Box::new(|_| {})), + }; + + inner.sdk_producer_or_worker = ProducerOrWorker::Producer(pipeline); + worker(self); + } + + fn collect(&self, rm: &mut ResourceMetrics) -> OTelSdkResult { + let inner = self + .inner + .lock() + .map_err(|_| OTelSdkError::InternalFailure("Failed to lock pipeline".into()))?; + + if inner.is_shutdown { + return Err(OTelSdkError::AlreadyShutdown); + } + + if let Some(producer) = match &inner.sdk_producer_or_worker { + ProducerOrWorker::Producer(sdk_producer) => sdk_producer.upgrade(), + ProducerOrWorker::Worker(_) => None, + } { + producer.produce(rm)?; + } else { + return Err(OTelSdkError::InternalFailure( + "reader is not registered".into(), + )); + } + + Ok(()) + } + + fn force_flush(&self) -> OTelSdkResult { + let mut inner = self + .inner + .lock() + .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?; + if inner.is_shutdown { + return Err(OTelSdkError::AlreadyShutdown); + } + let (sender, receiver) = oneshot::channel(); + inner + .message_sender + .try_send(Message::Flush(sender)) + .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?; + + drop(inner); // don't hold lock when blocking on future + + futures_executor::block_on(receiver) + .map_err(|err| OTelSdkError::InternalFailure(err.to_string())) + .and_then(|res| res) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + let mut inner = self + .inner + .lock() + .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?; + if inner.is_shutdown { + return Err(OTelSdkError::AlreadyShutdown); + } + + let (sender, receiver) = oneshot::channel(); + inner + .message_sender + .try_send(Message::Shutdown(sender)) + .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?; + drop(inner); // don't hold lock when blocking on future + + let shutdown_result = futures_executor::block_on(receiver) + .map_err(|err| OTelSdkError::InternalFailure(err.to_string()))?; + + // Acquire the lock again to set the shutdown flag + let mut inner = self + .inner + .lock() + .map_err(|e| OTelSdkError::InternalFailure(e.to_string()))?; + inner.is_shutdown = true; + + shutdown_result + } + + /// To construct a [MetricReader][metric-reader] when setting up an SDK, + /// The output temporality (optional), a function of instrument kind. + /// This function SHOULD be obtained from the exporter. + /// + /// If not configured, the Cumulative temporality SHOULD be used. + /// + /// [metric-reader]: https://github.com/open-telemetry/opentelemetry-specification/blob/0a78571045ca1dca48621c9648ec3c832c3c541c/specification/metrics/sdk.md#metricreader + fn temporality(&self, kind: InstrumentKind) -> super::Temporality { + kind.temporality_preference(self.exporter.temporality()) + } +} + +#[cfg(all(test, feature = "testing"))] +mod tests { + use super::PeriodicReader; + use crate::error::OTelSdkError; + use crate::metrics::reader::MetricReader; + use crate::{ + metrics::data::ResourceMetrics, metrics::InMemoryMetricExporter, metrics::SdkMeterProvider, + runtime, Resource, + }; + use opentelemetry::metrics::MeterProvider; + use std::sync::mpsc; + + #[test] + fn collection_triggered_by_interval_tokio_current() { + collection_triggered_by_interval_helper(runtime::TokioCurrentThread); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn collection_triggered_by_interval_from_tokio_multi_one_thread_on_runtime_tokio() { + collection_triggered_by_interval_helper(runtime::Tokio); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn collection_triggered_by_interval_from_tokio_multi_two_thread_on_runtime_tokio() { + collection_triggered_by_interval_helper(runtime::Tokio); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn collection_triggered_by_interval_from_tokio_multi_one_thread_on_runtime_tokio_current() + { + collection_triggered_by_interval_helper(runtime::TokioCurrentThread); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn collection_triggered_by_interval_from_tokio_multi_two_thread_on_runtime_tokio_current() + { + collection_triggered_by_interval_helper(runtime::TokioCurrentThread); + } + + #[tokio::test(flavor = "current_thread")] + #[ignore = "See issue https://github.com/open-telemetry/opentelemetry-rust/issues/2056"] + async fn collection_triggered_by_interval_from_tokio_current_on_runtime_tokio() { + collection_triggered_by_interval_helper(runtime::Tokio); + } + + #[tokio::test(flavor = "current_thread")] + async fn collection_triggered_by_interval_from_tokio_current_on_runtime_tokio_current() { + collection_triggered_by_interval_helper(runtime::TokioCurrentThread); + } + + #[test] + fn unregistered_collect() { + // Arrange + let exporter = InMemoryMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone(), runtime::Tokio).build(); + let mut rm = ResourceMetrics { + resource: Resource::empty(), + scope_metrics: Vec::new(), + }; + + // Act + let result = reader.collect(&mut rm); + + // Assert + assert!( + matches!(result.unwrap_err(), OTelSdkError::InternalFailure(err) if err == "reader is not registered") + ); + } + + fn collection_triggered_by_interval_helper(runtime: RT) + where + RT: crate::runtime::Runtime, + { + let interval = std::time::Duration::from_millis(1); + let exporter = InMemoryMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone(), runtime) + .with_interval(interval) + .build(); + let (sender, receiver) = mpsc::channel(); + + // Act + let meter_provider = SdkMeterProvider::builder().with_reader(reader).build(); + let meter = meter_provider.meter("test"); + let _counter = meter + .u64_observable_counter("testcounter") + .with_callback(move |_| { + sender.send(()).expect("channel should still be open"); + }) + .build(); + + // Assert + receiver + .recv() + .expect("message should be available in channel, indicating a collection occurred"); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/pipeline.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/pipeline.rs new file mode 100644 index 00000000..b8e8ebdd --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/pipeline.rs @@ -0,0 +1,774 @@ +use core::fmt; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, + sync::{Arc, Mutex}, +}; + +use opentelemetry::{otel_debug, otel_warn, InstrumentationScope, KeyValue}; + +use crate::{ + error::{OTelSdkError, OTelSdkResult}, + metrics::{ + aggregation, + data::{Metric, ResourceMetrics, ScopeMetrics}, + error::{MetricError, MetricResult}, + instrument::{Instrument, InstrumentId, InstrumentKind, Stream}, + internal::{self, AggregateBuilder, Number}, + reader::{MetricReader, SdkProducer}, + view::View, + }, + Resource, +}; + +use self::internal::AggregateFns; + +use super::{aggregation::Aggregation, Temporality}; + +/// Connects all of the instruments created by a meter provider to a [MetricReader]. +/// +/// This is the object that will be registered when a meter provider is +/// created. +/// +/// As instruments are created the instrument should be checked if it exists in +/// the views of a the reader, and if so each aggregate function should be added +/// to the pipeline. +#[doc(hidden)] +pub struct Pipeline { + pub(crate) resource: Resource, + reader: Box, + views: Vec>, + inner: Mutex, +} + +impl fmt::Debug for Pipeline { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Pipeline") + } +} + +/// Single or multi-instrument callbacks +type GenericCallback = Arc; + +const DEFAULT_CARDINALITY_LIMIT: usize = 2000; + +#[derive(Default)] +struct PipelineInner { + aggregations: HashMap>, + callbacks: Vec, +} + +impl fmt::Debug for PipelineInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PipelineInner") + .field("aggregations", &self.aggregations) + .field("callbacks", &self.callbacks.len()) + .finish() + } +} + +impl Pipeline { + /// Adds the [InstrumentSync] to pipeline with scope. + /// + /// This method is not idempotent. Duplicate calls will result in duplicate + /// additions, it is the callers responsibility to ensure this is called with + /// unique values. + fn add_sync(&self, scope: InstrumentationScope, i_sync: InstrumentSync) { + let _ = self.inner.lock().map(|mut inner| { + inner.aggregations.entry(scope).or_default().push(i_sync); + }); + } + + /// Registers a single instrument callback to be run when `produce` is called. + fn add_callback(&self, callback: GenericCallback) { + let _ = self + .inner + .lock() + .map(|mut inner| inner.callbacks.push(callback)); + } + + /// Send accumulated telemetry + fn force_flush(&self) -> OTelSdkResult { + self.reader.force_flush() + } + + /// Shut down pipeline + fn shutdown(&self) -> OTelSdkResult { + self.reader.shutdown() + } +} + +impl SdkProducer for Pipeline { + /// Returns aggregated metrics from a single collection. + fn produce(&self, rm: &mut ResourceMetrics) -> OTelSdkResult { + let inner = self + .inner + .lock() + .map_err(|_| OTelSdkError::InternalFailure("Failed to lock pipeline".into()))?; + otel_debug!( + name: "MeterProviderInvokingObservableCallbacks", + count = inner.callbacks.len(), + ); + for cb in &inner.callbacks { + // TODO consider parallel callbacks. + cb(); + } + + rm.resource = self.resource.clone(); + if inner.aggregations.len() > rm.scope_metrics.len() { + rm.scope_metrics + .reserve(inner.aggregations.len() - rm.scope_metrics.len()); + } + + let mut i = 0; + for (scope, instruments) in inner.aggregations.iter() { + let sm = match rm.scope_metrics.get_mut(i) { + Some(sm) => sm, + None => { + rm.scope_metrics.push(ScopeMetrics::default()); + rm.scope_metrics.last_mut().unwrap() + } + }; + if instruments.len() > sm.metrics.len() { + sm.metrics.reserve(instruments.len() - sm.metrics.len()); + } + + let mut j = 0; + for inst in instruments { + let mut m = sm.metrics.get_mut(j); + match (inst.comp_agg.call(m.as_mut().map(|m| &mut m.data)), m) { + // No metric to re-use, expect agg to create new metric data + ((len, Some(initial_agg)), None) if len > 0 => sm.metrics.push(Metric { + name: inst.name.clone(), + description: inst.description.clone(), + unit: inst.unit.clone(), + data: initial_agg, + }), + // Existing metric can be re-used, update its values + ((len, data), Some(prev_agg)) if len > 0 => { + if let Some(data) = data { + // previous aggregation was of a different type + prev_agg.data = data; + } + prev_agg.name.clone_from(&inst.name); + prev_agg.description.clone_from(&inst.description); + prev_agg.unit.clone_from(&inst.unit); + } + _ => continue, + } + + j += 1; + } + + sm.metrics.truncate(j); + if !sm.metrics.is_empty() { + sm.scope = scope.clone(); + i += 1; + } + } + + rm.scope_metrics.truncate(i); + + Ok(()) + } +} + +/// A synchronization point between a [Pipeline] and an instrument's aggregate function. +struct InstrumentSync { + name: Cow<'static, str>, + description: Cow<'static, str>, + unit: Cow<'static, str>, + comp_agg: Arc, +} + +impl fmt::Debug for InstrumentSync { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InstrumentSync") + .field("name", &self.name) + .field("description", &self.description) + .field("unit", &self.unit) + .finish() + } +} + +type Cache = Mutex>>>>>; + +/// Facilitates inserting of new instruments from a single scope into a pipeline. +struct Inserter { + /// A cache that holds aggregate function inputs whose + /// outputs have been inserted into the underlying reader pipeline. + /// + /// This cache ensures no duplicate aggregate functions are inserted into + /// the reader pipeline and if a new request during an instrument creation + /// asks for the same aggregate function input the same instance is + /// returned. + aggregators: Cache, + + /// A cache that holds instrument identifiers for all the instruments a [Meter] has + /// created. + /// + /// It is provided from the `Meter` that owns this inserter. This cache ensures + /// that during the creation of instruments with the same name but different + /// options (e.g. description, unit) a warning message is logged. + views: Arc, InstrumentId>>>, + + pipeline: Arc, +} + +impl Inserter +where + T: Number, +{ + fn new(p: Arc, vc: Arc, InstrumentId>>>) -> Self { + Inserter { + aggregators: Default::default(), + views: vc, + pipeline: Arc::clone(&p), + } + } + + /// Inserts the provided instrument into a pipeline. + /// + /// All views the pipeline contains are matched against, and any matching view + /// that creates a unique [Aggregator] will be inserted into the pipeline and + /// included in the returned list. + /// + /// The returned aggregate functions are ensured to be deduplicated and unique. + /// If another view in another pipeline that is cached by this inserter's cache + /// has already inserted the same aggregate function for the same instrument, + /// that function's instance is returned. + /// + /// If another instrument has already been inserted by this inserter, or any + /// other using the same cache, and it conflicts with the instrument being + /// inserted in this call, an aggregate function matching the arguments will + /// still be returned but a log message will also be logged to the OTel global + /// logger. + /// + /// If the passed instrument would result in an incompatible aggregate function, + /// an error is returned and that aggregate function is not inserted or + /// returned. + /// + /// If an instrument is determined to use a [aggregation::Aggregation::Drop], + /// that instrument is not inserted nor returned. + fn instrument( + &self, + inst: Instrument, + boundaries: Option<&[f64]>, + ) -> MetricResult>>> { + let mut matched = false; + let mut measures = vec![]; + let mut errs = vec![]; + let kind = inst.kind; + + // The cache will return the same Aggregator instance. Use stream ids to de duplicate. + let mut seen = HashSet::new(); + for v in &self.pipeline.views { + let mut stream = match v.match_inst(&inst) { + Some(stream) => stream, + None => continue, + }; + matched = true; + + if stream.name.is_none() { + stream.name = Some(inst.name.clone()); + } + if stream.description.is_none() { + stream.description = Some(inst.description.clone()); + } + if stream.unit.is_none() { + stream.unit = Some(inst.unit.clone()); + } + + // Override default histogram boundaries if provided. + if let Some(boundaries) = boundaries { + if kind == InstrumentKind::Histogram && stream.aggregation.is_none() { + stream.aggregation = Some(Aggregation::ExplicitBucketHistogram { + boundaries: boundaries.to_vec(), + record_min_max: true, + }); + } + } + + let id = self.inst_id(kind, &stream); + if seen.contains(&id) { + continue; // This aggregator has already been added + } + + let agg = match self.cached_aggregator(&inst.scope, kind, stream) { + Ok(Some(agg)) => agg, + Ok(None) => continue, // Drop aggregator. + Err(err) => { + errs.push(err); + continue; + } + }; + seen.insert(id); + measures.push(agg); + } + + // If at least one view matched, check the outcome: + // - If we have aggregators, return them + // - If all views resulted in Drop (measures empty, no errors), return empty (instrument is dropped) + // - If all views failed with errors, fall through to default aggregation per spec + if matched && (errs.is_empty() || !measures.is_empty()) { + return Ok(measures); + } + + // Log warning if views matched but all failed + if matched && !errs.is_empty() { + otel_warn!( + name: "ViewAggregationIncompatible", + message = "All matching views have incompatible aggregation. Falling back to default aggregation.", + instrument_name = inst.name.as_ref(), + errors = format!("{errs:?}").as_str(), + ); + } + + // Apply implicit default view if no explicit matched, or if all views failed. + let mut stream = Stream { + name: Some(inst.name), + description: Some(inst.description), + unit: Some(inst.unit), + aggregation: None, + allowed_attribute_keys: None, + cardinality_limit: None, + }; + + // Override default histogram boundaries if provided. + if let Some(boundaries) = boundaries { + stream.aggregation = Some(Aggregation::ExplicitBucketHistogram { + boundaries: boundaries.to_vec(), + record_min_max: true, + }); + } + + match self.cached_aggregator(&inst.scope, kind, stream) { + Ok(agg) => { + if let Some(agg) = agg { + measures.push(agg); + } + Ok(measures) + } + Err(err) => { + errs.push(err); + Err(MetricError::Other(format!("{errs:?}"))) + } + } + } + + /// Returns the appropriate aggregate functions for an instrument configuration. + /// + /// If the exact instrument has been created within the [Scope], that + /// aggregate function instance will be returned. Otherwise, a new computed + /// aggregate function will be cached and returned. + /// + /// If the instrument configuration conflicts with an instrument that has + /// already been created (e.g. description, unit, data type) a warning will be + /// logged with the global OTel logger. A valid new aggregate function for the + /// instrument configuration will still be returned without an error. + /// + /// If the instrument defines an unknown or incompatible aggregation, an error + /// is returned. + fn cached_aggregator( + &self, + scope: &InstrumentationScope, + kind: InstrumentKind, + mut stream: Stream, + ) -> MetricResult>>> { + // TODO: Create a separate pub (crate) Stream struct for the pipeline, + // as Stream will not have any optional fields as None at this point and + // new struct can better reflect this. + let mut agg = stream + .aggregation + .take() + .unwrap_or_else(|| default_aggregation_selector(kind)); + + // Apply default if stream or reader aggregation returns default + if matches!(agg, aggregation::Aggregation::Default) { + agg = default_aggregation_selector(kind); + } + + if let Err(err) = is_aggregator_compatible(&kind, &agg) { + return Err(MetricError::Other(format!( + "creating aggregator with instrumentKind: {:?}, aggregation {:?}: {:?}", + kind, stream.aggregation, err, + ))); + } + + let mut id = self.inst_id(kind, &stream); + // If there is a conflict, the specification says the view should + // still be applied and a warning should be logged. + self.log_conflict(&id); + + // If there are requests for the same instrument with different name + // casing, the first-seen needs to be returned. Use a normalize ID for the + // cache lookup to ensure the correct comparison. + id.normalize(); + + let mut cache = self.aggregators.lock()?; + + let cached = cache.entry(id).or_insert_with(|| { + let filter = stream + .allowed_attribute_keys + .clone() + .map(|allowed| Arc::new(move |kv: &KeyValue| allowed.contains(&kv.key)) as Arc<_>); + + let cardinality_limit = stream + .cardinality_limit + .unwrap_or(DEFAULT_CARDINALITY_LIMIT); + let b = AggregateBuilder::new( + self.pipeline.reader.temporality(kind), + filter, + cardinality_limit, + ); + let AggregateFns { measure, collect } = match aggregate_fn(b, &agg, kind) { + Ok(Some(inst)) => inst, + other => return other.map(|fs| fs.map(|inst| inst.measure)), // Drop aggregator or error + }; + + otel_debug!( + name : "Metrics.InstrumentCreated", + instrument_name = stream.name.clone().unwrap_or_default().as_ref(), + cardinality_limit = cardinality_limit, + ); + + self.pipeline.add_sync( + scope.clone(), + InstrumentSync { + name: stream.name.unwrap_or_default(), + description: stream.description.unwrap_or_default(), + unit: stream.unit.unwrap_or_default(), + comp_agg: collect, + }, + ); + + Ok(Some(measure)) + }); + + match cached { + Ok(opt) => Ok(opt.clone()), + Err(err) => Err(MetricError::Other(err.to_string())), + } + } + + /// Validates if an instrument with the same name as id has already been created. + /// + /// If that instrument conflicts with id, a warning is logged. + fn log_conflict(&self, id: &InstrumentId) { + if let Ok(views) = self.views.lock() { + if let Some(existing) = views.get(id.name.to_lowercase().as_str()) { + if existing == id { + return; + } + // If an existing instrument with the same name but different attributes is found, + // log a warning with details about the conflicting metric stream definitions. + otel_debug!( + name: "Instrument.DuplicateMetricStreamDefinitions", + message = "duplicate metric stream definitions", + reason = format!("names: ({} and {}), descriptions: ({} and {}), kinds: ({:?} and {:?}), units: ({:?} and {:?}), and numbers: ({} and {})", + existing.name, id.name, + existing.description, id.description, + existing.kind, id.kind, + existing.unit, id.unit, + existing.number, id.number,) + ); + } + } + } + + fn inst_id(&self, kind: InstrumentKind, stream: &Stream) -> InstrumentId { + InstrumentId { + name: stream.name.clone().unwrap_or_default(), + description: stream.description.clone().unwrap_or_default(), + kind, + unit: stream.unit.clone().unwrap_or_default(), + number: Cow::Borrowed(std::any::type_name::()), + } + } +} + +/// The default aggregation and parameters for an instrument of [InstrumentKind]. +/// +/// This aggregation selector uses the following selection mapping per [the spec]: +/// +/// * Counter ⇨ Sum +/// * Observable Counter ⇨ Sum +/// * UpDownCounter ⇨ Sum +/// * Observable UpDownCounter ⇨ Sum +/// * Gauge ⇨ LastValue +/// * Observable Gauge ⇨ LastValue +/// * Histogram ⇨ ExplicitBucketHistogram +/// +/// [the spec]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.19.0/specification/metrics/sdk.md#default-aggregation +fn default_aggregation_selector(kind: InstrumentKind) -> Aggregation { + match kind { + InstrumentKind::Counter + | InstrumentKind::UpDownCounter + | InstrumentKind::ObservableCounter + | InstrumentKind::ObservableUpDownCounter => Aggregation::Sum, + InstrumentKind::Gauge => Aggregation::LastValue, + InstrumentKind::ObservableGauge => Aggregation::LastValue, + InstrumentKind::Histogram => Aggregation::ExplicitBucketHistogram { + boundaries: vec![ + 0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, 2500.0, + 5000.0, 7500.0, 10000.0, + ], + record_min_max: true, + }, + } +} + +/// Returns new aggregate functions for the given params. +/// +/// If the aggregation is unknown or temporality is invalid, an error is returned. +fn aggregate_fn( + b: AggregateBuilder, + agg: &aggregation::Aggregation, + kind: InstrumentKind, +) -> MetricResult>> { + match agg { + Aggregation::Default => aggregate_fn(b, &default_aggregation_selector(kind), kind), + Aggregation::Drop => Ok(None), + Aggregation::LastValue => { + match kind { + InstrumentKind::Gauge => Ok(Some(b.last_value(None))), + // temporality for LastValue only affects how data points are reported, so we can always use + // delta temporality, because observable instruments should report data points only since previous collection + InstrumentKind::ObservableGauge => Ok(Some(b.last_value(Some(Temporality::Delta)))), + _ => Err(MetricError::Other(format!("LastValue aggregation is only available for Gauge or ObservableGauge, but not for {kind:?}"))) + } + } + Aggregation::Sum => { + let fns = match kind { + // Observable instruments use collect_and_reset to report only data points + // measured in the current callback, removing stale attributes + InstrumentKind::ObservableCounter => b.precomputed_sum(true), + InstrumentKind::ObservableUpDownCounter => b.precomputed_sum(false), + InstrumentKind::Counter | InstrumentKind::Histogram => b.sum(true), + _ => b.sum(false), + }; + Ok(Some(fns)) + } + Aggregation::ExplicitBucketHistogram { + boundaries, + record_min_max, + } => { + let record_sum = !matches!( + kind, + InstrumentKind::UpDownCounter + | InstrumentKind::ObservableUpDownCounter + | InstrumentKind::ObservableGauge + ); + // TODO implement: observable instruments should not report data points on every collect + // from SDK: For asynchronous instruments with Delta or Cumulative aggregation temporality, + // MetricReader.Collect MUST only receive data points with measurements recorded since the previous collection + Ok(Some(b.explicit_bucket_histogram( + boundaries.to_vec(), + *record_min_max, + record_sum, + ))) + } + Aggregation::Base2ExponentialHistogram { + max_size, + max_scale, + record_min_max, + } => { + let record_sum = !matches!( + kind, + InstrumentKind::UpDownCounter + | InstrumentKind::ObservableUpDownCounter + | InstrumentKind::ObservableGauge + ); + Ok(Some(b.exponential_bucket_histogram( + *max_size, + *max_scale, + *record_min_max, + record_sum, + ))) + } + } +} + +/// Checks if the aggregation can be used by the instrument. +/// +/// Current compatibility: +/// +/// | Instrument Kind | Drop | LastValue | Sum | Histogram | Exponential Histogram | +/// |--------------------------|------|-----------|-----|-----------|-----------------------| +/// | Counter | ✓ | | ✓ | ✓ | ✓ | +/// | UpDownCounter | ✓ | | ✓ | ✓ | ✓ | +/// | Histogram | ✓ | | ✓ | ✓ | ✓ | +/// | Observable Counter | ✓ | | ✓ | ✓ | ✓ | +/// | Observable UpDownCounter | ✓ | | ✓ | ✓ | ✓ | +/// | Gauge | ✓ | ✓ | | ✓ | ✓ | +/// | Observable Gauge | ✓ | ✓ | | ✓ | ✓ | +fn is_aggregator_compatible( + kind: &InstrumentKind, + agg: &aggregation::Aggregation, +) -> MetricResult<()> { + match agg { + Aggregation::Default => Ok(()), + Aggregation::ExplicitBucketHistogram { .. } + | Aggregation::Base2ExponentialHistogram { .. } => { + if matches!( + kind, + InstrumentKind::Counter + | InstrumentKind::UpDownCounter + | InstrumentKind::Gauge + | InstrumentKind::Histogram + | InstrumentKind::ObservableCounter + | InstrumentKind::ObservableUpDownCounter + | InstrumentKind::ObservableGauge + ) { + return Ok(()); + } + Err(MetricError::Other("incompatible aggregation".into())) + } + Aggregation::Sum => { + match kind { + InstrumentKind::ObservableCounter + | InstrumentKind::ObservableUpDownCounter + | InstrumentKind::Counter + | InstrumentKind::Histogram + | InstrumentKind::UpDownCounter => Ok(()), + _ => { + // TODO: review need for aggregation check after + // https://github.com/open-telemetry/opentelemetry-specification/issues/2710 + Err(MetricError::Other("incompatible aggregation".into())) + } + } + } + Aggregation::LastValue => { + match kind { + InstrumentKind::Gauge | InstrumentKind::ObservableGauge => Ok(()), + _ => { + // TODO: review need for aggregation check after + // https://github.com/open-telemetry/opentelemetry-specification/issues/2710 + Err(MetricError::Other("incompatible aggregation".into())) + } + } + } + Aggregation::Drop => Ok(()), + } +} + +/// The group of pipelines connecting Readers with instrument measurement. +#[derive(Clone, Debug)] +pub(crate) struct Pipelines(pub(crate) Vec>); + +impl Pipelines { + pub(crate) fn new( + res: Resource, + readers: Vec>, + views: Vec>, + ) -> Self { + let mut pipes = Vec::with_capacity(readers.len()); + for r in readers { + let p = Arc::new(Pipeline { + resource: res.clone(), + reader: r, + views: views.clone(), + inner: Default::default(), + }); + p.reader.register_pipeline(Arc::downgrade(&p)); + pipes.push(p); + } + + Pipelines(pipes) + } + + pub(crate) fn register_callback(&self, callback: F) + where + F: Fn() + Send + Sync + 'static, + { + let cb = Arc::new(callback); + for pipe in &self.0 { + pipe.add_callback(cb.clone()) + } + } + + /// Force flush all pipelines + pub(crate) fn force_flush(&self) -> OTelSdkResult { + let mut errs = vec![]; + for pipeline in &self.0 { + if let Err(err) = pipeline.force_flush() { + errs.push(err); + } + } + + if errs.is_empty() { + Ok(()) + } else { + Err(OTelSdkError::InternalFailure(format!("{errs:?}"))) + } + } + + /// Shut down all pipelines + pub(crate) fn shutdown(&self) -> OTelSdkResult { + let mut errs = vec![]; + for pipeline in &self.0 { + if let Err(err) = pipeline.shutdown() { + errs.push(err); + } + } + + if errs.is_empty() { + Ok(()) + } else { + Err(crate::error::OTelSdkError::InternalFailure(format!( + "{errs:?}" + ))) + } + } +} + +/// resolver facilitates resolving aggregate functions an instrument calls to +/// aggregate measurements with while updating all pipelines that need to pull from +/// those aggregations. +pub(crate) struct Resolver { + inserters: Vec>, +} + +impl Resolver +where + T: Number, +{ + pub(crate) fn new( + pipelines: Arc, + view_cache: Arc, InstrumentId>>>, + ) -> Self { + let inserters = pipelines + .0 + .iter() + .map(|pipe| Inserter::new(Arc::clone(pipe), Arc::clone(&view_cache))) + .collect(); + + Resolver { inserters } + } + + /// The measures that must be updated by the instrument defined by key. + pub(crate) fn measures( + &self, + id: Instrument, + boundaries: Option>, + ) -> MetricResult>>> { + let (mut measures, mut errs) = (vec![], vec![]); + + for inserter in &self.inserters { + match inserter.instrument(id.clone(), boundaries.as_deref()) { + Ok(ms) => measures.extend(ms), + Err(err) => errs.push(err), + } + } + + if errs.is_empty() { + if measures.is_empty() { + // TODO: Emit internal log that measurements from the instrument + // are being dropped due to view configuration + } + Ok(measures) + } else { + Err(MetricError::Other(format!("{errs:?}"))) + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/reader.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/reader.rs new file mode 100644 index 00000000..ae198411 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/reader.rs @@ -0,0 +1,67 @@ +//! Interfaces for reading and producing metrics +use crate::error::OTelSdkResult; +use std::time::Duration; +use std::{fmt, sync::Weak}; + +use super::{data::ResourceMetrics, instrument::InstrumentKind, pipeline::Pipeline, Temporality}; + +/// The interface used between the SDK and an exporter. +/// +/// Control flow is bi-directional through the `MetricReader`, since the SDK +/// initiates `force_flush` and `shutdown` while the reader initiates +/// collection. The `register_pipeline` method here informs the metric reader +/// that it can begin reading, signaling the start of bi-directional control +/// flow. +/// +/// Typically, push-based exporters that are periodic will implement +/// `MetricExporter` themselves and construct a `PeriodicReader` to satisfy this +/// interface. +/// +/// Pull-based exporters will typically implement `MetricReader` themselves, +/// since they read on demand. +pub trait MetricReader: fmt::Debug + Send + Sync + 'static { + /// Registers a [MetricReader] with a [Pipeline]. + /// + /// The pipeline argument allows the `MetricReader` to signal the sdk to collect + /// and send aggregated metric measurements. + fn register_pipeline(&self, pipeline: Weak); + + /// Gathers and returns all metric data related to the [MetricReader] from the + /// SDK and stores it in the provided [ResourceMetrics] reference. + /// + /// An error is returned if this is called after shutdown. + fn collect(&self, rm: &mut ResourceMetrics) -> OTelSdkResult; + + /// Flushes all metric measurements held in an export pipeline. + /// + /// There is no guaranteed that all telemetry be flushed or all resources have + /// been released on error. + fn force_flush(&self) -> OTelSdkResult; + + /// Flushes all metric measurements held in an export pipeline and releases any + /// held computational resources. + /// + /// There is no guaranteed that all telemetry be flushed or all resources have + /// been released on error. + /// + /// After `shutdown` is called, calls to `collect` will perform no operation and + /// instead will return an error indicating the shutdown state. + fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult; + + /// shutdown with default timeout + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } + + /// The output temporality, a function of instrument kind. + /// This SHOULD be obtained from the exporter. + /// + /// If not configured, the Cumulative temporality SHOULD be used. + fn temporality(&self, kind: InstrumentKind) -> Temporality; +} + +/// Produces metrics for a [MetricReader]. +pub(crate) trait SdkProducer: fmt::Debug + Send + Sync { + /// Returns aggregated metrics from a single collection. + fn produce(&self, rm: &mut ResourceMetrics) -> OTelSdkResult; +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/test.md b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/test.md new file mode 100644 index 00000000..bc0ac92d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/test.md @@ -0,0 +1,24 @@ +# Test Coverage for Metric Instruments + +Tests are located in [mod.rs](mod.rs) + +// TODO: Fill this correctly. + +## Sync Instruments + +| Test Type | Counter (Delta) | Counter (Cumulative) | UpDownCounter (Delta) | UpDownCounter (Cumulative) | Gauge (Delta) | Gauge (Cumulative) | Histogram (Delta) | Histogram (Cumulative) | +|----------------------------|-----------------|----------------------|----------------------|----------------------------|---------------|--------------------|-------------------|------------------------| +| Regular aggregation test | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | +| No-attribute test | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | +| Overflow test | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | +| Attr Order Sorted First | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | +| Attr Order Unsorted First | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | + +## Observable Instruments + +| Test Type | ObservableCounter (Delta) | ObservableCounter (Cumulative) | ObservableGauge (Delta) | ObservableGauge (Cumulative) | ObservableUpDownCounter (Delta) | ObservableUpDownCounter (Cumulative) | +|----------------------------|---------------------------|-------------------------------|-------------------------|------------------------------|---------------------------------|--------------------------------------| +| Regular aggregation test | :x: | :x: | :x: | :x: | :x: | :x: | +| No-attribute test | :x: | :x: | :x: | :x: | :x: | :x: | +| Attr Order Sorted First | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | +| Attr Order Unsorted First | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | :x: | \ No newline at end of file diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/view.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/view.rs new file mode 100644 index 00000000..a9da2db2 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/metrics/view.rs @@ -0,0 +1,54 @@ +use super::instrument::{Instrument, Stream}; + +/// Used to customize the metrics that are output by the SDK. +/// +/// Here are some examples when a [View] might be needed: +/// +/// * Customize which Instruments are to be processed/ignored. For example, an +/// instrumented library can provide both temperature and humidity, but the +/// application developer might only want temperature. +/// * Customize the aggregation - if the default aggregation associated with the +/// [Instrument] does not meet the needs of the user. For example, an HTTP client +/// library might expose HTTP client request duration as Histogram by default, +/// but the application developer might only want the total count of outgoing +/// requests. +/// * Customize which attribute(s) are to be reported on metrics. For example, +/// an HTTP server library might expose HTTP verb (e.g. GET, POST) and HTTP +/// status code (e.g. 200, 301, 404). The application developer might only care +/// about HTTP status code (e.g. reporting the total count of HTTP requests for +/// each HTTP status code). There could also be extreme scenarios in which the +/// application developer does not need any attributes (e.g. just get the total +/// count of all incoming requests). +/// +/// # Example Custom View +/// +/// View is implemented for all `Fn(&Instrument) -> Option`. +/// +/// ``` +/// use opentelemetry_sdk::metrics::{Instrument, SdkMeterProvider, Stream}; +/// +/// // return streams for the given instrument +/// let my_view = |i: &Instrument| { +/// // return Some(Stream) or +/// None +/// }; +/// +/// let provider = SdkMeterProvider::builder().with_view(my_view).build(); +/// # drop(provider) +/// ``` +pub(crate) trait View: Send + Sync + 'static { + /// Defines how data should be collected for certain instruments. + /// + /// Return [Stream] to use for matching [Instrument]s, + /// otherwise if there is no match, return `None`. + fn match_inst(&self, inst: &Instrument) -> Option; +} + +impl View for T +where + T: Fn(&Instrument) -> Option + Send + Sync + 'static, +{ + fn match_inst(&self, inst: &Instrument) -> Option { + self(inst) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/baggage.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/baggage.rs new file mode 100644 index 00000000..5c307316 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/baggage.rs @@ -0,0 +1,396 @@ +use opentelemetry::{ + baggage::{BaggageExt, KeyValueMetadata}, + otel_warn, + propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator}, + Context, +}; +use percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS}; +use std::iter; +use std::sync::OnceLock; + +static BAGGAGE_HEADER: &str = "baggage"; +const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b';').add(b',').add(b'='); + +// W3C Baggage specification limits. +// See https://www.w3.org/TR/baggage/#limits +const MAX_BAGGAGE_LENGTH: usize = 8192; +const MAX_BAGGAGE_ITEMS: usize = 64; + +// TODO Replace this with LazyLock once it is stable. +static BAGGAGE_FIELDS: OnceLock<[String; 1]> = OnceLock::new(); +#[inline] +fn baggage_fields() -> &'static [String; 1] { + BAGGAGE_FIELDS.get_or_init(|| [BAGGAGE_HEADER.to_owned()]) +} + +/// Propagates name-value pairs in [W3C Baggage] format. +/// +/// Baggage is used to annotate telemetry, adding context and +/// information to metrics, traces, and logs. It is an abstract data type +/// represented by a set of name-value pairs describing user-defined properties. +/// Each name in a [`Baggage`] is associated with exactly one value. +/// `Baggage`s are serialized according to the editor's draft of +/// the [W3C Baggage] specification. +/// +/// # Examples +/// +/// ``` +/// use opentelemetry::{baggage::{Baggage, BaggageExt}, propagation::TextMapPropagator}; +/// use opentelemetry_sdk::propagation::BaggagePropagator; +/// use std::collections::HashMap; +/// +/// // Example baggage value passed in externally via http headers +/// let mut headers = HashMap::new(); +/// headers.insert("baggage".to_string(), "user_id=1".to_string()); +/// +/// let propagator = BaggagePropagator::new(); +/// // can extract from any type that impls `Extractor`, usually an HTTP header map +/// let cx = propagator.extract(&headers); +/// +/// // Iterate over extracted name-value pairs +/// for (name, value) in cx.baggage() { +/// // ... +/// } +/// +/// // Add new baggage +/// let mut baggage = Baggage::new(); +/// let _ = baggage.insert("server_id", "42"); +/// +/// let cx_with_additions = cx.with_baggage(baggage); +/// +/// // Inject baggage into http request +/// propagator.inject_context(&cx_with_additions, &mut headers); +/// +/// let header_value = headers.get("baggage").expect("header is injected"); +/// assert!(!header_value.contains("user_id=1"), "still contains previous name-value"); +/// assert!(header_value.contains("server_id=42"), "does not contain new name-value pair"); +/// ``` +/// +/// [W3C Baggage]: https://w3c.github.io/baggage +/// [`Baggage`]: opentelemetry::baggage::Baggage +#[derive(Debug, Default)] +pub struct BaggagePropagator { + _private: (), +} + +impl BaggagePropagator { + /// Construct a new baggage propagator. + pub fn new() -> Self { + BaggagePropagator { _private: () } + } +} + +impl TextMapPropagator for BaggagePropagator { + /// Encodes the values of the `Context` and injects them into the provided `Injector`. + fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) { + let baggage = cx.baggage(); + if !baggage.is_empty() { + let header_value = baggage + .iter() + .map(|(name, (value, metadata))| { + let metadata_str = metadata.as_str().trim(); + let metadata_prefix = if metadata_str.is_empty() { "" } else { ";" }; + utf8_percent_encode(name.as_str().trim(), FRAGMENT) + .chain(iter::once("=")) + .chain(utf8_percent_encode(value.as_str().trim(), FRAGMENT)) + .chain(iter::once(metadata_prefix)) + .chain(iter::once(metadata_str)) + .collect() + }) + .collect::>() + .join(","); + injector.set(BAGGAGE_HEADER, header_value); + } + } + + /// Extracts a `Context` with baggage values from a `Extractor`. + fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context { + if let Some(header_value) = extractor.get(BAGGAGE_HEADER) { + // Enforce the W3C Baggage maximum header length up-front so a + // single oversize header cannot drive per-entry allocation work + // before the entries are dropped on insert. See + // https://www.w3.org/TR/baggage/#limits. + if header_value.len() > MAX_BAGGAGE_LENGTH { + otel_warn!( + name: "BaggagePropagator.Extract.HeaderTooLarge", + message = "Baggage header exceeds W3C maximum length and was dropped", + header_bytes = header_value.len() as i64, + limit_bytes = MAX_BAGGAGE_LENGTH as i64, + ); + return cx.clone(); + } + + let baggage = + header_value + .split(',') + .take(MAX_BAGGAGE_ITEMS) + .filter_map(|context_value| { + if let Some((name_and_value, props)) = context_value + .split(';') + .collect::>() + .split_first() + { + let mut iter = name_and_value.split('='); + if let (Some(name), Some(value)) = (iter.next(), iter.next()) { + let decode_name = percent_decode_str(name).decode_utf8(); + let decode_value = percent_decode_str(value).decode_utf8(); + + if let (Ok(name), Ok(value)) = (decode_name, decode_value) { + // Here we don't store the first ; into baggage since it should be treated + // as separator rather part of metadata + let decoded_props = props + .iter() + .flat_map(|prop| percent_decode_str(prop).decode_utf8()) + .map(|prop| prop.trim().to_string()) + .collect::>() + .join(";"); // join with ; because we deleted all ; when calling split above + + Some(KeyValueMetadata::new( + name.trim().to_owned(), + value.trim().to_string(), + decoded_props.as_str(), + )) + } else { + otel_warn!( + name: "BaggagePropagator.Extract.InvalidUTF8", + message = "Invalid UTF8 string in key values", + baggage_header = header_value, + ); + None + } + } else { + otel_warn!( + name: "BaggagePropagator.Extract.InvalidKeyValueFormat", + message = "Invalid baggage key-value format", + baggage_header = header_value, + ); + None + } + } else { + otel_warn!( + name: "BaggagePropagator.Extract.InvalidFormat", + message = "Invalid baggage format", + baggage_header = header_value); + None + } + }); + cx.with_baggage(baggage) + } else { + cx.clone() + } + } + + fn fields(&self) -> FieldIter<'_> { + FieldIter::new(baggage_fields()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use opentelemetry::{baggage::BaggageMetadata, Key, KeyValue, StringValue, Value}; + use std::collections::HashMap; + + #[rustfmt::skip] + fn valid_extract_data() -> Vec<(&'static str, HashMap)> { + vec![ + // "valid w3cHeader" + ("key1=val1,key2=val2", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()), + // "valid w3cHeader with spaces" + ("key1 = val1, key2 =val2 ", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()), + // "valid header with url-escaped comma" + ("key1=val1,key2=val2%2Cval3", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2,val3"))].into_iter().collect()), + // "valid header with an invalid header" + ("key1=val1,key2=val2,a,val3", vec![(Key::new("key1"), StringValue::from("val1")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()), + // "valid header with no value" + ("key1=,key2=val2", vec![(Key::new("key1"), StringValue::from("")), (Key::new("key2"), StringValue::from("val2"))].into_iter().collect()), + ] + } + + #[rustfmt::skip] + #[allow(clippy::type_complexity)] + fn valid_extract_data_with_metadata() -> Vec<(&'static str, HashMap)> { + vec![ + // "valid w3cHeader with properties" + ("key1=val1,key2=val2;prop=1", vec![(Key::new("key1"), (StringValue::from("val1"), BaggageMetadata::default())), (Key::new("key2"), (StringValue::from("val2"), BaggageMetadata::from("prop=1")))].into_iter().collect()), + // prop can don't need to be key value pair + ("key1=val1,key2=val2;prop1", vec![(Key::new("key1"), (StringValue::from("val1"), BaggageMetadata::default())), (Key::new("key2"), (StringValue::from("val2"), BaggageMetadata::from("prop1")))].into_iter().collect()), + ("key1=value1;property1;property2, key2 = value2, key3=value3; propertyKey=propertyValue", + vec![ + (Key::new("key1"), (StringValue::from("value1"), BaggageMetadata::from("property1;property2"))), + (Key::new("key2"), (StringValue::from("value2"), BaggageMetadata::default())), + (Key::new("key3"), (StringValue::from("value3"), BaggageMetadata::from("propertyKey=propertyValue"))), + ].into_iter().collect()), + ] + } + + #[rustfmt::skip] + fn valid_inject_data() -> Vec<(Vec, Vec<&'static str>)> { + vec![ + // "two simple values" + (vec![KeyValue::new("key1", "val1"), KeyValue::new("key2", "val2")], vec!["key1=val1", "key2=val2"]), + // "two values with escaped chars" + (vec![KeyValue::new("key1", "val1,val2"), KeyValue::new("key2", "val3=4")], vec!["key1=val1%2Cval2", "key2=val3%3D4"]), + // "values of non-string non-array types" + ( + vec![ + KeyValue::new("key1", true), + KeyValue::new("key2", Value::I64(123)), + KeyValue::new("key3", Value::F64(123.567)), + ], + vec![ + "key1=true", + "key2=123", + "key3=123.567", + ], + ), + // "values of array types" + ( + vec![ + KeyValue::new("key1", Value::Array(vec![true, false].into())), + KeyValue::new("key2", Value::Array(vec![123, 456].into())), + KeyValue::new("key3", Value::Array(vec![StringValue::from("val1"), StringValue::from("val2")].into())), + ], + vec![ + "key1=[true%2Cfalse]", + "key2=[123%2C456]", + "key3=[%22val1%22%2C%22val2%22]", + ], + ), + ] + } + + #[rustfmt::skip] + fn valid_inject_data_metadata() -> Vec<(Vec, Vec<&'static str>)> { + vec![ + ( + vec![ + KeyValueMetadata::new("key1", "val1", "prop1"), + KeyValue::new("key2", "val2").into(), + KeyValueMetadata::new("key3", "val3", "anykey=anyvalue"), + ], + vec![ + "key1=val1;prop1", + "key2=val2", + "key3=val3;anykey=anyvalue", + ], + ) + ] + } + + #[test] + fn extract_baggage() { + let propagator = BaggagePropagator::new(); + + for (header_value, kvs) in valid_extract_data() { + let mut extractor: HashMap = HashMap::new(); + extractor.insert(BAGGAGE_HEADER.to_string(), header_value.to_string()); + let context = propagator.extract(&extractor); + let baggage = context.baggage(); + + assert_eq!(kvs.len(), baggage.len()); + for (key, (value, _metadata)) in baggage { + assert_eq!(Some(value), kvs.get(key)) + } + } + } + + #[test] + fn inject_baggage() { + let propagator = BaggagePropagator::new(); + + for (kvm, header_parts) in valid_inject_data() { + let mut injector = HashMap::new(); + let cx = Context::current_with_baggage(kvm); + propagator.inject_context(&cx, &mut injector); + let header_value = injector.get(BAGGAGE_HEADER).unwrap(); + assert_eq!(header_parts.join(",").len(), header_value.len(),); + for header_part in &header_parts { + assert!(header_value.contains(header_part),) + } + } + } + + #[test] + fn extract_baggage_with_metadata() { + let propagator = BaggagePropagator::new(); + for (header_value, kvm) in valid_extract_data_with_metadata() { + let mut extractor: HashMap = HashMap::new(); + extractor.insert(BAGGAGE_HEADER.to_string(), header_value.to_string()); + let context = propagator.extract(&extractor); + let baggage = context.baggage(); + + assert_eq!(kvm.len(), baggage.len()); + for (key, value_and_prop) in baggage { + assert_eq!(Some(value_and_prop), kvm.get(key)) + } + } + } + + #[test] + fn inject_baggage_with_metadata() { + let propagator = BaggagePropagator::new(); + + for (kvm, header_parts) in valid_inject_data_metadata() { + let mut injector = HashMap::new(); + let cx = Context::current_with_baggage(kvm); + propagator.inject_context(&cx, &mut injector); + let header_value = injector.get(BAGGAGE_HEADER).unwrap(); + + assert_eq!(header_parts.join(",").len(), header_value.len()); + for header_part in &header_parts { + assert!(header_value.contains(header_part),) + } + } + } + + #[test] + fn extract_drops_header_exceeding_max_length() { + let propagator = BaggagePropagator::new(); + + // Build a syntactically valid header longer than the W3C 8192-byte + // limit. The header must be rejected outright; no entries are added. + let mut header = String::with_capacity(MAX_BAGGAGE_LENGTH + 1024); + let mut i = 0u32; + while header.len() < MAX_BAGGAGE_LENGTH + 256 { + if !header.is_empty() { + header.push(','); + } + header.push_str(&format!("k{i}=v{i}")); + i += 1; + } + assert!(header.len() > MAX_BAGGAGE_LENGTH); + + let mut extractor: HashMap = HashMap::new(); + extractor.insert(BAGGAGE_HEADER.to_string(), header); + + let context = propagator.extract(&extractor); + assert_eq!(context.baggage().len(), 0); + } + + #[test] + fn extract_caps_entry_count_at_max_baggage_items() { + let propagator = BaggagePropagator::new(); + + // Build a header whose total size stays under MAX_BAGGAGE_LENGTH but + // contains more than MAX_BAGGAGE_ITEMS entries. + let mut header = String::new(); + let entries = MAX_BAGGAGE_ITEMS + 32; + for i in 0..entries { + if i > 0 { + header.push(','); + } + header.push_str(&format!("k{i:03}=v")); + } + assert!(header.len() <= MAX_BAGGAGE_LENGTH); + + let mut extractor: HashMap = HashMap::new(); + extractor.insert(BAGGAGE_HEADER.to_string(), header); + + let context = propagator.extract(&extractor); + // The storage type also caps at MAX_KEY_VALUE_PAIRS=64; the propagator + // additionally stops parsing at MAX_BAGGAGE_ITEMS so allocator work + // does not grow with attacker-supplied entry counts. + assert_eq!(context.baggage().len(), MAX_BAGGAGE_ITEMS); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/mod.rs new file mode 100644 index 00000000..6cfb5d07 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/mod.rs @@ -0,0 +1,6 @@ +//! OpenTelemetry Propagators +mod baggage; +mod trace_context; + +pub use baggage::BaggagePropagator; +pub use trace_context::TraceContextPropagator; diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/trace_context.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/trace_context.rs new file mode 100644 index 00000000..1c36ced9 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/propagation/trace_context.rs @@ -0,0 +1,300 @@ +//! # W3C Trace Context Propagator +//! + +use opentelemetry::{ + propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator}, + trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState}, + Context, +}; +use std::str::FromStr; +use std::sync::OnceLock; + +const SUPPORTED_VERSION: u8 = 0; +const MAX_VERSION: u8 = 254; +const TRACEPARENT_HEADER: &str = "traceparent"; +const TRACESTATE_HEADER: &str = "tracestate"; + +// TODO Replace this with LazyLock once it is stable. +static TRACE_CONTEXT_HEADER_FIELDS: OnceLock<[String; 2]> = OnceLock::new(); + +fn trace_context_header_fields() -> &'static [String; 2] { + TRACE_CONTEXT_HEADER_FIELDS + .get_or_init(|| [TRACEPARENT_HEADER.to_owned(), TRACESTATE_HEADER.to_owned()]) +} + +/// Propagates `SpanContext`s in [W3C TraceContext] format under `traceparent` and `tracestate` header. +/// +/// The `traceparent` header represents the incoming request in a +/// tracing system in a common format, understood by all vendors. +/// Here’s an example of a `traceparent` header. +/// +/// `traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01` +/// +/// The `traceparent` HTTP header field identifies the incoming request in a +/// tracing system. It has four fields: +/// +/// - version +/// - trace-id +/// - parent-id +/// - trace-flags +/// +/// The `tracestate` header provides additional vendor-specific trace +/// identification information across different distributed tracing systems. +/// Here's an example of a `tracestate` header +/// +/// `tracestate: vendorname1=opaqueValue1,vendorname2=opaqueValue2` +/// +/// See the [w3c trace-context docs] for more details. +/// +/// [w3c trace-context docs]: https://w3c.github.io/trace-context/ +/// [W3C TraceContext]: https://www.w3.org/TR/trace-context/ +#[derive(Clone, Debug, Default)] +pub struct TraceContextPropagator { + _private: (), +} + +impl TraceContextPropagator { + /// Create a new `TraceContextPropagator`. + pub fn new() -> Self { + TraceContextPropagator { _private: () } + } + + /// Extract span context from w3c trace-context header. + fn extract_span_context(&self, extractor: &dyn Extractor) -> Result { + let header_value = extractor.get(TRACEPARENT_HEADER).unwrap_or("").trim(); + let parts = header_value.split_terminator('-').collect::>(); + // Ensure parts are not out of range. + if parts.len() < 4 { + return Err(()); + } + + // Ensure version is within range, for version 0 there must be 4 parts. + let version = u8::from_str_radix(parts[0], 16).map_err(|_| ())?; + if version > MAX_VERSION || version == 0 && parts.len() != 4 { + return Err(()); + } + + // Ensure trace id is lowercase + if parts[1].chars().any(|c| c.is_ascii_uppercase()) { + return Err(()); + } + + // Parse trace id section + let trace_id = TraceId::from_hex(parts[1]).map_err(|_| ())?; + + // Ensure span id is lowercase + if parts[2].chars().any(|c| c.is_ascii_uppercase()) { + return Err(()); + } + + // Parse span id section + let span_id = SpanId::from_hex(parts[2]).map_err(|_| ())?; + + // Ensure trace flags are lowercase + if parts[3].chars().any(|c| c.is_ascii_uppercase()) { + return Err(()); + } + + // Parse trace flags section + let opts = u8::from_str_radix(parts[3], 16).map_err(|_| ())?; + + // Build trace flags clearing all flags other than the trace-context + // supported sampling bit. Unknown flags are accepted but zeroed out, + // as required by https://www.w3.org/TR/trace-context/#other-flags + let trace_flags = TraceFlags::new(opts) & TraceFlags::SAMPLED; + + let trace_state = match extractor.get(TRACESTATE_HEADER) { + Some(trace_state_str) => { + TraceState::from_str(trace_state_str).unwrap_or_else(|_| TraceState::default()) + } + None => TraceState::default(), + }; + + // create context + let span_context = SpanContext::new(trace_id, span_id, trace_flags, true, trace_state); + + // Ensure span is valid + if !span_context.is_valid() { + return Err(()); + } + + Ok(span_context) + } +} + +impl TextMapPropagator for TraceContextPropagator { + /// Properly encodes the values of the `SpanContext` and injects them + /// into the `Injector`. + fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) { + let span = cx.span(); + let span_context = span.span_context(); + if span_context.is_valid() { + let header_value = format!( + "{:02x}-{}-{}-{:02x}", + SUPPORTED_VERSION, + span_context.trace_id(), + span_context.span_id(), + span_context.trace_flags() & TraceFlags::SAMPLED + ); + injector.set(TRACEPARENT_HEADER, header_value); + injector.set(TRACESTATE_HEADER, span_context.trace_state().header()); + } + } + + /// Retrieves encoded `SpanContext`s using the `Extractor`. It decodes + /// the `SpanContext` and returns it. If no `SpanContext` was retrieved + /// OR if the retrieved SpanContext is invalid then an empty `SpanContext` + /// is returned. + fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context { + self.extract_span_context(extractor) + .map(|sc| cx.with_remote_span_context(sc)) + .unwrap_or_else(|_| cx.clone()) + } + + fn fields(&self) -> FieldIter<'_> { + FieldIter::new(trace_context_header_fields()) + } +} + +#[cfg(all(test, feature = "testing", feature = "trace"))] +mod tests { + use super::*; + use crate::testing::trace::TestSpan; + use std::collections::HashMap; + + #[rustfmt::skip] + fn extract_data() -> Vec<(&'static str, &'static str, SpanContext)> { + vec![ + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::default(), true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-03", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), + ("02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), + ("02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), + ("02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-08", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::default(), true, TraceState::from_str("foo=bar").unwrap())), + ("02-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09-XYZxsf09", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), + ("01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-09-", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), + ] + } + + #[rustfmt::skip] + fn extract_data_invalid() -> Vec<(&'static str, &'static str)> { + vec![ + ("0000-00000000000000000000000000000000-0000000000000000-01", "wrong version length"), + ("00-ab00000000000000000000000000000000-cd00000000000000-01", "wrong trace ID length"), + ("00-ab000000000000000000000000000000-cd0000000000000000-01", "wrong span ID length"), + ("00-ab000000000000000000000000000000-cd00000000000000-0100", "wrong trace flag length"), + ("qw-00000000000000000000000000000000-0000000000000000-01", "bogus version"), + ("00-qw000000000000000000000000000000-cd00000000000000-01", "bogus trace ID"), + ("00-ab000000000000000000000000000000-qw00000000000000-01", "bogus span ID"), + ("00-ab000000000000000000000000000000-cd00000000000000-qw", "bogus trace flag"), + ("A0-00000000000000000000000000000000-0000000000000000-01", "upper case version"), + ("00-AB000000000000000000000000000000-cd00000000000000-01", "upper case trace ID"), + ("00-ab000000000000000000000000000000-CD00000000000000-01", "upper case span ID"), + ("00-ab000000000000000000000000000000-cd00000000000000-A1", "upper case trace flag"), + ("00-00000000000000000000000000000000-0000000000000000-01", "zero trace ID and span ID"), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7", "missing options"), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-", "empty options"), + ] + } + + #[rustfmt::skip] + fn inject_data() -> Vec<(&'static str, &'static str, SpanContext)> { + vec![ + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::SAMPLED, true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::default(), true, TraceState::from_str("foo=bar").unwrap())), + ("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "foo=bar", SpanContext::new(TraceId::from(0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736), SpanId::from(0x00f0_67aa_0ba9_02b7), TraceFlags::new(0xff), true, TraceState::from_str("foo=bar").unwrap())), + ("", "", SpanContext::empty_context()), + ] + } + + #[test] + fn extract_w3c() { + let propagator = TraceContextPropagator::new(); + + for (trace_parent, trace_state, expected_context) in extract_data() { + let mut extractor = HashMap::new(); + extractor.insert(TRACEPARENT_HEADER.to_string(), trace_parent.to_string()); + extractor.insert(TRACESTATE_HEADER.to_string(), trace_state.to_string()); + + assert_eq!( + propagator.extract(&extractor).span().span_context(), + &expected_context + ) + } + } + + #[test] + fn extract_w3c_tracestate() { + let propagator = TraceContextPropagator::new(); + let state = "foo=bar".to_string(); + let parent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00".to_string(); + + let mut extractor = HashMap::new(); + extractor.insert(TRACEPARENT_HEADER.to_string(), parent); + extractor.insert(TRACESTATE_HEADER.to_string(), state.clone()); + + assert_eq!( + propagator + .extract(&extractor) + .span() + .span_context() + .trace_state() + .header(), + state + ) + } + + #[test] + fn extract_w3c_reject_invalid() { + let propagator = TraceContextPropagator::new(); + + for (invalid_header, reason) in extract_data_invalid() { + let mut extractor = HashMap::new(); + extractor.insert(TRACEPARENT_HEADER.to_string(), invalid_header.to_string()); + + assert_eq!( + propagator.extract(&extractor).span().span_context(), + &SpanContext::empty_context(), + "{reason}" + ) + } + } + + #[test] + fn inject_w3c() { + let propagator = TraceContextPropagator::new(); + + for (expected_trace_parent, expected_trace_state, context) in inject_data() { + let mut injector = HashMap::new(); + propagator.inject_context( + &Context::current_with_span(TestSpan(context)), + &mut injector, + ); + + assert_eq!( + Extractor::get(&injector, TRACEPARENT_HEADER).unwrap_or(""), + expected_trace_parent + ); + + assert_eq!( + Extractor::get(&injector, TRACESTATE_HEADER).unwrap_or(""), + expected_trace_state + ); + } + } + + #[test] + fn inject_w3c_tracestate() { + let propagator = TraceContextPropagator::new(); + let state = "foo=bar"; + + let mut injector: HashMap = HashMap::new(); + injector.set(TRACESTATE_HEADER, state.to_string()); + + Context::map_current(|cx| propagator.inject_context(cx, &mut injector)); + + assert_eq!(Extractor::get(&injector, TRACESTATE_HEADER), Some(state)) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/attributes.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/attributes.rs new file mode 100644 index 00000000..41ab4a8d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/attributes.rs @@ -0,0 +1,32 @@ +/// Logical name of the service. +/// +/// MUST be the same for all instances of horizontally scaled services. If the value was not specified, SDKs MUST fallback to `unknown_service:` concatenated with [`process.executable.name`](process.md#process), e.g. `unknown_service:bash`. If `process.executable.name` is not available, the value MUST be set to `unknown_service`. +/// +/// # Examples +/// +/// - `shoppingcart` +pub(crate) const SERVICE_NAME: &str = "service.name"; + +/// The language of the telemetry SDK. +pub(crate) const TELEMETRY_SDK_LANGUAGE: &str = "telemetry.sdk.language"; + +/// The name of the telemetry SDK as defined above. +/// +/// The OpenTelemetry SDK MUST set the `telemetry.sdk.name` attribute to `opentelemetry`. +/// If another SDK, like a fork or a vendor-provided implementation, is used, this SDK MUST set the +/// `telemetry.sdk.name` attribute to the fully-qualified class or module name of this SDK's main entry point +/// or another suitable identifier depending on the language. +/// The identifier `opentelemetry` is reserved and MUST NOT be used in this case. +/// All custom identifiers SHOULD be stable across different versions of an implementation. +/// +/// # Examples +/// +/// - `opentelemetry` +pub(crate) const TELEMETRY_SDK_NAME: &str = "telemetry.sdk.name"; + +/// The version string of the telemetry SDK. +/// +/// # Examples +/// +/// - `1.2.3` +pub(crate) const TELEMETRY_SDK_VERSION: &str = "telemetry.sdk.version"; diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/env.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/env.rs new file mode 100644 index 00000000..da260045 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/env.rs @@ -0,0 +1,209 @@ +//! Environment variables resource detector +//! +//! Implementation of `ResourceDetector` to extract a `Resource` from environment +//! variables. +use crate::resource::{Resource, ResourceDetector}; +use opentelemetry::{Key, KeyValue, Value}; +use std::env; + +const OTEL_RESOURCE_ATTRIBUTES: &str = "OTEL_RESOURCE_ATTRIBUTES"; +const OTEL_SERVICE_NAME: &str = "OTEL_SERVICE_NAME"; + +/// EnvResourceDetector extract resource from environment variable +/// `OTEL_RESOURCE_ATTRIBUTES`. See [OpenTelemetry Resource +/// Spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable) +/// for details. +#[derive(Debug)] +pub struct EnvResourceDetector { + _private: (), +} + +impl ResourceDetector for EnvResourceDetector { + fn detect(&self) -> Resource { + match env::var(OTEL_RESOURCE_ATTRIBUTES) { + Ok(s) if !s.is_empty() => construct_otel_resources(s), + Ok(_) | Err(_) => Resource::empty(), // return empty resource + } + } +} + +impl EnvResourceDetector { + /// Create `EnvResourceDetector` instance. + pub fn new() -> Self { + EnvResourceDetector { _private: () } + } +} + +impl Default for EnvResourceDetector { + fn default() -> Self { + EnvResourceDetector::new() + } +} + +/// Extract key value pairs and construct a resource from resources string like +/// key1=value1,key2=value2,... +fn construct_otel_resources(s: String) -> Resource { + Resource::builder_empty() + .with_attributes(s.split_terminator(',').filter_map(|entry| { + let parts = match entry.split_once('=') { + Some(p) => p, + None => return None, + }; + let key = parts.0.trim(); + let value = parts.1.trim(); + + Some(KeyValue::new(key.to_owned(), value.to_owned())) + })) + .build() +} + +/// There are attributes which MUST be provided by the SDK as specified in +/// [the Resource SDK specification]. This detector detects those attributes and +/// if the attribute cannot be detected, it uses the default value. +/// +/// This detector will first try `OTEL_SERVICE_NAME` env. If it's not available, +/// then it will check the `OTEL_RESOURCE_ATTRIBUTES` env and see if it contains +/// `service.name` resource. If it's also not available, it will use `unknown_service`. +/// +/// If users want to set an empty service name, they can provide +/// a resource with empty value and `service.name` key. +/// +/// [the Resource SDK specification]:https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#sdk-provided-resource-attributes +#[derive(Debug)] +pub struct SdkProvidedResourceDetector; + +fn default_service_name() -> Value { + // Fallback to unknown_service: per spec. + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#sdk-provided-resource-attributes + if cfg!(miri) { + return "unknown_service".into(); + } + + env::current_exe() + .ok() + .and_then(|path| { + path.file_name() + .and_then(|name| name.to_str()) + .map(|name| format!("unknown_service:{}", name)) + }) + .unwrap_or_else(|| "unknown_service".to_string()) + .into() +} + +impl ResourceDetector for SdkProvidedResourceDetector { + fn detect(&self) -> Resource { + Resource::builder_empty() + .with_attributes([KeyValue::new( + super::SERVICE_NAME, + env::var(OTEL_SERVICE_NAME) + .ok() + .filter(|s| !s.is_empty()) + .map(Value::from) + .or_else(|| { + EnvResourceDetector::new() + .detect() + .get(&Key::new(super::SERVICE_NAME)) + }) + .unwrap_or_else(default_service_name), + )]) + .build() + } +} + +#[cfg(test)] +mod tests { + use crate::resource::env::{ + SdkProvidedResourceDetector, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, + }; + use crate::resource::{EnvResourceDetector, Resource, ResourceDetector}; + use opentelemetry::{Key, KeyValue, Value}; + + #[test] + fn test_read_from_env() { + temp_env::with_vars( + [ + ( + "OTEL_RESOURCE_ATTRIBUTES", + Some("key=value, k = v , a= x, a=z,base64=SGVsbG8sIFdvcmxkIQ=="), + ), + ("IRRELEVANT", Some("20200810")), + ], + || { + let detector = EnvResourceDetector::new(); + let resource = detector.detect(); + assert_eq!( + resource, + Resource::builder_empty() + .with_attributes([ + KeyValue::new("key", "value"), + KeyValue::new("k", "v"), + KeyValue::new("a", "x"), + KeyValue::new("a", "z"), + KeyValue::new("base64", "SGVsbG8sIFdvcmxkIQ=="), // base64('Hello, World!') + ]) + .build() + ); + }, + ); + + let detector = EnvResourceDetector::new(); + let resource = detector.detect(); + assert!(resource.is_empty()); + } + + #[test] + fn test_sdk_provided_resource_detector() { + // Ensure no env var set - should fallback to unknown_service: + // For cargo tests, the executable name is typically - + let no_env = SdkProvidedResourceDetector.detect(); + let service_name = no_env + .get(&Key::from_static_str(crate::resource::SERVICE_NAME)) + .map(|v| v.to_string()) + .unwrap(); + + if cfg!(miri) { + assert_eq!(service_name, "unknown_service"); + } else { + assert!( + service_name.starts_with("unknown_service:opentelemetry_sdk-"), + "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}", + service_name + ); + } + + temp_env::with_var(OTEL_SERVICE_NAME, Some("test service"), || { + let with_service = SdkProvidedResourceDetector.detect(); + assert_eq!( + with_service.get(&Key::from_static_str(crate::resource::SERVICE_NAME)), + Some(Value::from("test service")), + ) + }); + + temp_env::with_var( + OTEL_RESOURCE_ATTRIBUTES, + Some("service.name=test service1"), + || { + let with_service = SdkProvidedResourceDetector.detect(); + assert_eq!( + with_service.get(&Key::from_static_str(crate::resource::SERVICE_NAME)), + Some(Value::from("test service1")), + ) + }, + ); + + // OTEL_SERVICE_NAME takes priority + temp_env::with_vars( + [ + (OTEL_SERVICE_NAME, Some("test service")), + (OTEL_RESOURCE_ATTRIBUTES, Some("service.name=test service3")), + ], + || { + let with_service = SdkProvidedResourceDetector.detect(); + assert_eq!( + with_service.get(&Key::from_static_str(crate::resource::SERVICE_NAME)), + Some(Value::from("test service")) + ); + }, + ); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/mod.rs new file mode 100644 index 00000000..d7d6d6e2 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/mod.rs @@ -0,0 +1,557 @@ +//! Representations of entities producing telemetry. +//! +//! A [Resource] is an immutable representation of the entity producing +//! telemetry as attributes. For example, a process producing telemetry that is +//! running in a container on Kubernetes has a Pod name, it is in a namespace +//! and possibly is part of a Deployment which also has a name. All three of +//! these attributes can be included in the `Resource`. Note that there are +//! certain ["standard attributes"] that have prescribed meanings. +//! +//! ["standard attributes"]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/resource/semantic_conventions/README.md +//! +//! # Resource detectors +//! +//! [`ResourceDetector`]s are used to detect resource from runtime or +//! environmental variables. The following are provided by default with this +//! SDK. +//! +//! - [`EnvResourceDetector`] - detect resource from environmental variables. +//! - [`TelemetryResourceDetector`] - detect telemetry SDK's information. +//! +//! The OS and Process resource detectors are packaged separately in the +//! [`opentelemetry-resource-detector` crate](https://github.com/open-telemetry/opentelemetry-rust-contrib/tree/main/opentelemetry-resource-detectors). +mod env; +mod telemetry; + +mod attributes; +pub(crate) use attributes::*; + +pub use env::EnvResourceDetector; +pub use env::SdkProvidedResourceDetector; +pub use telemetry::TelemetryResourceDetector; + +use opentelemetry::{Key, KeyValue, Value}; +use std::borrow::Cow; +use std::collections::{hash_map, HashMap}; +use std::ops::Deref; +use std::sync::Arc; + +/// Inner structure of `Resource` holding the actual data. +/// This structure is designed to be shared among `Resource` instances via `Arc`. +#[derive(Debug, Clone, PartialEq)] +struct ResourceInner { + attrs: HashMap, + schema_url: Option>, +} + +/// An immutable representation of the entity producing telemetry as attributes. +/// Utilizes `Arc` for efficient sharing and cloning. +#[derive(Clone, Debug, PartialEq)] +pub struct Resource { + inner: Arc, +} + +impl Resource { + /// Creates a [ResourceBuilder] that allows you to configure multiple aspects of the Resource. + /// + /// This [ResourceBuilder] will include the following [ResourceDetector]s: + /// - [SdkProvidedResourceDetector] + /// - [TelemetryResourceDetector] + /// - [EnvResourceDetector] + /// If you'd like to start from an empty resource, use [Resource::builder_empty]. + pub fn builder() -> ResourceBuilder { + ResourceBuilder { + resource: Self::from_detectors(&[ + Box::new(SdkProvidedResourceDetector), + Box::new(TelemetryResourceDetector), + Box::new(EnvResourceDetector::new()), + ]), + } + } + + /// Creates a [ResourceBuilder] that allows you to configure multiple aspects of the Resource. + /// + /// This [ResourceBuilder] will not include any attributes or [ResourceDetector]s by default. + pub fn builder_empty() -> ResourceBuilder { + ResourceBuilder { + resource: Resource::empty(), + } + } + + /// Creates an empty resource. + /// This is the basic constructor that initializes a resource with no attributes and no schema URL. + pub(crate) fn empty() -> Self { + Resource { + inner: Arc::new(ResourceInner { + attrs: HashMap::new(), + schema_url: None, + }), + } + } + + /// Create a new `Resource` from key value pairs. + /// + /// Values are de-duplicated by key, and the last key-value pair will be retained + pub(crate) fn new>(kvs: T) -> Self { + let mut attrs = HashMap::new(); + for kv in kvs { + attrs.insert(kv.key, kv.value); + } + + Resource { + inner: Arc::new(ResourceInner { + attrs, + schema_url: None, + }), + } + } + + /// Create a new `Resource` from a key value pairs and [schema url]. + /// + /// Values are de-duplicated by key, and the first key-value pair with a non-empty string value + /// will be retained. + /// + /// schema_url must be a valid URL using HTTP or HTTPS protocol. + /// + /// [schema url]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url + fn from_schema_url(kvs: KV, schema_url: S) -> Self + where + KV: IntoIterator, + S: Into>, + { + let schema_url_str = schema_url.into(); + let normalized_schema_url = if schema_url_str.is_empty() { + None + } else { + Some(schema_url_str) + }; + let mut attrs = HashMap::new(); + for kv in kvs { + attrs.insert(kv.key, kv.value); + } + Resource { + inner: Arc::new(ResourceInner { + attrs, + schema_url: normalized_schema_url, + }), + } + } + + /// Create a new `Resource` from resource detectors. + fn from_detectors(detectors: &[Box]) -> Self { + let mut resource = Resource::empty(); + for detector in detectors { + let detected_res = detector.detect(); + // This call ensures that if the Arc is not uniquely owned, + // the data is cloned before modification, preserving safety. + // If the Arc is uniquely owned, it simply returns a mutable reference to the data. + let inner = Arc::make_mut(&mut resource.inner); + for (key, value) in detected_res.into_iter() { + inner.attrs.insert(Key::new(key.clone()), value.clone()); + } + } + + resource + } + + /// Create a new `Resource` by combining two resources. + /// + /// ### Key value pairs + /// Keys from the `other` resource have priority over keys from this resource, even if the + /// updated value is empty. + /// + /// ### [Schema url] + /// If both of the resource are not empty. Schema url is determined by the following rules, in order: + /// 1. If this resource has a schema url, it will be used. + /// 2. If this resource does not have a schema url, and the other resource has a schema url, it will be used. + /// 3. If both resources have a schema url and it's the same, it will be used. + /// 4. If both resources have a schema url and it's different, the schema url will be empty. + /// 5. If both resources do not have a schema url, the schema url will be empty. + /// + /// [Schema url]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url + pub(crate) fn merge>(&self, other: T) -> Self { + if self.is_empty() && self.schema_url().is_none() { + return other.clone(); + } + if other.is_empty() && other.schema_url().is_none() { + return self.clone(); + } + let mut combined_attrs = self.inner.attrs.clone(); + for (k, v) in other.inner.attrs.iter() { + combined_attrs.insert(k.clone(), v.clone()); + } + + // Resolve the schema URL according to the precedence rules + let combined_schema_url = match (&self.inner.schema_url, &other.inner.schema_url) { + // If both resources have a schema URL and it's the same, use it + (Some(url1), Some(url2)) if url1 == url2 => Some(url1.clone()), + // If both resources have a schema URL but they are not the same, the schema URL will be empty + (Some(_), Some(_)) => None, + // If this resource does not have a schema URL, and the other resource has a schema URL, it will be used + (None, Some(url)) => Some(url.clone()), + // If this resource has a schema URL, it will be used (covers case 1 and any other cases where `self` has a schema URL) + (Some(url), _) => Some(url.clone()), + // If both resources do not have a schema URL, the schema URL will be empty + (None, None) => None, + }; + Resource { + inner: Arc::new(ResourceInner { + attrs: combined_attrs, + schema_url: combined_schema_url, + }), + } + } + + /// Return the [schema url] of the resource. If the resource does not have a schema url, return `None`. + /// + /// [schema url]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url + pub fn schema_url(&self) -> Option<&str> { + self.inner.schema_url.as_ref().map(|s| s.as_ref()) + } + + /// Returns the number of attributes for this resource + pub fn len(&self) -> usize { + self.inner.attrs.len() + } + + /// Returns `true` if the resource contains no attributes. + pub fn is_empty(&self) -> bool { + self.inner.attrs.is_empty() + } + + /// Gets an iterator over the attributes of this resource. + pub fn iter(&self) -> Iter<'_> { + Iter(self.inner.attrs.iter()) + } + + /// Retrieve the value from resource associate with given key. + pub fn get(&self, key: &Key) -> Option { + self.inner.attrs.get(key).cloned() + } + + /// Returns a reference to the value for the resource associated with the given key without cloning. + pub fn get_ref(&self, key: &Key) -> Option<&Value> { + self.inner.attrs.get(key) + } +} + +/// An iterator over the entries of a `Resource`. +#[derive(Debug)] +pub struct Iter<'a>(hash_map::Iter<'a, Key, Value>); + +impl<'a> Iterator for Iter<'a> { + type Item = (&'a Key, &'a Value); + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl<'a> IntoIterator for &'a Resource { + type Item = (&'a Key, &'a Value); + type IntoIter = Iter<'a>; + + fn into_iter(self) -> Self::IntoIter { + Iter(self.inner.attrs.iter()) + } +} + +/// ResourceDetector detects OpenTelemetry resource information +/// +/// Implementations of this trait can be passed to +/// the [`ResourceBuilder::with_detectors`] function to generate a Resource from the merged information. +pub trait ResourceDetector { + /// detect returns an initialized Resource based on gathered information. + /// + /// If source information to construct a Resource is inaccessible, an empty Resource should be returned + /// + /// If source information to construct a Resource is invalid, for example, + /// missing required values. an empty Resource should be returned. + fn detect(&self) -> Resource; +} + +/// Builder for [Resource] +#[derive(Debug)] +pub struct ResourceBuilder { + resource: Resource, +} + +impl ResourceBuilder { + /// Add a single [ResourceDetector] to your resource. + pub fn with_detector(self, detector: Box) -> Self { + self.with_detectors(&[detector]) + } + + /// Add multiple [ResourceDetector]s to your resource. + pub fn with_detectors(mut self, detectors: &[Box]) -> Self { + self.resource = self.resource.merge(&Resource::from_detectors(detectors)); + self + } + + /// Add a [KeyValue] to the resource. + pub fn with_attribute(self, kv: KeyValue) -> Self { + self.with_attributes([kv]) + } + + /// Add multiple [KeyValue]s to the resource. + pub fn with_attributes>(mut self, kvs: T) -> Self { + self.resource = self.resource.merge(&Resource::new(kvs)); + self + } + + /// Add `service.name` resource attribute. + pub fn with_service_name(self, name: impl Into) -> Self { + self.with_attribute(KeyValue::new(SERVICE_NAME, name.into())) + } + + /// This will merge the provided `schema_url` with the current state of the Resource being built. It + /// will use the following rules to determine which `schema_url` should be used. + /// + /// ### [Schema url] + /// Schema url is determined by the following rules, in order: + /// 1. If the current builder resource doesn't have a `schema_url`, the provided `schema_url` will be used. + /// 2. If the current builder resource has a `schema_url`, and the provided `schema_url` is different from the builder resource, `schema_url` will be empty. + /// 3. If the provided `schema_url` is the same as the current builder resource, it will be used. + /// + /// [Schema url]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url + pub fn with_schema_url(mut self, attributes: KV, schema_url: S) -> Self + where + KV: IntoIterator, + S: Into>, + { + self.resource = Resource::from_schema_url(attributes, schema_url).merge(&self.resource); + self + } + + /// Create a [Resource] with the options provided to the [ResourceBuilder]. + pub fn build(self) -> Resource { + self.resource + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case([KeyValue::new("a", ""), KeyValue::new("a", "final")], [(Key::new("a"), Value::from("final"))])] + #[case([KeyValue::new("a", "final"), KeyValue::new("a", "")], [(Key::new("a"), Value::from(""))])] + fn new_resource( + #[case] given_attributes: [KeyValue; 2], + #[case] expected_attrs: [(Key, Value); 1], + ) { + // Arrange + let expected = HashMap::from_iter(expected_attrs.into_iter()); + + // Act + let resource = Resource::builder_empty() + .with_attributes(given_attributes) + .build(); + let resource_inner = Arc::try_unwrap(resource.inner).expect("Failed to unwrap Arc"); + + // Assert + assert_eq!(resource_inner.attrs, expected); + assert_eq!(resource_inner.schema_url, None); + } + + #[test] + fn merge_resource_key_value_pairs() { + let resource_a = Resource::builder_empty() + .with_attributes([ + KeyValue::new("a", ""), + KeyValue::new("b", "b-value"), + KeyValue::new("d", "d-value"), + ]) + .build(); + + let resource_b = Resource::builder_empty() + .with_attributes([ + KeyValue::new("a", "a-value"), + KeyValue::new("c", "c-value"), + KeyValue::new("d", ""), + ]) + .build(); + + let mut expected_attrs = HashMap::new(); + expected_attrs.insert(Key::new("a"), Value::from("a-value")); + expected_attrs.insert(Key::new("b"), Value::from("b-value")); + expected_attrs.insert(Key::new("c"), Value::from("c-value")); + expected_attrs.insert(Key::new("d"), Value::from("")); + + let expected_resource = Resource { + inner: Arc::new(ResourceInner { + attrs: expected_attrs, + schema_url: None, // Assuming schema_url handling if needed + }), + }; + + assert_eq!(resource_a.merge(&resource_b), expected_resource); + } + + #[rstest] + #[case(Some("http://schema/a"), None, Some("http://schema/a"))] + #[case(Some("http://schema/a"), Some("http://schema/b"), None)] + #[case(None, Some("http://schema/b"), Some("http://schema/b"))] + #[case( + Some("http://schema/a"), + Some("http://schema/a"), + Some("http://schema/a") + )] + #[case(None, None, None)] + fn merge_resource_schema_url( + #[case] schema_url_a: Option<&'static str>, + #[case] schema_url_b: Option<&'static str>, + #[case] expected_schema_url: Option<&'static str>, + ) { + let resource_a = + Resource::from_schema_url([KeyValue::new("key", "")], schema_url_a.unwrap_or("")); + let resource_b = + Resource::from_schema_url([KeyValue::new("key", "")], schema_url_b.unwrap_or("")); + + let merged_resource = resource_a.merge(&resource_b); + let result_schema_url = merged_resource.schema_url(); + + assert_eq!( + result_schema_url.map(|s| s as &str), + expected_schema_url, + "Merging schema_url_a {schema_url_a:?} with schema_url_b {schema_url_b:?} did not yield expected result {expected_schema_url:?}" + ); + } + + #[rstest] + #[case(vec![], vec![KeyValue::new("key", "b")], Some("http://schema/a"), None, Some("http://schema/a"))] + #[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], Some("http://schema/a"), None, Some("http://schema/a"))] + #[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], Some("http://schema/a"), None, Some("http://schema/a"))] + #[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], Some("http://schema/a"), Some("http://schema/b"), None)] + #[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], None, Some("http://schema/b"), Some("http://schema/b"))] + fn merge_resource_with_missing_attributes( + #[case] key_values_a: Vec, + #[case] key_values_b: Vec, + #[case] schema_url_a: Option<&'static str>, + #[case] schema_url_b: Option<&'static str>, + #[case] expected_schema_url: Option<&'static str>, + ) { + let resource = match schema_url_a { + Some(schema) => Resource::from_schema_url(key_values_a, schema), + None => Resource::new(key_values_a), + }; + + let other_resource = match schema_url_b { + Some(schema) => Resource::builder_empty() + .with_schema_url(key_values_b, schema) + .build(), + None => Resource::new(key_values_b), + }; + + assert_eq!( + resource.merge(&other_resource).schema_url(), + expected_schema_url + ); + } + + #[test] + fn detect_resource() { + temp_env::with_vars( + [ + ( + "OTEL_RESOURCE_ATTRIBUTES", + Some("key=value, k = v , a= x, a=z"), + ), + ("IRRELEVANT", Some("20200810")), + ], + || { + let detector = EnvResourceDetector::new(); + let resource = Resource::from_detectors(&[Box::new(detector)]); + assert_eq!( + resource, + Resource::builder_empty() + .with_attributes([ + KeyValue::new("key", "value"), + KeyValue::new("k", "v"), + KeyValue::new("a", "x"), + KeyValue::new("a", "z"), + ]) + .build() + ) + }, + ) + } + + #[rstest] + #[case(Some("http://schema/a"), Some("http://schema/b"), None)] + #[case(None, Some("http://schema/b"), Some("http://schema/b"))] + #[case( + Some("http://schema/a"), + Some("http://schema/a"), + Some("http://schema/a") + )] + fn builder_with_schema_url( + #[case] schema_url_a: Option<&'static str>, + #[case] schema_url_b: Option<&'static str>, + #[case] expected_schema_url: Option<&'static str>, + ) { + let base_builder = if let Some(url) = schema_url_a { + ResourceBuilder { + resource: Resource::from_schema_url(vec![KeyValue::new("key", "")], url), + } + } else { + ResourceBuilder { + resource: Resource::empty(), + } + }; + + let resource = base_builder + .with_schema_url( + vec![KeyValue::new("key", "")], + schema_url_b.expect("should always be Some for this test"), + ) + .build(); + + assert_eq!( + resource.schema_url().map(|s| s as &str), + expected_schema_url, + "Merging schema_url_a {schema_url_a:?} with schema_url_b {schema_url_b:?} did not yield expected result {expected_schema_url:?}" + ); + } + + #[test] + fn builder_detect_resource() { + temp_env::with_vars( + [ + ( + "OTEL_RESOURCE_ATTRIBUTES", + Some("key=value, k = v , a= x, a=z"), + ), + ("IRRELEVANT", Some("20200810")), + ], + || { + let resource = Resource::builder_empty() + .with_detector(Box::new(EnvResourceDetector::new())) + .with_service_name("testing_service") + .with_attribute(KeyValue::new("test1", "test_value")) + .with_attributes([ + KeyValue::new("test1", "test_value1"), + KeyValue::new("test2", "test_value2"), + ]) + .build(); + + assert_eq!( + resource, + Resource::builder_empty() + .with_attributes([ + KeyValue::new("key", "value"), + KeyValue::new("test1", "test_value1"), + KeyValue::new("test2", "test_value2"), + KeyValue::new(SERVICE_NAME, "testing_service"), + KeyValue::new("k", "v"), + KeyValue::new("a", "x"), + KeyValue::new("a", "z"), + ]) + .build() + ) + }, + ) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/telemetry.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/telemetry.rs new file mode 100644 index 00000000..cfc657ec --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/resource/telemetry.rs @@ -0,0 +1,27 @@ +use crate::resource::ResourceDetector; +use crate::Resource; +use opentelemetry::KeyValue; + +/// Detect the telemetry SDK information used to capture data recorded by the instrumentation libraries. +/// +/// It provides: +/// - The name of the telemetry SDK(`telemetry.sdk.name`). It will be `opentelemetry` for SDK provided by opentelemetry project. +/// - The language of the telemetry SDK(`telemetry.sdk.language`). It will be `rust` for this SDK. +/// - The version of the telemetry SDK(`telemetry.sdk.version`). It will be current `opentelemetry_sdk` crate version. +/// +/// +/// See [semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md#telemetry-sdk) for details. +#[derive(Debug)] +pub struct TelemetryResourceDetector; + +impl ResourceDetector for TelemetryResourceDetector { + fn detect(&self) -> Resource { + Resource::builder_empty() + .with_attributes([ + KeyValue::new(super::TELEMETRY_SDK_NAME, "opentelemetry"), + KeyValue::new(super::TELEMETRY_SDK_LANGUAGE, "rust"), + KeyValue::new(super::TELEMETRY_SDK_VERSION, env!("CARGO_PKG_VERSION")), + ]) + .build() + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/runtime.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/runtime.rs new file mode 100644 index 00000000..8ae9c61d --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/runtime.rs @@ -0,0 +1,275 @@ +//! Provides an abstraction of several async runtimes +//! +//! This allows OpenTelemetry to work with any current or future runtime. There is currently +//! built-in implementation for [Tokio]. +//! +//! [Tokio]: https://crates.io/crates/tokio + +use futures_util::stream::{unfold, Stream}; +use std::{fmt::Debug, future::Future, time::Duration}; +use thiserror::Error; + +/// A runtime is an abstraction of an async runtime like [Tokio]. It allows +/// OpenTelemetry to work with any current and hopefully future runtime implementations. +/// +/// [Tokio]: https://crates.io/crates/tokio +/// +/// # Note +/// +/// OpenTelemetry expects a *multithreaded* runtime because its types can move across threads. +/// For this reason, this trait requires the `Send` and `Sync` bounds. Single-threaded runtimes +/// can implement this trait in a way that spawns the tasks on the same thread as the calling code. +#[cfg(feature = "experimental_async_runtime")] +pub trait Runtime: Clone + Send + Sync + 'static { + /// Spawn a new task or thread, which executes the given future. + /// + /// # Note + /// + /// This is mainly used to run batch span processing in the background. Note, that the function + /// does not return a handle. OpenTelemetry will use a different way to wait for the future to + /// finish when the caller shuts down. + /// + /// At the moment, the shutdown happens by blocking the + /// current thread. This means runtime implementations need to make sure they can still execute + /// the given future even if the main thread is blocked. + fn spawn(&self, future: F) + where + F: Future + Send + 'static; + + /// Return a future that resolves after the specified [Duration]. + fn delay(&self, duration: Duration) -> impl Future + Send + 'static; +} + +/// Uses the given runtime to produce an interval stream. +#[cfg(feature = "experimental_async_runtime")] +#[allow(dead_code)] +pub(crate) fn to_interval_stream( + runtime: T, + interval: Duration, +) -> impl Stream { + unfold((), move |_| { + let runtime_cloned = runtime.clone(); + + async move { + runtime_cloned.delay(interval).await; + Some(((), ())) + } + }) +} + +/// Runtime implementation, which works with Tokio's multi thread runtime. +#[cfg(all(feature = "experimental_async_runtime", feature = "rt-tokio"))] +#[cfg_attr( + docsrs, + doc(cfg(all(feature = "experimental_async_runtime", feature = "rt-tokio"))) +)] +#[derive(Debug, Clone)] +pub struct Tokio; + +#[cfg(all(feature = "experimental_async_runtime", feature = "rt-tokio"))] +#[cfg_attr( + docsrs, + doc(cfg(all(feature = "experimental_async_runtime", feature = "rt-tokio"))) +)] +impl Runtime for Tokio { + fn spawn(&self, future: F) + where + F: Future + Send + 'static, + { + #[allow(clippy::let_underscore_future)] + // we don't have to await on the returned future to execute + let _ = tokio::spawn(future); + } + + fn delay(&self, duration: Duration) -> impl Future + Send + 'static { + tokio::time::sleep(duration) + } +} + +/// Runtime implementation, which works with Tokio's current thread runtime. +#[cfg(all( + feature = "experimental_async_runtime", + feature = "rt-tokio-current-thread" +))] +#[cfg_attr( + docsrs, + doc(cfg(all( + feature = "experimental_async_runtime", + feature = "rt-tokio-current-thread" + ))) +)] +#[derive(Debug, Clone)] +pub struct TokioCurrentThread; + +#[cfg(all( + feature = "experimental_async_runtime", + feature = "rt-tokio-current-thread" +))] +#[cfg_attr( + docsrs, + doc(cfg(all( + feature = "experimental_async_runtime", + feature = "rt-tokio-current-thread" + ))) +)] +impl Runtime for TokioCurrentThread { + fn spawn(&self, future: F) + where + F: Future + Send + 'static, + { + // We cannot force push tracing in current thread tokio scheduler because we rely on + // BatchSpanProcessor to export spans in a background task, meanwhile we need to block the + // shutdown function so that the runtime will not finish the blocked task and kill any + // remaining tasks. But there is only one thread to run task, so it's a deadlock + // + // Thus, we spawn the background task in a separate thread. + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("failed to create Tokio current thead runtime for OpenTelemetry batch processing"); + rt.block_on(future); + }); + } + + fn delay(&self, duration: Duration) -> impl Future + Send + 'static { + tokio::time::sleep(duration) + } +} + +/// `RuntimeChannel` is an extension to [`Runtime`]. Currently, it provides a +/// channel that is used by the [log] and [span] batch processors. +/// +/// [log]: crate::logs::BatchLogProcessor +/// [span]: crate::trace::BatchSpanProcessor +#[cfg(feature = "experimental_async_runtime")] +pub trait RuntimeChannel: Runtime { + /// A future stream to receive batch messages from channels. + type Receiver: Stream + Send; + /// A batch messages sender that can be sent across threads safely. + type Sender: TrySend + Debug; + + /// Return the sender and receiver used to send batch messages. + fn batch_message_channel( + &self, + capacity: usize, + ) -> (Self::Sender, Self::Receiver); +} + +/// Error returned by a [`TrySend`] implementation. +#[cfg(feature = "experimental_async_runtime")] +#[derive(Debug, Error)] +pub enum TrySendError { + /// Send failed due to the channel being full. + #[error("cannot send message to batch processor as the channel is full")] + ChannelFull, + /// Send failed due to the channel being closed. + #[error("cannot send message to batch processor as the channel is closed")] + ChannelClosed, + /// Any other send error that isn't covered above. + #[error(transparent)] + Other(#[from] Box), +} + +/// TrySend is an abstraction of `Sender` that is capable of sending messages through a reference. +#[cfg(feature = "experimental_async_runtime")] +pub trait TrySend: Sync + Send { + /// The message that will be sent. + type Message; + + /// Try to send a message batch to a worker thread. + /// + /// A failure can be due to either a closed receiver, or a depleted buffer. + fn try_send(&self, item: Self::Message) -> Result<(), TrySendError>; +} + +#[cfg(all( + feature = "experimental_async_runtime", + any(feature = "rt-tokio", feature = "rt-tokio-current-thread") +))] +impl TrySend for tokio::sync::mpsc::Sender { + type Message = T; + + fn try_send(&self, item: Self::Message) -> Result<(), TrySendError> { + self.try_send(item).map_err(|err| match err { + tokio::sync::mpsc::error::TrySendError::Full(_) => TrySendError::ChannelFull, + tokio::sync::mpsc::error::TrySendError::Closed(_) => TrySendError::ChannelClosed, + }) + } +} + +#[cfg(all(feature = "experimental_async_runtime", feature = "rt-tokio"))] +#[cfg_attr( + docsrs, + doc(cfg(all(feature = "experimental_async_runtime", feature = "rt-tokio"))) +)] +impl RuntimeChannel for Tokio { + type Receiver = tokio_stream::wrappers::ReceiverStream; + type Sender = tokio::sync::mpsc::Sender; + + fn batch_message_channel( + &self, + capacity: usize, + ) -> (Self::Sender, Self::Receiver) { + let (sender, receiver) = tokio::sync::mpsc::channel(capacity); + ( + sender, + tokio_stream::wrappers::ReceiverStream::new(receiver), + ) + } +} + +#[cfg(all( + feature = "experimental_async_runtime", + feature = "rt-tokio-current-thread" +))] +#[cfg_attr( + docsrs, + doc(cfg(all( + feature = "experimental_async_runtime", + feature = "rt-tokio-current-thread" + ))) +)] +impl RuntimeChannel for TokioCurrentThread { + type Receiver = tokio_stream::wrappers::ReceiverStream; + type Sender = tokio::sync::mpsc::Sender; + + fn batch_message_channel( + &self, + capacity: usize, + ) -> (Self::Sender, Self::Receiver) { + let (sender, receiver) = tokio::sync::mpsc::channel(capacity); + ( + sender, + tokio_stream::wrappers::ReceiverStream::new(receiver), + ) + } +} + +/// Runtime implementation for synchronous execution environments. +/// +/// This runtime can be used when executing in a non-async environment. +/// The runtime methods will perform their operations synchronously. +#[cfg(feature = "experimental_async_runtime")] +#[derive(Debug, Clone, Copy)] +pub struct NoAsync; + +#[cfg(feature = "experimental_async_runtime")] +impl Runtime for NoAsync { + fn spawn(&self, future: F) + where + F: Future + Send + 'static, + { + std::thread::spawn(move || { + futures_executor::block_on(future); + }); + } + + // Needed because async fn would borrow `self`, violating the `'static` requirement. + #[allow(clippy::manual_async_fn)] + fn delay(&self, duration: Duration) -> impl Future + Send + 'static { + async move { + std::thread::sleep(duration); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/metrics/metric_reader.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/metrics/metric_reader.rs new file mode 100644 index 00000000..27f72321 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/metrics/metric_reader.rs @@ -0,0 +1,57 @@ +use crate::error::{OTelSdkError, OTelSdkResult}; +use crate::metrics::Temporality; +use crate::metrics::{ + data::ResourceMetrics, instrument::InstrumentKind, pipeline::Pipeline, reader::MetricReader, +}; +use std::sync::{Arc, Mutex, Weak}; +use std::time::Duration; + +#[derive(Debug, Clone)] +pub struct TestMetricReader { + is_shutdown: Arc>, +} + +impl TestMetricReader { + // Constructor to initialize the TestMetricReader + pub fn new() -> Self { + TestMetricReader { + is_shutdown: Arc::new(Mutex::new(false)), + } + } + + // Method to check if the reader is shutdown + pub fn is_shutdown(&self) -> bool { + *self.is_shutdown.lock().unwrap() + } +} + +impl Default for TestMetricReader { + fn default() -> Self { + Self::new() + } +} + +impl MetricReader for TestMetricReader { + fn register_pipeline(&self, _pipeline: Weak) {} + + fn collect(&self, _rm: &mut ResourceMetrics) -> OTelSdkResult { + Ok(()) + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + let result = self.force_flush(); + { + let mut is_shutdown = self.is_shutdown.lock().unwrap(); + *is_shutdown = true; + } + result.map_err(|e| OTelSdkError::InternalFailure(e.to_string())) + } + + fn temporality(&self, _kind: InstrumentKind) -> Temporality { + Temporality::default() + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/metrics/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/metrics/mod.rs new file mode 100644 index 00000000..87b218c6 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/metrics/mod.rs @@ -0,0 +1,4 @@ +//! Structs for tests. +#[doc(hidden)] +pub mod metric_reader; +pub use metric_reader::TestMetricReader; diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/mod.rs new file mode 100644 index 00000000..97469caa --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/mod.rs @@ -0,0 +1,8 @@ +//! In-Memory exporters for testing purpose. + +/// Structs used for testing +#[cfg(all(feature = "testing", feature = "trace"))] +pub mod trace; + +#[cfg(all(feature = "testing", feature = "metrics"))] +pub mod metrics; diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/trace/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/trace/mod.rs new file mode 100644 index 00000000..48a887d2 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/trace/mod.rs @@ -0,0 +1,3 @@ +#[doc(hidden)] +mod span_exporters; +pub use span_exporters::*; diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/trace/span_exporters.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/trace/span_exporters.rs new file mode 100644 index 00000000..6f714e51 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/testing/trace/span_exporters.rs @@ -0,0 +1,117 @@ +use crate::error::{OTelSdkError, OTelSdkResult}; +use crate::{ + trace::{SpanData, SpanExporter}, + trace::{SpanEvents, SpanLinks}, + ExportError, +}; +pub use opentelemetry::testing::trace::TestSpan; +use opentelemetry::{ + trace::{SpanContext, SpanId, SpanKind, Status, TraceFlags, TraceId, TraceState}, + InstrumentationScope, +}; +use std::fmt::{Display, Formatter}; + +pub fn new_test_export_span_data() -> SpanData { + SpanData { + span_context: SpanContext::new( + TraceId::from(1), + SpanId::from(1), + TraceFlags::SAMPLED, + false, + TraceState::default(), + ), + parent_span_id: SpanId::INVALID, + parent_span_is_remote: false, + span_kind: SpanKind::Internal, + name: "opentelemetry".into(), + start_time: opentelemetry::time::now(), + end_time: opentelemetry::time::now(), + attributes: Vec::new(), + dropped_attributes_count: 0, + events: SpanEvents::default(), + links: SpanLinks::default(), + status: Status::Unset, + instrumentation_scope: InstrumentationScope::default(), + } +} + +#[derive(Debug)] +pub struct TestSpanExporter { + tx_export: tokio::sync::mpsc::UnboundedSender, + tx_shutdown: tokio::sync::mpsc::UnboundedSender<()>, +} + +impl SpanExporter for TestSpanExporter { + async fn export(&self, batch: Vec) -> OTelSdkResult { + batch.into_iter().try_for_each(|span_data| { + self.tx_export + .send(span_data) + .map_err(|err| OTelSdkError::InternalFailure(format!("Export failed: {err:?}"))) + }) + } + + fn shutdown(&self) -> OTelSdkResult { + self.tx_shutdown.send(()).map_err(|_| { + OTelSdkError::InternalFailure("Failed to send shutdown signal".to_string()) + }) + } +} + +pub fn new_test_exporter() -> ( + TestSpanExporter, + tokio::sync::mpsc::UnboundedReceiver, + tokio::sync::mpsc::UnboundedReceiver<()>, +) { + let (tx_export, rx_export) = tokio::sync::mpsc::unbounded_channel(); + let (tx_shutdown, rx_shutdown) = tokio::sync::mpsc::unbounded_channel(); + let exporter = TestSpanExporter { + tx_export, + tx_shutdown, + }; + (exporter, rx_export, rx_shutdown) +} + +#[derive(Debug)] +pub struct TestExportError(String); + +impl std::error::Error for TestExportError {} + +impl ExportError for TestExportError { + fn exporter_name(&self) -> &'static str { + "test" + } +} + +impl Display for TestExportError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +impl From> for TestExportError { + fn from(err: tokio::sync::mpsc::error::SendError) -> Self { + TestExportError(err.to_string()) + } +} + +/// A no-op instance of an [`SpanExporter`]. +/// +/// [`SpanExporter`]: crate::trace::SpanExporter +#[derive(Debug, Default)] +pub struct NoopSpanExporter { + _private: (), +} + +impl NoopSpanExporter { + /// Create a new noop span exporter + pub fn new() -> Self { + NoopSpanExporter { _private: () } + } +} + +impl SpanExporter for NoopSpanExporter { + async fn export(&self, _: Vec) -> OTelSdkResult { + Ok(()) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/config.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/config.rs new file mode 100644 index 00000000..67db9509 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/config.rs @@ -0,0 +1,137 @@ +//! SDK Configuration +//! +//! Configuration represents the global tracing configuration, overrides +//! can be set for the default OpenTelemetry limits and Sampler. +use crate::trace::{span_limit::SpanLimits, IdGenerator, RandomIdGenerator, Sampler, ShouldSample}; +use crate::Resource; +use opentelemetry::otel_warn; +use std::borrow::Cow; +use std::env; +use std::str::FromStr; + +/// Tracer configuration +#[derive(Debug)] +#[non_exhaustive] +pub struct Config { + /// The sampler that the sdk should use + pub sampler: Box, + + /// The id generator that the sdk should use + pub id_generator: Box, + + /// span limits + pub span_limits: SpanLimits, + + /// Contains attributes representing an entity that produces telemetry. + pub resource: Cow<'static, Resource>, +} + +impl Default for Config { + /// Create default global sdk configuration. + fn default() -> Self { + let mut config = Config { + sampler: Box::new(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))), + id_generator: Box::::default(), + span_limits: SpanLimits::default(), + resource: Cow::Owned(Resource::builder().build()), + }; + + if let Some(max_attributes_per_span) = env::var("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT") + .ok() + .and_then(|count_limit| u32::from_str(&count_limit).ok()) + { + config.span_limits.max_attributes_per_span = max_attributes_per_span; + } + + if let Some(max_events_per_span) = env::var("OTEL_SPAN_EVENT_COUNT_LIMIT") + .ok() + .and_then(|max_events| u32::from_str(&max_events).ok()) + { + config.span_limits.max_events_per_span = max_events_per_span; + } + + if let Some(max_links_per_span) = env::var("OTEL_SPAN_LINK_COUNT_LIMIT") + .ok() + .and_then(|max_links| u32::from_str(&max_links).ok()) + { + config.span_limits.max_links_per_span = max_links_per_span; + } + + let sampler_arg = env::var("OTEL_TRACES_SAMPLER_ARG").ok(); + if let Ok(sampler) = env::var("OTEL_TRACES_SAMPLER") { + config.sampler = match sampler.as_str() { + "always_on" => Box::new(Sampler::AlwaysOn), + "always_off" => Box::new(Sampler::AlwaysOff), + "traceidratio" => { + let ratio = sampler_arg.as_ref().and_then(|r| r.parse::().ok()); + if let Some(r) = ratio { + Box::new(Sampler::TraceIdRatioBased(r)) + } else { + otel_warn!( + name: "TracerProvider.Config.InvalidSamplerArgument", + message = "OTEL_TRACES_SAMPLER is set to 'traceidratio' but OTEL_TRACES_SAMPLER_ARG environment variable is missing or invalid. OTEL_TRACES_SAMPLER_ARG must be a valid float between 0.0 and 1.0 representing the desired sampling probability (0.0 = no traces sampled, 1.0 = all traces sampled, 0.5 = 50% of traces sampled). Falling back to default ratio: 1.0 (100% sampling)", + otel_traces_sampler_arg = format!("{:?}", sampler_arg) + ); + Box::new(Sampler::TraceIdRatioBased(1.0)) + } + } + "parentbased_always_on" => { + Box::new(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))) + } + "parentbased_always_off" => { + Box::new(Sampler::ParentBased(Box::new(Sampler::AlwaysOff))) + } + "parentbased_traceidratio" => { + let ratio = sampler_arg.as_ref().and_then(|r| r.parse::().ok()); + if let Some(r) = ratio { + Box::new(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased( + r, + )))) + } else { + otel_warn!( + name: "TracerProvider.Config.InvalidSamplerArgument", + message = "OTEL_TRACES_SAMPLER is set to 'parentbased_traceidratio' but OTEL_TRACES_SAMPLER_ARG environment variable is missing or invalid. OTEL_TRACES_SAMPLER_ARG must be a valid float between 0.0 and 1.0 representing the desired sampling probability (0.0 = no traces sampled, 1.0 = all traces sampled, 0.5 = 50% of traces sampled). Falling back to default ratio: 1.0 (100% sampling)", + otel_traces_sampler_arg = format!("{:?}", sampler_arg) + ); + Box::new(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased( + 1.0, + )))) + } + } + "parentbased_jaeger_remote" => { + otel_warn!( + name: "TracerProvider.Config.UnsupportedSampler", + message = "OTEL_TRACES_SAMPLER is set to 'parentbased_jaeger_remote' which is not implemented in this SDK version. Using fallback sampler: ParentBased(AlwaysOn). Configure an alternative sampler using OTEL_TRACES_SAMPLER" + ); + Box::new(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))) + } + "jaeger_remote" => { + otel_warn!( + name: "TracerProvider.Config.UnsupportedSampler", + message = "OTEL_TRACES_SAMPLER is set to 'jaeger_remote' which is implemented in this SDK version. Using fallback sampler: ParentBased(AlwaysOn). Configure an alternative sampler using OTEL_TRACES_SAMPLER" + ); + Box::new(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))) + } + "xray" => { + otel_warn!( + name: "TracerProvider.Config.UnsupportedSampler", + message = "OTEL_TRACES_SAMPLER is set to 'xray'. AWS X-Ray sampler is not implemented in this SDK version. Using fallback sampler: ParentBased(AlwaysOn). Configure an alternative sampler using OTEL_TRACES_SAMPLER" + ); + Box::new(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))) + } + s => { + otel_warn!( + name: "TracerProvider.Config.InvalidSamplerType", + message = format!( + "Unrecognized sampler type '{}' in OTEL_TRACES_SAMPLER environment variable. Valid values are: always_on, always_off, traceidratio, parentbased_always_on, parentbased_always_off, parentbased_traceidratio. Using fallback sampler: ParentBased(AlwaysOn)", + s + ), + ); + Box::new(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))) + } + } + } + + config + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/events.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/events.rs new file mode 100644 index 00000000..fbf2bcb9 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/events.rs @@ -0,0 +1,37 @@ +//! # Span Events + +use std::ops::Deref; + +use opentelemetry::trace::Event; +/// Stores span events along with dropped count. +#[derive(Clone, Debug, Default, PartialEq)] +#[non_exhaustive] +pub struct SpanEvents { + /// The events stored as a vector. Could be empty if there are no events. + pub events: Vec, + /// The number of Events dropped from the span. + pub dropped_count: u32, +} + +impl Deref for SpanEvents { + type Target = [Event]; + + fn deref(&self) -> &Self::Target { + &self.events + } +} + +impl IntoIterator for SpanEvents { + type Item = Event; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.events.into_iter() + } +} + +impl SpanEvents { + pub(crate) fn add_event(&mut self, event: Event) { + self.events.push(event); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/export.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/export.rs new file mode 100644 index 00000000..f663d9e9 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/export.rs @@ -0,0 +1,108 @@ +//! Trace exporters +use crate::error::OTelSdkResult; +use crate::Resource; +use opentelemetry::trace::{SpanContext, SpanId, SpanKind, Status}; +use opentelemetry::{InstrumentationScope, KeyValue}; +use std::borrow::Cow; +use std::fmt::Debug; +use std::time::{Duration, SystemTime}; + +/// `SpanExporter` defines the interface that protocol-specific exporters must +/// implement so that they can be plugged into OpenTelemetry SDK and support +/// sending of telemetry data. +/// +/// The goal of the interface is to minimize burden of implementation for +/// protocol-dependent telemetry exporters. The protocol exporter is expected to +/// be primarily a simple telemetry data encoder and transmitter. +pub trait SpanExporter: Send + Sync + Debug { + /// Exports a batch of readable spans. Protocol exporters that will + /// implement this function are typically expected to serialize and transmit + /// the data to the destination. + /// + /// This function will never be called concurrently for the same exporter + /// instance. It can be called again only after the current call returns. + /// + /// This function must not block indefinitely, there must be a reasonable + /// upper limit after which the call must time out with an error result. + /// + /// Any retry logic that is required by the exporter is the responsibility + /// of the exporter. + fn export( + &self, + batch: Vec, + ) -> impl std::future::Future + Send; + + /// Shuts down the exporter. Called when SDK is shut down. This is an + /// opportunity for exporter to do any cleanup required. + /// + /// This function should be called only once for each `SpanExporter` + /// instance. After the call to `shutdown`, subsequent calls to `export` are + /// not allowed and should return an error. + /// + /// This function should not block indefinitely (e.g. if it attempts to + /// flush the data and the destination is unavailable). SDK authors + /// can decide if they want to make the shutdown timeout + /// configurable. + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + Ok(()) + } + /// Shuts down the exporter with default timeout. + fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } + + /// This is a hint to ensure that the export of any Spans the exporter + /// has received prior to the call to this function SHOULD be completed + /// as soon as possible, preferably before returning from this method. + /// + /// This function SHOULD provide a way to let the caller know + /// whether it succeeded, failed or timed out. + /// + /// This function SHOULD only be called in cases where it is absolutely necessary, + /// such as when using some FaaS providers that may suspend the process after + /// an invocation, but before the exporter exports the completed spans. + /// + /// This function SHOULD complete or abort within some timeout. This function can be + /// implemented as a blocking API or an asynchronous API which notifies the caller via + /// a callback or an event. OpenTelemetry client authors can decide if they want to + /// make the flush timeout configurable. + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + /// Set the resource for the exporter. + fn set_resource(&mut self, _resource: &Resource) {} +} + +/// `SpanData` contains all the information collected by a `Span` and can be used +/// by exporters as a standard input. +#[derive(Clone, Debug, PartialEq)] +pub struct SpanData { + /// Exportable `SpanContext` + pub span_context: SpanContext, + /// Span parent id + pub parent_span_id: SpanId, + /// Parent span is remote flag (for span flags) + pub parent_span_is_remote: bool, + /// Span kind + pub span_kind: SpanKind, + /// Span name + pub name: Cow<'static, str>, + /// Span start time + pub start_time: SystemTime, + /// Span end time + pub end_time: SystemTime, + /// Span attributes + pub attributes: Vec, + /// The number of attributes that were above the configured limit, and thus + /// dropped. + pub dropped_attributes_count: u32, + /// Span events + pub events: crate::trace::SpanEvents, + /// Span Links + pub links: crate::trace::SpanLinks, + /// Span status + pub status: Status, + /// Instrumentation scope that produced this span + pub instrumentation_scope: InstrumentationScope, +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/id_generator/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/id_generator/mod.rs new file mode 100644 index 00000000..c753de15 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/id_generator/mod.rs @@ -0,0 +1,36 @@ +use opentelemetry::trace::{SpanId, TraceId}; +use rand::{rngs, Rng, SeedableRng}; +use std::cell::RefCell; +use std::fmt; + +/// Interface for generating IDs +pub trait IdGenerator: Send + Sync + fmt::Debug { + /// Generate a new `TraceId` + fn new_trace_id(&self) -> TraceId; + + /// Generate a new `SpanId` + fn new_span_id(&self) -> SpanId; +} + +/// Default [`IdGenerator`] implementation. +/// +/// Generates Trace and Span ids using a random number generator. +#[derive(Clone, Debug, Default)] +pub struct RandomIdGenerator { + _private: (), +} + +impl IdGenerator for RandomIdGenerator { + fn new_trace_id(&self) -> TraceId { + CURRENT_RNG.with(|rng| TraceId::from(rng.borrow_mut().random::())) + } + + fn new_span_id(&self) -> SpanId { + CURRENT_RNG.with(|rng| SpanId::from(rng.borrow_mut().random::())) + } +} + +thread_local! { + /// Store random number generator for each thread + static CURRENT_RNG: RefCell = RefCell::new(rngs::SmallRng::from_os_rng()); +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/in_memory_exporter.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/in_memory_exporter.rs new file mode 100644 index 00000000..20068c3b --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/in_memory_exporter.rs @@ -0,0 +1,179 @@ +use crate::error::{OTelSdkError, OTelSdkResult}; +use crate::resource::Resource; +use crate::trace::{SpanData, SpanExporter}; +use std::sync::{atomic::AtomicBool, Arc, Mutex}; +use std::time::Duration; + +/// An in-memory span exporter that stores span data in memory. +/// +/// This exporter is useful for testing and debugging purposes. It stores +/// span data in a `Vec`. Spans can be retrieved +/// using the `get_finished_spans` method. +/// # Example +/// ``` +///# use opentelemetry::trace::{SpanKind, TraceContextExt}; +///# use opentelemetry::{global, trace::Tracer, Context}; +///# use opentelemetry_sdk::propagation::TraceContextPropagator; +///# use opentelemetry_sdk::runtime; +///# use opentelemetry_sdk::trace::InMemorySpanExporterBuilder; +///# use opentelemetry_sdk::trace::{BatchSpanProcessor, SdkTracerProvider}; +/// +///# #[tokio::main] +///# async fn main() { +/// let exporter = InMemorySpanExporterBuilder::new().build(); +/// let provider = SdkTracerProvider::builder() +/// .with_span_processor(BatchSpanProcessor::builder(exporter.clone()).build()) +/// .build(); +/// +/// global::set_tracer_provider(provider.clone()); +/// +/// let tracer = global::tracer("example/in_memory_exporter"); +/// let span = tracer +/// .span_builder("say hello") +/// .with_kind(SpanKind::Server) +/// .start(&tracer); +/// +/// let cx = Context::current_with_span(span); +/// cx.span().add_event("handling this...", Vec::new()); +/// cx.span().end(); +/// +/// if let Err(e) = provider.force_flush() { +/// println!("{:?}", e) +/// } +/// let spans = exporter.get_finished_spans().unwrap(); +/// for span in spans { +/// println!("{:?}", span) +/// } +///# } +/// ``` +#[derive(Clone, Debug)] +pub struct InMemorySpanExporter { + spans: Arc>>, + resource: Arc>, + should_reset_on_shutdown: bool, + shutdown_called: Arc, +} + +impl Default for InMemorySpanExporter { + fn default() -> Self { + InMemorySpanExporterBuilder::new().build() + } +} + +/// Builder for [`InMemorySpanExporter`]. +/// # Example +/// ``` +///# use opentelemetry_sdk::trace::InMemorySpanExporterBuilder; +/// +/// let exporter = InMemorySpanExporterBuilder::new().build(); +/// ``` +#[derive(Clone, Debug)] +pub struct InMemorySpanExporterBuilder { + reset_on_shutdown: bool, +} + +impl Default for InMemorySpanExporterBuilder { + fn default() -> Self { + Self::new() + } +} + +impl InMemorySpanExporterBuilder { + /// Creates a new instance of the `InMemorySpanExporterBuilder`. + pub fn new() -> Self { + Self { + reset_on_shutdown: true, + } + } + + /// Creates a new instance of the `InMemorySpanExporter`. + pub fn build(&self) -> InMemorySpanExporter { + InMemorySpanExporter { + spans: Arc::new(Mutex::new(Vec::new())), + resource: Arc::new(Mutex::new(Resource::builder().build())), + should_reset_on_shutdown: self.reset_on_shutdown, + shutdown_called: Arc::new(AtomicBool::new(false)), + } + } + + /// If set, the records will not be [`InMemorySpanExporter::reset`] on shutdown. + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn keep_records_on_shutdown(self) -> Self { + Self { + reset_on_shutdown: false, + } + } +} + +impl InMemorySpanExporter { + /// Returns true if shutdown was called. + pub fn is_shutdown_called(&self) -> bool { + self.shutdown_called + .load(std::sync::atomic::Ordering::Relaxed) + } + + /// Returns the finished span as a vector of `SpanData`. + /// + /// # Errors + /// + /// Returns an `OTelSdkError`. + /// + /// # Example + /// + /// ``` + /// # use opentelemetry_sdk::trace::InMemorySpanExporter; + /// + /// let exporter = InMemorySpanExporter::default(); + /// let finished_spans = exporter.get_finished_spans().unwrap(); + /// ``` + pub fn get_finished_spans(&self) -> Result, OTelSdkError> { + let spans = self + .spans + .lock() + .map(|spans_guard| spans_guard.iter().cloned().collect()) + .map_err(OTelSdkError::from)?; + Ok(spans) + } + + /// Clears the internal storage of finished spans. + /// + /// # Example + /// + /// ``` + /// # use opentelemetry_sdk::trace::InMemorySpanExporter; + /// + /// let exporter = InMemorySpanExporter::default(); + /// exporter.reset(); + /// ``` + pub fn reset(&self) { + let _ = self.spans.lock().map(|mut spans_guard| spans_guard.clear()); + } +} + +impl SpanExporter for InMemorySpanExporter { + async fn export(&self, batch: Vec) -> OTelSdkResult { + let result = self + .spans + .lock() + .map(|mut spans_guard| spans_guard.append(&mut batch.clone())) + .map_err(|err| OTelSdkError::InternalFailure(format!("Failed to lock spans: {err:?}"))); + result + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + self.shutdown_called + .store(true, std::sync::atomic::Ordering::Relaxed); + if self.should_reset_on_shutdown { + self.reset(); + } + Ok(()) + } + + fn set_resource(&mut self, resource: &Resource) { + self.resource + .lock() + .map(|mut res_guard| *res_guard = resource.clone()) + .expect("Resource lock poisoned"); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/links.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/links.rs new file mode 100644 index 00000000..33d8074f --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/links.rs @@ -0,0 +1,37 @@ +//! # Span Links + +use std::ops::Deref; + +use opentelemetry::trace::Link; +/// Stores span links along with dropped count. +#[derive(Clone, Debug, Default, PartialEq)] +#[non_exhaustive] +pub struct SpanLinks { + /// The links stored as a vector. Could be empty if there are no links. + pub links: Vec, + /// The number of links dropped from the span. + pub dropped_count: u32, +} + +impl Deref for SpanLinks { + type Target = [Link]; + + fn deref(&self) -> &Self::Target { + &self.links + } +} + +impl IntoIterator for SpanLinks { + type Item = Link; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.links.into_iter() + } +} + +impl SpanLinks { + pub(crate) fn add_link(&mut self, link: Link) { + self.links.push(link); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/mod.rs new file mode 100644 index 00000000..bbbc52a1 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/mod.rs @@ -0,0 +1,608 @@ +//! # OpenTelemetry Trace SDK +//! +//! The tracing SDK consist of a few main structs: +//! +//! * The [`SdkTracer`] struct which performs all tracing operations. +//! * The [`Span`] struct with is a mutable object storing information about the +//! current operation execution. +//! * The [`SdkTracerProvider`] struct which configures and produces [`SdkTracer`]s. +mod config; +mod events; +mod export; +mod id_generator; +mod links; +mod provider; +mod sampler; +mod span; +mod span_limit; +mod span_processor; +#[cfg(feature = "experimental_trace_batch_span_processor_with_async_runtime")] +/// Experimental feature to use async runtime with batch span processor. +pub mod span_processor_with_async_runtime; +mod tracer; + +pub use config::Config; +pub use events::SpanEvents; +pub use export::{SpanData, SpanExporter}; + +/// In-Memory span exporter for testing purpose. +#[cfg(any(feature = "testing", test))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "testing", test))))] +pub mod in_memory_exporter; +#[cfg(any(feature = "testing", test))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "testing", test))))] +pub use in_memory_exporter::{InMemorySpanExporter, InMemorySpanExporterBuilder}; + +pub use id_generator::{IdGenerator, RandomIdGenerator}; +pub use links::SpanLinks; +pub use provider::{SdkTracerProvider, TracerProviderBuilder}; +pub use sampler::{Sampler, SamplingDecision, SamplingResult, ShouldSample}; +pub use span::Span; +pub use span_limit::SpanLimits; +pub use span_processor::{ + BatchConfig, BatchConfigBuilder, BatchSpanProcessor, BatchSpanProcessorBuilder, + SimpleSpanProcessor, SpanProcessor, OTEL_BSP_EXPORT_TIMEOUT, OTEL_BSP_EXPORT_TIMEOUT_DEFAULT, + OTEL_BSP_MAX_EXPORT_BATCH_SIZE, OTEL_BSP_MAX_EXPORT_BATCH_SIZE_DEFAULT, + OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_MAX_QUEUE_SIZE_DEFAULT, OTEL_BSP_SCHEDULE_DELAY, + OTEL_BSP_SCHEDULE_DELAY_DEFAULT, +}; + +pub use tracer::SdkTracer; +pub use tracer::SdkTracer as Tracer; // for back-compat else tracing-opentelemetry won't build + +#[cfg(feature = "jaeger_remote_sampler")] +pub use sampler::{JaegerRemoteSampler, JaegerRemoteSamplerBuilder}; + +#[cfg(feature = "experimental_trace_batch_span_processor_with_async_runtime")] +#[cfg(test)] +mod runtime_tests; + +#[cfg(all(test, feature = "testing"))] +mod tests { + use super::*; + use crate::error::OTelSdkResult; + use crate::trace::{SamplingDecision, SamplingResult}; + use crate::{ + trace::span_limit::{DEFAULT_MAX_EVENT_PER_SPAN, DEFAULT_MAX_LINKS_PER_SPAN}, + trace::{InMemorySpanExporter, InMemorySpanExporterBuilder}, + }; + use opentelemetry::{ + baggage::BaggageExt, + trace::{get_active_span, SpanKind, Status, TraceContextExt, TraceState}, + }; + use opentelemetry::{testing::trace::TestSpan, InstrumentationScope}; + use opentelemetry::{ + trace::{ + Event, Link, Span, SpanBuilder, SpanContext, SpanId, TraceFlags, TraceId, Tracer, + TracerProvider, + }, + Context, KeyValue, + }; + use std::time::Duration; + + // Regression test for https://github.com/open-telemetry/opentelemetry-rust/issues/3510 + #[test] + fn nested_span_in_get_active_span_with_simple_processor() { + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + let tracer = provider.tracer("test_tracer"); + + get_active_span(|_span| { + let _nested = tracer.span_builder("nested").start(&tracer); + }); + } + + #[test] + fn span_modification_via_context() { + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + let tracer = provider.tracer("test_tracer"); + + #[derive(Debug, PartialEq)] + struct ValueA(u64); + + let span = tracer.start("span-name"); + + // start with Current, which should have no span + let cx = Context::current(); + assert!(!cx.has_active_span()); + + // add span to context + let cx_with_span = cx.with_span(span); + assert!(cx_with_span.has_active_span()); + assert!(!cx.has_active_span()); + + // modify the span by using span_ref from the context + // this is the only way to modify the span as span + // is moved to context. + let span_ref = cx_with_span.span(); + span_ref.set_attribute(KeyValue::new("attribute1", "value1")); + + // create a new context, which should not affect the original + let cx_with_span_and_more = cx_with_span.with_value(ValueA(1)); + + // modify the span again using the new context. + // this should still be using the original span itself. + let span_ref_new = cx_with_span_and_more.span(); + span_ref_new.set_attribute(KeyValue::new("attribute2", "value2")); + + span_ref_new.end(); + + let exported_spans = exporter + .get_finished_spans() + .expect("Spans are expected to be exported."); + // There should be a single span, with attributes from both modifications. + assert_eq!(exported_spans.len(), 1); + let span = &exported_spans[0]; + assert_eq!(span.attributes.len(), 2); + } + + #[derive(Debug)] + struct BaggageInspectingSpanProcessor; + impl SpanProcessor for BaggageInspectingSpanProcessor { + fn on_start(&self, span: &mut crate::trace::Span, cx: &Context) { + let baggage = cx.baggage(); + if let Some(baggage_value) = baggage.get("bag-key") { + span.set_attribute(KeyValue::new("bag-key", baggage_value.to_string())); + } else { + unreachable!("Baggage should be present in the context"); + } + } + + fn on_end(&self, span: SpanData) { + // Fixed: Context::current() no longer panics from Drop + // See https://github.com/open-telemetry/opentelemetry-rust/issues/2871 + Context::current(); + + // IMPORTANT NOTE: The context returned here is whatever context happens + // to be current at this moment, which may be unrelated to the span being ended. + // The span's baggage was extracted in on_start and stored as attributes. + + // Verify: on_start stored the baggage as an attribute + assert!( + span.attributes + .iter() + .any(|kv| kv.key.as_str() == "bag-key"), + "Baggage should have been stored as span attribute in on_start" + ); + } + + fn force_flush(&self) -> crate::error::OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + Ok(()) + } + } + + #[test] + fn span_and_baggage() { + let provider = SdkTracerProvider::builder() + .with_span_processor(BaggageInspectingSpanProcessor) + .build(); + + let cx_with_baggage = + Context::current_with_baggage(vec![KeyValue::new("bag-key", "bag-value")]); + + // assert baggage is in the context + assert_eq!( + cx_with_baggage + .baggage() + .get("bag-key") + .unwrap() + .to_string(), + "bag-value" + ); + + // Attach context to current + let _cx_guard1 = cx_with_baggage.attach(); + // now Current should have the baggage + assert_eq!( + Context::current() + .baggage() + .get("bag-key") + .unwrap() + .to_string(), + "bag-value" + ); + + let tracer = provider.tracer("test_tracer"); + let mut span = tracer + .span_builder("span-name") + .start_with_context(&tracer, &Context::current()); + span.set_attribute(KeyValue::new("attribute1", "value1")); + + // We have not added span to the context yet + // so the current context should not have any span. + let cx = Context::current(); + assert!(!cx.has_active_span()); + + // Now add span to context which already has baggage. + let cx_with_baggage_and_span = cx.with_span(span); + assert!(cx_with_baggage_and_span.has_active_span()); + assert_eq!( + cx_with_baggage_and_span + .baggage() + .get("bag-key") + .unwrap() + .to_string(), + "bag-value" + ); + + let _cx_guard2 = cx_with_baggage_and_span.attach(); + // Now current context should have both baggage and span. + assert!(Context::current().has_active_span()); + assert_eq!( + Context::current() + .baggage() + .get("bag-key") + .unwrap() + .to_string(), + "bag-value" + ); + } + + #[test] + fn tracer_in_span() { + // Arrange + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + + // Act + let tracer = provider.tracer("test_tracer"); + tracer.in_span("span_name", |cx| { + let span = cx.span(); + assert!(span.is_recording()); + span.update_name("span_name_updated"); + span.set_attribute(KeyValue::new("attribute1", "value1")); + span.add_event("test-event".to_string(), vec![]); + span.add_link( + SpanContext::new( + TraceId::from(47), + SpanId::from(11), + TraceFlags::default(), + false, + Default::default(), + ), + vec![], + ); + }); + + // Assert + let exported_spans = exporter + .get_finished_spans() + .expect("Spans are expected to be exported."); + assert_eq!(exported_spans.len(), 1); + let span = &exported_spans[0]; + assert_eq!(span.name, "span_name_updated"); + assert_eq!(span.instrumentation_scope.name(), "test_tracer"); + assert_eq!(span.attributes.len(), 1); + assert_eq!(span.events.len(), 1); + assert_eq!(span.events[0].name, "test-event"); + assert_eq!(span.links.len(), 1); + assert_eq!(span.links[0].span_context.trace_id(), TraceId::from(47)); + assert_eq!(span.links[0].span_context.span_id(), SpanId::from(11)); + assert_eq!(span.span_context.trace_flags(), TraceFlags::SAMPLED); + assert!(!span.span_context.is_remote()); + assert_eq!(span.status, Status::Unset); + } + + #[test] + fn tracer_start() { + // Arrange + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + + // Act + let tracer = provider.tracer("test_tracer"); + let mut span = tracer.start("span_name"); + span.set_attribute(KeyValue::new("attribute1", "value1")); + span.add_event("test-event".to_string(), vec![]); + span.set_status(Status::error("cancelled")); + span.end(); + + // After span end, further operations should not have any effect + span.update_name("span_name_updated"); + + // Assert + let exported_spans = exporter + .get_finished_spans() + .expect("Spans are expected to be exported."); + assert_eq!(exported_spans.len(), 1); + let span = &exported_spans[0]; + assert_eq!(span.name, "span_name"); + assert_eq!(span.instrumentation_scope.name(), "test_tracer"); + assert_eq!(span.attributes.len(), 1); + assert_eq!(span.events.len(), 1); + assert_eq!(span.events[0].name, "test-event"); + assert_eq!(span.span_context.trace_flags(), TraceFlags::SAMPLED); + assert!(!span.span_context.is_remote()); + let status_expected = Status::error("cancelled"); + assert_eq!(span.status, status_expected); + } + + #[test] + fn tracer_span_builder() { + // Arrange + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + + // Act + let tracer = provider.tracer("test_tracer"); + let mut span = tracer + .span_builder("span_name") + .with_kind(SpanKind::Server) + .start(&tracer); + span.set_attribute(KeyValue::new("attribute1", "value1")); + span.add_event("test-event".to_string(), vec![]); + span.set_status(Status::Ok); + drop(span); + + // Assert + let exported_spans = exporter + .get_finished_spans() + .expect("Spans are expected to be exported."); + assert_eq!(exported_spans.len(), 1); + let span = &exported_spans[0]; + assert_eq!(span.name, "span_name"); + assert_eq!(span.span_kind, SpanKind::Server); + assert_eq!(span.instrumentation_scope.name(), "test_tracer"); + assert_eq!(span.attributes.len(), 1); + assert_eq!(span.events.len(), 1); + assert_eq!(span.events[0].name, "test-event"); + assert_eq!(span.span_context.trace_flags(), TraceFlags::SAMPLED); + assert!(!span.span_context.is_remote()); + assert_eq!(span.status, Status::Ok); + } + + #[test] + fn exceed_span_links_limit() { + // Arrange + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + + // Act + let tracer = provider.tracer("test_tracer"); + + let mut links = Vec::new(); + for _i in 0..(DEFAULT_MAX_LINKS_PER_SPAN * 2) { + links.push(Link::with_context(SpanContext::new( + TraceId::from(12), + SpanId::from(12), + TraceFlags::default(), + false, + Default::default(), + ))) + } + + let span_builder = SpanBuilder::from_name("span_name").with_links(links); + let mut span = tracer.build(span_builder); + span.end(); + + // Assert + let exported_spans = exporter + .get_finished_spans() + .expect("Spans are expected to be exported."); + assert_eq!(exported_spans.len(), 1); + let span = &exported_spans[0]; + assert_eq!(span.name, "span_name"); + assert_eq!(span.links.len(), DEFAULT_MAX_LINKS_PER_SPAN as usize); + } + + #[test] + fn exceed_span_events_limit() { + // Arrange + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + + // Act + let tracer = provider.tracer("test_tracer"); + + let mut events = Vec::new(); + for _i in 0..(DEFAULT_MAX_EVENT_PER_SPAN * 2) { + events.push(Event::with_name("test event")) + } + + // add events via span builder + let span_builder = SpanBuilder::from_name("span_name").with_events(events); + let mut span = tracer.build(span_builder); + + // add events using span api after building the span + span.add_event("test event again, after span builder", Vec::new()); + span.add_event("test event once again, after span builder", Vec::new()); + span.end(); + + // Assert + let exported_spans = exporter + .get_finished_spans() + .expect("Spans are expected to be exported."); + assert_eq!(exported_spans.len(), 1); + let span = &exported_spans[0]; + assert_eq!(span.name, "span_name"); + assert_eq!(span.events.len(), DEFAULT_MAX_EVENT_PER_SPAN as usize); + assert_eq!(span.events.dropped_count, DEFAULT_MAX_EVENT_PER_SPAN + 2); + } + + #[test] + fn trace_state_for_dropped_sampler() { + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOff) + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + + let tracer = provider.tracer("test"); + let trace_state = TraceState::from_key_value(vec![("foo", "bar")]).unwrap(); + + let parent_context = Context::new().with_span(TestSpan(SpanContext::new( + TraceId::from(10000), + SpanId::from(20), + TraceFlags::SAMPLED, + true, + trace_state.clone(), + ))); + + let span = tracer.start_with_context("span", &parent_context); + assert_eq!( + span.span_context().trace_state().get("foo"), + trace_state.get("foo") + ) + } + + #[derive(Clone, Debug, Default)] + struct TestRecordOnlySampler {} + + impl ShouldSample for TestRecordOnlySampler { + fn should_sample( + &self, + parent_context: Option<&Context>, + _trace_id: TraceId, + _name: &str, + _span_kind: &SpanKind, + _attributes: &[KeyValue], + _links: &[Link], + ) -> SamplingResult { + let trace_state = parent_context + .unwrap() + .span() + .span_context() + .trace_state() + .clone(); + SamplingResult { + decision: SamplingDecision::RecordOnly, + attributes: vec![KeyValue::new("record_only_key", "record_only_value")], + trace_state, + } + } + } + + #[test] + fn trace_state_for_record_only_sampler() { + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_sampler(TestRecordOnlySampler::default()) + .with_span_processor(SimpleSpanProcessor::new(exporter.clone())) + .build(); + + let tracer = provider.tracer("test"); + let trace_state = TraceState::from_key_value(vec![("foo", "bar")]).unwrap(); + + let parent_context = Context::new().with_span(TestSpan(SpanContext::new( + TraceId::from(10000), + SpanId::from(20), + TraceFlags::SAMPLED, + true, + trace_state.clone(), + ))); + + let span = tracer.build_with_context( + SpanBuilder::from_name("span") + .with_attributes(vec![KeyValue::new("extra_attr_key", "extra_attr_value")]), + &parent_context, + ); + assert!(!span.span_context().trace_flags().is_sampled()); + assert_eq!( + span.exported_data().unwrap().attributes, + vec![ + KeyValue::new("extra_attr_key", "extra_attr_value"), + KeyValue::new("record_only_key", "record_only_value") + ] + ); + assert_eq!(span.span_context().trace_state().get("foo"), Some("bar")); + } + + #[test] + fn tracer_attributes() { + let provider = SdkTracerProvider::builder().build(); + let scope = InstrumentationScope::builder("basic") + .with_attributes(vec![KeyValue::new("test_k", "test_v")]) + .build(); + + let tracer = provider.tracer_with_scope(scope); + let instrumentation_scope = tracer.instrumentation_scope(); + assert!(instrumentation_scope + .attributes() + .eq(&[KeyValue::new("test_k", "test_v")])); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn empty_tracer_name_retained() { + async fn tracer_name_retained_helper( + tracer: super::SdkTracer, + provider: SdkTracerProvider, + exporter: InMemorySpanExporter, + ) { + // Act + tracer.start("my_span").end(); + + // Force flush to ensure spans are exported + assert!(provider.force_flush().is_ok()); + + // Assert + let finished_spans = exporter + .get_finished_spans() + .expect("spans are expected to be exported."); + assert_eq!(finished_spans.len(), 1, "There should be a single span"); + + let tracer_name = finished_spans[0].instrumentation_scope.name(); + assert_eq!(tracer_name, "", "The tracer name should be an empty string"); + + exporter.reset(); + } + + let exporter = InMemorySpanExporter::default(); + let span_processor = SimpleSpanProcessor::new(exporter.clone()); + let tracer_provider = SdkTracerProvider::builder() + .with_span_processor(span_processor) + .build(); + + // Test Tracer creation in 2 ways, both with empty string as tracer name + let tracer1 = tracer_provider.tracer(""); + tracer_name_retained_helper(tracer1, tracer_provider.clone(), exporter.clone()).await; + + let tracer_scope = InstrumentationScope::builder("").build(); + let tracer2 = tracer_provider.tracer_with_scope(tracer_scope); + tracer_name_retained_helper(tracer2, tracer_provider, exporter).await; + } + + #[test] + fn trace_suppression() { + // Arrange + let exporter = InMemorySpanExporter::default(); + let span_processor = SimpleSpanProcessor::new(exporter.clone()); + let tracer_provider = SdkTracerProvider::builder() + .with_span_processor(span_processor) + .build(); + + // Act + let tracer = tracer_provider.tracer("test"); + { + let _suppressed_context = Context::enter_telemetry_suppressed_scope(); + // This span should not be emitted as it is created in a suppressed context + let _span = tracer.span_builder("span_name").start(&tracer); + } + + // Assert + let finished_spans = exporter.get_finished_spans().expect("this should not fail"); + assert_eq!( + finished_spans.len(), + 0, + "There should be a no spans as span emission is done inside a suppressed context" + ); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/provider.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/provider.rs new file mode 100644 index 00000000..b8dc113c --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/provider.rs @@ -0,0 +1,954 @@ +use super::IdGenerator; +use crate::error::{OTelSdkError, OTelSdkResult}; +/// # Trace Provider SDK +/// +/// The `TracerProvider` handles the creation and management of [`Tracer`] instances and coordinates +/// span processing. It serves as the central configuration point for tracing, ensuring consistency +/// across all [`Tracer`] instances it creates. +/// +/// ## Tracer Creation +/// +/// New [`Tracer`] instances are always created through a `TracerProvider`. These `Tracer`s share +/// a common configuration, which includes the [`Resource`], span processors, sampling strategies, +/// and span limits. This avoids the need for each `Tracer` to maintain its own version of these +/// configurations, ensuring uniform behavior across all instances. +/// +/// ## Cloning and Shutdown +/// +/// The `TracerProvider` is designed to be clonable. Cloning a `TracerProvider` creates a +/// new reference to the same provider, not a new instance. Dropping the last reference +/// to the `TracerProvider` will automatically trigger its shutdown. During shutdown, the provider +/// will flush all remaining spans, ensuring they are passed to the configured processors. +/// Users can also manually trigger shutdown using the [`shutdown`](TracerProvider::shutdown) +/// method, which will ensure the same behavior. +/// +/// Once shut down, the `TracerProvider` transitions into a disabled state. In this state, further +/// operations on its associated `Tracer` instances will result in no-ops, ensuring that no spans +/// are processed or exported after shutdown. +/// +/// ## Span Processing and Force Flush +/// +/// The `TracerProvider` manages the lifecycle of span processors, which are responsible for +/// collecting, processing, and exporting spans. The [`force_flush`](TracerProvider::force_flush) method +/// invoked at any time will trigger an immediate flush of all pending spans (if any) to the exporters. +/// This will block the user thread till all the spans are passed to exporters. +/// +/// # Examples +/// +/// ``` +/// use opentelemetry::global; +/// use opentelemetry_sdk::trace::SdkTracerProvider; +/// use opentelemetry::trace::Tracer; +/// +/// fn init_tracing() -> SdkTracerProvider { +/// let provider = SdkTracerProvider::default(); +/// +/// // Set the provider to be used globally +/// let _ = global::set_tracer_provider(provider.clone()); +/// +/// provider +/// } +/// +/// fn main() { +/// let provider = init_tracing(); +/// +/// // create tracer.. +/// let tracer = global::tracer("example/client"); +/// +/// // create span... +/// let span = tracer +/// .span_builder("test_span") +/// .start(&tracer); +/// +/// // Explicitly shut down the provider +/// provider.shutdown(); +/// } +/// ``` +use crate::trace::{ + BatchSpanProcessor, Config, RandomIdGenerator, Sampler, SdkTracer, SimpleSpanProcessor, + SpanLimits, +}; +use crate::Resource; +use crate::{trace::SpanExporter, trace::SpanProcessor}; +use opentelemetry::otel_debug; +use opentelemetry::{otel_info, InstrumentationScope}; +use std::borrow::Cow; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, OnceLock}; +use std::time::Duration; + +static PROVIDER_RESOURCE: OnceLock = OnceLock::new(); + +// a no nop tracer provider used as placeholder when the provider is shutdown +// TODO Replace with LazyLock once it is stable +static NOOP_TRACER_PROVIDER: OnceLock = OnceLock::new(); +#[inline] +fn noop_tracer_provider() -> &'static SdkTracerProvider { + NOOP_TRACER_PROVIDER.get_or_init(|| { + SdkTracerProvider { + inner: Arc::new(TracerProviderInner { + processors: Vec::new(), + config: Config { + // cannot use default here as the default resource is not empty + sampler: Box::new(Sampler::ParentBased(Box::new(Sampler::AlwaysOn))), + id_generator: Box::::default(), + span_limits: SpanLimits::default(), + resource: Cow::Owned(Resource::empty()), + }, + is_shutdown: AtomicBool::new(true), + }), + } + }) +} + +/// TracerProvider inner type +#[derive(Debug)] +pub(crate) struct TracerProviderInner { + processors: Vec>, + config: crate::trace::Config, + is_shutdown: AtomicBool, +} + +impl TracerProviderInner { + /// Crate-private shutdown method to be called both from explicit shutdown + /// and from Drop when the last reference is released. + pub(crate) fn shutdown_with_timeout(&self, timeout: Duration) -> Vec { + let mut results = vec![]; + for processor in &self.processors { + let result = processor.shutdown_with_timeout(timeout); + if let Err(err) = &result { + // Log at debug level because: + // - The error is also returned to the user for handling (if applicable) + // - Or the error occurs during `TracerProviderInner::Drop` as part of telemetry shutdown, + // which is non-actionable by the user + otel_debug!(name: "TracerProvider.Drop.ShutdownError", + error = format!("{err}")); + } + results.push(result); + } + results + } + /// shutdown with default timeout + pub(crate) fn shutdown(&self) -> Vec { + self.shutdown_with_timeout(Duration::from_secs(5)) + } +} + +impl Drop for TracerProviderInner { + fn drop(&mut self) { + if !self.is_shutdown.load(Ordering::Relaxed) { + let _ = self.shutdown(); // errors are handled within shutdown + } else { + otel_debug!( + name: "TracerProvider.Drop.AlreadyShutdown", + message = "TracerProvider was already shut down; drop will not attempt shutdown again." + ); + } + } +} + +/// Creator and registry of named [`SdkTracer`] instances. +/// +/// `TracerProvider` is a container holding pointers to `SpanProcessor` and other components. +/// Cloning a `TracerProvider` instance and dropping it will not stop span processing. To stop span processing, users +/// must either call the `shutdown` method explicitly or allow the last reference to the `TracerProvider` +/// to be dropped. When the last reference is dropped, the shutdown process will be automatically triggered +/// to ensure proper cleanup. +#[derive(Clone, Debug)] +pub struct SdkTracerProvider { + inner: Arc, +} + +impl Default for SdkTracerProvider { + fn default() -> Self { + SdkTracerProvider::builder().build() + } +} + +impl SdkTracerProvider { + /// Build a new tracer provider + pub(crate) fn new(inner: TracerProviderInner) -> Self { + SdkTracerProvider { + inner: Arc::new(inner), + } + } + + /// Create a new [`SdkTracerProvider`] builder. + pub fn builder() -> TracerProviderBuilder { + TracerProviderBuilder::default() + } + + /// Span processors associated with this provider + pub(crate) fn span_processors(&self) -> &[Box] { + &self.inner.processors + } + + /// Config associated with this tracer + pub(crate) fn config(&self) -> &crate::trace::Config { + &self.inner.config + } + + /// true if the provider has been shutdown + /// Don't start span or export spans when provider is shutdown + pub(crate) fn is_shutdown(&self) -> bool { + self.inner.is_shutdown.load(Ordering::Relaxed) + } + + /// Force flush all remaining spans in span processors and return results. + /// + /// # Examples + /// + /// ``` + /// use opentelemetry::global; + /// use opentelemetry_sdk::trace::SdkTracerProvider; + /// + /// fn init_tracing() -> SdkTracerProvider { + /// let provider = SdkTracerProvider::default(); + /// + /// // Set provider to be used as global tracer provider + /// let _ = global::set_tracer_provider(provider.clone()); + /// + /// provider + /// } + /// + /// fn main() { + /// let provider = init_tracing(); + /// + /// // create spans.. + /// + /// // force all spans to flush + /// if let Err(err) = provider.force_flush() { + /// // .. handle flush error + /// } + /// + /// // create more spans.. + /// + /// // dropping provider ensures all remaining spans are exported + /// drop(provider); + /// } + /// ``` + pub fn force_flush(&self) -> OTelSdkResult { + let result: Vec<_> = self + .span_processors() + .iter() + .map(|processor| processor.force_flush()) + .collect(); + if result.iter().all(|r| r.is_ok()) { + Ok(()) + } else { + Err(OTelSdkError::InternalFailure(format!("errs: {result:?}"))) + } + } + + /// Shuts down the current `TracerProvider`. + /// + /// Note that shut down doesn't means the TracerProvider has dropped + pub fn shutdown_with_timeout(&self, timeout: Duration) -> OTelSdkResult { + if self + .inner + .is_shutdown + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + { + // propagate the shutdown signal to processors + let results = self.inner.shutdown_with_timeout(timeout); + + if results.iter().all(|res| res.is_ok()) { + Ok(()) + } else { + Err(OTelSdkError::InternalFailure(format!( + "Shutdown errors: {:?}", + results + .into_iter() + .filter_map(Result::err) + .collect::>() // Collect only the errors + ))) + } + } else { + Err(OTelSdkError::AlreadyShutdown) + } + } + + /// shutdown with default timeout + pub fn shutdown(&self) -> OTelSdkResult { + self.shutdown_with_timeout(Duration::from_secs(5)) + } +} + +impl opentelemetry::trace::TracerProvider for SdkTracerProvider { + /// This implementation of `TracerProvider` produces `Tracer` instances. + type Tracer = SdkTracer; + + fn tracer(&self, name: impl Into>) -> Self::Tracer { + let scope = InstrumentationScope::builder(name).build(); + self.tracer_with_scope(scope) + } + + fn tracer_with_scope(&self, scope: InstrumentationScope) -> Self::Tracer { + if self.inner.is_shutdown.load(Ordering::Relaxed) { + return SdkTracer::new(scope, noop_tracer_provider().clone()); + } + if scope.name().is_empty() { + otel_info!(name: "TracerNameEmpty", message = "Tracer name is empty; consider providing a meaningful name. Tracer will function normally and the provided name will be used as-is."); + }; + SdkTracer::new(scope, self.clone()) + } +} + +/// Builder for provider attributes. +#[derive(Debug, Default)] +pub struct TracerProviderBuilder { + processors: Vec>, + config: crate::trace::Config, + resource: Option, +} + +impl TracerProviderBuilder { + /// Adds a [SimpleSpanProcessor] with the configured exporter to the pipeline. + /// + /// # Arguments + /// + /// * `exporter` - The exporter to be used by the SimpleSpanProcessor. + /// + /// # Returns + /// + /// A new `Builder` instance with the SimpleSpanProcessor added to the pipeline. + /// + /// Processors are invoked in the order they are added. + pub fn with_simple_exporter(self, exporter: T) -> Self { + let simple = SimpleSpanProcessor::new(exporter); + self.with_span_processor(simple) + } + + /// Adds a [BatchSpanProcessor] with the configured exporter to the pipeline. + /// + /// # Arguments + /// + /// * `exporter` - The exporter to be used by the BatchSpanProcessor. + /// + /// # Returns + /// + /// A new `Builder` instance with the BatchSpanProcessor added to the pipeline. + /// + /// Processors are invoked in the order they are added. + pub fn with_batch_exporter(self, exporter: T) -> Self { + let batch = BatchSpanProcessor::builder(exporter).build(); + self.with_span_processor(batch) + } + + /// Adds a custom [SpanProcessor] to the pipeline. + /// + /// # Arguments + /// + /// * `processor` - The `SpanProcessor` to be added. + /// + /// # Returns + /// + /// A new `Builder` instance with the custom `SpanProcessor` added to the pipeline. + /// + /// Processors are invoked in the order they are added. + pub fn with_span_processor(self, processor: T) -> Self { + let mut processors = self.processors; + processors.push(Box::new(processor)); + + TracerProviderBuilder { processors, ..self } + } + + /// Specify the sampler to be used. + /// + /// ## Dynamic sampler selection + /// + /// ``` + /// use opentelemetry_sdk::trace::{Sampler, ShouldSample}; + /// + /// fn should_return_dynamic_sampler() -> Box { + /// Box::new(opentelemetry_sdk::trace::Sampler::AlwaysOn) + /// } + /// + /// opentelemetry_sdk::trace::SdkTracerProvider::builder() + /// // You can pass already boxed sampler if you need to configure your sampler in a simple fashion + /// // This can be useful if you create your own sampler that implements `ShouldSample` + /// .with_sampler(should_return_dynamic_sampler()) + /// // Or you can pass exact instance if you do not have complex configuration + /// .with_sampler(Sampler::AlwaysOff); + /// ``` + pub fn with_sampler(mut self, sampler: impl Into>) -> Self { + self.config.sampler = sampler.into(); + self + } + + /// Specify the id generator to be used. + pub fn with_id_generator(mut self, id_generator: T) -> Self { + self.config.id_generator = Box::new(id_generator); + self + } + + /// Specify the number of events to be recorded per span. + pub fn with_max_events_per_span(mut self, max_events: u32) -> Self { + self.config.span_limits.max_events_per_span = max_events; + self + } + + /// Specify the number of attributes to be recorded per span. + pub fn with_max_attributes_per_span(mut self, max_attributes: u32) -> Self { + self.config.span_limits.max_attributes_per_span = max_attributes; + self + } + + /// Specify the number of events to be recorded per span. + pub fn with_max_links_per_span(mut self, max_links: u32) -> Self { + self.config.span_limits.max_links_per_span = max_links; + self + } + + /// Specify the number of attributes one event can have. + pub fn with_max_attributes_per_event(mut self, max_attributes: u32) -> Self { + self.config.span_limits.max_attributes_per_event = max_attributes; + self + } + + /// Specify the number of attributes one link can have. + pub fn with_max_attributes_per_link(mut self, max_attributes: u32) -> Self { + self.config.span_limits.max_attributes_per_link = max_attributes; + self + } + + /// Specify all limit via the span_limits + pub fn with_span_limits(mut self, span_limits: SpanLimits) -> Self { + self.config.span_limits = span_limits; + self + } + + /// Associates a [Resource] with a [SdkTracerProvider]. + /// + /// This [Resource] represents the entity producing telemetry and is associated + /// with all [Tracer]s the [SdkTracerProvider] will create. + /// + /// By default, if this option is not used, the default [Resource] will be used. + /// + /// When constructing a [Resource], use [`Resource::builder()`] to preserve + /// SDK-provided defaults such as `telemetry.sdk.*` and `service.name`. + /// Using [`Resource::builder_empty()`] will **not** include these attributes. + /// + /// # Example + /// + /// ``` + /// use opentelemetry_sdk::{Resource, trace::SdkTracerProvider}; + /// use opentelemetry::KeyValue; + /// + /// let provider = SdkTracerProvider::builder() + /// .with_resource( + /// Resource::builder() + /// .with_service_name("my-service") + /// .with_attributes([KeyValue::new("deployment.environment.name", "production")]) + /// .build(), + /// ) + /// .build(); + /// ``` + /// + /// *Note*: Calls to this method are additive, each call merges the provided + /// resource with the previous one. + /// + /// [Tracer]: opentelemetry::trace::Tracer + /// [`Resource::builder()`]: Resource::builder + /// [`Resource::builder_empty()`]: Resource::builder_empty + pub fn with_resource(self, resource: Resource) -> Self { + let resource = match self.resource { + Some(existing) => Some(existing.merge(&resource)), + None => Some(resource), + }; + + TracerProviderBuilder { resource, ..self } + } + + /// Create a new provider from this configuration. + pub fn build(self) -> SdkTracerProvider { + let mut config = self.config; + + // Now, we can update the config with the resource. + if let Some(resource) = self.resource { + config.resource = Cow::Owned(resource); + }; + + // Standard config will contain an owned [`Resource`] (either sdk default or use supplied) + // we can optimize the common case with a static ref to avoid cloning the underlying + // resource data for each span. + // + // For the uncommon case where there are multiple tracer providers with different resource + // configurations, users can optionally provide their own borrowed static resource. + if matches!(config.resource, Cow::Owned(_)) { + config.resource = + match PROVIDER_RESOURCE.get_or_init(|| config.resource.clone().into_owned()) { + static_resource if *static_resource == *config.resource.as_ref() => { + Cow::Borrowed(static_resource) + } + _ => config.resource, // Use the new resource if different + }; + } + + // Create a new vector to hold the modified processors + let mut processors = self.processors; + + // Set the resource for each processor + for p in &mut processors { + p.set_resource(config.resource.as_ref()); + } + + let is_shutdown = AtomicBool::new(false); + SdkTracerProvider::new(TracerProviderInner { + processors, + config, + is_shutdown, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::error::{OTelSdkError, OTelSdkResult}; + use crate::resource::{ + SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, TELEMETRY_SDK_VERSION, + }; + use crate::trace::provider::TracerProviderInner; + use crate::trace::{Config, Span, SpanProcessor}; + use crate::trace::{SdkTracerProvider, SpanData}; + use crate::Resource; + use opentelemetry::trace::{Tracer, TracerProvider}; + use opentelemetry::{Context, Key, KeyValue, Value}; + + use std::env; + use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + use std::sync::Arc; + use std::time::Duration; + + // fields below is wrapped with Arc so we can assert it + #[derive(Default, Debug)] + struct AssertInfo { + started_span: AtomicU32, + is_shutdown: AtomicBool, + } + + #[derive(Default, Debug, Clone)] + struct SharedAssertInfo(Arc); + + impl SharedAssertInfo { + fn started_span_count(&self, count: u32) -> bool { + self.0.started_span.load(Ordering::SeqCst) == count + } + } + + #[derive(Debug)] + struct TestSpanProcessor { + success: bool, + assert_info: SharedAssertInfo, + } + + impl TestSpanProcessor { + fn new(success: bool) -> TestSpanProcessor { + TestSpanProcessor { + success, + assert_info: SharedAssertInfo::default(), + } + } + + // get handle to assert info + fn assert_info(&self) -> SharedAssertInfo { + self.assert_info.clone() + } + } + + impl SpanProcessor for TestSpanProcessor { + fn on_start(&self, _span: &mut Span, _cx: &Context) { + self.assert_info + .0 + .started_span + .fetch_add(1, Ordering::SeqCst); + } + + fn on_end(&self, _span: SpanData) { + // ignore + } + + fn force_flush(&self) -> OTelSdkResult { + if self.success { + Ok(()) + } else { + Err(OTelSdkError::InternalFailure("cannot export".into())) + } + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + if self.assert_info.0.is_shutdown.load(Ordering::SeqCst) { + Ok(()) + } else { + let _ = self.assert_info.0.is_shutdown.compare_exchange( + false, + true, + Ordering::SeqCst, + Ordering::SeqCst, + ); + self.force_flush() + } + } + } + + #[test] + fn test_force_flush() { + let tracer_provider = super::SdkTracerProvider::new(TracerProviderInner { + processors: vec![ + Box::from(TestSpanProcessor::new(true)), + Box::from(TestSpanProcessor::new(false)), + ], + config: Default::default(), + is_shutdown: AtomicBool::new(false), + }); + + let results = tracer_provider.force_flush(); + assert!(results.is_err()); + } + + #[test] + fn test_tracer_provider_default_resource() { + let assert_resource = |provider: &super::SdkTracerProvider, + resource_key: &'static str, + expect: Option<&'static str>| { + assert_eq!( + provider + .config() + .resource + .get(&Key::from_static_str(resource_key)) + .map(|v| v.to_string()), + expect.map(|s| s.to_string()) + ); + }; + let assert_telemetry_resource = |provider: &super::SdkTracerProvider| { + assert_eq!( + provider + .config() + .resource + .get(&TELEMETRY_SDK_LANGUAGE.into()), + Some(Value::from("rust")) + ); + assert_eq!( + provider.config().resource.get(&TELEMETRY_SDK_NAME.into()), + Some(Value::from("opentelemetry")) + ); + assert_eq!( + provider + .config() + .resource + .get(&TELEMETRY_SDK_VERSION.into()), + Some(Value::from(env!("CARGO_PKG_VERSION"))) + ); + }; + + // If users didn't provide a resource and there isn't a env var set. Use default one. + temp_env::with_var_unset("OTEL_RESOURCE_ATTRIBUTES", || { + let default_config_provider = super::SdkTracerProvider::builder().build(); + let service_name = default_config_provider + .config() + .resource + .get(&Key::from_static_str(SERVICE_NAME)) + .map(|v| v.to_string()) + .unwrap(); + assert!( + service_name.starts_with("unknown_service:opentelemetry_sdk-"), + "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}", + service_name + ); + assert_telemetry_resource(&default_config_provider); + }); + + // If user provided config, use that. + let custom_config_provider = super::SdkTracerProvider::builder() + .with_resource( + Resource::builder_empty() + .with_service_name("test_service") + .build(), + ) + .build(); + assert_resource(&custom_config_provider, SERVICE_NAME, Some("test_service")); + assert_eq!(custom_config_provider.config().resource.len(), 1); + + // If `OTEL_RESOURCE_ATTRIBUTES` is set, read them automatically + temp_env::with_var( + "OTEL_RESOURCE_ATTRIBUTES", + Some("key1=value1, k2, k3=value2"), + || { + let env_resource_provider = super::SdkTracerProvider::builder().build(); + let service_name = env_resource_provider + .config() + .resource + .get(&Key::from_static_str(SERVICE_NAME)) + .map(|v| v.to_string()) + .unwrap(); + assert!( + service_name.starts_with("unknown_service:opentelemetry_sdk-"), + "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}", + service_name + ); + assert_resource(&env_resource_provider, "key1", Some("value1")); + assert_resource(&env_resource_provider, "k3", Some("value2")); + assert_telemetry_resource(&env_resource_provider); + assert_eq!(env_resource_provider.config().resource.len(), 6); + }, + ); + + // When `OTEL_RESOURCE_ATTRIBUTES` is set and also user provided config + temp_env::with_var( + "OTEL_RESOURCE_ATTRIBUTES", + Some("my-custom-key=env-val,k2=value2"), + || { + let user_provided_resource_config_provider = super::SdkTracerProvider::builder() + .with_resource( + Resource::builder() + .with_attributes([ + KeyValue::new("my-custom-key", "my-custom-value"), + KeyValue::new("my-custom-key2", "my-custom-value2"), + ]) + .build(), + ) + .build(); + let service_name = user_provided_resource_config_provider + .config() + .resource + .get(&Key::from_static_str(SERVICE_NAME)) + .map(|v| v.to_string()) + .unwrap(); + assert!( + service_name.starts_with("unknown_service:opentelemetry_sdk-"), + "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}", + service_name + ); + assert_resource( + &user_provided_resource_config_provider, + "my-custom-key", + Some("my-custom-value"), + ); + assert_resource( + &user_provided_resource_config_provider, + "my-custom-key2", + Some("my-custom-value2"), + ); + assert_resource( + &user_provided_resource_config_provider, + "k2", + Some("value2"), + ); + assert_telemetry_resource(&user_provided_resource_config_provider); + assert_eq!( + user_provided_resource_config_provider + .config() + .resource + .len(), + 7 + ); + }, + ); + + // If user provided a resource, it takes priority during collision. + let no_service_name = super::SdkTracerProvider::builder() + .with_resource(Resource::empty()) + .build(); + + assert_eq!(no_service_name.config().resource.len(), 0) + } + + #[test] + fn test_shutdown_noops() { + let processor = TestSpanProcessor::new(false); + let assert_handle = processor.assert_info(); + let tracer_provider = super::SdkTracerProvider::new(TracerProviderInner { + processors: vec![Box::from(processor)], + config: Default::default(), + is_shutdown: AtomicBool::new(false), + }); + + let test_tracer_1 = tracer_provider.tracer("test1"); + let _ = test_tracer_1.start("test"); + + assert!(assert_handle.started_span_count(1)); + + let _ = test_tracer_1.start("test"); + + assert!(assert_handle.started_span_count(2)); + + let shutdown = |tracer_provider: super::SdkTracerProvider| { + let _ = tracer_provider.shutdown(); // shutdown once + }; + + // assert tracer provider can be shutdown using on a cloned version + shutdown(tracer_provider.clone()); + + // after shutdown we should get noop tracer + let noop_tracer = tracer_provider.tracer("noop"); + + // noop tracer cannot start anything + let _ = noop_tracer.start("test"); + assert!(assert_handle.started_span_count(2)); + // noop tracer's tracer provider should be shutdown + assert!(noop_tracer.provider().is_shutdown()); + + // existing tracer becomes noop after shutdown + let _ = test_tracer_1.start("test"); + assert!(assert_handle.started_span_count(2)); + + // also existing tracer's tracer provider are in shutdown state + assert!(test_tracer_1.provider().is_shutdown()); + } + + #[test] + fn with_resource_multiple_calls_ensure_additive() { + let resource = SdkTracerProvider::builder() + .with_resource( + Resource::builder_empty() + .with_attributes(vec![KeyValue::new("key1", "value1")]) + .build(), + ) + .with_resource( + Resource::builder_empty() + .with_attributes(vec![KeyValue::new("key2", "value2")]) + .build(), + ) + .with_resource( + Resource::builder_empty() + .with_schema_url(vec![], "http://example.com") + .build(), + ) + .with_resource( + Resource::builder_empty() + .with_attributes(vec![KeyValue::new("key3", "value3")]) + .build(), + ) + .build() + .inner + .config + .resource + .clone() + .into_owned(); + + assert_eq!( + resource.get(&Key::from_static_str("key1")), + Some(Value::from("value1")) + ); + assert_eq!( + resource.get(&Key::from_static_str("key2")), + Some(Value::from("value2")) + ); + assert_eq!( + resource.get(&Key::from_static_str("key3")), + Some(Value::from("value3")) + ); + assert_eq!(resource.schema_url(), Some("http://example.com")); + } + + #[derive(Debug)] + struct CountingShutdownProcessor { + shutdown_count: Arc, + } + + impl CountingShutdownProcessor { + fn new(shutdown_count: Arc) -> Self { + CountingShutdownProcessor { shutdown_count } + } + } + + impl SpanProcessor for CountingShutdownProcessor { + fn on_start(&self, _span: &mut Span, _cx: &Context) { + // No operation needed for this processor + } + + fn on_end(&self, _span: SpanData) { + // No operation needed for this processor + } + + fn force_flush(&self) -> OTelSdkResult { + Ok(()) + } + + fn shutdown_with_timeout(&self, _timeout: Duration) -> OTelSdkResult { + self.shutdown_count.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + } + + #[test] + fn drop_test_with_multiple_providers() { + let shutdown_count = Arc::new(AtomicU32::new(0)); + + { + // Create a shared TracerProviderInner and use it across multiple providers + let shared_inner = Arc::new(TracerProviderInner { + processors: vec![Box::new(CountingShutdownProcessor::new( + shutdown_count.clone(), + ))], + config: Config::default(), + is_shutdown: AtomicBool::new(false), + }); + + { + let tracer_provider1 = super::SdkTracerProvider { + inner: shared_inner.clone(), + }; + let tracer_provider2 = super::SdkTracerProvider { + inner: shared_inner.clone(), + }; + + let tracer1 = tracer_provider1.tracer("test-tracer1"); + let tracer2 = tracer_provider2.tracer("test-tracer2"); + + let _span1 = tracer1.start("span1"); + let _span2 = tracer2.start("span2"); + + // TracerProviderInner should not be dropped yet, since both providers and `shared_inner` + // are still holding a reference. + } + // At this point, both `tracer_provider1` and `tracer_provider2` are dropped, + // but `shared_inner` still holds a reference, so `TracerProviderInner` is NOT dropped yet. + assert_eq!(shutdown_count.load(Ordering::SeqCst), 0); + } + // Verify shutdown was called during the drop of the shared TracerProviderInner + assert_eq!(shutdown_count.load(Ordering::SeqCst), 1); + } + + #[test] + fn drop_after_shutdown_test_with_multiple_providers() { + let shutdown_count = Arc::new(AtomicU32::new(0)); + + // Create a shared TracerProviderInner and use it across multiple providers + let shared_inner = Arc::new(TracerProviderInner { + processors: vec![Box::new(CountingShutdownProcessor::new( + shutdown_count.clone(), + ))], + config: Config::default(), + is_shutdown: AtomicBool::new(false), + }); + + // Create a scope to test behavior when providers are dropped + { + let tracer_provider1 = super::SdkTracerProvider { + inner: shared_inner.clone(), + }; + let tracer_provider2 = super::SdkTracerProvider { + inner: shared_inner.clone(), + }; + + // Explicitly shut down the tracer provider + let shutdown_result = tracer_provider1.shutdown(); + assert!(shutdown_result.is_ok()); + + // Verify that shutdown was called exactly once + assert_eq!(shutdown_count.load(Ordering::SeqCst), 1); + + // TracerProvider2 should observe the shutdown state but not trigger another shutdown + let shutdown_result2 = tracer_provider2.shutdown(); + assert!(shutdown_result2.is_err()); + assert_eq!(shutdown_count.load(Ordering::SeqCst), 1); + + // Both tracer providers will be dropped at the end of this scope + } + + // Verify that shutdown was only called once, even after drop + assert_eq!(shutdown_count.load(Ordering::SeqCst), 1); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/runtime_tests.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/runtime_tests.rs new file mode 100644 index 00000000..e068a377 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/runtime_tests.rs @@ -0,0 +1,158 @@ +// Note that all tests here should be marked as ignore so that it won't be picked up by default We +// need to run those tests one by one as the GlobalTracerProvider is a shared object between +// threads Use cargo test -- --ignored --test-threads=1 to run those tests. +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +use crate::runtime::RuntimeChannel; +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +use crate::trace::SpanExporter; +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +use crate::{error::OTelSdkResult, runtime}; +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +use opentelemetry::global::*; +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +use opentelemetry::trace::Tracer; +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +use std::fmt::Debug; +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +use std::sync::atomic::{AtomicUsize, Ordering}; +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +use std::sync::Arc; +#[derive(Debug)] +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +struct SpanCountExporter { + span_count: Arc, +} + +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +impl SpanExporter for SpanCountExporter { + async fn export(&self, batch: Vec) -> OTelSdkResult { + self.span_count.fetch_add(batch.len(), Ordering::SeqCst); + Ok(()) + } +} + +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +impl SpanCountExporter { + fn new() -> SpanCountExporter { + SpanCountExporter { + span_count: Arc::new(AtomicUsize::new(0)), + } + } +} + +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +fn build_batch_tracer_provider( + exporter: SpanCountExporter, + runtime: R, +) -> crate::trace::SdkTracerProvider { + use crate::trace::SdkTracerProvider; + let processor = crate::trace::span_processor_with_async_runtime::BatchSpanProcessor::builder( + exporter, runtime, + ) + .build(); + SdkTracerProvider::builder() + .with_span_processor(processor) + .build() +} + +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +fn build_simple_tracer_provider(exporter: SpanCountExporter) -> crate::trace::SdkTracerProvider { + use crate::trace::SdkTracerProvider; + SdkTracerProvider::builder() + .with_simple_exporter(exporter) + .build() +} + +#[cfg(any(feature = "rt-tokio", feature = "rt-tokio-current-thread"))] +async fn test_set_provider_in_tokio( + runtime: R, +) -> (Arc, crate::trace::SdkTracerProvider) { + let exporter = SpanCountExporter::new(); + let span_count = exporter.span_count.clone(); + let tracer_provider = build_batch_tracer_provider(exporter, runtime); + set_tracer_provider(tracer_provider.clone()); + let tracer = tracer("opentelemetery"); + + tracer.in_span("test", |_cx| {}); + + (span_count, tracer_provider) +} + +// When using `tokio::spawn` to spawn the worker task in batch processor +// +// multiple -> no shut down -> not export +// multiple -> shut down -> export +// single -> no shutdown -> not export +// single -> shutdown -> hang forever + +// When using |fut| tokio::task::spawn_blocking(|| futures::executor::block_on(fut)) +// to spawn the worker task in batch processor +// +// multiple -> no shutdown -> hang forever +// multiple -> shut down -> export +// single -> shut down -> export +// single -> no shutdown -> hang forever + +// Test if the multiple thread tokio runtime could exit successfully when not force flushing spans +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "requires --test-threads=1"] +#[cfg(feature = "rt-tokio")] +async fn test_set_provider_multiple_thread_tokio() { + let (span_count, _) = test_set_provider_in_tokio(runtime::Tokio).await; + assert_eq!(span_count.load(Ordering::SeqCst), 0); +} + +// Test if the multiple thread tokio runtime could exit successfully when force flushing spans +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[ignore = "requires --test-threads=1"] +#[cfg(feature = "rt-tokio")] +async fn test_set_provider_multiple_thread_tokio_shutdown() { + let (span_count, tracer_provider) = test_set_provider_in_tokio(runtime::Tokio).await; + tracer_provider + .shutdown() + .expect("TracerProvider should shutdown properly"); + assert!(span_count.load(Ordering::SeqCst) > 0); +} + +// Test use simple processor in single thread tokio runtime. +// Expected to see the spans being exported to buffer +#[tokio::test] +#[ignore = "requires --test-threads=1"] +#[cfg(feature = "rt-tokio")] +async fn test_set_provider_single_thread_tokio_with_simple_processor() { + let exporter = SpanCountExporter::new(); + let span_count = exporter.span_count.clone(); + let tracer_provider = build_simple_tracer_provider(exporter); + set_tracer_provider(tracer_provider.clone()); + let tracer = tracer("opentelemetry"); + + tracer.in_span("test", |_cx| {}); + + tracer_provider + .shutdown() + .expect("TracerProvider should shutdown properly"); + + assert!(span_count.load(Ordering::SeqCst) > 0); +} + +// Test if the single thread tokio runtime could exit successfully when not force flushing spans +#[tokio::test] +#[ignore = "requires --test-threads=1"] +#[cfg(feature = "rt-tokio-current-thread")] +async fn test_set_provider_single_thread_tokio() { + let (span_count, _) = test_set_provider_in_tokio(runtime::TokioCurrentThread).await; + assert_eq!(span_count.load(Ordering::SeqCst), 0) +} + +// Test if the single thread tokio runtime could exit successfully when force flushing spans. +#[tokio::test] +#[ignore = "requires --test-threads=1"] +#[cfg(feature = "rt-tokio-current-thread")] +async fn test_set_provider_single_thread_tokio_shutdown() { + let (span_count, tracer_provider) = + test_set_provider_in_tokio(runtime::TokioCurrentThread).await; + tracer_provider + .shutdown() + .expect("TracerProvider should shutdown properly"); + assert!(span_count.load(Ordering::SeqCst) > 0) +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler.rs new file mode 100644 index 00000000..c363cde0 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler.rs @@ -0,0 +1,469 @@ +use opentelemetry::{ + trace::{Link, SpanKind, TraceContextExt, TraceId, TraceState}, + Context, KeyValue, +}; + +#[cfg(feature = "jaeger_remote_sampler")] +mod jaeger_remote; + +/// The result of sampling logic for a given span. +#[derive(Clone, Debug, PartialEq)] +pub struct SamplingResult { + /// The decision about whether or not to sample. + pub decision: SamplingDecision, + + /// Extra attributes to be added to the span by the sampler + pub attributes: Vec, + + /// Trace state from parent context, may be modified by samplers. + pub trace_state: TraceState, +} + +/// Decision about whether or not to sample +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SamplingDecision { + /// Span will not be recorded and all events and attributes will be dropped. + Drop, + + /// Span data wil be recorded, but not exported. + RecordOnly, + + /// Span data will be recorded and exported. + RecordAndSample, +} + +#[cfg(feature = "jaeger_remote_sampler")] +pub use jaeger_remote::{JaegerRemoteSampler, JaegerRemoteSamplerBuilder}; +#[cfg(feature = "jaeger_remote_sampler")] +use opentelemetry_http::HttpClient; + +/// The [`ShouldSample`] interface allows implementations to provide samplers +/// which will return a sampling [`SamplingResult`] based on information that +/// is typically available just before the [`Span`] was created. +/// +/// # Sampling +/// +/// Sampling is a mechanism to control the noise and overhead introduced by +/// OpenTelemetry by reducing the number of samples of traces collected and +/// sent to the backend. +/// +/// Sampling may be implemented on different stages of a trace collection. +/// [OpenTelemetry SDK] defines a [`ShouldSample`] interface that can be used at +/// instrumentation points by libraries to check the sampling [`SamplingDecision`] +/// early and optimize the amount of telemetry that needs to be collected. +/// +/// All other sampling algorithms may be implemented on SDK layer in exporters, +/// or even out of process in Agent or Collector. +/// +/// The OpenTelemetry API has two properties responsible for the data collection: +/// +/// * [`Span::is_recording()`]. If `true` the current [`Span`] records +/// tracing events (attributes, events, status, etc.), otherwise all tracing +/// events are dropped. Users can use this property to determine if expensive +/// trace events can be avoided. [`SpanProcessor`]s will receive +/// all spans with this flag set. However, [`SpanExporter`]s will +/// not receive them unless the `Sampled` flag was set. +/// * `Sampled` flag in [`SpanContext::trace_flags()`]. This flag is propagated +/// via the [`SpanContext`] to child Spans. For more details see the [W3C +/// specification](https://w3c.github.io/trace-context/). This flag indicates +/// that the [`Span`] has been `sampled` and will be exported. [`SpanProcessor`]s +/// and [`SpanExporter`]s will receive spans with the `Sampled` flag set for +/// processing. +/// +/// The flag combination `Sampled == false` and `is_recording == true` means +/// that the current `Span` does record information, but most likely the child +/// `Span` will not. +/// +/// The flag combination `Sampled == true` and `is_recording == false` could +/// cause gaps in the distributed trace, and because of this OpenTelemetry API +/// MUST NOT allow this combination. +/// +/// [OpenTelemetry SDK]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#sampling +/// [`SpanContext`]: opentelemetry::trace::SpanContext +/// [`SpanContext::trace_flags()`]: opentelemetry::trace::SpanContext#method.trace_flags +/// [`SpanExporter`]: crate::trace::SpanExporter +/// [`SpanProcessor`]: crate::trace::SpanProcessor +/// [`Span`]: opentelemetry::trace::Span +/// [`Span::is_recording()`]: opentelemetry::trace::Span#tymethod.is_recording +pub trait ShouldSample: CloneShouldSample + Send + Sync + std::fmt::Debug { + /// Returns the [`SamplingDecision`] for a [`Span`] to be created. + /// + /// The [`should_sample`] function can use any of the information provided to it in order to + /// make a decision about whether or not a [`Span`] should or should not be sampled. However, + /// there are performance implications on the creation of a span + /// + /// [`Span`]: opentelemetry::trace::Span + /// [`should_sample`]: ShouldSample::should_sample + #[allow(clippy::too_many_arguments)] + fn should_sample( + &self, + parent_context: Option<&Context>, + trace_id: TraceId, + name: &str, + span_kind: &SpanKind, + attributes: &[KeyValue], + links: &[Link], + ) -> SamplingResult; +} + +impl From for Box { + #[inline] + fn from(value: T) -> Self { + Box::new(value) + } +} + +/// This trait should not be used directly instead users should use [`ShouldSample`]. +pub trait CloneShouldSample { + fn box_clone(&self) -> Box; +} + +impl CloneShouldSample for T +where + T: ShouldSample + Clone + 'static, +{ + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.box_clone() + } +} + +/// Default Sampling options +/// +/// The [built-in samplers] allow for simple decisions. For more complex scenarios consider +/// implementing your own sampler using [`ShouldSample`] trait. +/// +/// [built-in samplers]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-samplers +#[derive(Clone, Debug)] +#[non_exhaustive] +pub enum Sampler { + /// Always sample the trace + AlwaysOn, + /// Never sample the trace + AlwaysOff, + /// Respects the parent span's sampling decision or delegates a delegate sampler for root spans. + ParentBased(Box), + /// Sample a given fraction of traces. Fractions >= 1 will always sample. If the parent span is + /// sampled, then it's child spans will automatically be sampled. Fractions < 0 are treated as + /// zero, but spans may still be sampled if their parent is. + /// *Note:* If this is used then all Spans in a trace will become sampled assuming that the + /// first span is sampled as it is based on the `trace_id` not the `span_id` + TraceIdRatioBased(f64), + /// Jaeger remote sampler supports any remote service that implemented the jaeger remote sampler protocol. + /// The proto definition can be found [here](https://github.com/jaegertracing/jaeger-idl/blob/main/proto/api_v2/sampling.proto) + /// + /// Jaeger remote sampler allows remotely controlling the sampling configuration for the SDKs. + /// The sampling is typically configured at the collector and the SDKs actively poll for changes. + /// The sampler uses TraceIdRatioBased or rate-limited sampler under the hood. + /// These samplers can be configured per whole service (a.k.a default), or per span name in a + /// given service (a.k.a per operation). + #[cfg(feature = "jaeger_remote_sampler")] + JaegerRemote(JaegerRemoteSampler), +} + +impl Sampler { + /// Create a jaeger remote sampler builder. + /// + /// ### Arguments + /// * `runtime` - A runtime to run the HTTP client. + /// * `http_client` - An HTTP client to query the sampling endpoint. + /// * `default_sampler` - A default sampler to make a sampling decision when the remote is unavailable or before the SDK receives the first response from remote. + /// * `service_name` - The name of the service. This is a required parameter to query the sampling endpoint. + /// + /// See [here](https://github.com/open-telemetry/opentelemetry-rust/blob/main/examples/jaeger-remote-sampler/src/main.rs) for an example. + #[cfg(feature = "jaeger_remote_sampler")] + pub fn jaeger_remote( + runtime: R, + http_client: C, + default_sampler: Sampler, + service_name: Svc, + ) -> JaegerRemoteSamplerBuilder + where + C: HttpClient + 'static, + Sampler: ShouldSample, + R: crate::runtime::RuntimeChannel, + Svc: Into, + { + JaegerRemoteSamplerBuilder::new(runtime, http_client, default_sampler, service_name) + } +} + +impl ShouldSample for Sampler { + fn should_sample( + &self, + parent_context: Option<&Context>, + trace_id: TraceId, + name: &str, + span_kind: &SpanKind, + attributes: &[KeyValue], + links: &[Link], + ) -> SamplingResult { + let decision = match self { + // Always sample the trace + Sampler::AlwaysOn => SamplingDecision::RecordAndSample, + // Never sample the trace + Sampler::AlwaysOff => SamplingDecision::Drop, + // The parent decision if sampled; otherwise the decision of delegate_sampler + Sampler::ParentBased(delegate_sampler) => parent_context + .filter(|cx| cx.has_active_span()) + .map_or_else( + || { + delegate_sampler + .should_sample( + parent_context, + trace_id, + name, + span_kind, + attributes, + links, + ) + .decision + }, + |ctx| { + let span = ctx.span(); + let parent_span_context = span.span_context(); + if parent_span_context.is_sampled() { + SamplingDecision::RecordAndSample + } else { + SamplingDecision::Drop + } + }, + ), + // Probabilistically sample the trace. + Sampler::TraceIdRatioBased(prob) => sample_based_on_probability(prob, trace_id), + #[cfg(feature = "jaeger_remote_sampler")] + Sampler::JaegerRemote(remote_sampler) => { + remote_sampler + .should_sample(parent_context, trace_id, name, span_kind, attributes, links) + .decision + } + }; + SamplingResult { + decision, + // No extra attributes ever set by the SDK samplers. + attributes: Vec::new(), + // all sampler in SDK will not modify trace state. + trace_state: match parent_context { + Some(ctx) => ctx.span().span_context().trace_state().clone(), + None => TraceState::default(), + }, + } + } +} + +pub(crate) fn sample_based_on_probability(prob: &f64, trace_id: TraceId) -> SamplingDecision { + if *prob >= 1.0 { + SamplingDecision::RecordAndSample + } else { + let prob_upper_bound = (prob.max(0.0) * (1u64 << 63) as f64) as u64; + // TODO: update behavior when the spec definition resolves + // https://github.com/open-telemetry/opentelemetry-specification/issues/1413 + let bytes = trace_id.to_bytes(); + let (_, low) = bytes.split_at(8); + let trace_id_low = u64::from_be_bytes(low.try_into().unwrap()); + let rnd_from_trace_id = trace_id_low >> 1; + + if rnd_from_trace_id < prob_upper_bound { + SamplingDecision::RecordAndSample + } else { + SamplingDecision::Drop + } + } +} + +#[cfg(all(test, feature = "testing", feature = "trace"))] +mod tests { + use super::*; + use crate::testing::trace::TestSpan; + use opentelemetry::trace::{SpanContext, SpanId, TraceFlags}; + use rand::random; + + #[rustfmt::skip] + fn sampler_data() -> Vec<(&'static str, Sampler, f64, bool, bool)> { + vec![ + // Span w/o a parent + ("never_sample", Sampler::AlwaysOff, 0.0, false, false), + ("always_sample", Sampler::AlwaysOn, 1.0, false, false), + ("ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, false, false), + ("ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, false, false), + ("ratio_.50", Sampler::TraceIdRatioBased(0.50), 0.5, false, false), + ("ratio_.75", Sampler::TraceIdRatioBased(0.75), 0.75, false, false), + ("ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, false, false), + + // Spans w/o a parent delegate + ("delegate_to_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 1.0, false, false), + ("delegate_to_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 0.0, false, false), + ("delegate_to_ratio_-1", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(-1.0))), 0.0, false, false), + ("delegate_to_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 0.25, false, false), + ("delegate_to_ratio_.50", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.50))), 0.50, false, false), + ("delegate_to_ratio_.75", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.75))), 0.75, false, false), + ("delegate_to_ratio_2.0", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(2.0))), 1.0, false, false), + + // Spans with a parent that is *not* sampled act like spans w/o a parent + ("unsampled_parent_with_ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, true, false), + ("unsampled_parent_with_ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, true, false), + ("unsampled_parent_with_ratio_.50", Sampler::TraceIdRatioBased(0.50), 0.5, true, false), + ("unsampled_parent_with_ratio_.75", Sampler::TraceIdRatioBased(0.75), 0.75, true, false), + ("unsampled_parent_with_ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, true, false), + ("unsampled_parent_or_else_with_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 0.0, true, false), + ("unsampled_parent_or_else_with_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 0.0, true, false), + ("unsampled_parent_or_else_with_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 0.0, true, false), + + // A ratio sampler with a parent that is sampled will ignore the parent + ("sampled_parent_with_ratio_-1", Sampler::TraceIdRatioBased(-1.0), 0.0, true, true), + ("sampled_parent_with_ratio_.25", Sampler::TraceIdRatioBased(0.25), 0.25, true, true), + ("sampled_parent_with_ratio_2.0", Sampler::TraceIdRatioBased(2.0), 1.0, true, true), + + // Spans with a parent that is sampled, will always sample, regardless of the delegate sampler + ("sampled_parent_or_else_with_always_on", Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), 1.0, true, true), + ("sampled_parent_or_else_with_always_off", Sampler::ParentBased(Box::new(Sampler::AlwaysOff)), 1.0, true, true), + ("sampled_parent_or_else_with_ratio_.25", Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(0.25))), 1.0, true, true), + + // Spans with a sampled parent, but when using the NeverSample Sampler, aren't sampled + ("sampled_parent_span_with_never_sample", Sampler::AlwaysOff, 0.0, true, true), + ] + } + + #[test] + fn sampling() { + let total = 10_000; + for (name, sampler, expectation, parent, sample_parent) in sampler_data() { + let mut sampled = 0; + for _ in 0..total { + let parent_context = if parent { + let trace_flags = if sample_parent { + TraceFlags::SAMPLED + } else { + TraceFlags::default() + }; + let span_context = SpanContext::new( + TraceId::from(1), + SpanId::from(1), + trace_flags, + false, + TraceState::default(), + ); + + Some(Context::current_with_span(TestSpan(span_context))) + } else { + None + }; + + let trace_id = TraceId::from(random::()); + if sampler + .should_sample( + parent_context.as_ref(), + trace_id, + name, + &SpanKind::Internal, + &[], + &[], + ) + .decision + == SamplingDecision::RecordAndSample + { + sampled += 1; + } + } + let mut tolerance = 0.0; + let got = sampled as f64 / total as f64; + + if expectation > 0.0 && expectation < 1.0 { + // See https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval + let z = 4.75342; // This should succeed 99.9999% of the time + tolerance = z * (got * (1.0 - got) / total as f64).sqrt(); + } + + let diff = (got - expectation).abs(); + assert!( + diff <= tolerance, + "{name} got {got:?} (diff: {diff}), expected {expectation} (w/tolerance: {tolerance})" + ); + } + } + + #[test] + fn clone_a_parent_sampler() { + let sampler = Sampler::ParentBased(Box::new(Sampler::AlwaysOn)); + #[allow(clippy::redundant_clone)] + let cloned_sampler = sampler.clone(); + + let cx = Context::current_with_value("some_value"); + + let result = sampler.should_sample( + Some(&cx), + TraceId::from(1), + "should sample", + &SpanKind::Internal, + &[], + &[], + ); + + let cloned_result = cloned_sampler.should_sample( + Some(&cx), + TraceId::from(1), + "should sample", + &SpanKind::Internal, + &[], + &[], + ); + + assert_eq!(result, cloned_result); + } + + #[test] + fn parent_sampler() { + // name, delegate, context(with or without parent), expected decision + let test_cases = vec![ + ( + "should using delegate sampler", + Sampler::AlwaysOn, + Context::new(), + SamplingDecision::RecordAndSample, + ), + ( + "should use parent result, always off", + Sampler::AlwaysOn, + Context::current_with_span(TestSpan(SpanContext::new( + TraceId::from(1), + SpanId::from(1), + TraceFlags::default(), // not sampling + false, + TraceState::default(), + ))), + SamplingDecision::Drop, + ), + ( + "should use parent result, always on", + Sampler::AlwaysOff, + Context::current_with_span(TestSpan(SpanContext::new( + TraceId::from(1), + SpanId::from(1), + TraceFlags::SAMPLED, // not sampling + false, + TraceState::default(), + ))), + SamplingDecision::RecordAndSample, + ), + ]; + + for (name, delegate, parent_cx, expected) in test_cases { + let sampler = Sampler::ParentBased(Box::new(delegate)); + let result = sampler.should_sample( + Some(&parent_cx), + TraceId::from(1), + name, + &SpanKind::Internal, + &[], + &[], + ); + + assert_eq!(result.decision, expected); + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/mod.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/mod.rs new file mode 100644 index 00000000..5ee09940 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/mod.rs @@ -0,0 +1,10 @@ +mod rate_limit; +#[allow(dead_code)] +mod remote; +mod sampler; +mod sampling_strategy; + +pub use sampler::{JaegerRemoteSampler, JaegerRemoteSamplerBuilder}; + +#[cfg(test)] +mod tests {} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/rate_limit.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/rate_limit.rs new file mode 100644 index 00000000..3f11e5d9 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/rate_limit.rs @@ -0,0 +1,111 @@ +use opentelemetry::time::now; +use std::time::SystemTime; +// leaky bucket based rate limit +// should be Send+Sync +pub(crate) struct LeakyBucket { + span_per_sec: f64, + available: f64, + bucket_size: f64, + last_time: SystemTime, +} +use opentelemetry::otel_debug; + +impl LeakyBucket { + pub(crate) fn new(bucket_size: f64, span_per_sec: f64) -> LeakyBucket { + LeakyBucket { + span_per_sec, + available: bucket_size, + bucket_size, + last_time: now(), + } + } + + pub(crate) fn update(&mut self, span_per_sec: f64) { + self.span_per_sec = span_per_sec; + } + + pub(crate) fn should_sample(&mut self) -> bool { + self.check_availability(now) + } + + fn check_availability(&mut self, now: F) -> bool + where + F: Fn() -> SystemTime, + { + if self.available >= 1.0 { + self.available -= 1.0; + true + } else { + let cur_time = now(); + let elapsed = cur_time.duration_since(self.last_time); + match elapsed { + Ok(dur) => { + self.last_time = cur_time; + self.available = f64::min( + dur.as_secs() as f64 * self.span_per_sec + self.available, + self.bucket_size, + ); + + if self.available >= 1.0 { + self.available -= 1.0; + true + } else { + false + } + } + Err(err) => { + otel_debug!( + name: "JaegerRemoteSampler.LeakyBucket.ClockAdjustment", + message = "Jaeger remote sampler detected a rewind in system clock", + reason = format!("{:?}", err), + ); + true + } + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::trace::sampler::jaeger_remote::rate_limit::LeakyBucket; + use opentelemetry::time::now; + use std::ops::{Add, Sub}; + use std::time::Duration; + + #[test] + fn test_leaky_bucket() { + // maximum bucket size 2, add 1 allowance every 10 seconds + let mut leaky_bucket = LeakyBucket::new(2.0, 0.1); + let current_time = now(); + leaky_bucket.last_time = current_time; + + let test_cases = vec![ + (0, vec![true, true, false]), + (1, vec![false]), + (5, vec![false]), + (10, vec![true, false]), + (60, vec![true, true, false]), // maximum allowance is 2 + ]; + + for (elapsed_sec, cases) in test_cases.into_iter() { + for should_pass in cases { + assert_eq!( + should_pass, + leaky_bucket.check_availability(|| { + current_time.add(Duration::from_secs(elapsed_sec)) + }) + ) + } + } + } + + #[test] + fn test_rewind_clock_should_pass() { + let mut leaky_bucket = LeakyBucket::new(2.0, 0.1); + let current_time = now(); + leaky_bucket.last_time = current_time; + + assert!(leaky_bucket.check_availability(|| { current_time.sub(Duration::from_secs(10)) })) + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/remote.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/remote.rs new file mode 100644 index 00000000..4b28a691 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/remote.rs @@ -0,0 +1,86 @@ +/// Generate types based on proto +/// ProbabilisticSamplingStrategy samples traces with a fixed probability. +#[derive(serde::Serialize, serde::Deserialize, PartialOrd, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ProbabilisticSamplingStrategy { + /// samplingRate is the sampling probability in the range [0.0, 1.0]. + pub(crate) sampling_rate: f64, +} + +/// RateLimitingSamplingStrategy samples a fixed number of traces per time interval. +/// The typical implementations use the leaky bucket algorithm. +#[derive(serde::Serialize, serde::Deserialize, PartialOrd, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RateLimitingSamplingStrategy { + /// TODO this field type should be changed to double, to support rates like 1 per minute. + pub(crate) max_traces_per_second: i32, +} + +/// OperationSamplingStrategy is a sampling strategy for a given operation +/// (aka endpoint, span name). Only probabilistic sampling is currently supported. +#[derive(serde::Serialize, serde::Deserialize, PartialOrd, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct OperationSamplingStrategy { + pub(crate) operation: String, + pub(crate) probabilistic_sampling: ProbabilisticSamplingStrategy, +} + +/// PerOperationSamplingStrategies is a combination of strategies for different endpoints +/// as well as some service-wide defaults. It is particularly useful for services whose +/// endpoints receive vastly different traffic, so that any single rate of sampling would +/// result in either too much data for some endpoints or almost no data for other endpoints. +#[derive(serde::Serialize, serde::Deserialize, PartialOrd, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PerOperationSamplingStrategies { + /// defaultSamplingProbability is the sampling probability for spans that do not match + /// any of the perOperationStrategies. + pub(crate) default_sampling_probability: f64, + /// defaultLowerBoundTracesPerSecond defines a lower-bound rate limit used to ensure that + /// there is some minimal amount of traces sampled for an endpoint that might otherwise + /// be never sampled via probabilistic strategies. The limit is local to a service instance, + /// so if a service is deployed with many (N) instances, the effective minimum rate of sampling + /// will be N times higher. This setting applies to ALL operations, whether or not they match + /// one of the perOperationStrategies. + pub(crate) default_lower_bound_traces_per_second: f64, + /// perOperationStrategies describes sampling strategiesf for individual operations within + /// a given service. + pub(crate) per_operation_strategies: Vec, + /// defaultUpperBoundTracesPerSecond defines an upper bound rate limit. + /// However, almost no Jaeger SDKs support this parameter. + pub(crate) default_upper_bound_traces_per_second: f64, +} + +/// SamplingStrategyResponse contains an overall sampling strategy for a given service. +/// This type should be treated as a union where only one of the strategy field is present. +#[derive(serde::Serialize, serde::Deserialize, PartialOrd, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct SamplingStrategyResponse { + /// Legacy field that was meant to indicate which one of the strategy fields + /// below is present. This enum was not extended when per-operation strategy + /// was introduced, because extending enum has backwards compatiblity issues. + /// The recommended approach for consumers is to ignore this field and instead + /// checks the other fields being not null (starting with operationSampling). + /// For producers, it is recommended to set this field correctly for probabilistic + /// and rate-limiting strategies, but if per-operation strategy is returned, + /// the enum can be set to 0 (probabilistic). + pub(crate) strategy_type: SamplingStrategyType, + pub(crate) probabilistic_sampling: Option, + pub(crate) rate_limiting_sampling: Option, + pub(crate) operation_sampling: Option, +} + +/// SamplingStrategyParameters defines request parameters for remote sampler. +#[derive(serde::Serialize, serde::Deserialize, PartialOrd, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct SamplingStrategyParameters { + /// serviceName is a required argument. + pub(crate) service_name: String, +} + +/// See description of the SamplingStrategyResponse.strategyType field. +#[derive(serde::Serialize, serde::Deserialize, PartialOrd, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub(crate) enum SamplingStrategyType { + Probabilistic, + RateLimiting, +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/sampler.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/sampler.rs new file mode 100644 index 00000000..047ac1a3 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/sampler.rs @@ -0,0 +1,313 @@ +use crate::runtime::{to_interval_stream, RuntimeChannel}; +use crate::trace::sampler::jaeger_remote::remote::SamplingStrategyResponse; +use crate::trace::sampler::jaeger_remote::sampling_strategy::Inner; +use crate::trace::{Sampler, SamplingResult, ShouldSample}; +use futures_util::{stream, StreamExt as _}; +use http::Uri; +use opentelemetry::trace::{Link, SpanKind, TraceId}; +use opentelemetry::{otel_warn, Context, KeyValue}; +use opentelemetry_http::HttpClient; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +use thiserror::Error; + +const DEFAULT_REMOTE_SAMPLER_ENDPOINT: &str = "http://localhost:5778/sampling"; + +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum JaegerRemoteSamplerBuildError { + #[error("Invalid endpoint. Unable to create JaegerRemoteSampler.")] + InvalidEndpoint(String), +} + +/// Builder for [`JaegerRemoteSampler`]. +/// See [Sampler::jaeger_remote] for details. +#[derive(Debug)] +pub struct JaegerRemoteSamplerBuilder +where + R: RuntimeChannel, + C: HttpClient + 'static, + S: ShouldSample + 'static, +{ + pub(crate) update_interval: Duration, + pub(crate) client: C, + pub(crate) endpoint: String, + pub(crate) default_sampler: S, + pub(crate) leaky_bucket_size: f64, + pub(crate) runtime: R, + pub(crate) service_name: String, +} + +impl JaegerRemoteSamplerBuilder +where + C: HttpClient + 'static, + S: ShouldSample + 'static, + R: RuntimeChannel, +{ + pub(crate) fn new( + runtime: R, + http_client: C, + default_sampler: S, + service_name: Svc, + ) -> Self + where + Svc: Into, + { + JaegerRemoteSamplerBuilder { + runtime, + update_interval: Duration::from_secs(60 * 5), + client: http_client, + endpoint: DEFAULT_REMOTE_SAMPLER_ENDPOINT.to_string(), + default_sampler, + leaky_bucket_size: 100.0, + service_name: service_name.into(), + } + } + + /// Change how often the SDK should fetch the sampling strategy from remote servers + /// + /// By default it fetches every 5 minutes. + /// + /// A shorter interval have a performance overhead and should be avoid. + pub fn with_update_interval(self, interval: Duration) -> Self { + Self { + update_interval: interval, + ..self + } + } + + /// The endpoint of remote servers. + /// + /// By default it's `http://localhost:5778/sampling`. + /// + /// If service name is provided as part of the endpoint, it will be ignored. + pub fn with_endpoint>(self, endpoint: Str) -> Self { + Self { + endpoint: endpoint.into(), + ..self + } + } + + /// The size of the leaky bucket. + /// + /// By default the size is 100. + /// + /// It's used when sampling strategy is rate limiting. + pub fn with_leaky_bucket_size(self, size: f64) -> Self { + Self { + leaky_bucket_size: size, + ..self + } + } + + /// Build a [JaegerRemoteSampler] using provided configuration. + /// + /// Return errors if: + /// + /// - the endpoint provided is empty. + /// - the service name provided is empty. + pub fn build(self) -> Result { + let endpoint = Self::get_endpoint(&self.endpoint, &self.service_name) + .map_err(JaegerRemoteSamplerBuildError::InvalidEndpoint)?; + + Ok(Sampler::JaegerRemote(JaegerRemoteSampler::new( + self.runtime, + self.update_interval, + self.client, + endpoint, + self.default_sampler, + self.leaky_bucket_size, + ))) + } + + fn get_endpoint(endpoint: &str, service_name: &str) -> Result { + if endpoint.is_empty() || service_name.is_empty() { + return Err("endpoint and service name cannot be empty".to_string()); + } + let mut endpoint = url::Url::parse(endpoint) + .unwrap_or_else(|_| url::Url::parse(DEFAULT_REMOTE_SAMPLER_ENDPOINT).unwrap()); + + endpoint + .query_pairs_mut() + .append_pair("service", service_name); + + Uri::from_str(endpoint.as_str()).map_err(|_err| "invalid service name".to_string()) + } +} + +/// Sampler that fetches the sampling configuration from remotes. +/// +/// It offers the following sampling strategies: +/// - **Probabilistic**, fetch a probability between [0.0, 1.0] from remotes and use it to sample traces. If the probability is 0.0, it will never sample traces. If the probability is 1.0, it will always sample traces. +/// - **Rate limiting**, ses a leaky bucket rate limiter to ensure that traces are sampled with a certain constant rate. +/// - **Per Operations**, instead of sampling all traces, it samples traces based on the span name. Only probabilistic sampling is supported at the moment. +/// +/// User can build a [`JaegerRemoteSampler`] by getting a [`JaegerRemoteSamplerBuilder`] from [`Sampler::jaeger_remote`]. +/// +/// Note that the backend doesn't need to be Jaeger so long as it supports jaeger remote sampling +/// protocol. +#[derive(Clone, Debug)] +pub struct JaegerRemoteSampler { + inner: Arc, + default_sampler: Arc, +} + +impl JaegerRemoteSampler { + fn new( + runtime: R, + update_timeout: Duration, + client: C, + endpoint: Uri, + default_sampler: S, + leaky_bucket_size: f64, + ) -> Self + where + R: RuntimeChannel, + C: HttpClient + 'static, + S: ShouldSample + 'static, + { + let (shutdown_tx, shutdown_rx) = futures_channel::mpsc::channel(1); + let inner = Arc::new(Inner::new(leaky_bucket_size, shutdown_tx)); + let sampler = JaegerRemoteSampler { + inner, + default_sampler: Arc::new(default_sampler), + }; + Self::run_update_task( + runtime, + sampler.inner.clone(), + update_timeout, + client, + shutdown_rx, + endpoint, + ); + sampler + } + + // start a updating thread/task + fn run_update_task( + runtime: R, + strategy: Arc, + update_timeout: Duration, + client: C, + shutdown: futures_channel::mpsc::Receiver<()>, + endpoint: Uri, + ) where + R: RuntimeChannel, + C: HttpClient + 'static, + { + // todo: review if we need 'static here + let interval = to_interval_stream(runtime.clone(), update_timeout); + + runtime.spawn(async move { + // either update or shutdown + let mut update = Box::pin(stream::select( + shutdown.map(|_| false), + interval.map(|_| true), + )); + + while let Some(should_update) = update.next().await { + if should_update { + // poll next available configuration or shutdown + // send request + match Self::request_new_strategy(&client, endpoint.clone()).await { + Ok(remote_strategy_resp) => strategy.update(remote_strategy_resp), + Err(err_msg) => { + otel_warn!( + name: "JaegerRemoteSampler.FailedToFetchStrategy", + message= "Failed to fetch the sampling strategy from the remote endpoint. The last successfully fetched configuration will be used if available; otherwise, the default sampler will be applied until a successful configuration fetch.", + reason = err_msg.to_string(), + ); + } + }; + } else { + // shutdown + break; + } + } + }); + } + + async fn request_new_strategy( + client: &C, + endpoint: Uri, + ) -> Result + where + C: HttpClient, + { + let request = http::Request::get(endpoint) + .header("Content-Type", "application/json") + .body(Default::default()) + .unwrap(); + + let resp = client + .send_bytes(request) + .await + .map_err(|err| format!("the request is failed to send {err}"))?; + + // process failures + if resp.status() != http::StatusCode::OK { + return Err(format!( + "the http response code is not 200 but {}", + resp.status() + )); + } + + // deserialize the response + serde_json::from_slice(&resp.body()[..]) + .map_err(|err| format!("cannot deserialize the response, {err}")) + } +} + +impl ShouldSample for JaegerRemoteSampler { + fn should_sample( + &self, + parent_context: Option<&Context>, + trace_id: TraceId, + name: &str, + span_kind: &SpanKind, + attributes: &[KeyValue], + links: &[Link], + ) -> SamplingResult { + self.inner + .should_sample(parent_context, trace_id, name) + .unwrap_or_else(|| { + self.default_sampler.should_sample( + parent_context, + trace_id, + name, + span_kind, + attributes, + links, + ) + }) + } +} + +#[cfg(test)] +mod tests { + use crate::trace::sampler::jaeger_remote::remote::SamplingStrategyType; + use std::fmt::{Debug, Formatter}; + + impl Debug for SamplingStrategyType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self { + SamplingStrategyType::Probabilistic => f.write_str("Probabilistic"), + SamplingStrategyType::RateLimiting => f.write_str("RateLimiting"), + } + } + } + + #[test] + fn deserialize_sampling_strategy_response() { + let json = r#"{ + "strategyType": "PROBABILISTIC", + "probabilisticSampling": { + "samplingRate": 0.5 + } + }"#; + let resp: super::SamplingStrategyResponse = serde_json::from_str(json).unwrap(); + assert_eq!(resp.strategy_type, SamplingStrategyType::Probabilistic); + assert_eq!(resp.probabilistic_sampling.unwrap().sampling_rate, 0.5); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/sampling_strategy.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/sampling_strategy.rs new file mode 100644 index 00000000..b834dac8 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/sampler/jaeger_remote/sampling_strategy.rs @@ -0,0 +1,232 @@ +use crate::trace::sampler::jaeger_remote::remote::{ + PerOperationSamplingStrategies, ProbabilisticSamplingStrategy, RateLimitingSamplingStrategy, + SamplingStrategyResponse, +}; +use crate::trace::sampler::sample_based_on_probability; +use crate::trace::{SamplingDecision, SamplingResult}; +use opentelemetry::trace::{TraceContextExt, TraceId, TraceState}; +use opentelemetry::{otel_warn, Context}; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; +use std::sync::Mutex; + +use super::rate_limit::LeakyBucket; + +// todo: remove the mutex as probabilistic doesn't require mutable ref +// sampling strategy that sent by remote agents or collectors. +enum Strategy { + // probability to sample between [0.0, 1.0] + Probabilistic(f64), + //maxTracesPerSecond + RateLimiting(LeakyBucket), + PerOperation(PerOperationStrategies), +} + +pub(crate) struct Inner { + strategy: Mutex>, + // initial configuration for leaky bucket + leaky_bucket_size: f64, + shut_down: futures_channel::mpsc::Sender<()>, +} + +impl Debug for Inner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + //todo: add more debug information + f.debug_struct("JaegerRemoteSamplerInner") + .field("leaky_bucket_size", &self.leaky_bucket_size) + .finish() + } +} + +impl Drop for Inner { + fn drop(&mut self) { + let _ = self.shut_down.try_send(()); + } +} + +impl Inner { + pub(crate) fn new( + leaky_bucket_size: f64, + shut_down: futures_channel::mpsc::Sender<()>, + ) -> Self { + Inner { + strategy: Mutex::new(None), + leaky_bucket_size, + shut_down, + } + } + + pub(crate) fn update(&self, remote_strategy_resp: SamplingStrategyResponse) { + self.strategy + .lock() + .map(|mut old_strategy_opt| { + *old_strategy_opt = match old_strategy_opt.take() { + Some(mut old_strategy) => { + // update sample strategy + // the response should be an union type where + // - operation_sampling + // - rate_limiting_sampling + // - probabilistic_sampling + // are mutually exclusive. + match ( + remote_strategy_resp.operation_sampling, + remote_strategy_resp.rate_limiting_sampling, + remote_strategy_resp.probabilistic_sampling, + &mut old_strategy, + ) { + ( + None, + Some(rate_limiting), + None, + Strategy::RateLimiting(leaky_bucket), + ) => { + leaky_bucket.update(rate_limiting.max_traces_per_second as f64); + // in the future the remote response may support f64 + Some(old_strategy) + } + // only leaky bucket is a stateful sampler, meaning it's update is different from the initialization + // for other sampler, we can just re-init it + ( + operation_sampling, + rate_limiting_sampling, + probabilistic_sampling, + _, + ) => self.init_strategy( + operation_sampling, + rate_limiting_sampling, + probabilistic_sampling, + ), + } + } + None => self.init_strategy( + remote_strategy_resp.operation_sampling, + remote_strategy_resp.rate_limiting_sampling, + remote_strategy_resp.probabilistic_sampling, + ), + } + }) + .unwrap_or_else(|_err| { + otel_warn!( + name: "JaegerRemoteSampler.MutexPoisoned", + message = "Unable to update Jaeger Remote sampling strategy: the sampler's internal mutex is poisoned, likely due to a panic in another thread holding the lock. No further attempts to update the strategy will be made until the application or process restarts, and the last known configuration will continue to be used.", + ); + }); + } + + fn init_strategy( + &self, + operation_sampling: Option, + rate_limiting_sampling: Option, + probabilistic_sampling: Option, + ) -> Option { + match ( + operation_sampling, + rate_limiting_sampling, + probabilistic_sampling, + ) { + (Some(op_sampling), _, _) => { + // ops sampling + let mut per_ops_sampling = PerOperationStrategies::default(); + per_ops_sampling.update(op_sampling); + Some(Strategy::PerOperation(per_ops_sampling)) + } + (_, Some(rate_limiting), _) => Some(Strategy::RateLimiting(LeakyBucket::new( + self.leaky_bucket_size, + rate_limiting.max_traces_per_second as f64, + ))), + (_, _, Some(probabilistic)) => { + Some(Strategy::Probabilistic(probabilistic.sampling_rate)) + } + _ => { + otel_warn!( + name: "JaegerRemoteSampler.InvalidStrategyReceived", + message = "Invalid sampling strategy received from the remote endpoint. Expected one of: OperationSampling, RateLimitingSampling, or ProbabilisticSampling. Continuing to use the previous strategy or default sampler until a successful update.", + ); + None + } + } + } + + pub(crate) fn should_sample( + &self, + parent_context: Option<&Context>, + trace_id: TraceId, + name: &str, + ) -> Option { + self.strategy + .lock() + .map(|mut inner_opt| match inner_opt.as_mut() { + Some(inner) => { + let decision = match inner { + Strategy::RateLimiting(leaky_bucket) => { + if leaky_bucket.should_sample() { + SamplingDecision::RecordAndSample + } else { + SamplingDecision::Drop + } + } + Strategy::Probabilistic(prob) => { + sample_based_on_probability(prob, trace_id) + } + Strategy::PerOperation(per_operation_strategies) => { + sample_based_on_probability( + &per_operation_strategies.get_probability(name), + trace_id, + ) + } + }; + + Some(SamplingResult { + decision, + attributes: Vec::new(), + trace_state: match parent_context { + Some(ctx) => ctx.span().span_context().trace_state().clone(), + None => TraceState::default(), + }, + }) + } + None => None, + }) + .unwrap_or_else(|_| None) + } +} + +#[derive(Default)] +pub(crate) struct PerOperationStrategies { + default_prob: f64, + default_lower_bound_traces_per_second: f64, + operation_prob: HashMap, + // todo: guarantee the throughput using lower bound and upper bound + default_upper_bound_traces_per_second: f64, +} + +impl PerOperationStrategies { + pub(crate) fn update(&mut self, remote_strategies: PerOperationSamplingStrategies) { + self.default_prob = remote_strategies.default_sampling_probability; + self.default_lower_bound_traces_per_second = + remote_strategies.default_lower_bound_traces_per_second; + self.default_upper_bound_traces_per_second = + remote_strategies.default_upper_bound_traces_per_second; + + self.operation_prob = remote_strategies + .per_operation_strategies + .into_iter() + .map(|op_strategy| { + ( + op_strategy.operation, + op_strategy.probabilistic_sampling.sampling_rate, + ) + }) + .collect(); + } + + pub(crate) fn get_probability(&self, operation: &str) -> f64 { + *self + .operation_prob + .get(operation) + .unwrap_or(&self.default_prob) + } +} + +#[cfg(test)] +mod tests {} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span.rs new file mode 100644 index 00000000..09cc7474 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span.rs @@ -0,0 +1,768 @@ +//! # Span +//! +//! `Span`s represent a single operation within a trace. `Span`s can be nested to form a trace +//! tree. Each trace contains a root span, which typically describes the end-to-end latency and, +//! optionally, one or more sub-spans for its sub-operations. +//! +//! The `Span`'s start and end timestamps reflect the elapsed real time of the operation. A `Span`'s +//! start time is set to the current time on span creation. After the `Span` is created, it +//! is possible to change its name, set its `Attributes`, and add `Links` and `Events`. +//! These cannot be changed after the `Span`'s end time has been set. +use crate::trace::SpanLimits; +use opentelemetry::trace::{Event, Link, SpanContext, SpanId, SpanKind, Status}; +use opentelemetry::KeyValue; +use std::borrow::Cow; +use std::time::SystemTime; + +/// Single operation within a trace. +#[derive(Debug)] +pub struct Span { + span_context: SpanContext, + data: Option, + tracer: crate::trace::SdkTracer, + span_limits: SpanLimits, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct SpanData { + /// Span parent id + pub(crate) parent_span_id: SpanId, + /// Parent span is remote flag (for span flags) + pub(crate) parent_span_is_remote: bool, + /// Span kind + pub(crate) span_kind: SpanKind, + /// Span name + pub(crate) name: Cow<'static, str>, + /// Span start time + pub(crate) start_time: SystemTime, + /// Span end time + pub(crate) end_time: SystemTime, + /// Span attributes + pub(crate) attributes: Vec, + /// The number of attributes that were above the configured limit, and thus + /// dropped. + pub(crate) dropped_attributes_count: u32, + /// Span events + pub(crate) events: crate::trace::SpanEvents, + /// Span Links + pub(crate) links: crate::trace::SpanLinks, + /// Span status + pub(crate) status: Status, +} + +impl Span { + pub(crate) fn new( + span_context: SpanContext, + data: Option, + tracer: crate::trace::SdkTracer, + span_limit: SpanLimits, + ) -> Self { + Span { + span_context, + data, + tracer, + span_limits: span_limit, + } + } + + /// Operate on a mutable reference to span data + fn with_data(&mut self, f: F) -> Option + where + F: FnOnce(&mut SpanData) -> T, + { + self.data.as_mut().map(f) + } + + /// Convert information in this span into `exporter::trace::SpanData`. + /// This function copies all data from the current span, which will create a + /// overhead. + pub fn exported_data(&self) -> Option { + let (span_context, tracer) = (self.span_context.clone(), &self.tracer); + + self.data + .as_ref() + .map(|data| build_export_data(data.clone(), span_context, tracer)) + } +} + +impl opentelemetry::trace::Span for Span { + /// Records events at a specific time in the context of a given `Span`. + /// + /// Note that the OpenTelemetry project documents certain ["standard event names and + /// keys"](https://github.com/open-telemetry/opentelemetry-specification/tree/v0.5.0/specification/trace/semantic_conventions/README.md) + /// which have prescribed semantic meanings. + fn add_event_with_timestamp( + &mut self, + name: T, + timestamp: SystemTime, + mut attributes: Vec, + ) where + T: Into>, + { + let span_events_limit = self.span_limits.max_events_per_span as usize; + let event_attributes_limit = self.span_limits.max_attributes_per_event as usize; + self.with_data(|data| { + if data.events.len() < span_events_limit { + let dropped_attributes_count = + attributes.len().saturating_sub(event_attributes_limit); + attributes.truncate(event_attributes_limit); + + data.events.add_event(Event::new( + name, + timestamp, + attributes, + dropped_attributes_count as u32, + )); + } else { + data.events.dropped_count += 1; + } + }); + } + + /// Returns the `SpanContext` for the given `Span`. + fn span_context(&self) -> &SpanContext { + &self.span_context + } + + /// Returns true if this `Span` is recording information like events with the `add_event` + /// operation, attributes using `set_attributes`, status with `set_status`, etc. + /// Always returns false after span `end`. + fn is_recording(&self) -> bool { + self.data.is_some() + } + + /// Sets a single `Attribute` where the attribute properties are passed as arguments. + /// + /// Note that the OpenTelemetry project documents certain ["standard + /// attributes"](https://github.com/open-telemetry/opentelemetry-specification/tree/v0.5.0/specification/trace/semantic_conventions/README.md) + /// that have prescribed semantic meanings. + fn set_attribute(&mut self, attribute: KeyValue) { + let span_attribute_limit = self.span_limits.max_attributes_per_span as usize; + self.with_data(|data| { + if data.attributes.len() < span_attribute_limit { + data.attributes.push(attribute); + } else { + data.dropped_attributes_count += 1; + } + }); + } + + /// Sets the status of this `Span`. + /// + /// If used, this will override the default span status, which is [`Status::Unset`]. + fn set_status(&mut self, status: Status) { + self.with_data(|data| { + // check if we should update the status + // These values form a total order: Ok > Error > Unset. + if status > data.status { + data.status = status; + } + }); + } + + /// Updates the `Span`'s name. + fn update_name(&mut self, new_name: T) + where + T: Into>, + { + self.with_data(|data| { + data.name = new_name.into(); + }); + } + + /// Add `Link` to this `Span` + /// + fn add_link(&mut self, span_context: SpanContext, attributes: Vec) { + let span_links_limit = self.span_limits.max_links_per_span as usize; + let link_attributes_limit = self.span_limits.max_attributes_per_link as usize; + self.with_data(|data| { + if data.links.links.len() < span_links_limit { + let dropped_attributes_count = + attributes.len().saturating_sub(link_attributes_limit); + let mut attributes = attributes; + attributes.truncate(link_attributes_limit); + data.links.add_link(Link::new( + span_context, + attributes, + dropped_attributes_count as u32, + )); + } else { + data.links.dropped_count += 1; + } + }); + } + + /// Finishes the span with given timestamp. + fn end_with_timestamp(&mut self, timestamp: SystemTime) { + self.ensure_ended_and_exported(Some(timestamp)); + } +} + +impl Span { + fn ensure_ended_and_exported(&mut self, timestamp: Option) { + // skip if data has already been exported + let mut data = match self.data.take() { + Some(data) => data, + None => return, + }; + + let provider = self.tracer.provider(); + // skip if provider has been shut down + if provider.is_shutdown() { + return; + } + + // ensure end time is set via explicit end or implicitly on drop + if let Some(timestamp) = timestamp { + data.end_time = timestamp; + } else if data.end_time == data.start_time { + data.end_time = opentelemetry::time::now(); + } + + match provider.span_processors() { + [] => {} + [processor] => { + processor.on_end(build_export_data( + data, + self.span_context.clone(), + &self.tracer, + )); + } + processors => { + for processor in processors { + processor.on_end(build_export_data( + data.clone(), + self.span_context.clone(), + &self.tracer, + )); + } + } + } + } +} + +impl Drop for Span { + /// Report span on inner drop + fn drop(&mut self) { + self.ensure_ended_and_exported(None); + } +} + +fn build_export_data( + data: SpanData, + span_context: SpanContext, + tracer: &crate::trace::SdkTracer, +) -> crate::trace::SpanData { + crate::trace::SpanData { + span_context, + parent_span_id: data.parent_span_id, + parent_span_is_remote: data.parent_span_is_remote, + span_kind: data.span_kind, + name: data.name, + start_time: data.start_time, + end_time: data.end_time, + attributes: data.attributes, + dropped_attributes_count: data.dropped_attributes_count, + events: data.events, + links: data.links, + status: data.status, + instrumentation_scope: tracer.instrumentation_scope().clone(), + } +} + +#[cfg(all(test, feature = "testing"))] +mod tests { + use super::*; + use crate::testing::trace::NoopSpanExporter; + use crate::trace::span_limit::{ + DEFAULT_MAX_ATTRIBUTES_PER_EVENT, DEFAULT_MAX_ATTRIBUTES_PER_LINK, + DEFAULT_MAX_ATTRIBUTES_PER_SPAN, DEFAULT_MAX_EVENT_PER_SPAN, DEFAULT_MAX_LINKS_PER_SPAN, + }; + use crate::trace::{SpanEvents, SpanLinks}; + use opentelemetry::trace::{self, SpanBuilder, TraceFlags, TraceId, Tracer}; + use opentelemetry::{trace::Span as _, trace::TracerProvider}; + use std::time::Duration; + use std::vec; + + fn init() -> (crate::trace::SdkTracer, SpanData) { + let provider = crate::trace::SdkTracerProvider::default(); + let tracer = provider.tracer("opentelemetry"); + let data = SpanData { + parent_span_id: SpanId::from(0), + parent_span_is_remote: false, + span_kind: trace::SpanKind::Internal, + name: "opentelemetry".into(), + start_time: opentelemetry::time::now(), + end_time: opentelemetry::time::now(), + attributes: Vec::new(), + dropped_attributes_count: 0, + events: SpanEvents::default(), + links: SpanLinks::default(), + status: Status::Unset, + }; + (tracer, data) + } + + fn create_span() -> Span { + let (tracer, data) = init(); + Span::new( + SpanContext::empty_context(), + Some(data), + tracer, + Default::default(), + ) + } + + #[test] + fn create_span_without_data() { + let (tracer, _) = init(); + let mut span = Span::new( + SpanContext::empty_context(), + None, + tracer, + Default::default(), + ); + span.with_data(|_data| panic!("there are data")); + } + + #[test] + fn create_span_with_data_mut() { + let (tracer, data) = init(); + let mut span = Span::new( + SpanContext::empty_context(), + Some(data.clone()), + tracer, + Default::default(), + ); + span.with_data(|d| assert_eq!(*d, data)); + } + + #[test] + fn add_event() { + let mut span = create_span(); + let name = "some_event"; + let attributes = vec![KeyValue::new("k", "v")]; + span.add_event(name, attributes.clone()); + span.with_data(|data| { + if let Some(event) = data.events.iter().next() { + assert_eq!(event.name, name); + assert_eq!(event.attributes, attributes); + } else { + panic!("no event"); + } + }); + } + + #[test] + fn add_event_with_timestamp() { + let mut span = create_span(); + let name = "some_event"; + let attributes = vec![KeyValue::new("k", "v")]; + let timestamp = opentelemetry::time::now(); + span.add_event_with_timestamp(name, timestamp, attributes.clone()); + span.with_data(|data| { + if let Some(event) = data.events.iter().next() { + assert_eq!(event.timestamp, timestamp); + assert_eq!(event.name, name); + assert_eq!(event.attributes, attributes); + } else { + panic!("no event"); + } + }); + } + + #[test] + fn record_error() { + let mut span = create_span(); + let err = std::io::Error::from(std::io::ErrorKind::Other); + span.record_error(&err); + span.with_data(|data| { + if let Some(event) = data.events.iter().next() { + assert_eq!(event.name, "exception"); + assert_eq!( + event.attributes, + vec![KeyValue::new("exception.message", err.to_string())] + ); + } else { + panic!("no event"); + } + }); + } + + #[test] + fn set_attribute() { + let mut span = create_span(); + let attributes = KeyValue::new("k", "v"); + span.set_attribute(attributes.clone()); + span.with_data(|data| { + let matching_attribute: Vec<&KeyValue> = data + .attributes + .iter() + .filter(|kv| kv.key.as_str() == attributes.key.as_str()) + .collect(); + if matching_attribute.len() == 1 { + assert_eq!(matching_attribute[0].value, attributes.value); + } else { + panic!("no attribute"); + } + }); + } + + #[test] + fn set_attributes() { + let mut span = create_span(); + let attributes = vec![KeyValue::new("k1", "v1"), KeyValue::new("k2", "v2")]; + span.set_attributes(attributes); + span.with_data(|data| { + assert_eq!(data.attributes.len(), 2); + }); + } + + #[test] + fn set_status() { + { + let mut span = create_span(); + let status = Status::Ok; + span.set_status(status.clone()); + span.with_data(|data| assert_eq!(data.status, status)); + } + { + let mut span = create_span(); + let status = Status::Unset; + span.set_status(status.clone()); + span.with_data(|data| assert_eq!(data.status, status)); + } + { + let mut span = create_span(); + let status = Status::error("error"); + span.set_status(status.clone()); + span.with_data(|data| assert_eq!(data.status, status)); + } + { + let mut span = create_span(); + // ok status should be final + span.set_status(Status::Ok); + span.set_status(Status::error("error")); + span.with_data(|data| assert_eq!(data.status, Status::Ok)); + } + { + let mut span = create_span(); + // error status should be able to override unset + span.set_status(Status::Unset); + span.set_status(Status::error("error")); + span.with_data(|data| assert_ne!(data.status, Status::Ok)); + } + } + + #[test] + fn update_name() { + let mut span = create_span(); + let name = "new_name"; + span.update_name(name); + span.with_data(|data| { + assert_eq!(data.name, name); + }); + } + + #[test] + fn end() { + let mut span = create_span(); + span.end(); + } + + #[test] + fn end_with_timestamp() { + let mut span = create_span(); + let timestamp = opentelemetry::time::now(); + span.end_with_timestamp(timestamp); + span.with_data(|data| assert_eq!(data.end_time, timestamp)); + } + + #[test] + fn allows_to_get_span_context_after_end() { + let mut span = create_span(); + span.end(); + assert_eq!(span.span_context(), &SpanContext::empty_context()); + } + + #[test] + fn end_only_once() { + let mut span = create_span(); + let timestamp = opentelemetry::time::now(); + span.end_with_timestamp(timestamp); + span.end_with_timestamp(timestamp.checked_add(Duration::from_secs(10)).unwrap()); + span.with_data(|data| assert_eq!(data.end_time, timestamp)); + } + + #[test] + fn noop_after_end() { + let mut span = create_span(); + let initial = span.with_data(|data| data.clone()).unwrap(); + span.end(); + span.add_event("some_event", vec![KeyValue::new("k", "v")]); + span.add_event_with_timestamp( + "some_event", + opentelemetry::time::now(), + vec![KeyValue::new("k", "v")], + ); + let err = std::io::Error::from(std::io::ErrorKind::Other); + span.record_error(&err); + span.set_attribute(KeyValue::new("k", "v")); + span.set_status(Status::error("ERROR")); + span.update_name("new_name"); + span.with_data(|data| { + assert_eq!(data.events, initial.events); + assert_eq!(data.attributes, initial.attributes); + assert_eq!(data.status, initial.status); + assert_eq!(data.name, initial.name); + }); + } + + #[test] + fn is_recording_true_when_not_ended() { + let span = create_span(); + assert!(span.is_recording()); + } + + #[test] + fn is_recording_false_after_end() { + let mut span = create_span(); + span.end(); + assert!(!span.is_recording()); + } + + #[test] + fn exceed_span_attributes_limit() { + let exporter = NoopSpanExporter::new(); + let provider_builder = + crate::trace::SdkTracerProvider::builder().with_simple_exporter(exporter); + let provider = provider_builder.build(); + let tracer = provider.tracer("opentelemetry-test"); + + let mut initial_attributes = Vec::new(); + let mut expected_dropped_count = 1; + for i in 0..(DEFAULT_MAX_ATTRIBUTES_PER_SPAN + 1) { + initial_attributes.push(KeyValue::new(format!("key {i}"), i.to_string())) + } + let span_builder = SpanBuilder::from_name("test_span").with_attributes(initial_attributes); + + let mut span = tracer.build(span_builder); + expected_dropped_count += 1; + span.set_attribute(KeyValue::new("key3", "value3")); + + expected_dropped_count += 2; + let span_attributes_after_creation = + vec![KeyValue::new("foo", "1"), KeyValue::new("bar", "2")]; + span.set_attributes(span_attributes_after_creation); + + let actual_span = span + .data + .clone() + .expect("span data should not be empty as we already set it before"); + assert_eq!( + actual_span.attributes.len(), + DEFAULT_MAX_ATTRIBUTES_PER_SPAN as usize, + "Span attributes should be truncated to the max limit" + ); + assert_eq!( + actual_span.dropped_attributes_count, expected_dropped_count, + "Dropped count should match the actual count of attributes dropped" + ); + } + + #[test] + fn exceed_event_attributes_limit() { + let exporter = NoopSpanExporter::new(); + let provider_builder = + crate::trace::SdkTracerProvider::builder().with_simple_exporter(exporter); + let provider = provider_builder.build(); + let tracer = provider.tracer("opentelemetry-test"); + + let mut event1 = Event::with_name("test event"); + for i in 0..(DEFAULT_MAX_ATTRIBUTES_PER_EVENT * 2) { + event1 + .attributes + .push(KeyValue::new(format!("key {i}"), i.to_string())) + } + let event2 = event1.clone(); + + // add event when build + let span_builder = tracer.span_builder("test").with_events(vec![event1]); + let mut span = tracer.build(span_builder); + + // add event after build + span.add_event("another test event", event2.attributes); + + let event_queue = span + .data + .clone() + .expect("span data should not be empty as we already set it before") + .events; + let event_vec: Vec<_> = event_queue.iter().take(2).collect(); + #[allow(clippy::get_first)] // we want to extract first two elements + let processed_event_1 = event_vec.get(0).expect("should have at least two events"); + let processed_event_2 = event_vec.get(1).expect("should have at least two events"); + assert_eq!(processed_event_1.attributes.len(), 128); + assert_eq!(processed_event_2.attributes.len(), 128); + } + + #[test] + fn exceed_link_attributes_limit() { + let exporter = NoopSpanExporter::new(); + let provider_builder = + crate::trace::SdkTracerProvider::builder().with_simple_exporter(exporter); + let provider = provider_builder.build(); + let tracer = provider.tracer("opentelemetry-test"); + + let mut link = Link::with_context(SpanContext::new( + TraceId::from(12), + SpanId::from(12), + TraceFlags::default(), + false, + Default::default(), + )); + for i in 0..(DEFAULT_MAX_ATTRIBUTES_PER_LINK * 2) { + link.attributes + .push(KeyValue::new(format!("key {i}"), i.to_string())); + } + + let span_builder = tracer.span_builder("test").with_links(vec![link]); + let span = tracer.build(span_builder); + let link_queue = span + .data + .clone() + .expect("span data should not be empty as we already set it before") + .links; + let link_vec: Vec<_> = link_queue.links; + let processed_link = link_vec.first().expect("should have at least one link"); + assert_eq!(processed_link.attributes.len(), 128); + } + + #[test] + fn exceed_span_links_limit() { + let exporter = NoopSpanExporter::new(); + let provider_builder = + crate::trace::SdkTracerProvider::builder().with_simple_exporter(exporter); + let provider = provider_builder.build(); + let tracer = provider.tracer("opentelemetry-test"); + + let mut links = Vec::new(); + for _i in 0..(DEFAULT_MAX_LINKS_PER_SPAN * 2) { + links.push(Link::with_context(SpanContext::new( + TraceId::from(12), + SpanId::from(12), + TraceFlags::default(), + false, + Default::default(), + ))) + } + + let span_builder = tracer.span_builder("test").with_links(links); + let mut span = tracer.build(span_builder); + + // add links using span api after building the span + span.add_link( + SpanContext::new( + TraceId::from(12), + SpanId::from(12), + TraceFlags::default(), + false, + Default::default(), + ), + vec![], + ); + let link_queue = span + .data + .clone() + .expect("span data should not be empty as we already set it before") + .links; + let link_vec: Vec<_> = link_queue.links; + assert_eq!(link_vec.len(), DEFAULT_MAX_LINKS_PER_SPAN as usize); + } + + #[test] + fn exceed_span_events_limit() { + let exporter = NoopSpanExporter::new(); + let provider_builder = + crate::trace::SdkTracerProvider::builder().with_simple_exporter(exporter); + let provider = provider_builder.build(); + let tracer = provider.tracer("opentelemetry-test"); + + let mut events = Vec::new(); + for _i in 0..(DEFAULT_MAX_EVENT_PER_SPAN * 2) { + events.push(Event::with_name("test event")) + } + + // add events via span builder + let span_builder = tracer.span_builder("test").with_events(events); + let mut span = tracer.build(span_builder); + + // add events using span api after building the span + span.add_event("test event again, after span builder", Vec::new()); + span.add_event("test event once again, after span builder", Vec::new()); + let span_events = span + .data + .clone() + .expect("span data should not be empty as we already set it before") + .events; + let event_vec: Vec<_> = span_events.events; + assert_eq!(event_vec.len(), DEFAULT_MAX_EVENT_PER_SPAN as usize); + } + + #[test] + fn multiple_processors_receive_span_data() { + use crate::trace::InMemorySpanExporterBuilder; + + let exporter1 = InMemorySpanExporterBuilder::new().build(); + let exporter2 = InMemorySpanExporterBuilder::new().build(); + + let provider = crate::trace::SdkTracerProvider::builder() + .with_simple_exporter(exporter1.clone()) + .with_simple_exporter(exporter2.clone()) + .build(); + + let tracer = provider.tracer("test"); + let mut span = tracer.start("multi_processor_span"); + span.set_attribute(KeyValue::new("key", "value")); + span.end(); + + let spans1 = exporter1.get_finished_spans().unwrap(); + let spans2 = exporter2.get_finished_spans().unwrap(); + + assert_eq!(spans1.len(), 1); + assert_eq!(spans2.len(), 1); + assert_eq!(spans1[0].name, "multi_processor_span"); + assert_eq!(spans2[0].name, "multi_processor_span"); + assert_eq!(spans1[0].attributes, spans2[0].attributes); + // Verify instrumentation scope is correctly propagated to both processors + assert_eq!(spans1[0].instrumentation_scope.name(), "test"); + assert_eq!(spans2[0].instrumentation_scope.name(), "test"); + assert_eq!( + spans1[0].instrumentation_scope, + spans2[0].instrumentation_scope + ); + + let _ = provider.shutdown(); + } + + #[test] + fn test_span_exported_data() { + let provider = crate::trace::SdkTracerProvider::builder() + .with_simple_exporter(NoopSpanExporter::new()) + .build(); + let tracer = provider.tracer("test"); + + let mut span = tracer.start("test_span"); + span.add_event("test_event", vec![]); + span.set_status(Status::error("")); + + let exported_data = span.exported_data(); + assert!(exported_data.is_some()); + let res = provider.shutdown(); + println!("{res:?}"); + assert!(res.is_ok()); + let dropped_span = tracer.start("span_with_dropped_provider"); + // return none if the provider has already been dropped + assert!(dropped_span.exported_data().is_none()); + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_limit.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_limit.rs new file mode 100644 index 00000000..7dedce08 --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_limit.rs @@ -0,0 +1,46 @@ +/// # Span limit +/// Erroneous code can add unintended attributes, events, and links to a span. If these collections +/// are unbounded, they can quickly exhaust available memory, resulting in crashes that are +/// difficult to recover from safely. +/// +/// To protected against those errors. Users can use span limit to configure +/// - Maximum allowed span attribute count +/// - Maximum allowed span event count +/// - Maximum allowed span link count +/// - Maximum allowed attribute per span event count +/// - Maximum allowed attribute per span link count +/// +/// If the limit has been breached. The attributes, events or links will be dropped based on their +/// index in the collection. The one added to collections later will be dropped first. +pub(crate) const DEFAULT_MAX_EVENT_PER_SPAN: u32 = 128; +pub(crate) const DEFAULT_MAX_ATTRIBUTES_PER_SPAN: u32 = 128; +pub(crate) const DEFAULT_MAX_LINKS_PER_SPAN: u32 = 128; +pub(crate) const DEFAULT_MAX_ATTRIBUTES_PER_EVENT: u32 = 128; +pub(crate) const DEFAULT_MAX_ATTRIBUTES_PER_LINK: u32 = 128; + +/// Span limit configuration to keep attributes, events and links to a span in a reasonable number. +#[derive(Copy, Clone, Debug)] +pub struct SpanLimits { + /// The max events that can be added to a `Span`. + pub max_events_per_span: u32, + /// The max attributes that can be added to a `Span`. + pub max_attributes_per_span: u32, + /// The max links that can be added to a `Span`. + pub max_links_per_span: u32, + /// The max attributes that can be added into an `Event` + pub max_attributes_per_event: u32, + /// The max attributes that can be added into a `Link` + pub max_attributes_per_link: u32, +} + +impl Default for SpanLimits { + fn default() -> Self { + SpanLimits { + max_events_per_span: DEFAULT_MAX_EVENT_PER_SPAN, + max_attributes_per_span: DEFAULT_MAX_ATTRIBUTES_PER_SPAN, + max_links_per_span: DEFAULT_MAX_LINKS_PER_SPAN, + max_attributes_per_link: DEFAULT_MAX_ATTRIBUTES_PER_LINK, + max_attributes_per_event: DEFAULT_MAX_ATTRIBUTES_PER_EVENT, + } + } +} diff --git a/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_processor.rs b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_processor.rs new file mode 100644 index 00000000..06b8d9ac --- /dev/null +++ b/vendor/opentelemetry-rust/opentelemetry_sdk/src/trace/span_processor.rs @@ -0,0 +1,2361 @@ +//! # OpenTelemetry Span Processor Interface +//! +//! Span processor is an interface which allows hooks for span start and end method +//! invocations. The span processors are invoked only when +//! [`is_recording`] is true. +//! +//! Built-in span processors are responsible for batching and conversion of spans to +//! exportable representation and passing batches to exporters. +//! +//! Span processors can be registered directly on SDK [`TracerProvider`] and they are +//! invoked in the same order as they were registered. +//! +//! All `Tracer` instances created by a `TracerProvider` share the same span processors. +//! Changes to this collection reflect in all `Tracer` instances. +//! +//! The following diagram shows `SpanProcessor`'s relationship to other components +//! in the SDK: +//! +//! ```ascii +//! +-----+--------------+ +-----------------------+ +-------------------+ +//! | | | | | | | +//! | | | | (Batch)SpanProcessor | | SpanExporter | +//! | | +---> (Simple)SpanProcessor +---> (OTLPExporter) | +//! | | | | | | | +//! | SDK | Tracer.span()| +-----------------------+ +-------------------+ +//! | | Span.end() | +//! | | | +//! | | | +//! | | | +//! | | | +//! +-----+--------------+ +//! ``` +//! +//! [`is_recording`]: opentelemetry::trace::Span::is_recording() +//! [`TracerProvider`]: opentelemetry::trace::TracerProvider + +use crate::error::{OTelSdkError, OTelSdkResult}; +use crate::resource::Resource; +use crate::trace::Span; +use crate::trace::{SpanData, SpanExporter}; +use opentelemetry::Context; +#[cfg(feature = "experimental_metrics_bound_instruments")] +use opentelemetry::KeyValue; +use opentelemetry::{otel_debug, otel_error, otel_warn}; +use std::cmp::min; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::{env, str::FromStr, time::Duration}; + +use std::sync::atomic::AtomicBool; +use std::thread; +use std::time::Instant; + +/// Environment variable for configuring the delay interval (in milliseconds) +/// between two consecutive exports for the [`BatchSpanProcessor`]. +pub const OTEL_BSP_SCHEDULE_DELAY: &str = "OTEL_BSP_SCHEDULE_DELAY"; +/// Default delay interval between two consecutive exports. +pub const OTEL_BSP_SCHEDULE_DELAY_DEFAULT: Duration = Duration::from_millis(5_000); +/// Environment variable for configuring the maximum queue size for the +/// [`BatchSpanProcessor`]. +pub const OTEL_BSP_MAX_QUEUE_SIZE: &str = "OTEL_BSP_MAX_QUEUE_SIZE"; +/// Default maximum queue size +pub const OTEL_BSP_MAX_QUEUE_SIZE_DEFAULT: usize = 2_048; +/// Environment variable for configuring the maximum batch size for the +/// [`BatchSpanProcessor`], must be less than or equal to +/// `OTEL_BSP_MAX_QUEUE_SIZE`. +pub const OTEL_BSP_MAX_EXPORT_BATCH_SIZE: &str = "OTEL_BSP_MAX_EXPORT_BATCH_SIZE"; +/// Default maximum batch size +pub const OTEL_BSP_MAX_EXPORT_BATCH_SIZE_DEFAULT: usize = 512; +/// Environment variable for configuring the maximum allowed time to export +/// data. +/// +/// This value is honored by +/// `span_processor_with_async_runtime::BatchSpanProcessor`. The thread-based +/// [`BatchSpanProcessor`] ignores this setting. +pub const OTEL_BSP_EXPORT_TIMEOUT: &str = "OTEL_BSP_EXPORT_TIMEOUT"; +/// Default maximum allowed time to export data. +/// +/// See [`OTEL_BSP_EXPORT_TIMEOUT`] for which processors honor this value. +pub const OTEL_BSP_EXPORT_TIMEOUT_DEFAULT: Duration = Duration::from_millis(30_000); +pub(crate) const OTEL_BSP_MAX_CONCURRENT_EXPORTS: &str = "OTEL_BSP_MAX_CONCURRENT_EXPORTS"; +/// Default max concurrent exports for BSP +pub(crate) const OTEL_BSP_MAX_CONCURRENT_EXPORTS_DEFAULT: usize = 1; + +/// `SpanProcessor` is an interface which allows hooks for span start and end +/// method invocations. The span processors are invoked only when is_recording +/// is true. +pub trait SpanProcessor: Send + Sync + std::fmt::Debug { + /// `on_start` is called when a `Span` is started. This method is called + /// synchronously on the thread that started the span, therefore it should + /// not block or throw exceptions. + fn on_start(&self, span: &mut Span, cx: &Context); + + /// `on_end` is called after a `Span` is ended (i.e., the end timestamp is + /// already set). This method is called synchronously within the `Span::end` + /// API, therefore it should not block or throw an exception. + /// + /// # Accessing Context + /// + /// **Important**: Do not rely on [`Context::current()`] in `on_end`. When `on_end` + /// is called during span cleanup, `Context::current()` returns whatever context + /// happens to be active at that moment, which is typically unrelated to the span + /// being ended. Contexts can be activated in any order and are not necessarily + /// hierarchical. + /// + /// **Best Practice**: Extract any needed context information in [`on_start`] + /// and store it as span attributes. This ensures the information is available + /// in the [`SpanData`] passed to `on_end`. + /// + /// # Example + /// + /// ```rust,ignore + /// impl SpanProcessor for MyProcessor { + /// fn on_start(&self, span: &mut Span, cx: &Context) { + /// // Extract baggage and store as span attribute + /// if let Some(value) = cx.baggage().get("my-key") { + /// span.set_attribute(KeyValue::new("my-key", value.to_string())); + /// } + /// } + /// + /// fn on_end(&self, span: SpanData) { + /// // Access the attribute stored in on_start + /// let my_value = span.attributes.iter() + /// .find(|kv| kv.key.as_str() == "my-key"); + /// } + /// } + /// ``` + /// + /// # Filtering completed spans + /// + /// **Warning:** Filtering individual spans can produce incomplete or broken + /// traces, such as an exported child whose parent was discarded. This does + /// not coordinate filtering across spans or services. For coordinated + /// decisions based on completed spans, prefer [tail-based sampling] in the + /// OpenTelemetry Collector or another telemetry pipeline. All spans in a + /// trace must reach the same tail-sampling instance; it cannot recover spans + /// already discarded by SDK sampling or filtering. + /// + /// [tail-based sampling]: https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/tailsamplingprocessor + /// + /// If SDK processor filtering fits your requirements and you accept this + /// tradeoff, wrap another processor and delegate only the spans that satisfy + /// your condition. This example uses an attribute, but the + /// condition can use any information in [`SpanData`] or other processor state. + /// Register only the wrapper with [`SdkTracerProvider`](crate::trace::SdkTracerProvider), since separately + /// registered processors receive spans independently. + /// + /// ```rust + /// use opentelemetry::{Context, Value}; + /// use opentelemetry_sdk::{ + /// error::OTelSdkResult, + /// trace::{Span, SpanData, SpanProcessor}, + /// Resource, + /// }; + /// use std::time::Duration; + /// + /// #[derive(Debug)] + /// struct FilteringSpanProcessor