From 022b5d5c34f2cbaa10903ca79727ee6012712a83 Mon Sep 17 00:00:00 2001 From: querel Date: Fri, 9 Feb 2024 16:21:59 -0800 Subject: [PATCH] chore: migrate f5/otel-weaver repo to open-telemetry/weaver repo --- .clippy.toml | 6 + .github/ISSUE_TEMPLATE/bug_report.md | 24 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .github/dependabot.yml | 6 + .github/workflows/audit.yml | 51 + .github/workflows/ci.yml | 150 + .github/workflows/rust-next.yml | 59 + .github/workflows/spelling.yml | 21 + .gitignore | 24 + .typos.toml | 5 + CHANGELOG.md | 6 + CODEOWNERS | 17 + CONTRIBUTING.md | 14 + Cargo.toml | 69 + README.md | 179 + SECURITY.md | 13 + crates/README.md | 3 + crates/weaver_cache/Cargo.toml | 22 + crates/weaver_cache/src/lib.rs | 199 + crates/weaver_logger/Cargo.toml | 12 + crates/weaver_logger/src/lib.rs | 242 ++ crates/weaver_resolved_schema/Cargo.toml | 18 + .../weaver_resolved_schema/src/attribute.rs | 242 ++ crates/weaver_resolved_schema/src/catalog.rs | 33 + .../src/instrumentation_library.rs | 35 + crates/weaver_resolved_schema/src/lib.rs | 64 + crates/weaver_resolved_schema/src/lineage.rs | 142 + crates/weaver_resolved_schema/src/metric.rs | 42 + crates/weaver_resolved_schema/src/registry.rs | 160 + crates/weaver_resolved_schema/src/resource.rs | 14 + crates/weaver_resolved_schema/src/signal.rs | 146 + crates/weaver_resolved_schema/src/tags.rs | 24 + crates/weaver_resolved_schema/src/value.rs | 36 + crates/weaver_resolver/Cargo.toml | 29 + .../registry-test-1-single-attr-ref/README.md | 1 + .../expected-attribute-catalog.json | 39 + .../expected-registry.json | 64 + .../registry/metrics-messaging.yaml | 8 + .../registry/registry-messaging.yaml | 13 + .../registry-test-2-multi-attr-refs/README.md | 1 + .../expected-attribute-catalog.json | 625 +++ .../expected-registry.json | 124 + .../registry/metrics-messaging.yaml | 11 + .../registry/registry-messaging.yaml | 245 ++ .../data/registry-test-3-extends/README.md | 1 + .../expected-attribute-catalog.json | 2069 ++++++++++ .../expected-registry.json | 1330 ++++++ .../registry/http-common.yaml | 87 + .../registry/messaging-common.yaml | 23 + .../registry/metrics-messaging.yaml | 62 + .../registry/registry-error.yaml | 35 + .../registry/registry-http.yaml | 135 + .../registry/registry-messaging.yaml | 245 ++ .../registry/registry-network.yaml | 194 + .../registry/registry-server.yaml | 28 + .../registry/registry-url.yaml | 41 + .../data/registry-test-4-events/README.md | 1 + .../expected-attribute-catalog.json | 145 + .../expected-registry.json | 173 + .../registry/log-feature_flag.yaml | 11 + .../registry/mobile-events.yaml | 72 + .../registry/trace-feature-flag.yaml | 34 + .../data/registry-test-5-metrics/README.md | 1 + .../expected-attribute-catalog.json | 140 + .../expected-registry.json | 490 +++ .../registry/faas-common.yaml | 77 + .../registry/faas-metrics.yaml | 81 + .../data/registry-test-6-resources/README.md | 1 + .../expected-attribute-catalog.json | 107 + .../expected-registry.json | 61 + .../registry/registry-user-agent.yaml | 13 + .../registry/resource-browser.yaml | 56 + .../data/registry-test-7-spans/README.md | 1 + .../expected-attribute-catalog.json | 3609 +++++++++++++++++ .../expected-registry.json | 2508 ++++++++++++ .../registry/registry-db.yaml | 432 ++ .../registry/registry-http.yaml | 135 + .../registry/registry-network.yaml | 194 + .../registry/registry-server.yaml | 28 + .../registry/registry-url.yaml | 41 + .../registry/registry-user-agent.yaml | 13 + .../registry/trace-database.yaml | 263 ++ crates/weaver_resolver/src/attribute.rs | 524 +++ crates/weaver_resolver/src/constraint.rs | 22 + crates/weaver_resolver/src/events.rs | 32 + crates/weaver_resolver/src/lib.rs | 626 +++ crates/weaver_resolver/src/metrics.rs | 188 + crates/weaver_resolver/src/registry.rs | 431 ++ crates/weaver_resolver/src/resource.rs | 26 + crates/weaver_resolver/src/spans.rs | 58 + crates/weaver_resolver/src/stability.rs | 15 + crates/weaver_resolver/src/tags.rs | 14 + crates/weaver_schema/Cargo.toml | 19 + .../data/root-schema-1.21.0.yaml | 138 + crates/weaver_schema/src/attribute.rs | 412 ++ crates/weaver_schema/src/event.rs | 36 + .../src/instrumentation_library.rs | 22 + crates/weaver_schema/src/lib.rs | 360 ++ crates/weaver_schema/src/log.rs | 54 + crates/weaver_schema/src/metric_group.rs | 83 + crates/weaver_schema/src/resource.rs | 34 + crates/weaver_schema/src/resource_events.rs | 44 + crates/weaver_schema/src/resource_metrics.rs | 69 + crates/weaver_schema/src/resource_spans.rs | 44 + crates/weaver_schema/src/schema_spec.rs | 138 + crates/weaver_schema/src/span.rs | 49 + crates/weaver_schema/src/span_event.rs | 28 + crates/weaver_schema/src/span_link.rs | 28 + crates/weaver_schema/src/tags.rs | 67 + crates/weaver_schema/src/univariate_metric.rs | 89 + crates/weaver_semconv/Cargo.toml | 17 + crates/weaver_semconv/README.md | 11 + crates/weaver_semconv/data/client.yaml | 46 + crates/weaver_semconv/data/cloud.yaml | 179 + crates/weaver_semconv/data/cloudevents.yaml | 36 + .../weaver_semconv/data/database-metrics.yaml | 107 + crates/weaver_semconv/data/database.yaml | 588 +++ crates/weaver_semconv/data/exception.yaml | 33 + crates/weaver_semconv/data/faas-common.yaml | 77 + crates/weaver_semconv/data/faas-metrics.yaml | 81 + crates/weaver_semconv/data/faas.yaml | 144 + crates/weaver_semconv/data/http-common.yaml | 140 + crates/weaver_semconv/data/http-metrics.yaml | 119 + crates/weaver_semconv/data/http.yaml | 153 + crates/weaver_semconv/data/jvm-metrics.yaml | 141 + crates/weaver_semconv/data/media.yaml | 49 + crates/weaver_semconv/data/messaging.yaml | 319 ++ crates/weaver_semconv/data/network.yaml | 252 ++ crates/weaver_semconv/data/rpc-metrics.yaml | 115 + crates/weaver_semconv/data/rpc.yaml | 263 ++ crates/weaver_semconv/data/server.yaml | 53 + crates/weaver_semconv/data/source.yaml | 24 + crates/weaver_semconv/data/tls.yaml | 165 + .../weaver_semconv/data/trace-exception.yaml | 38 + crates/weaver_semconv/data/url.yaml | 39 + crates/weaver_semconv/data/user-agent.yaml | 10 + .../data/vm-metrics-experimental.yaml | 70 + crates/weaver_semconv/src/attribute.rs | 437 ++ crates/weaver_semconv/src/group.rs | 277 ++ crates/weaver_semconv/src/lib.rs | 854 ++++ crates/weaver_semconv/src/metric.rs | 48 + crates/weaver_semconv/src/stability.rs | 29 + crates/weaver_template/Cargo.toml | 25 + crates/weaver_template/src/config.rs | 137 + crates/weaver_template/src/filters.rs | 347 ++ crates/weaver_template/src/functions.rs | 35 + crates/weaver_template/src/lib.rs | 86 + crates/weaver_template/src/sdkgen.rs | 536 +++ crates/weaver_template/src/testers.rs | 27 + crates/weaver_version/Cargo.toml | 15 + crates/weaver_version/data/app_versions.yaml | 35 + .../weaver_version/data/parent_versions.yaml | 147 + crates/weaver_version/src/lib.rs | 766 ++++ crates/weaver_version/src/logs_change.rs | 22 + crates/weaver_version/src/logs_version.rs | 14 + crates/weaver_version/src/metrics_change.rs | 30 + crates/weaver_version/src/metrics_version.rs | 14 + crates/weaver_version/src/resource_change.rs | 22 + crates/weaver_version/src/resource_version.rs | 14 + crates/weaver_version/src/spans_change.rs | 22 + crates/weaver_version/src/spans_version.rs | 14 + data/app-telemetry-schema-1.yaml | 73 + data/app-telemetry-schema-2.yaml | 161 + data/app-telemetry-schema-events.yaml | 57 + data/app-telemetry-schema-metrics.yaml | 49 + data/app-telemetry-schema-simple.yaml | 35 + data/app-telemetry-schema-spans.yaml | 41 + data/app-telemetry-schema-traces.yaml | 65 + data/app-telemetry-schema.yaml | 90 + data/open-telemetry-schema.1.22.0.yaml | 353 ++ data/resolved-schema-events.yaml | 573 +++ data/resolved-schema-metrics.yaml | 587 +++ data/resolved-schema-spans.yaml | 490 +++ data/resolved_schema.yaml | 561 +++ demo/alternative-resolved-schema.yaml | 657 +++ demo/app-telemetry-schema.yaml | 149 + demo/resolved-schema.yaml | 980 +++++ demo/root-telemetry-schema.1.22.0.yaml | 354 ++ deny.toml | 160 + docs/component-telemetry-schema-proposal.yaml | 65 + docs/component-telemetry-schema.md | 139 + docs/contribution.md | 10 + docs/dependencies.md | 8 + .../0240-otel-weaver-component-schema.svg | 1 + .../0240-otel-weaver-resolved-schema.svg | 1 + docs/images/0240-otel-weaver-use-cases.svg | 1 + docs/images/dependencies.svg | 186 + docs/images/otel-schema-v1.2.0.png | Bin 0 -> 172747 bytes docs/images/otel-weaver-platform.png | Bin 0 -> 313512 bytes docs/resolved-telemetry-schema-proposal.yaml | 136 + docs/resolved-telemetry-schema.md | 189 + docs/telemetry-schema-v1.2.0.md | 179 + docs/template.md | 51 + examples/ex1.rs | 117 + justfile | 20 + rust-toolchain.toml | 3 + schemas/telemetry-schema-1.yml | 229 ++ src/cli.rs | 35 + src/gen_client.rs | 57 + src/languages.rs | 40 + src/main.rs | 34 + src/resolve.rs | 165 + src/search/mod.rs | 765 ++++ src/search/schema/attribute.rs | 183 + src/search/schema/attributes.rs | 55 + src/search/schema/event.rs | 79 + src/search/schema/metric.rs | 133 + src/search/schema/metric_group.rs | 110 + src/search/schema/mod.rs | 11 + src/search/schema/resource.rs | 33 + src/search/schema/span.rs | 126 + src/search/schema/tags.rs | 23 + src/search/semconv/attribute.rs | 118 + src/search/semconv/attributes.rs | 40 + src/search/semconv/examples.rs | 41 + src/search/semconv/metric.rs | 67 + src/search/semconv/mod.rs | 8 + src/search/theme.rs | 13 + templates/go/config.yaml | 15 + templates/go/optional_attrs.macro.tera | 52 + templates/go/otel/attribute/attrs.go.tera | 15 + templates/go/otel/client.go.tera | 181 + templates/go/otel/eventer/event.tera | 57 + templates/go/otel/meter/metric.tera | 462 +++ templates/go/otel/meter/metric_group.tera | 20 + templates/go/otel/tracer/span.tera | 160 + templates/go/required_attrs.macro.tera | 54 + templates/rust/config.yaml | 15 + templates/rust/eventer/mod.rs.tera | 19 + templates/rust/meter/mod.rs.tera | 94 + templates/rust/mod.rs.tera | 7 + templates/rust/span.tera.bak | 142 + templates/rust/tracer/mod.rs.tera | 142 + 233 files changed, 37408 insertions(+) create mode 100644 .clippy.toml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/audit.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/rust-next.yml create mode 100644 .github/workflows/spelling.yml create mode 100644 .gitignore create mode 100644 .typos.toml create mode 100644 CHANGELOG.md create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 crates/README.md create mode 100644 crates/weaver_cache/Cargo.toml create mode 100644 crates/weaver_cache/src/lib.rs create mode 100644 crates/weaver_logger/Cargo.toml create mode 100644 crates/weaver_logger/src/lib.rs create mode 100644 crates/weaver_resolved_schema/Cargo.toml create mode 100644 crates/weaver_resolved_schema/src/attribute.rs create mode 100644 crates/weaver_resolved_schema/src/catalog.rs create mode 100644 crates/weaver_resolved_schema/src/instrumentation_library.rs create mode 100644 crates/weaver_resolved_schema/src/lib.rs create mode 100644 crates/weaver_resolved_schema/src/lineage.rs create mode 100644 crates/weaver_resolved_schema/src/metric.rs create mode 100644 crates/weaver_resolved_schema/src/registry.rs create mode 100644 crates/weaver_resolved_schema/src/resource.rs create mode 100644 crates/weaver_resolved_schema/src/signal.rs create mode 100644 crates/weaver_resolved_schema/src/tags.rs create mode 100644 crates/weaver_resolved_schema/src/value.rs create mode 100644 crates/weaver_resolver/Cargo.toml create mode 100644 crates/weaver_resolver/data/registry-test-1-single-attr-ref/README.md create mode 100644 crates/weaver_resolver/data/registry-test-1-single-attr-ref/expected-attribute-catalog.json create mode 100644 crates/weaver_resolver/data/registry-test-1-single-attr-ref/expected-registry.json create mode 100644 crates/weaver_resolver/data/registry-test-1-single-attr-ref/registry/metrics-messaging.yaml create mode 100644 crates/weaver_resolver/data/registry-test-1-single-attr-ref/registry/registry-messaging.yaml create mode 100644 crates/weaver_resolver/data/registry-test-2-multi-attr-refs/README.md create mode 100644 crates/weaver_resolver/data/registry-test-2-multi-attr-refs/expected-attribute-catalog.json create mode 100644 crates/weaver_resolver/data/registry-test-2-multi-attr-refs/expected-registry.json create mode 100644 crates/weaver_resolver/data/registry-test-2-multi-attr-refs/registry/metrics-messaging.yaml create mode 100644 crates/weaver_resolver/data/registry-test-2-multi-attr-refs/registry/registry-messaging.yaml create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/README.md create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/expected-attribute-catalog.json create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/expected-registry.json create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/registry/http-common.yaml create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/registry/messaging-common.yaml create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/registry/metrics-messaging.yaml create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/registry/registry-error.yaml create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/registry/registry-http.yaml create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/registry/registry-messaging.yaml create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/registry/registry-network.yaml create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/registry/registry-server.yaml create mode 100644 crates/weaver_resolver/data/registry-test-3-extends/registry/registry-url.yaml create mode 100644 crates/weaver_resolver/data/registry-test-4-events/README.md create mode 100644 crates/weaver_resolver/data/registry-test-4-events/expected-attribute-catalog.json create mode 100644 crates/weaver_resolver/data/registry-test-4-events/expected-registry.json create mode 100644 crates/weaver_resolver/data/registry-test-4-events/registry/log-feature_flag.yaml create mode 100644 crates/weaver_resolver/data/registry-test-4-events/registry/mobile-events.yaml create mode 100644 crates/weaver_resolver/data/registry-test-4-events/registry/trace-feature-flag.yaml create mode 100644 crates/weaver_resolver/data/registry-test-5-metrics/README.md create mode 100644 crates/weaver_resolver/data/registry-test-5-metrics/expected-attribute-catalog.json create mode 100644 crates/weaver_resolver/data/registry-test-5-metrics/expected-registry.json create mode 100644 crates/weaver_resolver/data/registry-test-5-metrics/registry/faas-common.yaml create mode 100644 crates/weaver_resolver/data/registry-test-5-metrics/registry/faas-metrics.yaml create mode 100644 crates/weaver_resolver/data/registry-test-6-resources/README.md create mode 100644 crates/weaver_resolver/data/registry-test-6-resources/expected-attribute-catalog.json create mode 100644 crates/weaver_resolver/data/registry-test-6-resources/expected-registry.json create mode 100644 crates/weaver_resolver/data/registry-test-6-resources/registry/registry-user-agent.yaml create mode 100644 crates/weaver_resolver/data/registry-test-6-resources/registry/resource-browser.yaml create mode 100644 crates/weaver_resolver/data/registry-test-7-spans/README.md create mode 100644 crates/weaver_resolver/data/registry-test-7-spans/expected-attribute-catalog.json create mode 100644 crates/weaver_resolver/data/registry-test-7-spans/expected-registry.json create mode 100644 crates/weaver_resolver/data/registry-test-7-spans/registry/registry-db.yaml create mode 100644 crates/weaver_resolver/data/registry-test-7-spans/registry/registry-http.yaml create mode 100644 crates/weaver_resolver/data/registry-test-7-spans/registry/registry-network.yaml create mode 100644 crates/weaver_resolver/data/registry-test-7-spans/registry/registry-server.yaml create mode 100644 crates/weaver_resolver/data/registry-test-7-spans/registry/registry-url.yaml create mode 100644 crates/weaver_resolver/data/registry-test-7-spans/registry/registry-user-agent.yaml create mode 100644 crates/weaver_resolver/data/registry-test-7-spans/registry/trace-database.yaml create mode 100644 crates/weaver_resolver/src/attribute.rs create mode 100644 crates/weaver_resolver/src/constraint.rs create mode 100644 crates/weaver_resolver/src/events.rs create mode 100644 crates/weaver_resolver/src/lib.rs create mode 100644 crates/weaver_resolver/src/metrics.rs create mode 100644 crates/weaver_resolver/src/registry.rs create mode 100644 crates/weaver_resolver/src/resource.rs create mode 100644 crates/weaver_resolver/src/spans.rs create mode 100644 crates/weaver_resolver/src/stability.rs create mode 100644 crates/weaver_resolver/src/tags.rs create mode 100644 crates/weaver_schema/Cargo.toml create mode 100644 crates/weaver_schema/data/root-schema-1.21.0.yaml create mode 100644 crates/weaver_schema/src/attribute.rs create mode 100644 crates/weaver_schema/src/event.rs create mode 100644 crates/weaver_schema/src/instrumentation_library.rs create mode 100644 crates/weaver_schema/src/lib.rs create mode 100644 crates/weaver_schema/src/log.rs create mode 100644 crates/weaver_schema/src/metric_group.rs create mode 100644 crates/weaver_schema/src/resource.rs create mode 100644 crates/weaver_schema/src/resource_events.rs create mode 100644 crates/weaver_schema/src/resource_metrics.rs create mode 100644 crates/weaver_schema/src/resource_spans.rs create mode 100644 crates/weaver_schema/src/schema_spec.rs create mode 100644 crates/weaver_schema/src/span.rs create mode 100644 crates/weaver_schema/src/span_event.rs create mode 100644 crates/weaver_schema/src/span_link.rs create mode 100644 crates/weaver_schema/src/tags.rs create mode 100644 crates/weaver_schema/src/univariate_metric.rs create mode 100644 crates/weaver_semconv/Cargo.toml create mode 100644 crates/weaver_semconv/README.md create mode 100644 crates/weaver_semconv/data/client.yaml create mode 100644 crates/weaver_semconv/data/cloud.yaml create mode 100644 crates/weaver_semconv/data/cloudevents.yaml create mode 100644 crates/weaver_semconv/data/database-metrics.yaml create mode 100644 crates/weaver_semconv/data/database.yaml create mode 100644 crates/weaver_semconv/data/exception.yaml create mode 100644 crates/weaver_semconv/data/faas-common.yaml create mode 100644 crates/weaver_semconv/data/faas-metrics.yaml create mode 100644 crates/weaver_semconv/data/faas.yaml create mode 100644 crates/weaver_semconv/data/http-common.yaml create mode 100644 crates/weaver_semconv/data/http-metrics.yaml create mode 100644 crates/weaver_semconv/data/http.yaml create mode 100644 crates/weaver_semconv/data/jvm-metrics.yaml create mode 100644 crates/weaver_semconv/data/media.yaml create mode 100644 crates/weaver_semconv/data/messaging.yaml create mode 100644 crates/weaver_semconv/data/network.yaml create mode 100644 crates/weaver_semconv/data/rpc-metrics.yaml create mode 100644 crates/weaver_semconv/data/rpc.yaml create mode 100644 crates/weaver_semconv/data/server.yaml create mode 100644 crates/weaver_semconv/data/source.yaml create mode 100644 crates/weaver_semconv/data/tls.yaml create mode 100644 crates/weaver_semconv/data/trace-exception.yaml create mode 100644 crates/weaver_semconv/data/url.yaml create mode 100644 crates/weaver_semconv/data/user-agent.yaml create mode 100644 crates/weaver_semconv/data/vm-metrics-experimental.yaml create mode 100644 crates/weaver_semconv/src/attribute.rs create mode 100644 crates/weaver_semconv/src/group.rs create mode 100644 crates/weaver_semconv/src/lib.rs create mode 100644 crates/weaver_semconv/src/metric.rs create mode 100644 crates/weaver_semconv/src/stability.rs create mode 100644 crates/weaver_template/Cargo.toml create mode 100644 crates/weaver_template/src/config.rs create mode 100644 crates/weaver_template/src/filters.rs create mode 100644 crates/weaver_template/src/functions.rs create mode 100644 crates/weaver_template/src/lib.rs create mode 100644 crates/weaver_template/src/sdkgen.rs create mode 100644 crates/weaver_template/src/testers.rs create mode 100644 crates/weaver_version/Cargo.toml create mode 100644 crates/weaver_version/data/app_versions.yaml create mode 100644 crates/weaver_version/data/parent_versions.yaml create mode 100644 crates/weaver_version/src/lib.rs create mode 100644 crates/weaver_version/src/logs_change.rs create mode 100644 crates/weaver_version/src/logs_version.rs create mode 100644 crates/weaver_version/src/metrics_change.rs create mode 100644 crates/weaver_version/src/metrics_version.rs create mode 100644 crates/weaver_version/src/resource_change.rs create mode 100644 crates/weaver_version/src/resource_version.rs create mode 100644 crates/weaver_version/src/spans_change.rs create mode 100644 crates/weaver_version/src/spans_version.rs create mode 100644 data/app-telemetry-schema-1.yaml create mode 100644 data/app-telemetry-schema-2.yaml create mode 100644 data/app-telemetry-schema-events.yaml create mode 100644 data/app-telemetry-schema-metrics.yaml create mode 100644 data/app-telemetry-schema-simple.yaml create mode 100644 data/app-telemetry-schema-spans.yaml create mode 100644 data/app-telemetry-schema-traces.yaml create mode 100644 data/app-telemetry-schema.yaml create mode 100644 data/open-telemetry-schema.1.22.0.yaml create mode 100644 data/resolved-schema-events.yaml create mode 100644 data/resolved-schema-metrics.yaml create mode 100644 data/resolved-schema-spans.yaml create mode 100644 data/resolved_schema.yaml create mode 100644 demo/alternative-resolved-schema.yaml create mode 100644 demo/app-telemetry-schema.yaml create mode 100644 demo/resolved-schema.yaml create mode 100644 demo/root-telemetry-schema.1.22.0.yaml create mode 100644 deny.toml create mode 100644 docs/component-telemetry-schema-proposal.yaml create mode 100644 docs/component-telemetry-schema.md create mode 100644 docs/contribution.md create mode 100644 docs/dependencies.md create mode 100644 docs/images/0240-otel-weaver-component-schema.svg create mode 100644 docs/images/0240-otel-weaver-resolved-schema.svg create mode 100644 docs/images/0240-otel-weaver-use-cases.svg create mode 100644 docs/images/dependencies.svg create mode 100644 docs/images/otel-schema-v1.2.0.png create mode 100644 docs/images/otel-weaver-platform.png create mode 100644 docs/resolved-telemetry-schema-proposal.yaml create mode 100644 docs/resolved-telemetry-schema.md create mode 100644 docs/telemetry-schema-v1.2.0.md create mode 100644 docs/template.md create mode 100644 examples/ex1.rs create mode 100644 justfile create mode 100644 rust-toolchain.toml create mode 100644 schemas/telemetry-schema-1.yml create mode 100644 src/cli.rs create mode 100644 src/gen_client.rs create mode 100644 src/languages.rs create mode 100644 src/main.rs create mode 100644 src/resolve.rs create mode 100644 src/search/mod.rs create mode 100644 src/search/schema/attribute.rs create mode 100644 src/search/schema/attributes.rs create mode 100644 src/search/schema/event.rs create mode 100644 src/search/schema/metric.rs create mode 100644 src/search/schema/metric_group.rs create mode 100644 src/search/schema/mod.rs create mode 100644 src/search/schema/resource.rs create mode 100644 src/search/schema/span.rs create mode 100644 src/search/schema/tags.rs create mode 100644 src/search/semconv/attribute.rs create mode 100644 src/search/semconv/attributes.rs create mode 100644 src/search/semconv/examples.rs create mode 100644 src/search/semconv/metric.rs create mode 100644 src/search/semconv/mod.rs create mode 100644 src/search/theme.rs create mode 100644 templates/go/config.yaml create mode 100644 templates/go/optional_attrs.macro.tera create mode 100644 templates/go/otel/attribute/attrs.go.tera create mode 100644 templates/go/otel/client.go.tera create mode 100644 templates/go/otel/eventer/event.tera create mode 100644 templates/go/otel/meter/metric.tera create mode 100644 templates/go/otel/meter/metric_group.tera create mode 100644 templates/go/otel/tracer/span.tera create mode 100644 templates/go/required_attrs.macro.tera create mode 100644 templates/rust/config.yaml create mode 100644 templates/rust/eventer/mod.rs.tera create mode 100644 templates/rust/meter/mod.rs.tera create mode 100644 templates/rust/mod.rs.tera create mode 100644 templates/rust/span.tera.bak create mode 100644 templates/rust/tracer/mod.rs.tera diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 00000000..a5d88057 --- /dev/null +++ b/.clippy.toml @@ -0,0 +1,6 @@ +msrv = "1.70.0" # MSRV +warn-on-all-wildcard-imports = true +allow-expect-in-tests = true +allow-unwrap-in-tests = true +allow-dbg-in-tests = true +disallowed-methods = [] \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..74514987 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,24 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..08aa9049 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 00000000..e9a23284 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,51 @@ +name: Security Audit + +permissions: + contents: read + +on: + pull_request: + paths: + - '**/Cargo.toml' + - '**/Cargo.lock' + push: + branches: + - main + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CLICOLOR: 1 + +jobs: + # security_audit: + # permissions: + # issues: write # to create issues (actions-rs/audit-check) + # checks: write # to create check (actions-rs/audit-check) + # runs-on: ubuntu-latest + # Prevent sudden announcement of a new advisory from failing ci: + # continue-on-error: true + # steps: + # - name: Checkout repository + # uses: actions/checkout@v4 + # - uses: actions-rs/audit-check@v1 + # with: + # token: ${{ secrets.GITHUB_TOKEN }} + + cargo_deny: + permissions: + issues: write # to create issues (actions-rs/audit-check) + checks: write # to create check (actions-rs/audit-check) + runs-on: ubuntu-latest + strategy: + matrix: + checks: + - bans licenses sources + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - uses: EmbarkStudios/cargo-deny-action@v1 + with: + command: check ${{ matrix.checks }} + rust-version: stable diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..7947e33a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,150 @@ +name: CI + +permissions: + contents: read + +on: + pull_request: + push: + branches: + - main + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CLICOLOR: 1 + +jobs: + ci: + permissions: + contents: none + name: CI + needs: [ test, msrv, docs, rustfmt, clippy ] + runs-on: ubuntu-latest + steps: + - name: Done + run: exit 0 + test: + name: Test + strategy: + matrix: + os: [ "ubuntu-latest", "windows-latest", "macos-latest" ] + rust: [ "stable" ] + continue-on-error: ${{ matrix.rust != 'stable' }} + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + - name: Install protoc + uses: arduino/setup-protoc@v2 + - uses: Swatinem/rust-cache@v2 + - name: Build + run: cargo test --no-run --workspace --all-features + - name: Default features + run: cargo test --workspace + - name: All features + run: cargo test --workspace --all-features + - name: No-default features + run: cargo test --workspace --no-default-features + msrv: + name: "Check MSRV: 1.70.0" + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: "1.70" # MSRV + - name: Install protoc + uses: arduino/setup-protoc@v2 + - uses: Swatinem/rust-cache@v2 + - name: Default features + run: cargo check --workspace --all-targets + - name: All features + run: cargo check --workspace --all-targets --all-features + - name: No-default features + run: cargo check --workspace --all-targets --no-default-features + lockfile: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - uses: Swatinem/rust-cache@v2 + - name: "Is lockfile updated?" + run: cargo fetch --locked + docs: + name: Docs + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - name: Install protoc + uses: arduino/setup-protoc@v2 + - uses: Swatinem/rust-cache@v2 + - name: Check documentation + env: + RUSTDOCFLAGS: -D warnings + run: cargo doc --workspace --all-features --no-deps --document-private-items + rustfmt: + name: rustfmt + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + # Not MSRV because its harder to jump between versions and people are + # more likely to have stable + toolchain: stable + components: rustfmt + - uses: Swatinem/rust-cache@v2 + - name: Check formatting + run: cargo fmt --all -- --check + clippy: + name: clippy + runs-on: ubuntu-latest + permissions: + security-events: write # to upload sarif results + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: "nightly" + components: clippy + - uses: Swatinem/rust-cache@v2 + - name: Install SARIF tools + run: cargo install clippy-sarif --locked + - name: Install SARIF tools + run: cargo install sarif-fmt --locked + - name: Install protoc + uses: arduino/setup-protoc@v2 + - name: Check + run: > + cargo clippy --workspace --all-features --all-targets --message-format=json -- -D warnings --allow deprecated + | clippy-sarif + | tee clippy-results.sarif + | sarif-fmt + continue-on-error: true + - name: Upload + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: clippy-results.sarif + wait-for-processing: true + - name: Report status + run: cargo clippy --workspace --all-features --all-targets -- -D warnings --allow deprecated diff --git a/.github/workflows/rust-next.yml b/.github/workflows/rust-next.yml new file mode 100644 index 00000000..4af688eb --- /dev/null +++ b/.github/workflows/rust-next.yml @@ -0,0 +1,59 @@ +name: Rust Next + +permissions: + contents: read + +on: + schedule: + - cron: '3 3 3 * *' + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CLICOLOR: 1 + +jobs: + test: + name: Test + strategy: + matrix: + os: ["ubuntu-latest", "windows-latest", "macos-latest"] + rust: ["stable", "beta"] + include: + - os: ubuntu-latest + rust: "nightly" + continue-on-error: ${{ matrix.rust != 'stable' }} + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + - uses: Swatinem/rust-cache@v2 + - name: Default features + run: cargo test --workspace + - name: All features + run: cargo test --workspace --all-features + - name: No-default features + run: cargo test --workspace --no-default-features + latest: + name: "Check latest dependencies" + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + - uses: Swatinem/rust-cache@v2 + - name: Update dependencies + run: cargo update + - name: Default features + run: cargo test --workspace --all-targets + - name: All features + run: cargo test --workspace --all-targets --all-features + - name: No-default features + run: cargo test --workspace --all-targets --no-default-features diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml new file mode 100644 index 00000000..6de947bb --- /dev/null +++ b/.github/workflows/spelling.yml @@ -0,0 +1,21 @@ +name: Spelling + +permissions: + contents: read + +on: [push, pull_request] + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CLICOLOR: 1 + +jobs: + spelling: + name: Spell Check with Typos + runs-on: ubuntu-latest + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v4 + - name: Spell Check Repo + uses: crate-ci/typos@v1.16.21 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c623a0ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Rust specific +/target +**/*.rs.bk +Cargo.lock +bench-log + +# Just +just.zsh + +# IntelliJ IDEs +/.idea +*.iml + +# VS Code +.vscode/ +.devcontainer/ + +# Emacs +*~ +\#*\# + +# Miscellaneous files +*.sw[op] +*.DS_Store diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 00000000..d8dedec8 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,5 @@ +[files] +extend-exclude = ["*.cast", "**/*.yaml", "**/*.svg"] + +[default.extend-words] +ratatui = "ratatui" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..1d013ff9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..1e056db7 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,17 @@ +##################################################### +# +# List of approvers for this repository +# +##################################################### +# +# Learn about membership in OpenTelemetry community: +# https://github.com/open-telemetry/community/blob/main/community-membership.md +# +# +# Learn about CODEOWNERS file format: +# https://help.github.com/en/articles/about-code-owners +# + +* @lquerel @jsuereth + +CODEOWNERS @lquerel @jsuereth \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4859273c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,14 @@ +# Contributing to the OpenTelemetry Weaver project + +We want to make contributing to this project as easy and transparent +as possible. Please see the OpenTelemetry [CONTRIBUTING.md][] +guidelines for project-wide information, including code of conduct, +and contributor license agreements, copyright notices, and how to +engage with the OpenTelemetry community. + +## Our Development Process + +### Repository background + +The OpenTelemetry Weaver was initially developed in the +`github.com/f5/otel-weaver` repository. \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..375bd276 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "weaver" +version = "0.1.0" +authors = ["Laurent Querel "] +edition = "2021" +repository = "https://github.com/f5/otel-weaver" +description = "OTel Weaver - A Schema-Driven Client SDK Generator for OpenTelemetry" +keywords = ["opentelemetry", "client", "schema", "arrow", "generator"] +categories = ["command-line-utilities"] +license = "Apache-2.0" +readme = "README.md" +publish = false + +# Workspace definition ======================================================== +[workspace] +members = [ + "crates/*", +] + +[workspace.package] +authors = ["Laurent Querel "] +edition = "2021" +repository = "https://github.com/f5/otel-weaver" +license = "Apache-2.0" +publish = false + +[workspace.dependencies] +serde = { version = "1.0.195", features = ["derive"] } +serde_yaml = "0.9.30" +serde_json = "1.0.111" +thiserror = "1.0.56" +ureq = "2.9.1" +regex = "1.10.3" +rayon = "1.8.1" +ordered-float = { version = "4.2.0", features = ["serde"] } + +# Crate definitions =========================================================== +[[bin]] +bench = false +path = "src/main.rs" +name = "weaver" + +[dependencies] +# local crates dependencies +weaver_logger = { path = "crates/weaver_logger" } +weaver_resolver = { path = "crates/weaver_resolver" } +weaver_template = { path = "crates/weaver_template" } +weaver_semconv = { path = "crates/weaver_semconv" } +weaver_schema = { path = "crates/weaver_schema" } +weaver_cache = { path = "crates/weaver_cache" } + +clap = { version = "4.4.18", features = ["derive"] } +crossterm = "0.27.0" +ratatui = "0.26.0" +tui-textarea = "0.4.0" +tantivy = "0.21.1" + +# workspace dependencies +serde.workspace = true +serde_yaml.workspace = true + +[package.metadata.cargo-machete] +# force cargo machete to ignore the following crates +ignored = ["serde"] + +[profile.release] +lto = true +strip = true +panic = "abort" diff --git a/README.md b/README.md new file mode 100644 index 00000000..41733f63 --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +# OTel Weaver (status: Proof of Concept) + +## Overview + +> At this stage, the project is being used as a **Proof of Concept** to explore and +> refine the 'Application Telemetry Schema: Vision and Roadmap' [OTEP](https://github.com/open-telemetry/oteps/blob/main/text/0243-app-telemetry-schema-vision-roadmap.md), +> which has been approved and merged. +> +> This project is a **work in progress and is not ready for production use**. + +OTel Weaver is a CLI tool that enables users to: + +- Search for and retrieve information from a semantic convention registry or a telemetry schema. +- Resolve a semantic convention registry or a telemetry schema. +- Generate a client SDK/API from a telemetry schema. + +## Install + +Currently, there is no binary distribution available. To install the tool, you +must build it from source. To do so, you need to have Rust installed on your +system (see [Install Rust](https://www.rust-lang.org/tools/install)). + +To build the tool: +- In debug mode, run the following command: + ``` + cargo build + ``` +- In release mode, run the following command: + ``` + cargo build --release + ``` + +The generated `weaver` binary will be located in the `target/debug` directory +for debug mode or the `target/release` directory for release mode. + +## Usage + +``` +Usage: weaver [OPTIONS] [COMMAND] + +Commands: + resolve Resolve a semantic convention registry or a telemetry schema + gen-client Generate a client SDK or client API + languages List all supported languages + search Search in a semantic convention registry or a telemetry schema + help Print this message or the help of the given subcommand(s) + +Options: + -d, --debug... Turn debugging information on + -h, --help Print help + -V, --version Print version +``` + +### Command `search` + +This command provides an interactive terminal UI, allowing users to search for +attributes and metrics specified within a given semantic convention registry or +a telemetry schema (including dependencies). + +To search into the OpenTelemetry Semantic Convention Registry, run the following +command: + +```bash +weaver search registry https://github.com/open-telemetry/semantic-conventions.git model +``` + +To search into a telemetry schema, run the following command: + +```bash +weaver search schema demo/app-telemetry-schema.yaml +``` + +This search engine leverages [Tantivy](https://github.com/quickwit-oss/tantivy) +and supports a simple [search syntax](https://docs.rs/tantivy/latest/tantivy/query/struct.QueryParser.html) +in the search bar. + +### Command `resolve` + +This command resolves a schema or a semantic convention registry (not yet +implemented) and displays the result on the standard output. +Alternatively, the result can be written to a file if specified using the +`--output` option. This command is primarily used for validating and debugging +telemetry schemas and semantic convention registries. + +```bash +weaver resolve schema telemetry-schema.yaml --output telemetry-schema-resolved.yaml +``` + +A "resolved schema" is one where: +- All references have been resolved and expanded. +- All overrides have been applied. +- This resolved schema is what the code generator and upcoming plugins utilize. + +### Command `gen-client` + +This command generates a client SDK from a telemetry schema for a given language +specified with the `--language` option. + +```bash +weaver gen-client --schema telemetry-schema.yaml --language go +``` + +In the future, users will be able to specify the protocol to use for the generated +client SDK (i.e. OTLP or OTel Arrow Protocol) and few others options. + +### Command `languages` + +This command displays all the languages for which a client SDK/API can +be generated. + +```bash +weaver languages +``` + +### Architecture + +The OTel Weaver tool is architecturally designed as a platform. By default, this +tool incorporates a template engine that facilitates Client SDK/API generation +across various programming languages. In the future, we plan to integrate a +WASM plugin system, allowing the community to enhance the platform. This would +pave the way for features like enterprise data catalog integration, privacy policy enforcement, +documentation generation, dashboard creation, and more. + +Below is a diagram detailing the primary components of the OTel Weaver tool. + +![OTel Weaver Platform](docs/images/otel-weaver-platform.png) + +## ToDo +**Semantic Convention Registry and Application Telemetry Schema** +- [ ] Add support for open enum types (i.e. allow custom values=true). +- [ ] Add support for template types. +- [ ] Add support for `all` in telemetry schema versions section. +- [ ] Add support for `span_events` in telemetry schema versions section. +- [ ] Add support for `apply_to_spans` in telemetry schema versions section. +- [ ] Add support for `apply_to_metrics` in telemetry schema metrics versions section. +- [ ] Add support for `split` in telemetry schema metrics versions section. +- [ ] Add support for group constraints `any_of`, ... +- [ ] Support more than 2 levels of telemetry schema inheritance. +- [ ] Minimize number of declaration duplications in the resolved schema (especially for attributes). + +**Client SDK/API Code Generation** +- Generate Go Client SDK/API on top of the generic Go Client SDK/API. + - [ ] Generate type-safe API for metric groups. + - [ ] Support obfuscation and masking. +- Generate Go Client SDK/API with support for OTel Arrow Protocol. +- Generate Rust Client SDK/API on top of the generic Rust Client SDK/API. +- Generate Rust Client SDK/API with support for OTel Arrow Protocol. + +**Tooling and Plugins** + - [ ] Add support for WASM plugins. + - [ ] Add Tera filter to apply obfuscation, masking, ... based on tags and language configuration. + +## Links + +Internal links: +- [Component Telemetry Schema](docs/component-telemetry-schema.md) (proposal) +- [Resolved Telemetry Schema](docs/resolved-telemetry-schema.md) (proposal) +- [Internal crates interdependencies](docs/dependencies.md) +- [Change log](CHANGELOG.md) + +External links: +- Application Telemetry Schema: Vision and Roadmap - [PR](https://github.com/open-telemetry/oteps/pull/243) +- OpenTelemetry Telemetry Schema v1.2.0 [Draft](https://github.com/lquerel/oteps/blob/app-telemetry-schema-format/text/0241-telemetry-schema-ext.md) (not yet ready). +- [OpenTelemetry Semantic Convention File Format](https://github.com/open-telemetry/build-tools/blob/main/semantic-conventions/syntax.md) +- [OpenTelemetry Schema File Format v1.1.0](https://opentelemetry.io/docs/specs/otel/schemas/file_format_v1.1.0/) +- Presentation slides from the Semantic Convention SIG meeting on October 23, 2023 [here](https://docs.google.com/presentation/d/1nxt5VFlC1mUjZ8eecUYK4e4SxThpIVj1IRnIcodMsNI/edit?usp=sharing). +- Meta/Facebook's [positional paper](https://research.facebook.com/publications/positional-paper-schema-first-application-telemetry/) + presenting a similar approach but based on Thrift+Annotations+Automations. + +## Contributing + +Pull requests are welcome. For major changes, please open an issue +first to discuss what you would like to change. For more information, please +read [CONTRIBUTING](CONTRIBUTING.md). + + +## License + +OTel Weaver is licensed under Apache License Version 2.0. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..482da6c7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +## Security + +OpenTelemetry Weaver takes the security of our project seriously. + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +Please see the [OpenTelemetry org-wide security +policy](https://github.com/open-telemetry/.github/blob/main/SECURITY.md) +for instructions on reporting vulnerabilities, including how to +contact the maintainers technical committee of the project secure +using a secure channel. \ No newline at end of file diff --git a/crates/README.md b/crates/README.md new file mode 100644 index 00000000..379a4d80 --- /dev/null +++ b/crates/README.md @@ -0,0 +1,3 @@ +# Crates + +![Dependencies](/docs/images/dependencies.svg) diff --git a/crates/weaver_cache/Cargo.toml b/crates/weaver_cache/Cargo.toml new file mode 100644 index 00000000..5eabf705 --- /dev/null +++ b/crates/weaver_cache/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "weaver_cache" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true +edition.workspace = true + +[dependencies] +tempdir = "0.3.7" +dirs = "5.0.1" +gix = { version = "0.57.0", default-features = false, features = [ + "comfort", + "blocking-http-transport-reqwest", + "max-performance-safe", + "worktree-mutation", + "blocking-http-transport-reqwest-rust-tls", +] } + +thiserror.workspace = true + diff --git a/crates/weaver_cache/src/lib.rs b/crates/weaver_cache/src/lib.rs new file mode 100644 index 00000000..35ff30a3 --- /dev/null +++ b/crates/weaver_cache/src/lib.rs @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A cache system for OTel Weaver. +//! +//! Semantic conventions, schemas and other assets are cached +//! locally to avoid fetching them from the network every time. + +use std::default::Default; +use std::fs::create_dir_all; +use std::num::NonZeroU32; +use std::path::PathBuf; +use std::sync::atomic::AtomicBool; +use std::sync::Mutex; + +use crate::Error::GitError; +use gix::clone::PrepareFetch; +use gix::create::Kind; +use gix::remote::fetch::Shallow; +use gix::{create, open, progress}; +use tempdir::TempDir; + +/// An error that can occur while creating or using a cache. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Home directory not found. + #[error("Home directory not found")] + HomeDirNotFound, + + /// Cache directory not created. + #[error("Cache directory not created: {message}")] + CacheDirNotCreated { + /// The error message + message: String, + }, + + /// Git repo not created. + #[error("Git repo `{repo_url}` not created: {message}")] + GitRepoNotCreated { + /// The git repo URL + repo_url: String, + /// The error message + message: String, + }, + + /// A git error occurred. + #[error("Git error occurred while cloning `{repo_url}`: {message}")] + GitError { + /// The git repo URL + repo_url: String, + /// The error message + message: String, + }, +} + +/// A cache system for OTel Weaver. +#[derive(Default)] +pub struct Cache { + path: PathBuf, + git_repo_dirs: Mutex>, +} + +/// A git repo that is cloned into a tempdir. +struct GitRepo { + /// Need to allow dead code because we need to keep the tempdir live + /// for the lifetime of the GitRepo. + #[allow(dead_code)] + temp_dir: TempDir, + path: PathBuf, +} + +impl Cache { + /// Creates the `.otel-weaver/cache` directory in the home directory. + /// This directory is used to store the semantic conventions, schemas + /// and other assets that are fetched from the network. + pub fn try_new() -> Result { + let home = dirs::home_dir().ok_or(Error::HomeDirNotFound)?; + let cache_path = home.join(".otel-weaver/cache"); + + create_dir_all(cache_path.as_path()).map_err(|e| Error::CacheDirNotCreated { + message: e.to_string(), + })?; + + Ok(Self { + path: cache_path, + ..Default::default() + }) + } + + /// The given repo_url is cloned into the cache and the path to the repo is returned. + /// The optional path parameter is relative to the root of the repo. + /// The intent is to allow the caller to specify a subdirectory of the repo and + /// use a sparse checkout once `gitoxide` supports it. In the meantime, the + /// path is checked to exist in the repo and an error is returned if it doesn't. + /// If the path exists in the repo, the returned pathbuf is the path to the + /// subdirectory in the git repo directory. + pub fn git_repo(&self, repo_url: String, path: Option) -> Result { + // Checks if a tempdir already exists for this repo + if let Some(git_repo_dir) = self + .git_repo_dirs + .lock() + .expect("git_repo_dirs lock failed") + .get(&repo_url) + { + return Ok(git_repo_dir.path.clone()); + } + + // Otherwise creates a tempdir for the repo and keeps track of it + // in the git_repo_dirs hashmap. + let git_repo_dir = TempDir::new_in(self.path.as_path(), "git-repo").map_err(|e| { + Error::GitRepoNotCreated { + repo_url: repo_url.to_string(), + message: e.to_string(), + } + })?; + let git_repo_pathbuf = git_repo_dir.path().to_path_buf(); + let git_repo_path = git_repo_pathbuf.as_path(); + + // Clones the repo into the tempdir. + // Use shallow clone to save time and space. + let mut fetch = PrepareFetch::new( + repo_url.as_str(), + git_repo_path, + Kind::WithWorktree, + create::Options { + destination_must_be_empty: true, + fs_capabilities: None, + }, + open::Options::isolated(), + ) + .map_err(|e| GitError { + repo_url: repo_url.to_string(), + message: e.to_string(), + })? + .with_shallow(Shallow::DepthAtRemote(NonZeroU32::new(1).unwrap())); + + let (mut prepare, _outcome) = fetch + .fetch_then_checkout(progress::Discard, &AtomicBool::new(false)) + .map_err(|e| GitError { + repo_url: repo_url.to_string(), + message: e.to_string(), + })?; + + let (_repo, _outcome) = prepare + .main_worktree(progress::Discard, &AtomicBool::new(false)) + .map_err(|e| GitError { + repo_url: repo_url.to_string(), + message: e.to_string(), + })?; + + // Determines the path to the repo. + let git_repo_path = if let Some(path) = &path { + // Checks the existence of the path in the repo. + // If the path doesn't exist, returns an error. + if !git_repo_path.join(path).exists() { + return Err(Error::GitError { + repo_url: repo_url.to_string(), + message: format!("Path `{}` not found in repo", path), + }); + } + + git_repo_path.join(path) + } else { + git_repo_path.to_path_buf() + }; + + // Adds the repo to the git_repo_dirs hashmap. + self.git_repo_dirs + .lock() + .expect("git_repo_dirs lock failed") + .insert( + repo_url.to_string(), + GitRepo { + temp_dir: git_repo_dir, + path: git_repo_path, + }, + ); + + Ok(git_repo_pathbuf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Marked as ignore because we don't want to clone the repo every + /// time we run the tests in CI. + #[test] + #[ignore] + fn test_cache() { + let cache = Cache::try_new().unwrap(); + let result = cache.git_repo( + "https://github.com/open-telemetry/semantic-conventions.git".into(), + Some("model".into()), + ); + assert!(result.is_ok()); + assert!(result.unwrap().exists()); + } +} diff --git a/crates/weaver_logger/Cargo.toml b/crates/weaver_logger/Cargo.toml new file mode 100644 index 00000000..190c4345 --- /dev/null +++ b/crates/weaver_logger/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "weaver_logger" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true +edition.workspace = true + +[dependencies] +paris = { version = "1.5.15", features = ["macros"] } + diff --git a/crates/weaver_logger/src/lib.rs b/crates/weaver_logger/src/lib.rs new file mode 100644 index 00000000..11f7e9e2 --- /dev/null +++ b/crates/weaver_logger/src/lib.rs @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A generic logger that can be used to log messages to the console. + +#![deny(missing_docs)] +#![deny(clippy::print_stdout)] +#![deny(clippy::print_stderr)] + +use std::sync::{Arc, Mutex}; + +/// A trait that defines the interface of a logger. +pub trait Logger { + /// Logs an trace message (only with debug enabled). + fn trace(&self, message: &str) -> &Self; + + /// Logs an info message. + fn info(&self, message: &str) -> &Self; + + /// Logs a warning message. + fn warn(&self, message: &str) -> &Self; + + /// Logs an error message. + fn error(&self, message: &str) -> &Self; + + /// Logs a success message. + fn success(&self, message: &str) -> &Self; + + /// Logs a newline. + fn newline(&self, count: usize) -> &Self; + + /// Indents the logger. + fn indent(&self, count: usize) -> &Self; + + /// Stops a loading message. + fn done(&self); + + /// Adds a style to the logger. + fn add_style(&self, name: &str, styles: Vec<&'static str>) -> &Self; + + /// Logs a loading message with a spinner. + fn loading(&self, message: &str) -> &Self; + + /// Forces the logger to not print a newline for the next message. + fn same(&self) -> &Self; + + /// Logs a message without icon. + fn log(&self, message: &str) -> &Self; +} + +/// A generic logger that can be used to log messages to the console. +/// This logger is thread-safe and can be cloned. +#[derive(Default, Clone)] +pub struct ConsoleLogger { + logger: Arc>>, + debug_level: u8, +} + +impl ConsoleLogger { + /// Creates a new logger. + pub fn new(debug_level: u8) -> Self { + ConsoleLogger { + logger: Arc::new(Mutex::new(paris::Logger::new())), + debug_level, + } + } +} + +impl Logger for ConsoleLogger { + /// Logs an trace message (only with debug enabled). + fn trace(&self, message: &str) -> &Self { + if self.debug_level > 0 { + self.logger + .lock() + .expect("Failed to lock logger") + .log(message); + } + self + } + + /// Logs an info message. + fn info(&self, message: &str) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .info(message); + self + } + + /// Logs a warning message. + fn warn(&self, message: &str) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .warn(message); + self + } + + /// Logs an error message. + fn error(&self, message: &str) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .error(message); + self + } + + /// Logs a success message. + fn success(&self, message: &str) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .success(message); + self + } + + /// Logs a newline. + fn newline(&self, count: usize) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .newline(count); + self + } + + /// Indents the logger. + fn indent(&self, count: usize) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .indent(count); + self + } + + /// Stops a loading message. + fn done(&self) { + self.logger.lock().expect("Failed to lock logger").done(); + } + + /// Adds a style to the logger. + fn add_style(&self, name: &str, styles: Vec<&'static str>) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .add_style(name, styles); + self + } + + /// Logs a loading message with a spinner. + fn loading(&self, message: &str) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .loading(message); + self + } + + /// Forces the logger to not print a newline for the next message. + fn same(&self) -> &Self { + self.logger.lock().expect("Failed to lock logger").same(); + self + } + + /// Logs a message without icon. + fn log(&self, message: &str) -> &Self { + self.logger + .lock() + .expect("Failed to lock logger") + .log(message); + self + } +} + +/// A logger that does not log anything. +#[derive(Default, Clone)] +pub struct NullLogger {} + +impl NullLogger { + /// Creates a new logger. + pub fn new() -> Self { + NullLogger {} + } +} + +impl Logger for NullLogger { + /// Logs an trace message (only with debug enabled). + fn trace(&self, _: &str) -> &Self { + self + } + + /// Logs an info message. + fn info(&self, _: &str) -> &Self { + self + } + + /// Logs a warning message. + fn warn(&self, _: &str) -> &Self { + self + } + + /// Logs an error message. + fn error(&self, _: &str) -> &Self { + self + } + + /// Logs a success message. + fn success(&self, _: &str) -> &Self { + self + } + + /// Logs a newline. + fn newline(&self, _: usize) -> &Self { + self + } + + /// Indents the logger. + fn indent(&self, _: usize) -> &Self { + self + } + + /// Stops a loading message. + fn done(&self) {} + + /// Adds a style to the logger. + fn add_style(&self, _: &str, _: Vec<&'static str>) -> &Self { + self + } + + /// Logs a loading message with a spinner. + fn loading(&self, _: &str) -> &Self { + self + } + + /// Forces the logger to not print a newline for the next message. + fn same(&self) -> &Self { + self + } + + /// Logs a message without icon. + fn log(&self, _: &str) -> &Self { + self + } +} diff --git a/crates/weaver_resolved_schema/Cargo.toml b/crates/weaver_resolved_schema/Cargo.toml new file mode 100644 index 00000000..70fe817a --- /dev/null +++ b/crates/weaver_resolved_schema/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "weaver_resolved_schema" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true +edition.workspace = true + +[dependencies] +weaver_version = { path = "../weaver_version" } +weaver_semconv = { path = "../weaver_semconv" } + +serde.workspace = true +ordered-float.workspace = true + +[dev-dependencies] +serde_json = "1.0.64" \ No newline at end of file diff --git a/crates/weaver_resolved_schema/src/attribute.rs b/crates/weaver_resolved_schema/src/attribute.rs new file mode 100644 index 00000000..da322fb5 --- /dev/null +++ b/crates/weaver_resolved_schema/src/attribute.rs @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: Apache-2.0 + +#![allow(rustdoc::invalid_html_tags)] + +//! Specification of a resolved attribute. + +use crate::catalog::Stability; +use crate::tags::Tags; +use crate::value::Value; +use ordered_float::OrderedFloat; +use serde::{Deserialize, Serialize}; +use weaver_semconv::attribute::AttributeSpec; + +/// An attribute definition. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct Attribute { + /// Attribute name. + pub name: String, + /// Either a string literal denoting the type as a primitive or an + /// array type, a template type or an enum definition. + pub r#type: AttributeType, + /// A brief description of the attribute. + #[serde(skip_serializing_if = "String::is_empty")] + pub brief: String, + /// Sequence of example values for the attribute or single example + /// value. They are required only for string and string array + /// attributes. Example values must be of the same type of the + /// attribute. If only a single example is provided, it can directly + /// be reported without encapsulating it into a sequence/dictionary. + #[serde(skip_serializing_if = "Option::is_none")] + pub examples: Option, + /// Associates a tag ("sub-group") to the attribute. It carries no + /// particular semantic meaning but can be used e.g. for filtering + /// in the markdown generator. + #[serde(skip_serializing_if = "Option::is_none")] + pub tag: Option, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + pub requirement_level: RequirementLevel, + /// Specifies if the attribute is (especially) relevant for sampling + /// and thus should be set at span start. It defaults to false. + /// Note: this field is experimental. + #[serde(skip_serializing_if = "Option::is_none")] + pub sampling_relevant: Option, + /// A more elaborate description of the attribute. + /// It defaults to an empty string. + #[serde(skip_serializing_if = "String::is_empty")] + #[serde(default)] + pub note: String, + /// Specifies the stability of the attribute. + /// Note that, if stability is missing but deprecated is present, it will + /// automatically set the stability to deprecated. If deprecated is + /// present and stability differs from deprecated, this will result in an + /// error. + #[serde(skip_serializing_if = "Option::is_none")] + pub stability: Option, + /// Specifies if the attribute is deprecated. The string + /// provided as MUST specify why it's deprecated and/or what + /// to use instead. See also stability. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecated: Option, + /// A set of tags for the attribute. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, + + /// The value of the attribute. + /// Note: This is only used in a telemetry schema specification. + #[serde(skip_serializing_if = "Option::is_none")] + pub value: Option, +} + +/// An unresolved attribute definition. +#[derive(Debug, Clone)] +pub struct UnresolvedAttribute { + /// The attribute specification. + pub spec: AttributeSpec, +} + +/// The different types of attributes. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(tag = "type")] +pub enum AttributeType { + /// A boolean attribute. + Boolean, + /// A integer attribute (signed 64 bit integer). + Int, + /// A double attribute (double precision floating point (IEEE 754-1985)). + Double, + /// A string attribute. + String, + /// An array of strings attribute. + Strings, + /// An array of integer attribute. + Ints, + /// An array of double attribute. + Doubles, + /// An array of boolean attribute. + Booleans, + + /// A template boolean attribute. + TemplateBoolean, + /// A template integer attribute. + #[serde(rename = "template[int]")] + TemplateInt, + /// A template double attribute. + #[serde(rename = "template[double]")] + TemplateDouble, + /// A template string attribute. + #[serde(rename = "template[string]")] + TemplateString, + /// A template array of strings attribute. + #[serde(rename = "template[string[]]")] + TemplateStrings, + /// A template array of integer attribute. + #[serde(rename = "template[int[]]")] + TemplateInts, + /// A template array of double attribute. + #[serde(rename = "template[double[]]")] + TemplateDoubles, + /// A template array of boolean attribute. + #[serde(rename = "template[boolean[]]")] + TemplateBooleans, + + /// An enum definition type. + Enum { + /// Set to false to not accept values other than the specified members. + /// It defaults to true. + allow_custom_values: bool, + /// List of enum entries. + members: Vec, + }, +} + +/// Possible enum entries. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct EnumEntries { + /// String that uniquely identifies the enum entry. + pub id: String, + /// String, int, or boolean; value of the enum entry. + pub value: Value, + /// Brief description of the enum entry value. + /// It defaults to the value of id. + #[serde(skip_serializing_if = "Option::is_none")] + pub brief: Option, + /// Longer description. + /// It defaults to an empty string. + #[serde(skip_serializing_if = "Option::is_none")] + pub note: Option, +} + +/// The different types of examples. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(tag = "type")] +pub enum Example { + /// A boolean example. + Bool { + /// The value of the example. + value: bool, + }, + /// A integer example. + Int { + /// The value of the example. + value: i64, + }, + /// A double example. + Double { + /// The value of the example. + value: OrderedFloat, + }, + /// A string example. + String { + /// The value of the example. + value: String, + }, + /// A array of integers example. + Ints { + /// The value of the example. + values: Vec, + }, + /// A array of doubles example. + Doubles { + /// The value of the example. + values: Vec>, + }, + /// A array of bools example. + Bools { + /// The value of the example. + values: Vec, + }, + /// A array of strings example. + Strings { + /// The value of the example. + values: Vec, + }, +} + +impl Example { + /// Creates an example from a f64. + pub fn from_f64(value: f64) -> Self { + Example::Double { + value: OrderedFloat(value), + } + } + + /// Creates an example from several f64. + pub fn from_f64s(values: Vec) -> Self { + Example::Doubles { + values: values.into_iter().map(OrderedFloat).collect(), + } + } +} + +/// The different requirement levels. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(tag = "type")] +pub enum RequirementLevel { + /// A required requirement level. + Required, + /// An optional requirement level. + Recommended { + /// The description of the recommendation. + #[serde(skip_serializing_if = "Option::is_none")] + text: Option, + }, + /// An opt-in requirement level. + OptIn, + /// A conditional requirement level. + ConditionallyRequired { + /// The description of the condition. + #[serde(skip_serializing_if = "String::is_empty")] + text: String, + }, +} + +/// An internal reference to an attribute in the catalog. +#[derive(Serialize, Deserialize, Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub struct AttributeRef(pub u32); diff --git a/crates/weaver_resolved_schema/src/catalog.rs b/crates/weaver_resolved_schema/src/catalog.rs new file mode 100644 index 00000000..2fb6a0b7 --- /dev/null +++ b/crates/weaver_resolved_schema/src/catalog.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Defines the catalog of attributes, metrics, and other telemetry items +//! that are shared across multiple signals in the Resolved Telemetry Schema. + +use crate::attribute::Attribute; +use crate::metric::Metric; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +/// A catalog of attributes, metrics, and other telemetry signals that are shared +/// in the Resolved Telemetry Schema. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct Catalog { + /// Catalog of attributes used in the schema. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// Catalog of metrics used in the schema. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub metrics: Vec, +} + +/// The level of stability for a definition. +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] +pub enum Stability { + /// A deprecated definition. + Deprecated, + /// An experimental definition. + Experimental, + /// A stable definition. + Stable, +} diff --git a/crates/weaver_resolved_schema/src/instrumentation_library.rs b/crates/weaver_resolved_schema/src/instrumentation_library.rs new file mode 100644 index 00000000..907ef3d6 --- /dev/null +++ b/crates/weaver_resolved_schema/src/instrumentation_library.rs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Define an instrumentation library. + +use crate::signal::{Event, MultivariateMetric, Span, UnivariateMetric}; +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; + +/// An instrumentation library specification. +/// MUST be used both by applications and libraries. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct InstrumentationLibrary { + /// An optional name for the instrumentation library. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// An optional version for the instrumentation library. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + /// A set of tags for the instrumentation library. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, + /// A set of univariate metrics produced by the instrumentation library. + #[serde(skip_serializing_if = "Vec::is_empty")] + univariate_metrics: Vec, + /// A set of multivariate metrics produced by the instrumentation library. + #[serde(skip_serializing_if = "Vec::is_empty")] + multivariate_metrics: Vec, + /// A set of events produced by the instrumentation library. + #[serde(skip_serializing_if = "Vec::is_empty")] + events: Vec, + /// A set of spans produced by the instrumentation library. + #[serde(skip_serializing_if = "Vec::is_empty")] + spans: Vec, +} diff --git a/crates/weaver_resolved_schema/src/lib.rs b/crates/weaver_resolved_schema/src/lib.rs new file mode 100644 index 00000000..01dfffd8 --- /dev/null +++ b/crates/weaver_resolved_schema/src/lib.rs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Define the concept of Resolved Telemetry Schema. +//! A Resolved Telemetry Schema is self-contained and doesn't contain any +//! external references to other schemas or semantic conventions. + +#![deny(missing_docs)] +#![deny(clippy::print_stdout)] +#![deny(clippy::print_stderr)] + +use crate::catalog::Catalog; +use crate::instrumentation_library::InstrumentationLibrary; +use crate::registry::Registry; +use crate::resource::Resource; +use serde::{Deserialize, Serialize}; +use weaver_version::Versions; + +pub mod attribute; +pub mod catalog; +pub mod instrumentation_library; +pub mod lineage; +pub mod metric; +pub mod registry; +pub mod resource; +pub mod signal; +pub mod tags; +pub mod value; + +/// A Resolved Telemetry Schema. +/// A Resolved Telemetry Schema is self-contained and doesn't contain any +/// external references to other schemas or semantic conventions. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct ResolvedTelemetrySchema { + /// Version of the file structure. + pub file_format: String, + /// Schema URL that this file is published at. + pub schema_url: String, + /// A list of semantic convention registries that can be used in this schema + /// and its descendants. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub registries: Vec, + /// Catalog of unique items that are shared across multiple registries + /// and signals. + pub catalog: Catalog, + /// Resource definition (only for application). + #[serde(skip_serializing_if = "Option::is_none")] + pub resource: Option, + /// Definition of the instrumentation library for the instrumented application or library. + /// Or none if the resolved telemetry schema represents a semantic convention registry. + #[serde(skip_serializing_if = "Option::is_none")] + pub instrumentation_library: Option, + /// The list of dependencies of the current instrumentation application or library. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub dependencies: Vec, + /// Definitions for each schema version in this family. + /// Note: the ordering of versions is defined according to semver + /// version number ordering rules. + /// This section is described in more details in the OTEP 0152 and in a dedicated + /// section below. + /// + #[serde(skip_serializing_if = "Option::is_none")] + pub versions: Option, +} diff --git a/crates/weaver_resolved_schema/src/lineage.rs b/crates/weaver_resolved_schema/src/lineage.rs new file mode 100644 index 00000000..7781cd17 --- /dev/null +++ b/crates/weaver_resolved_schema/src/lineage.rs @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Data structures used to keep track of the lineage of a semantic convention. + +use crate::attribute::AttributeRef; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// Resolution mode. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum ResolutionMode { + /// Represents the resolution of a reference. + Reference, + /// Represents the resolution of an `extends` clause. + Extends, +} + +/// Field id. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Hash, Eq, Ord, PartialOrd)] +pub enum FieldId { + /// The group id. + GroupId, + /// The group brief. + GroupBrief, + /// The group note. + GroupNote, + /// The group prefix. + GroupPrefix, + /// The group extends. + GroupExtends, + /// The group stability. + GroupStability, + /// The group deprecated. + GroupDeprecated, + /// The group constraints. + GroupConstraints, + /// The group attributes. + GroupAttributes, + + /// The span kind. + SpanKind, + /// The span event. + SpanEvent, + + /// The event name. + EventName, + + /// The metric name. + MetricName, + /// The metric instrument type. + MetricInstrument, + /// The metric unit. + MetricUnit, + + /// The attribute brief. + AttributeBrief, + /// The attribute examples. + AttributeExamples, + /// The attribute tag. + AttributeTag, + /// The attribute requirement level. + AttributeRequirementLevel, + /// The attribute sampling relevant. + AttributeSamplingRelevant, + /// The attribute note. + AttributeNote, + /// The attribute stability. + AttributeStability, + /// The attribute deprecated. + AttributeDeprecated, + /// The attribute tags. + AttributeTags, + /// The attribute value. + AttributeValue, +} + +/// Field lineage. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct FieldLineage { + /// The resolution mode used to resolve the field. + pub resolution_mode: ResolutionMode, + /// The id of the group where the field is defined. + pub group_id: String, +} + +/// Group lineage. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct GroupLineage { + /// The provenance of the group. + provenance: String, + /// The lineage per group field. + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default)] + fields: BTreeMap, + /// The lineage per attribute field. + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(default)] + attributes: BTreeMap>, +} + +impl GroupLineage { + /// Creates a new group lineage. + pub fn new(provenance: String) -> Self { + Self { + provenance, + fields: BTreeMap::new(), + attributes: BTreeMap::new(), + } + } + + /// Adds a group field lineage. + pub fn add_group_field_lineage(&mut self, field_id: FieldId, field_lineage: FieldLineage) { + let prev = self.fields.insert(field_id.clone(), field_lineage.clone()); + if prev.is_some() { + panic!("Group field `{field_id:?}` lineage already exists (prev: {prev:?}, new: {field_lineage:?}). This is a bug."); + } + } + + /// Adds an attribute field lineage. + pub fn add_attribute_field_lineage( + &mut self, + attr_ref: AttributeRef, + field_id: FieldId, + field_lineage: FieldLineage, + ) { + let attribute_fields = self.attributes.entry(attr_ref).or_default(); + let prev = attribute_fields.insert(field_id.clone(), field_lineage.clone()); + if prev.is_some() { + panic!("Group attribute `{attr_ref:?}.{field_id:?}` lineage already exists (prev: {prev:?}, new: {field_lineage:?}). This is a bug."); + } + } + + /// Returns the provenance of the group. + pub fn provenance(&self) -> &str { + &self.provenance + } + + /// Returns the lineage of the specified field. + pub fn field_lineage(&self, field_id: &FieldId) -> Option<&FieldLineage> { + self.fields.get(field_id) + } +} diff --git a/crates/weaver_resolved_schema/src/metric.rs b/crates/weaver_resolved_schema/src/metric.rs new file mode 100644 index 00000000..9a5fd7ce --- /dev/null +++ b/crates/weaver_resolved_schema/src/metric.rs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Specification of a resolved metric. + +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; + +/// An internal reference to a metric in the catalog. +#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +pub struct MetricRef(pub u32); + +/// A metric definition. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct Metric { + /// Metric name. + pub name: String, + /// Brief description of the metric. + pub brief: String, + /// Brief description of the metric. + pub note: String, + /// Type of the metric (e.g. gauge, histogram, ...). + pub instrument: Instrument, + /// Unit of the metric. + pub unit: Option, + /// A set of tags for the metric. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} + +/// The type of the metric. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum Instrument { + /// An up-down counter metric. + UpDownCounter, + /// A counter metric. + Counter, + /// A gauge metric. + Gauge, + /// A histogram metric. + Histogram, +} diff --git a/crates/weaver_resolved_schema/src/registry.rs b/crates/weaver_resolved_schema/src/registry.rs new file mode 100644 index 00000000..ed14eb25 --- /dev/null +++ b/crates/weaver_resolved_schema/src/registry.rs @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 + +#![allow(rustdoc::invalid_html_tags)] + +//! A semantic convention registry. + +use crate::attribute::{AttributeRef, UnresolvedAttribute}; +use serde::{Deserialize, Serialize}; + +use crate::catalog::Stability; +use crate::lineage::GroupLineage; +use crate::metric::Instrument; +use crate::signal::SpanKind; + +/// A semantic convention registry. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct Registry { + /// The semantic convention registry url. + #[serde(skip_serializing_if = "String::is_empty")] + pub registry_url: String, + /// A list of semantic convention groups. + pub groups: Vec, +} + +/// A registry containing unresolved groups. +#[derive(Debug)] +pub struct UnresolvedRegistry { + /// The semantic convention registry. + pub registry: Registry, + /// List of unresolved groups that belong to the registry. + pub groups: Vec, +} + +/// Group specification. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Group { + /// The id that uniquely identifies the semantic convention. + pub id: String, + /// The type of the group including the specific fields for each type. + pub typed_group: TypedGroup, + /// A brief description of the semantic convention. + #[serde(skip_serializing_if = "String::is_empty")] + pub brief: String, + /// A more elaborate description of the semantic convention. + /// It defaults to an empty string. + #[serde(default)] + #[serde(skip_serializing_if = "String::is_empty")] + pub note: String, + /// Prefix for the attributes for this semantic convention. + /// It defaults to an empty string. + #[serde(default)] + #[serde(skip_serializing_if = "String::is_empty")] + pub prefix: String, + /// Reference another semantic convention id. It inherits the prefix, + /// constraints, and all attributes defined in the specified semantic + /// convention. + #[serde(skip_serializing_if = "Option::is_none")] + pub extends: Option, + /// Specifies the stability of the semantic convention. + /// Note that, if stability is missing but deprecated is present, it will + /// automatically set the stability to deprecated. If deprecated is + /// present and stability differs from deprecated, this will result in an + /// error. + #[serde(skip_serializing_if = "Option::is_none")] + pub stability: Option, + /// Specifies if the semantic convention is deprecated. The string + /// provided as MUST specify why it's deprecated and/or what + /// to use instead. See also stability. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecated: Option, + /// Additional constraints. + /// Allow to define additional requirements on the semantic convention. + /// It defaults to an empty list. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub constraints: Vec, + /// List of attributes that belong to the semantic convention. + #[serde(default)] + pub attributes: Vec, + /// The lineage of the group. + #[serde(skip_serializing_if = "Option::is_none")] + pub lineage: Option, +} + +/// A group containing unresolved attributes. +#[derive(Debug)] +pub struct UnresolvedGroup { + /// The group specification. + pub group: Group, + /// List of unresolved attributes that belong to the semantic convention + /// group. + pub attributes: Vec, + /// The provenance of the group (URL or path). + pub provenance: String, +} + +/// An enum representing the type of the group including the specific fields +/// for each type. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(tag = "type")] +pub enum TypedGroup { + /// A semantic convention group representing an attribute group. + AttributeGroup {}, + /// A semantic convention group representing a span. + Span { + /// Specifies the kind of the span. + /// Note: only valid if type is span (the default) + span_kind: Option, + /// List of strings that specify the ids of event semantic conventions + /// associated with this span semantic convention. + /// Note: only valid if type is span (the default) + #[serde(default)] + events: Vec, + }, + /// A semantic convention group representing an event. + Event { + /// The name of the event. If not specified, the prefix is used. + /// If prefix is empty (or unspecified), name is required. + name: Option, + }, + /// A semantic convention group representing a metric. + Metric { + /// The metric name as described by the [OpenTelemetry Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#timeseries-model). + /// Note: This field is required if type is metric. + metric_name: Option, + /// The instrument type that should be used to record the metric. Note that + /// the semantic conventions must be written using the names of the + /// synchronous instrument types (counter, gauge, updowncounter and + /// histogram). + /// For more details: [Metrics semantic conventions - Instrument types](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-types). + /// Note: This field is required if type is metric. + instrument: Option, + /// The unit in which the metric is measured, which should adhere to the + /// [guidelines](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-units). + /// Note: This field is required if type is metric. + unit: Option, + }, + /// A semantic convention group representing a metric group. + MetricGroup {}, + /// A semantic convention group representing a resource. + Resource {}, + /// A semantic convention group representing a scope. + Scope {}, +} + +/// Allow to define additional requirements on the semantic convention. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(deny_unknown_fields)] +pub struct Constraint { + /// any_of accepts a list of sequences. Each sequence contains a list of + /// attribute ids that are required. any_of enforces that all attributes + /// of at least one of the sequences are set. + #[serde(default)] + pub any_of: Vec, + /// include accepts a semantic conventions id. It includes as part of this + /// semantic convention all constraints and required attributes that are + /// not already defined in the current semantic convention. + pub include: Option, +} diff --git a/crates/weaver_resolved_schema/src/resource.rs b/crates/weaver_resolved_schema/src/resource.rs new file mode 100644 index 00000000..e5303f2a --- /dev/null +++ b/crates/weaver_resolved_schema/src/resource.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Define an OpenTelemetry resource. + +use crate::attribute::AttributeRef; +use serde::{Deserialize, Serialize}; + +/// Definition of attributes associated with the resource. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Resource { + /// List of references to attributes present in the shared catalog. + pub attributes: Vec, +} diff --git a/crates/weaver_resolved_schema/src/signal.rs b/crates/weaver_resolved_schema/src/signal.rs new file mode 100644 index 00000000..b0b60a19 --- /dev/null +++ b/crates/weaver_resolved_schema/src/signal.rs @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Define the concept of signal. + +use serde::{Deserialize, Serialize}; + +use crate::attribute::AttributeRef; +use crate::metric::MetricRef; +use crate::tags::Tags; + +/// A univariate metric signal. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct UnivariateMetric { + /// References to attributes defined in the catalog. + #[serde(skip_serializing_if = "Vec::is_empty")] + attributes: Vec, + /// Reference to a metric defined in the catalog. + metric: MetricRef, + /// A set of tags for the univariate metric. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, +} + +/// A multivariate metric signal. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct MultivariateMetric { + /// The name of the multivariate metric. + name: String, + /// References to attributes defined in the catalog. + #[serde(skip_serializing_if = "Vec::is_empty")] + attributes: Vec, + /// The metrics of the multivariate metric. + metrics: Vec, + /// Brief description of the multivariate metric. + brief: Option, + /// Longer description. + /// It defaults to an empty string. + note: Option, + /// A set of tags for the multivariate metric. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, +} + +/// An event signal. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Event { + /// The name of the event. + name: String, + /// References to attributes defined in the catalog. + #[serde(skip_serializing_if = "Vec::is_empty")] + attributes: Vec, + /// The domain of the event. + domain: String, + /// Brief description of the event. + brief: Option, + /// Longer description. + /// It defaults to an empty string. + note: Option, + /// A set of tags for the event. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, +} + +/// A span signal. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Span { + /// The name of the span. + name: String, + /// References to attributes defined in the catalog. + #[serde(skip_serializing_if = "Vec::is_empty")] + attributes: Vec, + /// The kind of the span. + #[serde(skip_serializing_if = "Option::is_none")] + kind: Option, + /// The events of the span. + #[serde(skip_serializing_if = "Vec::is_empty")] + events: Vec, + /// The links of the span. + #[serde(skip_serializing_if = "Vec::is_empty")] + links: Vec, + /// Brief description of the span. + brief: Option, + /// Longer description. + /// It defaults to an empty string. + note: Option, + /// A set of tags for the span. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, +} + +/// The span kind. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum SpanKind { + /// An internal span. + Internal, + /// A client span. + Client, + /// A server span. + Server, + /// A producer span. + Producer, + /// A consumer span. + Consumer, +} + +/// A span event specification. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct SpanEvent { + /// The name of the span event. + pub event_name: String, + /// The attributes of the span event. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// Brief description of the span event. + pub brief: Option, + /// Longer description. + /// It defaults to an empty string. + pub note: Option, + /// A set of tags for the span event. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} + +/// A span link specification. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct SpanLink { + /// The name of the span link. + pub link_name: String, + /// The attributes of the span link. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// Brief description of the span link. + pub brief: Option, + /// Longer description. + /// It defaults to an empty string. + pub note: Option, + /// A set of tags for the span link. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} diff --git a/crates/weaver_resolved_schema/src/tags.rs b/crates/weaver_resolved_schema/src/tags.rs new file mode 100644 index 00000000..572dfe86 --- /dev/null +++ b/crates/weaver_resolved_schema/src/tags.rs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Define the concept of tag. + +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// A set of tags. +/// +/// Examples of tags: +/// - sensitivity: pii +/// - sensitivity: phi +/// - data_classification: restricted +/// - semantic_type: email +/// - semantic_type: first_name +/// - owner: +/// - provenance: browser_sensor +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(transparent)] +#[serde(deny_unknown_fields)] +pub struct Tags { + /// The tags. + pub tags: BTreeMap, +} diff --git a/crates/weaver_resolved_schema/src/value.rs b/crates/weaver_resolved_schema/src/value.rs new file mode 100644 index 00000000..b6a4eac9 --- /dev/null +++ b/crates/weaver_resolved_schema/src/value.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Specification of a resolved value. + +use ordered_float::OrderedFloat; +use serde::{Deserialize, Serialize}; + +/// The different types of values. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(tag = "type")] +pub enum Value { + /// A integer value. + Int { + /// The value + value: i64, + }, + /// A double value. + Double { + /// The value + value: OrderedFloat, + }, + /// A string value. + String { + /// The value + value: String, + }, +} + +impl Value { + /// Creates a double value from a f64. + pub fn from_f64(value: f64) -> Self { + Value::Double { + value: OrderedFloat(value), + } + } +} diff --git a/crates/weaver_resolver/Cargo.toml b/crates/weaver_resolver/Cargo.toml new file mode 100644 index 00000000..f0a13ed0 --- /dev/null +++ b/crates/weaver_resolver/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "weaver_resolver" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true +edition.workspace = true + +[dependencies] +weaver_logger = { path = "../weaver_logger" } +weaver_semconv = { path = "../weaver_semconv" } +weaver_schema = { path = "../weaver_schema" } +weaver_version = { path = "../weaver_version" } +weaver_cache = { path = "../weaver_cache" } +weaver_resolved_schema = { path = "../weaver_resolved_schema" } + +regex.workspace = true +thiserror.workspace = true +rayon.workspace = true +serde_json.workspace = true +serde_yaml.workspace = true + +url = "2.5.0" +walkdir = "2.4.0" +serde = { version = "1.0.193", features = ["derive"] } + +[dev-dependencies] +glob = "0.3.1" \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-1-single-attr-ref/README.md b/crates/weaver_resolver/data/registry-test-1-single-attr-ref/README.md new file mode 100644 index 00000000..2dfbdb7f --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-1-single-attr-ref/README.md @@ -0,0 +1 @@ +Test a single attribute reference and apply overriding rules. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-1-single-attr-ref/expected-attribute-catalog.json b/crates/weaver_resolver/data/registry-test-1-single-attr-ref/expected-attribute-catalog.json new file mode 100644 index 00000000..2e220385 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-1-single-attr-ref/expected-attribute-catalog.json @@ -0,0 +1,39 @@ +[ + { + "name": "messaging.destination.name", + "type": { + "type": "String" + }, + "brief": "The message destination name", + "examples": { + "type": "Strings", + "values": [ + "MyQueue", + "MyTopic" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If\nthe broker doesn't have such notion, the destination name SHOULD uniquely identify the broker.\n" + }, + { + "name": "messaging.destination.name", + "type": { + "type": "String" + }, + "brief": "The message destination name", + "examples": { + "type": "Strings", + "values": [ + "MyQueue", + "MyTopic" + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "if and only if `messaging.destination.name` is known to have low cardinality. Otherwise, `messaging.destination.template` MAY be populated." + }, + "note": "Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If\nthe broker doesn't have such notion, the destination name SHOULD uniquely identify the broker.\n" + } +] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-1-single-attr-ref/expected-registry.json b/crates/weaver_resolver/data/registry-test-1-single-attr-ref/expected-registry.json new file mode 100644 index 00000000..49eb396a --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-1-single-attr-ref/expected-registry.json @@ -0,0 +1,64 @@ +{ + "registry_url": "https://semconv-registry.com", + "groups": [ + { + "id": "metric.messaging.attributes", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Common messaging metrics attributes.", + "attributes": [ + 1 + ], + "lineage": { + "provenance": "data/registry-test-1-single-attr-ref/registry/metrics-messaging.yaml", + "attributes": { + "1": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + } + } + } + } + }, + { + "id": "registry.messaging", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Attributes describing telemetry around messaging systems and messaging activities.", + "prefix": "messaging", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-1-single-attr-ref/registry/registry-messaging.yaml" + } + } + ] +} \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-1-single-attr-ref/registry/metrics-messaging.yaml b/crates/weaver_resolver/data/registry-test-1-single-attr-ref/registry/metrics-messaging.yaml new file mode 100644 index 00000000..d93f2aee --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-1-single-attr-ref/registry/metrics-messaging.yaml @@ -0,0 +1,8 @@ +groups: + - id: metric.messaging.attributes + type: attribute_group + brief: "Common messaging metrics attributes." + attributes: + - ref: messaging.destination.name + requirement_level: + conditionally_required: if and only if `messaging.destination.name` is known to have low cardinality. Otherwise, `messaging.destination.template` MAY be populated. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-1-single-attr-ref/registry/registry-messaging.yaml b/crates/weaver_resolver/data/registry-test-1-single-attr-ref/registry/registry-messaging.yaml new file mode 100644 index 00000000..6cd60966 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-1-single-attr-ref/registry/registry-messaging.yaml @@ -0,0 +1,13 @@ +groups: + - id: registry.messaging + prefix: messaging + type: attribute_group + brief: 'Attributes describing telemetry around messaging systems and messaging activities.' + attributes: + - id: destination.name + type: string + brief: 'The message destination name' + note: | + 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'] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/README.md b/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/README.md new file mode 100644 index 00000000..5f349994 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/README.md @@ -0,0 +1 @@ +Test multiple attribute references and apply the corresponding overriding rules. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/expected-attribute-catalog.json b/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/expected-attribute-catalog.json new file mode 100644 index 00000000..5baa47a7 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/expected-attribute-catalog.json @@ -0,0 +1,625 @@ +[ + { + "name": "messaging.batch.message_count", + "type": { + "type": "Int" + }, + "brief": "The number of messages sent, received, or processed in the scope of the batching operation.", + "examples": { + "type": "Ints", + "values": [ + 0, + 1, + 2 + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n" + }, + { + "name": "messaging.client_id", + "type": { + "type": "String" + }, + "brief": "A unique identifier for the client that consumes or produces a message.\n", + "examples": { + "type": "Strings", + "values": [ + "client-5", + "myhost@8742@s8083jm" + ] + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.destination.name", + "type": { + "type": "String" + }, + "brief": "The message destination name", + "examples": { + "type": "Strings", + "values": [ + "MyQueue", + "MyTopic" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If\nthe broker doesn't have such notion, the destination name SHOULD uniquely identify the broker.\n" + }, + { + "name": "messaging.destination.template", + "type": { + "type": "String" + }, + "brief": "Low cardinality representation of the messaging destination name", + "examples": { + "type": "Strings", + "values": [ + "/customers/{customerId}" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n" + }, + { + "name": "messaging.destination.anonymous", + "type": { + "type": "Boolean" + }, + "brief": "A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name).", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.destination.temporary", + "type": { + "type": "Boolean" + }, + "brief": "A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed.", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.destination_publish.anonymous", + "type": { + "type": "Boolean" + }, + "brief": "A boolean that is true if the publish message destination is anonymous (could be unnamed or have auto-generated name).", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.destination_publish.name", + "type": { + "type": "String" + }, + "brief": "The name of the original destination the message was published to", + "examples": { + "type": "Strings", + "values": [ + "MyQueue", + "MyTopic" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "The name SHOULD uniquely identify a specific queue, topic, or other entity within the broker. If\nthe broker doesn't have such notion, the original destination name SHOULD uniquely identify the broker.\n" + }, + { + "name": "messaging.kafka.consumer.group", + "type": { + "type": "String" + }, + "brief": "Name of the Kafka Consumer Group that is handling the message. Only applies to consumers, not producers.\n", + "examples": { + "type": "String", + "value": "my-group" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.kafka.destination.partition", + "type": { + "type": "Int" + }, + "brief": "Partition the message is sent to.\n", + "examples": { + "type": "Int", + "value": 2 + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.kafka.message.key", + "type": { + "type": "String" + }, + "brief": "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.\n", + "examples": { + "type": "String", + "value": "myKey" + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n" + }, + { + "name": "messaging.kafka.message.offset", + "type": { + "type": "Int" + }, + "brief": "The offset of a record in the corresponding Kafka partition.\n", + "examples": { + "type": "Int", + "value": 42 + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.kafka.message.tombstone", + "type": { + "type": "Boolean" + }, + "brief": "A boolean that is true if the message is a tombstone.", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.message.conversation_id", + "type": { + "type": "String" + }, + "brief": "The conversation ID identifying the conversation to which the message belongs, represented as a string. Sometimes called \"Correlation ID\".\n", + "examples": { + "type": "String", + "value": "MyConversationId" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.message.envelope.size", + "type": { + "type": "Int" + }, + "brief": "The size of the message body and metadata in bytes.\n", + "examples": { + "type": "Int", + "value": 2738 + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "This can refer to both the compressed or uncompressed size. If both sizes are known, the uncompressed\nsize should be used.\n" + }, + { + "name": "messaging.message.id", + "type": { + "type": "String" + }, + "brief": "A value used by the messaging system as an identifier for the message, represented as a string.", + "examples": { + "type": "String", + "value": "452a7c7c7c7048c2f887f61572b18fc2" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.message.body.size", + "type": { + "type": "Int" + }, + "brief": "The size of the message body in bytes.\n", + "examples": { + "type": "Int", + "value": 1439 + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "This can refer to both the compressed or uncompressed body size. If both sizes are known, the uncompressed\nbody size should be used.\n" + }, + { + "name": "messaging.operation", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "publish", + "value": { + "type": "String", + "value": "publish" + }, + "brief": "One or more messages are provided for publishing to an intermediary. If a single message is published, the context of the \"Publish\" span can be used as the creation context and no \"Create\" span needs to be created.\n" + }, + { + "id": "create", + "value": { + "type": "String", + "value": "create" + }, + "brief": "A message is created. \"Create\" spans always refer to a single message and are used to provide a unique creation context for messages in batch publishing scenarios.\n" + }, + { + "id": "receive", + "value": { + "type": "String", + "value": "receive" + }, + "brief": "One or more messages are requested by a consumer. This operation refers to pull-based scenarios, where consumers explicitly call methods of messaging SDKs to receive messages.\n" + }, + { + "id": "deliver", + "value": { + "type": "String", + "value": "deliver" + }, + "brief": "One or more messages are passed to a consumer. This operation refers to push-based scenarios, where consumer register callbacks which get called by messaging SDKs.\n" + } + ] + }, + "brief": "A string identifying the kind of messaging operation.\n", + "requirement_level": { + "type": "Recommended" + }, + "note": "If a custom value is used, it MUST be of low cardinality." + }, + { + "name": "messaging.rabbitmq.destination.routing_key", + "type": { + "type": "String" + }, + "brief": "RabbitMQ message routing key.\n", + "examples": { + "type": "String", + "value": "myKey" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.client_group", + "type": { + "type": "String" + }, + "brief": "Name of the RocketMQ producer/consumer group that is handling the message. The client type is identified by the SpanKind.\n", + "examples": { + "type": "String", + "value": "myConsumerGroup" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.consumption_model", + "type": { + "type": "Enum", + "allow_custom_values": false, + "members": [ + { + "id": "clustering", + "value": { + "type": "String", + "value": "clustering" + }, + "brief": "Clustering consumption model" + }, + { + "id": "broadcasting", + "value": { + "type": "String", + "value": "broadcasting" + }, + "brief": "Broadcasting consumption model" + } + ] + }, + "brief": "Model of message consumption. This only applies to consumer spans.\n", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.delay_time_level", + "type": { + "type": "Int" + }, + "brief": "The delay time level for delay message, which determines the message delay time.\n", + "examples": { + "type": "Int", + "value": 3 + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.delivery_timestamp", + "type": { + "type": "Int" + }, + "brief": "The timestamp in milliseconds that the delay message is expected to be delivered to consumer.\n", + "examples": { + "type": "Int", + "value": 1665987217045 + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.group", + "type": { + "type": "String" + }, + "brief": "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.\n", + "examples": { + "type": "String", + "value": "myMessageGroup" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.keys", + "type": { + "type": "Strings" + }, + "brief": "Key(s) of message, another way to mark message besides message id.\n", + "examples": { + "type": "Strings", + "values": [ + "keyA", + "keyB" + ] + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.tag", + "type": { + "type": "String" + }, + "brief": "The secondary classifier of message besides topic.\n", + "examples": { + "type": "String", + "value": "tagA" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.type", + "type": { + "type": "Enum", + "allow_custom_values": false, + "members": [ + { + "id": "normal", + "value": { + "type": "String", + "value": "normal" + }, + "brief": "Normal message" + }, + { + "id": "fifo", + "value": { + "type": "String", + "value": "fifo" + }, + "brief": "FIFO message" + }, + { + "id": "delay", + "value": { + "type": "String", + "value": "delay" + }, + "brief": "Delay message" + }, + { + "id": "transaction", + "value": { + "type": "String", + "value": "transaction" + }, + "brief": "Transaction message" + } + ] + }, + "brief": "Type of message.\n", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.namespace", + "type": { + "type": "String" + }, + "brief": "Namespace of RocketMQ resources, resources in different namespaces are individual.\n", + "examples": { + "type": "String", + "value": "myNamespace" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.gcp_pubsub.message.ordering_key", + "type": { + "type": "String" + }, + "brief": "The ordering key for a given message. If the attribute is not present, the message does not have an ordering key.\n", + "examples": { + "type": "String", + "value": "ordering_key" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.system", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "activemq", + "value": { + "type": "String", + "value": "activemq" + }, + "brief": "Apache ActiveMQ" + }, + { + "id": "aws_sqs", + "value": { + "type": "String", + "value": "aws_sqs" + }, + "brief": "Amazon Simple Queue Service (SQS)" + }, + { + "id": "azure_eventgrid", + "value": { + "type": "String", + "value": "azure_eventgrid" + }, + "brief": "Azure Event Grid" + }, + { + "id": "azure_eventhubs", + "value": { + "type": "String", + "value": "azure_eventhubs" + }, + "brief": "Azure Event Hubs" + }, + { + "id": "azure_servicebus", + "value": { + "type": "String", + "value": "azure_servicebus" + }, + "brief": "Azure Service Bus" + }, + { + "id": "gcp_pubsub", + "value": { + "type": "String", + "value": "gcp_pubsub" + }, + "brief": "Google Cloud Pub/Sub" + }, + { + "id": "jms", + "value": { + "type": "String", + "value": "jms" + }, + "brief": "Java Message Service" + }, + { + "id": "kafka", + "value": { + "type": "String", + "value": "kafka" + }, + "brief": "Apache Kafka" + }, + { + "id": "rabbitmq", + "value": { + "type": "String", + "value": "rabbitmq" + }, + "brief": "RabbitMQ" + }, + { + "id": "rocketmq", + "value": { + "type": "String", + "value": "rocketmq" + }, + "brief": "Apache RocketMQ" + } + ] + }, + "brief": "An identifier for the messaging system being used. See below for a list of well-known identifiers.\n", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.destination.name", + "type": { + "type": "String" + }, + "brief": "The message destination name", + "examples": { + "type": "Strings", + "values": [ + "MyQueue", + "MyTopic" + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "if and only if `messaging.destination.name` is known to have low cardinality. Otherwise, `messaging.destination.template` MAY be populated." + }, + "note": "Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If\nthe broker doesn't have such notion, the destination name SHOULD uniquely identify the broker.\n" + }, + { + "name": "messaging.destination.template", + "type": { + "type": "String" + }, + "brief": "Low cardinality representation of the messaging destination name", + "examples": { + "type": "Strings", + "values": [ + "/customers/{customerId}" + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "if available." + }, + "note": "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.\n" + } +] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/expected-registry.json b/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/expected-registry.json new file mode 100644 index 00000000..985316ee --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/expected-registry.json @@ -0,0 +1,124 @@ +{ + "registry_url": "https://semconv-registry.com", + "groups": [ + { + "id": "metric.messaging.attributes", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Common messaging metrics attributes.", + "attributes": [ + 30, + 31 + ], + "lineage": { + "provenance": "data/registry-test-2-multi-attr-refs/registry/metrics-messaging.yaml", + "attributes": { + "30": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + } + }, + "31": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + } + } + } + } + }, + { + "id": "registry.messaging", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Attributes describing telemetry around messaging systems and messaging activities.", + "prefix": "messaging", + "attributes": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29 + ], + "lineage": { + "provenance": "data/registry-test-2-multi-attr-refs/registry/registry-messaging.yaml" + } + } + ] +} \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/registry/metrics-messaging.yaml b/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/registry/metrics-messaging.yaml new file mode 100644 index 00000000..9f424d43 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/registry/metrics-messaging.yaml @@ -0,0 +1,11 @@ +groups: + - id: metric.messaging.attributes + type: attribute_group + brief: "Common messaging metrics attributes." + attributes: + - ref: messaging.destination.name + requirement_level: + conditionally_required: if and only if `messaging.destination.name` is known to have low cardinality. Otherwise, `messaging.destination.template` MAY be populated. + - ref: messaging.destination.template + requirement_level: + conditionally_required: if available. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/registry/registry-messaging.yaml b/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/registry/registry-messaging.yaml new file mode 100644 index 00000000..c7ba8fd4 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-2-multi-attr-refs/registry/registry-messaging.yaml @@ -0,0 +1,245 @@ +groups: + - id: registry.messaging + prefix: messaging + type: attribute_group + brief: 'Attributes describing telemetry around messaging systems and messaging activities.' + attributes: + - id: batch.message_count + type: int + brief: The number of messages sent, received, or processed in the scope of the batching operation. + note: > + 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] + - id: client_id + type: string + brief: > + A unique identifier for the client that consumes or produces a message. + examples: ['client-5', 'myhost@8742@s8083jm'] + - id: destination.name + type: string + brief: 'The message destination name' + note: | + 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'] + - id: destination.template + type: string + brief: Low cardinality representation of the messaging destination name + note: > + 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}'] + - id: destination.anonymous + type: boolean + brief: 'A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name).' + - id: destination.temporary + type: boolean + brief: 'A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed.' + - id: destination_publish.anonymous + type: boolean + brief: 'A boolean that is true if the publish message destination is anonymous (could be unnamed or have auto-generated name).' + - id: destination_publish.name + type: string + brief: 'The name of the original destination the message was published to' + note: | + The name SHOULD uniquely identify a specific queue, topic, or other entity within the broker. If + the broker doesn't have such notion, the original destination name SHOULD uniquely identify the broker. + examples: ['MyQueue', 'MyTopic'] + - id: kafka.consumer.group + type: string + brief: > + Name of the Kafka Consumer Group that is handling the message. + Only applies to consumers, not producers. + examples: 'my-group' + - id: kafka.destination.partition + type: int + brief: > + Partition the message is sent to. + examples: 2 + - id: kafka.message.key + type: string + brief: > + 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. + note: > + 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' + - id: kafka.message.offset + type: int + brief: > + The offset of a record in the corresponding Kafka partition. + examples: 42 + - id: kafka.message.tombstone + type: boolean + brief: 'A boolean that is true if the message is a tombstone.' + - id: message.conversation_id + type: string + brief: > + The conversation ID identifying the conversation to which the message belongs, + represented as a string. Sometimes called "Correlation ID". + examples: 'MyConversationId' + - id: message.envelope.size + type: int + brief: > + The size of the message body and metadata in bytes. + note: | + This can refer to both the compressed or uncompressed size. If both sizes are known, the uncompressed + size should be used. + examples: 2738 + - id: message.id + type: string + brief: 'A value used by the messaging system as an identifier for the message, represented as a string.' + examples: '452a7c7c7c7048c2f887f61572b18fc2' + - id: message.body.size + type: int + brief: > + The size of the message body in bytes. + note: | + 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 + - id: operation + type: + allow_custom_values: true + members: + - id: publish + value: "publish" + brief: > + One or more messages are provided for publishing to an intermediary. + If a single message is published, the context of the "Publish" span can be used as the creation context and no "Create" span needs to be created. + - id: create + value: "create" + brief: > + A message is created. + "Create" spans always refer to a single message and are used to provide a unique creation context for messages in batch publishing scenarios. + - id: receive + value: "receive" + brief: > + One or more messages are requested by a consumer. + This operation refers to pull-based scenarios, where consumers explicitly call methods of messaging SDKs to receive messages. + - id: deliver + value: "deliver" + brief: > + One or more messages are passed to a consumer. + This operation refers to push-based scenarios, where consumer register callbacks which get called by messaging SDKs. + brief: > + A string identifying the kind of messaging operation. + note: If a custom value is used, it MUST be of low cardinality. + - id: rabbitmq.destination.routing_key + type: string + brief: > + RabbitMQ message routing key. + examples: 'myKey' + - id: rocketmq.client_group + type: string + brief: > + Name of the RocketMQ producer/consumer group that is handling the message. The client type is identified by the SpanKind. + examples: 'myConsumerGroup' + - id: rocketmq.consumption_model + type: + allow_custom_values: false + members: + - id: clustering + value: 'clustering' + brief: 'Clustering consumption model' + - id: broadcasting + value: 'broadcasting' + brief: 'Broadcasting consumption model' + brief: > + Model of message consumption. This only applies to consumer spans. + - id: rocketmq.message.delay_time_level + type: int + brief: > + The delay time level for delay message, which determines the message delay time. + examples: 3 + - id: rocketmq.message.delivery_timestamp + type: int + brief: > + The timestamp in milliseconds that the delay message is expected to be delivered to consumer. + examples: 1665987217045 + - id: rocketmq.message.group + type: string + brief: > + 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. + examples: 'myMessageGroup' + - id: rocketmq.message.keys + type: string[] + brief: > + Key(s) of message, another way to mark message besides message id. + examples: ['keyA', 'keyB'] + - id: rocketmq.message.tag + type: string + brief: > + The secondary classifier of message besides topic. + examples: tagA + - id: rocketmq.message.type + type: + allow_custom_values: false + members: + - id: normal + value: 'normal' + brief: "Normal message" + - id: fifo + value: 'fifo' + brief: 'FIFO message' + - id: delay + value: 'delay' + brief: 'Delay message' + - id: transaction + value: 'transaction' + brief: 'Transaction message' + brief: > + Type of message. + - id: rocketmq.namespace + type: string + brief: > + Namespace of RocketMQ resources, resources in different namespaces are individual. + examples: 'myNamespace' + - id: gcp_pubsub.message.ordering_key + type: string + brief: > + The ordering key for a given message. If the attribute is not present, the message does not have an ordering key. + examples: 'ordering_key' + - id: system + brief: > + An identifier for the messaging system being used. See below for a list of well-known identifiers. + type: + allow_custom_values: true + members: + - id: activemq + value: 'activemq' + brief: 'Apache ActiveMQ' + - id: aws_sqs + value: 'aws_sqs' + brief: 'Amazon Simple Queue Service (SQS)' + - id: azure_eventgrid + value: 'azure_eventgrid' + brief: 'Azure Event Grid' + - id: azure_eventhubs + value: 'azure_eventhubs' + brief: 'Azure Event Hubs' + - id: azure_servicebus + value: 'azure_servicebus' + brief: 'Azure Service Bus' + - id: gcp_pubsub + value: 'gcp_pubsub' + brief: 'Google Cloud Pub/Sub' + - id: jms + value: 'jms' + brief: 'Java Message Service' + - id: kafka + value: 'kafka' + brief: 'Apache Kafka' + - id: rabbitmq + value: 'rabbitmq' + brief: 'RabbitMQ' + - id: rocketmq + value: 'rocketmq' + brief: 'Apache RocketMQ' diff --git a/crates/weaver_resolver/data/registry-test-3-extends/README.md b/crates/weaver_resolver/data/registry-test-3-extends/README.md new file mode 100644 index 00000000..e253db9f --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/README.md @@ -0,0 +1 @@ +Test the `extends` directive in attribute_group and metric. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-3-extends/expected-attribute-catalog.json b/crates/weaver_resolver/data/registry-test-3-extends/expected-attribute-catalog.json new file mode 100644 index 00000000..fc216371 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/expected-attribute-catalog.json @@ -0,0 +1,2069 @@ +[ + { + "name": "error.type", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "other", + "value": { + "type": "String", + "value": "_OTHER" + }, + "brief": "A fallback error value to be used when the instrumentation doesn't define a custom value.\n" + } + ] + }, + "brief": "Describes a class of error the operation ended with.\n", + "examples": { + "type": "Strings", + "values": [ + "timeout", + "java.net.UnknownHostException", + "server_certificate_invalid", + "500" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "The `error.type` SHOULD be predictable and SHOULD have low cardinality.\nInstrumentations SHOULD document the list of errors they report.\n\nThe cardinality of `error.type` within one instrumentation library SHOULD be low.\nTelemetry consumers that aggregate data from multiple instrumentation libraries and applications\nshould be prepared for `error.type` to have high cardinality at query time when no\nadditional filters are applied.\n\nIf the operation has completed successfully, instrumentations SHOULD NOT set `error.type`.\n\nIf a specific domain defines its own set of error identifiers (such as HTTP or gRPC status codes),\nit's RECOMMENDED to:\n\n* Use a domain-specific attribute\n* Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not.", + "stability": "Stable" + }, + { + "name": "http.request.body.size", + "type": { + "type": "Int" + }, + "brief": "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.\n", + "examples": { + "type": "Int", + "value": 3495 + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Experimental" + }, + { + "name": "http.request.header", + "type": { + "type": "template[string[]]" + }, + "brief": "HTTP request headers, `` being the normalized HTTP Header name (lowercase), the value being the header values.\n", + "examples": { + "type": "Strings", + "values": [ + "http.request.header.content-type=[\"application/json\"]", + "http.request.header.x-forwarded-for=[\"1.2.3.4\", \"1.2.3.5\"]" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\nThe `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.\nThe 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.\n", + "stability": "Stable" + }, + { + "name": "http.request.method", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "connect", + "value": { + "type": "String", + "value": "CONNECT" + }, + "brief": "CONNECT method." + }, + { + "id": "delete", + "value": { + "type": "String", + "value": "DELETE" + }, + "brief": "DELETE method." + }, + { + "id": "get", + "value": { + "type": "String", + "value": "GET" + }, + "brief": "GET method." + }, + { + "id": "head", + "value": { + "type": "String", + "value": "HEAD" + }, + "brief": "HEAD method." + }, + { + "id": "options", + "value": { + "type": "String", + "value": "OPTIONS" + }, + "brief": "OPTIONS method." + }, + { + "id": "patch", + "value": { + "type": "String", + "value": "PATCH" + }, + "brief": "PATCH method." + }, + { + "id": "post", + "value": { + "type": "String", + "value": "POST" + }, + "brief": "POST method." + }, + { + "id": "put", + "value": { + "type": "String", + "value": "PUT" + }, + "brief": "PUT method." + }, + { + "id": "trace", + "value": { + "type": "String", + "value": "TRACE" + }, + "brief": "TRACE method." + }, + { + "id": "other", + "value": { + "type": "String", + "value": "_OTHER" + }, + "brief": "Any HTTP method that the instrumentation has no prior knowledge of." + } + ] + }, + "brief": "HTTP request method.", + "examples": { + "type": "Strings", + "values": [ + "GET", + "POST", + "HEAD" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "HTTP request method value SHOULD be \"known\" to the instrumentation.\nBy default, this convention defines \"known\" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods)\nand the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html).\n\nIf the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER`.\n\nIf the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override\nthe list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named\nOTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods\n(this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults).\n\nHTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly.\nInstrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent.\nTracing instrumentations that do so, MUST also set `http.request.method_original` to the original value.\n", + "stability": "Stable" + }, + { + "name": "http.request.method_original", + "type": { + "type": "String" + }, + "brief": "Original HTTP method sent by the client in the request line.", + "examples": { + "type": "Strings", + "values": [ + "GeT", + "ACL", + "foo" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "http.request.resend_count", + "type": { + "type": "Int" + }, + "brief": "The ordinal number of request resending attempt (for any reason, including redirects).\n", + "examples": { + "type": "Int", + "value": 3 + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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).\n", + "stability": "Stable" + }, + { + "name": "http.response.body.size", + "type": { + "type": "Int" + }, + "brief": "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.\n", + "examples": { + "type": "Int", + "value": 3495 + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Experimental" + }, + { + "name": "http.response.header", + "type": { + "type": "template[string[]]" + }, + "brief": "HTTP response headers, `` being the normalized HTTP Header name (lowercase), the value being the header values.\n", + "examples": { + "type": "Strings", + "values": [ + "http.response.header.content-type=[\"application/json\"]", + "http.response.header.my-custom-header=[\"abc\", \"def\"]" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\nUsers MAY explicitly configure instrumentations to capture them even though it is not recommended.\nThe 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.\n", + "stability": "Stable" + }, + { + "name": "http.response.status_code", + "type": { + "type": "Int" + }, + "brief": "[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).", + "examples": { + "type": "Ints", + "values": [ + 200 + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "http.route", + "type": { + "type": "String" + }, + "brief": "The matched route, that is, the path template in the format used by the respective server framework.\n", + "examples": { + "type": "Strings", + "values": [ + "/users/:userID?", + "{controller}/{action}/{id?}" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\nSHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one.", + "stability": "Stable" + }, + { + "name": "messaging.batch.message_count", + "type": { + "type": "Int" + }, + "brief": "The number of messages sent, received, or processed in the scope of the batching operation.", + "examples": { + "type": "Ints", + "values": [ + 0, + 1, + 2 + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n" + }, + { + "name": "messaging.client_id", + "type": { + "type": "String" + }, + "brief": "A unique identifier for the client that consumes or produces a message.\n", + "examples": { + "type": "Strings", + "values": [ + "client-5", + "myhost@8742@s8083jm" + ] + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.destination.name", + "type": { + "type": "String" + }, + "brief": "The message destination name", + "examples": { + "type": "Strings", + "values": [ + "MyQueue", + "MyTopic" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If\nthe broker doesn't have such notion, the destination name SHOULD uniquely identify the broker.\n" + }, + { + "name": "messaging.destination.template", + "type": { + "type": "String" + }, + "brief": "Low cardinality representation of the messaging destination name", + "examples": { + "type": "Strings", + "values": [ + "/customers/{customerId}" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n" + }, + { + "name": "messaging.destination.anonymous", + "type": { + "type": "Boolean" + }, + "brief": "A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name).", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.destination.temporary", + "type": { + "type": "Boolean" + }, + "brief": "A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed.", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.destination_publish.anonymous", + "type": { + "type": "Boolean" + }, + "brief": "A boolean that is true if the publish message destination is anonymous (could be unnamed or have auto-generated name).", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.destination_publish.name", + "type": { + "type": "String" + }, + "brief": "The name of the original destination the message was published to", + "examples": { + "type": "Strings", + "values": [ + "MyQueue", + "MyTopic" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "The name SHOULD uniquely identify a specific queue, topic, or other entity within the broker. If\nthe broker doesn't have such notion, the original destination name SHOULD uniquely identify the broker.\n" + }, + { + "name": "messaging.kafka.consumer.group", + "type": { + "type": "String" + }, + "brief": "Name of the Kafka Consumer Group that is handling the message. Only applies to consumers, not producers.\n", + "examples": { + "type": "String", + "value": "my-group" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.kafka.destination.partition", + "type": { + "type": "Int" + }, + "brief": "Partition the message is sent to.\n", + "examples": { + "type": "Int", + "value": 2 + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.kafka.message.key", + "type": { + "type": "String" + }, + "brief": "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.\n", + "examples": { + "type": "String", + "value": "myKey" + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n" + }, + { + "name": "messaging.kafka.message.offset", + "type": { + "type": "Int" + }, + "brief": "The offset of a record in the corresponding Kafka partition.\n", + "examples": { + "type": "Int", + "value": 42 + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.kafka.message.tombstone", + "type": { + "type": "Boolean" + }, + "brief": "A boolean that is true if the message is a tombstone.", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.message.conversation_id", + "type": { + "type": "String" + }, + "brief": "The conversation ID identifying the conversation to which the message belongs, represented as a string. Sometimes called \"Correlation ID\".\n", + "examples": { + "type": "String", + "value": "MyConversationId" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.message.envelope.size", + "type": { + "type": "Int" + }, + "brief": "The size of the message body and metadata in bytes.\n", + "examples": { + "type": "Int", + "value": 2738 + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "This can refer to both the compressed or uncompressed size. If both sizes are known, the uncompressed\nsize should be used.\n" + }, + { + "name": "messaging.message.id", + "type": { + "type": "String" + }, + "brief": "A value used by the messaging system as an identifier for the message, represented as a string.", + "examples": { + "type": "String", + "value": "452a7c7c7c7048c2f887f61572b18fc2" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.message.body.size", + "type": { + "type": "Int" + }, + "brief": "The size of the message body in bytes.\n", + "examples": { + "type": "Int", + "value": 1439 + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "This can refer to both the compressed or uncompressed body size. If both sizes are known, the uncompressed\nbody size should be used.\n" + }, + { + "name": "messaging.operation", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "publish", + "value": { + "type": "String", + "value": "publish" + }, + "brief": "One or more messages are provided for publishing to an intermediary. If a single message is published, the context of the \"Publish\" span can be used as the creation context and no \"Create\" span needs to be created.\n" + }, + { + "id": "create", + "value": { + "type": "String", + "value": "create" + }, + "brief": "A message is created. \"Create\" spans always refer to a single message and are used to provide a unique creation context for messages in batch publishing scenarios.\n" + }, + { + "id": "receive", + "value": { + "type": "String", + "value": "receive" + }, + "brief": "One or more messages are requested by a consumer. This operation refers to pull-based scenarios, where consumers explicitly call methods of messaging SDKs to receive messages.\n" + }, + { + "id": "deliver", + "value": { + "type": "String", + "value": "deliver" + }, + "brief": "One or more messages are passed to a consumer. This operation refers to push-based scenarios, where consumer register callbacks which get called by messaging SDKs.\n" + } + ] + }, + "brief": "A string identifying the kind of messaging operation.\n", + "requirement_level": { + "type": "Recommended" + }, + "note": "If a custom value is used, it MUST be of low cardinality." + }, + { + "name": "messaging.rabbitmq.destination.routing_key", + "type": { + "type": "String" + }, + "brief": "RabbitMQ message routing key.\n", + "examples": { + "type": "String", + "value": "myKey" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.client_group", + "type": { + "type": "String" + }, + "brief": "Name of the RocketMQ producer/consumer group that is handling the message. The client type is identified by the SpanKind.\n", + "examples": { + "type": "String", + "value": "myConsumerGroup" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.consumption_model", + "type": { + "type": "Enum", + "allow_custom_values": false, + "members": [ + { + "id": "clustering", + "value": { + "type": "String", + "value": "clustering" + }, + "brief": "Clustering consumption model" + }, + { + "id": "broadcasting", + "value": { + "type": "String", + "value": "broadcasting" + }, + "brief": "Broadcasting consumption model" + } + ] + }, + "brief": "Model of message consumption. This only applies to consumer spans.\n", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.delay_time_level", + "type": { + "type": "Int" + }, + "brief": "The delay time level for delay message, which determines the message delay time.\n", + "examples": { + "type": "Int", + "value": 3 + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.delivery_timestamp", + "type": { + "type": "Int" + }, + "brief": "The timestamp in milliseconds that the delay message is expected to be delivered to consumer.\n", + "examples": { + "type": "Int", + "value": 1665987217045 + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.group", + "type": { + "type": "String" + }, + "brief": "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.\n", + "examples": { + "type": "String", + "value": "myMessageGroup" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.keys", + "type": { + "type": "Strings" + }, + "brief": "Key(s) of message, another way to mark message besides message id.\n", + "examples": { + "type": "Strings", + "values": [ + "keyA", + "keyB" + ] + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.tag", + "type": { + "type": "String" + }, + "brief": "The secondary classifier of message besides topic.\n", + "examples": { + "type": "String", + "value": "tagA" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.message.type", + "type": { + "type": "Enum", + "allow_custom_values": false, + "members": [ + { + "id": "normal", + "value": { + "type": "String", + "value": "normal" + }, + "brief": "Normal message" + }, + { + "id": "fifo", + "value": { + "type": "String", + "value": "fifo" + }, + "brief": "FIFO message" + }, + { + "id": "delay", + "value": { + "type": "String", + "value": "delay" + }, + "brief": "Delay message" + }, + { + "id": "transaction", + "value": { + "type": "String", + "value": "transaction" + }, + "brief": "Transaction message" + } + ] + }, + "brief": "Type of message.\n", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.rocketmq.namespace", + "type": { + "type": "String" + }, + "brief": "Namespace of RocketMQ resources, resources in different namespaces are individual.\n", + "examples": { + "type": "String", + "value": "myNamespace" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.gcp_pubsub.message.ordering_key", + "type": { + "type": "String" + }, + "brief": "The ordering key for a given message. If the attribute is not present, the message does not have an ordering key.\n", + "examples": { + "type": "String", + "value": "ordering_key" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "messaging.system", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "activemq", + "value": { + "type": "String", + "value": "activemq" + }, + "brief": "Apache ActiveMQ" + }, + { + "id": "aws_sqs", + "value": { + "type": "String", + "value": "aws_sqs" + }, + "brief": "Amazon Simple Queue Service (SQS)" + }, + { + "id": "azure_eventgrid", + "value": { + "type": "String", + "value": "azure_eventgrid" + }, + "brief": "Azure Event Grid" + }, + { + "id": "azure_eventhubs", + "value": { + "type": "String", + "value": "azure_eventhubs" + }, + "brief": "Azure Event Hubs" + }, + { + "id": "azure_servicebus", + "value": { + "type": "String", + "value": "azure_servicebus" + }, + "brief": "Azure Service Bus" + }, + { + "id": "gcp_pubsub", + "value": { + "type": "String", + "value": "gcp_pubsub" + }, + "brief": "Google Cloud Pub/Sub" + }, + { + "id": "jms", + "value": { + "type": "String", + "value": "jms" + }, + "brief": "Java Message Service" + }, + { + "id": "kafka", + "value": { + "type": "String", + "value": "kafka" + }, + "brief": "Apache Kafka" + }, + { + "id": "rabbitmq", + "value": { + "type": "String", + "value": "rabbitmq" + }, + "brief": "RabbitMQ" + }, + { + "id": "rocketmq", + "value": { + "type": "String", + "value": "rocketmq" + }, + "brief": "Apache RocketMQ" + } + ] + }, + "brief": "An identifier for the messaging system being used. See below for a list of well-known identifiers.\n", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.carrier.icc", + "type": { + "type": "String" + }, + "brief": "The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network.", + "examples": { + "type": "String", + "value": "DE" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.carrier.mcc", + "type": { + "type": "String" + }, + "brief": "The mobile carrier country code.", + "examples": { + "type": "String", + "value": "310" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.carrier.mnc", + "type": { + "type": "String" + }, + "brief": "The mobile carrier network code.", + "examples": { + "type": "String", + "value": "001" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.carrier.name", + "type": { + "type": "String" + }, + "brief": "The name of the mobile carrier.", + "examples": { + "type": "String", + "value": "sprint" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.connection.subtype", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "gprs", + "value": { + "type": "String", + "value": "gprs" + }, + "brief": "GPRS" + }, + { + "id": "edge", + "value": { + "type": "String", + "value": "edge" + }, + "brief": "EDGE" + }, + { + "id": "umts", + "value": { + "type": "String", + "value": "umts" + }, + "brief": "UMTS" + }, + { + "id": "cdma", + "value": { + "type": "String", + "value": "cdma" + }, + "brief": "CDMA" + }, + { + "id": "evdo_0", + "value": { + "type": "String", + "value": "evdo_0" + }, + "brief": "EVDO Rel. 0" + }, + { + "id": "evdo_a", + "value": { + "type": "String", + "value": "evdo_a" + }, + "brief": "EVDO Rev. A" + }, + { + "id": "cdma2000_1xrtt", + "value": { + "type": "String", + "value": "cdma2000_1xrtt" + }, + "brief": "CDMA2000 1XRTT" + }, + { + "id": "hsdpa", + "value": { + "type": "String", + "value": "hsdpa" + }, + "brief": "HSDPA" + }, + { + "id": "hsupa", + "value": { + "type": "String", + "value": "hsupa" + }, + "brief": "HSUPA" + }, + { + "id": "hspa", + "value": { + "type": "String", + "value": "hspa" + }, + "brief": "HSPA" + }, + { + "id": "iden", + "value": { + "type": "String", + "value": "iden" + }, + "brief": "IDEN" + }, + { + "id": "evdo_b", + "value": { + "type": "String", + "value": "evdo_b" + }, + "brief": "EVDO Rev. B" + }, + { + "id": "lte", + "value": { + "type": "String", + "value": "lte" + }, + "brief": "LTE" + }, + { + "id": "ehrpd", + "value": { + "type": "String", + "value": "ehrpd" + }, + "brief": "EHRPD" + }, + { + "id": "hspap", + "value": { + "type": "String", + "value": "hspap" + }, + "brief": "HSPAP" + }, + { + "id": "gsm", + "value": { + "type": "String", + "value": "gsm" + }, + "brief": "GSM" + }, + { + "id": "td_scdma", + "value": { + "type": "String", + "value": "td_scdma" + }, + "brief": "TD-SCDMA" + }, + { + "id": "iwlan", + "value": { + "type": "String", + "value": "iwlan" + }, + "brief": "IWLAN" + }, + { + "id": "nr", + "value": { + "type": "String", + "value": "nr" + }, + "brief": "5G NR (New Radio)" + }, + { + "id": "nrnsa", + "value": { + "type": "String", + "value": "nrnsa" + }, + "brief": "5G NRNSA (New Radio Non-Standalone)" + }, + { + "id": "lte_ca", + "value": { + "type": "String", + "value": "lte_ca" + }, + "brief": "LTE CA" + } + ] + }, + "brief": "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.", + "examples": { + "type": "String", + "value": "LTE" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.connection.type", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "wifi", + "value": { + "type": "String", + "value": "wifi" + } + }, + { + "id": "wired", + "value": { + "type": "String", + "value": "wired" + } + }, + { + "id": "cell", + "value": { + "type": "String", + "value": "cell" + } + }, + { + "id": "unavailable", + "value": { + "type": "String", + "value": "unavailable" + } + }, + { + "id": "unknown", + "value": { + "type": "String", + "value": "unknown" + } + } + ] + }, + "brief": "The internet connection type.", + "examples": { + "type": "String", + "value": "wifi" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.local.address", + "type": { + "type": "String" + }, + "brief": "Local address of the network connection - IP address or Unix domain socket name.", + "examples": { + "type": "Strings", + "values": [ + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "network.local.port", + "type": { + "type": "Int" + }, + "brief": "Local port number of the network connection.", + "examples": { + "type": "Ints", + "values": [ + 65123 + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "network.peer.address", + "type": { + "type": "String" + }, + "brief": "Peer address of the network connection - IP address or Unix domain socket name.", + "examples": { + "type": "Strings", + "values": [ + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "network.peer.port", + "type": { + "type": "Int" + }, + "brief": "Peer port number of the network connection.", + "examples": { + "type": "Ints", + "values": [ + 65123 + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "network.protocol.name", + "type": { + "type": "String" + }, + "brief": "[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.", + "examples": { + "type": "Strings", + "values": [ + "amqp", + "http", + "mqtt" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "The value SHOULD be normalized to lowercase.", + "stability": "Stable" + }, + { + "name": "network.protocol.version", + "type": { + "type": "String" + }, + "brief": "Version of the protocol specified in `network.protocol.name`.", + "examples": { + "type": "String", + "value": "3.1.1" + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "`network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`.\n", + "stability": "Stable" + }, + { + "name": "network.transport", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "tcp", + "value": { + "type": "String", + "value": "tcp" + }, + "brief": "TCP" + }, + { + "id": "udp", + "value": { + "type": "String", + "value": "udp" + }, + "brief": "UDP" + }, + { + "id": "pipe", + "value": { + "type": "String", + "value": "pipe" + }, + "brief": "Named or anonymous pipe." + }, + { + "id": "unix", + "value": { + "type": "String", + "value": "unix" + }, + "brief": "Unix domain socket" + } + ] + }, + "brief": "[OSI transport layer](https://osi-model.com/transport-layer/) or [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication).\n", + "examples": { + "type": "Strings", + "values": [ + "tcp", + "udp" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "The value SHOULD be normalized to lowercase.\n\nConsider always setting the transport when setting a port number, since\na port number is ambiguous without knowing the transport. For example\ndifferent processes could be listening on TCP port 12345 and UDP port 12345.\n", + "stability": "Stable" + }, + { + "name": "network.type", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "ipv4", + "value": { + "type": "String", + "value": "ipv4" + }, + "brief": "IPv4" + }, + { + "id": "ipv6", + "value": { + "type": "String", + "value": "ipv6" + }, + "brief": "IPv6" + } + ] + }, + "brief": "[OSI network layer](https://osi-model.com/network-layer/) or non-OSI equivalent.", + "examples": { + "type": "Strings", + "values": [ + "ipv4", + "ipv6" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "The value SHOULD be normalized to lowercase.", + "stability": "Stable" + }, + { + "name": "network.io.direction", + "type": { + "type": "Enum", + "allow_custom_values": false, + "members": [ + { + "id": "transmit", + "value": { + "type": "String", + "value": "transmit" + } + }, + { + "id": "receive", + "value": { + "type": "String", + "value": "receive" + } + } + ] + }, + "brief": "The network IO operation direction.", + "examples": { + "type": "Strings", + "values": [ + "transmit" + ] + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "server.address", + "type": { + "type": "String" + }, + "brief": "Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.", + "examples": { + "type": "Strings", + "values": [ + "example.com", + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n", + "stability": "Stable" + }, + { + "name": "server.port", + "type": { + "type": "Int" + }, + "brief": "Server port number.", + "examples": { + "type": "Ints", + "values": [ + 80, + 8080, + 443 + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n", + "stability": "Stable" + }, + { + "name": "url.scheme", + "type": { + "type": "String" + }, + "brief": "The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol.", + "examples": { + "type": "Strings", + "values": [ + "https", + "ftp", + "telnet" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "url.full", + "type": { + "type": "String" + }, + "brief": "Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986)", + "examples": { + "type": "Strings", + "values": [ + "https://www.foo.bar/search?q=OpenTelemetry#SemConv", + "//localhost" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n`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/`.\n`url.full` SHOULD capture the absolute URL when it is available (or can be reconstructed) and SHOULD NOT be validated or modified except for sanitizing purposes.\n", + "stability": "Stable" + }, + { + "name": "url.path", + "type": { + "type": "String" + }, + "brief": "The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component", + "examples": { + "type": "Strings", + "values": [ + "/search" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "url.query", + "type": { + "type": "String" + }, + "brief": "The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component", + "examples": { + "type": "Strings", + "values": [ + "q=OpenTelemetry" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it.", + "stability": "Stable" + }, + { + "name": "url.fragment", + "type": { + "type": "String" + }, + "brief": "The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component", + "examples": { + "type": "Strings", + "values": [ + "SemConv" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "http.request.method", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "connect", + "value": { + "type": "String", + "value": "CONNECT" + }, + "brief": "CONNECT method." + }, + { + "id": "delete", + "value": { + "type": "String", + "value": "DELETE" + }, + "brief": "DELETE method." + }, + { + "id": "get", + "value": { + "type": "String", + "value": "GET" + }, + "brief": "GET method." + }, + { + "id": "head", + "value": { + "type": "String", + "value": "HEAD" + }, + "brief": "HEAD method." + }, + { + "id": "options", + "value": { + "type": "String", + "value": "OPTIONS" + }, + "brief": "OPTIONS method." + }, + { + "id": "patch", + "value": { + "type": "String", + "value": "PATCH" + }, + "brief": "PATCH method." + }, + { + "id": "post", + "value": { + "type": "String", + "value": "POST" + }, + "brief": "POST method." + }, + { + "id": "put", + "value": { + "type": "String", + "value": "PUT" + }, + "brief": "PUT method." + }, + { + "id": "trace", + "value": { + "type": "String", + "value": "TRACE" + }, + "brief": "TRACE method." + }, + { + "id": "other", + "value": { + "type": "String", + "value": "_OTHER" + }, + "brief": "Any HTTP method that the instrumentation has no prior knowledge of." + } + ] + }, + "brief": "HTTP request method.", + "examples": { + "type": "Strings", + "values": [ + "GET", + "POST", + "HEAD" + ] + }, + "requirement_level": { + "type": "Required" + }, + "note": "HTTP request method value SHOULD be \"known\" to the instrumentation.\nBy default, this convention defines \"known\" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods)\nand the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html).\n\nIf the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER`.\n\nIf the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override\nthe list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named\nOTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods\n(this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults).\n\nHTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly.\nInstrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent.\nTracing instrumentations that do so, MUST also set `http.request.method_original` to the original value.\n", + "stability": "Stable" + }, + { + "name": "http.response.status_code", + "type": { + "type": "Int" + }, + "brief": "[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).", + "examples": { + "type": "Ints", + "values": [ + 200 + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "If and only if one was received/sent." + }, + "stability": "Stable" + }, + { + "name": "error.type", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "other", + "value": { + "type": "String", + "value": "_OTHER" + }, + "brief": "A fallback error value to be used when the instrumentation doesn't define a custom value.\n" + } + ] + }, + "brief": "Describes a class of error the operation ended with.\n", + "examples": { + "type": "Strings", + "values": [ + "timeout", + "java.net.UnknownHostException", + "server_certificate_invalid", + "500" + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "If request has ended with an error." + }, + "note": "If the request fails with an error before response status code was sent or received,\n`error.type` SHOULD be set to exception type (its fully-qualified class name, if applicable)\nor a component-specific low cardinality error identifier.\n\nIf response status code was sent or received and status indicates an error according to [HTTP span status definition](/docs/http/http-spans.md),\n`error.type` SHOULD be set to the status code number (represented as a string), an exception type (if thrown) or a component-specific error identifier.\n\nThe `error.type` value SHOULD be predictable and SHOULD have low cardinality.\nInstrumentations SHOULD document the list of errors they report.\n\nThe cardinality of `error.type` within one instrumentation library SHOULD be low, but\ntelemetry consumers that aggregate data from multiple instrumentation libraries and applications\nshould be prepared for `error.type` to have high cardinality at query time, when no\nadditional filters are applied.\n\nIf the request has completed successfully, instrumentations SHOULD NOT set `error.type`.\n", + "stability": "Stable" + }, + { + "name": "network.protocol.name", + "type": { + "type": "String" + }, + "brief": "[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.", + "examples": { + "type": "Strings", + "values": [ + "http", + "spdy" + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "If not `http` and `network.protocol.version` is set." + }, + "note": "The value SHOULD be normalized to lowercase.", + "stability": "Stable" + }, + { + "name": "network.protocol.version", + "type": { + "type": "String" + }, + "brief": "Version of the protocol specified in `network.protocol.name`.", + "examples": { + "type": "Strings", + "values": [ + "1.0", + "1.1", + "2", + "3" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "`network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`.\n", + "stability": "Stable" + }, + { + "name": "server.address", + "type": { + "type": "String" + }, + "brief": "Host identifier of the [\"URI origin\"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to.\n", + "examples": { + "type": "Strings", + "values": [ + "example.com", + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "requirement_level": { + "type": "Required" + }, + "note": "If an HTTP client request is explicitly made to an IP address, e.g. `http://x.x.x.x:8080`, then `server.address` SHOULD be the IP address `x.x.x.x`. A DNS lookup SHOULD NOT be used.\n", + "stability": "Stable" + }, + { + "name": "server.port", + "type": { + "type": "Int" + }, + "brief": "Port identifier of the [\"URI origin\"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to.\n", + "examples": { + "type": "Ints", + "values": [ + 80, + 8080, + 443 + ] + }, + "requirement_level": { + "type": "Required" + }, + "note": "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.\n", + "stability": "Stable" + }, + { + "name": "url.scheme", + "type": { + "type": "String" + }, + "brief": "The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol.", + "examples": { + "type": "Strings", + "values": [ + "http", + "https" + ] + }, + "requirement_level": { + "type": "OptIn" + }, + "stability": "Stable" + }, + { + "name": "http.route", + "type": { + "type": "String" + }, + "brief": "The matched route, that is, the path template in the format used by the respective server framework.\n", + "examples": { + "type": "Strings", + "values": [ + "/users/:userID?", + "{controller}/{action}/{id?}" + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "If and only if it's available" + }, + "note": "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.\nSHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one.", + "stability": "Stable" + }, + { + "name": "server.address", + "type": { + "type": "String" + }, + "brief": "Name of the local HTTP server that received the request.\n", + "examples": { + "type": "Strings", + "values": [ + "example.com", + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes).\n", + "stability": "Stable" + }, + { + "name": "server.port", + "type": { + "type": "Int" + }, + "brief": "Port of the local HTTP server that received the request.\n", + "examples": { + "type": "Ints", + "values": [ + 80, + 8080, + 443 + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "If `server.address` is set." + }, + "note": "See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes).\n", + "stability": "Stable" + }, + { + "name": "url.scheme", + "type": { + "type": "String" + }, + "brief": "The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol.", + "examples": { + "type": "Strings", + "values": [ + "http", + "https" + ] + }, + "requirement_level": { + "type": "Required" + }, + "note": "The scheme of the original client request, if known (e.g. from [Forwarded#proto](https://developer.mozilla.org/docs/Web/HTTP/Headers/Forwarded#proto), [X-Forwarded-Proto](https://developer.mozilla.org/docs/Web/HTTP/Headers/X-Forwarded-Proto), or a similar header). Otherwise, the scheme of the immediate peer request.", + "stability": "Stable" + }, + { + "name": "messaging.system", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "activemq", + "value": { + "type": "String", + "value": "activemq" + }, + "brief": "Apache ActiveMQ" + }, + { + "id": "aws_sqs", + "value": { + "type": "String", + "value": "aws_sqs" + }, + "brief": "Amazon Simple Queue Service (SQS)" + }, + { + "id": "azure_eventgrid", + "value": { + "type": "String", + "value": "azure_eventgrid" + }, + "brief": "Azure Event Grid" + }, + { + "id": "azure_eventhubs", + "value": { + "type": "String", + "value": "azure_eventhubs" + }, + "brief": "Azure Event Hubs" + }, + { + "id": "azure_servicebus", + "value": { + "type": "String", + "value": "azure_servicebus" + }, + "brief": "Azure Service Bus" + }, + { + "id": "gcp_pubsub", + "value": { + "type": "String", + "value": "gcp_pubsub" + }, + "brief": "Google Cloud Pub/Sub" + }, + { + "id": "jms", + "value": { + "type": "String", + "value": "jms" + }, + "brief": "Java Message Service" + }, + { + "id": "kafka", + "value": { + "type": "String", + "value": "kafka" + }, + "brief": "Apache Kafka" + }, + { + "id": "rabbitmq", + "value": { + "type": "String", + "value": "rabbitmq" + }, + "brief": "RabbitMQ" + }, + { + "id": "rocketmq", + "value": { + "type": "String", + "value": "rocketmq" + }, + "brief": "Apache RocketMQ" + } + ] + }, + "brief": "An identifier for the messaging system being used. See below for a list of well-known identifiers.\n", + "requirement_level": { + "type": "Required" + } + }, + { + "name": "error.type", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "other", + "value": { + "type": "String", + "value": "_OTHER" + }, + "brief": "A fallback error value to be used when the instrumentation doesn't define a custom value.\n" + } + ] + }, + "brief": "Describes a class of error the operation ended with.\n", + "examples": { + "type": "Strings", + "values": [ + "amqp:decode-error", + "KAFKA_STORAGE_ERROR", + "channel-error" + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "If and only if the messaging operation has failed." + }, + "note": "The `error.type` SHOULD be predictable and SHOULD have low cardinality.\nInstrumentations SHOULD document the list of errors they report.\n\nThe cardinality of `error.type` within one instrumentation library SHOULD be low.\nTelemetry consumers that aggregate data from multiple instrumentation libraries and applications\nshould be prepared for `error.type` to have high cardinality at query time when no\nadditional filters are applied.\n\nIf the operation has completed successfully, instrumentations SHOULD NOT set `error.type`.\n\nIf a specific domain defines its own set of error identifiers (such as HTTP or gRPC status codes),\nit's RECOMMENDED to:\n\n* Use a domain-specific attribute\n* Set `error.type` to capture all errors, regardless of whether they are defined within the domain-specific set or not.", + "stability": "Stable" + }, + { + "name": "server.address", + "type": { + "type": "String" + }, + "brief": "Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.", + "examples": { + "type": "Strings", + "values": [ + "example.com", + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "If available." + }, + "note": "This should be the IP/hostname of the broker (or other network-level peer) this specific message is sent to/received from.\n", + "stability": "Stable" + }, + { + "name": "network.protocol.name", + "type": { + "type": "String" + }, + "brief": "[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.", + "examples": { + "type": "Strings", + "values": [ + "amqp", + "mqtt" + ] + }, + "tag": "connection-level", + "requirement_level": { + "type": "Recommended" + }, + "note": "The value SHOULD be normalized to lowercase.", + "stability": "Stable" + }, + { + "name": "network.protocol.version", + "type": { + "type": "String" + }, + "brief": "Version of the protocol specified in `network.protocol.name`.", + "examples": { + "type": "String", + "value": "3.1.1" + }, + "tag": "connection-level", + "requirement_level": { + "type": "Recommended" + }, + "note": "`network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`.\n", + "stability": "Stable" + }, + { + "name": "messaging.destination.name", + "type": { + "type": "String" + }, + "brief": "The message destination name", + "examples": { + "type": "Strings", + "values": [ + "MyQueue", + "MyTopic" + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "if and only if `messaging.destination.name` is known to have low cardinality. Otherwise, `messaging.destination.template` MAY be populated." + }, + "note": "Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If\nthe broker doesn't have such notion, the destination name SHOULD uniquely identify the broker.\n" + }, + { + "name": "messaging.destination.template", + "type": { + "type": "String" + }, + "brief": "Low cardinality representation of the messaging destination name", + "examples": { + "type": "Strings", + "values": [ + "/customers/{customerId}" + ] + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "if available." + }, + "note": "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.\n" + } +] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-3-extends/expected-registry.json b/crates/weaver_resolver/data/registry-test-3-extends/expected-registry.json new file mode 100644 index 00000000..53995344 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/expected-registry.json @@ -0,0 +1,1330 @@ +{ + "registry_url": "https://semconv-registry.com", + "groups": [ + { + "id": "attributes.http.common", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Describes HTTP attributes.", + "attributes": [ + 62, + 63, + 64, + 65, + 66 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/http-common.yaml", + "attributes": { + "62": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.http" + } + }, + "63": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.http" + } + }, + "64": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.error" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.error" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.error" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.error" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.error" + } + }, + "65": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.network" + } + }, + "66": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.network" + } + } + } + } + }, + { + "id": "attributes.http.client", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "HTTP Client attributes", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/http-common.yaml", + "attributes": { + "62": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "attributes.http.common" + } + }, + "63": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "attributes.http.common" + } + }, + "64": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "attributes.http.common" + } + }, + "65": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "attributes.http.common" + } + }, + "66": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "attributes.http.common" + } + }, + "67": { + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "server" + } + }, + "68": { + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "server" + } + }, + "69": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.url" + } + } + } + } + }, + { + "id": "attributes.http.server", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "HTTP Server attributes", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 70, + 71, + 72, + 73 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/http-common.yaml", + "attributes": { + "62": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "attributes.http.common" + } + }, + "63": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "attributes.http.common" + } + }, + "64": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "attributes.http.common" + } + }, + "65": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "attributes.http.common" + } + }, + "66": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "attributes.http.common" + } + }, + "70": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.http" + } + }, + "71": { + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "server" + } + }, + "72": { + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "server" + } + }, + "73": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.url" + } + } + } + } + }, + { + "id": "messaging.attributes.common", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Common messaging attributes.", + "prefix": "messaging", + "attributes": [ + 56, + 74, + 75, + 76, + 77, + 78 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/messaging-common.yaml", + "attributes": { + "56": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "server" + } + }, + "74": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + } + }, + "75": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.error" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.error" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.error" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.error" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.error" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.error" + } + }, + "76": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "server" + } + }, + "77": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.network" + } + }, + "78": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.network" + } + } + } + } + }, + { + "id": "metric.messaging.attributes", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Common messaging metrics attributes.", + "attributes": [ + 56, + 74, + 75, + 76, + 77, + 78, + 79, + 80 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/metrics-messaging.yaml", + "attributes": { + "56": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "messaging.attributes.common" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "messaging.attributes.common" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "messaging.attributes.common" + } + }, + "76": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "messaging.attributes.common" + } + }, + "77": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "messaging.attributes.common" + } + }, + "78": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "messaging.attributes.common" + } + }, + "79": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + } + }, + "80": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.messaging" + } + } + } + } + }, + { + "id": "metric.messaging.publish.duration", + "typed_group": { + "type": "Metric", + "metric_name": "messaging.publish.duration", + "instrument": "Histogram", + "unit": "s" + }, + "brief": "Measures the duration of publish operation.", + "attributes": [ + 56, + 74, + 75, + 76, + 77, + 78, + 79, + 80 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/metrics-messaging.yaml", + "attributes": { + "56": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "76": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "77": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "78": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "79": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "80": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + } + } + } + }, + { + "id": "metric.messaging.receive.duration", + "typed_group": { + "type": "Metric", + "metric_name": "messaging.receive.duration", + "instrument": "Histogram", + "unit": "s" + }, + "brief": "Measures the duration of receive operation.", + "attributes": [ + 56, + 74, + 75, + 76, + 77, + 78, + 79, + 80 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/metrics-messaging.yaml", + "attributes": { + "56": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "76": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "77": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "78": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "79": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "80": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + } + } + } + }, + { + "id": "metric.messaging.deliver.duration", + "typed_group": { + "type": "Metric", + "metric_name": "messaging.deliver.duration", + "instrument": "Histogram", + "unit": "s" + }, + "brief": "Measures the duration of deliver operation.", + "attributes": [ + 56, + 74, + 75, + 76, + 77, + 78, + 79, + 80 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/metrics-messaging.yaml", + "attributes": { + "56": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "76": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "77": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "78": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "79": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "80": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + } + } + } + }, + { + "id": "metric.messaging.publish.messages", + "typed_group": { + "type": "Metric", + "metric_name": "messaging.publish.messages", + "instrument": "Counter", + "unit": "{message}" + }, + "brief": "Measures the number of published messages.", + "attributes": [ + 56, + 74, + 75, + 76, + 77, + 78, + 79, + 80 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/metrics-messaging.yaml", + "attributes": { + "56": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "76": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "77": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "78": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "79": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "80": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + } + } + } + }, + { + "id": "metric.messaging.receive.messages", + "typed_group": { + "type": "Metric", + "metric_name": "messaging.receive.messages", + "instrument": "Counter", + "unit": "{message}" + }, + "brief": "Measures the number of received messages.", + "attributes": [ + 56, + 74, + 75, + 76, + 77, + 78, + 79, + 80 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/metrics-messaging.yaml", + "attributes": { + "56": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "76": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "77": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "78": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "79": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "80": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + } + } + } + }, + { + "id": "metric.messaging.deliver.messages", + "typed_group": { + "type": "Metric", + "metric_name": "messaging.deliver.messages", + "instrument": "Counter", + "unit": "{message}" + }, + "brief": "Measures the number of delivered messages.", + "attributes": [ + 56, + 74, + 75, + 76, + 77, + 78, + 79, + 80 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/metrics-messaging.yaml", + "attributes": { + "56": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "76": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "77": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "78": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "79": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + }, + "80": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "metric.messaging.attributes" + } + } + } + } + }, + { + "id": "registry.error", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "This document defines the shared attributes used to report an error.\n", + "prefix": "error", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/registry-error.yaml" + } + }, + { + "id": "registry.http", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "This document defines semantic convention attributes in the HTTP namespace.", + "prefix": "http", + "attributes": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/registry-http.yaml" + } + }, + { + "id": "registry.messaging", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Attributes describing telemetry around messaging systems and messaging activities.", + "prefix": "messaging", + "attributes": [ + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/registry-messaging.yaml" + } + }, + { + "id": "registry.network", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "These attributes may be used for any network related operation.\n", + "prefix": "network", + "attributes": [ + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/registry-network.yaml" + } + }, + { + "id": "server", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "These attributes may be used to describe the server in a connection-based network interaction where there is one side that initiates the connection (the client is the side that initiates the connection). This covers all TCP network interactions since TCP is connection-based and one side initiates the connection (an exception is made for peer-to-peer communication over TCP where the \"user-facing\" surface of the protocol / API doesn't expose a clear notion of client and server). This also covers UDP network interactions where one side initiates the interaction, e.g. QUIC (HTTP/3) and DNS.\n", + "prefix": "server", + "attributes": [ + 55, + 56 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/registry-server.yaml" + } + }, + { + "id": "registry.url", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Attributes describing URL.", + "prefix": "url", + "attributes": [ + 57, + 58, + 59, + 60, + 61 + ], + "lineage": { + "provenance": "data/registry-test-3-extends/registry/registry-url.yaml" + } + } + ] +} \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-3-extends/registry/http-common.yaml b/crates/weaver_resolver/data/registry-test-3-extends/registry/http-common.yaml new file mode 100644 index 00000000..f175215f --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/registry/http-common.yaml @@ -0,0 +1,87 @@ +groups: + - id: attributes.http.common + type: attribute_group + brief: "Describes HTTP attributes." + attributes: + - ref: http.request.method + requirement_level: required + - ref: http.response.status_code + requirement_level: + conditionally_required: If and only if one was received/sent. + - ref: error.type + requirement_level: + conditionally_required: If request has ended with an error. + examples: ['timeout', 'java.net.UnknownHostException', 'server_certificate_invalid', '500'] + note: | + If the request fails with an error before response status code was sent or received, + `error.type` SHOULD be set to exception type (its fully-qualified class name, if applicable) + or a component-specific low cardinality error identifier. + + If response status code was sent or received and status indicates an error according to [HTTP span status definition](/docs/http/http-spans.md), + `error.type` SHOULD be set to the status code number (represented as a string), an exception type (if thrown) or a component-specific error identifier. + + The `error.type` value SHOULD be predictable and SHOULD have low cardinality. + Instrumentations SHOULD document the list of errors they report. + + The cardinality of `error.type` within one instrumentation library SHOULD be low, but + 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 request has completed successfully, instrumentations SHOULD NOT set `error.type`. + - ref: network.protocol.name + examples: ['http', 'spdy'] + requirement_level: + conditionally_required: If not `http` and `network.protocol.version` is set. + - ref: network.protocol.version + examples: ['1.0', '1.1', '2', '3'] + + - id: attributes.http.client + type: attribute_group + brief: 'HTTP Client attributes' + extends: attributes.http.common + attributes: + - ref: server.address + requirement_level: required + brief: > + Host identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. + note: > + If an HTTP client request is explicitly made to an IP address, e.g. `http://x.x.x.x:8080`, then + `server.address` SHOULD be the IP address `x.x.x.x`. A DNS lookup SHOULD NOT be used. + - ref: server.port + requirement_level: required + brief: > + Port identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. + - ref: url.scheme + requirement_level: opt_in + examples: ["http", "https"] + + - id: attributes.http.server + type: attribute_group + brief: 'HTTP Server attributes' + extends: attributes.http.common + attributes: + - ref: http.route + requirement_level: + conditionally_required: If and only if it's available + - ref: server.address + brief: > + Name of the local HTTP server that received the request. + note: > + See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes). + - ref: server.port + brief: > + Port of the local HTTP server that received the request. + note: > + See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes). + requirement_level: + conditionally_required: If `server.address` is set. + - ref: url.scheme + requirement_level: required + examples: ["http", "https"] + note: > + The scheme of the original client request, if known + (e.g. from [Forwarded#proto](https://developer.mozilla.org/docs/Web/HTTP/Headers/Forwarded#proto), + [X-Forwarded-Proto](https://developer.mozilla.org/docs/Web/HTTP/Headers/X-Forwarded-Proto), + or a similar header). + Otherwise, the scheme of the immediate peer request. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-3-extends/registry/messaging-common.yaml b/crates/weaver_resolver/data/registry-test-3-extends/registry/messaging-common.yaml new file mode 100644 index 00000000..d44d1042 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/registry/messaging-common.yaml @@ -0,0 +1,23 @@ +groups: + - id: messaging.attributes.common + type: attribute_group + brief: "Common messaging attributes." + prefix: messaging + attributes: + - ref: messaging.system + requirement_level: required + - ref: error.type + examples: ['amqp:decode-error', 'KAFKA_STORAGE_ERROR', 'channel-error'] + requirement_level: + conditionally_required: If and only if the messaging operation has failed. + - ref: server.address + note: > + This should be the IP/hostname of the broker (or other network-level peer) this specific message is sent to/received from. + requirement_level: + conditionally_required: If available. + - ref: server.port + - ref: network.protocol.name + examples: ['amqp', 'mqtt'] + tag: connection-level + - ref: network.protocol.version + tag: connection-level \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-3-extends/registry/metrics-messaging.yaml b/crates/weaver_resolver/data/registry-test-3-extends/registry/metrics-messaging.yaml new file mode 100644 index 00000000..2d2f2a29 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/registry/metrics-messaging.yaml @@ -0,0 +1,62 @@ +groups: + - id: metric.messaging.attributes + type: attribute_group + brief: "Common messaging metrics attributes." + extends: messaging.attributes.common + attributes: + - ref: messaging.destination.name + requirement_level: + conditionally_required: if and only if `messaging.destination.name` is known to have low cardinality. Otherwise, `messaging.destination.template` MAY be populated. + - ref: messaging.destination.template + requirement_level: + conditionally_required: if available. + + # durations + - id: metric.messaging.publish.duration + type: metric + metric_name: messaging.publish.duration + brief: "Measures the duration of publish operation." + instrument: histogram + unit: "s" + extends: metric.messaging.attributes + + - id: metric.messaging.receive.duration + type: metric + metric_name: messaging.receive.duration + brief: "Measures the duration of receive operation." + instrument: histogram + unit: "s" + extends: metric.messaging.attributes + + - id: metric.messaging.deliver.duration + type: metric + metric_name: messaging.deliver.duration + brief: "Measures the duration of deliver operation." + instrument: histogram + unit: "s" + extends: metric.messaging.attributes + + # counters + - id: metric.messaging.publish.messages + type: metric + metric_name: messaging.publish.messages + brief: "Measures the number of published messages." + instrument: counter + unit: "{message}" + extends: metric.messaging.attributes + + - id: metric.messaging.receive.messages + type: metric + metric_name: messaging.receive.messages + brief: "Measures the number of received messages." + instrument: counter + unit: "{message}" + extends: metric.messaging.attributes + + - id: metric.messaging.deliver.messages + type: metric + metric_name: messaging.deliver.messages + brief: "Measures the number of delivered messages." + instrument: counter + unit: "{message}" + extends: metric.messaging.attributes diff --git a/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-error.yaml b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-error.yaml new file mode 100644 index 00000000..6831a7a5 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-error.yaml @@ -0,0 +1,35 @@ +groups: + - id: registry.error + type: attribute_group + prefix: error + brief: > + This document defines the shared attributes used to report an error. + attributes: + - id: type + stability: stable + brief: > + Describes a class of error the operation ended with. + type: + allow_custom_values: true + members: + - id: other + value: "_OTHER" + brief: > + A fallback error value to be used when the instrumentation doesn't define a custom value. + examples: ['timeout', 'java.net.UnknownHostException', 'server_certificate_invalid', '500'] + note: | + The `error.type` SHOULD be predictable and SHOULD have low cardinality. + 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 gRPC 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. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-http.yaml b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-http.yaml new file mode 100644 index 00000000..5ed9a182 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-http.yaml @@ -0,0 +1,135 @@ +groups: + - id: registry.http + prefix: http + type: attribute_group + brief: 'This document defines semantic convention attributes in the HTTP namespace.' + attributes: + - id: request.body.size + type: int + brief: > + 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. + examples: 3495 + stability: experimental # this should not be marked stable with other HTTP attributes + - id: request.header + stability: stable + type: template[string[]] + brief: > + HTTP request headers, `` being the normalized HTTP Header name (lowercase), the value being the header values. + note: > + 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: ['http.request.header.content-type=["application/json"]', 'http.request.header.x-forwarded-for=["1.2.3.4", "1.2.3.5"]'] + - id: request.method + stability: stable + type: + allow_custom_values: true + members: + - id: connect + value: "CONNECT" + brief: 'CONNECT method.' + - id: delete + value: "DELETE" + brief: 'DELETE method.' + - id: get + value: "GET" + brief: 'GET method.' + - id: head + value: "HEAD" + brief: 'HEAD method.' + - id: options + value: "OPTIONS" + brief: 'OPTIONS method.' + - id: patch + value: "PATCH" + brief: 'PATCH method.' + - id: post + value: "POST" + brief: 'POST method.' + - id: put + value: "PUT" + brief: 'PUT method.' + - id: trace + value: "TRACE" + brief: 'TRACE method.' + - id: other + value: "_OTHER" + brief: 'Any HTTP method that the instrumentation has no prior knowledge of.' + brief: 'HTTP request method.' + examples: ["GET", "POST", "HEAD"] + note: | + 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) + and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html). + + 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 + (this list MUST be a full override of the default known method, 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. + - id: request.method_original + stability: stable + type: string + brief: Original HTTP method sent by the client in the request line. + examples: ["GeT", "ACL", "foo"] + - id: request.resend_count + stability: stable + type: int + brief: > + The ordinal number of request resending attempt (for any reason, including redirects). + note: > + 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 + - id: response.body.size + type: int + brief: > + 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. + examples: 3495 + stability: experimental # this should not be marked stable with other HTTP attributes + - id: response.header + stability: stable + type: template[string[]] + brief: > + HTTP response headers, `` being the normalized HTTP Header name (lowercase), the value being the header values. + note: > + 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: ['http.response.header.content-type=["application/json"]', 'http.response.header.my-custom-header=["abc", "def"]'] + - id: response.status_code + stability: stable + type: int + brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).' + examples: [200] + - id: route + stability: stable + type: string + brief: > + The matched route, that is, the path template in the format used by the respective server framework. + examples: ['/users/:userID?', '{controller}/{action}/{id?}'] + note: > + 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. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-messaging.yaml b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-messaging.yaml new file mode 100644 index 00000000..c7ba8fd4 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-messaging.yaml @@ -0,0 +1,245 @@ +groups: + - id: registry.messaging + prefix: messaging + type: attribute_group + brief: 'Attributes describing telemetry around messaging systems and messaging activities.' + attributes: + - id: batch.message_count + type: int + brief: The number of messages sent, received, or processed in the scope of the batching operation. + note: > + 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] + - id: client_id + type: string + brief: > + A unique identifier for the client that consumes or produces a message. + examples: ['client-5', 'myhost@8742@s8083jm'] + - id: destination.name + type: string + brief: 'The message destination name' + note: | + 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'] + - id: destination.template + type: string + brief: Low cardinality representation of the messaging destination name + note: > + 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}'] + - id: destination.anonymous + type: boolean + brief: 'A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name).' + - id: destination.temporary + type: boolean + brief: 'A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed.' + - id: destination_publish.anonymous + type: boolean + brief: 'A boolean that is true if the publish message destination is anonymous (could be unnamed or have auto-generated name).' + - id: destination_publish.name + type: string + brief: 'The name of the original destination the message was published to' + note: | + The name SHOULD uniquely identify a specific queue, topic, or other entity within the broker. If + the broker doesn't have such notion, the original destination name SHOULD uniquely identify the broker. + examples: ['MyQueue', 'MyTopic'] + - id: kafka.consumer.group + type: string + brief: > + Name of the Kafka Consumer Group that is handling the message. + Only applies to consumers, not producers. + examples: 'my-group' + - id: kafka.destination.partition + type: int + brief: > + Partition the message is sent to. + examples: 2 + - id: kafka.message.key + type: string + brief: > + 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. + note: > + 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' + - id: kafka.message.offset + type: int + brief: > + The offset of a record in the corresponding Kafka partition. + examples: 42 + - id: kafka.message.tombstone + type: boolean + brief: 'A boolean that is true if the message is a tombstone.' + - id: message.conversation_id + type: string + brief: > + The conversation ID identifying the conversation to which the message belongs, + represented as a string. Sometimes called "Correlation ID". + examples: 'MyConversationId' + - id: message.envelope.size + type: int + brief: > + The size of the message body and metadata in bytes. + note: | + This can refer to both the compressed or uncompressed size. If both sizes are known, the uncompressed + size should be used. + examples: 2738 + - id: message.id + type: string + brief: 'A value used by the messaging system as an identifier for the message, represented as a string.' + examples: '452a7c7c7c7048c2f887f61572b18fc2' + - id: message.body.size + type: int + brief: > + The size of the message body in bytes. + note: | + 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 + - id: operation + type: + allow_custom_values: true + members: + - id: publish + value: "publish" + brief: > + One or more messages are provided for publishing to an intermediary. + If a single message is published, the context of the "Publish" span can be used as the creation context and no "Create" span needs to be created. + - id: create + value: "create" + brief: > + A message is created. + "Create" spans always refer to a single message and are used to provide a unique creation context for messages in batch publishing scenarios. + - id: receive + value: "receive" + brief: > + One or more messages are requested by a consumer. + This operation refers to pull-based scenarios, where consumers explicitly call methods of messaging SDKs to receive messages. + - id: deliver + value: "deliver" + brief: > + One or more messages are passed to a consumer. + This operation refers to push-based scenarios, where consumer register callbacks which get called by messaging SDKs. + brief: > + A string identifying the kind of messaging operation. + note: If a custom value is used, it MUST be of low cardinality. + - id: rabbitmq.destination.routing_key + type: string + brief: > + RabbitMQ message routing key. + examples: 'myKey' + - id: rocketmq.client_group + type: string + brief: > + Name of the RocketMQ producer/consumer group that is handling the message. The client type is identified by the SpanKind. + examples: 'myConsumerGroup' + - id: rocketmq.consumption_model + type: + allow_custom_values: false + members: + - id: clustering + value: 'clustering' + brief: 'Clustering consumption model' + - id: broadcasting + value: 'broadcasting' + brief: 'Broadcasting consumption model' + brief: > + Model of message consumption. This only applies to consumer spans. + - id: rocketmq.message.delay_time_level + type: int + brief: > + The delay time level for delay message, which determines the message delay time. + examples: 3 + - id: rocketmq.message.delivery_timestamp + type: int + brief: > + The timestamp in milliseconds that the delay message is expected to be delivered to consumer. + examples: 1665987217045 + - id: rocketmq.message.group + type: string + brief: > + 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. + examples: 'myMessageGroup' + - id: rocketmq.message.keys + type: string[] + brief: > + Key(s) of message, another way to mark message besides message id. + examples: ['keyA', 'keyB'] + - id: rocketmq.message.tag + type: string + brief: > + The secondary classifier of message besides topic. + examples: tagA + - id: rocketmq.message.type + type: + allow_custom_values: false + members: + - id: normal + value: 'normal' + brief: "Normal message" + - id: fifo + value: 'fifo' + brief: 'FIFO message' + - id: delay + value: 'delay' + brief: 'Delay message' + - id: transaction + value: 'transaction' + brief: 'Transaction message' + brief: > + Type of message. + - id: rocketmq.namespace + type: string + brief: > + Namespace of RocketMQ resources, resources in different namespaces are individual. + examples: 'myNamespace' + - id: gcp_pubsub.message.ordering_key + type: string + brief: > + The ordering key for a given message. If the attribute is not present, the message does not have an ordering key. + examples: 'ordering_key' + - id: system + brief: > + An identifier for the messaging system being used. See below for a list of well-known identifiers. + type: + allow_custom_values: true + members: + - id: activemq + value: 'activemq' + brief: 'Apache ActiveMQ' + - id: aws_sqs + value: 'aws_sqs' + brief: 'Amazon Simple Queue Service (SQS)' + - id: azure_eventgrid + value: 'azure_eventgrid' + brief: 'Azure Event Grid' + - id: azure_eventhubs + value: 'azure_eventhubs' + brief: 'Azure Event Hubs' + - id: azure_servicebus + value: 'azure_servicebus' + brief: 'Azure Service Bus' + - id: gcp_pubsub + value: 'gcp_pubsub' + brief: 'Google Cloud Pub/Sub' + - id: jms + value: 'jms' + brief: 'Java Message Service' + - id: kafka + value: 'kafka' + brief: 'Apache Kafka' + - id: rabbitmq + value: 'rabbitmq' + brief: 'RabbitMQ' + - id: rocketmq + value: 'rocketmq' + brief: 'Apache RocketMQ' diff --git a/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-network.yaml b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-network.yaml new file mode 100644 index 00000000..c16763bb --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-network.yaml @@ -0,0 +1,194 @@ +groups: + - id: registry.network + prefix: network + type: attribute_group + brief: > + These attributes may be used for any network related operation. + attributes: + - id: carrier.icc + type: string + brief: "The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network." + examples: "DE" + - id: carrier.mcc + type: string + brief: "The mobile carrier country code." + examples: "310" + - id: carrier.mnc + type: string + brief: "The mobile carrier network code." + examples: "001" + - id: carrier.name + type: string + brief: "The name of the mobile carrier." + examples: "sprint" + - id: connection.subtype + type: + allow_custom_values: true + members: + - id: gprs + brief: GPRS + value: "gprs" + - id: edge + brief: EDGE + value: "edge" + - id: umts + brief: UMTS + value: "umts" + - id: cdma + brief: CDMA + value: "cdma" + - id: evdo_0 + brief: EVDO Rel. 0 + value: "evdo_0" + - id: evdo_a + brief: "EVDO Rev. A" + value: "evdo_a" + - id: cdma2000_1xrtt + brief: CDMA2000 1XRTT + value: "cdma2000_1xrtt" + - id: hsdpa + brief: HSDPA + value: "hsdpa" + - id: hsupa + brief: HSUPA + value: "hsupa" + - id: hspa + brief: HSPA + value: "hspa" + - id: iden + brief: IDEN + value: "iden" + - id: evdo_b + brief: "EVDO Rev. B" + value: "evdo_b" + - id: lte + brief: LTE + value: "lte" + - id: ehrpd + brief: EHRPD + value: "ehrpd" + - id: hspap + brief: HSPAP + value: "hspap" + - id: gsm + brief: GSM + value: "gsm" + - id: td_scdma + brief: TD-SCDMA + value: "td_scdma" + - id: iwlan + brief: IWLAN + value: "iwlan" + - id: nr + brief: "5G NR (New Radio)" + value: "nr" + - id: nrnsa + brief: "5G NRNSA (New Radio Non-Standalone)" + value: "nrnsa" + - id: lte_ca + brief: LTE CA + value: "lte_ca" + brief: '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.' + examples: 'LTE' + - id: connection.type + type: + allow_custom_values: true + members: + - id: wifi + value: "wifi" + - id: wired + value: "wired" + - id: cell + value: "cell" + - id: unavailable + value: "unavailable" + - id: unknown + value: "unknown" + brief: 'The internet connection type.' + examples: 'wifi' + - id: local.address + stability: stable + type: string + brief: Local address of the network connection - IP address or Unix domain socket name. + examples: ['10.1.2.80', '/tmp/my.sock'] + - id: local.port + stability: stable + type: int + brief: Local port number of the network connection. + examples: [65123] + - id: peer.address + stability: stable + type: string + brief: Peer address of the network connection - IP address or Unix domain socket name. + examples: ['10.1.2.80', '/tmp/my.sock'] + - id: peer.port + stability: stable + type: int + brief: Peer port number of the network connection. + examples: [65123] + - id: protocol.name + stability: stable + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + note: The value SHOULD be normalized to lowercase. + examples: ['amqp', 'http', 'mqtt'] + - id: protocol.version + stability: stable + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: '3.1.1' + note: > + `network.protocol.version` refers to the version of the protocol used and might be + different from the protocol client's version. If the HTTP client has a version + of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - id: transport + stability: stable + type: + allow_custom_values: true + members: + - id: tcp + value: 'tcp' + brief: "TCP" + - id: udp + value: 'udp' + brief: "UDP" + - id: pipe + value: "pipe" + brief: 'Named or anonymous pipe.' + - id: unix + value: 'unix' + brief: "Unix domain socket" + brief: > + [OSI transport layer](https://osi-model.com/transport-layer/) or + [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication). + note: | + 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'] + - id: type + stability: stable + type: + allow_custom_values: true + members: + - id: ipv4 + value: 'ipv4' + brief: "IPv4" + - id: ipv6 + value: 'ipv6' + brief: "IPv6" + brief: '[OSI network layer](https://osi-model.com/network-layer/) or non-OSI equivalent.' + note: The value SHOULD be normalized to lowercase. + examples: ['ipv4', 'ipv6'] + - id: io.direction + type: + allow_custom_values: false + members: + - id: transmit + value: 'transmit' + - id: receive + value: 'receive' + brief: "The network IO operation direction." + examples: ["transmit"] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-server.yaml b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-server.yaml new file mode 100644 index 00000000..0523bb0d --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-server.yaml @@ -0,0 +1,28 @@ +groups: + - id: server + prefix: server + type: attribute_group + brief: > + These attributes may be used to describe the server in a connection-based network interaction + where there is one side that initiates the connection (the client is the side that initiates the connection). + This covers all TCP network interactions since TCP is connection-based and one side initiates the + connection (an exception is made for peer-to-peer communication over TCP where the "user-facing" surface of the + protocol / API doesn't expose a clear notion of client and server). + This also covers UDP network interactions where one side initiates the interaction, e.g. QUIC (HTTP/3) and DNS. + attributes: + - id: address + stability: stable + type: string + brief: "Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name." + note: > + 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'] + - id: port + stability: stable + type: int + brief: Server port number. + note: > + 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] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-url.yaml b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-url.yaml new file mode 100644 index 00000000..3042f32c --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-3-extends/registry/registry-url.yaml @@ -0,0 +1,41 @@ +groups: + - id: registry.url + brief: Attributes describing URL. + type: attribute_group + prefix: url + attributes: + - id: scheme + stability: stable + type: string + brief: 'The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol.' + examples: ["https", "ftp", "telnet"] + - id: full + stability: stable + type: string + brief: Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) + note: > + 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) + and SHOULD NOT be validated or modified except for sanitizing purposes. + examples: ['https://www.foo.bar/search?q=OpenTelemetry#SemConv', '//localhost'] + - id: path + stability: stable + type: string + brief: 'The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component' + examples: ['/search'] + - id: query + stability: stable + type: string + brief: 'The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component' + examples: ["q=OpenTelemetry"] + note: Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it. + - id: fragment + stability: stable + type: string + brief: 'The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component' + examples: ["SemConv"] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-4-events/README.md b/crates/weaver_resolver/data/registry-test-4-events/README.md new file mode 100644 index 00000000..e253db9f --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-4-events/README.md @@ -0,0 +1 @@ +Test the `extends` directive in attribute_group and metric. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-4-events/expected-attribute-catalog.json b/crates/weaver_resolver/data/registry-test-4-events/expected-attribute-catalog.json new file mode 100644 index 00000000..27efb1f9 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-4-events/expected-attribute-catalog.json @@ -0,0 +1,145 @@ +[ + { + "name": "ios.state", + "type": { + "type": "Enum", + "allow_custom_values": false, + "members": [ + { + "id": "active", + "value": { + "type": "String", + "value": "active" + }, + "brief": "The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`.\n" + }, + { + "id": "inactive", + "value": { + "type": "String", + "value": "inactive" + }, + "brief": "The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`.\n" + }, + { + "id": "background", + "value": { + "type": "String", + "value": "background" + }, + "brief": "The app is now in the background. This value is associated with UIKit notification `applicationDidEnterBackground`.\n" + }, + { + "id": "foreground", + "value": { + "type": "String", + "value": "foreground" + }, + "brief": "The app is now in the foreground. This value is associated with UIKit notification `applicationWillEnterForeground`.\n" + }, + { + "id": "terminate", + "value": { + "type": "String", + "value": "terminate" + }, + "brief": "The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`.\n" + } + ] + }, + "brief": "This attribute represents the state the application has transitioned into at the occurrence of the event.\n", + "requirement_level": { + "type": "Required" + }, + "note": "The iOS lifecycle states are defined in the [UIApplicationDelegate documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate#1656902), and from which the `OS terminology` column values are derived.\n" + }, + { + "name": "android.state", + "type": { + "type": "Enum", + "allow_custom_values": false, + "members": [ + { + "id": "created", + "value": { + "type": "String", + "value": "created" + }, + "brief": "Any time before Activity.onResume() or, if the app has no Activity, Context.startService() has been called in the app for the first time.\n" + }, + { + "id": "background", + "value": { + "type": "String", + "value": "background" + }, + "brief": "Any time after Activity.onPause() or, if the app has no Activity, Context.stopService() has been called when the app was in the foreground state.\n" + }, + { + "id": "foreground", + "value": { + "type": "String", + "value": "foreground" + }, + "brief": "Any time after Activity.onResume() or, if the app has no Activity, Context.startService() has been called when the app was in either the created or background states." + } + ] + }, + "brief": "This attribute represents the state the application has transitioned into at the occurrence of the event.\n", + "requirement_level": { + "type": "Required" + }, + "note": "The Android lifecycle states are defined in [Activity lifecycle callbacks](https://developer.android.com/guide/components/activities/activity-lifecycle#lc), and from which the `OS identifiers` are derived.\n" + }, + { + "name": "feature_flag.key", + "type": { + "type": "String" + }, + "brief": "The unique identifier of the feature flag.", + "examples": { + "type": "Strings", + "values": [ + "logo-color" + ] + }, + "requirement_level": { + "type": "Required" + } + }, + { + "name": "feature_flag.provider_name", + "type": { + "type": "String" + }, + "brief": "The name of the service provider that performs the flag evaluation.", + "examples": { + "type": "Strings", + "values": [ + "Flag Manager" + ] + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "feature_flag.variant", + "type": { + "type": "String" + }, + "brief": "SHOULD be a semantic identifier for a value. If one is unavailable, a stringified version of the value can be used.\n", + "examples": { + "type": "Strings", + "values": [ + "red", + "true", + "on" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "A semantic identifier, commonly referred to as a variant, provides a means\nfor referring to a value without including the value itself. This can\nprovide additional context for understanding the meaning behind a value.\nFor example, the variant `red` maybe be used for the value `#c05543`.\n\nA stringified version of the value can be used in situations where a\nsemantic identifier is unavailable. String representation of the value\nshould be determined by the implementer." + } +] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-4-events/expected-registry.json b/crates/weaver_resolver/data/registry-test-4-events/expected-registry.json new file mode 100644 index 00000000..ea6b9471 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-4-events/expected-registry.json @@ -0,0 +1,173 @@ +{ + "registry_url": "https://semconv-registry.com", + "groups": [ + { + "id": "log-feature_flag", + "typed_group": { + "type": "Event", + "name": null + }, + "brief": "This document defines attributes for feature flag evaluations represented using Log Records.\n", + "prefix": "feature_flag", + "attributes": [ + 2, + 3, + 4 + ], + "lineage": { + "provenance": "data/registry-test-4-events/registry/log-feature_flag.yaml", + "attributes": { + "2": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + } + }, + "3": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + } + }, + "4": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "feature_flag" + } + } + } + } + }, + { + "id": "ios.lifecycle.events", + "typed_group": { + "type": "Event", + "name": "device.app.lifecycle" + }, + "brief": "This event represents an occurrence of a lifecycle transition on the iOS platform.\n", + "prefix": "ios", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-4-events/registry/mobile-events.yaml" + } + }, + { + "id": "android.lifecycle.events", + "typed_group": { + "type": "Event", + "name": "device.app.lifecycle" + }, + "brief": "This event represents an occurrence of a lifecycle transition on the Android platform.\n", + "prefix": "android", + "attributes": [ + 1 + ], + "lineage": { + "provenance": "data/registry-test-4-events/registry/mobile-events.yaml" + } + }, + { + "id": "feature_flag", + "typed_group": { + "type": "Event", + "name": null + }, + "brief": "This semantic convention defines the attributes used to represent a feature flag evaluation as an event.\n", + "prefix": "feature_flag", + "attributes": [ + 2, + 3, + 4 + ], + "lineage": { + "provenance": "data/registry-test-4-events/registry/trace-feature-flag.yaml" + } + } + ] +} \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-4-events/registry/log-feature_flag.yaml b/crates/weaver_resolver/data/registry-test-4-events/registry/log-feature_flag.yaml new file mode 100644 index 00000000..081125bf --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-4-events/registry/log-feature_flag.yaml @@ -0,0 +1,11 @@ +groups: + - id: log-feature_flag + type: event + prefix: feature_flag + brief: > + This document defines attributes for feature flag evaluations + represented using Log Records. + attributes: + - ref: feature_flag.key + - ref: feature_flag.provider_name + - ref: feature_flag.variant \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-4-events/registry/mobile-events.yaml b/crates/weaver_resolver/data/registry-test-4-events/registry/mobile-events.yaml new file mode 100644 index 00000000..94f93ebb --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-4-events/registry/mobile-events.yaml @@ -0,0 +1,72 @@ +groups: + - id: ios.lifecycle.events + type: event + prefix: ios + name: device.app.lifecycle + brief: > + This event represents an occurrence of a lifecycle transition on the iOS platform. + attributes: + - id: state + requirement_level: "required" + note: > + The iOS lifecycle states are defined in the [UIApplicationDelegate documentation](https://developer.apple.com/documentation/uikit/uiapplicationdelegate#1656902), + and from which the `OS terminology` column values are derived. + brief: > + This attribute represents the state the application has transitioned into at the occurrence of the event. + type: + allow_custom_values: false + members: + - id: active + value: 'active' + brief: > + The app has become `active`. Associated with UIKit notification `applicationDidBecomeActive`. + - id: inactive + value: 'inactive' + brief: > + The app is now `inactive`. Associated with UIKit notification `applicationWillResignActive`. + - id: background + value: 'background' + brief: > + The app is now in the background. + This value is associated with UIKit notification `applicationDidEnterBackground`. + - id: foreground + value: 'foreground' + brief: > + The app is now in the foreground. + This value is associated with UIKit notification `applicationWillEnterForeground`. + - id: terminate + value: 'terminate' + brief: > + The app is about to terminate. Associated with UIKit notification `applicationWillTerminate`. + - id: android.lifecycle.events + type: event + prefix: android + name: device.app.lifecycle + brief: > + This event represents an occurrence of a lifecycle transition on the Android platform. + attributes: + - id: state + requirement_level: required + brief: > + This attribute represents the state the application has transitioned into at the occurrence of the event. + note: > + The Android lifecycle states are defined in [Activity lifecycle callbacks](https://developer.android.com/guide/components/activities/activity-lifecycle#lc), + and from which the `OS identifiers` are derived. + type: + allow_custom_values: false + members: + - id: created + value: 'created' + brief: > + Any time before Activity.onResume() or, if the app has no Activity, Context.startService() + has been called in the app for the first time. + - id: background + value: 'background' + brief: > + Any time after Activity.onPause() or, if the app has no Activity, + Context.stopService() has been called when the app was in the foreground state. + - id: foreground + value: 'foreground' + brief: > + Any time after Activity.onResume() or, if the app has no Activity, + Context.startService() has been called when the app was in either the created or background states. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-4-events/registry/trace-feature-flag.yaml b/crates/weaver_resolver/data/registry-test-4-events/registry/trace-feature-flag.yaml new file mode 100644 index 00000000..fa4c8816 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-4-events/registry/trace-feature-flag.yaml @@ -0,0 +1,34 @@ +groups: + - id: feature_flag + prefix: feature_flag + type: event + brief: > + This semantic convention defines the attributes used to represent a + feature flag evaluation as an event. + attributes: + - id: key + type: string + requirement_level: required + brief: The unique identifier of the feature flag. + examples: ["logo-color"] + - id: provider_name + type: string + requirement_level: recommended + brief: The name of the service provider that performs the flag evaluation. + examples: ["Flag Manager"] + - id: variant + type: string + requirement_level: recommended + examples: ["red", "true", "on"] + brief: > + SHOULD be a semantic identifier for a value. If one is unavailable, a + stringified version of the value can be used. + note: |- + 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`. + + A stringified version of the value can be used in situations where a + semantic identifier is unavailable. String representation of the value + should be determined by the implementer. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-5-metrics/README.md b/crates/weaver_resolver/data/registry-test-5-metrics/README.md new file mode 100644 index 00000000..e253db9f --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-5-metrics/README.md @@ -0,0 +1 @@ +Test the `extends` directive in attribute_group and metric. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-5-metrics/expected-attribute-catalog.json b/crates/weaver_resolver/data/registry-test-5-metrics/expected-attribute-catalog.json new file mode 100644 index 00000000..63e3a890 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-5-metrics/expected-attribute-catalog.json @@ -0,0 +1,140 @@ +[ + { + "name": "faas.trigger", + "type": { + "type": "Enum", + "allow_custom_values": false, + "members": [ + { + "id": "datasource", + "value": { + "type": "String", + "value": "datasource" + }, + "brief": "A response to some data source operation such as a database or filesystem read/write" + }, + { + "id": "http", + "value": { + "type": "String", + "value": "http" + }, + "brief": "To provide an answer to an inbound HTTP request" + }, + { + "id": "pubsub", + "value": { + "type": "String", + "value": "pubsub" + }, + "brief": "A function is set to be executed when messages are sent to a messaging system" + }, + { + "id": "timer", + "value": { + "type": "String", + "value": "timer" + }, + "brief": "A function is scheduled to be executed regularly" + }, + { + "id": "other", + "value": { + "type": "String", + "value": "other" + }, + "brief": "If none of the others apply" + } + ] + }, + "brief": "Type of the trigger which caused this function invocation.", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "faas.invoked_name", + "type": { + "type": "String" + }, + "brief": "The name of the invoked function.\n", + "examples": { + "type": "String", + "value": "my-function" + }, + "requirement_level": { + "type": "Required" + }, + "note": "SHOULD be equal to the `faas.name` resource attribute of the invoked function.\n" + }, + { + "name": "faas.invoked_provider", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "alibaba_cloud", + "value": { + "type": "String", + "value": "alibaba_cloud" + }, + "brief": "Alibaba Cloud" + }, + { + "id": "aws", + "value": { + "type": "String", + "value": "aws" + }, + "brief": "Amazon Web Services" + }, + { + "id": "azure", + "value": { + "type": "String", + "value": "azure" + }, + "brief": "Microsoft Azure" + }, + { + "id": "gcp", + "value": { + "type": "String", + "value": "gcp" + }, + "brief": "Google Cloud Platform" + }, + { + "id": "tencent_cloud", + "value": { + "type": "String", + "value": "tencent_cloud" + }, + "brief": "Tencent Cloud" + } + ] + }, + "brief": "The cloud provider of the invoked function.\n", + "requirement_level": { + "type": "Required" + }, + "note": "SHOULD be equal to the `cloud.provider` resource attribute of the invoked function.\n" + }, + { + "name": "faas.invoked_region", + "type": { + "type": "String" + }, + "brief": "The cloud region of the invoked function.\n", + "examples": { + "type": "String", + "value": "eu-central-1" + }, + "requirement_level": { + "type": "ConditionallyRequired", + "text": "For some cloud providers, like AWS or GCP, the region in which a function is hosted is essential to uniquely identify the function and also part of its endpoint. Since it's part of the endpoint being called, the region is always known to clients. In these cases, `faas.invoked_region` MUST be set accordingly. If the region is unknown to the client or not required for identifying the invoked function, setting `faas.invoked_region` is optional.\n" + }, + "note": "SHOULD be equal to the `cloud.region` resource attribute of the invoked function.\n" + } +] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-5-metrics/expected-registry.json b/crates/weaver_resolver/data/registry-test-5-metrics/expected-registry.json new file mode 100644 index 00000000..cf24712d --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-5-metrics/expected-registry.json @@ -0,0 +1,490 @@ +{ + "registry_url": "https://semconv-registry.com", + "groups": [ + { + "id": "attributes.faas.common", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Describes FaaS attributes.", + "prefix": "faas", + "attributes": [ + 0, + 1, + 2, + 3 + ], + "lineage": { + "provenance": "data/registry-test-5-metrics/registry/faas-common.yaml" + } + }, + { + "id": "metric.faas.invoke_duration", + "typed_group": { + "type": "Metric", + "metric_name": "faas.invoke_duration", + "instrument": "Histogram", + "unit": "s" + }, + "brief": "Measures the duration of the function's logic execution", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-5-metrics/registry/faas-metrics.yaml", + "attributes": { + "0": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + } + } + } + } + }, + { + "id": "metric.faas.init_duration", + "typed_group": { + "type": "Metric", + "metric_name": "faas.init_duration", + "instrument": "Histogram", + "unit": "s" + }, + "brief": "Measures the duration of the function's initialization, such as a cold start", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-5-metrics/registry/faas-metrics.yaml", + "attributes": { + "0": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + } + } + } + } + }, + { + "id": "metric.faas.coldstarts", + "typed_group": { + "type": "Metric", + "metric_name": "faas.coldstarts", + "instrument": "Counter", + "unit": "{coldstart}" + }, + "brief": "Number of invocation cold starts", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-5-metrics/registry/faas-metrics.yaml", + "attributes": { + "0": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + } + } + } + } + }, + { + "id": "metric.faas.errors", + "typed_group": { + "type": "Metric", + "metric_name": "faas.errors", + "instrument": "Counter", + "unit": "{error}" + }, + "brief": "Number of invocation errors", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-5-metrics/registry/faas-metrics.yaml", + "attributes": { + "0": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + } + } + } + } + }, + { + "id": "metric.faas.invocations", + "typed_group": { + "type": "Metric", + "metric_name": "faas.invocations", + "instrument": "Counter", + "unit": "{invocation}" + }, + "brief": "Number of successful invocations", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-5-metrics/registry/faas-metrics.yaml", + "attributes": { + "0": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + } + } + } + } + }, + { + "id": "metric.faas.timeouts", + "typed_group": { + "type": "Metric", + "metric_name": "faas.timeouts", + "instrument": "Counter", + "unit": "{timeout}" + }, + "brief": "Number of invocation timeouts", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-5-metrics/registry/faas-metrics.yaml", + "attributes": { + "0": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + } + } + } + } + }, + { + "id": "metric.faas.mem_usage", + "typed_group": { + "type": "Metric", + "metric_name": "faas.mem_usage", + "instrument": "Histogram", + "unit": "By" + }, + "brief": "Distribution of max memory usage per invocation", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-5-metrics/registry/faas-metrics.yaml", + "attributes": { + "0": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + } + } + } + } + }, + { + "id": "metric.faas.cpu_usage", + "typed_group": { + "type": "Metric", + "metric_name": "faas.cpu_usage", + "instrument": "Histogram", + "unit": "s" + }, + "brief": "Distribution of CPU usage per invocation", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-5-metrics/registry/faas-metrics.yaml", + "attributes": { + "0": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + } + } + } + } + }, + { + "id": "metric.faas.net_io", + "typed_group": { + "type": "Metric", + "metric_name": "faas.net_io", + "instrument": "Histogram", + "unit": "By" + }, + "brief": "Distribution of net I/O usage per invocation", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-5-metrics/registry/faas-metrics.yaml", + "attributes": { + "0": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "attributes.faas.common" + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-5-metrics/registry/faas-common.yaml b/crates/weaver_resolver/data/registry-test-5-metrics/registry/faas-common.yaml new file mode 100644 index 00000000..ca424b17 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-5-metrics/registry/faas-common.yaml @@ -0,0 +1,77 @@ +groups: + - id: attributes.faas.common + type: attribute_group + brief: "Describes FaaS attributes." + prefix: faas + attributes: + - id: trigger + brief: 'Type of the trigger which caused this function invocation.' + type: + allow_custom_values: false + members: + - id: datasource + value: 'datasource' + brief: 'A response to some data source operation such as a database or filesystem read/write' + - id: http + value: 'http' + brief: 'To provide an answer to an inbound HTTP request' + - id: pubsub + value: 'pubsub' + brief: 'A function is set to be executed when messages are sent to a messaging system' + - id: timer + value: 'timer' + brief: 'A function is scheduled to be executed regularly' + - id: other + value: 'other' + brief: 'If none of the others apply' + - id: invoked_name + type: string + requirement_level: required + brief: > + The name of the invoked function. + note: > + SHOULD be equal to the `faas.name` resource attribute of the + invoked function. + examples: 'my-function' + - id: invoked_provider + type: + allow_custom_values: true + members: + - id: 'alibaba_cloud' + value: 'alibaba_cloud' + brief: 'Alibaba Cloud' + - id: 'aws' + value: 'aws' + brief: 'Amazon Web Services' + - id: 'azure' + value: 'azure' + brief: 'Microsoft Azure' + - id: 'gcp' + value: 'gcp' + brief: 'Google Cloud Platform' + - id: 'tencent_cloud' + value: 'tencent_cloud' + brief: 'Tencent Cloud' + requirement_level: required + brief: > + The cloud provider of the invoked function. + note: > + SHOULD be equal to the `cloud.provider` resource attribute of the + invoked function. + - id: invoked_region + type: string + requirement_level: + conditionally_required: > + For some cloud providers, like AWS or GCP, the region in which a + function is hosted is essential to uniquely identify the function + and also part of its endpoint. Since it's part of the endpoint + being called, the region is always known to clients. In these cases, + `faas.invoked_region` MUST be set accordingly. If the region is + unknown to the client or not required for identifying the invoked + function, setting `faas.invoked_region` is optional. + brief: > + The cloud region of the invoked function. + note: > + SHOULD be equal to the `cloud.region` resource attribute of the + invoked function. + examples: 'eu-central-1' \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-5-metrics/registry/faas-metrics.yaml b/crates/weaver_resolver/data/registry-test-5-metrics/registry/faas-metrics.yaml new file mode 100644 index 00000000..4d97c463 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-5-metrics/registry/faas-metrics.yaml @@ -0,0 +1,81 @@ +groups: + - id: metric.faas.invoke_duration + type: metric + metric_name: faas.invoke_duration + brief: "Measures the duration of the function's logic execution" + instrument: histogram + unit: "s" + attributes: + - ref: faas.trigger + + - id: metric.faas.init_duration + type: metric + metric_name: faas.init_duration + brief: "Measures the duration of the function's initialization, such as a cold start" + instrument: histogram + unit: "s" + attributes: + - ref: faas.trigger + + - id: metric.faas.coldstarts + type: metric + metric_name: faas.coldstarts + brief: "Number of invocation cold starts" + instrument: counter + unit: "{coldstart}" + attributes: + - ref: faas.trigger + + - id: metric.faas.errors + type: metric + metric_name: faas.errors + brief: "Number of invocation errors" + instrument: counter + unit: "{error}" + attributes: + - ref: faas.trigger + + - id: metric.faas.invocations + type: metric + metric_name: faas.invocations + brief: "Number of successful invocations" + instrument: counter + unit: "{invocation}" + attributes: + - ref: faas.trigger + + - id: metric.faas.timeouts + type: metric + metric_name: faas.timeouts + brief: "Number of invocation timeouts" + instrument: counter + unit: "{timeout}" + attributes: + - ref: faas.trigger + + - id: metric.faas.mem_usage + type: metric + metric_name: faas.mem_usage + brief: "Distribution of max memory usage per invocation" + instrument: histogram + unit: "By" + attributes: + - ref: faas.trigger + + - id: metric.faas.cpu_usage + type: metric + metric_name: faas.cpu_usage + brief: "Distribution of CPU usage per invocation" + instrument: histogram + unit: "s" + attributes: + - ref: faas.trigger + + - id: metric.faas.net_io + type: metric + metric_name: faas.net_io + brief: "Distribution of net I/O usage per invocation" + instrument: histogram + unit: "By" + attributes: + - ref: faas.trigger \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-6-resources/README.md b/crates/weaver_resolver/data/registry-test-6-resources/README.md new file mode 100644 index 00000000..e253db9f --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-6-resources/README.md @@ -0,0 +1 @@ +Test the `extends` directive in attribute_group and metric. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-6-resources/expected-attribute-catalog.json b/crates/weaver_resolver/data/registry-test-6-resources/expected-attribute-catalog.json new file mode 100644 index 00000000..ee1a889d --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-6-resources/expected-attribute-catalog.json @@ -0,0 +1,107 @@ +[ + { + "name": "user_agent.original", + "type": { + "type": "String" + }, + "brief": "Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client.\n", + "examples": { + "type": "Strings", + "values": [ + "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" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "browser.brands", + "type": { + "type": "Strings" + }, + "brief": "Array of brand name and version separated by a space", + "examples": { + "type": "Strings", + "values": [ + " Not A;Brand 99", + "Chromium 99", + "Chrome 99" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "This value is intended to be taken from the [UA client hints API](https://wicg.github.io/ua-client-hints/#interface) (`navigator.userAgentData.brands`).\n" + }, + { + "name": "browser.platform", + "type": { + "type": "String" + }, + "brief": "The platform on which the browser is running", + "examples": { + "type": "Strings", + "values": [ + "Windows", + "macOS", + "Android" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\nThe 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.\n" + }, + { + "name": "browser.mobile", + "type": { + "type": "Boolean" + }, + "brief": "A boolean that is true if the browser is running on a mobile device", + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n" + }, + { + "name": "browser.language", + "type": { + "type": "String" + }, + "brief": "Preferred language of the user using the browser", + "examples": { + "type": "Strings", + "values": [ + "en", + "en-US", + "fr", + "fr-FR" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "This value is intended to be taken from the Navigator API `navigator.language`.\n" + }, + { + "name": "user_agent.original", + "type": { + "type": "String" + }, + "brief": "Full user-agent string provided by the browser", + "examples": { + "type": "Strings", + "values": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "The user-agent value SHOULD be provided only from browsers that do not have a mechanism to retrieve brands and platform individually from the User-Agent Client Hints API. To retrieve the value, the legacy `navigator.userAgent` API can be used.\n", + "stability": "Stable" + } +] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-6-resources/expected-registry.json b/crates/weaver_resolver/data/registry-test-6-resources/expected-registry.json new file mode 100644 index 00000000..e23561f4 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-6-resources/expected-registry.json @@ -0,0 +1,61 @@ +{ + "registry_url": "https://semconv-registry.com", + "groups": [ + { + "id": "registry.user_agent", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Describes user-agent attributes.", + "prefix": "user_agent", + "attributes": [ + 0 + ], + "lineage": { + "provenance": "data/registry-test-6-resources/registry/registry-user-agent.yaml" + } + }, + { + "id": "browser", + "typed_group": { + "type": "Resource" + }, + "brief": "The web browser in which the application represented by the resource is running. The `browser.*` attributes MUST be used only for resources that represent applications running in a web browser (regardless of whether running on a mobile or desktop device).\n", + "prefix": "browser", + "attributes": [ + 1, + 2, + 3, + 4, + 5 + ], + "lineage": { + "provenance": "data/registry-test-6-resources/registry/resource-browser.yaml", + "attributes": { + "5": { + "AttributeTag": { + "resolution_mode": "Reference", + "group_id": "registry.user_agent" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.user_agent" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.user_agent" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.user_agent" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.user_agent" + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-6-resources/registry/registry-user-agent.yaml b/crates/weaver_resolver/data/registry-test-6-resources/registry/registry-user-agent.yaml new file mode 100644 index 00000000..3f902d18 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-6-resources/registry/registry-user-agent.yaml @@ -0,0 +1,13 @@ +groups: + - id: registry.user_agent + prefix: user_agent + type: attribute_group + brief: "Describes user-agent attributes." + attributes: + - id: original + stability: stable + type: string + brief: > + Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client. + 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'] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-6-resources/registry/resource-browser.yaml b/crates/weaver_resolver/data/registry-test-6-resources/registry/resource-browser.yaml new file mode 100644 index 00000000..14eda67f --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-6-resources/registry/resource-browser.yaml @@ -0,0 +1,56 @@ +groups: + - id: browser + prefix: browser + type: resource + brief: > + The web browser in which the application represented by the resource is running. + The `browser.*` attributes MUST be used only for resources that represent applications + running in a web browser (regardless of whether running on a mobile or desktop device). + attributes: + - id: brands + type: string[] + brief: 'Array of brand name and version separated by a space' + note: > + 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"] + - id: platform + type: string + brief: 'The platform on which the browser is running' + note: > + 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'] + - id: mobile + type: boolean + brief: 'A boolean that is true if the browser is running on a mobile device' + note: > + 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. + - id: language + type: string + brief: 'Preferred language of the user using the browser' + note: > + This value is intended to be taken from the Navigator API + `navigator.language`. + examples: ["en", "en-US", "fr", "fr-FR"] + - ref: user_agent.original + brief: 'Full user-agent string provided by the browser' + note: > + The user-agent value SHOULD be provided only from browsers that do not have a mechanism + to retrieve brands and platform individually from the User-Agent Client Hints API. + To retrieve the value, the legacy `navigator.userAgent` API can be used. + examples: ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36'] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-7-spans/README.md b/crates/weaver_resolver/data/registry-test-7-spans/README.md new file mode 100644 index 00000000..e253db9f --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-7-spans/README.md @@ -0,0 +1 @@ +Test the `extends` directive in attribute_group and metric. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-7-spans/expected-attribute-catalog.json b/crates/weaver_resolver/data/registry-test-7-spans/expected-attribute-catalog.json new file mode 100644 index 00000000..126a5697 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-7-spans/expected-attribute-catalog.json @@ -0,0 +1,3609 @@ +[ + { + "name": "db.cassandra.coordinator.dc", + "type": { + "type": "String" + }, + "brief": "The data center of the coordinating node for a query.\n", + "examples": { + "type": "String", + "value": "us-west-2" + }, + "tag": "tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cassandra.coordinator.id", + "type": { + "type": "String" + }, + "brief": "The ID of the coordinating node for a query.\n", + "examples": { + "type": "String", + "value": "be13faa2-8574-4d71-926d-27f16cf8a7af" + }, + "tag": "tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cassandra.consistency_level", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "all", + "value": { + "type": "String", + "value": "all" + } + }, + { + "id": "each_quorum", + "value": { + "type": "String", + "value": "each_quorum" + } + }, + { + "id": "quorum", + "value": { + "type": "String", + "value": "quorum" + } + }, + { + "id": "local_quorum", + "value": { + "type": "String", + "value": "local_quorum" + } + }, + { + "id": "one", + "value": { + "type": "String", + "value": "one" + } + }, + { + "id": "two", + "value": { + "type": "String", + "value": "two" + } + }, + { + "id": "three", + "value": { + "type": "String", + "value": "three" + } + }, + { + "id": "local_one", + "value": { + "type": "String", + "value": "local_one" + } + }, + { + "id": "any", + "value": { + "type": "String", + "value": "any" + } + }, + { + "id": "serial", + "value": { + "type": "String", + "value": "serial" + } + }, + { + "id": "local_serial", + "value": { + "type": "String", + "value": "local_serial" + } + } + ] + }, + "brief": "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).\n", + "tag": "tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cassandra.idempotence", + "type": { + "type": "Boolean" + }, + "brief": "Whether or not the query is idempotent.\n", + "tag": "tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cassandra.page_size", + "type": { + "type": "Int" + }, + "brief": "The fetch size used for paging, i.e. how many rows will be returned at once.\n", + "examples": { + "type": "Ints", + "values": [ + 5000 + ] + }, + "tag": "tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cassandra.speculative_execution_count", + "type": { + "type": "Int" + }, + "brief": "The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively.\n", + "examples": { + "type": "Ints", + "values": [ + 0, + 2 + ] + }, + "tag": "tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cassandra.table", + "type": { + "type": "String" + }, + "brief": "The name of the primary Cassandra table that the operation is acting upon, including the keyspace name (if applicable).", + "examples": { + "type": "String", + "value": "mytable" + }, + "tag": "tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + }, + "note": "This mirrors the db.sql.table attribute but references cassandra rather than sql. It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set.\n" + }, + { + "name": "db.connection_string", + "type": { + "type": "String" + }, + "brief": "The connection string used to connect to the database. It is recommended to remove embedded credentials.\n", + "examples": { + "type": "String", + "value": "Server=(localdb)\\v11.0;Integrated Security=true;" + }, + "tag": "db-generic", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cosmosdb.client_id", + "type": { + "type": "String" + }, + "brief": "Unique Cosmos client instance id.", + "examples": { + "type": "String", + "value": "3ba4827d-4422-483f-b59f-85b74211c11d" + }, + "tag": "tech-specific-cosmosdb", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cosmosdb.connection_mode", + "type": { + "type": "Enum", + "allow_custom_values": false, + "members": [ + { + "id": "gateway", + "value": { + "type": "String", + "value": "gateway" + }, + "brief": "Gateway (HTTP) connections mode" + }, + { + "id": "direct", + "value": { + "type": "String", + "value": "direct" + }, + "brief": "Direct connection." + } + ] + }, + "brief": "Cosmos client connection mode.", + "tag": "tech-specific-cosmosdb", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cosmosdb.container", + "type": { + "type": "String" + }, + "brief": "Cosmos DB container name.", + "examples": { + "type": "String", + "value": "anystring" + }, + "tag": "tech-specific-cosmosdb", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cosmosdb.operation_type", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "invalid", + "value": { + "type": "String", + "value": "Invalid" + } + }, + { + "id": "create", + "value": { + "type": "String", + "value": "Create" + } + }, + { + "id": "patch", + "value": { + "type": "String", + "value": "Patch" + } + }, + { + "id": "read", + "value": { + "type": "String", + "value": "Read" + } + }, + { + "id": "read_feed", + "value": { + "type": "String", + "value": "ReadFeed" + } + }, + { + "id": "delete", + "value": { + "type": "String", + "value": "Delete" + } + }, + { + "id": "replace", + "value": { + "type": "String", + "value": "Replace" + } + }, + { + "id": "execute", + "value": { + "type": "String", + "value": "Execute" + } + }, + { + "id": "query", + "value": { + "type": "String", + "value": "Query" + } + }, + { + "id": "head", + "value": { + "type": "String", + "value": "Head" + } + }, + { + "id": "head_feed", + "value": { + "type": "String", + "value": "HeadFeed" + } + }, + { + "id": "upsert", + "value": { + "type": "String", + "value": "Upsert" + } + }, + { + "id": "batch", + "value": { + "type": "String", + "value": "Batch" + } + }, + { + "id": "query_plan", + "value": { + "type": "String", + "value": "QueryPlan" + } + }, + { + "id": "execute_javascript", + "value": { + "type": "String", + "value": "ExecuteJavaScript" + } + } + ] + }, + "brief": "CosmosDB Operation Type.", + "tag": "tech-specific-cosmosdb", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cosmosdb.request_charge", + "type": { + "type": "Double" + }, + "brief": "RU consumed for that operation", + "examples": { + "type": "Doubles", + "values": [ + 46.18, + 1.0 + ] + }, + "tag": "tech-specific-cosmosdb", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cosmosdb.request_content_length", + "type": { + "type": "Int" + }, + "brief": "Request payload size in bytes", + "tag": "tech-specific-cosmosdb", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cosmosdb.status_code", + "type": { + "type": "Int" + }, + "brief": "Cosmos DB status code.", + "examples": { + "type": "Ints", + "values": [ + 200, + 201 + ] + }, + "tag": "tech-specific-cosmosdb", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cosmosdb.sub_status_code", + "type": { + "type": "Int" + }, + "brief": "Cosmos DB sub status code.", + "examples": { + "type": "Ints", + "values": [ + 1000, + 1002 + ] + }, + "tag": "tech-specific-cosmosdb", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.elasticsearch.cluster.name", + "type": { + "type": "String" + }, + "brief": "Represents the identifier of an Elasticsearch cluster.\n", + "examples": { + "type": "Strings", + "values": [ + "e9106fc68e3044f0b1475b04bf4ffd5f" + ] + }, + "tag": "tech-specific-elasticsearch", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.elasticsearch.node.name", + "type": { + "type": "String" + }, + "brief": "Represents the human-readable identifier of the node/instance to which a request was routed.\n", + "examples": { + "type": "Strings", + "values": [ + "instance-0000000001" + ] + }, + "tag": "tech-specific-elasticsearch", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.elasticsearch.path_parts", + "type": { + "type": "template[string]" + }, + "brief": "A dynamic value in the url path.\n", + "examples": { + "type": "Strings", + "values": [ + "db.elasticsearch.path_parts.index=test-index", + "db.elasticsearch.path_parts.doc_id=123" + ] + }, + "tag": "tech-specific-elasticsearch", + "requirement_level": { + "type": "Recommended" + }, + "note": "Many Elasticsearch url paths allow dynamic values. These SHOULD be recorded in span attributes in the format `db.elasticsearch.path_parts.`, where `` is the url path part name. The implementation SHOULD reference the [elasticsearch schema](https://raw.githubusercontent.com/elastic/elasticsearch-specification/main/output/schema/schema.json) in order to map the path part values to their names.\n" + }, + { + "name": "db.jdbc.driver_classname", + "type": { + "type": "String" + }, + "brief": "The fully-qualified class name of the [Java Database Connectivity (JDBC)](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/) driver used to connect.\n", + "examples": { + "type": "Strings", + "values": [ + "org.postgresql.Driver", + "com.microsoft.sqlserver.jdbc.SQLServerDriver" + ] + }, + "tag": "tech-specific-jdbc", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.mongodb.collection", + "type": { + "type": "String" + }, + "brief": "The MongoDB collection being accessed within the database stated in `db.name`.\n", + "examples": { + "type": "Strings", + "values": [ + "customers", + "products" + ] + }, + "tag": "tech-specific-mongodb", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.mssql.instance_name", + "type": { + "type": "String" + }, + "brief": "The Microsoft SQL Server [instance name](https://docs.microsoft.com/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) connecting to. This name is used to determine the port of a named instance.\n", + "examples": { + "type": "String", + "value": "MSSQLSERVER" + }, + "tag": "tech-specific-mssql", + "requirement_level": { + "type": "Recommended" + }, + "note": "If setting a `db.mssql.instance_name`, `server.port` is no longer required (but still recommended if non-standard).\n" + }, + { + "name": "db.name", + "type": { + "type": "String" + }, + "brief": "This attribute is used to report the name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails).\n", + "examples": { + "type": "Strings", + "values": [ + "customers", + "main" + ] + }, + "tag": "db-generic", + "requirement_level": { + "type": "Recommended" + }, + "note": "In some SQL databases, the database name to be used is called \"schema name\". In case there are multiple layers that could be considered for database name (e.g. Oracle instance name and schema name), the database name to be used is the more specific layer (e.g. Oracle schema name).\n" + }, + { + "name": "db.operation", + "type": { + "type": "String" + }, + "brief": "The name of the operation being executed, e.g. the [MongoDB command name](https://docs.mongodb.com/manual/reference/command/#database-operations) such as `findAndModify`, or the SQL keyword.\n", + "examples": { + "type": "Strings", + "values": [ + "findAndModify", + "HMSET", + "SELECT" + ] + }, + "tag": "db-generic", + "requirement_level": { + "type": "Recommended" + }, + "note": "When setting this to an SQL keyword, it is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if the operation name is provided by the library being instrumented. If the SQL statement has an ambiguous operation, or performs more than one operation, this value may be omitted.\n" + }, + { + "name": "db.redis.database_index", + "type": { + "type": "Int" + }, + "brief": "The index of the database being accessed as used in the [`SELECT` command](https://redis.io/commands/select), provided as an integer. To be used instead of the generic `db.name` attribute.\n", + "examples": { + "type": "Ints", + "values": [ + 0, + 1, + 15 + ] + }, + "tag": "tech-specific-redis", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.sql.table", + "type": { + "type": "String" + }, + "brief": "The name of the primary table that the operation is acting upon, including the database name (if applicable).", + "examples": { + "type": "Strings", + "values": [ + "public.users", + "customers" + ] + }, + "tag": "tech-specific-sql", + "requirement_level": { + "type": "Recommended" + }, + "note": "It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set.\n" + }, + { + "name": "db.statement", + "type": { + "type": "String" + }, + "brief": "The database statement being executed.\n", + "examples": { + "type": "Strings", + "values": [ + "SELECT * FROM wuser_table", + "SET mykey \"WuValue\"" + ] + }, + "tag": "db-generic", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.system", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "other_sql", + "value": { + "type": "String", + "value": "other_sql" + }, + "brief": "Some other SQL database. Fallback only. See notes." + }, + { + "id": "mssql", + "value": { + "type": "String", + "value": "mssql" + }, + "brief": "Microsoft SQL Server" + }, + { + "id": "mssqlcompact", + "value": { + "type": "String", + "value": "mssqlcompact" + }, + "brief": "Microsoft SQL Server Compact" + }, + { + "id": "mysql", + "value": { + "type": "String", + "value": "mysql" + }, + "brief": "MySQL" + }, + { + "id": "oracle", + "value": { + "type": "String", + "value": "oracle" + }, + "brief": "Oracle Database" + }, + { + "id": "db2", + "value": { + "type": "String", + "value": "db2" + }, + "brief": "IBM Db2" + }, + { + "id": "postgresql", + "value": { + "type": "String", + "value": "postgresql" + }, + "brief": "PostgreSQL" + }, + { + "id": "redshift", + "value": { + "type": "String", + "value": "redshift" + }, + "brief": "Amazon Redshift" + }, + { + "id": "hive", + "value": { + "type": "String", + "value": "hive" + }, + "brief": "Apache Hive" + }, + { + "id": "cloudscape", + "value": { + "type": "String", + "value": "cloudscape" + }, + "brief": "Cloudscape" + }, + { + "id": "hsqldb", + "value": { + "type": "String", + "value": "hsqldb" + }, + "brief": "HyperSQL DataBase" + }, + { + "id": "progress", + "value": { + "type": "String", + "value": "progress" + }, + "brief": "Progress Database" + }, + { + "id": "maxdb", + "value": { + "type": "String", + "value": "maxdb" + }, + "brief": "SAP MaxDB" + }, + { + "id": "hanadb", + "value": { + "type": "String", + "value": "hanadb" + }, + "brief": "SAP HANA" + }, + { + "id": "ingres", + "value": { + "type": "String", + "value": "ingres" + }, + "brief": "Ingres" + }, + { + "id": "firstsql", + "value": { + "type": "String", + "value": "firstsql" + }, + "brief": "FirstSQL" + }, + { + "id": "edb", + "value": { + "type": "String", + "value": "edb" + }, + "brief": "EnterpriseDB" + }, + { + "id": "cache", + "value": { + "type": "String", + "value": "cache" + }, + "brief": "InterSystems Caché" + }, + { + "id": "adabas", + "value": { + "type": "String", + "value": "adabas" + }, + "brief": "Adabas (Adaptable Database System)" + }, + { + "id": "firebird", + "value": { + "type": "String", + "value": "firebird" + }, + "brief": "Firebird" + }, + { + "id": "derby", + "value": { + "type": "String", + "value": "derby" + }, + "brief": "Apache Derby" + }, + { + "id": "filemaker", + "value": { + "type": "String", + "value": "filemaker" + }, + "brief": "FileMaker" + }, + { + "id": "informix", + "value": { + "type": "String", + "value": "informix" + }, + "brief": "Informix" + }, + { + "id": "instantdb", + "value": { + "type": "String", + "value": "instantdb" + }, + "brief": "InstantDB" + }, + { + "id": "interbase", + "value": { + "type": "String", + "value": "interbase" + }, + "brief": "InterBase" + }, + { + "id": "mariadb", + "value": { + "type": "String", + "value": "mariadb" + }, + "brief": "MariaDB" + }, + { + "id": "netezza", + "value": { + "type": "String", + "value": "netezza" + }, + "brief": "Netezza" + }, + { + "id": "pervasive", + "value": { + "type": "String", + "value": "pervasive" + }, + "brief": "Pervasive PSQL" + }, + { + "id": "pointbase", + "value": { + "type": "String", + "value": "pointbase" + }, + "brief": "PointBase" + }, + { + "id": "sqlite", + "value": { + "type": "String", + "value": "sqlite" + }, + "brief": "SQLite" + }, + { + "id": "sybase", + "value": { + "type": "String", + "value": "sybase" + }, + "brief": "Sybase" + }, + { + "id": "teradata", + "value": { + "type": "String", + "value": "teradata" + }, + "brief": "Teradata" + }, + { + "id": "vertica", + "value": { + "type": "String", + "value": "vertica" + }, + "brief": "Vertica" + }, + { + "id": "h2", + "value": { + "type": "String", + "value": "h2" + }, + "brief": "H2" + }, + { + "id": "coldfusion", + "value": { + "type": "String", + "value": "coldfusion" + }, + "brief": "ColdFusion IMQ" + }, + { + "id": "cassandra", + "value": { + "type": "String", + "value": "cassandra" + }, + "brief": "Apache Cassandra" + }, + { + "id": "hbase", + "value": { + "type": "String", + "value": "hbase" + }, + "brief": "Apache HBase" + }, + { + "id": "mongodb", + "value": { + "type": "String", + "value": "mongodb" + }, + "brief": "MongoDB" + }, + { + "id": "redis", + "value": { + "type": "String", + "value": "redis" + }, + "brief": "Redis" + }, + { + "id": "couchbase", + "value": { + "type": "String", + "value": "couchbase" + }, + "brief": "Couchbase" + }, + { + "id": "couchdb", + "value": { + "type": "String", + "value": "couchdb" + }, + "brief": "CouchDB" + }, + { + "id": "cosmosdb", + "value": { + "type": "String", + "value": "cosmosdb" + }, + "brief": "Microsoft Azure Cosmos DB" + }, + { + "id": "dynamodb", + "value": { + "type": "String", + "value": "dynamodb" + }, + "brief": "Amazon DynamoDB" + }, + { + "id": "neo4j", + "value": { + "type": "String", + "value": "neo4j" + }, + "brief": "Neo4j" + }, + { + "id": "geode", + "value": { + "type": "String", + "value": "geode" + }, + "brief": "Apache Geode" + }, + { + "id": "elasticsearch", + "value": { + "type": "String", + "value": "elasticsearch" + }, + "brief": "Elasticsearch" + }, + { + "id": "memcached", + "value": { + "type": "String", + "value": "memcached" + }, + "brief": "Memcached" + }, + { + "id": "cockroachdb", + "value": { + "type": "String", + "value": "cockroachdb" + }, + "brief": "CockroachDB" + }, + { + "id": "opensearch", + "value": { + "type": "String", + "value": "opensearch" + }, + "brief": "OpenSearch" + }, + { + "id": "clickhouse", + "value": { + "type": "String", + "value": "clickhouse" + }, + "brief": "ClickHouse" + }, + { + "id": "spanner", + "value": { + "type": "String", + "value": "spanner" + }, + "brief": "Cloud Spanner" + }, + { + "id": "trino", + "value": { + "type": "String", + "value": "trino" + }, + "brief": "Trino" + } + ] + }, + "brief": "An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers.", + "tag": "db-generic", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.user", + "type": { + "type": "String" + }, + "brief": "Username for accessing the database.\n", + "examples": { + "type": "Strings", + "values": [ + "readonly_user", + "reporting_user" + ] + }, + "tag": "db-generic", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.instance.id", + "type": { + "type": "String" + }, + "brief": "An identifier (address, unique name, or any other identifier) of the database instance that is executing queries or mutations on the current connection. This is useful in cases where the database is running in a clustered environment and the instrumentation is able to record the node executing the query. The client may obtain this value in databases like MySQL using queries like `select @@hostname`.\n", + "examples": { + "type": "String", + "value": "mysql-e26b99z.example.com" + }, + "tag": "db-generic", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "http.request.body.size", + "type": { + "type": "Int" + }, + "brief": "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.\n", + "examples": { + "type": "Int", + "value": 3495 + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Experimental" + }, + { + "name": "http.request.header", + "type": { + "type": "template[string[]]" + }, + "brief": "HTTP request headers, `` being the normalized HTTP Header name (lowercase), the value being the header values.\n", + "examples": { + "type": "Strings", + "values": [ + "http.request.header.content-type=[\"application/json\"]", + "http.request.header.x-forwarded-for=[\"1.2.3.4\", \"1.2.3.5\"]" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\nThe `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.\nThe 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.\n", + "stability": "Stable" + }, + { + "name": "http.request.method", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "connect", + "value": { + "type": "String", + "value": "CONNECT" + }, + "brief": "CONNECT method." + }, + { + "id": "delete", + "value": { + "type": "String", + "value": "DELETE" + }, + "brief": "DELETE method." + }, + { + "id": "get", + "value": { + "type": "String", + "value": "GET" + }, + "brief": "GET method." + }, + { + "id": "head", + "value": { + "type": "String", + "value": "HEAD" + }, + "brief": "HEAD method." + }, + { + "id": "options", + "value": { + "type": "String", + "value": "OPTIONS" + }, + "brief": "OPTIONS method." + }, + { + "id": "patch", + "value": { + "type": "String", + "value": "PATCH" + }, + "brief": "PATCH method." + }, + { + "id": "post", + "value": { + "type": "String", + "value": "POST" + }, + "brief": "POST method." + }, + { + "id": "put", + "value": { + "type": "String", + "value": "PUT" + }, + "brief": "PUT method." + }, + { + "id": "trace", + "value": { + "type": "String", + "value": "TRACE" + }, + "brief": "TRACE method." + }, + { + "id": "other", + "value": { + "type": "String", + "value": "_OTHER" + }, + "brief": "Any HTTP method that the instrumentation has no prior knowledge of." + } + ] + }, + "brief": "HTTP request method.", + "examples": { + "type": "Strings", + "values": [ + "GET", + "POST", + "HEAD" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "HTTP request method value SHOULD be \"known\" to the instrumentation.\nBy default, this convention defines \"known\" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods)\nand the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html).\n\nIf the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER`.\n\nIf the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override\nthe list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named\nOTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods\n(this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults).\n\nHTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly.\nInstrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent.\nTracing instrumentations that do so, MUST also set `http.request.method_original` to the original value.\n", + "stability": "Stable" + }, + { + "name": "http.request.method_original", + "type": { + "type": "String" + }, + "brief": "Original HTTP method sent by the client in the request line.", + "examples": { + "type": "Strings", + "values": [ + "GeT", + "ACL", + "foo" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "http.request.resend_count", + "type": { + "type": "Int" + }, + "brief": "The ordinal number of request resending attempt (for any reason, including redirects).\n", + "examples": { + "type": "Int", + "value": 3 + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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).\n", + "stability": "Stable" + }, + { + "name": "http.response.body.size", + "type": { + "type": "Int" + }, + "brief": "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.\n", + "examples": { + "type": "Int", + "value": 3495 + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Experimental" + }, + { + "name": "http.response.header", + "type": { + "type": "template[string[]]" + }, + "brief": "HTTP response headers, `` being the normalized HTTP Header name (lowercase), the value being the header values.\n", + "examples": { + "type": "Strings", + "values": [ + "http.response.header.content-type=[\"application/json\"]", + "http.response.header.my-custom-header=[\"abc\", \"def\"]" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\nUsers MAY explicitly configure instrumentations to capture them even though it is not recommended.\nThe 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.\n", + "stability": "Stable" + }, + { + "name": "http.response.status_code", + "type": { + "type": "Int" + }, + "brief": "[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).", + "examples": { + "type": "Ints", + "values": [ + 200 + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "http.route", + "type": { + "type": "String" + }, + "brief": "The matched route, that is, the path template in the format used by the respective server framework.\n", + "examples": { + "type": "Strings", + "values": [ + "/users/:userID?", + "{controller}/{action}/{id?}" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\nSHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one.", + "stability": "Stable" + }, + { + "name": "network.carrier.icc", + "type": { + "type": "String" + }, + "brief": "The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network.", + "examples": { + "type": "String", + "value": "DE" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.carrier.mcc", + "type": { + "type": "String" + }, + "brief": "The mobile carrier country code.", + "examples": { + "type": "String", + "value": "310" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.carrier.mnc", + "type": { + "type": "String" + }, + "brief": "The mobile carrier network code.", + "examples": { + "type": "String", + "value": "001" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.carrier.name", + "type": { + "type": "String" + }, + "brief": "The name of the mobile carrier.", + "examples": { + "type": "String", + "value": "sprint" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.connection.subtype", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "gprs", + "value": { + "type": "String", + "value": "gprs" + }, + "brief": "GPRS" + }, + { + "id": "edge", + "value": { + "type": "String", + "value": "edge" + }, + "brief": "EDGE" + }, + { + "id": "umts", + "value": { + "type": "String", + "value": "umts" + }, + "brief": "UMTS" + }, + { + "id": "cdma", + "value": { + "type": "String", + "value": "cdma" + }, + "brief": "CDMA" + }, + { + "id": "evdo_0", + "value": { + "type": "String", + "value": "evdo_0" + }, + "brief": "EVDO Rel. 0" + }, + { + "id": "evdo_a", + "value": { + "type": "String", + "value": "evdo_a" + }, + "brief": "EVDO Rev. A" + }, + { + "id": "cdma2000_1xrtt", + "value": { + "type": "String", + "value": "cdma2000_1xrtt" + }, + "brief": "CDMA2000 1XRTT" + }, + { + "id": "hsdpa", + "value": { + "type": "String", + "value": "hsdpa" + }, + "brief": "HSDPA" + }, + { + "id": "hsupa", + "value": { + "type": "String", + "value": "hsupa" + }, + "brief": "HSUPA" + }, + { + "id": "hspa", + "value": { + "type": "String", + "value": "hspa" + }, + "brief": "HSPA" + }, + { + "id": "iden", + "value": { + "type": "String", + "value": "iden" + }, + "brief": "IDEN" + }, + { + "id": "evdo_b", + "value": { + "type": "String", + "value": "evdo_b" + }, + "brief": "EVDO Rev. B" + }, + { + "id": "lte", + "value": { + "type": "String", + "value": "lte" + }, + "brief": "LTE" + }, + { + "id": "ehrpd", + "value": { + "type": "String", + "value": "ehrpd" + }, + "brief": "EHRPD" + }, + { + "id": "hspap", + "value": { + "type": "String", + "value": "hspap" + }, + "brief": "HSPAP" + }, + { + "id": "gsm", + "value": { + "type": "String", + "value": "gsm" + }, + "brief": "GSM" + }, + { + "id": "td_scdma", + "value": { + "type": "String", + "value": "td_scdma" + }, + "brief": "TD-SCDMA" + }, + { + "id": "iwlan", + "value": { + "type": "String", + "value": "iwlan" + }, + "brief": "IWLAN" + }, + { + "id": "nr", + "value": { + "type": "String", + "value": "nr" + }, + "brief": "5G NR (New Radio)" + }, + { + "id": "nrnsa", + "value": { + "type": "String", + "value": "nrnsa" + }, + "brief": "5G NRNSA (New Radio Non-Standalone)" + }, + { + "id": "lte_ca", + "value": { + "type": "String", + "value": "lte_ca" + }, + "brief": "LTE CA" + } + ] + }, + "brief": "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.", + "examples": { + "type": "String", + "value": "LTE" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.connection.type", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "wifi", + "value": { + "type": "String", + "value": "wifi" + } + }, + { + "id": "wired", + "value": { + "type": "String", + "value": "wired" + } + }, + { + "id": "cell", + "value": { + "type": "String", + "value": "cell" + } + }, + { + "id": "unavailable", + "value": { + "type": "String", + "value": "unavailable" + } + }, + { + "id": "unknown", + "value": { + "type": "String", + "value": "unknown" + } + } + ] + }, + "brief": "The internet connection type.", + "examples": { + "type": "String", + "value": "wifi" + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "network.local.address", + "type": { + "type": "String" + }, + "brief": "Local address of the network connection - IP address or Unix domain socket name.", + "examples": { + "type": "Strings", + "values": [ + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "network.local.port", + "type": { + "type": "Int" + }, + "brief": "Local port number of the network connection.", + "examples": { + "type": "Ints", + "values": [ + 65123 + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "network.peer.address", + "type": { + "type": "String" + }, + "brief": "Peer address of the network connection - IP address or Unix domain socket name.", + "examples": { + "type": "Strings", + "values": [ + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "network.peer.port", + "type": { + "type": "Int" + }, + "brief": "Peer port number of the network connection.", + "examples": { + "type": "Ints", + "values": [ + 65123 + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "network.protocol.name", + "type": { + "type": "String" + }, + "brief": "[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.", + "examples": { + "type": "Strings", + "values": [ + "amqp", + "http", + "mqtt" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "The value SHOULD be normalized to lowercase.", + "stability": "Stable" + }, + { + "name": "network.protocol.version", + "type": { + "type": "String" + }, + "brief": "Version of the protocol specified in `network.protocol.name`.", + "examples": { + "type": "String", + "value": "3.1.1" + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "`network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`.\n", + "stability": "Stable" + }, + { + "name": "network.transport", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "tcp", + "value": { + "type": "String", + "value": "tcp" + }, + "brief": "TCP" + }, + { + "id": "udp", + "value": { + "type": "String", + "value": "udp" + }, + "brief": "UDP" + }, + { + "id": "pipe", + "value": { + "type": "String", + "value": "pipe" + }, + "brief": "Named or anonymous pipe." + }, + { + "id": "unix", + "value": { + "type": "String", + "value": "unix" + }, + "brief": "Unix domain socket" + } + ] + }, + "brief": "[OSI transport layer](https://osi-model.com/transport-layer/) or [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication).\n", + "examples": { + "type": "Strings", + "values": [ + "tcp", + "udp" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "The value SHOULD be normalized to lowercase.\n\nConsider always setting the transport when setting a port number, since\na port number is ambiguous without knowing the transport. For example\ndifferent processes could be listening on TCP port 12345 and UDP port 12345.\n", + "stability": "Stable" + }, + { + "name": "network.type", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "ipv4", + "value": { + "type": "String", + "value": "ipv4" + }, + "brief": "IPv4" + }, + { + "id": "ipv6", + "value": { + "type": "String", + "value": "ipv6" + }, + "brief": "IPv6" + } + ] + }, + "brief": "[OSI network layer](https://osi-model.com/network-layer/) or non-OSI equivalent.", + "examples": { + "type": "Strings", + "values": [ + "ipv4", + "ipv6" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "The value SHOULD be normalized to lowercase.", + "stability": "Stable" + }, + { + "name": "network.io.direction", + "type": { + "type": "Enum", + "allow_custom_values": false, + "members": [ + { + "id": "transmit", + "value": { + "type": "String", + "value": "transmit" + } + }, + { + "id": "receive", + "value": { + "type": "String", + "value": "receive" + } + } + ] + }, + "brief": "The network IO operation direction.", + "examples": { + "type": "Strings", + "values": [ + "transmit" + ] + }, + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "server.address", + "type": { + "type": "String" + }, + "brief": "Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.", + "examples": { + "type": "Strings", + "values": [ + "example.com", + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n", + "stability": "Stable" + }, + { + "name": "server.port", + "type": { + "type": "Int" + }, + "brief": "Server port number.", + "examples": { + "type": "Ints", + "values": [ + 80, + 8080, + 443 + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n", + "stability": "Stable" + }, + { + "name": "url.scheme", + "type": { + "type": "String" + }, + "brief": "The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol.", + "examples": { + "type": "Strings", + "values": [ + "https", + "ftp", + "telnet" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "url.full", + "type": { + "type": "String" + }, + "brief": "Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986)", + "examples": { + "type": "Strings", + "values": [ + "https://www.foo.bar/search?q=OpenTelemetry#SemConv", + "//localhost" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n`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/`.\n`url.full` SHOULD capture the absolute URL when it is available (or can be reconstructed) and SHOULD NOT be validated or modified except for sanitizing purposes.\n", + "stability": "Stable" + }, + { + "name": "url.path", + "type": { + "type": "String" + }, + "brief": "The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component", + "examples": { + "type": "Strings", + "values": [ + "/search" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "url.query", + "type": { + "type": "String" + }, + "brief": "The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component", + "examples": { + "type": "Strings", + "values": [ + "q=OpenTelemetry" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "note": "Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it.", + "stability": "Stable" + }, + { + "name": "url.fragment", + "type": { + "type": "String" + }, + "brief": "The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component", + "examples": { + "type": "Strings", + "values": [ + "SemConv" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "user_agent.original", + "type": { + "type": "String" + }, + "brief": "Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client.\n", + "examples": { + "type": "Strings", + "values": [ + "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" + ] + }, + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "db.system", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "other_sql", + "value": { + "type": "String", + "value": "other_sql" + }, + "brief": "Some other SQL database. Fallback only. See notes." + }, + { + "id": "mssql", + "value": { + "type": "String", + "value": "mssql" + }, + "brief": "Microsoft SQL Server" + }, + { + "id": "mssqlcompact", + "value": { + "type": "String", + "value": "mssqlcompact" + }, + "brief": "Microsoft SQL Server Compact" + }, + { + "id": "mysql", + "value": { + "type": "String", + "value": "mysql" + }, + "brief": "MySQL" + }, + { + "id": "oracle", + "value": { + "type": "String", + "value": "oracle" + }, + "brief": "Oracle Database" + }, + { + "id": "db2", + "value": { + "type": "String", + "value": "db2" + }, + "brief": "IBM Db2" + }, + { + "id": "postgresql", + "value": { + "type": "String", + "value": "postgresql" + }, + "brief": "PostgreSQL" + }, + { + "id": "redshift", + "value": { + "type": "String", + "value": "redshift" + }, + "brief": "Amazon Redshift" + }, + { + "id": "hive", + "value": { + "type": "String", + "value": "hive" + }, + "brief": "Apache Hive" + }, + { + "id": "cloudscape", + "value": { + "type": "String", + "value": "cloudscape" + }, + "brief": "Cloudscape" + }, + { + "id": "hsqldb", + "value": { + "type": "String", + "value": "hsqldb" + }, + "brief": "HyperSQL DataBase" + }, + { + "id": "progress", + "value": { + "type": "String", + "value": "progress" + }, + "brief": "Progress Database" + }, + { + "id": "maxdb", + "value": { + "type": "String", + "value": "maxdb" + }, + "brief": "SAP MaxDB" + }, + { + "id": "hanadb", + "value": { + "type": "String", + "value": "hanadb" + }, + "brief": "SAP HANA" + }, + { + "id": "ingres", + "value": { + "type": "String", + "value": "ingres" + }, + "brief": "Ingres" + }, + { + "id": "firstsql", + "value": { + "type": "String", + "value": "firstsql" + }, + "brief": "FirstSQL" + }, + { + "id": "edb", + "value": { + "type": "String", + "value": "edb" + }, + "brief": "EnterpriseDB" + }, + { + "id": "cache", + "value": { + "type": "String", + "value": "cache" + }, + "brief": "InterSystems Caché" + }, + { + "id": "adabas", + "value": { + "type": "String", + "value": "adabas" + }, + "brief": "Adabas (Adaptable Database System)" + }, + { + "id": "firebird", + "value": { + "type": "String", + "value": "firebird" + }, + "brief": "Firebird" + }, + { + "id": "derby", + "value": { + "type": "String", + "value": "derby" + }, + "brief": "Apache Derby" + }, + { + "id": "filemaker", + "value": { + "type": "String", + "value": "filemaker" + }, + "brief": "FileMaker" + }, + { + "id": "informix", + "value": { + "type": "String", + "value": "informix" + }, + "brief": "Informix" + }, + { + "id": "instantdb", + "value": { + "type": "String", + "value": "instantdb" + }, + "brief": "InstantDB" + }, + { + "id": "interbase", + "value": { + "type": "String", + "value": "interbase" + }, + "brief": "InterBase" + }, + { + "id": "mariadb", + "value": { + "type": "String", + "value": "mariadb" + }, + "brief": "MariaDB" + }, + { + "id": "netezza", + "value": { + "type": "String", + "value": "netezza" + }, + "brief": "Netezza" + }, + { + "id": "pervasive", + "value": { + "type": "String", + "value": "pervasive" + }, + "brief": "Pervasive PSQL" + }, + { + "id": "pointbase", + "value": { + "type": "String", + "value": "pointbase" + }, + "brief": "PointBase" + }, + { + "id": "sqlite", + "value": { + "type": "String", + "value": "sqlite" + }, + "brief": "SQLite" + }, + { + "id": "sybase", + "value": { + "type": "String", + "value": "sybase" + }, + "brief": "Sybase" + }, + { + "id": "teradata", + "value": { + "type": "String", + "value": "teradata" + }, + "brief": "Teradata" + }, + { + "id": "vertica", + "value": { + "type": "String", + "value": "vertica" + }, + "brief": "Vertica" + }, + { + "id": "h2", + "value": { + "type": "String", + "value": "h2" + }, + "brief": "H2" + }, + { + "id": "coldfusion", + "value": { + "type": "String", + "value": "coldfusion" + }, + "brief": "ColdFusion IMQ" + }, + { + "id": "cassandra", + "value": { + "type": "String", + "value": "cassandra" + }, + "brief": "Apache Cassandra" + }, + { + "id": "hbase", + "value": { + "type": "String", + "value": "hbase" + }, + "brief": "Apache HBase" + }, + { + "id": "mongodb", + "value": { + "type": "String", + "value": "mongodb" + }, + "brief": "MongoDB" + }, + { + "id": "redis", + "value": { + "type": "String", + "value": "redis" + }, + "brief": "Redis" + }, + { + "id": "couchbase", + "value": { + "type": "String", + "value": "couchbase" + }, + "brief": "Couchbase" + }, + { + "id": "couchdb", + "value": { + "type": "String", + "value": "couchdb" + }, + "brief": "CouchDB" + }, + { + "id": "cosmosdb", + "value": { + "type": "String", + "value": "cosmosdb" + }, + "brief": "Microsoft Azure Cosmos DB" + }, + { + "id": "dynamodb", + "value": { + "type": "String", + "value": "dynamodb" + }, + "brief": "Amazon DynamoDB" + }, + { + "id": "neo4j", + "value": { + "type": "String", + "value": "neo4j" + }, + "brief": "Neo4j" + }, + { + "id": "geode", + "value": { + "type": "String", + "value": "geode" + }, + "brief": "Apache Geode" + }, + { + "id": "elasticsearch", + "value": { + "type": "String", + "value": "elasticsearch" + }, + "brief": "Elasticsearch" + }, + { + "id": "memcached", + "value": { + "type": "String", + "value": "memcached" + }, + "brief": "Memcached" + }, + { + "id": "cockroachdb", + "value": { + "type": "String", + "value": "cockroachdb" + }, + "brief": "CockroachDB" + }, + { + "id": "opensearch", + "value": { + "type": "String", + "value": "opensearch" + }, + "brief": "OpenSearch" + }, + { + "id": "clickhouse", + "value": { + "type": "String", + "value": "clickhouse" + }, + "brief": "ClickHouse" + }, + { + "id": "spanner", + "value": { + "type": "String", + "value": "spanner" + }, + "brief": "Cloud Spanner" + }, + { + "id": "trino", + "value": { + "type": "String", + "value": "trino" + }, + "brief": "Trino" + } + ] + }, + "brief": "An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers.", + "tag": "connection-level", + "requirement_level": { + "type": "Required" + } + }, + { + "name": "db.connection_string", + "type": { + "type": "String" + }, + "brief": "The connection string used to connect to the database. It is recommended to remove embedded credentials.\n", + "examples": { + "type": "String", + "value": "Server=(localdb)\\v11.0;Integrated Security=true;" + }, + "tag": "connection-level", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.user", + "type": { + "type": "String" + }, + "brief": "Username for accessing the database.\n", + "examples": { + "type": "Strings", + "values": [ + "readonly_user", + "reporting_user" + ] + }, + "tag": "connection-level", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.jdbc.driver_classname", + "type": { + "type": "String" + }, + "brief": "The fully-qualified class name of the [Java Database Connectivity (JDBC)](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/) driver used to connect.\n", + "examples": { + "type": "Strings", + "values": [ + "org.postgresql.Driver", + "com.microsoft.sqlserver.jdbc.SQLServerDriver" + ] + }, + "tag": "connection-level-tech-specific", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.name", + "type": { + "type": "String" + }, + "brief": "This attribute is used to report the name of the database being accessed. For commands that switch the database, this should be set to the target database (even if the command fails).\n", + "examples": { + "type": "Strings", + "values": [ + "customers", + "main" + ] + }, + "tag": "call-level", + "requirement_level": { + "type": "ConditionallyRequired", + "text": "If applicable." + }, + "note": "In some SQL databases, the database name to be used is called \"schema name\". In case there are multiple layers that could be considered for database name (e.g. Oracle instance name and schema name), the database name to be used is the more specific layer (e.g. Oracle schema name).\n" + }, + { + "name": "db.statement", + "type": { + "type": "String" + }, + "brief": "The database statement being executed.\n", + "examples": { + "type": "Strings", + "values": [ + "SELECT * FROM wuser_table", + "SET mykey \"WuValue\"" + ] + }, + "tag": "call-level", + "requirement_level": { + "type": "Recommended", + "text": "Should be collected by default only if there is sanitization that excludes sensitive information.\n" + } + }, + { + "name": "db.operation", + "type": { + "type": "String" + }, + "brief": "The name of the operation being executed, e.g. the [MongoDB command name](https://docs.mongodb.com/manual/reference/command/#database-operations) such as `findAndModify`, or the SQL keyword.\n", + "examples": { + "type": "Strings", + "values": [ + "findAndModify", + "HMSET", + "SELECT" + ] + }, + "tag": "call-level", + "requirement_level": { + "type": "ConditionallyRequired", + "text": "If `db.statement` is not applicable." + }, + "note": "When setting this to an SQL keyword, it is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if the operation name is provided by the library being instrumented. If the SQL statement has an ambiguous operation, or performs more than one operation, this value may be omitted.\n" + }, + { + "name": "server.address", + "type": { + "type": "String" + }, + "brief": "Name of the database host.\n", + "examples": { + "type": "Strings", + "values": [ + "example.com", + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "tag": "connection-level", + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n", + "stability": "Stable" + }, + { + "name": "server.port", + "type": { + "type": "Int" + }, + "brief": "Server port number.", + "examples": { + "type": "Ints", + "values": [ + 80, + 8080, + 443 + ] + }, + "tag": "connection-level", + "requirement_level": { + "type": "ConditionallyRequired", + "text": "If using a port other than the default port for this DBMS and if `server.address` is set." + }, + "note": "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.\n", + "stability": "Stable" + }, + { + "name": "network.peer.address", + "type": { + "type": "String" + }, + "brief": "Peer address of the network connection - IP address or Unix domain socket name.", + "examples": { + "type": "Strings", + "values": [ + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "tag": "connection-level", + "requirement_level": { + "type": "Recommended" + }, + "stability": "Stable" + }, + { + "name": "network.peer.port", + "type": { + "type": "Int" + }, + "brief": "Peer port number of the network connection.", + "examples": { + "type": "Ints", + "values": [ + 65123 + ] + }, + "tag": "connection-level", + "requirement_level": { + "type": "Recommended", + "text": "If `network.peer.address` is set." + }, + "stability": "Stable" + }, + { + "name": "network.transport", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "tcp", + "value": { + "type": "String", + "value": "tcp" + }, + "brief": "TCP" + }, + { + "id": "udp", + "value": { + "type": "String", + "value": "udp" + }, + "brief": "UDP" + }, + { + "id": "pipe", + "value": { + "type": "String", + "value": "pipe" + }, + "brief": "Named or anonymous pipe." + }, + { + "id": "unix", + "value": { + "type": "String", + "value": "unix" + }, + "brief": "Unix domain socket" + } + ] + }, + "brief": "[OSI transport layer](https://osi-model.com/transport-layer/) or [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication).\n", + "examples": { + "type": "Strings", + "values": [ + "tcp", + "udp" + ] + }, + "tag": "connection-level", + "requirement_level": { + "type": "Recommended" + }, + "note": "The value SHOULD be normalized to lowercase.\n\nConsider always setting the transport when setting a port number, since\na port number is ambiguous without knowing the transport. For example\ndifferent processes could be listening on TCP port 12345 and UDP port 12345.\n", + "stability": "Stable" + }, + { + "name": "network.type", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "ipv4", + "value": { + "type": "String", + "value": "ipv4" + }, + "brief": "IPv4" + }, + { + "id": "ipv6", + "value": { + "type": "String", + "value": "ipv6" + }, + "brief": "IPv6" + } + ] + }, + "brief": "[OSI network layer](https://osi-model.com/network-layer/) or non-OSI equivalent.", + "examples": { + "type": "Strings", + "values": [ + "ipv4", + "ipv6" + ] + }, + "tag": "connection-level", + "requirement_level": { + "type": "Recommended" + }, + "note": "The value SHOULD be normalized to lowercase.", + "stability": "Stable" + }, + { + "name": "db.instance.id", + "type": { + "type": "String" + }, + "brief": "An identifier (address, unique name, or any other identifier) of the database instance that is executing queries or mutations on the current connection. This is useful in cases where the database is running in a clustered environment and the instrumentation is able to record the node executing the query. The client may obtain this value in databases like MySQL using queries like `select @@hostname`.\n", + "examples": { + "type": "String", + "value": "mysql-e26b99z.example.com" + }, + "tag": "connection-level", + "requirement_level": { + "type": "Recommended", + "text": "If different from the `server.address`" + } + }, + { + "name": "db.mssql.instance_name", + "type": { + "type": "String" + }, + "brief": "The Microsoft SQL Server [instance name](https://docs.microsoft.com/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) connecting to. This name is used to determine the port of a named instance.\n", + "examples": { + "type": "String", + "value": "MSSQLSERVER" + }, + "tag": "connection-level-tech-specific", + "requirement_level": { + "type": "Recommended" + }, + "note": "If setting a `db.mssql.instance_name`, `server.port` is no longer required (but still recommended if non-standard).\n" + }, + { + "name": "db.name", + "type": { + "type": "String" + }, + "brief": "The keyspace name in Cassandra.\n", + "examples": { + "type": "Strings", + "values": [ + "mykeyspace" + ] + }, + "tag": "call-level-tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + }, + "note": "For Cassandra the `db.name` should be set to the Cassandra keyspace name." + }, + { + "name": "db.cassandra.page_size", + "type": { + "type": "Int" + }, + "brief": "The fetch size used for paging, i.e. how many rows will be returned at once.\n", + "examples": { + "type": "Ints", + "values": [ + 5000 + ] + }, + "tag": "call-level-tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cassandra.consistency_level", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "all", + "value": { + "type": "String", + "value": "all" + } + }, + { + "id": "each_quorum", + "value": { + "type": "String", + "value": "each_quorum" + } + }, + { + "id": "quorum", + "value": { + "type": "String", + "value": "quorum" + } + }, + { + "id": "local_quorum", + "value": { + "type": "String", + "value": "local_quorum" + } + }, + { + "id": "one", + "value": { + "type": "String", + "value": "one" + } + }, + { + "id": "two", + "value": { + "type": "String", + "value": "two" + } + }, + { + "id": "three", + "value": { + "type": "String", + "value": "three" + } + }, + { + "id": "local_one", + "value": { + "type": "String", + "value": "local_one" + } + }, + { + "id": "any", + "value": { + "type": "String", + "value": "any" + } + }, + { + "id": "serial", + "value": { + "type": "String", + "value": "serial" + } + }, + { + "id": "local_serial", + "value": { + "type": "String", + "value": "local_serial" + } + } + ] + }, + "brief": "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).\n", + "tag": "call-level-tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cassandra.table", + "type": { + "type": "String" + }, + "brief": "The name of the primary Cassandra table that the operation is acting upon, including the keyspace name (if applicable).", + "examples": { + "type": "String", + "value": "mytable" + }, + "tag": "call-level-tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + }, + "note": "This mirrors the db.sql.table attribute but references cassandra rather than sql. It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set.\n" + }, + { + "name": "db.cassandra.idempotence", + "type": { + "type": "Boolean" + }, + "brief": "Whether or not the query is idempotent.\n", + "tag": "call-level-tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cassandra.speculative_execution_count", + "type": { + "type": "Int" + }, + "brief": "The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively.\n", + "examples": { + "type": "Ints", + "values": [ + 0, + 2 + ] + }, + "tag": "call-level-tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cassandra.coordinator.id", + "type": { + "type": "String" + }, + "brief": "The ID of the coordinating node for a query.\n", + "examples": { + "type": "String", + "value": "be13faa2-8574-4d71-926d-27f16cf8a7af" + }, + "tag": "call-level-tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cassandra.coordinator.dc", + "type": { + "type": "String" + }, + "brief": "The data center of the coordinating node for a query.\n", + "examples": { + "type": "String", + "value": "us-west-2" + }, + "tag": "call-level-tech-specific-cassandra", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.name", + "type": { + "type": "String" + }, + "brief": "The HBase namespace.\n", + "examples": { + "type": "Strings", + "values": [ + "mynamespace" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended" + }, + "note": "For HBase the `db.name` should be set to the HBase namespace." + }, + { + "name": "db.operation", + "type": { + "type": "String" + }, + "brief": "The HTTP method + the target REST route.\n", + "examples": { + "type": "Strings", + "values": [ + "GET /{db}/{docid}" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended" + }, + "note": "In **CouchDB**, `db.operation` should be set to the HTTP method + the target REST route according to the API reference documentation. For example, when retrieving a document, `db.operation` would be set to (literally, i.e., without replacing the placeholders with concrete values): [`GET /{db}/{docid}`](http://docs.couchdb.org/en/stable/api/document/common.html#get--db-docid).\n" + }, + { + "name": "db.redis.database_index", + "type": { + "type": "Int" + }, + "brief": "The index of the database being accessed as used in the [`SELECT` command](https://redis.io/commands/select), provided as an integer. To be used instead of the generic `db.name` attribute.\n", + "examples": { + "type": "Ints", + "values": [ + 0, + 1, + 15 + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "ConditionallyRequired", + "text": "If other than the default database (`0`)." + } + }, + { + "name": "db.statement", + "type": { + "type": "String" + }, + "brief": "The full syntax of the Redis CLI command.\n", + "examples": { + "type": "Strings", + "values": [ + "HMSET myhash field1 'Hello' field2 'World'" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended" + }, + "note": "For **Redis**, the value provided for `db.statement` SHOULD correspond to the syntax of the Redis CLI. If, for example, the [`HMSET` command](https://redis.io/commands/hmset) is invoked, `\"HMSET myhash field1 'Hello' field2 'World'\"` would be a suitable value for `db.statement`.\n" + }, + { + "name": "db.mongodb.collection", + "type": { + "type": "String" + }, + "brief": "The MongoDB collection being accessed within the database stated in `db.name`.\n", + "examples": { + "type": "Strings", + "values": [ + "customers", + "products" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Required" + } + }, + { + "name": "http.request.method", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "connect", + "value": { + "type": "String", + "value": "CONNECT" + }, + "brief": "CONNECT method." + }, + { + "id": "delete", + "value": { + "type": "String", + "value": "DELETE" + }, + "brief": "DELETE method." + }, + { + "id": "get", + "value": { + "type": "String", + "value": "GET" + }, + "brief": "GET method." + }, + { + "id": "head", + "value": { + "type": "String", + "value": "HEAD" + }, + "brief": "HEAD method." + }, + { + "id": "options", + "value": { + "type": "String", + "value": "OPTIONS" + }, + "brief": "OPTIONS method." + }, + { + "id": "patch", + "value": { + "type": "String", + "value": "PATCH" + }, + "brief": "PATCH method." + }, + { + "id": "post", + "value": { + "type": "String", + "value": "POST" + }, + "brief": "POST method." + }, + { + "id": "put", + "value": { + "type": "String", + "value": "PUT" + }, + "brief": "PUT method." + }, + { + "id": "trace", + "value": { + "type": "String", + "value": "TRACE" + }, + "brief": "TRACE method." + }, + { + "id": "other", + "value": { + "type": "String", + "value": "_OTHER" + }, + "brief": "Any HTTP method that the instrumentation has no prior knowledge of." + } + ] + }, + "brief": "HTTP request method.", + "examples": { + "type": "Strings", + "values": [ + "GET", + "POST", + "HEAD" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Required" + }, + "note": "HTTP request method value SHOULD be \"known\" to the instrumentation.\nBy default, this convention defines \"known\" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods)\nand the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html).\n\nIf the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER`.\n\nIf the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override\nthe list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named\nOTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods\n(this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults).\n\nHTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly.\nInstrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent.\nTracing instrumentations that do so, MUST also set `http.request.method_original` to the original value.\n", + "stability": "Stable" + }, + { + "name": "db.operation", + "type": { + "type": "String" + }, + "brief": "The endpoint identifier for the request.", + "examples": { + "type": "Strings", + "values": [ + "search", + "ml.close_job", + "cat.aliases" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Required" + }, + "note": "When setting this to an SQL keyword, it is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if the operation name is provided by the library being instrumented. If the SQL statement has an ambiguous operation, or performs more than one operation, this value may be omitted.\n" + }, + { + "name": "url.full", + "type": { + "type": "String" + }, + "brief": "Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986)", + "examples": { + "type": "Strings", + "values": [ + "https://localhost:9200/index/_search?q=user.id:kimchy" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Required" + }, + "note": "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.\n`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/`.\n`url.full` SHOULD capture the absolute URL when it is available (or can be reconstructed) and SHOULD NOT be validated or modified except for sanitizing purposes.\n", + "stability": "Stable" + }, + { + "name": "db.statement", + "type": { + "type": "String" + }, + "brief": "The request body for a [search-type query](https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html), as a json string.", + "examples": { + "type": "Strings", + "values": [ + "\"{\\\"query\\\":{\\\"term\\\":{\\\"user.id\\\":\\\"kimchy\\\"}}}\"" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended", + "text": "Should be collected by default for search-type queries and only if there is sanitization that excludes sensitive information.\n" + } + }, + { + "name": "server.address", + "type": { + "type": "String" + }, + "brief": "Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name.", + "examples": { + "type": "Strings", + "values": [ + "example.com", + "10.1.2.80", + "/tmp/my.sock" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n", + "stability": "Stable" + }, + { + "name": "server.port", + "type": { + "type": "Int" + }, + "brief": "Server port number.", + "examples": { + "type": "Ints", + "values": [ + 80, + 8080, + 443 + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended" + }, + "note": "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.\n", + "stability": "Stable" + }, + { + "name": "db.elasticsearch.cluster.name", + "type": { + "type": "String" + }, + "brief": "Represents the identifier of an Elasticsearch cluster.\n", + "examples": { + "type": "Strings", + "values": [ + "e9106fc68e3044f0b1475b04bf4ffd5f" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended", + "text": "When communicating with an Elastic Cloud deployment, this should be collected from the \"X-Found-Handling-Cluster\" HTTP response header.\n" + } + }, + { + "name": "db.elasticsearch.node.name", + "type": { + "type": "String" + }, + "brief": "Represents the human-readable identifier of the node/instance to which a request was routed.\n", + "examples": { + "type": "Strings", + "values": [ + "instance-0000000001" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended", + "text": "When communicating with an Elastic Cloud deployment, this should be collected from the \"X-Found-Handling-Instance\" HTTP response header.\n" + } + }, + { + "name": "db.elasticsearch.path_parts", + "type": { + "type": "template[string]" + }, + "brief": "A dynamic value in the url path.\n", + "examples": { + "type": "Strings", + "values": [ + "db.elasticsearch.path_parts.index=test-index", + "db.elasticsearch.path_parts.doc_id=123" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "ConditionallyRequired", + "text": "when the url has dynamic values" + }, + "note": "Many Elasticsearch url paths allow dynamic values. These SHOULD be recorded in span attributes in the format `db.elasticsearch.path_parts.`, where `` is the url path part name. The implementation SHOULD reference the [elasticsearch schema](https://raw.githubusercontent.com/elastic/elasticsearch-specification/main/output/schema/schema.json) in order to map the path part values to their names.\n" + }, + { + "name": "db.sql.table", + "type": { + "type": "String" + }, + "brief": "The name of the primary table that the operation is acting upon, including the database name (if applicable).", + "examples": { + "type": "Strings", + "values": [ + "public.users", + "customers" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended" + }, + "note": "It is not recommended to attempt any client-side parsing of `db.statement` just to get this property, but it should be set if it is provided by the library being instrumented. If the operation is acting upon an anonymous table, or more than one table, this value MUST NOT be set.\n" + }, + { + "name": "db.cosmosdb.client_id", + "type": { + "type": "String" + }, + "brief": "Unique Cosmos client instance id.", + "examples": { + "type": "String", + "value": "3ba4827d-4422-483f-b59f-85b74211c11d" + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cosmosdb.operation_type", + "type": { + "type": "Enum", + "allow_custom_values": true, + "members": [ + { + "id": "invalid", + "value": { + "type": "String", + "value": "Invalid" + } + }, + { + "id": "create", + "value": { + "type": "String", + "value": "Create" + } + }, + { + "id": "patch", + "value": { + "type": "String", + "value": "Patch" + } + }, + { + "id": "read", + "value": { + "type": "String", + "value": "Read" + } + }, + { + "id": "read_feed", + "value": { + "type": "String", + "value": "ReadFeed" + } + }, + { + "id": "delete", + "value": { + "type": "String", + "value": "Delete" + } + }, + { + "id": "replace", + "value": { + "type": "String", + "value": "Replace" + } + }, + { + "id": "execute", + "value": { + "type": "String", + "value": "Execute" + } + }, + { + "id": "query", + "value": { + "type": "String", + "value": "Query" + } + }, + { + "id": "head", + "value": { + "type": "String", + "value": "Head" + } + }, + { + "id": "head_feed", + "value": { + "type": "String", + "value": "HeadFeed" + } + }, + { + "id": "upsert", + "value": { + "type": "String", + "value": "Upsert" + } + }, + { + "id": "batch", + "value": { + "type": "String", + "value": "Batch" + } + }, + { + "id": "query_plan", + "value": { + "type": "String", + "value": "QueryPlan" + } + }, + { + "id": "execute_javascript", + "value": { + "type": "String", + "value": "ExecuteJavaScript" + } + } + ] + }, + "brief": "CosmosDB Operation Type.", + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "ConditionallyRequired", + "text": "when performing one of the operations in this list" + } + }, + { + "name": "user_agent.original", + "type": { + "type": "String" + }, + "brief": "Full user-agent string is generated by Cosmos DB SDK", + "examples": { + "type": "Strings", + "values": [ + "cosmos-netstandard-sdk/3.23.0\\|3.23.1\\|1\\|X64\\|Linux 5.4.0-1098-azure 104 18\\|.NET Core 3.1.32\\|S\\|" + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended" + }, + "note": "The user-agent value is generated by SDK which is a combination of
`sdk_version` : Current version of SDK. e.g. 'cosmos-netstandard-sdk/3.23.0'
`direct_pkg_version` : Direct package version used by Cosmos DB SDK. e.g. '3.23.1'
`number_of_client_instances` : Number of cosmos client instances created by the application. e.g. '1'
`type_of_machine_architecture` : Machine architecture. e.g. 'X64'
`operating_system` : Operating System. e.g. 'Linux 5.4.0-1098-azure 104 18'
`runtime_framework` : Runtime Framework. e.g. '.NET Core 3.1.32'
`failover_information` : Generated key to determine if region failover enabled.\n Format Reg-{D (Disabled discovery)}-S(application region)|L(List of preferred regions)|N(None, user did not configure it).\n Default value is \"NS\".\n", + "stability": "Stable" + }, + { + "name": "db.cosmosdb.connection_mode", + "type": { + "type": "Enum", + "allow_custom_values": false, + "members": [ + { + "id": "gateway", + "value": { + "type": "String", + "value": "gateway" + }, + "brief": "Gateway (HTTP) connections mode" + }, + { + "id": "direct", + "value": { + "type": "String", + "value": "direct" + }, + "brief": "Direct connection." + } + ] + }, + "brief": "Cosmos client connection mode.", + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "ConditionallyRequired", + "text": "if not `direct` (or pick gw as default)" + } + }, + { + "name": "db.cosmosdb.container", + "type": { + "type": "String" + }, + "brief": "Cosmos DB container name.", + "examples": { + "type": "String", + "value": "anystring" + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "ConditionallyRequired", + "text": "if available" + } + }, + { + "name": "db.cosmosdb.request_content_length", + "type": { + "type": "Int" + }, + "brief": "Request payload size in bytes", + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "Recommended" + } + }, + { + "name": "db.cosmosdb.status_code", + "type": { + "type": "Int" + }, + "brief": "Cosmos DB status code.", + "examples": { + "type": "Ints", + "values": [ + 200, + 201 + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "ConditionallyRequired", + "text": "if response was received" + } + }, + { + "name": "db.cosmosdb.sub_status_code", + "type": { + "type": "Int" + }, + "brief": "Cosmos DB sub status code.", + "examples": { + "type": "Ints", + "values": [ + 1000, + 1002 + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "ConditionallyRequired", + "text": "when response was received and contained sub-code." + } + }, + { + "name": "db.cosmosdb.request_charge", + "type": { + "type": "Double" + }, + "brief": "RU consumed for that operation", + "examples": { + "type": "Doubles", + "values": [ + 46.18, + 1.0 + ] + }, + "tag": "call-level-tech-specific", + "requirement_level": { + "type": "ConditionallyRequired", + "text": "when available" + } + } +] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-7-spans/expected-registry.json b/crates/weaver_resolver/data/registry-test-7-spans/expected-registry.json new file mode 100644 index 00000000..ac588fb7 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-7-spans/expected-registry.json @@ -0,0 +1,2508 @@ +{ + "registry_url": "https://semconv-registry.com", + "groups": [ + { + "id": "registry.db", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "This document defines the attributes used to describe telemetry in the context of databases.\n", + "prefix": "db", + "attributes": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/registry-db.yaml" + } + }, + { + "id": "registry.http", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "This document defines semantic convention attributes in the HTTP namespace.", + "prefix": "http", + "attributes": [ + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/registry-http.yaml" + } + }, + { + "id": "registry.network", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "These attributes may be used for any network related operation.\n", + "prefix": "network", + "attributes": [ + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/registry-network.yaml" + } + }, + { + "id": "server", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "These attributes may be used to describe the server in a connection-based network interaction where there is one side that initiates the connection (the client is the side that initiates the connection). This covers all TCP network interactions since TCP is connection-based and one side initiates the connection (an exception is made for peer-to-peer communication over TCP where the \"user-facing\" surface of the protocol / API doesn't expose a clear notion of client and server). This also covers UDP network interactions where one side initiates the interaction, e.g. QUIC (HTTP/3) and DNS.\n", + "prefix": "server", + "attributes": [ + 54, + 55 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/registry-server.yaml" + } + }, + { + "id": "registry.url", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Attributes describing URL.", + "prefix": "url", + "attributes": [ + 56, + 57, + 58, + 59, + 60 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/registry-url.yaml" + } + }, + { + "id": "registry.user_agent", + "typed_group": { + "type": "AttributeGroup" + }, + "brief": "Describes user-agent attributes.", + "prefix": "user_agent", + "attributes": [ + 61 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/registry-user-agent.yaml" + } + }, + { + "id": "db", + "typed_group": { + "type": "Span", + "span_kind": "Client", + "events": [] + }, + "brief": "This document defines the attributes used to perform database client calls.\n", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/trace-database.yaml", + "attributes": { + "62": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "63": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "64": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "65": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "66": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "67": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "68": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "69": { + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "server" + } + }, + "70": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "server" + } + }, + "71": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.network" + } + }, + "72": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.network" + } + }, + "73": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.network" + } + }, + "74": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.network" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.network" + } + }, + "75": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + } + } + } + }, + { + "id": "db.mssql", + "typed_group": { + "type": "Span", + "span_kind": null, + "events": [] + }, + "brief": "Connection-level attributes for Microsoft SQL Server\n", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/trace-database.yaml", + "attributes": { + "62": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "63": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "64": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "65": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "66": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "67": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "68": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "69": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "70": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "71": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "72": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "73": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "76": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + } + } + } + }, + { + "id": "db.cassandra", + "typed_group": { + "type": "Span", + "span_kind": null, + "events": [] + }, + "brief": "Call-level attributes for Cassandra\n", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/trace-database.yaml", + "attributes": { + "62": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "63": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "64": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "65": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "66": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "67": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "68": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "69": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "70": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "71": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "72": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "73": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "77": { + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "78": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "79": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "80": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "81": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "82": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "83": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "84": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + } + } + } + }, + { + "id": "db.hbase", + "typed_group": { + "type": "Span", + "span_kind": null, + "events": [] + }, + "brief": "Call-level attributes for HBase\n", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 85 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/trace-database.yaml", + "attributes": { + "62": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "63": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "64": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "65": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "66": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "67": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "68": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "69": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "70": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "71": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "72": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "73": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "85": { + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + } + } + } + }, + { + "id": "db.couchdb", + "typed_group": { + "type": "Span", + "span_kind": null, + "events": [] + }, + "brief": "Call-level attributes for CouchDB\n", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 86 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/trace-database.yaml", + "attributes": { + "62": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "63": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "64": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "65": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "66": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "67": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "68": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "69": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "70": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "71": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "72": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "73": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "86": { + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + } + } + } + }, + { + "id": "db.redis", + "typed_group": { + "type": "Span", + "span_kind": null, + "events": [] + }, + "brief": "Call-level attributes for Redis\n", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 87, + 88 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/trace-database.yaml", + "attributes": { + "62": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "63": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "64": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "65": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "66": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "67": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "68": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "69": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "70": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "71": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "72": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "73": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "87": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "88": { + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + } + } + } + }, + { + "id": "db.mongodb", + "typed_group": { + "type": "Span", + "span_kind": null, + "events": [] + }, + "brief": "Call-level attributes for MongoDB\n", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 89 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/trace-database.yaml", + "attributes": { + "62": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "63": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "64": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "65": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "66": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "67": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "68": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "69": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "70": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "71": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "72": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "73": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "89": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + } + } + } + }, + { + "id": "db.elasticsearch", + "typed_group": { + "type": "Span", + "span_kind": null, + "events": [] + }, + "brief": "Call-level attributes for Elasticsearch\n", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/trace-database.yaml", + "attributes": { + "62": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "63": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "64": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "65": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "66": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "67": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "68": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "69": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "70": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "71": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "72": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "73": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "90": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.http" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.http" + } + }, + "91": { + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "92": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.url" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.url" + } + }, + "93": { + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "94": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "server" + } + }, + "95": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "server" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "server" + } + }, + "96": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "97": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "98": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + } + } + } + }, + { + "id": "db.sql", + "typed_group": { + "type": "Span", + "span_kind": null, + "events": [] + }, + "brief": "Call-level attributes for SQL databases\n", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 99 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/trace-database.yaml", + "attributes": { + "62": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "63": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "64": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "65": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "66": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "67": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "68": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "69": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "70": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "71": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "72": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "73": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "99": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + } + } + } + }, + { + "id": "db.cosmosdb", + "typed_group": { + "type": "Span", + "span_kind": null, + "events": [] + }, + "brief": "Call-level attributes for Cosmos DB.\n", + "prefix": "db.cosmosdb", + "attributes": [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108 + ], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/trace-database.yaml", + "attributes": { + "62": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "63": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "64": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "65": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "66": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "67": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "68": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "69": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "70": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "71": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "72": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "73": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "74": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "75": { + "GroupAttributes": { + "resolution_mode": "Extends", + "group_id": "db" + } + }, + "100": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "101": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "102": { + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.user_agent" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.user_agent" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.user_agent" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.user_agent" + } + }, + "103": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "104": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "105": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeRequirementLevel": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "106": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "107": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + }, + "108": { + "AttributeBrief": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeExamples": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeSamplingRelevant": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeNote": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeStability": { + "resolution_mode": "Reference", + "group_id": "registry.db" + }, + "AttributeDeprecated": { + "resolution_mode": "Reference", + "group_id": "registry.db" + } + } + } + } + }, + { + "id": "db.tech", + "typed_group": { + "type": "Span", + "span_kind": null, + "events": [] + }, + "brief": "Semantic convention group for specific technologies", + "constraints": [ + { + "any_of": [], + "include": "db.cassandra" + }, + { + "any_of": [], + "include": "db.redis" + }, + { + "any_of": [], + "include": "db.mongodb" + }, + { + "any_of": [], + "include": "db.sql" + }, + { + "any_of": [], + "include": "db.cosmosdb" + } + ], + "attributes": [], + "lineage": { + "provenance": "data/registry-test-7-spans/registry/trace-database.yaml" + } + } + ] +} \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-db.yaml b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-db.yaml new file mode 100644 index 00000000..a17c1046 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-db.yaml @@ -0,0 +1,432 @@ +groups: + - id: registry.db + prefix: db + type: attribute_group + brief: > + This document defines the attributes used to describe telemetry in the context of databases. + attributes: + - id: cassandra.coordinator.dc + type: string + brief: > + The data center of the coordinating node for a query. + examples: 'us-west-2' + tag: tech-specific-cassandra + - id: cassandra.coordinator.id + type: string + brief: > + The ID of the coordinating node for a query. + examples: 'be13faa2-8574-4d71-926d-27f16cf8a7af' + tag: tech-specific-cassandra + - id: cassandra.consistency_level + brief: > + 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). + type: + members: + - id: all + value: 'all' + - id: each_quorum + value: 'each_quorum' + - id: quorum + value: 'quorum' + - id: local_quorum + value: 'local_quorum' + - id: one + value: 'one' + - id: two + value: 'two' + - id: three + value: 'three' + - id: local_one + value: 'local_one' + - id: any + value: 'any' + - id: serial + value: 'serial' + - id: local_serial + value: 'local_serial' + tag: tech-specific-cassandra + - id: cassandra.idempotence + type: boolean + brief: > + Whether or not the query is idempotent. + tag: tech-specific-cassandra + - id: cassandra.page_size + type: int + brief: > + The fetch size used for paging, i.e. how many rows will be returned at once. + examples: [5000] + tag: tech-specific-cassandra + - id: cassandra.speculative_execution_count + type: int + brief: > + The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively. + examples: [0, 2] + tag: tech-specific-cassandra + - id: cassandra.table + type: string + brief: The name of the primary Cassandra table that the operation is acting upon, including the keyspace name (if applicable). + note: > + This mirrors the db.sql.table attribute but references cassandra rather than sql. + It is not recommended to attempt any client-side parsing of + `db.statement` just to get this property, but it should be set if + it is provided by the library being instrumented. + If the operation is acting upon an anonymous table, or more than one table, this + value MUST NOT be set. + examples: 'mytable' + tag: tech-specific-cassandra + - id: connection_string + type: string + brief: > + The connection string used to connect to the database. + It is recommended to remove embedded credentials. + examples: 'Server=(localdb)\v11.0;Integrated Security=true;' + tag: db-generic + - id: cosmosdb.client_id + type: string + brief: Unique Cosmos client instance id. + examples: '3ba4827d-4422-483f-b59f-85b74211c11d' + tag: tech-specific-cosmosdb + - id: cosmosdb.connection_mode + type: + allow_custom_values: false + members: + - id: gateway + value: 'gateway' + brief: Gateway (HTTP) connections mode + - id: direct + value: 'direct' + brief: Direct connection. + brief: Cosmos client connection mode. + tag: tech-specific-cosmosdb + - id: cosmosdb.container + type: string + brief: Cosmos DB container name. + examples: 'anystring' + tag: tech-specific-cosmosdb + - id: cosmosdb.operation_type + type: + allow_custom_values: true + members: + - id: invalid + value: 'Invalid' + - id: create + value: 'Create' + - id: patch + value: 'Patch' + - id: read + value: 'Read' + - id: read_feed + value: 'ReadFeed' + - id: delete + value: 'Delete' + - id: replace + value: 'Replace' + - id: execute + value: 'Execute' + - id: query + value: 'Query' + - id: head + value: 'Head' + - id: head_feed + value: 'HeadFeed' + - id: upsert + value: 'Upsert' + - id: batch + value: 'Batch' + - id: query_plan + value: 'QueryPlan' + - id: execute_javascript + value: 'ExecuteJavaScript' + brief: CosmosDB Operation Type. + tag: tech-specific-cosmosdb + - id: cosmosdb.request_charge + type: double + brief: RU consumed for that operation + examples: [46.18, 1.0] + tag: tech-specific-cosmosdb + - id: cosmosdb.request_content_length + type: int + brief: Request payload size in bytes + tag: tech-specific-cosmosdb + - id: cosmosdb.status_code + type: int + brief: Cosmos DB status code. + examples: [200, 201] + tag: tech-specific-cosmosdb + - id: cosmosdb.sub_status_code + type: int + brief: Cosmos DB sub status code. + examples: [1000, 1002] + tag: tech-specific-cosmosdb + - id: elasticsearch.cluster.name + type: string + brief: > + Represents the identifier of an Elasticsearch cluster. + examples: ["e9106fc68e3044f0b1475b04bf4ffd5f"] + tag: tech-specific-elasticsearch + - id: elasticsearch.node.name + type: string + brief: > + Represents the human-readable identifier of the node/instance to which a request was routed. + examples: ["instance-0000000001"] + tag: tech-specific-elasticsearch + - id: elasticsearch.path_parts + type: template[string] + brief: > + A dynamic value in the url path. + note: > + Many Elasticsearch url paths allow dynamic values. These SHOULD be recorded in span attributes in the format + `db.elasticsearch.path_parts.`, where `` is the url path part name. The implementation SHOULD + reference the [elasticsearch schema](https://raw.githubusercontent.com/elastic/elasticsearch-specification/main/output/schema/schema.json) + in order to map the path part values to their names. + examples: ['db.elasticsearch.path_parts.index=test-index', 'db.elasticsearch.path_parts.doc_id=123'] + tag: tech-specific-elasticsearch + - id: jdbc.driver_classname + type: string + brief: > + The fully-qualified class name of the [Java Database Connectivity (JDBC)](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/) driver used to connect. + examples: ['org.postgresql.Driver', 'com.microsoft.sqlserver.jdbc.SQLServerDriver'] + tag: tech-specific-jdbc + - id: mongodb.collection + type: string + brief: > + The MongoDB collection being accessed within the database stated in `db.name`. + examples: [ 'customers', 'products' ] + tag: tech-specific-mongodb + - id: mssql.instance_name + type: string + note: > + If setting a `db.mssql.instance_name`, `server.port` is no longer + required (but still recommended if non-standard). + brief: > + The Microsoft SQL Server [instance name](https://docs.microsoft.com/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) + connecting to. This name is used to determine the port of a named instance. + examples: 'MSSQLSERVER' + tag: tech-specific-mssql + - id: name + type: string + brief: > + This attribute is used to report the name of the database being accessed. + For commands that switch the database, this should be set to the target database + (even if the command fails). + note: > + In some SQL databases, the database name to be used is called "schema name". + In case there are multiple layers that could be considered for database name + (e.g. Oracle instance name and schema name), + the database name to be used is the more specific layer (e.g. Oracle schema name). + examples: [ 'customers', 'main' ] + tag: db-generic + - id: operation + type: string + brief: > + The name of the operation being executed, e.g. the [MongoDB command name](https://docs.mongodb.com/manual/reference/command/#database-operations) + such as `findAndModify`, or the SQL keyword. + note: > + When setting this to an SQL keyword, it is not recommended to + attempt any client-side parsing of `db.statement` just to get this + property, but it should be set if the operation name is provided by + the library being instrumented. + If the SQL statement has an ambiguous operation, or performs more + than one operation, this value may be omitted. + examples: ['findAndModify', 'HMSET', 'SELECT'] + tag: db-generic + - id: redis.database_index + type: int + brief: > + The index of the database being accessed as used in the [`SELECT` command](https://redis.io/commands/select), provided as an integer. + To be used instead of the generic `db.name` attribute. + examples: [0, 1, 15] + tag: tech-specific-redis + - id: sql.table + type: string + brief: The name of the primary table that the operation is acting upon, including the database name (if applicable). + note: > + It is not recommended to attempt any client-side parsing of + `db.statement` just to get this property, but it should be set if + it is provided by the library being instrumented. + If the operation is acting upon an anonymous table, or more than one table, this + value MUST NOT be set. + examples: ['public.users', 'customers'] + tag: tech-specific-sql + - id: statement + type: string + brief: > + The database statement being executed. + examples: ['SELECT * FROM wuser_table', 'SET mykey "WuValue"'] + tag: db-generic + - id: system + brief: An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers. + type: + allow_custom_values: true + members: + - id: other_sql + value: 'other_sql' + brief: 'Some other SQL database. Fallback only. See notes.' + - id: mssql + value: 'mssql' + brief: 'Microsoft SQL Server' + - id: mssqlcompact + value: 'mssqlcompact' + brief: 'Microsoft SQL Server Compact' + - id: mysql + value: 'mysql' + brief: 'MySQL' + - id: oracle + value: 'oracle' + brief: 'Oracle Database' + - id: db2 + value: 'db2' + brief: 'IBM Db2' + - id: postgresql + value: 'postgresql' + brief: 'PostgreSQL' + - id: redshift + value: 'redshift' + brief: 'Amazon Redshift' + - id: hive + value: 'hive' + brief: 'Apache Hive' + - id: cloudscape + value: 'cloudscape' + brief: 'Cloudscape' + - id: hsqldb + value: 'hsqldb' + brief: 'HyperSQL DataBase' + - id: progress + value: 'progress' + brief: 'Progress Database' + - id: maxdb + value: 'maxdb' + brief: 'SAP MaxDB' + - id: hanadb + value: 'hanadb' + brief: 'SAP HANA' + - id: ingres + value: 'ingres' + brief: 'Ingres' + - id: firstsql + value: 'firstsql' + brief: 'FirstSQL' + - id: edb + value: 'edb' + brief: 'EnterpriseDB' + - id: cache + value: 'cache' + brief: 'InterSystems Caché' + - id: adabas + value: 'adabas' + brief: 'Adabas (Adaptable Database System)' + - id: firebird + value: 'firebird' + brief: 'Firebird' + - id: derby + value: 'derby' + brief: 'Apache Derby' + - id: filemaker + value: 'filemaker' + brief: 'FileMaker' + - id: informix + value: 'informix' + brief: 'Informix' + - id: instantdb + value: 'instantdb' + brief: 'InstantDB' + - id: interbase + value: 'interbase' + brief: 'InterBase' + - id: mariadb + value: 'mariadb' + brief: 'MariaDB' + - id: netezza + value: 'netezza' + brief: 'Netezza' + - id: pervasive + value: 'pervasive' + brief: 'Pervasive PSQL' + - id: pointbase + value: 'pointbase' + brief: 'PointBase' + - id: sqlite + value: 'sqlite' + brief: 'SQLite' + - id: sybase + value: 'sybase' + brief: 'Sybase' + - id: teradata + value: 'teradata' + brief: 'Teradata' + - id: vertica + value: 'vertica' + brief: 'Vertica' + - id: h2 + value: 'h2' + brief: 'H2' + - id: coldfusion + value: 'coldfusion' + brief: 'ColdFusion IMQ' + - id: cassandra + value: 'cassandra' + brief: 'Apache Cassandra' + - id: hbase + value: 'hbase' + brief: 'Apache HBase' + - id: mongodb + value: 'mongodb' + brief: 'MongoDB' + - id: redis + value: 'redis' + brief: 'Redis' + - id: couchbase + value: 'couchbase' + brief: 'Couchbase' + - id: couchdb + value: 'couchdb' + brief: 'CouchDB' + - id: cosmosdb + value: 'cosmosdb' + brief: 'Microsoft Azure Cosmos DB' + - id: dynamodb + value: 'dynamodb' + brief: 'Amazon DynamoDB' + - id: neo4j + value: 'neo4j' + brief: 'Neo4j' + - id: geode + value: 'geode' + brief: 'Apache Geode' + - id: elasticsearch + value: 'elasticsearch' + brief: 'Elasticsearch' + - id: memcached + value: 'memcached' + brief: 'Memcached' + - id: cockroachdb + value: 'cockroachdb' + brief: 'CockroachDB' + - id: opensearch + value: 'opensearch' + brief: 'OpenSearch' + - id: clickhouse + value: 'clickhouse' + brief: 'ClickHouse' + - id: spanner + value: 'spanner' + brief: 'Cloud Spanner' + - id: trino + value: 'trino' + brief: 'Trino' + tag: db-generic + - id: user + type: string + brief: > + Username for accessing the database. + examples: ['readonly_user', 'reporting_user'] + tag: db-generic + - id: instance.id + tag: db-generic + type: string + brief: > + An identifier (address, unique name, or any other identifier) of the database instance that is executing queries or mutations on the current connection. + This is useful in cases where the database is running in a clustered environment and the instrumentation is able to record the node executing the query. + The client may obtain this value in databases like MySQL using queries like `select @@hostname`. + examples: 'mysql-e26b99z.example.com' diff --git a/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-http.yaml b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-http.yaml new file mode 100644 index 00000000..5ed9a182 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-http.yaml @@ -0,0 +1,135 @@ +groups: + - id: registry.http + prefix: http + type: attribute_group + brief: 'This document defines semantic convention attributes in the HTTP namespace.' + attributes: + - id: request.body.size + type: int + brief: > + 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. + examples: 3495 + stability: experimental # this should not be marked stable with other HTTP attributes + - id: request.header + stability: stable + type: template[string[]] + brief: > + HTTP request headers, `` being the normalized HTTP Header name (lowercase), the value being the header values. + note: > + 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: ['http.request.header.content-type=["application/json"]', 'http.request.header.x-forwarded-for=["1.2.3.4", "1.2.3.5"]'] + - id: request.method + stability: stable + type: + allow_custom_values: true + members: + - id: connect + value: "CONNECT" + brief: 'CONNECT method.' + - id: delete + value: "DELETE" + brief: 'DELETE method.' + - id: get + value: "GET" + brief: 'GET method.' + - id: head + value: "HEAD" + brief: 'HEAD method.' + - id: options + value: "OPTIONS" + brief: 'OPTIONS method.' + - id: patch + value: "PATCH" + brief: 'PATCH method.' + - id: post + value: "POST" + brief: 'POST method.' + - id: put + value: "PUT" + brief: 'PUT method.' + - id: trace + value: "TRACE" + brief: 'TRACE method.' + - id: other + value: "_OTHER" + brief: 'Any HTTP method that the instrumentation has no prior knowledge of.' + brief: 'HTTP request method.' + examples: ["GET", "POST", "HEAD"] + note: | + 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) + and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html). + + 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 + (this list MUST be a full override of the default known method, 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. + - id: request.method_original + stability: stable + type: string + brief: Original HTTP method sent by the client in the request line. + examples: ["GeT", "ACL", "foo"] + - id: request.resend_count + stability: stable + type: int + brief: > + The ordinal number of request resending attempt (for any reason, including redirects). + note: > + 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 + - id: response.body.size + type: int + brief: > + 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. + examples: 3495 + stability: experimental # this should not be marked stable with other HTTP attributes + - id: response.header + stability: stable + type: template[string[]] + brief: > + HTTP response headers, `` being the normalized HTTP Header name (lowercase), the value being the header values. + note: > + 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: ['http.response.header.content-type=["application/json"]', 'http.response.header.my-custom-header=["abc", "def"]'] + - id: response.status_code + stability: stable + type: int + brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).' + examples: [200] + - id: route + stability: stable + type: string + brief: > + The matched route, that is, the path template in the format used by the respective server framework. + examples: ['/users/:userID?', '{controller}/{action}/{id?}'] + note: > + 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. \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-network.yaml b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-network.yaml new file mode 100644 index 00000000..c16763bb --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-network.yaml @@ -0,0 +1,194 @@ +groups: + - id: registry.network + prefix: network + type: attribute_group + brief: > + These attributes may be used for any network related operation. + attributes: + - id: carrier.icc + type: string + brief: "The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network." + examples: "DE" + - id: carrier.mcc + type: string + brief: "The mobile carrier country code." + examples: "310" + - id: carrier.mnc + type: string + brief: "The mobile carrier network code." + examples: "001" + - id: carrier.name + type: string + brief: "The name of the mobile carrier." + examples: "sprint" + - id: connection.subtype + type: + allow_custom_values: true + members: + - id: gprs + brief: GPRS + value: "gprs" + - id: edge + brief: EDGE + value: "edge" + - id: umts + brief: UMTS + value: "umts" + - id: cdma + brief: CDMA + value: "cdma" + - id: evdo_0 + brief: EVDO Rel. 0 + value: "evdo_0" + - id: evdo_a + brief: "EVDO Rev. A" + value: "evdo_a" + - id: cdma2000_1xrtt + brief: CDMA2000 1XRTT + value: "cdma2000_1xrtt" + - id: hsdpa + brief: HSDPA + value: "hsdpa" + - id: hsupa + brief: HSUPA + value: "hsupa" + - id: hspa + brief: HSPA + value: "hspa" + - id: iden + brief: IDEN + value: "iden" + - id: evdo_b + brief: "EVDO Rev. B" + value: "evdo_b" + - id: lte + brief: LTE + value: "lte" + - id: ehrpd + brief: EHRPD + value: "ehrpd" + - id: hspap + brief: HSPAP + value: "hspap" + - id: gsm + brief: GSM + value: "gsm" + - id: td_scdma + brief: TD-SCDMA + value: "td_scdma" + - id: iwlan + brief: IWLAN + value: "iwlan" + - id: nr + brief: "5G NR (New Radio)" + value: "nr" + - id: nrnsa + brief: "5G NRNSA (New Radio Non-Standalone)" + value: "nrnsa" + - id: lte_ca + brief: LTE CA + value: "lte_ca" + brief: '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.' + examples: 'LTE' + - id: connection.type + type: + allow_custom_values: true + members: + - id: wifi + value: "wifi" + - id: wired + value: "wired" + - id: cell + value: "cell" + - id: unavailable + value: "unavailable" + - id: unknown + value: "unknown" + brief: 'The internet connection type.' + examples: 'wifi' + - id: local.address + stability: stable + type: string + brief: Local address of the network connection - IP address or Unix domain socket name. + examples: ['10.1.2.80', '/tmp/my.sock'] + - id: local.port + stability: stable + type: int + brief: Local port number of the network connection. + examples: [65123] + - id: peer.address + stability: stable + type: string + brief: Peer address of the network connection - IP address or Unix domain socket name. + examples: ['10.1.2.80', '/tmp/my.sock'] + - id: peer.port + stability: stable + type: int + brief: Peer port number of the network connection. + examples: [65123] + - id: protocol.name + stability: stable + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + note: The value SHOULD be normalized to lowercase. + examples: ['amqp', 'http', 'mqtt'] + - id: protocol.version + stability: stable + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: '3.1.1' + note: > + `network.protocol.version` refers to the version of the protocol used and might be + different from the protocol client's version. If the HTTP client has a version + of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - id: transport + stability: stable + type: + allow_custom_values: true + members: + - id: tcp + value: 'tcp' + brief: "TCP" + - id: udp + value: 'udp' + brief: "UDP" + - id: pipe + value: "pipe" + brief: 'Named or anonymous pipe.' + - id: unix + value: 'unix' + brief: "Unix domain socket" + brief: > + [OSI transport layer](https://osi-model.com/transport-layer/) or + [inter-process communication method](https://wikipedia.org/wiki/Inter-process_communication). + note: | + 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'] + - id: type + stability: stable + type: + allow_custom_values: true + members: + - id: ipv4 + value: 'ipv4' + brief: "IPv4" + - id: ipv6 + value: 'ipv6' + brief: "IPv6" + brief: '[OSI network layer](https://osi-model.com/network-layer/) or non-OSI equivalent.' + note: The value SHOULD be normalized to lowercase. + examples: ['ipv4', 'ipv6'] + - id: io.direction + type: + allow_custom_values: false + members: + - id: transmit + value: 'transmit' + - id: receive + value: 'receive' + brief: "The network IO operation direction." + examples: ["transmit"] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-server.yaml b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-server.yaml new file mode 100644 index 00000000..0523bb0d --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-server.yaml @@ -0,0 +1,28 @@ +groups: + - id: server + prefix: server + type: attribute_group + brief: > + These attributes may be used to describe the server in a connection-based network interaction + where there is one side that initiates the connection (the client is the side that initiates the connection). + This covers all TCP network interactions since TCP is connection-based and one side initiates the + connection (an exception is made for peer-to-peer communication over TCP where the "user-facing" surface of the + protocol / API doesn't expose a clear notion of client and server). + This also covers UDP network interactions where one side initiates the interaction, e.g. QUIC (HTTP/3) and DNS. + attributes: + - id: address + stability: stable + type: string + brief: "Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name." + note: > + 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'] + - id: port + stability: stable + type: int + brief: Server port number. + note: > + 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] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-url.yaml b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-url.yaml new file mode 100644 index 00000000..3042f32c --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-url.yaml @@ -0,0 +1,41 @@ +groups: + - id: registry.url + brief: Attributes describing URL. + type: attribute_group + prefix: url + attributes: + - id: scheme + stability: stable + type: string + brief: 'The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol.' + examples: ["https", "ftp", "telnet"] + - id: full + stability: stable + type: string + brief: Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) + note: > + 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) + and SHOULD NOT be validated or modified except for sanitizing purposes. + examples: ['https://www.foo.bar/search?q=OpenTelemetry#SemConv', '//localhost'] + - id: path + stability: stable + type: string + brief: 'The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component' + examples: ['/search'] + - id: query + stability: stable + type: string + brief: 'The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component' + examples: ["q=OpenTelemetry"] + note: Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it. + - id: fragment + stability: stable + type: string + brief: 'The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component' + examples: ["SemConv"] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-user-agent.yaml b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-user-agent.yaml new file mode 100644 index 00000000..3f902d18 --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-7-spans/registry/registry-user-agent.yaml @@ -0,0 +1,13 @@ +groups: + - id: registry.user_agent + prefix: user_agent + type: attribute_group + brief: "Describes user-agent attributes." + attributes: + - id: original + stability: stable + type: string + brief: > + Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client. + 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'] \ No newline at end of file diff --git a/crates/weaver_resolver/data/registry-test-7-spans/registry/trace-database.yaml b/crates/weaver_resolver/data/registry-test-7-spans/registry/trace-database.yaml new file mode 100644 index 00000000..2946f80a --- /dev/null +++ b/crates/weaver_resolver/data/registry-test-7-spans/registry/trace-database.yaml @@ -0,0 +1,263 @@ +groups: + - id: db + type: span + brief: > + This document defines the attributes used to perform database client calls. + span_kind: client + attributes: + - ref: db.system + tag: connection-level + requirement_level: required + + - ref: db.connection_string + tag: connection-level + - ref: db.user + tag: connection-level + - ref: db.jdbc.driver_classname + tag: connection-level-tech-specific + - ref: db.name + tag: call-level + requirement_level: + conditionally_required: If applicable. + - ref: db.statement + tag: call-level + requirement_level: + recommended: > + Should be collected by default only if there is sanitization that excludes sensitive information. + - ref: db.operation + tag: call-level + requirement_level: + conditionally_required: If `db.statement` is not applicable. + - ref: server.address + tag: connection-level + brief: > + Name of the database host. + - ref: server.port + tag: connection-level + requirement_level: + conditionally_required: If using a port other than the default port for this DBMS and if `server.address` is set. + - ref: network.peer.address + tag: connection-level + - ref: network.peer.port + requirement_level: + recommended: If `network.peer.address` is set. + tag: connection-level + - ref: network.transport + tag: connection-level + - ref: network.type + tag: connection-level + - ref: db.instance.id + tag: connection-level + requirement_level: + recommended: If different from the `server.address` + + - id: db.mssql + type: span + extends: db + brief: > + Connection-level attributes for Microsoft SQL Server + attributes: + - ref: db.mssql.instance_name + tag: connection-level-tech-specific + + - id: db.cassandra + type: span + extends: db + brief: > + Call-level attributes for Cassandra + attributes: + - ref: db.name + tag: call-level-tech-specific-cassandra + brief: > + The keyspace name in Cassandra. + examples: ["mykeyspace"] + note: For Cassandra the `db.name` should be set to the Cassandra keyspace name. + - ref: db.cassandra.page_size + tag: call-level-tech-specific-cassandra + - ref: db.cassandra.consistency_level + tag: call-level-tech-specific-cassandra + - ref: db.cassandra.table + tag: call-level-tech-specific-cassandra + - ref: db.cassandra.idempotence + tag: call-level-tech-specific-cassandra + - ref: db.cassandra.speculative_execution_count + tag: call-level-tech-specific-cassandra + - ref: db.cassandra.coordinator.id + tag: call-level-tech-specific-cassandra + - ref: db.cassandra.coordinator.dc + tag: call-level-tech-specific-cassandra + + - id: db.hbase + type: span + extends: db + brief: > + Call-level attributes for HBase + attributes: + - ref: db.name + tag: call-level-tech-specific + brief: > + The HBase namespace. + examples: ['mynamespace'] + note: For HBase the `db.name` should be set to the HBase namespace. + + - id: db.couchdb + type: span + extends: db + brief: > + Call-level attributes for CouchDB + attributes: + - ref: db.operation + tag: call-level-tech-specific + brief: > + The HTTP method + the target REST route. + examples: ['GET /{db}/{docid}'] + note: > + In **CouchDB**, `db.operation` should be set to the HTTP method + + the target REST route according to the API reference documentation. + For example, when retrieving a document, `db.operation` would be set to + (literally, i.e., without replacing the placeholders with concrete values): + [`GET /{db}/{docid}`](http://docs.couchdb.org/en/stable/api/document/common.html#get--db-docid). + + - id: db.redis + type: span + extends: db + brief: > + Call-level attributes for Redis + attributes: + - ref: db.redis.database_index + requirement_level: + conditionally_required: If other than the default database (`0`). + tag: call-level-tech-specific + - ref: db.statement + tag: call-level-tech-specific + brief: > + The full syntax of the Redis CLI command. + examples: ["HMSET myhash field1 'Hello' field2 'World'"] + note: > + For **Redis**, the value provided for `db.statement` SHOULD correspond to the syntax of the Redis CLI. + If, for example, the [`HMSET` command](https://redis.io/commands/hmset) is invoked, `"HMSET myhash field1 'Hello' field2 'World'"` would be a suitable value for `db.statement`. + + - id: db.mongodb + type: span + extends: db + brief: > + Call-level attributes for MongoDB + attributes: + - ref: db.mongodb.collection + requirement_level: required + tag: call-level-tech-specific + + - id: db.elasticsearch + type: span + extends: db + brief: > + Call-level attributes for Elasticsearch + attributes: + - ref: http.request.method + requirement_level: required + tag: call-level-tech-specific + - ref: db.operation + requirement_level: required + brief: The endpoint identifier for the request. + examples: [ 'search', 'ml.close_job', 'cat.aliases' ] + tag: call-level-tech-specific + - ref: url.full + requirement_level: required + examples: [ 'https://localhost:9200/index/_search?q=user.id:kimchy' ] + tag: call-level-tech-specific + - ref: db.statement + requirement_level: + recommended: > + Should be collected by default for search-type queries and only if there is sanitization that excludes + sensitive information. + brief: The request body for a [search-type query](https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html), as a json string. + examples: [ '"{\"query\":{\"term\":{\"user.id\":\"kimchy\"}}}"' ] + tag: call-level-tech-specific + - ref: server.address + tag: call-level-tech-specific + - ref: server.port + tag: call-level-tech-specific + - ref: db.elasticsearch.cluster.name + requirement_level: + recommended: > + When communicating with an Elastic Cloud deployment, this should be collected from the "X-Found-Handling-Cluster" HTTP response header. + tag: call-level-tech-specific + - ref: db.elasticsearch.node.name + requirement_level: + recommended: > + When communicating with an Elastic Cloud deployment, this should be collected from the "X-Found-Handling-Instance" HTTP response header. + tag: call-level-tech-specific + - ref: db.elasticsearch.path_parts + requirement_level: + conditionally_required: when the url has dynamic values + tag: call-level-tech-specific + + - id: db.sql + type: span + extends: 'db' + brief: > + Call-level attributes for SQL databases + attributes: + - ref: db.sql.table + tag: call-level-tech-specific + + - id: db.cosmosdb + type: span + extends: db + prefix: db.cosmosdb + brief: > + Call-level attributes for Cosmos DB. + attributes: + - ref: db.cosmosdb.client_id + tag: call-level-tech-specific + - ref: db.cosmosdb.operation_type + requirement_level: + conditionally_required: when performing one of the operations in this list + tag: call-level-tech-specific + - ref: user_agent.original + brief: 'Full user-agent string is generated by Cosmos DB SDK' + note: > + The user-agent value is generated by SDK which is a combination of
+ `sdk_version` : Current version of SDK. e.g. 'cosmos-netstandard-sdk/3.23.0'
+ `direct_pkg_version` : Direct package version used by Cosmos DB SDK. e.g. '3.23.1'
+ `number_of_client_instances` : Number of cosmos client instances created by the application. e.g. '1'
+ `type_of_machine_architecture` : Machine architecture. e.g. 'X64'
+ `operating_system` : Operating System. e.g. 'Linux 5.4.0-1098-azure 104 18'
+ `runtime_framework` : Runtime Framework. e.g. '.NET Core 3.1.32'
+ `failover_information` : Generated key to determine if region failover enabled. + Format Reg-{D (Disabled discovery)}-S(application region)|L(List of preferred regions)|N(None, user did not configure it). + Default value is "NS". + examples: ['cosmos-netstandard-sdk/3.23.0\|3.23.1\|1\|X64\|Linux 5.4.0-1098-azure 104 18\|.NET Core 3.1.32\|S\|'] + tag: call-level-tech-specific + - ref: db.cosmosdb.connection_mode + requirement_level: + conditionally_required: if not `direct` (or pick gw as default) + tag: call-level-tech-specific + - ref: db.cosmosdb.container + requirement_level: + conditionally_required: if available + tag: call-level-tech-specific + - ref: db.cosmosdb.request_content_length + tag: call-level-tech-specific + - ref: db.cosmosdb.status_code + requirement_level: + conditionally_required: if response was received + tag: call-level-tech-specific + - ref: db.cosmosdb.sub_status_code + requirement_level: + conditionally_required: when response was received and contained sub-code. + tag: call-level-tech-specific + - ref: db.cosmosdb.request_charge + requirement_level: + conditionally_required: when available + tag: call-level-tech-specific + + - id: db.tech + type: span + brief: "Semantic convention group for specific technologies" + constraints: + - include: 'db.cassandra' + - include: 'db.redis' + - include: 'db.mongodb' + - include: 'db.sql' + - include: 'db.cosmosdb' \ No newline at end of file diff --git a/crates/weaver_resolver/src/attribute.rs b/crates/weaver_resolver/src/attribute.rs new file mode 100644 index 00000000..4ddcf53d --- /dev/null +++ b/crates/weaver_resolver/src/attribute.rs @@ -0,0 +1,524 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Attribute resolution. + +use serde::Deserialize; +use std::collections::{BTreeMap, HashMap, HashSet}; + +use weaver_resolved_schema::attribute; +use weaver_resolved_schema::attribute::AttributeRef; +use weaver_resolved_schema::lineage::{FieldId, FieldLineage, GroupLineage, ResolutionMode}; +use weaver_schema::attribute::Attribute; +use weaver_schema::tags::Tags; +use weaver_semconv::attribute::{ + AttributeSpec, AttributeTypeSpec, BasicRequirementLevelSpec, ExamplesSpec, + PrimitiveOrArrayTypeSpec, RequirementLevelSpec, TemplateTypeSpec, ValueSpec, +}; +use weaver_semconv::group::ConvTypeSpec; +use weaver_semconv::SemConvSpecs; +use weaver_version::VersionAttributeChanges; + +use crate::{stability, Error}; + +/// A catalog of deduplicated resolved attributes with their corresponding reference. +#[derive(Deserialize, Debug, Default, PartialEq)] +pub struct AttributeCatalog { + /// A map of deduplicated resolved attributes with their corresponding reference. + attribute_refs: HashMap, + #[serde(skip)] + /// A map of root attributes indexed by their name. + /// Root attributes are attributes that doesn't inherit from another attribute. + root_attributes: HashMap, +} + +#[derive(Debug, PartialEq)] +struct AttributeWithGroupId { + pub attribute: attribute::Attribute, + pub group_id: String, +} + +impl AttributeCatalog { + /// Returns the reference of the given attribute or creates a new reference if the attribute + /// does not exist in the catalog. + pub fn attribute_ref(&mut self, attr: attribute::Attribute) -> AttributeRef { + let next_id = self.attribute_refs.len() as u32; + *self + .attribute_refs + .entry(attr) + .or_insert_with(|| AttributeRef(next_id)) + } + + /// Returns a list of deduplicated attributes ordered by their references. + pub fn drain_attributes(self) -> Vec { + let mut attributes: Vec<(attribute::Attribute, AttributeRef)> = + self.attribute_refs.into_iter().collect(); + attributes.sort_by_key(|(_, attr_ref)| attr_ref.0); + attributes.into_iter().map(|(attr, _)| attr).collect() + } + + /// Tries to resolve the given attribute spec (ref or id) from the catalog. + /// Returns `None` if the attribute spec is a ref and it does not exist yet + /// in the catalog. + pub fn resolve( + &mut self, + group_id: &str, + prefix: &str, + attr: &AttributeSpec, + lineage: Option<&mut GroupLineage>, + ) -> Option { + match attr { + AttributeSpec::Ref { + r#ref, + brief, + examples, + tag, + requirement_level, + sampling_relevant, + note, + stability, + deprecated, + } => { + let root_attr = self.root_attributes.get(r#ref); + if let Some(root_attr) = root_attr { + let mut inherited_fields = vec![]; + + // Create a fully resolved attribute from an attribute spec + // (ref) and override the root attribute with the new + // values if they are present. + let resolved_attr = attribute::Attribute { + name: r#ref.clone(), + r#type: root_attr.attribute.r#type.clone(), + brief: match brief { + Some(brief) => brief.clone(), + None => { + inherited_fields.push(FieldId::AttributeBrief); + root_attr.attribute.brief.clone() + } + }, + examples: match examples { + Some(_) => semconv_to_resolved_examples(examples), + None => { + inherited_fields.push(FieldId::AttributeExamples); + root_attr.attribute.examples.clone() + } + }, + tag: match tag { + Some(_) => tag.clone(), + None => { + inherited_fields.push(FieldId::AttributeTag); + root_attr.attribute.tag.clone() + } + }, + requirement_level: match requirement_level { + Some(requirement_level) => { + semconv_to_resolved_req_level(requirement_level) + } + None => { + inherited_fields.push(FieldId::AttributeRequirementLevel); + root_attr.attribute.requirement_level.clone() + } + }, + sampling_relevant: match sampling_relevant { + Some(_) => *sampling_relevant, + None => { + inherited_fields.push(FieldId::AttributeSamplingRelevant); + root_attr.attribute.sampling_relevant + } + }, + note: match note { + Some(note) => note.clone(), + None => { + inherited_fields.push(FieldId::AttributeNote); + root_attr.attribute.note.clone() + } + }, + stability: match stability { + Some(_) => stability::resolve_stability(stability), + None => { + inherited_fields.push(FieldId::AttributeStability); + root_attr.attribute.stability.clone() + } + }, + deprecated: match deprecated { + Some(_) => deprecated.clone(), + None => { + inherited_fields.push(FieldId::AttributeDeprecated); + root_attr.attribute.deprecated.clone() + } + }, + tags: root_attr.attribute.tags.clone(), + value: root_attr.attribute.value.clone(), + }; + + let group_id = root_attr.group_id.clone(); + let attr_ref = self.attribute_ref(resolved_attr); + + // Update the lineage based on the inherited fields. + // Note: the lineage is only updated if a group lineage is provided. + if let Some(lineage) = lineage { + for field_id in inherited_fields { + lineage.add_attribute_field_lineage( + attr_ref, + field_id, + FieldLineage { + resolution_mode: ResolutionMode::Reference, + group_id: group_id.clone(), + }, + ); + } + } + + Some(attr_ref) + } else { + None + } + } + AttributeSpec::Id { + id, + r#type, + brief, + examples, + tag, + requirement_level, + sampling_relevant, + note, + stability, + deprecated, + } => { + let root_attr_id = if prefix.is_empty() { + id.clone() + } else { + format!("{}.{}", prefix, id) + }; + + // Create a fully resolved attribute from an attribute spec (id), + // and check if it already exists in the catalog. + // If it does, return the reference to the existing attribute. + // If it does not, add it to the catalog and return a new reference. + let attr = attribute::Attribute { + name: root_attr_id.clone(), + r#type: semconv_to_resolved_attr_type(r#type), + brief: brief.clone(), + examples: semconv_to_resolved_examples(examples), + tag: tag.clone(), + requirement_level: semconv_to_resolved_req_level(requirement_level), + sampling_relevant: *sampling_relevant, + note: note.clone(), + stability: stability::resolve_stability(stability), + deprecated: deprecated.clone(), + tags: None, + value: None, + }; + + self.root_attributes.insert( + root_attr_id, + AttributeWithGroupId { + attribute: attr.clone(), + group_id: group_id.to_string(), + }, + ); + Some(self.attribute_ref(attr)) + } + } + } +} + +/// Resolves a collection of attributes (i.e. `Attribute::Ref`, `Attribute::AttributeGroupRef`, +/// and `Attribute::SpanRef`) from the given semantic convention catalog and local attributes +/// (i.e. `Attribute::Id`). +/// `Attribute::AttributeGroupRef` are first resolved, then `Attribute::SpanRef`, then +/// `Attribute::Ref`, and finally `Attribute::Id` are added. +/// An `Attribute::Ref` can override an attribute contained in an `Attribute::AttributeGroupRef` +/// or an `Attribute::SpanRef`. +/// An `Attribute::Id` can override an attribute contains in an `Attribute::Ref`, an +/// `Attribute::AttributeGroupRef`, or an `Attribute::SpanRef`. +/// +/// Note: Version changes are used during the resolution process to determine the names of the +/// attributes. +pub fn resolve_attributes( + attributes: &[Attribute], + sem_conv_catalog: &weaver_semconv::SemConvSpecs, + version_changes: impl VersionAttributeChanges, +) -> Result, Error> { + let mut resolved_attrs = BTreeMap::new(); + let mut copy_into_resolved_attrs = + |attrs: HashMap<&String, &weaver_semconv::attribute::AttributeSpec>, + tags: &Option| { + for (attr_id, attr) in attrs { + let mut attr: Attribute = attr.into(); + attr.set_tags(tags); + resolved_attrs.insert(attr_id.clone(), attr); + } + }; + + // Resolve `Attribute::AttributeGroupRef` + for attribute in attributes.iter() { + if let Attribute::AttributeGroupRef { + attribute_group_ref, + tags, + } = attribute + { + let attrs = sem_conv_catalog + .attributes(attribute_group_ref, ConvTypeSpec::AttributeGroup) + .map_err(|e| Error::FailToResolveAttributes { + ids: vec![attribute_group_ref.clone()], + error: e.to_string(), + })?; + copy_into_resolved_attrs(attrs, tags); + } + } + + // Resolve `Attribute::ResourceRef` + for attribute in attributes.iter() { + if let Attribute::ResourceRef { resource_ref, tags } = attribute { + let attrs = sem_conv_catalog + .attributes(resource_ref, ConvTypeSpec::Resource) + .map_err(|e| Error::FailToResolveAttributes { + ids: vec![resource_ref.clone()], + error: e.to_string(), + })?; + copy_into_resolved_attrs(attrs, tags); + } + } + + // Resolve `Attribute::SpanRef` + for attribute in attributes.iter() { + if let Attribute::SpanRef { span_ref, tags } = attribute { + let attrs = sem_conv_catalog + .attributes(span_ref, ConvTypeSpec::Span) + .map_err(|e| Error::FailToResolveAttributes { + ids: vec![span_ref.clone()], + error: e.to_string(), + })?; + copy_into_resolved_attrs(attrs, tags); + } + } + + // Resolve `Attribute::EventRef` + for attribute in attributes.iter() { + if let Attribute::EventRef { event_ref, tags } = attribute { + let attrs = sem_conv_catalog + .attributes(event_ref, ConvTypeSpec::Event) + .map_err(|e| Error::FailToResolveAttributes { + ids: vec![event_ref.clone()], + error: e.to_string(), + })?; + copy_into_resolved_attrs(attrs, tags); + } + } + + // Resolve `Attribute::Ref` + for attribute in attributes.iter() { + if let Attribute::Ref { r#ref, .. } = attribute { + let normalized_ref = version_changes.get_attribute_name(r#ref); + let sem_conv_attr = sem_conv_catalog.attribute(&normalized_ref); + let resolved_attribute = attribute.resolve_from(sem_conv_attr).map_err(|e| { + Error::FailToResolveAttributes { + ids: vec![r#ref.clone()], + error: e.to_string(), + } + })?; + resolved_attrs.insert(normalized_ref, resolved_attribute); + } + } + + // Resolve `Attribute::Id` + // Note: any resolved attributes with the same id will be overridden. + for attribute in attributes.iter() { + if let Attribute::Id { id, .. } = attribute { + resolved_attrs.insert(id.clone(), attribute.clone()); + } + } + + Ok(resolved_attrs.into_values().collect()) +} + +/// Merges the given main attributes with the inherited attributes. +/// Main attributes have precedence over inherited attributes. +pub fn merge_attributes(main_attrs: &[Attribute], inherited_attrs: &[Attribute]) -> Vec { + let mut merged_attrs = main_attrs.to_vec(); + let main_attr_ids = main_attrs + .iter() + .map(|attr| match attr { + Attribute::Ref { r#ref, .. } => r#ref.clone(), + Attribute::Id { id, .. } => id.clone(), + Attribute::AttributeGroupRef { .. } => { + panic!("Attribute groups are not supported yet") + } + Attribute::SpanRef { .. } => { + panic!("Span references are not supported yet") + } + Attribute::ResourceRef { .. } => { + panic!("Resource references are not supported yet") + } + Attribute::EventRef { .. } => { + panic!("Event references are not supported yet") + } + }) + .collect::>(); + + for inherited_attr in inherited_attrs.iter() { + match inherited_attr { + Attribute::Ref { r#ref, .. } => { + if main_attr_ids.contains(r#ref) { + continue; + } + } + Attribute::Id { id, .. } => { + if main_attr_ids.contains(id) { + continue; + } + } + Attribute::AttributeGroupRef { .. } => { + panic!("Attribute groups are not supported yet") + } + Attribute::SpanRef { .. } => { + panic!("Span references are not supported yet") + } + Attribute::ResourceRef { .. } => { + panic!("Resource references are not supported yet") + } + Attribute::EventRef { .. } => { + panic!("Event references are not supported yet") + } + } + merged_attrs.push(inherited_attr.clone()); + } + merged_attrs +} + +/// Converts a semantic convention attribute to a resolved attribute. +pub fn resolve_attribute( + registry: &SemConvSpecs, + attr: &AttributeSpec, +) -> Result { + match attr { + AttributeSpec::Ref { r#ref, .. } => { + let sem_conv_attr = + registry + .attribute(r#ref) + .ok_or(Error::FailToResolveAttributes { + ids: vec![r#ref.clone()], + error: "Attribute ref not found in the resolved registry".to_string(), + })?; + resolve_attribute(registry, sem_conv_attr) + } + AttributeSpec::Id { + id, + r#type, + brief, + examples, + tag, + requirement_level, + sampling_relevant, + note, + stability, + deprecated, + } => Ok(attribute::Attribute { + name: id.clone(), + r#type: semconv_to_resolved_attr_type(r#type), + brief: brief.clone(), + examples: semconv_to_resolved_examples(examples), + tag: tag.clone(), + requirement_level: semconv_to_resolved_req_level(requirement_level), + sampling_relevant: *sampling_relevant, + note: note.clone(), + stability: stability::resolve_stability(stability), + deprecated: deprecated.clone(), + tags: None, + value: None, + }), + } +} + +fn semconv_to_resolved_attr_type(attr_type: &AttributeTypeSpec) -> attribute::AttributeType { + match attr_type { + AttributeTypeSpec::PrimitiveOrArray(poa) => match poa { + PrimitiveOrArrayTypeSpec::Boolean => attribute::AttributeType::Boolean, + PrimitiveOrArrayTypeSpec::Int => weaver_resolved_schema::attribute::AttributeType::Int, + PrimitiveOrArrayTypeSpec::Double => attribute::AttributeType::Double, + PrimitiveOrArrayTypeSpec::String => attribute::AttributeType::String, + PrimitiveOrArrayTypeSpec::Strings => attribute::AttributeType::Strings, + PrimitiveOrArrayTypeSpec::Ints => attribute::AttributeType::Ints, + PrimitiveOrArrayTypeSpec::Doubles => attribute::AttributeType::Doubles, + PrimitiveOrArrayTypeSpec::Booleans => attribute::AttributeType::Booleans, + }, + AttributeTypeSpec::Template(template) => match template { + TemplateTypeSpec::Boolean => attribute::AttributeType::TemplateBoolean, + TemplateTypeSpec::Int => attribute::AttributeType::TemplateInt, + TemplateTypeSpec::Double => attribute::AttributeType::TemplateDouble, + TemplateTypeSpec::String => attribute::AttributeType::TemplateString, + TemplateTypeSpec::Strings => attribute::AttributeType::TemplateStrings, + TemplateTypeSpec::Ints => attribute::AttributeType::TemplateInts, + TemplateTypeSpec::Doubles => attribute::AttributeType::TemplateDoubles, + TemplateTypeSpec::Booleans => attribute::AttributeType::TemplateBooleans, + }, + AttributeTypeSpec::Enum { + allow_custom_values, + members, + } => attribute::AttributeType::Enum { + allow_custom_values: *allow_custom_values, + members: members + .iter() + .map(|member| attribute::EnumEntries { + id: member.id.clone(), + value: match &member.value { + ValueSpec::String(s) => { + weaver_resolved_schema::value::Value::String { value: s.clone() } + } + ValueSpec::Int(i) => { + weaver_resolved_schema::value::Value::Int { value: *i } + } + ValueSpec::Double(d) => { + weaver_resolved_schema::value::Value::Double { value: *d } + } + }, + brief: member.brief.clone(), + note: member.note.clone(), + }) + .collect(), + }, + } +} + +fn semconv_to_resolved_examples(examples: &Option) -> Option { + examples.as_ref().map(|examples| match examples { + ExamplesSpec::Bool(v) => attribute::Example::Bool { value: *v }, + ExamplesSpec::Int(v) => attribute::Example::Int { value: *v }, + ExamplesSpec::Double(v) => attribute::Example::Double { value: *v }, + ExamplesSpec::String(v) => attribute::Example::String { value: v.clone() }, + ExamplesSpec::Ints(v) => attribute::Example::Ints { values: v.clone() }, + ExamplesSpec::Doubles(v) => attribute::Example::Doubles { values: v.clone() }, + ExamplesSpec::Bools(v) => attribute::Example::Bools { values: v.clone() }, + ExamplesSpec::Strings(v) => attribute::Example::Strings { values: v.clone() }, + }) +} + +fn semconv_to_resolved_req_level(req_level: &RequirementLevelSpec) -> attribute::RequirementLevel { + match req_level { + RequirementLevelSpec::Basic(level) => match level { + BasicRequirementLevelSpec::Required => attribute::RequirementLevel::Required, + BasicRequirementLevelSpec::Recommended => { + attribute::RequirementLevel::Recommended { text: None } + } + BasicRequirementLevelSpec::OptIn => attribute::RequirementLevel::OptIn, + }, + RequirementLevelSpec::Recommended { text } => attribute::RequirementLevel::Recommended { + text: Some(text.clone()), + }, + RequirementLevelSpec::ConditionallyRequired { text } => { + attribute::RequirementLevel::ConditionallyRequired { text: text.clone() } + } + } +} + +#[allow(dead_code)] // ToDo Remove this once we have values in the resolved schema +fn semconv_to_resolved_value( + value: &Option, +) -> Option { + value.as_ref().map(|value| match value { + ValueSpec::String(s) => weaver_resolved_schema::value::Value::String { value: s.clone() }, + ValueSpec::Int(i) => weaver_resolved_schema::value::Value::Int { value: *i }, + ValueSpec::Double(d) => weaver_resolved_schema::value::Value::Double { value: *d }, + }) +} diff --git a/crates/weaver_resolver/src/constraint.rs b/crates/weaver_resolver/src/constraint.rs new file mode 100644 index 00000000..458f89b2 --- /dev/null +++ b/crates/weaver_resolver/src/constraint.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Functions to resolve a semantic convention constraint field. + +use weaver_semconv::group::ConstraintSpec; + +/// Resolve a list of semantic convention constraints. +pub fn resolve_constraints( + constraints: &[ConstraintSpec], +) -> Vec { + constraints.iter().map(resolve_constraint).collect() +} + +/// Resolve a semantic convention constraint. +pub fn resolve_constraint( + constraint: &ConstraintSpec, +) -> weaver_resolved_schema::registry::Constraint { + weaver_resolved_schema::registry::Constraint { + any_of: constraint.any_of.clone(), + include: constraint.include.clone(), + } +} diff --git a/crates/weaver_resolver/src/events.rs b/crates/weaver_resolver/src/events.rs new file mode 100644 index 00000000..4e1c2ca0 --- /dev/null +++ b/crates/weaver_resolver/src/events.rs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Resolve events + +use crate::attribute::resolve_attributes; +use crate::Error; +use weaver_schema::schema_spec::SchemaSpec; +use weaver_semconv::SemConvSpecs; +use weaver_version::VersionChanges; + +/// Resolves resource events and their attributes. +pub fn resolve_events( + schema: &mut SchemaSpec, + sem_conv_catalog: &SemConvSpecs, + version_changes: &VersionChanges, +) -> Result<(), Error> { + if let Some(events) = schema.resource_events.as_mut() { + events.attributes = resolve_attributes( + events.attributes.as_ref(), + sem_conv_catalog, + version_changes.log_attribute_changes(), + )?; + for event in events.events.iter_mut() { + event.attributes = resolve_attributes( + event.attributes.as_ref(), + sem_conv_catalog, + version_changes.log_attribute_changes(), + )?; + } + } + Ok(()) +} diff --git a/crates/weaver_resolver/src/lib.rs b/crates/weaver_resolver/src/lib.rs new file mode 100644 index 00000000..568f84b1 --- /dev/null +++ b/crates/weaver_resolver/src/lib.rs @@ -0,0 +1,626 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! This crate implements the process of reference resolution for telemetry schemas. + +#![deny(missing_docs)] +#![deny(clippy::print_stdout)] +#![deny(clippy::print_stderr)] + +use std::path::Path; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering::Relaxed; +use std::time::Instant; + +use rayon::iter::IntoParallelRefIterator; +use rayon::iter::ParallelIterator; +use regex::Regex; +use url::Url; +use walkdir::DirEntry; + +use crate::attribute::AttributeCatalog; +use weaver_cache::Cache; +use weaver_logger::Logger; +use weaver_resolved_schema::catalog::Catalog; +use weaver_resolved_schema::ResolvedTelemetrySchema; +use weaver_schema::{SemConvImport, TelemetrySchema}; +use weaver_semconv::{ResolverConfig, SemConvSpec, SemConvSpecWithProvenance, SemConvSpecs}; +use weaver_version::VersionChanges; + +use crate::events::resolve_events; +use crate::metrics::{resolve_metrics, semconv_to_resolved_metric}; +use crate::registry::resolve_semconv_registry; +use crate::resource::resolve_resource; +use crate::spans::resolve_spans; + +pub mod attribute; +mod constraint; +mod events; +mod metrics; +pub mod registry; +mod resource; +mod spans; +mod stability; +mod tags; + +/// A resolver that can be used to resolve telemetry schemas. +/// All references to semantic conventions will be resolved. +pub struct SchemaResolver {} + +/// Different types of unresolved references. +#[derive(Debug)] +pub enum UnresolvedReference { + /// An unresolved attribute reference. + AttributeRef { + /// The id of the group containing the attribute reference. + group_id: String, + /// The unresolved attribute reference. + attribute_ref: String, + /// The provenance of the reference (URL or path). + provenance: String, + }, + /// An unresolved `extends` clause reference. + ExtendsRef { + /// The id of the group containing the `extends` clause reference. + group_id: String, + /// The unresolved `extends` clause reference. + extends_ref: String, + /// The provenance of the reference (URL or path). + provenance: String, + }, +} + +/// An error that can occur while resolving a telemetry schema. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// A telemetry schema error. + #[error("Telemetry schema error (error: {0:?})")] + TelemetrySchemaError(weaver_schema::Error), + + /// A parent schema error. + #[error("Parent schema error (error: {0:?})")] + ParentSchemaError(weaver_schema::Error), + + /// An invalid URL. + #[error("Invalid URL `{url:?}`, error: {error:?})")] + InvalidUrl { + /// The invalid URL. + url: String, + /// The error that occurred. + error: String, + }, + + /// A semantic convention error. + #[error("Semantic convention error: {message}")] + SemConvError { + /// The error that occurred. + message: String, + }, + + /// Failed to resolve a set of attributes. + #[error("Failed to resolve a set of attributes {ids:?}: {error}")] + FailToResolveAttributes { + /// The ids of the attributes. + ids: Vec, + /// The error that occurred. + error: String, + }, + + /// Failed to resolve a set of references. + #[error("Failed to resolve the following references {refs:?}")] + UnresolvedReferences { + /// The list of unresolved references. + refs: Vec, + }, + + /// Failed to resolve a metric. + #[error("Failed to resolve the metric '{r#ref}'")] + FailToResolveMetric { + /// The reference to the metric. + r#ref: String, + }, + + /// Metric attributes are incompatible within the metric group. + #[error("Metric attributes are incompatible within the metric group '{metric_group_ref}' for metric '{metric_ref}' (error: {error})")] + IncompatibleMetricAttributes { + /// The metric group reference. + metric_group_ref: String, + /// The reference to the metric. + metric_ref: String, + /// The error that occurred. + error: String, + }, + + /// A generic conversion error. + #[error("Conversion error: {message}")] + ConversionError { + /// The error that occurred. + message: String, + }, +} + +impl SchemaResolver { + /// Loads a telemetry schema from an URL or a file and returns the resolved + /// schema. + pub fn resolve_schema( + schema_url_or_path: &str, + cache: &Cache, + log: impl Logger + Clone + Sync, + ) -> Result { + let mut schema = Self::load_schema(schema_url_or_path, log.clone())?; + Self::resolve(&mut schema, schema_url_or_path, cache, log)?; + + Ok(schema) + } + + /// Loads a telemetry schema file and returns the resolved schema. + pub fn resolve_schema_file + Clone>( + schema_path: P, + cache: &Cache, + log: impl Logger + Clone + Sync, + ) -> Result { + let mut schema = Self::load_schema_from_path(schema_path.clone(), log.clone())?; + Self::resolve( + &mut schema, + schema_path.as_ref().to_str().unwrap(), + cache, + log, + )?; + + Ok(schema) + } + + /// Resolve the given telemetry schema. + fn resolve( + schema: &mut TelemetrySchema, + schema_path: &str, + cache: &Cache, + log: impl Logger + Clone + Sync, + ) -> Result<(), Error> { + let sem_conv_catalog = Self::semconv_registry_from_schema(schema, cache, log.clone())?; + let start = Instant::now(); + + // Merges the versions of the parent schema into the current schema. + schema.merge_versions(); + + // Generates version changes + let version_changes = schema + .versions + .as_ref() + .map(|versions| { + if let Some(latest_version) = versions.latest_version() { + versions.version_changes_for(latest_version) + } else { + VersionChanges::default() + } + }) + .unwrap_or_default(); + + // Resolve the references to the semantic conventions. + log.loading("Solving semantic convention references"); + if let Some(schema) = schema.schema.as_mut() { + resolve_resource(schema, &sem_conv_catalog, &version_changes)?; + resolve_metrics(schema, &sem_conv_catalog, &version_changes)?; + resolve_events(schema, &sem_conv_catalog, &version_changes)?; + resolve_spans(schema, &sem_conv_catalog, version_changes)?; + } + log.success(&format!( + "Resolved schema '{}' ({:.2}s)", + schema_path, + start.elapsed().as_secs_f32() + )); + + schema.semantic_conventions.clear(); + schema.set_semantic_convention_catalog(sem_conv_catalog); + + Ok(()) + } + + /// Loads and resolves a semantic convention registry from the given Git URL. + pub fn resolve_semconv_registry( + registry_git_url: String, + path: Option, + cache: &Cache, + log: impl Logger + Clone + Sync, + ) -> Result { + Self::semconv_registry_from_imports( + &[SemConvImport::GitUrl { + git_url: registry_git_url, + path, + }], + ResolverConfig::default(), + cache, + log.clone(), + ) + } + + /// Loads a semantic convention registry from the given Git URL. + pub fn load_semconv_registry( + registry_git_url: String, + path: Option, + cache: &Cache, + log: impl Logger + Clone + Sync, + ) -> Result { + Self::load_semconv_registry_from_imports( + &[SemConvImport::GitUrl { + git_url: registry_git_url, + path, + }], + cache, + log.clone(), + ) + } + + /// Loads a telemetry schema from the given URL or path. + pub fn load_schema( + schema_url_or_path: &str, + log: impl Logger + Clone + Sync, + ) -> Result { + let start = Instant::now(); + log.loading(&format!("Loading schema '{}'", schema_url_or_path)); + + let mut schema = TelemetrySchema::load(schema_url_or_path).map_err(|e| { + log.error(&format!("Failed to load schema '{}'", schema_url_or_path)); + Error::TelemetrySchemaError(e) + })?; + log.success(&format!( + "Loaded schema '{}' ({:.2}s)", + schema_url_or_path, + start.elapsed().as_secs_f32() + )); + + let parent_schema = Self::load_parent_schema(&schema, log.clone())?; + schema.set_parent_schema(parent_schema); + Ok(schema) + } + + /// Loads a telemetry schema from the given path. + pub fn load_schema_from_path + Clone>( + schema_path: P, + log: impl Logger + Clone + Sync, + ) -> Result { + let start = Instant::now(); + log.loading(&format!( + "Loading schema '{}'", + schema_path.as_ref().display() + )); + + let mut schema = TelemetrySchema::load_from_file(schema_path.clone()).map_err(|e| { + log.error(&format!( + "Failed to load schema '{}'", + schema_path.as_ref().display() + )); + Error::TelemetrySchemaError(e) + })?; + log.success(&format!( + "Loaded schema '{}' ({:.2}s)", + schema_path.as_ref().display(), + start.elapsed().as_secs_f32() + )); + + let parent_schema = Self::load_parent_schema(&schema, log.clone())?; + schema.set_parent_schema(parent_schema); + Ok(schema) + } + + /// Loads a semantic convention registry from the given schema. + pub fn semconv_registry_from_schema( + schema: &TelemetrySchema, + cache: &Cache, + log: impl Logger + Clone + Sync, + ) -> Result { + Self::semconv_registry_from_imports( + &schema.merged_semantic_conventions(), + ResolverConfig::default(), + cache, + log.clone(), + ) + } + + /// Loads a semantic convention registry from the given semantic convention imports. + pub fn load_semconv_registry_from_imports( + imports: &[SemConvImport], + cache: &Cache, + log: impl Logger + Clone + Sync, + ) -> Result { + let start = Instant::now(); + let registry = Self::create_semantic_convention_registry(imports, cache, log.clone())?; + log.success(&format!( + "Loaded {} semantic convention files containing the definition of {} attributes and {} metrics ({:.2}s)", + registry.asset_count(), + registry.attribute_count(), + registry.metric_count(), + start.elapsed().as_secs_f32() + )); + + Ok(registry) + } + + /// Loads a semantic convention registry from the given semantic convention imports. + pub fn semconv_registry_from_imports( + imports: &[SemConvImport], + resolver_config: ResolverConfig, + cache: &Cache, + log: impl Logger + Clone + Sync, + ) -> Result { + let start = Instant::now(); + let mut registry = Self::create_semantic_convention_registry(imports, cache, log.clone())?; + let warnings = registry + .resolve(resolver_config) + .map_err(|e| Error::SemConvError { + message: e.to_string(), + })?; + for warning in warnings { + log.warn("Semantic convention warning") + .log(&warning.error.to_string()); + } + log.success(&format!( + "Loaded {} semantic convention files containing the definition of {} attributes and {} metrics ({:.2}s)", + registry.asset_count(), + registry.attribute_count(), + registry.metric_count(), + start.elapsed().as_secs_f32() + )); + + Ok(registry) + } + + /// Resolves the given semantic convention registry and returns the + /// corresponding resolved telemetry schema. + pub fn resolve_semantic_convention_registry( + registry: &mut SemConvSpecs, + log: impl Logger + Clone + Sync, + ) -> Result { + let start = Instant::now(); + + let mut attr_catalog = AttributeCatalog::default(); + let resolved_registry = + resolve_semconv_registry(&mut attr_catalog, "", registry, log.clone())?; + + let metrics = registry + .metrics_iter() + .map(semconv_to_resolved_metric) + .collect(); + + let resolved_schema = ResolvedTelemetrySchema { + file_format: "1.0.0".to_string(), + schema_url: "".to_string(), + registries: vec![resolved_registry], + catalog: Catalog { + attributes: attr_catalog.drain_attributes(), + metrics, + }, + resource: None, + instrumentation_library: None, + dependencies: vec![], + versions: None, // ToDo LQ: Implement this! + }; + + log.success(&format!( + "Resolved {} semantic convention files containing the definition of {} attributes and {} metrics ({:.2}s)", + registry.asset_count(), + registry.attribute_count(), + registry.metric_count(), + start.elapsed().as_secs_f32() + )); + + Ok(resolved_schema) + } + + /// Loads the parent telemetry schema if it exists. + fn load_parent_schema( + schema: &TelemetrySchema, + log: impl Logger, + ) -> Result, Error> { + let start = Instant::now(); + // Load the parent schema and merge it into the current schema. + let parent_schema = if let Some(parent_schema_url) = schema.parent_schema_url.as_ref() { + log.loading(&format!("Loading parent schema '{}'", parent_schema_url)); + let url_pattern = Regex::new(r"^(https|http|file):.*") + .expect("invalid regex, please report this bug"); + let parent_schema = if url_pattern.is_match(parent_schema_url) { + let url = Url::parse(parent_schema_url).map_err(|e| { + log.error(&format!( + "Failed to parset parent schema url '{}'", + parent_schema_url + )); + Error::InvalidUrl { + url: parent_schema_url.clone(), + error: e.to_string(), + } + })?; + TelemetrySchema::load_from_url(&url).map_err(|e| { + log.error(&format!( + "Failed to load parent schema '{}'", + parent_schema_url + )); + Error::ParentSchemaError(e) + })? + } else { + TelemetrySchema::load_from_file(parent_schema_url).map_err(|e| { + log.error(&format!( + "Failed to load parent schema '{}'", + parent_schema_url + )); + Error::ParentSchemaError(e) + })? + }; + + log.success(&format!( + "Loaded parent schema '{}' ({:.2}s)", + parent_schema_url, + start.elapsed().as_secs_f32() + )); + Some(parent_schema) + } else { + None + }; + + Ok(parent_schema) + } + + /// Creates a semantic convention registry from the given telemetry schema. + fn create_semantic_convention_registry( + sem_convs: &[SemConvImport], + cache: &Cache, + log: impl Logger + Sync, + ) -> Result { + // Load all the semantic convention catalogs. + let mut sem_conv_catalog = SemConvSpecs::default(); + let total_file_count = sem_convs.len(); + let loaded_files_count = AtomicUsize::new(0); + let error_count = AtomicUsize::new(0); + + let result: Vec> = sem_convs + .par_iter() + .flat_map(|sem_conv_import| { + let results = Self::import_sem_conv_specs(sem_conv_import, cache); + for result in results.iter() { + if result.is_err() { + error_count.fetch_add(1, Relaxed); + } + loaded_files_count.fetch_add(1, Relaxed); + if error_count.load(Relaxed) == 0 { + log.loading(&format!( + "Loaded {}/{} semantic convention files (no error detected)", + loaded_files_count.load(Relaxed), + total_file_count + )); + } else { + log.loading(&format!( + "Loaded {}/{} semantic convention files ({} error(s) detected)", + loaded_files_count.load(Relaxed), + total_file_count, + error_count.load(Relaxed) + )); + } + } + results + }) + .collect(); + + let mut errors = vec![]; + result.into_iter().for_each(|result| match result { + Ok((provenance, spec)) => { + sem_conv_catalog + .append_sem_conv_spec(SemConvSpecWithProvenance { provenance, spec }); + } + Err(e) => { + log.error(&e.to_string()); + errors.push(e); + } + }); + + // ToDo LQ: Propagate the errors! + + Ok(sem_conv_catalog) + } + + /// Imports the semantic convention specifications from the given import declaration. + /// This function returns a vector of results because the import declaration can be a + /// URL or a git URL (containing potentially multiple semantic convention specifications). + fn import_sem_conv_specs( + import_decl: &SemConvImport, + cache: &Cache, + ) -> Vec> { + match import_decl { + SemConvImport::Url { url } => { + let spec = SemConvSpecs::load_sem_conv_spec_from_url(url).map_err(|e| { + Error::SemConvError { + message: e.to_string(), + } + }); + vec![spec] + } + SemConvImport::GitUrl { git_url, path } => { + fn is_hidden(entry: &DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| s.starts_with('.')) + .unwrap_or(false) + } + fn is_semantic_convention_file(entry: &DirEntry) -> bool { + let path = entry.path(); + let extension = path.extension().unwrap_or_else(|| std::ffi::OsStr::new("")); + let file_name = path.file_name().unwrap_or_else(|| std::ffi::OsStr::new("")); + path.is_file() + && (extension == "yaml" || extension == "yml") + && file_name != "schema-next.yaml" + } + + let mut result = vec![]; + let git_repo = cache.git_repo(git_url.clone(), path.clone()).map_err(|e| { + Error::SemConvError { + message: e.to_string(), + } + }); + + if let Ok(git_repo) = git_repo { + // Loads the semantic convention specifications from the git repo. + // All yaml files are recursively loaded from the given path. + for entry in walkdir::WalkDir::new(git_repo.clone()) + .into_iter() + .filter_entry(|e| !is_hidden(e)) + { + match entry { + Ok(entry) => { + if is_semantic_convention_file(&entry) { + let spec = + SemConvSpecs::load_sem_conv_spec_from_file(entry.path()) + .map_err(|e| Error::SemConvError { + message: e.to_string(), + }); + result.push(match spec { + Ok((path, spec)) => { + // Replace the local path with the git URL combined with the relative path + // of the semantic convention file. + let prefix = git_repo + .to_str() + .map(|s| s.to_string()) + .unwrap_or_default(); + let path = format!( + "{}/{}", + git_url, + &path[prefix.len() + 1..] + ); + Ok((path, spec)) + } + Err(e) => Err(e), + }); + } + } + Err(e) => result.push(Err(Error::SemConvError { + message: e.to_string(), + })), + } + } + } + + result + } + } + } +} + +#[cfg(test)] +mod test { + use weaver_cache::Cache; + use weaver_logger::{ConsoleLogger, Logger}; + + use crate::SchemaResolver; + + #[test] + fn resolve_schema() { + let log = ConsoleLogger::new(0); + let cache = Cache::try_new().unwrap_or_else(|e| { + log.error(&e.to_string()); + std::process::exit(1); + }); + let schema = SchemaResolver::resolve_schema_file( + "../../data/app-telemetry-schema.yaml", + &cache, + log, + ); + assert!(schema.is_ok(), "{:#?}", schema.err().unwrap()); + } +} diff --git a/crates/weaver_resolver/src/metrics.rs b/crates/weaver_resolver/src/metrics.rs new file mode 100644 index 00000000..9ff86cbf --- /dev/null +++ b/crates/weaver_resolver/src/metrics.rs @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Resolve metric and metric_group + +use crate::attribute::{merge_attributes, resolve_attributes}; +use crate::Error; +use std::collections::{HashMap, HashSet}; +use weaver_schema::attribute::to_schema_attributes; +use weaver_schema::metric_group::Metric; +use weaver_schema::schema_spec::SchemaSpec; +use weaver_schema::univariate_metric::UnivariateMetric; +use weaver_semconv::group::InstrumentSpec; +use weaver_semconv::SemConvSpecs; +use weaver_version::VersionChanges; + +/// Resolves metrics and their attributes. +pub fn resolve_metrics( + schema: &mut SchemaSpec, + sem_conv_catalog: &SemConvSpecs, + version_changes: &VersionChanges, +) -> Result<(), Error> { + if let Some(metrics) = schema.resource_metrics.as_mut() { + metrics.attributes = resolve_attributes( + metrics.attributes.as_ref(), + sem_conv_catalog, + version_changes.metric_attribute_changes(), + )?; + + // Resolve metrics (univariate) + for metric in metrics.metrics.iter_mut() { + if let UnivariateMetric::Ref { + r#ref, + attributes, + tags, + } = metric + { + *attributes = resolve_attributes( + attributes, + sem_conv_catalog, + version_changes.metric_attribute_changes(), + )?; + if let Some(referenced_metric) = sem_conv_catalog.metric(r#ref) { + let mut inherited_attrs = to_schema_attributes(&referenced_metric.attributes); + inherited_attrs = resolve_attributes( + &inherited_attrs, + sem_conv_catalog, + version_changes.metric_attribute_changes(), + )?; + let merged_attrs = merge_attributes(attributes, &inherited_attrs); + *metric = UnivariateMetric::Metric { + name: referenced_metric.name.clone(), + brief: referenced_metric.brief.clone(), + note: referenced_metric.note.clone(), + attributes: merged_attrs, + instrument: referenced_metric.instrument.clone(), + unit: referenced_metric.unit.clone(), + tags: tags.clone(), + }; + } else { + return Err(Error::FailToResolveMetric { + r#ref: r#ref.clone(), + }); + } + } + } + + // Resolve metric groups (multivariate metrics). + // Attributes handling for the metrics present in the metric group: + // - If the metrics share the same set of require attributes then all the attributes are + // merged into the metric group attributes. + // - Otherwise, an error is returned. + for metrics in metrics.metric_groups.iter_mut() { + let mut metric_group_attrs = HashMap::new(); + + // Resolve metric group attributes + resolve_attributes( + metrics.attributes.as_ref(), + sem_conv_catalog, + version_changes.metric_attribute_changes(), + )? + .into_iter() + .for_each(|attr| { + metric_group_attrs.insert(attr.id(), attr); + }); + + // Process each metric defined in the metric group. + let mut all_shared_attributes = vec![]; + let mut required_shared_attributes = HashSet::new(); + for (i, metric) in metrics.metrics.iter_mut().enumerate() { + if let Metric::Ref { r#ref, tags } = metric { + if let Some(referenced_metric) = sem_conv_catalog.metric(r#ref) { + let inherited_attrs = referenced_metric.attributes.clone(); + + // Initialize all/required_shared_attributes only if first metric. + if i == 0 { + all_shared_attributes = inherited_attrs.clone(); + all_shared_attributes + .iter() + .filter(|attr| attr.is_required()) + .for_each(|attr| { + required_shared_attributes.insert(attr.id()); + }); + } + + let mut required_count = 0; + for attr in inherited_attrs.iter() { + if attr.is_required() { + required_count += 1; + if !required_shared_attributes.contains(&attr.id()) { + return Err(Error::IncompatibleMetricAttributes { + metric_group_ref: metrics.name.clone(), + metric_ref: referenced_metric.name.clone(), + error: format!("The attribute '{}' is required but not required in other metrics", attr.id()), + }); + } + } + } + if required_count != required_shared_attributes.len() { + return Err(Error::IncompatibleMetricAttributes { + metric_group_ref: metrics.name.clone(), + metric_ref: referenced_metric.name.clone(), + error: "Some required attributes are missing in this metric" + .to_string(), + }); + } + + *metric = Metric::Metric { + name: referenced_metric.name.clone(), + brief: referenced_metric.brief.clone(), + note: referenced_metric.note.clone(), + attributes: vec![], + instrument: referenced_metric.instrument.clone(), + unit: referenced_metric.unit.clone(), + tags: tags.clone(), + }; + } else { + return Err(Error::FailToResolveMetric { + r#ref: r#ref.clone(), + }); + } + } + } + + let all_shared_attributes = resolve_attributes( + &to_schema_attributes(&all_shared_attributes), + sem_conv_catalog, + version_changes.metric_attribute_changes(), + )?; + all_shared_attributes + .into_iter() + .for_each(|attr| _ = metric_group_attrs.insert(attr.id(), attr)); + + metrics.attributes = metric_group_attrs.into_values().collect(); + } + } + Ok(()) +} + +/// Converts a semantic convention metric to a resolved metric that will be +/// part of the catalog of metrics of a resolved telemetry schema. +/// +/// Note: References to attribute of the metric are not part of the catalog of +/// metrics but are part of the schema specification in the instrumentation +/// library section. +pub fn semconv_to_resolved_metric( + metric: &weaver_semconv::metric::MetricSpec, +) -> weaver_resolved_schema::metric::Metric { + weaver_resolved_schema::metric::Metric { + name: metric.name.clone(), + brief: metric.brief.clone(), + note: metric.note.clone(), + instrument: resolve_instrument(&metric.instrument), + unit: metric.unit.clone(), + tags: None, // ToDo we need a mechanism to transmit tags here from the input schema. + } +} + +/// Resolve a metric instrument. +pub fn resolve_instrument( + instrument: &InstrumentSpec, +) -> weaver_resolved_schema::metric::Instrument { + match instrument { + InstrumentSpec::Counter => weaver_resolved_schema::metric::Instrument::Counter, + InstrumentSpec::UpDownCounter => weaver_resolved_schema::metric::Instrument::UpDownCounter, + InstrumentSpec::Gauge => weaver_resolved_schema::metric::Instrument::Gauge, + InstrumentSpec::Histogram => weaver_resolved_schema::metric::Instrument::Histogram, + } +} diff --git a/crates/weaver_resolver/src/registry.rs b/crates/weaver_resolver/src/registry.rs new file mode 100644 index 00000000..5875feca --- /dev/null +++ b/crates/weaver_resolver/src/registry.rs @@ -0,0 +1,431 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Functions to resolve a semantic convention registry. + +use std::collections::HashMap; + +use weaver_logger::Logger; +use weaver_resolved_schema::attribute::{AttributeRef, UnresolvedAttribute}; +use weaver_resolved_schema::lineage::{FieldId, FieldLineage, GroupLineage, ResolutionMode}; +use weaver_resolved_schema::registry::{ + Group, Registry, TypedGroup, UnresolvedGroup, UnresolvedRegistry, +}; +use weaver_semconv::attribute::AttributeSpec; +use weaver_semconv::group::{ConvTypeSpec, GroupSpec}; +use weaver_semconv::{GroupSpecWithProvenance, SemConvSpecs}; + +use crate::attribute::{resolve_attribute, AttributeCatalog}; +use crate::constraint::resolve_constraints; +use crate::metrics::resolve_instrument; +use crate::spans::resolve_span_kind; +use crate::stability::resolve_stability; +use crate::{Error, UnresolvedReference}; + +/// Creates a registry from a set of semantic convention specifications. +/// Note: this function does not resolve references. +#[allow(dead_code)] // ToDo remove this once this function is called from the CLI. +pub fn unresolved_registry_from_specs(url: &str, specs: &SemConvSpecs) -> UnresolvedRegistry { + let groups = specs + .groups_with_provenance() + .map(group_from_spec) + .collect(); + + UnresolvedRegistry { + registry: Registry { + registry_url: url.to_string(), + groups: vec![], + }, + groups, + } +} + +/// Creates a group from a semantic convention group specification. +/// Note: this function does not resolve references. +fn group_from_spec(group: GroupSpecWithProvenance) -> UnresolvedGroup { + let attrs = group + .spec + .attributes + .into_iter() + .map(|attr| UnresolvedAttribute { spec: attr }) + .collect(); + + UnresolvedGroup { + group: Group { + id: group.spec.id, + typed_group: match group.spec.r#type { + ConvTypeSpec::AttributeGroup => TypedGroup::AttributeGroup {}, + ConvTypeSpec::Span => TypedGroup::Span { + span_kind: group.spec.span_kind.as_ref().map(resolve_span_kind), + events: group.spec.events, + }, + ConvTypeSpec::Event => TypedGroup::Event { + name: group.spec.name, + }, + ConvTypeSpec::Metric => TypedGroup::Metric { + metric_name: group.spec.metric_name, + instrument: group.spec.instrument.as_ref().map(resolve_instrument), + unit: group.spec.unit, + }, + ConvTypeSpec::MetricGroup => TypedGroup::MetricGroup {}, + ConvTypeSpec::Resource => TypedGroup::Resource {}, + ConvTypeSpec::Scope => TypedGroup::Scope {}, + }, + brief: group.spec.brief, + note: group.spec.note, + prefix: group.spec.prefix, + extends: group.spec.extends, + stability: resolve_stability(&group.spec.stability), + deprecated: group.spec.deprecated, + constraints: resolve_constraints(&group.spec.constraints), + attributes: vec![], + lineage: Some(GroupLineage::new(group.provenance.clone())), + }, + attributes: attrs, + provenance: group.provenance, + } +} + +/// Resolve a semantic convention registry. +pub fn resolve_semconv_registry( + attr_catalog: &mut AttributeCatalog, + url: &str, + registry: &SemConvSpecs, + _log: impl Logger + Sync + Clone, +) -> Result { + let groups: Result, Error> = registry + .groups() + .map(|group| semconv_to_resolved_group(registry, attr_catalog, group)) + .collect(); + + Ok(Registry { + registry_url: url.to_string(), + groups: groups?, + }) +} + +/// Resolve a semantic convention group. +fn semconv_to_resolved_group( + registry: &SemConvSpecs, + attr_catalog: &mut AttributeCatalog, + group: &GroupSpec, +) -> Result { + let attr_refs: Result, Error> = group + .attributes + .iter() + .map(|attr| Ok(attr_catalog.attribute_ref(resolve_attribute(registry, attr)?))) + .collect(); + + Ok(Group { + id: group.id.clone(), + typed_group: match group.r#type { + ConvTypeSpec::AttributeGroup => TypedGroup::AttributeGroup {}, + ConvTypeSpec::Span => TypedGroup::Span { + span_kind: group.span_kind.as_ref().map(resolve_span_kind), + events: group.events.clone(), + }, + ConvTypeSpec::Event => TypedGroup::Event { + name: group.name.clone(), + }, + ConvTypeSpec::Metric => TypedGroup::Metric { + metric_name: group.metric_name.clone(), + instrument: group.instrument.as_ref().map(resolve_instrument), + unit: group.unit.clone(), + }, + ConvTypeSpec::MetricGroup => TypedGroup::MetricGroup {}, + ConvTypeSpec::Resource => TypedGroup::Resource {}, + ConvTypeSpec::Scope => TypedGroup::Scope {}, + }, + brief: group.brief.to_string(), + note: group.note.to_string(), + prefix: group.prefix.to_string(), + extends: group.extends.clone(), + stability: resolve_stability(&group.stability), + deprecated: group.deprecated.clone(), + constraints: resolve_constraints(&group.constraints), + attributes: attr_refs?, + lineage: None, + }) +} + +/// Resolves attribute references in the given registry. +/// The resolution process is iterative. The process stops when all the +/// attribute references are resolved or when no attribute reference could +/// be resolved in an iteration. +/// +/// The resolve method of the attribute catalog is used to resolve the +/// attribute references. +/// +/// Returns true if all the attribute references could be resolved. +pub fn resolve_attribute_references( + ureg: &mut UnresolvedRegistry, + attr_catalog: &mut AttributeCatalog, +) -> bool { + loop { + let mut unresolved_attr_count = 0; + let mut resolved_attr_count = 0; + + // Iterate over all groups and resolve the attributes. + for unresolved_group in ureg.groups.iter_mut() { + let mut resolved_attr = vec![]; + + unresolved_group.attributes = unresolved_group + .attributes + .clone() + .into_iter() + .filter_map(|attr| { + let attr_ref = attr_catalog.resolve( + &unresolved_group.group.id, + &unresolved_group.group.prefix, + &attr.spec, + unresolved_group.group.lineage.as_mut(), + ); + if let Some(attr_ref) = attr_ref { + resolved_attr.push(attr_ref); + resolved_attr_count += 1; + None + } else { + unresolved_attr_count += 1; + Some(attr) + } + }) + .collect(); + + unresolved_group.group.attributes.extend(resolved_attr); + } + + if unresolved_attr_count == 0 { + break; + } + // If we still have unresolved attributes but we did not resolve any + // attributes in the last iteration, we are stuck in an infinite loop. + // It means that we have an issue with the semantic convention + // specifications. + if resolved_attr_count == 0 { + return false; + } + } + true +} + +/// Resolves the `extends` references in the given registry. +/// The resolution process is iterative. The process stops when all the +/// `extends` references are resolved or when no `extends` reference could +/// be resolved in an iteration. +/// +/// Returns true if all the `extends` references could be resolved. +pub fn resolve_extends_references(ureg: &mut UnresolvedRegistry) -> bool { + loop { + let mut unresolved_extends_count = 0; + let mut resolved_extends_count = 0; + + // Create a map group_id -> vector of attribute ref for groups + // that don't have an `extends` clause. + let mut group_index = HashMap::new(); + for group in ureg.groups.iter() { + if group.group.extends.is_none() { + group_index.insert(group.group.id.clone(), group.group.attributes.clone()); + } + } + + // Iterate over all groups and resolve the `extends` clauses. + for unresolved_group in ureg.groups.iter_mut() { + if let Some(extends) = unresolved_group.group.extends.as_ref() { + if let Some(attr_refs) = group_index.get(extends) { + for attr_ref in attr_refs.iter() { + unresolved_group.group.attributes.push(*attr_ref); + + // Update the lineage based on the inherited fields. + // Note: the lineage is only updated if a group lineage is provided. + if let Some(lineage) = unresolved_group.group.lineage.as_mut() { + lineage.add_attribute_field_lineage( + *attr_ref, + FieldId::GroupAttributes, + FieldLineage { + resolution_mode: ResolutionMode::Extends, + group_id: extends.clone(), + }, + ); + } + } + unresolved_group.group.extends.take(); + resolved_extends_count += 1; + } else { + unresolved_extends_count += 1; + } + } + } + + if unresolved_extends_count == 0 { + break; + } + // If we still have unresolved `extends` but we did not resolve any + // `extends` in the last iteration, we are stuck in an infinite loop. + // It means that we have an issue with the semantic convention + // specifications. + if resolved_extends_count == 0 { + return false; + } + } + true +} + +/// Resolves the registry by resolving all groups and attributes. +/// The resolution process consists of the following steps: +/// - Resolve all attribute references and apply the overrides when needed. +/// - Resolve all the `extends` references. +#[allow(dead_code)] // ToDo remove this once this function is called from the CLI. +pub fn resolve_registry( + mut ureg: UnresolvedRegistry, + attr_catalog: &mut AttributeCatalog, +) -> Result { + let mut all_refs_resolved = true; + + all_refs_resolved &= resolve_attribute_references(&mut ureg, attr_catalog); + all_refs_resolved &= resolve_extends_references(&mut ureg); + + if !all_refs_resolved { + // Process all unresolved references. + // An Error::UnresolvedReferences is built and returned. + let mut unresolved_refs = vec![]; + for group in ureg.groups.iter() { + if let Some(extends) = group.group.extends.as_ref() { + unresolved_refs.push(UnresolvedReference::ExtendsRef { + group_id: group.group.id.clone(), + extends_ref: extends.clone(), + provenance: group.provenance.clone(), + }); + } + for attr in group.attributes.iter() { + if let AttributeSpec::Ref { r#ref, .. } = &attr.spec { + unresolved_refs.push(UnresolvedReference::AttributeRef { + group_id: group.group.id.clone(), + attribute_ref: r#ref.clone(), + provenance: group.provenance.clone(), + }); + } + } + } + if !unresolved_refs.is_empty() { + return Err(Error::UnresolvedReferences { + refs: unresolved_refs, + }); + } + } + + // Sort the attribute internal references in each group. + // This is needed to ensure that the resolved registry is easy to compare + // in unit tests. + ureg.registry.groups = ureg + .groups + .into_iter() + .map(|mut g| { + g.group.attributes.sort(); + g.group + }) + .collect(); + + Ok(ureg.registry) +} + +#[cfg(test)] +mod tests { + use glob::glob; + + use weaver_resolved_schema::attribute; + use weaver_resolved_schema::registry::Registry; + use weaver_semconv::SemConvSpecs; + + use crate::attribute::AttributeCatalog; + use crate::registry::{resolve_registry, unresolved_registry_from_specs}; + + /// Test the resolution of semantic convention registries stored in the + /// data directory. + /// + /// Each test is stored in a directory named `registry-test-*` and contains + /// the following directory and files: + /// - directory `registry` containing the semantic convention specifications + /// in YAML format. + /// - file `expected-attribute-catalog.json` containing the expected + /// attribute catalog in JSON format. + /// - file `expected-registry.json` containing the expected registry in + /// JSON format. + #[test] + #[allow(clippy::print_stdout)] + fn test_registry_resolution() { + // Iterate over all directories in the data directory and + // starting with registry-test-* + for test_entry in glob("data/registry-test-*").expect("Failed to read glob pattern") { + let path_buf = test_entry.expect("Failed to read test directory"); + let test_dir = path_buf + .to_str() + .expect("Failed to convert test directory to string"); + + println!("Testing `{}`", test_dir); + + let mut sc_specs = SemConvSpecs::default(); + for sc_entry in + glob(&format!("{}/registry/*.yaml", test_dir)).expect("Failed to read glob pattern") + { + let path_buf = sc_entry.expect("Failed to read semconv file"); + let semconv_file = path_buf + .to_str() + .expect("Failed to convert semconv file to string"); + let result = sc_specs.load_from_file(semconv_file); + assert!( + result.is_ok(), + "Failed to load semconv file `{}, error: {:#?}", + semconv_file, + result.err().unwrap() + ); + } + + let mut attr_catalog = AttributeCatalog::default(); + let observed_registry = resolve_registry( + unresolved_registry_from_specs("https://semconv-registry.com", &sc_specs), + &mut attr_catalog, + ) + .expect("Failed to resolve registry"); + + // Load the expected registry and attribute catalog. + let expected_attr_catalog: Vec = serde_json::from_reader( + std::fs::File::open(format!("{}/expected-attribute-catalog.json", test_dir)) + .expect("Failed to open expected attribute catalog"), + ) + .expect("Failed to deserialize expected attribute catalog"); + let expected_registry: Registry = serde_json::from_reader( + std::fs::File::open(format!("{}/expected-registry.json", test_dir)) + .expect("Failed to open expected registry"), + ) + .expect("Failed to deserialize expected registry"); + + // Check that the resolved attribute catalog matches the expected attribute catalog. + let observed_attr_catalog = attr_catalog.drain_attributes(); + let observed_attr_catalog_json = serde_json::to_string_pretty(&observed_attr_catalog) + .expect("Failed to serialize observed attribute catalog"); + + assert_eq!( + observed_attr_catalog, expected_attr_catalog, + "Attribute catalog does not match for `{}`.\nObserved catalog:\n{}", + test_dir, observed_attr_catalog_json + ); + + let yaml = serde_yaml::to_string(&observed_attr_catalog).unwrap(); + println!("{}", yaml); + + // Check that the resolved registry matches the expected registry. + let observed_registry_json = serde_json::to_string_pretty(&observed_registry) + .expect("Failed to serialize observed registry"); + + assert_eq!( + observed_registry, expected_registry, + "Registry does not match for `{}`.\nObserved registry:\n{}", + test_dir, observed_registry_json + ); + + let yaml = serde_yaml::to_string(&observed_registry).unwrap(); + println!("{}", yaml); + } + } +} + +// ToDo Remove #[allow(dead_code)] once the corresponding functions are called from the CLI. diff --git a/crates/weaver_resolver/src/resource.rs b/crates/weaver_resolver/src/resource.rs new file mode 100644 index 00000000..a9b3c0ce --- /dev/null +++ b/crates/weaver_resolver/src/resource.rs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Resolve resource + +use crate::attribute::resolve_attributes; +use crate::Error; +use weaver_schema::schema_spec::SchemaSpec; +use weaver_semconv::SemConvSpecs; +use weaver_version::VersionChanges; + +/// Resolves resource attributes. +pub fn resolve_resource( + schema: &mut SchemaSpec, + sem_conv_catalog: &SemConvSpecs, + version_changes: &VersionChanges, +) -> Result<(), Error> { + // Resolve resource attributes + if let Some(res) = schema.resource.as_mut() { + res.attributes = resolve_attributes( + res.attributes.as_ref(), + sem_conv_catalog, + version_changes.log_attribute_changes(), + )?; + } + Ok(()) +} diff --git a/crates/weaver_resolver/src/spans.rs b/crates/weaver_resolver/src/spans.rs new file mode 100644 index 00000000..1f07a7d1 --- /dev/null +++ b/crates/weaver_resolver/src/spans.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Resolve resource spans + +use crate::attribute::resolve_attributes; +use crate::Error; +use weaver_schema::schema_spec::SchemaSpec; +use weaver_semconv::group::SpanKindSpec; +use weaver_semconv::SemConvSpecs; +use weaver_version::VersionChanges; + +/// Resolves resource spans in the given schema. +pub fn resolve_spans( + schema: &mut SchemaSpec, + sem_conv_catalog: &SemConvSpecs, + version_changes: VersionChanges, +) -> Result<(), Error> { + if let Some(spans) = schema.resource_spans.as_mut() { + spans.attributes = resolve_attributes( + spans.attributes.as_ref(), + sem_conv_catalog, + version_changes.span_attribute_changes(), + )?; + for span in spans.spans.iter_mut() { + span.attributes = resolve_attributes( + span.attributes.as_ref(), + sem_conv_catalog, + version_changes.span_attribute_changes(), + )?; + for event in span.events.iter_mut() { + event.attributes = resolve_attributes( + event.attributes.as_ref(), + sem_conv_catalog, + version_changes.span_attribute_changes(), + )?; + } + for link in span.links.iter_mut() { + link.attributes = resolve_attributes( + link.attributes.as_ref(), + sem_conv_catalog, + version_changes.span_attribute_changes(), + )?; + } + } + } + Ok(()) +} + +/// Resolve a span kind. +pub fn resolve_span_kind(span_kind: &SpanKindSpec) -> weaver_resolved_schema::signal::SpanKind { + match span_kind { + SpanKindSpec::Client => weaver_resolved_schema::signal::SpanKind::Client, + SpanKindSpec::Consumer => weaver_resolved_schema::signal::SpanKind::Consumer, + SpanKindSpec::Internal => weaver_resolved_schema::signal::SpanKind::Internal, + SpanKindSpec::Producer => weaver_resolved_schema::signal::SpanKind::Producer, + SpanKindSpec::Server => weaver_resolved_schema::signal::SpanKind::Server, + } +} diff --git a/crates/weaver_resolver/src/stability.rs b/crates/weaver_resolver/src/stability.rs new file mode 100644 index 00000000..d992698e --- /dev/null +++ b/crates/weaver_resolver/src/stability.rs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Functions to resolve a semantic convention stability field. + +use weaver_semconv::stability::StabilitySpec; + +pub fn resolve_stability( + stability: &Option, +) -> Option { + stability.as_ref().map(|stability| match stability { + StabilitySpec::Deprecated => weaver_resolved_schema::catalog::Stability::Deprecated, + StabilitySpec::Experimental => weaver_resolved_schema::catalog::Stability::Experimental, + StabilitySpec::Stable => weaver_resolved_schema::catalog::Stability::Stable, + }) +} diff --git a/crates/weaver_resolver/src/tags.rs b/crates/weaver_resolver/src/tags.rs new file mode 100644 index 00000000..b7bfad0f --- /dev/null +++ b/crates/weaver_resolver/src/tags.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Resolves or converts tags into their resolved form. + +use weaver_schema::tags::Tags; + +/// Converts tags into their resolved form. +#[allow(dead_code)] // ToDo Remove this once we have tags in the resolved schema +pub fn semconv_to_resolved_tags(tags: &Option) -> Option { + tags.as_ref() + .map(|tags| weaver_resolved_schema::tags::Tags { + tags: tags.tags.clone(), + }) +} diff --git a/crates/weaver_schema/Cargo.toml b/crates/weaver_schema/Cargo.toml new file mode 100644 index 00000000..728fec45 --- /dev/null +++ b/crates/weaver_schema/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "weaver_schema" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true +edition.workspace = true + +[dependencies] +weaver_semconv = { path = "../weaver_semconv" } +weaver_version = { path = "../weaver_version" } + +serde.workspace = true +serde_yaml.workspace = true +thiserror.workspace = true +ureq.workspace = true + +url = {version="2.5.0", features = ["serde"]} \ No newline at end of file diff --git a/crates/weaver_schema/data/root-schema-1.21.0.yaml b/crates/weaver_schema/data/root-schema-1.21.0.yaml new file mode 100644 index 00000000..679000c5 --- /dev/null +++ b/crates/weaver_schema/data/root-schema-1.21.0.yaml @@ -0,0 +1,138 @@ +file_format: 1.1.0 +schema_url: https://opentelemetry.io/schemas/1.21.0 +versions: + 1.21.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3336 + - rename_attributes: + attribute_map: + messaging.kafka.client_id: messaging.client_id + messaging.rocketmq.client_id: messaging.client_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3402 + - rename_attributes: + attribute_map: + # net.peer.(name|port) attributes were usually populated on client side + # so they should be usually translated to server.(address|port) + # net.host.* attributes were only populated on server side + net.host.name: server.address + net.host.port: server.port + # was only populated on client side + net.sock.peer.name: server.socket.domain + # net.sock.peer.(addr|port) mapping is not possible + # since they applied to both client and server side + # were only populated on server side + net.sock.host.addr: server.socket.address + net.sock.host.port: server.socket.port + http.client_ip: client.address + # https://github.com/open-telemetry/opentelemetry-specification/pull/3426 + - rename_attributes: + attribute_map: + net.protocol.name: network.protocol.name + net.protocol.version: network.protocol.version + net.host.connection.type: network.connection.type + net.host.connection.subtype: network.connection.subtype + net.host.carrier.name: network.carrier.name + net.host.carrier.mcc: network.carrier.mcc + net.host.carrier.mnc: network.carrier.mnc + net.host.carrier.icc: network.carrier.icc + # https://github.com/open-telemetry/opentelemetry-specification/pull/3355 + - rename_attributes: + attribute_map: + http.method: http.request.method + http.status_code: http.response.status_code + http.scheme: url.scheme + http.url: url.full + http.request_content_length: http.request.body.size + http.response_content_length: http.response.body.size + metrics: + changes: + # https://github.com/open-telemetry/semantic-conventions/pull/53 + - rename_metrics: + process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization + 1.20.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3272 + - rename_attributes: + attribute_map: + net.app.protocol.name: net.protocol.name + net.app.protocol.version: net.protocol.version + 1.19.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3209 + - rename_attributes: + attribute_map: + faas.execution: faas.invocation_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3188 + - rename_attributes: + attribute_map: + faas.id: cloud.resource_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3190 + - rename_attributes: + attribute_map: + http.user_agent: user_agent.original + resources: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3190 + - rename_attributes: + attribute_map: + browser.user_agent: user_agent.original + 1.18.0: + 1.17.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2957 + - rename_attributes: + attribute_map: + messaging.consumer_id: messaging.consumer.id + messaging.protocol: net.app.protocol.name + messaging.protocol_version: net.app.protocol.version + messaging.destination: messaging.destination.name + messaging.temp_destination: messaging.destination.temporary + messaging.destination_kind: messaging.destination.kind + messaging.message_id: messaging.message.id + messaging.conversation_id: messaging.message.conversation_id + messaging.message_payload_size_bytes: messaging.message.payload_size_bytes + messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes + messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key + messaging.kafka.message_key: messaging.kafka.message.key + messaging.kafka.partition: messaging.kafka.destination.partition + messaging.kafka.tombstone: messaging.kafka.message.tombstone + messaging.rocketmq.message_type: messaging.rocketmq.message.type + messaging.rocketmq.message_tag: messaging.rocketmq.message.tag + messaging.rocketmq.message_keys: messaging.rocketmq.message.keys + messaging.kafka.consumer_group: messaging.kafka.consumer.group + 1.16.0: + 1.15.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2743 + - rename_attributes: + attribute_map: + http.retry_count: http.resend_count + 1.14.0: + 1.13.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2614 + - rename_attributes: + attribute_map: + net.peer.ip: net.sock.peer.addr + net.host.ip: net.sock.host.addr + 1.12.0: + 1.11.0: + 1.10.0: + 1.9.0: + 1.8.0: + spans: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: db.name + db.hbase.namespace: db.name + 1.7.0: + 1.6.1: + 1.5.0: + 1.4.0: \ No newline at end of file diff --git a/crates/weaver_schema/src/attribute.rs b/crates/weaver_schema/src/attribute.rs new file mode 100644 index 00000000..65f58e14 --- /dev/null +++ b/crates/weaver_schema/src/attribute.rs @@ -0,0 +1,412 @@ +// SPDX-License-Identifier: Apache-2.0 + +#![allow(rustdoc::invalid_html_tags)] + +//! Definition of an attribute in the context of a telemetry schema. + +use serde::{Deserialize, Serialize}; + +use weaver_semconv::attribute::{AttributeTypeSpec, ExamplesSpec, RequirementLevelSpec, ValueSpec}; +use weaver_semconv::stability::StabilitySpec; + +use crate::tags::Tags; +use crate::Error; + +/// An attribute specification. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +#[serde(untagged)] +#[serde(rename_all = "snake_case")] +pub enum Attribute { + /// Reference to another attribute. + /// + /// ref MUST have an id of an existing attribute. + /// ref is useful for specifying that an existing attribute of another + /// semantic convention is part of the current semantic convention and + /// inherit its brief, note, and example values. However, if these fields + /// are present in the current attribute definition, they override the + /// inherited values. + Ref { + /// Reference an existing attribute. + r#ref: String, + /// A brief description of the attribute. + #[serde(skip_serializing_if = "Option::is_none")] + brief: Option, + /// Sequence of example values for the attribute or single example + /// value. They are required only for string and string array + /// attributes. Example values must be of the same type of the + /// attribute. If only a single example is provided, it can directly + /// be reported without encapsulating it into a sequence/dictionary. + #[serde(skip_serializing_if = "Option::is_none")] + examples: Option, + /// Associates a tag ("sub-group") to the attribute. It carries no + /// particular semantic meaning but can be used e.g. for filtering + /// in the markdown generator. + #[serde(skip_serializing_if = "Option::is_none")] + tag: Option, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + #[serde(skip_serializing_if = "Option::is_none")] + requirement_level: Option, + /// Specifies if the attribute is (especially) relevant for sampling + /// and thus should be set at span start. It defaults to false. + /// Note: this field is experimental. + #[serde(skip_serializing_if = "Option::is_none")] + sampling_relevant: Option, + /// A more elaborate description of the attribute. + /// It defaults to an empty string. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + note: Option, + /// Specifies the stability of the attribute. + /// Note that, if stability is missing but deprecated is present, it will + /// automatically set the stability to deprecated. If deprecated is + /// present and stability differs from deprecated, this will result in an + /// error. + #[serde(skip_serializing_if = "Option::is_none")] + stability: Option, + /// Specifies if the attribute is deprecated. The string + /// provided as MUST specify why it's deprecated and/or what + /// to use instead. See also stability. + #[serde(skip_serializing_if = "Option::is_none")] + deprecated: Option, + /// A set of tags for the attribute. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, + + /// The value of the attribute. + /// Note: This is only used in a telemetry schema specification. + #[serde(skip_serializing_if = "Option::is_none")] + value: Option, + }, + /// Reference to an attribute group. + /// + /// `attribute_group_ref` MUST have an id of an existing attribute. + AttributeGroupRef { + /// Reference an existing attribute group. + attribute_group_ref: String, + /// A set of tags for the attribute. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, + }, + /// Reference to a span group, i.e. a group of attributes used in the context of + /// a span. + /// + /// `span_ref` MUST have an id of an existing span. + SpanRef { + /// Reference an existing span. + span_ref: String, + /// A set of tags for the attribute. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, + }, + /// Reference to a resource group, i.e. a group of attributes used in the context of + /// a resource. + /// + /// `resource_ref` MUST have an id of an existing resource. + ResourceRef { + /// Reference an existing resource. + resource_ref: String, + /// A set of tags for the attribute. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, + }, + /// Reference to an event group, i.e. a group of attributes used in the context of + /// an event. + /// + /// `event_ref` MUST have an id of an existing event. + EventRef { + /// Reference an existing event. + event_ref: String, + /// A set of tags for the attribute. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, + }, + /// Attribute definition. + Id { + /// String that uniquely identifies the attribute. + id: String, + /// Either a string literal denoting the type as a primitive or an + /// array type, a template type or an enum definition. + r#type: AttributeTypeSpec, + /// A brief description of the attribute. + brief: String, + /// Sequence of example values for the attribute or single example + /// value. They are required only for string and string array + /// attributes. Example values must be of the same type of the + /// attribute. If only a single example is provided, it can directly + /// be reported without encapsulating it into a sequence/dictionary. + // #[serde(skip_serializing_if = "Option::is_none")] + examples: Option, + /// Associates a tag ("sub-group") to the attribute. It carries no + /// particular semantic meaning but can be used e.g. for filtering + /// in the markdown generator. + #[serde(skip_serializing_if = "Option::is_none")] + tag: Option, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + #[serde(default)] + requirement_level: RequirementLevelSpec, + /// Specifies if the attribute is (especially) relevant for sampling + /// and thus should be set at span start. It defaults to false. + /// Note: this field is experimental. + #[serde(skip_serializing_if = "Option::is_none")] + sampling_relevant: Option, + /// A more elaborate description of the attribute. + /// It defaults to an empty string. + #[serde(default)] + note: String, + /// Specifies the stability of the attribute. + /// Note that, if stability is missing but deprecated is present, it will + /// automatically set the stability to deprecated. If deprecated is + /// present and stability differs from deprecated, this will result in an + /// error. + #[serde(skip_serializing_if = "Option::is_none")] + stability: Option, + /// Specifies if the attribute is deprecated. The string + /// provided as MUST specify why it's deprecated and/or what + /// to use instead. See also stability. + #[serde(skip_serializing_if = "Option::is_none")] + deprecated: Option, + /// A set of tags for the attribute. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, + + /// The value of the attribute. + /// Note: This is only used in a telemetry schema specification. + #[serde(skip_serializing_if = "Option::is_none")] + value: Option, + }, +} + +impl From<&weaver_semconv::attribute::AttributeSpec> for Attribute { + /// Convert a semantic convention attribute to a schema attribute. + fn from(attr: &weaver_semconv::attribute::AttributeSpec) -> Self { + match attr.clone() { + weaver_semconv::attribute::AttributeSpec::Ref { + r#ref, + brief, + examples, + tag, + requirement_level, + sampling_relevant, + note, + stability, + deprecated, + } => Attribute::Ref { + r#ref, + brief, + examples, + tag, + requirement_level, + sampling_relevant, + note, + stability, + deprecated, + tags: None, + value: None, + }, + weaver_semconv::attribute::AttributeSpec::Id { + id, + r#type, + brief, + examples, + tag, + requirement_level, + sampling_relevant, + note, + stability, + deprecated, + } => Attribute::Id { + id, + r#type, + brief, + examples, + tag, + requirement_level, + sampling_relevant, + note, + stability, + deprecated, + tags: None, + value: None, + }, + } + } +} + +/// Convert a slice of semantic convention attributes to a vector of schema attributes. +pub fn to_schema_attributes(attrs: &[weaver_semconv::attribute::AttributeSpec]) -> Vec { + attrs.iter().map(|attr| attr.into()).collect() +} + +impl Attribute { + /// Returns the id or the reference of the attribute. + pub fn id(&self) -> String { + match self { + Attribute::Ref { r#ref, .. } => r#ref.clone(), + Attribute::AttributeGroupRef { + attribute_group_ref, + .. + } => attribute_group_ref.clone(), + Attribute::SpanRef { span_ref, .. } => span_ref.clone(), + Attribute::ResourceRef { resource_ref, .. } => resource_ref.clone(), + Attribute::EventRef { event_ref, .. } => event_ref.clone(), + Attribute::Id { id, .. } => id.clone(), + } + } + + /// Sets the tags of the attribute. + pub fn set_tags(&mut self, tags: &Option) { + match self { + Attribute::Ref { tags: tags_ref, .. } => { + *tags_ref = tags.clone(); + } + Attribute::Id { tags: tags_id, .. } => { + *tags_id = tags.clone(); + } + Attribute::AttributeGroupRef { + tags: tags_group, .. + } => { + *tags_group = tags.clone(); + } + Attribute::ResourceRef { + tags: tags_resource, + .. + } => { + *tags_resource = tags.clone(); + } + Attribute::SpanRef { tags, .. } => { + *tags = tags.clone(); + } + Attribute::EventRef { tags, .. } => { + *tags = tags.clone(); + } + } + } + + /// Returns a resolved attribute. The current attribute is expected to be a reference to another + /// attribute. The semantic convention attribute provided as argument is used to resolve the + /// reference. The semantic attribute must be an `Attribute::Id` otherwise an error is returned. + pub fn resolve_from( + &self, + sem_conv_attr: Option<&weaver_semconv::attribute::AttributeSpec>, + ) -> Result { + match self { + Attribute::Ref { + r#ref, + brief: brief_from_ref, + examples: examples_from_ref, + tag: tag_from_ref, + requirement_level: requirement_level_from_ref, + sampling_relevant: sampling_from_ref, + note: note_from_ref, + stability: stability_from_ref, + deprecated: deprecated_from_ref, + tags: tags_from_ref, + value: value_from_ref, + } => { + if let Some(weaver_semconv::attribute::AttributeSpec::Id { + id, + r#type, + brief, + examples, + tag, + requirement_level, + sampling_relevant, + note, + stability, + deprecated, + }) = sem_conv_attr + { + let id = id.clone(); + let r#type = r#type.clone(); + let mut brief = brief.clone(); + let mut examples = examples.clone(); + let mut requirement_level = requirement_level.clone(); + let mut tag = tag.clone(); + let mut sampling_relevant = *sampling_relevant; + let mut note = note.clone(); + let mut stability = stability.clone(); + let mut deprecated = deprecated.clone(); + + // Override process. + // Use the field values from the reference when defined in the reference. + if let Some(brief_from_ref) = brief_from_ref { + brief = brief_from_ref.clone(); + } + if let Some(requirement_level_from_ref) = requirement_level_from_ref { + requirement_level = requirement_level_from_ref.clone(); + } + if let Some(examples_from_ref) = examples_from_ref { + examples = Some(examples_from_ref.clone()); + } + if let Some(tag_from_ref) = tag_from_ref { + tag = Some(tag_from_ref.clone()); + } + if let Some(sampling_from_ref) = sampling_from_ref { + sampling_relevant = Some(*sampling_from_ref); + } + if let Some(note_from_ref) = note_from_ref { + note = note_from_ref.clone(); + } + if let Some(stability_from_ref) = stability_from_ref { + stability = Some(stability_from_ref.clone()); + } + if let Some(deprecated_from_ref) = deprecated_from_ref { + deprecated = Some(deprecated_from_ref.clone()); + } + + Ok(Attribute::Id { + id, + r#type, + brief, + examples, + tag, + requirement_level, + sampling_relevant, + note, + stability, + deprecated, + tags: tags_from_ref.clone(), + value: value_from_ref.clone(), + }) + } else { + Err(Error::InvalidAttribute { + id: r#ref.clone(), + error: "Cannot resolve an attribute from a semantic convention attribute reference.".into(), + }) + } + } + Attribute::Id { id, .. } => Err(Error::InvalidAttribute { + id: id.clone(), + error: "Cannot resolve an attribute from a non-reference attribute.".into(), + }), + Attribute::AttributeGroupRef { + attribute_group_ref, + .. + } => Err(Error::InvalidAttribute { + id: attribute_group_ref.clone(), + error: "Cannot resolve an attribute from an attribute group reference.".into(), + }), + Attribute::SpanRef { span_ref, .. } => Err(Error::InvalidAttribute { + id: span_ref.clone(), + error: "Cannot resolve an attribute from a span reference.".into(), + }), + Attribute::ResourceRef { resource_ref, .. } => Err(Error::InvalidAttribute { + id: resource_ref.clone(), + error: "Cannot resolve an attribute from a resource reference.".into(), + }), + Attribute::EventRef { event_ref, .. } => Err(Error::InvalidAttribute { + id: event_ref.clone(), + error: "Cannot resolve an attribute from an event reference.".into(), + }), + } + } +} diff --git a/crates/weaver_schema/src/event.rs b/crates/weaver_schema/src/event.rs new file mode 100644 index 00000000..62145028 --- /dev/null +++ b/crates/weaver_schema/src/event.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Log record specification. + +use crate::attribute::Attribute; +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; + +/// An event specification. +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(deny_unknown_fields)] +pub struct Event { + /// The name of the event. + pub event_name: String, + /// The domain of the event. + pub domain: String, + /// The attributes of the log record. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// Brief description of the event. + pub brief: Option, + /// Longer description. + /// It defaults to an empty string. + pub note: Option, + /// A set of tags for the event. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} + +impl Event { + /// Returns an attribute by its name. + pub fn attribute(&self, id: &str) -> Option<&Attribute> { + self.attributes.iter().find(|a| a.id() == id) + } +} diff --git a/crates/weaver_schema/src/instrumentation_library.rs b/crates/weaver_schema/src/instrumentation_library.rs new file mode 100644 index 00000000..3596d5c3 --- /dev/null +++ b/crates/weaver_schema/src/instrumentation_library.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Instrumentation library specification. + +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; + +/// An instrumentation library specification. +/// MUST be used both by applications and libraries. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct InstrumentationLibrary { + /// An optional name for the instrumentation library. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + /// An optional version for the instrumentation library. + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + /// A set of tags for the instrumentation library. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, +} diff --git a/crates/weaver_schema/src/lib.rs b/crates/weaver_schema/src/lib.rs new file mode 100644 index 00000000..72eca562 --- /dev/null +++ b/crates/weaver_schema/src/lib.rs @@ -0,0 +1,360 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A Rust library for loading and validating telemetry schemas. + +#![deny(missing_docs)] +#![deny(clippy::print_stdout)] +#![deny(clippy::print_stderr)] + +extern crate core; + +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +use serde::{Deserialize, Serialize}; +use url::Url; + +use weaver_semconv::SemConvSpecs; +use weaver_version::Versions; + +use crate::event::Event; +use crate::metric_group::MetricGroup; +use crate::schema_spec::SchemaSpec; +use crate::span::Span; + +pub mod attribute; +pub mod event; +pub mod instrumentation_library; +pub mod log; +pub mod metric_group; +pub mod resource; +pub mod resource_events; +pub mod resource_metrics; +pub mod resource_spans; +pub mod schema_spec; +pub mod span; +pub mod span_event; +pub mod span_link; +pub mod tags; +pub mod univariate_metric; + +/// An error that can occur while loading a telemetry schema. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// The telemetry schema was not found. + #[error("Schema {path_or_url:?} not found\n{error:?}")] + SchemaNotFound { + /// The path or URL of the telemetry schema. + path_or_url: String, + /// The error that occurred. + error: String, + }, + + /// The telemetry schema is invalid. + #[error("Invalid schema {path_or_url:?}\n{error:?}")] + InvalidSchema { + /// The path or URL of the telemetry schema. + path_or_url: String, + /// The line number where the error occurred. + line: Option, + /// The column number where the error occurred. + column: Option, + /// The error that occurred. + error: String, + }, + + /// The attribute is invalid. + #[error("Invalid attribute `{id:?}`\n{error:?}")] + InvalidAttribute { + /// The attribute id. + id: String, + /// The error that occurred. + error: String, + }, +} + +/// A telemetry schema. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct TelemetrySchema { + /// Defines the file format. MUST be set to 1.2.0. + pub file_format: String, + /// Optional field specifying the schema url of the parent schema. The current + /// schema overrides the parent schema. + /// Usually the parent schema is the official OpenTelemetry Telemetry schema + /// containing the versioning and their corresponding transformations. + /// However, it can also include any of the new fields defined in this OTEP. + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_schema_url: Option, + /// The Schema URL that this file is published at. + pub schema_url: String, + /// The semantic conventions that are imported by the current schema (optional). + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub semantic_conventions: Vec, + /// Definition of the telemetry schema for an application or a library. + #[serde(skip_serializing_if = "Option::is_none")] + pub schema: Option, + /// Definitions for each schema version in this family. + /// Note: the ordering of versions is defined according to semver + /// version number ordering rules. + /// This section is described in more details in the OTEP 0152 and in a dedicated + /// section below. + /// + #[serde(skip_serializing_if = "Option::is_none")] + pub versions: Option, + + /// The parent schema. + #[serde(skip)] + pub parent_schema: Option>, + + /// The semantic convention registry used to resolve the schema + /// (if resolved). + #[serde(skip)] + pub semantic_convention_registry: SemConvSpecs, +} + +/// A semantic convention import. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +#[serde(untagged)] +pub enum SemConvImport { + /// Variant to import a semantic convention from a URL. + Url { + /// The URL of the semantic convention. + url: String, + }, + /// Variant to import semantic conventions from a git repo. + GitUrl { + /// The git URL of the semantic convention git repo. + git_url: String, + /// An optional path to the semantic convention directory containing + /// the semantic convention files. + path: Option, + }, +} + +impl TelemetrySchema { + /// Loads a telemetry schema from an URL or a local path. + pub fn load(schema: &str) -> Result { + if schema.starts_with("http://") || schema.starts_with("https://") { + let schema_url = Url::parse(schema).map_err(|e| Error::SchemaNotFound { + path_or_url: schema.to_string(), + error: e.to_string(), + })?; + Self::load_from_url(&schema_url) + } else { + Self::load_from_file(schema) + } + } + + /// Loads a telemetry schema file and returns the schema. + pub fn load_from_file>(path: P) -> Result { + let path_buf = path.as_ref().to_path_buf(); + + // Load and deserialize the telemetry schema + let schema_file = File::open(path).map_err(|e| Error::SchemaNotFound { + path_or_url: path_buf.as_path().display().to_string(), + error: e.to_string(), + })?; + let schema: TelemetrySchema = serde_yaml::from_reader(BufReader::new(schema_file)) + .map_err(|e| Error::InvalidSchema { + path_or_url: path_buf.as_path().display().to_string(), + line: e.location().map(|loc| loc.line()), + column: e.location().map(|loc| loc.column()), + error: e.to_string(), + })?; + + Ok(schema) + } + + /// Loads a telemetry schema from a URL and returns the schema. + pub fn load_from_url(schema_url: &Url) -> Result { + match schema_url.scheme() { + "http" | "https" => { + // Create a content reader from the schema URL + let reader = ureq::get(schema_url.as_ref()) + .call() + .map_err(|e| Error::SchemaNotFound { + path_or_url: schema_url.to_string(), + error: e.to_string(), + })? + .into_reader(); + + // Deserialize the telemetry schema from the content reader + let schema: TelemetrySchema = + serde_yaml::from_reader(reader).map_err(|e| Error::InvalidSchema { + path_or_url: schema_url.to_string(), + line: e.location().map(|loc| loc.line()), + column: e.location().map(|loc| loc.column()), + error: e.to_string(), + })?; + Ok(schema) + } + "file" => { + let path = schema_url.path(); + Self::load_from_file(path) + } + _ => Err(Error::SchemaNotFound { + path_or_url: schema_url.to_string(), + error: format!("Unsupported URL scheme: {}", schema_url.scheme()), + }), + } + } + + /// Sets the semantic convention catalog used to resolve the schema. + pub fn set_semantic_convention_catalog(&mut self, catalog: SemConvSpecs) { + self.semantic_convention_registry = catalog; + } + + /// Sets the parent schema. + pub fn set_parent_schema(&mut self, parent_schema: Option) { + self.parent_schema = parent_schema.map(Box::new); + } + + /// Returns the semantic conventions for the schema and its parent schemas. + pub fn merged_semantic_conventions(&self) -> Vec { + let mut result = vec![]; + if let Some(parent_schema) = self.parent_schema.as_ref() { + result.extend(parent_schema.merged_semantic_conventions().iter().cloned()); + } + result.extend(self.semantic_conventions.iter().cloned()); + result + } + + /// Merges versions from the parent schema into the current schema. + pub fn merge_versions(&mut self) { + if let Some(parent_schema) = &self.parent_schema { + match self.versions { + Some(ref mut versions) => { + if let Some(parent_versions) = parent_schema.versions.as_ref() { + versions.extend(parent_versions.clone()); + } + } + None => { + self.versions = parent_schema.versions.clone(); + } + } + } + } + + /// Returns the semantic convention catalog used to resolve the schema (if resolved). + pub fn semantic_convention_catalog(&self) -> &SemConvSpecs { + &self.semantic_convention_registry + } + + /// Returns the number of metrics. + pub fn metrics_count(&self) -> usize { + self.schema + .as_ref() + .map_or(0, |schema| schema.metrics_count()) + } + + /// Returns the number of metric groups. + pub fn metric_groups_count(&self) -> usize { + self.schema + .as_ref() + .map_or(0, |schema| schema.metric_groups_count()) + } + + /// Returns the number of events. + pub fn events_count(&self) -> usize { + self.schema + .as_ref() + .map_or(0, |schema| schema.events_count()) + } + + /// Returns the number of spans. + pub fn spans_count(&self) -> usize { + self.schema + .as_ref() + .map_or(0, |schema| schema.spans_count()) + } + + /// Returns the number of versions. + pub fn version_count(&self) -> usize { + self.versions.as_ref().map_or(0, |versions| versions.len()) + } + + /// Returns the metric by name or None if not found. + pub fn metric(&self, metric_name: &str) -> Option<&univariate_metric::UnivariateMetric> { + self.schema + .as_ref() + .and_then(|schema| schema.metric(metric_name)) + } + + /// Returns the metric group by name or None if not found. + pub fn metric_group(&self, name: &str) -> Option<&MetricGroup> { + self.schema + .as_ref() + .and_then(|schema| schema.metric_group(name)) + } + + /// Returns a resource or None if not found. + pub fn resource(&self) -> Option<&resource::Resource> { + self.schema.as_ref().and_then(|schema| schema.resource()) + } + + /// Returns a vector of metrics. + pub fn metrics(&self) -> Vec<&univariate_metric::UnivariateMetric> { + self.schema.as_ref().map_or( + Vec::<&univariate_metric::UnivariateMetric>::new(), + |schema| schema.metrics(), + ) + } + + /// Returns a vector of metric groups. + pub fn metric_groups(&self) -> Vec<&metric_group::MetricGroup> { + self.schema + .as_ref() + .map_or(Vec::<&metric_group::MetricGroup>::new(), |schema| { + schema.metric_groups() + }) + } + + /// Returns an iterator over the events. + pub fn events(&self) -> Vec<&Event> { + self.schema + .as_ref() + .map_or(Vec::<&Event>::new(), |schema| schema.events()) + } + + /// Returns a slice of spans. + pub fn spans(&self) -> Vec<&Span> { + self.schema + .as_ref() + .map_or(Vec::<&Span>::new(), |schema| schema.spans()) + } + + /// Returns an event by name or None if not found. + pub fn event(&self, event_name: &str) -> Option<&Event> { + self.schema + .as_ref() + .and_then(|schema| schema.event(event_name)) + } + + /// Returns a span by name or None if not found. + pub fn span(&self, span_name: &str) -> Option<&Span> { + self.schema + .as_ref() + .and_then(|schema| schema.span(span_name)) + } +} + +#[cfg(test)] +mod test { + use crate::TelemetrySchema; + + #[test] + fn load_root_schema() { + let schema = TelemetrySchema::load_from_file("data/root-schema-1.21.0.yaml"); + assert!(schema.is_ok(), "{:#?}", schema.err().unwrap()); + } + + #[test] + fn load_app_telemetry_schema() { + let schema = TelemetrySchema::load_from_file("../../data/app-telemetry-schema.yaml"); + assert!(schema.is_ok(), "{:#?}", schema.err().unwrap()); + } +} diff --git a/crates/weaver_schema/src/log.rs b/crates/weaver_schema/src/log.rs new file mode 100644 index 00000000..91f60e2a --- /dev/null +++ b/crates/weaver_schema/src/log.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Log record specification. + +use crate::attribute::Attribute; +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; + +/// A log record specification. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct LogRecord { + /// The id of the log record. + pub id: String, + /// The attributes of the log record. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// Brief description of the log record. + pub brief: Option, + /// Longer description. + /// It defaults to an empty string. + pub note: Option, + /// A set of tags for the log record. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} + +/// The type of body of a log record. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +pub enum BodyType { + /// A boolean body. + Boolean(bool), + /// An integer body. + Int(i64), + /// A double body. + Double(f64), + /// A string body. + String(String), + /// A boolean array body. + #[serde(rename = "boolean[]")] + Booleans(Vec), + /// An integer array body. + #[serde(rename = "int[]")] + Ints(Vec), + /// A double array body. + #[serde(rename = "double[]")] + Doubles(Vec), + /// A string array body. + #[serde(rename = "string[]")] + Strings(Vec), +} diff --git a/crates/weaver_schema/src/metric_group.rs b/crates/weaver_schema/src/metric_group.rs new file mode 100644 index 00000000..fdbadfc7 --- /dev/null +++ b/crates/weaver_schema/src/metric_group.rs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Multivariate metrics. + +use serde::{Deserialize, Serialize}; + +use crate::attribute::Attribute; +use crate::tags::Tags; +use weaver_semconv::group::InstrumentSpec; + +/// The specification of a metric group. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct MetricGroup { + /// The name of the metric group. + pub name: String, + /// The attributes of the metric group. + #[serde(default)] + pub attributes: Vec, + /// The metrics of the metric group. + #[serde(default)] + pub metrics: Vec, + /// Brief description of the metric group. + pub brief: Option, + /// Longer description. + /// It defaults to an empty string. + pub note: Option, + /// A set of tags for the metric group. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} + +/// A metric specification. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +#[serde(untagged)] +pub enum Metric { + /// A reference to a metric defined in a semantic convention catalog. + Ref { + /// The reference to the metric. + r#ref: String, + /// A set of tags for the metric group. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, + }, + + /// A fully defined metric. + Metric { + /// Metric name. + name: String, + /// Brief description of the metric. + brief: String, + /// Note on the metric. + note: String, + /// Attributes of the metric. + #[serde(default)] + attributes: Vec, + /// Type of the metric (e.g. gauge, histogram, ...). + instrument: InstrumentSpec, + /// Unit of the metric. + unit: Option, + /// A set of tags for the metric. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, + }, +} + +impl MetricGroup { + /// Returns the name of the metric group + pub fn name(&self) -> &str { + &self.name + } + + /// Returns an attribute by its id. + pub fn attribute(&self, id: &str) -> Option<&Attribute> { + self.attributes.iter().find(|a| a.id() == id) + } + + /// Returns the tags of the metric group. + pub fn tags(&self) -> Option<&Tags> { + self.tags.as_ref() + } +} diff --git a/crates/weaver_schema/src/resource.rs b/crates/weaver_schema/src/resource.rs new file mode 100644 index 00000000..24b87f6d --- /dev/null +++ b/crates/weaver_schema/src/resource.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A common resource specification. + +use crate::attribute::Attribute; +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; + +/// A common resource specification. +/// All the attributes mentioned in this specification will be inherited by all +/// the other specialized resource specifications. +/// Only used when a Client SDK is generated. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Resource { + /// The common attributes of the resource. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// A set of tags for the resource. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, +} + +impl Resource { + /// Returns an iterator over the attributes. + pub fn attributes(&self) -> impl Iterator { + self.attributes.iter() + } + + /// Returns the tags of the resource or None if not set. + pub fn tags(&self) -> Option<&Tags> { + self.tags.as_ref() + } +} diff --git a/crates/weaver_schema/src/resource_events.rs b/crates/weaver_schema/src/resource_events.rs new file mode 100644 index 00000000..8d9cfd70 --- /dev/null +++ b/crates/weaver_schema/src/resource_events.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Resource logs specification. + +use crate::attribute::Attribute; +use crate::event::Event; +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; + +/// A resource events specification. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct ResourceEvents { + /// Common attributes shared across events (implemented as log records). + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// Definitions of structured events this application or library generates. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub events: Vec, + /// A set of tags for the resource events. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} + +impl ResourceEvents { + /// Returns the number of events. + pub fn events_count(&self) -> usize { + self.events.len() + } + + /// Returns an event by name or None if not found. + pub fn event(&self, event_name: &str) -> Option<&Event> { + self.events + .iter() + .find(|event| event.event_name.as_str() == event_name) + } + + /// Returns a vector of events. + pub fn events(&self) -> Vec<&Event> { + self.events.iter().collect() + } +} diff --git a/crates/weaver_schema/src/resource_metrics.rs b/crates/weaver_schema/src/resource_metrics.rs new file mode 100644 index 00000000..adfd0d08 --- /dev/null +++ b/crates/weaver_schema/src/resource_metrics.rs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A resource metrics specification. + +use crate::attribute::Attribute; +use crate::metric_group::MetricGroup; +use crate::tags::Tags; +use crate::univariate_metric::UnivariateMetric; +use serde::{Deserialize, Serialize}; + +/// A resource metrics specification. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +pub struct ResourceMetrics { + /// Common attributes shared across metrics and metric groups. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// Definitions of all metrics this application or library generates (classic + /// univariate OTel metrics). + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub metrics: Vec, + /// Definitions of all multivariate metrics this application or library + /// generates. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub metric_groups: Vec, + /// A set of tags for the resource metrics. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} + +impl ResourceMetrics { + /// Returns the number of metrics. + pub fn metrics_count(&self) -> usize { + self.metrics.len() + } + + /// Returns the number of metric groups. + pub fn metric_groups_count(&self) -> usize { + self.metric_groups.len() + } + + /// Returns a metric by name or None if not found. + /// Note: this is a linear search. + pub fn metric(&self, name: &str) -> Option<&UnivariateMetric> { + self.metrics.iter().find(|metric| metric.name() == name) + } + + /// Returns a vector of metrics. + pub fn metrics(&self) -> Vec<&UnivariateMetric> { + self.metrics.iter().collect() + } + + /// Returns a metric group by name or None if not found. + /// Note: this is a linear search. + pub fn metric_group(&self, name: &str) -> Option<&MetricGroup> { + self.metric_groups + .iter() + .find(|metric_group| metric_group.name() == name) + } + + /// Returns a vector of metric groups. + pub fn metric_groups(&self) -> Vec<&MetricGroup> { + self.metric_groups.iter().collect() + } +} diff --git a/crates/weaver_schema/src/resource_spans.rs b/crates/weaver_schema/src/resource_spans.rs new file mode 100644 index 00000000..764c8f0e --- /dev/null +++ b/crates/weaver_schema/src/resource_spans.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A resource spans specification. + +use crate::attribute::Attribute; +use crate::span::Span; +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; + +/// A resource spans specification. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct ResourceSpans { + /// Common attributes shared across spans. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// Definitions of all spans this application or library generates. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub spans: Vec, + /// A set of tags for the resource spans. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} + +impl ResourceSpans { + /// Returns the number of spans. + pub fn spans_count(&self) -> usize { + self.spans.len() + } + + /// Returns a slice of spans. + pub fn spans(&self) -> Vec<&Span> { + self.spans.iter().collect() + } + + /// Returns a span by name or None if not found. + pub fn span(&self, name: &str) -> Option<&Span> { + self.spans + .iter() + .find(|span| span.span_name.as_str() == name) + } +} diff --git a/crates/weaver_schema/src/schema_spec.rs b/crates/weaver_schema/src/schema_spec.rs new file mode 100644 index 00000000..65396626 --- /dev/null +++ b/crates/weaver_schema/src/schema_spec.rs @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A schema specification. + +use serde::{Deserialize, Serialize}; + +use crate::event::Event; +use crate::instrumentation_library::InstrumentationLibrary; +use crate::metric_group::MetricGroup; +use crate::resource::Resource; +use crate::resource_events::ResourceEvents; +use crate::resource_metrics::ResourceMetrics; +use crate::resource_spans::ResourceSpans; +use crate::span::Span; +use crate::tags::Tags; +use crate::univariate_metric::UnivariateMetric; + +/// Definition of the telemetry schema for an application or a library. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +pub struct SchemaSpec { + /// A set of tags for the schema. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, + /// A common resource specification. + #[serde(skip_serializing_if = "Option::is_none")] + pub resource: Option, + /// The instrumentation library specification. + #[serde(skip_serializing_if = "Option::is_none")] + pub instrumentation_library: Option, + /// A resource metrics specification. + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_metrics: Option, + /// A resource events specification. + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_events: Option, + /// A resource spans specification. + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_spans: Option, +} + +impl SchemaSpec { + /// Returns the number of metrics. + pub fn metrics_count(&self) -> usize { + self.resource_metrics + .as_ref() + .map_or(0, |resource_metrics| resource_metrics.metrics_count()) + } + + /// Returns the number of metric groups. + pub fn metric_groups_count(&self) -> usize { + self.resource_metrics + .as_ref() + .map_or(0, |resource_metrics| resource_metrics.metric_groups_count()) + } + + /// Returns the number of events. + pub fn events_count(&self) -> usize { + self.resource_events + .as_ref() + .map_or(0, |resource_events| resource_events.events_count()) + } + + /// Returns the number of spans. + pub fn spans_count(&self) -> usize { + self.resource_spans + .as_ref() + .map_or(0, |resource_spans| resource_spans.spans_count()) + } + + /// Returns a metric by name or None if not found. + pub fn metric(&self, name: &str) -> Option<&UnivariateMetric> { + self.resource_metrics + .as_ref() + .and_then(|resource_metrics| resource_metrics.metric(name)) + } + + /// Returns a metric group by name or None if not found. + pub fn metric_group(&self, name: &str) -> Option<&MetricGroup> { + self.resource_metrics + .as_ref() + .and_then(|resource_metrics| resource_metrics.metric_group(name)) + } + + /// Returns a resource or None if not found. + pub fn resource(&self) -> Option<&Resource> { + self.resource.as_ref() + } + + /// Returns a vector of metrics. + pub fn metrics(&self) -> Vec<&UnivariateMetric> { + self.resource_metrics + .as_ref() + .map_or(Vec::<&UnivariateMetric>::new(), |resource_metrics| { + resource_metrics.metrics() + }) + } + + /// Returns a vector of metric groups. + pub fn metric_groups(&self) -> Vec<&MetricGroup> { + self.resource_metrics + .as_ref() + .map_or(Vec::<&MetricGroup>::new(), |resource_metrics| { + resource_metrics.metric_groups() + }) + } + + /// Returns a vector over the events. + pub fn events(&self) -> Vec<&Event> { + self.resource_events + .as_ref() + .map_or(Vec::<&Event>::new(), |resource_events| { + resource_events.events() + }) + } + + /// Returns a slice of spans. + pub fn spans(&self) -> Vec<&Span> { + self.resource_spans + .as_ref() + .map_or(Vec::<&Span>::new(), |resource_spans| resource_spans.spans()) + } + + /// Returns an event by name or None if not found. + pub fn event(&self, event_name: &str) -> Option<&Event> { + self.resource_events + .as_ref() + .and_then(|resource_events| resource_events.event(event_name)) + } + + /// Returns a span by name or None if not found. + pub fn span(&self, span_name: &str) -> Option<&Span> { + self.resource_spans + .as_ref() + .and_then(|resource_spans| resource_spans.span(span_name)) + } +} diff --git a/crates/weaver_schema/src/span.rs b/crates/weaver_schema/src/span.rs new file mode 100644 index 00000000..a9ee89a6 --- /dev/null +++ b/crates/weaver_schema/src/span.rs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Span specification. + +use crate::attribute::Attribute; +use crate::span_event::SpanEvent; +use crate::span_link::SpanLink; +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; +use weaver_semconv::group::SpanKindSpec; + +/// A span specification. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +pub struct Span { + /// The name of the span. + pub span_name: String, + /// The kind of the span. + #[serde(skip_serializing_if = "Option::is_none")] + pub kind: Option, + /// The attributes of the span. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// The events of the span. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub events: Vec, + /// The links of the span. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub links: Vec, + /// Brief description of the span. + pub brief: Option, + /// Longer description. + /// It defaults to an empty string. + pub note: Option, + /// A set of tags for the span. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} + +impl Span { + /// Returns an attribute by its name. + pub fn attribute(&self, id: &str) -> Option<&Attribute> { + self.attributes.iter().find(|a| a.id() == id) + } +} diff --git a/crates/weaver_schema/src/span_event.rs b/crates/weaver_schema/src/span_event.rs new file mode 100644 index 00000000..54d979c3 --- /dev/null +++ b/crates/weaver_schema/src/span_event.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Event specification. + +use crate::attribute::Attribute; +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; + +/// A span event specification. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +pub struct SpanEvent { + /// The name of the span event. + pub event_name: String, + /// The attributes of the span event. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// Brief description of the span event. + pub brief: Option, + /// Longer description. + /// It defaults to an empty string. + pub note: Option, + /// A set of tags for the span event. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} diff --git a/crates/weaver_schema/src/span_link.rs b/crates/weaver_schema/src/span_link.rs new file mode 100644 index 00000000..8e9f3a6f --- /dev/null +++ b/crates/weaver_schema/src/span_link.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Event specification. + +use crate::attribute::Attribute; +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; + +/// A span link specification. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +#[serde(rename_all = "snake_case")] +pub struct SpanLink { + /// The name of the span link. + pub link_name: String, + /// The attributes of the span link. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub attributes: Vec, + /// Brief description of the span link. + pub brief: Option, + /// Longer description. + /// It defaults to an empty string. + pub note: Option, + /// A set of tags for the span link. + #[serde(skip_serializing_if = "Option::is_none")] + pub tags: Option, +} diff --git a/crates/weaver_schema/src/tags.rs b/crates/weaver_schema/src/tags.rs new file mode 100644 index 00000000..32dec2af --- /dev/null +++ b/crates/weaver_schema/src/tags.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Tags for telemetry schemas. + +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// A set of tags. +/// +/// Examples of tags: +/// - sensitivity: pii +/// - sensitivity: phi +/// - data_classification: restricted +/// - semantic_type: email +/// - semantic_type: first_name +/// - owner: +/// - provenance: browser_sensor +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(transparent)] +#[serde(deny_unknown_fields)] +pub struct Tags { + /// The tags. + pub tags: BTreeMap, +} + +impl Tags { + /// Checks if the tags contain a specific tag. + pub fn has_tag(&self, tag: &str) -> bool { + self.tags.contains_key(tag) + } + + /// Gets a specific tag value from the tags if it exists or `None` otherwise. + pub fn get_tag(&self, tag: &str) -> Option<&String> { + self.tags.get(tag) + } + + /// Gets an iterator over the tags. + pub fn iter(&self) -> impl Iterator { + self.tags.iter() + } + + /// Checks if the tags are empty. + pub fn is_empty(&self) -> bool { + self.tags.is_empty() + } + + /// Merges the tags with another set of tags. If a tag exists in both sets of tags, the tag + /// from the current set of tags is used (i.e. self). + pub fn merge_with_override(&self, other: &Tags) -> Tags { + let mut tags = other.tags.clone(); + for (key, value) in self.tags.iter() { + _ = tags.insert(key.clone(), value.clone()); + } + Tags { tags } + } +} + +/// Merges two sets of tags. If a tag exists in both sets of tags, the tag from `tags` +/// is used to override the tag from `parent_tags`. +pub fn merge_with_override(tags: Option<&Tags>, parent_tags: Option<&Tags>) -> Option { + match (tags, parent_tags) { + (Some(tags), Some(parent_tags)) => Some(tags.merge_with_override(parent_tags)), + (Some(tags), None) => Some(tags.clone()), + (None, Some(parent_tags)) => Some(parent_tags.clone()), + (None, None) => None, + } +} diff --git a/crates/weaver_schema/src/univariate_metric.rs b/crates/weaver_schema/src/univariate_metric.rs new file mode 100644 index 00000000..b75861de --- /dev/null +++ b/crates/weaver_schema/src/univariate_metric.rs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! A univariate metric specification. + +use crate::attribute::Attribute; +use crate::tags::Tags; +use serde::{Deserialize, Serialize}; +use weaver_semconv::group::InstrumentSpec; + +/// A univariate metric specification. +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +#[serde(untagged)] +pub enum UnivariateMetric { + /// A reference to a metric. + Ref { + /// The reference to the metric. + r#ref: String, + /// The attributes of the metric. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + attributes: Vec, + /// A set of tags for the metric. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, + }, + + /// A fully defined metric. + Metric { + /// Metric name. + name: String, + /// Brief description of the metric. + brief: String, + /// Note on the metric. + note: String, + /// Attributes of the metric. + #[serde(default)] + attributes: Vec, + /// Type of the metric (e.g. gauge, histogram, ...). + instrument: InstrumentSpec, + /// Unit of the metric. + unit: Option, + /// A set of tags for the metric. + #[serde(skip_serializing_if = "Option::is_none")] + tags: Option, + }, +} + +impl UnivariateMetric { + /// Returns the name of the metric. + pub fn name(&self) -> String { + match self { + UnivariateMetric::Ref { r#ref, .. } => r#ref.clone(), + UnivariateMetric::Metric { name, .. } => name.clone(), + } + } + + /// Returns the brief description of the metric. + pub fn brief(&self) -> String { + match self { + UnivariateMetric::Ref { .. } => String::new(), + UnivariateMetric::Metric { brief, .. } => brief.clone(), + } + } + + /// Returns the note on the metric. + pub fn note(&self) -> String { + match self { + UnivariateMetric::Ref { .. } => String::new(), + UnivariateMetric::Metric { note, .. } => note.clone(), + } + } + + /// Returns the tags of the metric. + pub fn tags(&self) -> Option<&Tags> { + match self { + UnivariateMetric::Ref { tags, .. } => tags.as_ref(), + UnivariateMetric::Metric { tags, .. } => tags.as_ref(), + } + } + + /// Returns an attribute by its id. + pub fn attribute(&self, id: &str) -> Option<&Attribute> { + match self { + UnivariateMetric::Ref { attributes, .. } => attributes.iter().find(|a| a.id() == id), + UnivariateMetric::Metric { attributes, .. } => attributes.iter().find(|a| a.id() == id), + } + } +} diff --git a/crates/weaver_semconv/Cargo.toml b/crates/weaver_semconv/Cargo.toml new file mode 100644 index 00000000..d128d403 --- /dev/null +++ b/crates/weaver_semconv/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "weaver_semconv" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true +edition.workspace = true + +[dependencies] +serde.workspace = true +serde_yaml.workspace = true +thiserror.workspace = true +ureq.workspace = true +ordered-float.workspace = true + +validator = { version = "0.16.1", features = ["derive"] } diff --git a/crates/weaver_semconv/README.md b/crates/weaver_semconv/README.md new file mode 100644 index 00000000..6a8f26ea --- /dev/null +++ b/crates/weaver_semconv/README.md @@ -0,0 +1,11 @@ +# Semantic Convention Catalog + +## Introduction + +This crate provides serialization and deserialization support for YAML files +adhering to the semantic convention (semconv catalog), as well as a set of +methods to resolve references between different semconv catalogs. + +See [semantic convention YAML language](https://github.com/open-telemetry/build-tools/blob/main/semantic-conventions/syntax.md) +for more details on the syntax. + diff --git a/crates/weaver_semconv/data/client.yaml b/crates/weaver_semconv/data/client.yaml new file mode 100644 index 00000000..13c2552e --- /dev/null +++ b/crates/weaver_semconv/data/client.yaml @@ -0,0 +1,46 @@ +groups: + - id: client + prefix: client + type: attribute_group + brief: > + These attributes may be used to describe the client in a connection-based network interaction + where there is one side that initiates the connection (the client is the side that initiates the connection). + This covers all TCP network interactions since TCP is connection-based and one side initiates the + connection (an exception is made for peer-to-peer communication over TCP where the "user-facing" surface of the + protocol / API does not expose a clear notion of client and server). + This also covers UDP network interactions where one side initiates the interaction, e.g. QUIC (HTTP/3) and DNS. + attributes: + - id: address + type: string + brief: Client address - IP address or Unix domain socket name. + note: > + When observed from the server side, and when communicating through an intermediary, `client.address` SHOULD represent + the client address behind any intermediaries (e.g. proxies) if it's available. + examples: ['/tmp/my.sock', '10.1.2.80'] + - id: port + type: int + brief: Client port number. + examples: [65123] + note: > + When observed from the server side, and when communicating through an intermediary, `client.port` SHOULD represent + the client port behind any intermediaries (e.g. proxies) if it's available. + - id: socket.address + type: string + brief: Client address of the socket connection - IP address or Unix domain socket name. + note: > + When observed from the server side, this SHOULD represent the immediate client peer address. + + When observed from the client side, this SHOULD represent the physical client address. + examples: ['/tmp/my.sock', '127.0.0.1'] + requirement_level: + recommended: If different than `client.address`. + - id: socket.port + type: int + brief: Client port number of the socket connection. + note: > + When observed from the server side, this SHOULD represent the immediate client peer port. + + When observed from the client side, this SHOULD represent the physical client port. + examples: [35555] + requirement_level: + recommended: If different than `client.port`. \ No newline at end of file diff --git a/crates/weaver_semconv/data/cloud.yaml b/crates/weaver_semconv/data/cloud.yaml new file mode 100644 index 00000000..e57eeaf5 --- /dev/null +++ b/crates/weaver_semconv/data/cloud.yaml @@ -0,0 +1,179 @@ +groups: + - id: cloud + prefix: cloud + type: resource + brief: > + A cloud environment (e.g. GCP, Azure, AWS) + attributes: + - id: provider + type: + allow_custom_values: true + members: + - id: 'alibaba_cloud' + value: 'alibaba_cloud' + brief: 'Alibaba Cloud' + - id: 'aws' + value: 'aws' + brief: 'Amazon Web Services' + - id: 'azure' + value: 'azure' + brief: 'Microsoft Azure' + - id: 'gcp' + value: 'gcp' + brief: 'Google Cloud Platform' + - id: 'heroku' + value: 'heroku' + brief: 'Heroku Platform as a Service' + - id: 'ibm_cloud' + value: 'ibm_cloud' + brief: 'IBM Cloud' + - id: 'tencent_cloud' + value: 'tencent_cloud' + brief: 'Tencent Cloud' + + brief: > + Name of the cloud provider. + - id: account.id + type: string + brief: > + The cloud account ID the resource is assigned to. + examples: ['111111111111', 'opentelemetry'] + - id: region + type: string + brief: > + The geographical region the resource is running. + note: > + 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/en-us/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'] + - id: resource_id + type: string + brief: > + 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/en-us/rest/api/resources/resources/get-by-id) on Azure, + a [full resource name](https://cloud.google.com/apis/design/resource_names#full_resource_name) on GCP) + note: | + 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 invokable 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://docs.microsoft.com/en-us/rest/api/resources/resources/get-by-id) of the invoked function, + *not* the function app, having the form + `/subscriptions//resourceGroups//providers/Microsoft.Web/sites//functions/`. + 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/' + - id: availability_zone + type: string + brief: > + Cloud regions often have multiple, isolated locations known as zones + to increase availability. Availability zone represents the + zone where the resource is running. + note: > + Availability zones are called "zones" on Alibaba Cloud and Google Cloud. + examples: ['us-east-1c'] + - id: platform + type: + allow_custom_values: true + members: + - id: alibaba_cloud_ecs + value: 'alibaba_cloud_ecs' + brief: Alibaba Cloud Elastic Compute Service + - id: alibaba_cloud_fc + value: 'alibaba_cloud_fc' + brief: Alibaba Cloud Function Compute + - id: alibaba_cloud_openshift + value: 'alibaba_cloud_openshift' + brief: Red Hat OpenShift on Alibaba Cloud + - id: aws_ec2 + value: 'aws_ec2' + brief: AWS Elastic Compute Cloud + - id: aws_ecs + value: 'aws_ecs' + brief: AWS Elastic Container Service + - id: aws_eks + value: 'aws_eks' + brief: AWS Elastic Kubernetes Service + - id: aws_lambda + value: 'aws_lambda' + brief: AWS Lambda + - id: aws_elastic_beanstalk + value: 'aws_elastic_beanstalk' + brief: AWS Elastic Beanstalk + - id: aws_app_runner + value: 'aws_app_runner' + brief: AWS App Runner + - id: aws_openshift + value: 'aws_openshift' + brief: Red Hat OpenShift on AWS (ROSA) + - id: azure_vm + value: 'azure_vm' + brief: Azure Virtual Machines + - id: azure_container_instances + value: 'azure_container_instances' + brief: Azure Container Instances + - id: azure_aks + value: 'azure_aks' + brief: Azure Kubernetes Service + - id: azure_functions + value: 'azure_functions' + brief: Azure Functions + - id: azure_app_service + value: 'azure_app_service' + brief: Azure App Service + - id: azure_openshift + value: 'azure_openshift' + brief: Azure Red Hat OpenShift + - id: gcp_bare_metal_solution + value: 'gcp_bare_metal_solution' + brief: Google Bare Metal Solution (BMS) + - id: gcp_compute_engine + value: 'gcp_compute_engine' + brief: Google Cloud Compute Engine (GCE) + - id: gcp_cloud_run + value: 'gcp_cloud_run' + brief: Google Cloud Run + - id: gcp_kubernetes_engine + value: 'gcp_kubernetes_engine' + brief: Google Cloud Kubernetes Engine (GKE) + - id: gcp_cloud_functions + value: 'gcp_cloud_functions' + brief: Google Cloud Functions (GCF) + - id: gcp_app_engine + value: 'gcp_app_engine' + brief: Google Cloud App Engine (GAE) + - id: gcp_openshift + value: 'gcp_openshift' + brief: Red Hat OpenShift on Google Cloud + - id: ibm_cloud_openshift + value: 'ibm_cloud_openshift' + brief: Red Hat OpenShift on IBM Cloud + - id: tencent_cloud_cvm + value: 'tencent_cloud_cvm' + brief: Tencent Cloud Cloud Virtual Machine (CVM) + - id: tencent_cloud_eks + value: 'tencent_cloud_eks' + brief: Tencent Cloud Elastic Kubernetes Service (EKS) + - id: tencent_cloud_scf + value: 'tencent_cloud_scf' + brief: Tencent Cloud Serverless Cloud Function (SCF) + brief: > + The cloud platform in use. + note: > + The prefix of the service SHOULD match the one specified in `cloud.provider`. \ No newline at end of file diff --git a/crates/weaver_semconv/data/cloudevents.yaml b/crates/weaver_semconv/data/cloudevents.yaml new file mode 100644 index 00000000..8ee54c59 --- /dev/null +++ b/crates/weaver_semconv/data/cloudevents.yaml @@ -0,0 +1,36 @@ +groups: + - id: cloudevents + prefix: cloudevents + type: span + brief: > + This document defines attributes for CloudEvents. + CloudEvents is a specification on how to define event data in a standard way. + These attributes can be attached to spans when performing operations with CloudEvents, regardless of the protocol being used. + attributes: + - id: event_id + type: string + requirement_level: required + brief: > + The [event_id](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#id) uniquely identifies the event. + examples: ['123e4567-e89b-12d3-a456-426614174000', '0001'] + - id: event_source + type: string + requirement_level: required + brief: > + The [source](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#source-1) identifies the context in which an event happened. + examples: ['https://github.com/cloudevents', '/cloudevents/spec/pull/123', 'my-service' ] + - id: event_spec_version + type: string + brief: > + The [version of the CloudEvents specification](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#specversion) which the event uses. + examples: '1.0' + - id: event_type + type: string + brief: > + 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. + examples: ['com.github.pull_request.opened', 'com.example.object.deleted.v2'] + - id: event_subject + type: string + brief: > + 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). + examples: 'mynewfile.jpg' \ No newline at end of file diff --git a/crates/weaver_semconv/data/database-metrics.yaml b/crates/weaver_semconv/data/database-metrics.yaml new file mode 100644 index 00000000..701dbc62 --- /dev/null +++ b/crates/weaver_semconv/data/database-metrics.yaml @@ -0,0 +1,107 @@ +groups: + - id: attributes.db + type: attribute_group + brief: Describes Database attributes + attributes: + - id: state + type: + allow_custom_values: false + members: + - id: idle + value: 'idle' + - id: used + value: 'used' + requirement_level: required + brief: "The state of a connection in the pool" + examples: ["idle"] + - id: pool.name + type: string + requirement_level: required + brief: > + The name of the connection pool; unique within the instrumented application. + In case the connection pool implementation does not provide a name, + then the [db.connection_string](/docs/database/database-spans.md#connection-level-attributes) + should be used + examples: ["myDataSource"] + + - id: metric.db.client.connections.usage + type: metric + metric_name: db.client.connections.usage + brief: "The number of connections that are currently in state described by the `state` attribute" + instrument: updowncounter + unit: "{connection}" + attributes: + - ref: state + - ref: pool.name + + - id: metric.db.client.connections.idle.max + type: metric + metric_name: db.client.connections.idle.max + brief: "The maximum number of idle open connections allowed" + instrument: updowncounter + unit: "{connection}" + attributes: + - ref: pool.name + + - id: metric.db.client.connections.idle.min + type: metric + metric_name: db.client.connections.idle.min + brief: "The minimum number of idle open connections allowed" + instrument: updowncounter + unit: "{connection}" + attributes: + - ref: pool.name + + - id: metric.db.client.connections.max + type: metric + metric_name: db.client.connections.max + brief: "The maximum number of open connections allowed" + instrument: updowncounter + unit: "{connection}" + attributes: + - ref: pool.name + + - id: metric.db.client.connections.pending_requests + type: metric + metric_name: db.client.connections.pending_requests + brief: "The number of pending requests for an open connection, cumulative for the entire pool" + instrument: updowncounter + unit: "{request}" + attributes: + - ref: pool.name + + - id: metric.db.client.connections.timeouts + type: metric + metric_name: db.client.connections.timeouts + brief: "The number of connection timeouts that have occurred trying to obtain a connection from the pool" + instrument: counter + unit: "{timeout}" + attributes: + - ref: pool.name + + - id: metric.db.client.connections.create_time + type: metric + metric_name: db.client.connections.create_time + brief: "The time it took to create a new connection" + instrument: histogram + unit: "ms" + attributes: + - ref: pool.name + + - id: metric.db.client.connections.wait_time + type: metric + metric_name: db.client.connections.wait_time + brief: "The time it took to obtain an open connection from the pool" + instrument: histogram + unit: "ms" + attributes: + - ref: pool.name + + - id: metric.db.client.connections.use_time + type: metric + metric_name: db.client.connections.use_time + brief: "The time between borrowing a connection and returning it to the pool" + instrument: histogram + unit: "ms" + attributes: + - ref: pool.name \ No newline at end of file diff --git a/crates/weaver_semconv/data/database.yaml b/crates/weaver_semconv/data/database.yaml new file mode 100644 index 00000000..baf5d2e8 --- /dev/null +++ b/crates/weaver_semconv/data/database.yaml @@ -0,0 +1,588 @@ +groups: + - id: db + prefix: db + type: span + brief: > + This document defines the attributes used to perform database client calls. + span_kind: client + attributes: + - id: system + tag: connection-level + brief: An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers. + requirement_level: required + type: + allow_custom_values: true + members: + - id: other_sql + value: 'other_sql' + brief: 'Some other SQL database. Fallback only. See notes.' + - id: mssql + value: 'mssql' + brief: 'Microsoft SQL Server' + - id: mssqlcompact + value: 'mssqlcompact' + brief: 'Microsoft SQL Server Compact' + - id: mysql + value: 'mysql' + brief: 'MySQL' + - id: oracle + value: 'oracle' + brief: 'Oracle Database' + - id: db2 + value: 'db2' + brief: 'IBM Db2' + - id: postgresql + value: 'postgresql' + brief: 'PostgreSQL' + - id: redshift + value: 'redshift' + brief: 'Amazon Redshift' + - id: hive + value: 'hive' + brief: 'Apache Hive' + - id: cloudscape + value: 'cloudscape' + brief: 'Cloudscape' + - id: hsqldb + value: 'hsqldb' + brief: 'HyperSQL DataBase' + - id: progress + value: 'progress' + brief: 'Progress Database' + - id: maxdb + value: 'maxdb' + brief: 'SAP MaxDB' + - id: hanadb + value: 'hanadb' + brief: 'SAP HANA' + - id: ingres + value: 'ingres' + brief: 'Ingres' + - id: firstsql + value: 'firstsql' + brief: 'FirstSQL' + - id: edb + value: 'edb' + brief: 'EnterpriseDB' + - id: cache + value: 'cache' + brief: 'InterSystems Caché' + - id: adabas + value: 'adabas' + brief: 'Adabas (Adaptable Database System)' + - id: firebird + value: 'firebird' + brief: 'Firebird' + - id: derby + value: 'derby' + brief: 'Apache Derby' + - id: filemaker + value: 'filemaker' + brief: 'FileMaker' + - id: informix + value: 'informix' + brief: 'Informix' + - id: instantdb + value: 'instantdb' + brief: 'InstantDB' + - id: interbase + value: 'interbase' + brief: 'InterBase' + - id: mariadb + value: 'mariadb' + brief: 'MariaDB' + - id: netezza + value: 'netezza' + brief: 'Netezza' + - id: pervasive + value: 'pervasive' + brief: 'Pervasive PSQL' + - id: pointbase + value: 'pointbase' + brief: 'PointBase' + - id: sqlite + value: 'sqlite' + brief: 'SQLite' + - id: sybase + value: 'sybase' + brief: 'Sybase' + - id: teradata + value: 'teradata' + brief: 'Teradata' + - id: vertica + value: 'vertica' + brief: 'Vertica' + - id: h2 + value: 'h2' + brief: 'H2' + - id: coldfusion + value: 'coldfusion' + brief: 'ColdFusion IMQ' + - id: cassandra + value: 'cassandra' + brief: 'Apache Cassandra' + - id: hbase + value: 'hbase' + brief: 'Apache HBase' + - id: mongodb + value: 'mongodb' + brief: 'MongoDB' + - id: redis + value: 'redis' + brief: 'Redis' + - id: couchbase + value: 'couchbase' + brief: 'Couchbase' + - id: couchdb + value: 'couchdb' + brief: 'CouchDB' + - id: cosmosdb + value: 'cosmosdb' + brief: 'Microsoft Azure Cosmos DB' + - id: dynamodb + value: 'dynamodb' + brief: 'Amazon DynamoDB' + - id: neo4j + value: 'neo4j' + brief: 'Neo4j' + - id: geode + value: 'geode' + brief: 'Apache Geode' + - id: elasticsearch + value: 'elasticsearch' + brief: 'Elasticsearch' + - id: memcached + value: 'memcached' + brief: 'Memcached' + - id: cockroachdb + value: 'cockroachdb' + brief: 'CockroachDB' + - id: opensearch + value: 'opensearch' + brief: 'OpenSearch' + - id: clickhouse + value: 'clickhouse' + brief: 'ClickHouse' + - id: spanner + value: 'spanner' + brief: 'Cloud Spanner' + - id: trino + value: 'trino' + brief: 'Trino' + - id: connection_string + tag: connection-level + type: string + brief: > + The connection string used to connect to the database. + It is recommended to remove embedded credentials. + examples: 'Server=(localdb)\v11.0;Integrated Security=true;' + - id: user + tag: connection-level + type: string + brief: > + Username for accessing the database. + examples: ['readonly_user', 'reporting_user'] + - id: jdbc.driver_classname + tag: connection-level-tech-specific + type: string + brief: > + The fully-qualified class name of the [Java Database Connectivity (JDBC)](https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/) driver used to connect. + examples: ['org.postgresql.Driver', 'com.microsoft.sqlserver.jdbc.SQLServerDriver'] + - id: name + tag: call-level + type: string + requirement_level: + conditionally_required: If applicable. + brief: > + This attribute is used to report the name of the database being accessed. + For commands that switch the database, this should be set to the target database + (even if the command fails). + note: > + In some SQL databases, the database name to be used is called "schema name". + In case there are multiple layers that could be considered for database name + (e.g. Oracle instance name and schema name), + the database name to be used is the more specific layer (e.g. Oracle schema name). + examples: [ 'customers', 'main' ] + - id: statement + tag: call-level + type: string + requirement_level: + recommended: > + Should be collected by default only if there is sanitization that excludes sensitive information. + brief: > + The database statement being executed. + examples: ['SELECT * FROM wuser_table', 'SET mykey "WuValue"'] + - id: operation + tag: call-level + type: string + requirement_level: + conditionally_required: If `db.statement` is not applicable. + brief: > + The name of the operation being executed, e.g. the [MongoDB command name](https://docs.mongodb.com/manual/reference/command/#database-operations) + such as `findAndModify`, or the SQL keyword. + note: > + When setting this to an SQL keyword, it is not recommended to + attempt any client-side parsing of `db.statement` just to get this + property, but it should be set if the operation name is provided by + the library being instrumented. + If the SQL statement has an ambiguous operation, or performs more + than one operation, this value may be omitted. + examples: ['findAndModify', 'HMSET', 'SELECT'] + - ref: server.address + tag: connection-level + requirement_level: + conditionally_required: See alternative attributes below. + brief: > + Name of the database host. + - ref: server.port + tag: connection-level + requirement_level: + conditionally_required: If using a port other than the default port for this DBMS and if `server.address` is set. + - ref: server.socket.address + tag: connection-level + - ref: server.socket.port + tag: connection-level + - ref: network.transport + tag: connection-level + - ref: network.type + tag: connection-level + - ref: server.socket.domain + requirement_level: + recommended: If different than `server.address` and if `server.socket.address` is set. + constraints: + - any_of: + - 'server.address' + - 'server.socket.address' + + - id: db.mssql + prefix: db.mssql + type: span + extends: db + brief: > + Connection-level attributes for Microsoft SQL Server + attributes: + - id: instance_name + tag: connection-level-tech-specific + type: string + note: > + If setting a `db.mssql.instance_name`, `server.port` is no longer + required (but still recommended if non-standard). + brief: > + The Microsoft SQL Server [instance name](https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) + connecting to. This name is used to determine the port of a named instance. + examples: 'MSSQLSERVER' + + - id: db.cassandra + prefix: db.cassandra + type: span + extends: db + brief: > + Call-level attributes for Cassandra + attributes: + - ref: db.name + tag: call-level-tech-specific-cassandra + brief: > + The keyspace name in Cassandra. + examples: ["mykeyspace"] + note: For Cassandra the `db.name` should be set to the Cassandra keyspace name. + - id: page_size + type: int + tag: call-level-tech-specific-cassandra + brief: > + The fetch size used for paging, i.e. how many rows will be returned at once. + examples: [5000] + - id: consistency_level + tag: call-level-tech-specific-cassandra + brief: > + 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). + type: + members: + - id: all + value: 'all' + - id: each_quorum + value: 'each_quorum' + - id: quorum + value: 'quorum' + - id: local_quorum + value: 'local_quorum' + - id: one + value: 'one' + - id: two + value: 'two' + - id: three + value: 'three' + - id: local_one + value: 'local_one' + - id: any + value: 'any' + - id: serial + value: 'serial' + - id: local_serial + value: 'local_serial' + - id: table + type: string + tag: call-level-tech-specific-cassandra + requirement_level: recommended + brief: The name of the primary table that the operation is acting upon, including the keyspace name (if applicable). + note: > + This mirrors the db.sql.table attribute but references cassandra rather than sql. + It is not recommended to attempt any client-side parsing of + `db.statement` just to get this property, but it should be set if + it is provided by the library being instrumented. + If the operation is acting upon an anonymous table, or more than one table, this + value MUST NOT be set. + examples: 'mytable' + - id: idempotence + type: boolean + tag: call-level-tech-specific-cassandra + brief: > + Whether or not the query is idempotent. + - id: speculative_execution_count + type: int + tag: call-level-tech-specific-cassandra + brief: > + The number of times a query was speculatively executed. Not set or `0` if the query was not executed speculatively. + examples: [0, 2] + - id: coordinator.id + type: string + tag: call-level-tech-specific-cassandra + brief: > + The ID of the coordinating node for a query. + examples: 'be13faa2-8574-4d71-926d-27f16cf8a7af' + - id: coordinator.dc + type: string + tag: call-level-tech-specific-cassandra + brief: > + The data center of the coordinating node for a query. + examples: 'us-west-2' + + - id: db.hbase + prefix: db.hbase + type: span + extends: db + brief: > + Call-level attributes for HBase + attributes: + - ref: db.name + tag: call-level-tech-specific + brief: > + The HBase namespace. + examples: ['mynamespace'] + note: For HBase the `db.name` should be set to the HBase namespace. + + - id: db.couchdb + prefix: db.couchdb + type: span + extends: db + brief: > + Call-level attributes for CouchDB + attributes: + - ref: db.operation + tag: call-level-tech-specific + brief: > + The HTTP method + the target REST route. + examples: ['GET /{db}/{docid}'] + note: > + In **CouchDB**, `db.operation` should be set to the HTTP method + + the target REST route according to the API reference documentation. + For example, when retrieving a document, `db.operation` would be set to + (literally, i.e., without replacing the placeholders with concrete values): + [`GET /{db}/{docid}`](http://docs.couchdb.org/en/stable/api/document/common.html#get--db-docid). + + - id: db.redis + prefix: db.redis + type: span + extends: db + brief: > + Call-level attributes for Redis + attributes: + - id: database_index + type: int + requirement_level: + conditionally_required: If other than the default database (`0`). + tag: call-level-tech-specific + brief: > + The index of the database being accessed as used in the [`SELECT` command](https://redis.io/commands/select), provided as an integer. + To be used instead of the generic `db.name` attribute. + examples: [0, 1, 15] + - ref: db.statement + tag: call-level-tech-specific + brief: > + The full syntax of the Redis CLI command. + examples: ["HMSET myhash field1 'Hello' field2 'World'"] + note: > + For **Redis**, the value provided for `db.statement` SHOULD correspond to the syntax of the Redis CLI. + If, for example, the [`HMSET` command](https://redis.io/commands/hmset) is invoked, `"HMSET myhash field1 'Hello' field2 'World'"` would be a suitable value for `db.statement`. + - id: db.mongodb + prefix: db.mongodb + type: span + extends: db + brief: > + Call-level attributes for MongoDB + attributes: + - id: collection + type: string + requirement_level: required + tag: call-level-tech-specific + brief: > + The collection being accessed within the database stated in `db.name`. + examples: [ 'customers', 'products' ] + + - id: db.elasticsearch + prefix: db.elasticsearch + type: span + extends: db + brief: > + Call-level attributes for Elasticsearch + attributes: + - ref: http.request.method + requirement_level: required + - ref: db.operation + requirement_level: required + brief: The endpoint identifier for the request. + examples: [ 'search', 'ml.close_job', 'cat.aliases' ] + - ref: url.full + requirement_level: required + examples: [ 'https://localhost:9200/index/_search?q=user.id:kimchy' ] + - ref: db.statement + requirement_level: + recommended: > + Should be collected by default for search-type queries and only if there is sanitization that excludes + sensitive information. + brief: The request body for a [search-type query](https://www.elastic.co/guide/en/elasticsearch/reference/current/search.html), as a json string. + examples: [ '"{\"query\":{\"term\":{\"user.id\":\"kimchy\"}}}"' ] + - ref: server.address + - ref: server.port + + - id: db.sql + prefix: 'db.sql' + type: span + extends: 'db' + brief: > + Call-level attributes for SQL databases + attributes: + - id: table + tag: call-level-tech-specific + type: string + requirement_level: recommended + brief: The name of the primary table that the operation is acting upon, including the database name (if applicable). + note: > + It is not recommended to attempt any client-side parsing of + `db.statement` just to get this property, but it should be set if + it is provided by the library being instrumented. + If the operation is acting upon an anonymous table, or more than one table, this + value MUST NOT be set. + examples: ['public.users', 'customers'] + + - id: db.cosmosdb + type: span + extends: db + prefix: db.cosmosdb + brief: > + Call-level attributes for Cosmos DB. + attributes: + - id: client_id + type: string + brief: Unique Cosmos client instance id. + examples: '3ba4827d-4422-483f-b59f-85b74211c11d' + - id: operation_type + type: + allow_custom_values: true + members: + - id: invalid + value: 'Invalid' + - id: create + value: 'Create' + - id: patch + value: 'Patch' + - id: read + value: 'Read' + - id: read_feed + value: 'ReadFeed' + - id: delete + value: 'Delete' + - id: replace + value: 'Replace' + - id: execute + value: 'Execute' + - id: query + value: 'Query' + - id: head + value: 'Head' + - id: head_feed + value: 'HeadFeed' + - id: upsert + value: 'Upsert' + - id: batch + value: 'Batch' + - id: query_plan + value: 'QueryPlan' + - id: execute_javascript + value: 'ExecuteJavaScript' + brief: CosmosDB Operation Type. + requirement_level: + conditionally_required: when performing one of the operations in this list + - ref: user_agent.original + brief: 'Full user-agent string is generated by Cosmos DB SDK' + note: > + The user-agent value is generated by SDK which is a combination of
+ `sdk_version` : Current version of SDK. e.g. 'cosmos-netstandard-sdk/3.23.0'
+ `direct_pkg_version` : Direct package version used by Cosmos DB SDK. e.g. '3.23.1'
+ `number_of_client_instances` : Number of cosmos client instances created by the application. e.g. '1'
+ `type_of_machine_architecture` : Machine architecture. e.g. 'X64'
+ `operating_system` : Operating System. e.g. 'Linux 5.4.0-1098-azure 104 18'
+ `runtime_framework` : Runtime Framework. e.g. '.NET Core 3.1.32'
+ `failover_information` : Generated key to determine if region failover enabled. + Format Reg-{D (Disabled discovery)}-S(application region)|L(List of preferred regions)|N(None, user did not configure it). + Default value is "NS". + examples: ['cosmos-netstandard-sdk/3.23.0\|3.23.1\|1\|X64\|Linux 5.4.0-1098-azure 104 18\|.NET Core 3.1.32\|S\|'] + - id: connection_mode + type: + allow_custom_values: false + members: + - id: gateway + value: 'gateway' + brief: Gateway (HTTP) connections mode + - id: direct + value: 'direct' + brief: Direct connection. + brief: Cosmos client connection mode. + requirement_level: + conditionally_required: if not `direct` (or pick gw as default) + - id: container + type: string + brief: Cosmos DB container name. + requirement_level: + conditionally_required: if available + examples: 'anystring' + - id: request_content_length + type: int + brief: Request payload size in bytes + - id: status_code + type: int + brief: Cosmos DB status code. + examples: [200, 201] + requirement_level: + conditionally_required: if response was received + - id: sub_status_code + type: int + brief: Cosmos DB sub status code. + examples: [1000, 1002] + requirement_level: + conditionally_required: when response was received and contained sub-code. + - id: request_charge + type: double + brief: RU consumed for that operation + examples: [46.18, 1.0] + requirement_level: + conditionally_required: when available + + - id: db.tech + type: span + brief: "Semantic convention group for specific technologies" + constraints: + - include: 'db.cassandra' + - include: 'db.redis' + - include: 'db.mongodb' + - include: 'db.sql' + - include: 'db.cosmosdb' \ No newline at end of file diff --git a/crates/weaver_semconv/data/exception.yaml b/crates/weaver_semconv/data/exception.yaml new file mode 100644 index 00000000..1c4eb9a5 --- /dev/null +++ b/crates/weaver_semconv/data/exception.yaml @@ -0,0 +1,33 @@ +groups: + - id: exception + type: span + prefix: exception + brief: > + This document defines the shared attributes used to + report a single exception associated with a span or log. + attributes: + - id: type + type: string + brief: > + 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. + examples: ["java.net.ConnectException", "OSError"] + - id: message + type: string + brief: The exception message. + examples: ["Division by zero", "Can't convert 'int' object to str implicitly"] + - id: stacktrace + type: string + brief: > + 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. + 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)' + + constraints: + - any_of: + - "exception.type" + - "exception.message" \ No newline at end of file diff --git a/crates/weaver_semconv/data/faas-common.yaml b/crates/weaver_semconv/data/faas-common.yaml new file mode 100644 index 00000000..ca424b17 --- /dev/null +++ b/crates/weaver_semconv/data/faas-common.yaml @@ -0,0 +1,77 @@ +groups: + - id: attributes.faas.common + type: attribute_group + brief: "Describes FaaS attributes." + prefix: faas + attributes: + - id: trigger + brief: 'Type of the trigger which caused this function invocation.' + type: + allow_custom_values: false + members: + - id: datasource + value: 'datasource' + brief: 'A response to some data source operation such as a database or filesystem read/write' + - id: http + value: 'http' + brief: 'To provide an answer to an inbound HTTP request' + - id: pubsub + value: 'pubsub' + brief: 'A function is set to be executed when messages are sent to a messaging system' + - id: timer + value: 'timer' + brief: 'A function is scheduled to be executed regularly' + - id: other + value: 'other' + brief: 'If none of the others apply' + - id: invoked_name + type: string + requirement_level: required + brief: > + The name of the invoked function. + note: > + SHOULD be equal to the `faas.name` resource attribute of the + invoked function. + examples: 'my-function' + - id: invoked_provider + type: + allow_custom_values: true + members: + - id: 'alibaba_cloud' + value: 'alibaba_cloud' + brief: 'Alibaba Cloud' + - id: 'aws' + value: 'aws' + brief: 'Amazon Web Services' + - id: 'azure' + value: 'azure' + brief: 'Microsoft Azure' + - id: 'gcp' + value: 'gcp' + brief: 'Google Cloud Platform' + - id: 'tencent_cloud' + value: 'tencent_cloud' + brief: 'Tencent Cloud' + requirement_level: required + brief: > + The cloud provider of the invoked function. + note: > + SHOULD be equal to the `cloud.provider` resource attribute of the + invoked function. + - id: invoked_region + type: string + requirement_level: + conditionally_required: > + For some cloud providers, like AWS or GCP, the region in which a + function is hosted is essential to uniquely identify the function + and also part of its endpoint. Since it's part of the endpoint + being called, the region is always known to clients. In these cases, + `faas.invoked_region` MUST be set accordingly. If the region is + unknown to the client or not required for identifying the invoked + function, setting `faas.invoked_region` is optional. + brief: > + The cloud region of the invoked function. + note: > + SHOULD be equal to the `cloud.region` resource attribute of the + invoked function. + examples: 'eu-central-1' \ No newline at end of file diff --git a/crates/weaver_semconv/data/faas-metrics.yaml b/crates/weaver_semconv/data/faas-metrics.yaml new file mode 100644 index 00000000..d0eba942 --- /dev/null +++ b/crates/weaver_semconv/data/faas-metrics.yaml @@ -0,0 +1,81 @@ +groups: + - id: metric.faas.invoke_duration + type: metric + metric_name: faas.invoke_duration + brief: "Measures the duration of the function's logic execution" + instrument: histogram + unit: "ms" + attributes: + - ref: faas.trigger + + - id: metric.faas.init_duration + type: metric + metric_name: faas.init_duration + brief: "Measures the duration of the function's initialization, such as a cold start" + instrument: histogram + unit: "ms" + attributes: + - ref: faas.trigger + + - id: metric.faas.coldstarts + type: metric + metric_name: faas.coldstarts + brief: "Number of invocation cold starts" + instrument: counter + unit: "{coldstart}" + attributes: + - ref: faas.trigger + + - id: metric.faas.errors + type: metric + metric_name: faas.errors + brief: "Number of invocation errors" + instrument: counter + unit: "{error}" + attributes: + - ref: faas.trigger + + - id: metric.faas.invocations + type: metric + metric_name: faas.invocations + brief: "Number of successful invocations" + instrument: counter + unit: "{invocation}" + attributes: + - ref: faas.trigger + + - id: metric.faas.timeouts + type: metric + metric_name: faas.timeouts + brief: "Number of invocation timeouts" + instrument: counter + unit: "{timeout}" + attributes: + - ref: faas.trigger + + - id: metric.faas.mem_usage + type: metric + metric_name: faas.mem_usage + brief: "Distribution of max memory usage per invocation" + instrument: histogram + unit: "By" + attributes: + - ref: faas.trigger + + - id: metric.faas.cpu_usage + type: metric + metric_name: faas.cpu_usage + brief: "Distribution of CPU usage per invocation" + instrument: histogram + unit: "ms" + attributes: + - ref: faas.trigger + + - id: metric.faas.net_io + type: metric + metric_name: faas.net_io + brief: "Distribution of net I/O usage per invocation" + instrument: histogram + unit: "By" + attributes: + - ref: faas.trigger \ No newline at end of file diff --git a/crates/weaver_semconv/data/faas.yaml b/crates/weaver_semconv/data/faas.yaml new file mode 100644 index 00000000..3ce4fc25 --- /dev/null +++ b/crates/weaver_semconv/data/faas.yaml @@ -0,0 +1,144 @@ +groups: + - id: faas_span + prefix: faas + type: span + brief: > + This semantic convention describes an instance of a function that + runs without provisioning or managing of servers (also known as + serverless functions or Function as a Service (FaaS)) with spans. + attributes: + - ref: faas.trigger + note: | + For the server/consumer span on the incoming side, + `faas.trigger` MUST be set. + + Clients invoking FaaS instances usually cannot set `faas.trigger`, + since they would typically need to look in the payload to determine + the event type. If clients set it, it should be the same as the + trigger that corresponding incoming would have (i.e., this has + nothing to do with the underlying transport used to make the API + call to invoke the lambda, which is often HTTP). + - id: invocation_id + type: string + brief: 'The invocation ID of the current function invocation.' + examples: 'af9d5aa4-a685-4c5f-a22b-444f80b3cc28' + - ref: cloud.resource_id + + - id: faas_span.datasource + prefix: faas.document + type: span + brief: > + Semantic Convention for FaaS triggered as a response to some data + source operation such as a database or filesystem read/write. + attributes: + - id: collection + type: string + requirement_level: required + brief: > + 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. + examples: ['myBucketName', 'myDbName'] + - id: operation + requirement_level: required + type: + allow_custom_values: true + members: + - id: insert + value: 'insert' + brief: 'When a new object is created.' + - id: edit + value: 'edit' + brief: 'When an object is modified.' + - id: delete + value: 'delete' + brief: 'When an object is deleted.' + brief: 'Describes the type of the operation that was performed on the data.' + - id: time + type: string + brief: > + 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). + examples: "2020-01-23T13:47:06Z" + - id: name + type: string + brief: > + 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. + examples: ["myFile.txt", "myTableName"] + + - id: faas_span.http + type: span + brief: > + Semantic Convention for FaaS triggered as a response to some data + source operation such as a database or filesystem read/write. + constraints: + - include: trace.http.server + attributes: [] + + - id: faas_span.pubsub + type: span + brief: > + Semantic Convention for FaaS set to be executed when messages are + sent to a messaging system. + constraints: + - include: messaging + attributes: [] + + - id: faas_span.timer + prefix: faas + type: span + brief: > + Semantic Convention for FaaS scheduled to be executed regularly. + attributes: + - id: time + type: string + brief: > + 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). + examples: "2020-01-23T13:47:06Z" + - id: cron + type: string + brief: > + A string containing the schedule period as + [Cron Expression](https://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm). + examples: "0/5 * * * ? *" + + - id: faas_span.in + span_kind: server + prefix: faas + type: span + brief: > + Contains additional attributes for incoming FaaS spans. + attributes: + - id: coldstart + type: boolean + brief: > + A boolean that is true if the serverless function is executed for the + first time (aka cold-start). + - ref: faas.trigger + requirement_level: required + note: | + For the server/consumer span on the incoming side, + `faas.trigger` MUST be set. + + Clients invoking FaaS instances usually cannot set `faas.trigger`, + since they would typically need to look in the payload to determine + the event type. If clients set it, it should be the same as the + trigger that corresponding incoming would have (i.e., this has + nothing to do with the underlying transport used to make the API + call to invoke the lambda, which is often HTTP). + + - id: faas_span.out + span_kind: client + prefix: faas + type: span + brief: > + Contains additional attributes for outgoing FaaS spans. + attributes: + - ref: faas.invoked_name + - ref: faas.invoked_provider + - ref: faas.invoked_region \ No newline at end of file diff --git a/crates/weaver_semconv/data/http-common.yaml b/crates/weaver_semconv/data/http-common.yaml new file mode 100644 index 00000000..41b30c68 --- /dev/null +++ b/crates/weaver_semconv/data/http-common.yaml @@ -0,0 +1,140 @@ +groups: + - id: attributes.http.common + type: attribute_group + brief: "Describes HTTP attributes." + prefix: http + attributes: + - id: request.method + type: + allow_custom_values: true + members: + - id: connect + value: "CONNECT" + brief: 'CONNECT method.' + - id: delete + value: "DELETE" + brief: 'DELETE method.' + - id: get + value: "GET" + brief: 'GET method.' + - id: head + value: "HEAD" + brief: 'HEAD method.' + - id: options + value: "OPTIONS" + brief: 'OPTIONS method.' + - id: patch + value: "PATCH" + brief: 'PATCH method.' + - id: post + value: "POST" + brief: 'POST method.' + - id: put + value: "PUT" + brief: 'PUT method.' + - id: trace + value: "TRACE" + brief: 'TRACE method.' + - id: other + value: "_OTHER" + brief: 'Any HTTP method that the instrumentation has no prior knowledge of.' + requirement_level: required + brief: 'HTTP request method.' + examples: ["GET", "POST", "HEAD"] + note: | + 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) + and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html). + + 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 + (this list MUST be a full override of the default known method, 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. + - id: response.status_code + type: int + requirement_level: + conditionally_required: If and only if one was received/sent. + brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).' + examples: [200] + - ref: network.protocol.name + examples: ['http', 'spdy'] + requirement_level: + recommended: if not default (`http`). + - ref: network.protocol.version + examples: ['1.0', '1.1', '2', '3'] + + - id: attributes.http.client + prefix: http + type: attribute_group + brief: 'HTTP Client attributes' + attributes: + - ref: server.address + requirement_level: required + brief: > + Host identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. + note: | + Determined by using the first of the following that applies + + - Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) + if it's sent in absolute-form + - Host identifier of the `Host` header + + SHOULD NOT be set if capturing it would require an extra DNS lookup. + - ref: server.port + requirement_level: + conditionally_required: If not default (`80` for `http` scheme, `443` for `https`). + brief: > + Port identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. + note: > + When [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) is absolute URI, `server.port` MUST match + URI port identifier, otherwise it MUST match `Host` header port identifier. + + - id: attributes.http.server + prefix: http + type: attribute_group + brief: 'HTTP Server attributes' + attributes: + - id: route + type: string + requirement_level: + conditionally_required: If and only if it's available + brief: > + The matched route (path template in the format used by the respective server framework). See note below + examples: ['/users/:userID?', '{controller}/{action}/{id?}'] + note: > + 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. + - ref: server.address + brief: > + Name of the local HTTP server that received the request. + note: | + Determined by using the first of the following that applies + + - The [primary server name](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host. MUST only + include host identifier. + - Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) + if it's sent in absolute-form. + - Host identifier of the `Host` header + + SHOULD NOT be set if only IP address is available and capturing name would require a reverse DNS lookup. + + - ref: server.port + brief: > + Port of the local HTTP server that received the request. + note: | + Determined by using the first of the following that applies + + - Port identifier of the [primary server host](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host. + - Port identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) + if it's sent in absolute-form. + - Port identifier of the `Host` header + - ref: url.scheme + requirement_level: required + examples: ["http", "https"] \ No newline at end of file diff --git a/crates/weaver_semconv/data/http-metrics.yaml b/crates/weaver_semconv/data/http-metrics.yaml new file mode 100644 index 00000000..24e5b9d6 --- /dev/null +++ b/crates/weaver_semconv/data/http-metrics.yaml @@ -0,0 +1,119 @@ +groups: + - id: metric_attributes.http.server + type: attribute_group + brief: 'HTTP server attributes' + extends: attributes.http.server + attributes: + - ref: server.address + requirement_level: opt_in + note: | + See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes). + > **Warning** + > Since this attribute is based on HTTP headers, opting in to it may allow an attacker + > to trigger cardinality limits, degrading the usefulness of the metric. + - ref: server.port + requirement_level: opt_in + note: | + See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes). + > **Warning** + > Since this attribute is based on HTTP headers, opting in to it may allow an attacker + > to trigger cardinality limits, degrading the usefulness of the metric. + - id: metric_attributes.http.client + type: attribute_group + brief: 'HTTP client attributes' + extends: attributes.http.client + + - id: metric.http.server.request.duration + type: metric + metric_name: http.server.request.duration + brief: "Duration of HTTP server requests." + instrument: histogram + unit: "s" + extends: metric_attributes.http.server + + - id: metric.http.server.active_requests + type: metric + metric_name: http.server.active_requests + brief: "Number of active HTTP server requests." + instrument: updowncounter + unit: "{request}" + attributes: + - ref: http.request.method + requirement_level: required + - ref: url.scheme + requirement_level: required + examples: ["http", "https"] + - ref: server.address + requirement_level: opt_in + brief: > + Name of the local HTTP server that received the request. + note: | + See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes). + > **Warning** + > Since this attribute is based on HTTP headers, opting in to it may allow an attacker + > to trigger cardinality limits, degrading the usefulness of the metric. + - ref: server.port + requirement_level: opt_in + brief: > + Port of the local HTTP server that received the request. + note: | + See [Setting `server.address` and `server.port` attributes](/docs/http/http-spans.md#setting-serveraddress-and-serverport-attributes). + > **Warning** + > Since this attribute is based on HTTP headers, opting in to it may allow an attacker + > to trigger cardinality limits, degrading the usefulness of the metric. + + - id: metric.http.server.request.body.size + type: metric + metric_name: http.server.request.body.size + brief: "Size of HTTP server request bodies." + instrument: histogram + unit: "By" + note: > + 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. + extends: metric_attributes.http.server + + - id: metric.http.server.response.body.size + type: metric + metric_name: http.server.response.body.size + brief: "Size of HTTP server response bodies." + instrument: histogram + unit: "By" + note: > + 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. + extends: metric_attributes.http.server + + - id: metric.http.client.request.duration + type: metric + metric_name: http.client.request.duration + brief: "Duration of HTTP client requests." + instrument: histogram + unit: "s" + extends: metric_attributes.http.client + + - id: metric.http.client.request.body.size + type: metric + metric_name: http.client.request.body.size + brief: "Size of HTTP client request bodies." + instrument: histogram + unit: "By" + note: > + 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. + extends: metric_attributes.http.client + + - id: metric.http.client.response.body.size + type: metric + metric_name: http.client.response.body.size + brief: "Size of HTTP client response bodies." + instrument: histogram + unit: "By" + note: > + 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. + extends: metric_attributes.http.client \ No newline at end of file diff --git a/crates/weaver_semconv/data/http.yaml b/crates/weaver_semconv/data/http.yaml new file mode 100644 index 00000000..27d346bd --- /dev/null +++ b/crates/weaver_semconv/data/http.yaml @@ -0,0 +1,153 @@ +groups: + - id: trace.http.common + prefix: http + extends: attributes.http.common + type: attribute_group + brief: 'This document defines semantic conventions for HTTP client and server Spans.' + note: > + These conventions can be used for http and https schemes + and various HTTP versions like 1.1, 2 and SPDY. + attributes: + - id: request.method_original + type: string + requirement_level: + conditionally_required: If and only if it's different than `http.request.method`. + brief: Original HTTP method sent by the client in the request line. + examples: ["GeT", "ACL", "foo"] + - id: request.body.size + type: int + brief: > + 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. + examples: 3495 + - id: response.body.size + type: int + brief: > + 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. + examples: 3495 + - ref: http.request.method + sampling_relevant: true + - ref: network.transport + requirement_level: + conditionally_required: If not default (`tcp` for `HTTP/1.1` and `HTTP/2`, `udp` for `HTTP/3`). + - ref: network.type + - ref: user_agent.original + + - id: trace.http.client + prefix: http + type: span + extends: attributes.http.client + span_kind: client + brief: 'Semantic Convention for HTTP Client' + attributes: + - id: resend_count + type: int + brief: > + The ordinal number of request resending attempt (for any reason, including redirects). + note: > + 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). + requirement_level: + recommended: if and only if request was retried. + examples: 3 + - ref: server.address + sampling_relevant: true + requirement_level: required + brief: > + Host identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. + note: | + Determined by using the first of the following that applies + + - Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) + if it's sent in absolute-form + - Host identifier of the `Host` header + + If an HTTP client request is explicitly made to an IP address, e.g. `http://x.x.x.x:8080`, then + `server.address` SHOULD be the IP address `x.x.x.x`. A DNS lookup SHOULD NOT be used. + - ref: server.port + sampling_relevant: true + requirement_level: + conditionally_required: If not default (`80` for `http` scheme, `443` for `https`). + brief: > + Port identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to. + note: > + When [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) is absolute URI, `server.port` MUST match + URI port identifier, otherwise it MUST match `Host` header port identifier. + - ref: server.socket.domain + - ref: server.socket.address + - ref: server.socket.port + - ref: url.full + sampling_relevant: true + requirement_level: required + + + - id: trace.http.server + prefix: http + type: span + extends: attributes.http.server + span_kind: server + brief: 'Semantic Convention for HTTP Server' + attributes: + - ref: server.address + requirement_level: recommended + sampling_relevant: true + brief: > + Name of the local HTTP server that received the request. + note: | + Determined by using the first of the following that applies + + - The [primary server name](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host. MUST only + include host identifier. + - Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) + if it's sent in absolute-form. + - Host identifier of the `Host` header + + SHOULD NOT be set if only IP address is available and capturing name would require a reverse DNS lookup. + - ref: server.port + sampling_relevant: true + requirement_level: + recommended: If not default (`80` for `http` scheme, `443` for `https`). + brief: > + Port of the local HTTP server that received the request. + note: | + Determined by using the first of the following that applies + + - Port identifier of the [primary server host](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host. + - Port identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) + if it's sent in absolute-form. + - Port identifier of the `Host` header + - ref: server.socket.address + requirement_level: opt_in + brief: Local socket address. Useful in case of a multi-IP host. + - ref: server.socket.port + requirement_level: opt_in + brief: Local socket port. Useful in case of a multi-port host. + - ref: client.address + note: > + The IP address of the original client behind all proxies, if + known (e.g. from [Forwarded](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded), + [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For), or a similar header). + Otherwise, the immediate client peer address. + examples: ['83.164.160.102'] + - ref: client.port + brief: > + The port of the original client behind all proxies, if + known (e.g. from [Forwarded](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded) or a similar header). + Otherwise, the immediate client peer port. + - ref: client.socket.address + - ref: client.socket.port + - ref: url.path + requirement_level: required + sampling_relevant: true + - ref: url.query + requirement_level: + conditionally_required: If and only if one was received/sent. + sampling_relevant: true + - ref: url.scheme + sampling_relevant: true + requirement_level: required + examples: ["http", "https"] \ No newline at end of file diff --git a/crates/weaver_semconv/data/jvm-metrics.yaml b/crates/weaver_semconv/data/jvm-metrics.yaml new file mode 100644 index 00000000..9e6d2b33 --- /dev/null +++ b/crates/weaver_semconv/data/jvm-metrics.yaml @@ -0,0 +1,141 @@ +groups: + - id: attributes.jvm.memory + type: attribute_group + brief: "Describes JVM memory metric attributes." + prefix: jvm.memory + attributes: + - id: type + type: + allow_custom_values: false + members: + - id: heap + value: 'heap' + brief: 'Heap memory.' + - id: non_heap + value: 'non_heap' + brief: 'Non-heap memory' + requirement_level: recommended + brief: The type of memory. + examples: ["heap", "non_heap"] + - id: pool.name + type: string + requirement_level: recommended + brief: Name of the memory pool. + examples: ["G1 Old Gen", "G1 Eden space", "G1 Survivor Space"] + note: > + 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()). + + - id: metric.jvm.memory.usage + type: metric + metric_name: jvm.memory.usage + extends: attributes.jvm.memory + brief: "Measure of memory used." + instrument: updowncounter + unit: "By" + + - id: metric.jvm.memory.committed + type: metric + metric_name: jvm.memory.committed + extends: attributes.jvm.memory + brief: "Measure of memory committed." + instrument: updowncounter + unit: "By" + + - id: metric.jvm.memory.limit + type: metric + metric_name: jvm.memory.limit + extends: attributes.jvm.memory + brief: "Measure of max obtainable memory." + instrument: updowncounter + unit: "By" + + - id: metric.jvm.memory.usage_after_last_gc + type: metric + metric_name: jvm.memory.usage_after_last_gc + extends: attributes.jvm.memory + brief: "Measure of memory used, as measured after the most recent garbage collection event on this pool." + instrument: updowncounter + unit: "By" + + - id: metric.jvm.gc.duration + type: metric + metric_name: jvm.gc.duration + brief: "Duration of JVM garbage collection actions." + instrument: histogram + unit: "s" + prefix: jvm.gc + attributes: + - id: name + type: string + requirement_level: recommended + brief: Name of the garbage collector. + examples: ["G1 Young Generation", "G1 Old Generation"] + note: > + 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()). + - id: action + type: string + requirement_level: recommended + brief: Name of the garbage collector action. + examples: ["end of minor GC", "end of major GC"] + note: > + 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()). + + - id: metric.jvm.thread.count + type: metric + metric_name: jvm.thread.count + brief: "Number of executing platform threads." + instrument: updowncounter + unit: "{thread}" + attributes: + - ref: thread.daemon + requirement_level: recommended + + - id: metric.jvm.class.loaded + type: metric + metric_name: jvm.class.loaded + brief: "Number of classes loaded since JVM start." + instrument: counter + unit: "{class}" + + - id: metric.jvm.class.unloaded + type: metric + metric_name: jvm.class.unloaded + brief: "Number of classes unloaded since JVM start." + instrument: counter + unit: "{class}" + + - id: metric.jvm.class.count + type: metric + metric_name: jvm.class.count + brief: "Number of classes currently loaded." + instrument: updowncounter + unit: "{class}" + + - id: metric.jvm.cpu.count + type: metric + metric_name: jvm.cpu.count + brief: "Number of processors available to the Java virtual machine." + instrument: updowncounter + unit: "{cpu}" + + - id: metric.jvm.cpu.time + type: metric + metric_name: jvm.cpu.time + brief: "CPU time used by the process as reported by the JVM." + instrument: counter + unit: "s" + + - id: metric.jvm.cpu.recent_utilization + type: metric + metric_name: jvm.cpu.recent_utilization + brief: "Recent CPU utilization for the process as reported by the JVM." + note: > + 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()). + instrument: gauge + unit: "1" \ No newline at end of file diff --git a/crates/weaver_semconv/data/media.yaml b/crates/weaver_semconv/data/media.yaml new file mode 100644 index 00000000..d6aa0108 --- /dev/null +++ b/crates/weaver_semconv/data/media.yaml @@ -0,0 +1,49 @@ +groups: + - id: attributes.log + prefix: log + type: attribute_group + brief: "Describes Log attributes" + attributes: + - id: iostream + requirement_level: opt_in + brief: > + The stream associated with the log. See below for a list of well-known values. + type: + allow_custom_values: false + members: + - id: stdout + value: 'stdout' + brief: 'Logs from stdout stream' + - id: stderr + value: 'stderr' + brief: 'Events from stderr stream' + - id: attributes.log.file + prefix: log.file + type: attribute_group + brief: > + A file to which log was emitted. + attributes: + - id: name + type: string + requirement_level: recommended + brief: > + The basename of the file. + examples: ["audit.log"] + - id: path + type: string + requirement_level: opt_in + brief: > + The full path to the file. + examples: [ "/var/log/mysql/audit.log" ] + - id: name_resolved + type: string + requirement_level: opt_in + brief: > + The basename of the file, with symlinks resolved. + examples: [ "uuid.log" ] + - id: path_resolved + type: string + requirement_level: opt_in + brief: > + The full path to the file, with symlinks resolved. + examples: [ "/var/lib/docker/uuid.log" ] \ No newline at end of file diff --git a/crates/weaver_semconv/data/messaging.yaml b/crates/weaver_semconv/data/messaging.yaml new file mode 100644 index 00000000..b0f5f12d --- /dev/null +++ b/crates/weaver_semconv/data/messaging.yaml @@ -0,0 +1,319 @@ +groups: + - id: messaging.message + prefix: messaging + type: attribute_group + brief: 'Semantic convention describing per-message attributes populated on messaging spans or links.' + attributes: + - ref: messaging.destination.name + - id: message.id + type: string + brief: 'A value used by the messaging system as an identifier for the message, represented as a string.' + examples: '452a7c7c7c7048c2f887f61572b18fc2' + - id: message.conversation_id + type: string + brief: > + The [conversation ID](#conversations) identifying the conversation to which the message belongs, + represented as a string. Sometimes called "Correlation ID". + examples: 'MyConversationId' + - id: message.payload_size_bytes + type: int + brief: > + The (uncompressed) size of the message payload in bytes. + Also use this attribute if it is unknown whether the compressed or uncompressed payload size is reported. + examples: 2738 + - id: message.payload_compressed_size_bytes + type: int + brief: 'The compressed size of the message payload in bytes.' + examples: 2048 + + - id: messaging.destination + prefix: messaging.destination + type: attribute_group + brief: 'Semantic convention for attributes that describe messaging destination on broker' + note: | + Destination attributes should be set on publish, receive, or other spans + describing messaging operations. + + Destination attributes should be set when the messaging operation handles + single messages. When the operation handles a batch of messages, + the destination attributes should only be applied when the attribute value + applies to all messages in the batch. + In other cases, destination attributes may be set on links. + attributes: + - id: name + type: string + brief: 'The message destination name' + note: | + Destination name SHOULD uniquely identify a specific queue, topic or other entity within the broker. If + the broker does not have such notion, the destination name SHOULD uniquely identify the broker. + examples: ['MyQueue', 'MyTopic'] + - id: template + type: string + brief: Low cardinality representation of the messaging destination name + note: > + 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}'] + - id: temporary + type: boolean + brief: 'A boolean that is true if the message destination is temporary and might not exist anymore after messages are processed.' + - id: anonymous + type: boolean + brief: 'A boolean that is true if the message destination is anonymous (could be unnamed or have auto-generated name).' + + - id: messaging.destination_publish + prefix: messaging.destination_publish + type: attribute_group + brief: > + Semantic convention for attributes that describe the publish messaging destination on broker. + The term Publish Destination refers to the destination the message was originally published to. + These attributes should be used on the consumer side when information about + the publish destination is available and different than the destination message are consumed from. + note: | + Publish destination attributes should be set on publish, receive, + or other spans describing messaging operations. + Destination attributes should be set when the messaging operation handles + single messages. When the operation handles a batch of messages, + the destination attributes should only be applied when the attribute value + applies to all messages in the batch. + In other cases, destination attributes may be set on links. + attributes: + - id: name + type: string + brief: 'The name of the original destination the message was published to' + note: | + The name SHOULD uniquely identify a specific queue, topic, or other entity within the broker. If + the broker does not have such notion, the original destination name SHOULD uniquely identify the broker. + examples: ['MyQueue', 'MyTopic'] + - id: anonymous + type: boolean + brief: 'A boolean that is true if the publish message destination is anonymous (could be unnamed or have auto-generated name).' + + - id: messaging + prefix: messaging + type: span + brief: > + This document defines general attributes used in + messaging systems. + attributes: + - id: system + type: string + requirement_level: required + brief: 'A string identifying the messaging system.' + examples: ['kafka', 'rabbitmq', 'rocketmq', 'activemq', 'AmazonSQS'] + - id: operation + type: + allow_custom_values: true + members: + - id: publish + value: "publish" + - id: receive + value: "receive" + - id: process + value: "process" + requirement_level: required + brief: > + A string identifying the kind of messaging operation as defined in the + [Operation names](#operation-names) section above. + note: If a custom value is used, it MUST be of low cardinality. + - id: batch.message_count + type: int + brief: The number of messages sent, received, or processed in the scope of the batching operation. + requirement_level: + conditionally_required: If the span describes an operation on a batch of messages. + note: > + 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] + - id: client_id + type: string + requirement_level: + recommended: If a client id is available + brief: > + A unique identifier for the client that consumes or produces a message. + examples: ['client-5', 'myhost@8742@s8083jm'] + - ref: messaging.destination.name + requirement_level: + conditionally_required: If span describes operation on a single message or if the value applies to all messages in the batch. + - ref: messaging.destination.template + requirement_level: + conditionally_required: > + If available. Instrumentations MUST NOT use `messaging.destination.name` as template + unless low-cardinality of destination name is guaranteed. + - ref: messaging.destination.temporary + requirement_level: + conditionally_required: If value is `true`. When missing, the value is assumed to be `false`. + - ref: messaging.destination.anonymous + requirement_level: + conditionally_required: If value is `true`. When missing, the value is assumed to be `false`. + - ref: messaging.message.id + requirement_level: + recommended: Only for spans that represent an operation on a single message. + - ref: messaging.message.conversation_id + requirement_level: + recommended: Only if span represents operation on a single message. + - ref: messaging.message.payload_size_bytes + requirement_level: + recommended: Only if span represents operation on a single message. + - ref: messaging.message.payload_compressed_size_bytes + requirement_level: + recommended: Only if span represents operation on a single message. + - ref: server.address + note: > + This should be the IP/hostname of the broker (or other network-level peer) this specific message is sent to/received from. + requirement_level: + conditionally_required: If available. + - ref: server.socket.address + tag: connection-level + - ref: server.socket.port + tag: connection-level + - ref: network.transport + tag: connection-level + - ref: network.type + tag: connection-level + - ref: server.socket.domain + tag: connection-level + requirement_level: + recommended: If different than `server.address` and if `server.socket.address` is set. + - ref: network.protocol.name + examples: ['amqp', 'mqtt'] + - ref: network.protocol.version + + - id: messaging.rabbitmq + prefix: messaging.rabbitmq + type: attribute_group + extends: messaging + brief: > + Attributes for RabbitMQ + attributes: + - id: destination.routing_key + type: string + requirement_level: + conditionally_required: If not empty. + brief: > + RabbitMQ message routing key. + examples: 'myKey' + + - id: messaging.kafka + prefix: messaging.kafka + type: attribute_group + extends: messaging + brief: > + Attributes for Apache Kafka + attributes: + - id: message.key + type: string + brief: > + 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. + note: > + 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' + - id: consumer.group + type: string + brief: > + Name of the Kafka Consumer Group that is handling the message. + Only applies to consumers, not producers. + examples: 'my-group' + - id: destination.partition + type: int + brief: > + Partition the message is sent to. + examples: 2 + - id: message.offset + type: int + brief: > + The offset of a record in the corresponding Kafka partition. + examples: 42 + - id: message.tombstone + type: boolean + requirement_level: + conditionally_required: If value is `true`. When missing, the value is assumed to be `false`. + brief: 'A boolean that is true if the message is a tombstone.' + + - id: messaging.rocketmq + prefix: messaging.rocketmq + type: attribute_group + extends: messaging + brief: > + Attributes for Apache RocketMQ + attributes: + - id: namespace + type: string + requirement_level: required + brief: > + Namespace of RocketMQ resources, resources in different namespaces are individual. + examples: 'myNamespace' + - id: client_group + type: string + requirement_level: required + brief: > + Name of the RocketMQ producer/consumer group that is handling the message. The client type is identified by the SpanKind. + examples: 'myConsumerGroup' + - id: message.delivery_timestamp + type: int + requirement_level: + conditionally_required: If the message type is delay and delay time level is not specified. + brief: > + The timestamp in milliseconds that the delay message is expected to be delivered to consumer. + examples: 1665987217045 + - id: message.delay_time_level + type: int + requirement_level: + conditionally_required: If the message type is delay and delivery timestamp is not specified. + brief: > + The delay time level for delay message, which determines the message delay time. + examples: 3 + - id: message.group + type: string + requirement_level: + conditionally_required: If the message type is FIFO. + brief: > + 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. + examples: 'myMessageGroup' + - id: message.type + type: + allow_custom_values: false + members: + - id: normal + value: 'normal' + brief: "Normal message" + - id: fifo + value: 'fifo' + brief: 'FIFO message' + - id: delay + value: 'delay' + brief: 'Delay message' + - id: transaction + value: 'transaction' + brief: 'Transaction message' + brief: > + Type of message. + - id: message.tag + type: string + brief: > + The secondary classifier of message besides topic. + examples: tagA + - id: message.keys + type: string[] + brief: > + Key(s) of message, another way to mark message besides message id. + examples: ['keyA', 'keyB'] + - id: consumption_model + type: + allow_custom_values: false + members: + - id: clustering + value: 'clustering' + brief: 'Clustering consumption model' + - id: broadcasting + value: 'broadcasting' + brief: 'Broadcasting consumption model' + brief: > + Model of message consumption. This only applies to consumer spans. \ No newline at end of file diff --git a/crates/weaver_semconv/data/network.yaml b/crates/weaver_semconv/data/network.yaml new file mode 100644 index 00000000..ea74399a --- /dev/null +++ b/crates/weaver_semconv/data/network.yaml @@ -0,0 +1,252 @@ +groups: + - id: network-core + prefix: network + type: attribute_group + brief: > + These attributes may be used for any network related operation. + attributes: + - id: transport + type: + allow_custom_values: true + members: + - id: tcp + value: 'tcp' + brief: "TCP" + - id: udp + value: 'udp' + brief: "UDP" + - id: pipe + value: "pipe" + brief: 'Named or anonymous pipe. See note below.' + - id: unix + value: 'unix' + brief: "Unix domain socket" + brief: > + [OSI Transport Layer](https://osi-model.com/transport-layer/) or + [Inter-process Communication method](https://en.wikipedia.org/wiki/Inter-process_communication). + The value SHOULD be normalized to lowercase. + examples: ['tcp', 'udp'] + - id: type + type: + allow_custom_values: true + members: + - id: ipv4 + value: 'ipv4' + brief: "IPv4" + - id: ipv6 + value: 'ipv6' + brief: "IPv6" + brief: > + [OSI Network Layer](https://osi-model.com/network-layer/) or non-OSI equivalent. + The value SHOULD be normalized to lowercase. + examples: ['ipv4', 'ipv6'] + - id: protocol.name + type: string + brief: > + [OSI Application Layer](https://osi-model.com/application-layer/) or non-OSI equivalent. + The value SHOULD be normalized to lowercase. + examples: ['amqp', 'http', 'mqtt'] + - id: protocol.version + type: string + brief: 'Version of the application layer protocol used. See note below.' + examples: '3.1.1' + note: > + `network.protocol.version` refers to the version of the protocol used and might be + different from the protocol client's version. If the HTTP client used has a version + of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + + - id: network-connection-and-carrier + prefix: network + type: attribute_group + brief: > + These attributes may be used for any network related operation. + attributes: + - id: connection.type + type: + allow_custom_values: true + members: + - id: wifi + value: "wifi" + - id: wired + value: "wired" + - id: cell + value: "cell" + - id: unavailable + value: "unavailable" + - id: unknown + value: "unknown" + brief: 'The internet connection type.' + examples: 'wifi' + - id: connection.subtype + type: + allow_custom_values: true + members: + - id: gprs + brief: GPRS + value: "gprs" + - id: edge + brief: EDGE + value: "edge" + - id: umts + brief: UMTS + value: "umts" + - id: cdma + brief: CDMA + value: "cdma" + - id: evdo_0 + brief: EVDO Rel. 0 + value: "evdo_0" + - id: evdo_a + brief: "EVDO Rev. A" + value: "evdo_a" + - id: cdma2000_1xrtt + brief: CDMA2000 1XRTT + value: "cdma2000_1xrtt" + - id: hsdpa + brief: HSDPA + value: "hsdpa" + - id: hsupa + brief: HSUPA + value: "hsupa" + - id: hspa + brief: HSPA + value: "hspa" + - id: iden + brief: IDEN + value: "iden" + - id: evdo_b + brief: "EVDO Rev. B" + value: "evdo_b" + - id: lte + brief: LTE + value: "lte" + - id: ehrpd + brief: EHRPD + value: "ehrpd" + - id: hspap + brief: HSPAP + value: "hspap" + - id: gsm + brief: GSM + value: "gsm" + - id: td_scdma + brief: TD-SCDMA + value: "td_scdma" + - id: iwlan + brief: IWLAN + value: "iwlan" + - id: nr + brief: "5G NR (New Radio)" + value: "nr" + - id: nrnsa + brief: "5G NRNSA (New Radio Non-Standalone)" + value: "nrnsa" + - id: lte_ca + brief: LTE CA + value: "lte_ca" + brief: '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.' + examples: 'LTE' + - id: carrier.name + type: string + brief: "The name of the mobile carrier." + examples: "sprint" + - id: carrier.mcc + type: string + brief: "The mobile carrier country code." + examples: "310" + - id: carrier.mnc + type: string + brief: "The mobile carrier network code." + examples: "001" + - id: carrier.icc + type: string + brief: "The ISO 3166-1 alpha-2 2-character country code associated with the mobile carrier network." + examples: "DE" + - id: peer + prefix: peer + type: span + brief: "Operations that access some remote service." + attributes: + - id: service + type: string + brief: > + 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. + examples: "AuthTokenCache" + - id: identity + prefix: enduser + type: span + brief: > + These attributes may be used for any operation with an authenticated and/or authorized enduser. + attributes: + - id: id + type: string + brief: > + Username or client_id extracted from the access token or + [Authorization](https://tools.ietf.org/html/rfc7235#section-4.2) + header in the inbound request from outside the system. + examples: 'username' + - id: role + type: string + brief: 'Actual/assumed role the client is making the request under extracted from token or application security context.' + examples: 'admin' + - id: scope + type: string + brief: > + Scopes or granted authorities the client currently possesses extracted from token + or application security context. The value would come from the scope associated + with an [OAuth 2.0 Access Token](https://tools.ietf.org/html/rfc6749#section-3.3) + or an attribute value in a [SAML 2.0 Assertion](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html). + examples: 'read:message, write:files' + - id: thread + prefix: thread + type: span + brief: > + These attributes may be used for any operation to store information about a thread that started a span. + attributes: + - id: id + type: int + brief: > + Current "managed" thread ID (as opposed to OS thread ID). + examples: 42 + - id: name + type: string + brief: > + Current thread name. + examples: main + - id: daemon + brief: "Whether the thread is daemon or not." + type: boolean + - id: code + prefix: code + type: span + brief: > + These attributes allow to report this unit of code and therefore to provide more context about the span. + attributes: + - id: function + type: string + brief: > + The method or function name, or equivalent (usually rightmost part of the code unit's name). + examples: serveRequest + - id: namespace + type: string + brief: > + The "namespace" within which `code.function` is defined. Usually the qualified class or module name, + such that `code.namespace` + some separator + `code.function` form a unique identifier for the code unit. + examples: com.example.MyHttpService + - id: filepath + type: string + brief: > + The source code file name that identifies the code unit as uniquely as possible (preferably an absolute file path). + examples: /usr/local/MyApplication/content_root/app/index.php + - id: lineno + type: int + brief: > + The line number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + examples: 42 + - id: column + type: int + brief: > + The column number in `code.filepath` best representing the operation. It SHOULD point within the code unit named in `code.function`. + examples: 16 \ No newline at end of file diff --git a/crates/weaver_semconv/data/rpc-metrics.yaml b/crates/weaver_semconv/data/rpc-metrics.yaml new file mode 100644 index 00000000..9e888d7a --- /dev/null +++ b/crates/weaver_semconv/data/rpc-metrics.yaml @@ -0,0 +1,115 @@ +groups: + # TODO: Should we list the attributes on each metric + # OR leave a single table like it is today? Since all attributes are applied + # to all metrics, the repetition of them bloats the page + - id: attributes.metrics.rpc + type: attribute_group + brief: "Describes RPC metric attributes." + attributes: + - ref: rpc.system + - ref: rpc.service + - ref: rpc.method + - ref: network.transport + - ref: network.type + - ref: server.address + - ref: server.port + - ref: server.socket.address + - ref: server.socket.port + + # RPC Server metrics + - id: metric.rpc.server.duration + type: metric + metric_name: rpc.server.duration + brief: > + Measures the duration of inbound RPC. + **Streaming**: N/A. + instrument: histogram + unit: "ms" + note: | + While streaming RPCs may record this metric as start-of-batch + to end-of-batch, it's hard to interpret in practice. + + - id: metric.rpc.server.request.size + type: metric + metric_name: rpc.server.request.size + brief: > + Measures the size of RPC request messages (uncompressed). + **Streaming**: Recorded per message in a streaming batch + instrument: histogram + unit: "By" + + - id: metric.rpc.server.response.size + type: metric + metric_name: rpc.server.response.size + brief: > + Measures the size of RPC response messages (uncompressed). + **Streaming**: Recorded per response in a streaming batch + instrument: histogram + unit: "By" + + - id: metric.rpc.server.requests_per_rpc + type: metric + metric_name: rpc.server.requests_per_rpc + brief: > + Measures the number of messages received per RPC. Should be 1 for all non-streaming RPCs. + **Streaming**: This metric is required for server and client streaming RPCs + instrument: histogram + unit: "{count}" + + - id: metric.rpc.server.responses_per_rpc + type: metric + metric_name: rpc.server.responses_per_rpc + brief: > + Measures the number of messages sent per RPC. Should be 1 for all non-streaming RPCs. + **Streaming**: This metric is required for server and client streaming RPCs + instrument: histogram + unit: "{count}" + + # RPC Client metrics + - id: metric.rpc.client.duration + type: metric + metric_name: rpc.client.duration + brief: > + Measures the duration of outbound RPC + **Streaming**: N/A. + instrument: histogram + unit: "ms" + note: | + While streaming RPCs may record this metric as start-of-batch + to end-of-batch, it's hard to interpret in practice. + + - id: metric.rpc.client.request.size + type: metric + metric_name: rpc.client.request.size + brief: > + Measures the size of RPC request messages (uncompressed). + **Streaming**: Recorded per message in a streaming batch + instrument: histogram + unit: "By" + + - id: metric.rpc.client.response.size + type: metric + metric_name: rpc.client.response.size + brief: > + Measures the size of RPC response messages (uncompressed). + **Streaming**: Recorded per response in a streaming batch + instrument: histogram + unit: "By" + + - id: metric.rpc.client.requests_per_rpc + type: metric + metric_name: rpc.client.requests_per_rpc + brief: > + Measures the number of messages received per RPC. Should be 1 for all non-streaming RPCs. + **Streaming**: This metric is required for server and client streaming RPCs + instrument: histogram + unit: "{count}" + + - id: metric.rpc.client.responses_per_rpc + type: metric + metric_name: rpc.client.responses_per_rpc + brief: > + Measures the number of messages sent per RPC. Should be 1 for all non-streaming RPCs. + **Streaming**: This metric is required for server and client streaming RPCs + instrument: histogram + unit: "{count}" \ No newline at end of file diff --git a/crates/weaver_semconv/data/rpc.yaml b/crates/weaver_semconv/data/rpc.yaml new file mode 100644 index 00000000..9acc7548 --- /dev/null +++ b/crates/weaver_semconv/data/rpc.yaml @@ -0,0 +1,263 @@ +groups: + - id: rpc + prefix: rpc + type: span + brief: 'This document defines semantic conventions for remote procedure calls.' + events: [rpc.message] + attributes: + - id: system + requirement_level: required + brief: 'A string identifying the remoting system. See below for a list of well-known identifiers.' + type: + allow_custom_values: true + members: + - id: grpc + value: 'grpc' + brief: 'gRPC' + - id: java_rmi + value: 'java_rmi' + brief: 'Java RMI' + - id: dotnet_wcf + value: 'dotnet_wcf' + brief: '.NET WCF' + - id: apache_dubbo + value: 'apache_dubbo' + brief: 'Apache Dubbo' + - id: connect_rpc + value: 'connect_rpc' + brief: 'Connect RPC' + - id: service + type: string + requirement_level: recommended + brief: 'The full (logical) name of the service being called, including its package name, if applicable.' + note: > + This is the logical name of the service from the RPC interface perspective, + which can be different from the name of any implementing class. + The `code.namespace` attribute may be used to store the latter + (despite the attribute name, it may include a class name; + e.g., class with method actually executing the call on the server side, + RPC client stub class on the client side). + examples: "myservice.EchoService" + - id: method + type: string + requirement_level: recommended + brief: 'The name of the (logical) method being called, must be equal to the $method part in the span name.' + note: > + This is the logical name of the method from the RPC interface perspective, + which can be different from the name of any implementing method/function. + The `code.function` attribute may be used to store the latter + (e.g., method actually executing the call on the server side, + RPC client stub method on the client side). + examples: "exampleMethod" + - ref: server.socket.address + - ref: server.socket.port + requirement_level: + recommended: If different than `server.port` and if `server.socket.address` is set. + - ref: network.transport + - ref: network.type + - ref: server.address + requirement_level: required + brief: > + RPC server [host name](https://grpc.github.io/grpc/core/md_doc_naming.html). + note: > + May contain server IP address, DNS name, or local socket name. When host component is an IP address, + instrumentations SHOULD NOT do a reverse proxy lookup to obtain DNS name and SHOULD set + `server.address` to the IP address provided in the host component. + - ref: server.port + requirement_level: + conditionally_required: See below + constraints: + - any_of: + - server.socket.address + - server.address + + - id: rpc.client + type: span + brief: 'This document defines semantic conventions for remote procedure call client spans.' + extends: rpc + attributes: + - ref: server.socket.domain + requirement_level: + recommended: If different than `server.address` and if `server.socket.address` is set. + + - id: rpc.server + prefix: rpc + type: span + extends: rpc + span_kind: server + brief: 'Semantic Convention for RPC server spans' + attributes: + - ref: client.address + - ref: client.port + - ref: client.socket.address + - ref: client.socket.port + - ref: network.transport + - ref: network.type + + - id: rpc.grpc + prefix: rpc.grpc + type: span + extends: rpc + brief: 'Tech-specific attributes for gRPC.' + attributes: + - id: status_code + type: + members: + - id: ok + brief: OK + value: 0 + - id: cancelled + brief: CANCELLED + value: 1 + - id: unknown + brief: UNKNOWN + value: 2 + - id: invalid_argument + brief: INVALID_ARGUMENT + value: 3 + - id: deadline_exceeded + brief: DEADLINE_EXCEEDED + value: 4 + - id: not_found + brief: NOT_FOUND + value: 5 + - id: already_exists + brief: ALREADY_EXISTS + value: 6 + - id: permission_denied + brief: PERMISSION_DENIED + value: 7 + - id: resource_exhausted + brief: RESOURCE_EXHAUSTED + value: 8 + - id: failed_precondition + brief: FAILED_PRECONDITION + value: 9 + - id: aborted + brief: ABORTED + value: 10 + - id: out_of_range + brief: OUT_OF_RANGE + value: 11 + - id: unimplemented + brief: UNIMPLEMENTED + value: 12 + - id: internal + brief: INTERNAL + value: 13 + - id: unavailable + brief: UNAVAILABLE + value: 14 + - id: data_loss + brief: DATA_LOSS + value: 15 + - id: unauthenticated + brief: UNAUTHENTICATED + value: 16 + requirement_level: required + brief: "The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the gRPC request." + + - id: rpc.jsonrpc + prefix: rpc.jsonrpc + type: span + extends: rpc + brief: 'Tech-specific attributes for [JSON RPC](https://www.jsonrpc.org/).' + attributes: + - id: version + type: string + requirement_level: + conditionally_required: If other than the default version (`1.0`) + brief: "Protocol version as in `jsonrpc` property of request/response. Since JSON-RPC 1.0 does not specify this, the value can be omitted." + examples: ['2.0', '1.0'] + - id: request_id + type: string + brief: > + `id` property of request or response. + Since protocol allows id to be int, string, `null` or missing (for notifications), + value is expected to be cast to string for simplicity. + Use empty string in case of `null` value. Omit entirely if this is a notification. + examples: ['10', 'request-7', ''] + - id: error_code + type: int + requirement_level: + conditionally_required: If response is not successful. + brief: "`error.code` property of response if it is an error response." + examples: [-32700, 100] + - id: error_message + type: string + brief: "`error.message` property of response if it is an error response." + examples: ['Parse error', 'User already exists'] + - ref: rpc.method + requirement_level: required + note: > + This is always required for jsonrpc. See the note in the general + RPC conventions for more information. + + - id: rpc.message + prefix: "message" # TODO: Change the prefix to rpc.message? + type: event + brief: "RPC received/sent message." + attributes: + - id: type + type: + members: + - id: sent + value: "SENT" + - id: received + value: "RECEIVED" + brief: "Whether this is a received or sent message." + - id: id + type: int + brief: "MUST be calculated as two different counters starting from `1` one for sent messages and one for received message." + note: "This way we guarantee that the values will be consistent between different implementations." + - id: compressed_size + type: int + brief: "Compressed size of the message in bytes." + - id: uncompressed_size + type: int + brief: "Uncompressed size of the message in bytes." + + - id: rpc.connect_rpc + prefix: rpc.connect_rpc + type: span + extends: rpc + brief: 'Tech-specific attributes for Connect RPC.' + attributes: + - id: error_code + type: + members: + - id: cancelled + value: cancelled + - id: unknown + value: unknown + - id: invalid_argument + value: invalid_argument + - id: deadline_exceeded + value: deadline_exceeded + - id: not_found + value: not_found + - id: already_exists + value: already_exists + - id: permission_denied + value: permission_denied + - id: resource_exhausted + value: resource_exhausted + - id: failed_precondition + value: failed_precondition + - id: aborted + value: aborted + - id: out_of_range + value: out_of_range + - id: unimplemented + value: unimplemented + - id: internal + value: internal + - id: unavailable + value: unavailable + - id: data_loss + value: data_loss + - id: unauthenticated + value: unauthenticated + requirement_level: + conditionally_required: If response is not successful and if error code available. + brief: "The [error codes](https://connect.build/docs/protocol/#error-codes) of the Connect request. Error codes are always string values." \ No newline at end of file diff --git a/crates/weaver_semconv/data/server.yaml b/crates/weaver_semconv/data/server.yaml new file mode 100644 index 00000000..85690b2d --- /dev/null +++ b/crates/weaver_semconv/data/server.yaml @@ -0,0 +1,53 @@ +groups: + - id: server + prefix: server + type: attribute_group + brief: > + These attributes may be used to describe the server in a connection-based network interaction + where there is one side that initiates the connection (the client is the side that initiates the connection). + This covers all TCP network interactions since TCP is connection-based and one side initiates the + connection (an exception is made for peer-to-peer communication over TCP where the "user-facing" surface of the + protocol / API does not expose a clear notion of client and server). + This also covers UDP network interactions where one side initiates the interaction, e.g. QUIC (HTTP/3) and DNS. + attributes: + - id: address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + examples: ['example.com'] + - id: port + type: int + brief: Server port number + note: > + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent + the server port behind any intermediaries (e.g. proxies) if it's available. + examples: [80, 8080, 443] + - id: socket.domain + type: string + brief: Immediate server peer's domain name if available without reverse DNS lookup + examples: ['proxy.example.com'] + note: Typically observed from the client side, and represents a proxy or other intermediary domain name. + requirement_level: + recommended: If different than `server.address`. + - id: socket.address + type: string + brief: Server address of the socket connection - IP address or Unix domain socket name. + note: > + When observed from the client side, this SHOULD represent the immediate server peer address. + + When observed from the server side, this SHOULD represent the physical server address. + examples: ['10.5.3.2'] + requirement_level: + recommended: If different than `server.address`. + - id: socket.port + type: int + brief: Server port number of the socket connection. + note: > + When observed from the client side, this SHOULD represent the immediate server peer port. + + When observed from the server side, this SHOULD represent the physical server port. + examples: [16456] + requirement_level: + recommended: If different than `server.port`. \ No newline at end of file diff --git a/crates/weaver_semconv/data/source.yaml b/crates/weaver_semconv/data/source.yaml new file mode 100644 index 00000000..76feccfa --- /dev/null +++ b/crates/weaver_semconv/data/source.yaml @@ -0,0 +1,24 @@ +groups: + - id: source + prefix: source + type: attribute_group + brief: These attributes may be used to describe the sender of a network exchange/packet. These should be used + when there is no client/server relationship between the two sides, or when that relationship is unknown. + This covers low-level network interactions (e.g. packet tracing) where you don't know if + there was a connection or which side initiated it. + This also covers unidirectional UDP flows and peer-to-peer communication where the + "user-facing" surface of the protocol / API does not expose a clear notion of client and server. + attributes: + - id: domain + type: string + brief: The domain name of the source system. + examples: ['foo.example.com'] + note: This value may be a host name, a fully qualified domain name, or another host naming format. + - id: address + type: string + brief: 'Source address, for example IP address or Unix socket name.' + examples: ['10.5.3.2'] + - id: port + type: int + brief: 'Source port number' + examples: [3389, 2888] \ No newline at end of file diff --git a/crates/weaver_semconv/data/tls.yaml b/crates/weaver_semconv/data/tls.yaml new file mode 100644 index 00000000..8f9c0ab5 --- /dev/null +++ b/crates/weaver_semconv/data/tls.yaml @@ -0,0 +1,165 @@ +groups: + - id: registry.tls + prefix: tls + type: attribute_group + brief: "This document defines semantic convention attributes in the TLS namespace." + attributes: + - id: cipher + brief: > + String indicating the [cipher](https://datatracker.ietf.org/doc/html/rfc5246#appendix-A.5) used during the current connection. + type: string + note: > + 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", + ] + - id: client.certificate + type: string + brief: > + 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. + examples: ["MII..."] + - id: client.certificate_chain + type: string[] + brief: > + 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. + examples: ["MII...", "MI..."] + - id: client.hash.md5 + type: string + brief: > + 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. + examples: ["0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC"] + - id: client.hash.sha1 + type: string + brief: > + 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. + examples: ["9E393D93138888D288266C2D915214D1D1CCEB2A"] + - id: client.hash.sha256 + type: string + brief: > + 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. + examples: + ["0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0"] + - id: client.issuer + type: string + brief: "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." + examples: + ["CN=Example Root CA, OU=Infrastructure Team, DC=example, DC=com"] + - id: client.ja3 + type: string + brief: "A hash that identifies clients based on how they perform an SSL/TLS handshake." + examples: ["d4e5b18d6b55c71272893221c96ba240"] + - id: client.not_after + type: string + brief: "Date/Time indicating when client certificate is no longer considered valid." + examples: ["2021-01-01T00:00:00.000Z"] + - id: client.not_before + type: string + brief: "Date/Time indicating when client certificate is first considered valid." + examples: ["1970-01-01T00:00:00.000Z"] + - id: client.server_name + type: string + brief: "Also called an SNI, this tells the server which hostname to which the client is attempting to connect to." + examples: ["opentelemetry.io"] + - id: client.subject + type: string + brief: "Distinguished name of subject of the x.509 certificate presented by the client." + examples: ["CN=myclient, OU=Documentation Team, DC=example, DC=com"] + - id: client.supported_ciphers + type: string[] + brief: Array of ciphers offered by the client during the client hello. + examples: + [ + '"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "..."', + ] + - id: curve + brief: "String indicating the curve used for the given cipher, when applicable" + type: string + examples: ["secp256r1"] + - id: established + brief: "Boolean flag indicating if the TLS negotiation was successful and transitioned to an encrypted tunnel." + type: boolean + examples: [true] + - id: next_protocol + brief: > + 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. + type: string + examples: ["http/1.1"] + - id: protocol.name + brief: > + Normalized lowercase protocol name parsed from original string of the negotiated [SSL/TLS protocol version](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html#RETURN-VALUES) + type: + allow_custom_values: true + members: + - id: ssl + value: ssl + - id: tls + value: tls + - id: protocol.version + brief: > + Numeric part of the version parsed from the original string of the negotiated [SSL/TLS protocol version](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html#RETURN-VALUES) + type: string + examples: ["1.2", "3"] + - id: resumed + brief: "Boolean flag indicating if this TLS connection was resumed from an existing TLS negotiation." + type: boolean + examples: [true] + - id: server.certificate + type: string + brief: > + 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. + examples: ["MII..."] + - id: server.certificate_chain + type: string[] + brief: > + 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. + examples: ["MII...", "MI..."] + - id: server.hash.md5 + type: string + brief: > + 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. + examples: ["0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC"] + - id: server.hash.sha1 + type: string + brief: > + 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. + examples: ["9E393D93138888D288266C2D915214D1D1CCEB2A"] + - id: server.hash.sha256 + type: string + brief: > + 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. + examples: + ["0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0"] + - id: server.issuer + type: string + brief: "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." + examples: + ["CN=Example Root CA, OU=Infrastructure Team, DC=example, DC=com"] + - id: server.ja3s + type: string + brief: "A hash that identifies servers based on how they perform an SSL/TLS handshake." + examples: ["d4e5b18d6b55c71272893221c96ba240"] + - id: server.not_after + type: string + brief: "Date/Time indicating when server certificate is no longer considered valid." + examples: ["2021-01-01T00:00:00.000Z"] + - id: server.not_before + type: string + brief: "Date/Time indicating when server certificate is first considered valid." + examples: ["1970-01-01T00:00:00.000Z"] + - id: server.subject + type: string + brief: "Distinguished name of subject of the x.509 certificate presented by the server." + examples: ["CN=myserver, OU=Documentation Team, DC=example, DC=com"] \ No newline at end of file diff --git a/crates/weaver_semconv/data/trace-exception.yaml b/crates/weaver_semconv/data/trace-exception.yaml new file mode 100644 index 00000000..08e6420c --- /dev/null +++ b/crates/weaver_semconv/data/trace-exception.yaml @@ -0,0 +1,38 @@ +groups: + - id: trace-exception + prefix: exception + type: event + brief: > + This document defines the attributes used to + report a single exception associated with a span. + attributes: + - ref: exception.type + - ref: exception.message + - ref: exception.stacktrace + - id: escaped + type: boolean + brief: > + SHOULD be set to true if the exception event is recorded at a point where + it is known that the exception is escaping the scope of the span. + note: |- + An exception is considered to have escaped (or left) the scope of a span, + if that span is ended while the exception is still logically "in flight". + This may be actually "in flight" in some languages (e.g. if the exception + is passed to a Context manager's `__exit__` method in Python) but will + usually be caught at the point of recording the exception in most languages. + + It is usually not possible to determine at the point where an exception is thrown + whether it will escape the scope of a span. + However, it is trivial to know that an exception + will escape, if one checks for an active exception just before ending the span, + as done in the [example above](#recording-an-exception). + + It follows that an exception may still escape the scope of the span + even if the `exception.escaped` attribute was not set or set to false, + since the event might have been recorded at a time where it was not + clear whether the exception will escape. + + constraints: + - any_of: + - "exception.type" + - "exception.message" \ No newline at end of file diff --git a/crates/weaver_semconv/data/url.yaml b/crates/weaver_semconv/data/url.yaml new file mode 100644 index 00000000..815bf43d --- /dev/null +++ b/crates/weaver_semconv/data/url.yaml @@ -0,0 +1,39 @@ +groups: + - id: url + brief: Attributes describing URL. + type: attribute_group + prefix: url + attributes: + - id: scheme + type: string + brief: 'The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol.' + examples: ["https", "ftp", "telnet"] + - id: full + type: string + brief: Absolute URL describing a network resource according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) + note: > + 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) + and SHOULD NOT be validated or modified except for sanitizing purposes. + examples: ['https://www.foo.bar/search?q=OpenTelemetry#SemConv', '//localhost'] + tag: sensitive-information + - id: path + type: string + brief: 'The [URI path](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) component' + examples: ['/search'] + note: When missing, the value is assumed to be `/` + - id: query + type: string + brief: 'The [URI query](https://www.rfc-editor.org/rfc/rfc3986#section-3.4) component' + examples: ["q=OpenTelemetry"] + note: Sensitive content provided in query string SHOULD be scrubbed when instrumentations can identify it. + tag: sensitive-information + - id: fragment + type: string + brief: 'The [URI fragment](https://www.rfc-editor.org/rfc/rfc3986#section-3.5) component' + examples: ["SemConv"] \ No newline at end of file diff --git a/crates/weaver_semconv/data/user-agent.yaml b/crates/weaver_semconv/data/user-agent.yaml new file mode 100644 index 00000000..33fbafd2 --- /dev/null +++ b/crates/weaver_semconv/data/user-agent.yaml @@ -0,0 +1,10 @@ +groups: + - id: attributes.user_agent + type: attribute_group + brief: "Describes user-agent attributes." + prefix: user_agent + attributes: + - id: original + type: string + brief: 'Value of the [HTTP User-Agent](https://www.rfc-editor.org/rfc/rfc9110.html#field.user-agent) header sent by the client.' + examples: ['CERN-LineMode/2.15 libwww/2.17b3'] \ No newline at end of file diff --git a/crates/weaver_semconv/data/vm-metrics-experimental.yaml b/crates/weaver_semconv/data/vm-metrics-experimental.yaml new file mode 100644 index 00000000..8b2bec43 --- /dev/null +++ b/crates/weaver_semconv/data/vm-metrics-experimental.yaml @@ -0,0 +1,70 @@ +groups: + - id: metric.jvm.memory.init + type: metric + metric_name: jvm.memory.init + extends: attributes.jvm.memory + brief: "Measure of initial memory requested." + instrument: updowncounter + unit: "By" + + - id: metric.jvm.system.cpu.utilization + type: metric + metric_name: jvm.system.cpu.utilization + brief: "Recent CPU utilization for the whole system as reported by the JVM." + note: > + 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()). + instrument: gauge + unit: "1" + + - id: metric.jvm.system.cpu.load_1m + type: metric + metric_name: jvm.system.cpu.load_1m + brief: "Average CPU load of the whole system for the last minute as reported by the JVM." + note: > + 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()). + instrument: gauge + unit: "{run_queue_item}" + + - id: attributes.jvm.buffer + type: attribute_group + brief: "Describes JVM buffer metric attributes." + prefix: jvm.buffer + attributes: + - id: pool.name + type: string + requirement_level: recommended + brief: Name of the buffer pool. + examples: [ "mapped", "direct" ] + note: > + 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()). + + - id: metric.jvm.buffer.memory.usage + type: metric + metric_name: jvm.buffer.memory.usage + extends: attributes.jvm.buffer + brief: "Measure of memory used by buffers." + instrument: updowncounter + unit: "By" + + - id: metric.jvm.buffer.memory.limit + type: metric + metric_name: jvm.buffer.memory.limit + extends: attributes.jvm.buffer + brief: "Measure of total memory capacity of buffers." + instrument: updowncounter + unit: "By" + + - id: metric.jvm.buffer.count + type: metric + metric_name: jvm.buffer.count + extends: attributes.jvm.buffer + brief: "Number of buffers in the pool." + instrument: updowncounter + unit: "{buffer}" \ No newline at end of file diff --git a/crates/weaver_semconv/src/attribute.rs b/crates/weaver_semconv/src/attribute.rs new file mode 100644 index 00000000..553a9e1b --- /dev/null +++ b/crates/weaver_semconv/src/attribute.rs @@ -0,0 +1,437 @@ +// SPDX-License-Identifier: Apache-2.0 + +#![allow(rustdoc::invalid_html_tags)] + +//! Attribute specification. + +use ordered_float::OrderedFloat; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +use crate::stability::StabilitySpec; + +/// An attribute specification. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +#[serde(untagged)] +#[serde(rename_all = "snake_case")] +pub enum AttributeSpec { + /// Reference to another attribute. + /// + /// ref MUST have an id of an existing attribute. + Ref { + /// Reference an existing attribute. + r#ref: String, + /// A brief description of the attribute. + #[serde(skip_serializing_if = "Option::is_none")] + brief: Option, + /// Sequence of example values for the attribute or single example + /// value. They are required only for string and string array + /// attributes. Example values must be of the same type of the + /// attribute. If only a single example is provided, it can directly + /// be reported without encapsulating it into a sequence/dictionary. + #[serde(skip_serializing_if = "Option::is_none")] + examples: Option, + /// Associates a tag ("sub-group") to the attribute. It carries no + /// particular semantic meaning but can be used e.g. for filtering + /// in the markdown generator. + #[serde(skip_serializing_if = "Option::is_none")] + tag: Option, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + #[serde(skip_serializing_if = "Option::is_none")] + requirement_level: Option, + /// Specifies if the attribute is (especially) relevant for sampling + /// and thus should be set at span start. It defaults to false. + /// Note: this field is experimental. + #[serde(skip_serializing_if = "Option::is_none")] + sampling_relevant: Option, + /// A more elaborate description of the attribute. + /// It defaults to an empty string. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + note: Option, + /// Specifies the stability of the attribute. + /// Note that, if stability is missing but deprecated is present, it will + /// automatically set the stability to deprecated. If deprecated is + /// present and stability differs from deprecated, this will result in an + /// error. + #[serde(skip_serializing_if = "Option::is_none")] + stability: Option, + /// Specifies if the attribute is deprecated. The string + /// provided as MUST specify why it's deprecated and/or what + /// to use instead. See also stability. + #[serde(skip_serializing_if = "Option::is_none")] + deprecated: Option, + }, + /// Attribute definition. + Id { + /// String that uniquely identifies the attribute. + id: String, + /// Either a string literal denoting the type as a primitive or an + /// array type, a template type or an enum definition. + r#type: AttributeTypeSpec, + /// A brief description of the attribute. + brief: String, + /// Sequence of example values for the attribute or single example + /// value. They are required only for string and string array + /// attributes. Example values must be of the same type of the + /// attribute. If only a single example is provided, it can directly + /// be reported without encapsulating it into a sequence/dictionary. + #[serde(skip_serializing_if = "Option::is_none")] + examples: Option, + /// Associates a tag ("sub-group") to the attribute. It carries no + /// particular semantic meaning but can be used e.g. for filtering + /// in the markdown generator. + #[serde(skip_serializing_if = "Option::is_none")] + tag: Option, + /// Specifies if the attribute is mandatory. Can be "required", + /// "conditionally_required", "recommended" or "opt_in". When omitted, + /// the attribute is "recommended". When set to + /// "conditionally_required", the string provided as MUST + /// specify the conditions under which the attribute is required. + #[serde(default)] + requirement_level: RequirementLevelSpec, + /// Specifies if the attribute is (especially) relevant for sampling + /// and thus should be set at span start. It defaults to false. + /// Note: this field is experimental. + #[serde(skip_serializing_if = "Option::is_none")] + sampling_relevant: Option, + /// A more elaborate description of the attribute. + /// It defaults to an empty string. + #[serde(default)] + note: String, + /// Specifies the stability of the attribute. + /// Note that, if stability is missing but deprecated is present, it will + /// automatically set the stability to deprecated. If deprecated is + /// present and stability differs from deprecated, this will result in an + /// error. + #[serde(skip_serializing_if = "Option::is_none")] + stability: Option, + /// Specifies if the attribute is deprecated. The string + /// provided as MUST specify why it's deprecated and/or what + /// to use instead. See also stability. + #[serde(skip_serializing_if = "Option::is_none")] + deprecated: Option, + }, +} + +impl AttributeSpec { + /// Returns true if the attribute is required. + pub fn is_required(&self) -> bool { + matches!( + self, + AttributeSpec::Ref { + requirement_level: Some(RequirementLevelSpec::Basic( + BasicRequirementLevelSpec::Required + )), + .. + } | AttributeSpec::Id { + requirement_level: RequirementLevelSpec::Basic(BasicRequirementLevelSpec::Required), + .. + } + ) + } + + /// Returns the id of the attribute. + pub fn id(&self) -> String { + match self { + AttributeSpec::Ref { r#ref, .. } => r#ref.clone(), + AttributeSpec::Id { id, .. } => id.clone(), + } + } + + /// Returns the brief of the attribute. + pub fn brief(&self) -> String { + match self { + AttributeSpec::Ref { brief, .. } => brief.clone().unwrap_or_default(), + AttributeSpec::Id { brief, .. } => brief.clone(), + } + } + + /// Returns the note of the attribute. + pub fn note(&self) -> String { + match self { + AttributeSpec::Ref { note, .. } => note.clone().unwrap_or_default(), + AttributeSpec::Id { note, .. } => note.clone(), + } + } + + /// Returns the tag of the attribute (if any). + pub fn tag(&self) -> Option { + match self { + AttributeSpec::Ref { tag, .. } => tag.clone(), + AttributeSpec::Id { tag, .. } => tag.clone(), + } + } +} + +/// The different types of attributes (specification). +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +pub enum AttributeTypeSpec { + /// Primitive or array type. + PrimitiveOrArray(PrimitiveOrArrayTypeSpec), + /// A template type. + Template(TemplateTypeSpec), + /// An enum definition type. + Enum { + /// Set to false to not accept values other than the specified members. + /// It defaults to true. + #[serde(default = "default_as_true")] + allow_custom_values: bool, + /// List of enum entries. + members: Vec, + }, +} + +/// Implements a human readable display for AttributeType. +impl Display for AttributeTypeSpec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AttributeTypeSpec::PrimitiveOrArray(t) => write!(f, "{}", t), + AttributeTypeSpec::Template(t) => write!(f, "{}", t), + AttributeTypeSpec::Enum { members, .. } => { + let entries = members + .iter() + .map(|m| m.id.clone()) + .collect::>() + .join(", "); + write!(f, "enum {{{}}}", entries) + } + } + } +} + +/// Specifies the default value for allow_custom_values. +fn default_as_true() -> bool { + true +} + +/// Primitive or array types. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum PrimitiveOrArrayTypeSpec { + /// A boolean attribute. + Boolean, + /// A integer attribute (signed 64 bit integer). + Int, + /// A double attribute (double precision floating point (IEEE 754-1985)). + Double, + /// A string attribute. + String, + /// An array of strings attribute. + #[serde(rename = "string[]")] + Strings, + /// An array of integer attribute. + #[serde(rename = "int[]")] + Ints, + /// An array of double attribute. + #[serde(rename = "double[]")] + Doubles, + /// An array of boolean attribute. + #[serde(rename = "boolean[]")] + Booleans, +} + +/// Implements a human readable display for PrimitiveOrArrayType. +impl Display for PrimitiveOrArrayTypeSpec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PrimitiveOrArrayTypeSpec::Boolean => write!(f, "boolean"), + PrimitiveOrArrayTypeSpec::Int => write!(f, "int"), + PrimitiveOrArrayTypeSpec::Double => write!(f, "double"), + PrimitiveOrArrayTypeSpec::String => write!(f, "string"), + PrimitiveOrArrayTypeSpec::Strings => write!(f, "string[]"), + PrimitiveOrArrayTypeSpec::Ints => write!(f, "int[]"), + PrimitiveOrArrayTypeSpec::Doubles => write!(f, "double[]"), + PrimitiveOrArrayTypeSpec::Booleans => write!(f, "boolean[]"), + } + } +} + +/// Template types. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum TemplateTypeSpec { + /// A boolean attribute. + #[serde(rename = "template[boolean]")] + Boolean, + /// A integer attribute. + #[serde(rename = "template[int]")] + Int, + /// A double attribute. + #[serde(rename = "template[double]")] + Double, + /// A string attribute. + #[serde(rename = "template[string]")] + String, + /// An array of strings attribute. + #[serde(rename = "template[string[]]")] + Strings, + /// An array of integer attribute. + #[serde(rename = "template[int[]]")] + Ints, + /// An array of double attribute. + #[serde(rename = "template[double[]]")] + Doubles, + /// An array of boolean attribute. + #[serde(rename = "template[boolean[]]")] + Booleans, +} + +/// Implements a human readable display for TemplateType. +impl Display for TemplateTypeSpec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TemplateTypeSpec::Boolean => write!(f, "template[boolean]"), + TemplateTypeSpec::Int => write!(f, "template[int]"), + TemplateTypeSpec::Double => write!(f, "template[double]"), + TemplateTypeSpec::String => write!(f, "template[string]"), + TemplateTypeSpec::Strings => write!(f, "template[string[]]"), + TemplateTypeSpec::Ints => write!(f, "template[int[]]"), + TemplateTypeSpec::Doubles => write!(f, "template[double[]]"), + TemplateTypeSpec::Booleans => write!(f, "template[boolean[]]"), + } + } +} + +/// Possible enum entries. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(deny_unknown_fields)] +pub struct EnumEntriesSpec { + /// String that uniquely identifies the enum entry. + pub id: String, + /// String, int, or boolean; value of the enum entry. + pub value: ValueSpec, + /// Brief description of the enum entry value. + /// It defaults to the value of id. + pub brief: Option, + /// Longer description. + /// It defaults to an empty string. + pub note: Option, +} + +/// Implements a human readable display for EnumEntries. +impl Display for EnumEntriesSpec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "id={}, type={}", self.id, self.value) + } +} + +/// The different types of values. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +pub enum ValueSpec { + /// A integer value. + Int(i64), + /// A double value. + Double(OrderedFloat), + /// A string value. + String(String), +} + +/// Implements a human readable display for Value. +impl Display for ValueSpec { + /// Formats the value. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ValueSpec::Int(v) => write!(f, "{}", v), + ValueSpec::Double(v) => write!(f, "{}", v), + ValueSpec::String(v) => write!(f, "{}", v), + } + } +} + +/// The different types of examples. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +pub enum ExamplesSpec { + /// A boolean example. + Bool(bool), + /// A integer example. + Int(i64), + /// A double example. + Double(OrderedFloat), + /// A string example. + String(String), + /// A array of integers example. + Ints(Vec), + /// A array of doubles example. + Doubles(Vec>), + /// A array of bools example. + Bools(Vec), + /// A array of strings example. + Strings(Vec), +} + +/// The different requirement level specifications. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +#[serde(untagged)] +pub enum RequirementLevelSpec { + /// A basic requirement level. + Basic(BasicRequirementLevelSpec), + /// A conditional requirement level. + ConditionallyRequired { + /// The description of the condition. + #[serde(rename = "conditionally_required")] + text: String, + }, + /// A recommended requirement level. + Recommended { + /// The description of the recommendation. + #[serde(rename = "recommended")] + text: String, + }, +} + +/// Implements a human readable display for RequirementLevel. +impl Display for RequirementLevelSpec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RequirementLevelSpec::Basic(brl) => write!(f, "{}", brl), + RequirementLevelSpec::ConditionallyRequired { text } => { + write!(f, "conditionally required (condition: {})", text) + } + RequirementLevelSpec::Recommended { text } => write!(f, "recommended ({})", text), + } + } +} + +// Specifies the default requirement level as defined in the OTel +// specification. +impl Default for RequirementLevelSpec { + fn default() -> Self { + RequirementLevelSpec::Basic(BasicRequirementLevelSpec::Recommended) + } +} + +/// The different types of basic requirement levels. +#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum BasicRequirementLevelSpec { + /// A required requirement level. + Required, + /// An optional requirement level. + Recommended, + /// An opt-in requirement level. + OptIn, +} + +/// Implements a human readable display for BasicRequirementLevel. +impl Display for BasicRequirementLevelSpec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + BasicRequirementLevelSpec::Required => write!(f, "required"), + BasicRequirementLevelSpec::Recommended => write!(f, "recommended"), + BasicRequirementLevelSpec::OptIn => write!(f, "opt-in"), + } + } +} diff --git a/crates/weaver_semconv/src/group.rs b/crates/weaver_semconv/src/group.rs new file mode 100644 index 00000000..737eaf3a --- /dev/null +++ b/crates/weaver_semconv/src/group.rs @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: Apache-2.0 + +#![allow(rustdoc::invalid_html_tags)] + +//! A group specification. + +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use validator::{Validate, ValidationError}; + +use crate::attribute::{AttributeSpec, AttributeTypeSpec, PrimitiveOrArrayTypeSpec}; +use crate::group::InstrumentSpec::{Counter, Gauge, Histogram, UpDownCounter}; +use crate::stability::StabilitySpec; + +/// Group Spec contain the list of semantic conventions and it is the root node +/// of each yaml file. +#[derive(Serialize, Deserialize, Debug, Validate, Clone)] +#[serde(deny_unknown_fields)] +#[validate(schema(function = "validate_group"))] +pub struct GroupSpec { + /// The id that uniquely identifies the semantic convention. + pub id: String, + /// The type of the semantic convention (default to span). + #[serde(default)] + pub r#type: ConvTypeSpec, + /// A brief description of the semantic convention. + pub brief: String, + /// A more elaborate description of the semantic convention. + /// It defaults to an empty string. + #[serde(default)] + pub note: String, + /// Prefix for the attributes for this semantic convention. + /// It defaults to an empty string. + #[serde(default)] + pub prefix: String, + /// Reference another semantic convention id. It inherits the prefix, + /// constraints, and all attributes defined in the specified semantic + /// convention. + pub extends: Option, + /// Specifies the stability of the semantic convention. + /// Note that, if stability is missing but deprecated is present, it will + /// automatically set the stability to deprecated. If deprecated is + /// present and stability differs from deprecated, this will result in an + /// error. + #[serde(skip_serializing_if = "Option::is_none")] + pub stability: Option, + /// Specifies if the semantic convention is deprecated. The string + /// provided as MUST specify why it's deprecated and/or what + /// to use instead. See also stability. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecated: Option, + /// List of attributes that belong to the semantic convention. + #[serde(default)] + pub attributes: Vec, + /// Additional constraints. + /// Allow to define additional requirements on the semantic convention. + /// It defaults to an empty list. + #[serde(default)] + pub constraints: Vec, + /// Specifies the kind of the span. + /// Note: only valid if type is span (the default) + pub span_kind: Option, + /// List of strings that specify the ids of event semantic conventions + /// associated with this span semantic convention. + /// Note: only valid if type is span (the default) + #[serde(default)] + pub events: Vec, + /// The metric name as described by the [OpenTelemetry Specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#timeseries-model). + /// Note: This field is required if type is metric. + pub metric_name: Option, + /// The instrument type that should be used to record the metric. Note that + /// the semantic conventions must be written using the names of the + /// synchronous instrument types (counter, gauge, updowncounter and + /// histogram). + /// For more details: [Metrics semantic conventions - Instrument types](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-types). + /// Note: This field is required if type is metric. + pub instrument: Option, + /// The unit in which the metric is measured, which should adhere to the + /// [guidelines](https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/metrics/semantic_conventions#instrument-units). + /// Note: This field is required if type is metric. + pub unit: Option, + /// The name of the event. If not specified, the prefix is used. + /// If prefix is empty (or unspecified), name is required. + pub name: Option, +} + +/// Validation logic for the group. +fn validate_group(group: &GroupSpec) -> Result<(), ValidationError> { + // If deprecated is present and stability differs from deprecated, this + // will result in an error. + if group.deprecated.is_some() + && group.stability.is_some() + && group.stability != Some(StabilitySpec::Deprecated) + { + return Err(ValidationError::new( + "This group contains a deprecated field but the stability is not set to deprecated.", + )); + } + + // Fields span_kind and events are only valid if type is span (the default). + if group.r#type != ConvTypeSpec::Span { + if group.span_kind.is_some() { + return Err(ValidationError::new( + "This group contains a span_kind field but the type is not set to span.", + )); + } + if !group.events.is_empty() { + return Err(ValidationError::new( + "This group contains an events field but the type is not set to span.", + )); + } + } + + // Field name is required if prefix is empty and if type is event. + if group.r#type == ConvTypeSpec::Event && group.prefix.is_empty() && group.name.is_none() { + return Err(ValidationError::new( + "This group contains an event type but the prefix is empty and the name is not set.", + )); + } + + // Fields metric_name, instrument and unit are required if type is metric. + if group.r#type == ConvTypeSpec::Metric { + if group.metric_name.is_none() { + return Err(ValidationError::new( + "This group contains a metric type but the metric_name is not set.", + )); + } + if group.instrument.is_none() { + return Err(ValidationError::new( + "This group contains a metric type but the instrument is not set.", + )); + } + if group.unit.is_none() { + return Err(ValidationError::new( + "This group contains a metric type but the unit is not set.", + )); + } + } + + // Validates the attributes. + for attribute in &group.attributes { + // If deprecated is present and stability differs from deprecated, this + // will result in an error. + match attribute { + AttributeSpec::Id { + stability, + deprecated, + .. + } + | AttributeSpec::Ref { + stability, + deprecated, + .. + } => { + if deprecated.is_some() + && stability.is_some() + && *stability != Some(StabilitySpec::Deprecated) + { + return Err(ValidationError::new("This attribute contains a deprecated field but the stability is not set to deprecated.")); + } + } + } + + // Examples are required only for string and string array attributes. + if let AttributeSpec::Id { + r#type, examples, .. + } = attribute + { + if examples.is_some() { + continue; + } + + if *r#type == AttributeTypeSpec::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::String) { + return Err(ValidationError::new( + "This attribute is a string but it does not contain any examples.", + )); + } + if *r#type == AttributeTypeSpec::PrimitiveOrArray(PrimitiveOrArrayTypeSpec::Strings) { + return Err(ValidationError::new( + "This attribute is a string array but it does not contain any examples.", + )); + } + } + } + + Ok(()) +} + +/// The different types of groups (specification). +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(rename_all = "snake_case")] +pub enum ConvTypeSpec { + /// Attribute group (attribute_group type) defines a set of attributes that + /// can be declared once and referenced by semantic conventions for + /// different signals, for example spans and logs. Attribute groups don't + /// have any specific fields and follow the general semconv semantics. + AttributeGroup, + /// Span semantic convention. + Span, + /// Event semantic convention. + Event, + /// Metric semantic convention. + Metric, + /// The metric group semconv is a group where related metric attributes can + /// be defined and then referenced from other metric groups using ref. + MetricGroup, + /// A group of resources. + Resource, + /// Scope. + Scope, +} + +impl Default for ConvTypeSpec { + /// Returns the default convention type that is span based on + /// the OpenTelemetry specification. + fn default() -> Self { + Self::Span + } +} + +/// The span kind. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum SpanKindSpec { + /// An internal span. + Internal, + /// A client span. + Client, + /// A server span. + Server, + /// A producer span. + Producer, + /// A consumer span. + Consumer, +} + +/// Allow to define additional requirements on the semantic convention. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct ConstraintSpec { + /// any_of accepts a list of sequences. Each sequence contains a list of + /// attribute ids that are required. any_of enforces that all attributes + /// of at least one of the sequences are set. + #[serde(default)] + pub any_of: Vec, + /// include accepts a semantic conventions id. It includes as part of this + /// semantic convention all constraints and required attributes that are + /// not already defined in the current semantic convention. + pub include: Option, +} + +/// The type of the metric. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum InstrumentSpec { + /// An up-down counter metric. + #[serde(rename = "updowncounter")] + UpDownCounter, + /// A counter metric. + Counter, + /// A gauge metric. + Gauge, + /// A histogram metric. + Histogram, +} + +/// Implements a human readable display for the instrument. +impl Display for InstrumentSpec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + UpDownCounter => write!(f, "updowncounter"), + Counter => write!(f, "counter"), + Gauge => write!(f, "gauge"), + Histogram => write!(f, "histogram"), + } + } +} diff --git a/crates/weaver_semconv/src/lib.rs b/crates/weaver_semconv/src/lib.rs new file mode 100644 index 00000000..d54e7b38 --- /dev/null +++ b/crates/weaver_semconv/src/lib.rs @@ -0,0 +1,854 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! This crate defines the concept of a 'semantic convention catalog', which is +//! fueled by one or more semantic convention YAML files. +//! +//! The YAML language syntax used to define a semantic convention file +//! can be found [here](https://github.com/open-telemetry/build-tools/blob/main/semantic-conventions/syntax.md). + +#![deny( + missing_docs, + clippy::print_stdout, + unstable_features, + unused_import_braces, + unused_qualifications, + unused_results, + unused_extern_crates +)] + +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +use serde::{Deserialize, Serialize}; +use validator::Validate; + +use crate::attribute::AttributeSpec; +use crate::group::GroupSpec; +use crate::metric::MetricSpec; + +pub mod attribute; +pub mod group; +pub mod metric; +pub mod stability; + +/// An error that can occur while loading a semantic convention registry. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// The semantic convention asset was not found. + #[error("Semantic convention registry {path_or_url:?} not found\n{error}")] + CatalogNotFound { + /// The path or URL of the semantic convention asset. + path_or_url: String, + /// The error that occurred. + error: String, + }, + + /// The semantic convention asset is invalid. + #[error("Invalid semantic convention registry {path_or_url:?}\n{error}")] + InvalidCatalog { + /// The path or URL of the semantic convention asset. + path_or_url: String, + /// The line where the error occurred. + line: Option, + /// The column where the error occurred. + column: Option, + /// The error that occurred. + error: String, + }, + + /// The semantic convention asset contains a duplicate attribute id. + #[error("Duplicate attribute id `{id}` detected while loading {path_or_url:?}, already defined in {origin_path_or_url:?}")] + DuplicateAttributeId { + /// The path or URL where the attribute id was defined for the first time. + origin_path_or_url: String, + /// The path or URL of the semantic convention asset. + path_or_url: String, + /// The duplicated attribute id. + id: String, + }, + + /// The semantic convention asset contains a duplicate group id. + #[error("Duplicate group id `{id}` detected while loading {path_or_url:?} and already defined in {origin}")] + DuplicateGroupId { + /// The path or URL of the semantic convention asset. + path_or_url: String, + /// The duplicated group id. + id: String, + /// The asset where the group id was already defined. + origin: String, + }, + + /// The semantic convention asset contains a duplicate metric name. + #[error("Duplicate metric name `{name}` detected while loading {path_or_url:?}")] + DuplicateMetricName { + /// The path or URL of the semantic convention asset. + path_or_url: String, + /// The duplicated metric name. + name: String, + }, + + /// The semantic convention asset contains an invalid attribute definition. + #[error("Invalid attribute definition detected while resolving {path_or_url:?}, group_id=`{group_id}`.\n{error}")] + InvalidAttribute { + /// The path or URL of the semantic convention asset. + path_or_url: String, + /// The group id of the attribute. + group_id: String, + /// The reason of the error. + error: String, + }, + + /// The attribute reference is not found. + #[error("Attribute reference `{r#ref}` not found.")] + AttributeNotFound { + /// The attribute reference. + r#ref: String, + }, + + /// The semantic convention asset contains an invalid metric definition. + #[error("Invalid metric definition in {path_or_url:?}.\ngroup_id=`{group_id}`.\n{error}")] + InvalidMetric { + /// The path or URL of the semantic convention asset. + path_or_url: String, + /// The group id of the metric. + group_id: String, + /// The reason of the error. + error: String, + }, +} + +/// A semantic convention spec with its provenance (path or URL). +#[derive(Debug, Clone)] +pub struct SemConvSpecWithProvenance { + /// The semantic convention spec. + pub spec: SemConvSpec, + /// The provenance of the semantic convention spec (path or URL). + pub provenance: String, +} + +/// A group spec with its provenance (path or URL). +#[derive(Debug, Clone)] +pub struct GroupSpecWithProvenance { + /// The group spec. + pub spec: GroupSpec, + /// The provenance of the group spec (path or URL). + pub provenance: String, +} + +/// An attribute definition with its provenance (path or URL). +#[derive(Debug, Clone)] +pub struct AttributeSpecWithProvenance { + /// The attribute definition. + pub attribute: AttributeSpec, + /// The provenance of the attribute (path or URL). + pub provenance: String, +} + +/// A metric definition with its provenance (path or URL). +#[derive(Debug, Clone)] +pub struct MetricSpecWithProvenance { + /// The metric definition. + pub metric: MetricSpec, + /// The provenance of the metric (path or URL). + pub provenance: String, +} + +/// A semantic convention specs is a collection of semantic convention +/// specifications indexed by group id. +#[derive(Default, Debug)] +pub struct SemConvSpecs { + /// The number of semantic convention assets added in the semantic convention registry. + /// A asset can be a semantic convention loaded from a file or an URL. + asset_count: usize, + + /// A collection of semantic convention specifications loaded in the semantic convention registry. + specs: Vec, + + /// Attributes indexed by their respective id independently of their + /// semantic convention group. + /// + /// This collection contains all the attributes defined in the semantic convention registry. + all_attributes: HashMap, + + /// Metrics indexed by their respective id. + /// + /// This collection contains all the metrics defined in the semantic convention registry. + all_metrics: HashMap, + + /// Collection of attribute ids index by group id and defined in a + /// `resource` semantic convention group. + /// Attribute ids are references to of attributes defined in the + /// all_attributes field. + resource_group_attributes: HashMap, + + /// Collection of attribute ids index by group id and defined in a + /// `attribute_group` semantic convention group. + /// Attribute ids are references to of attributes defined in the + /// all_attributes field. + attr_grp_group_attributes: HashMap, + + /// Collection of attribute ids index by group id and defined in a + /// `span` semantic convention group. + /// Attribute ids are references to of attributes defined in the + /// all_attributes field. + span_group_attributes: HashMap, + + /// Collection of attribute ids index by group id and defined in a + /// `event` semantic convention group. + /// Attribute ids are references to of attributes defined in the + /// all_attributes field. + event_group_attributes: HashMap, + + /// Collection of attribute ids index by group id and defined in a + /// `metric` semantic convention group. + /// Attribute ids are references to of attributes defined in the + /// all_attributes field. + metric_group_attributes: HashMap, + + /// Collection of attribute ids index by group id and defined in a + /// `metric_group` semantic convention group. + /// Attribute ids are references to of attributes defined in the + /// all_attributes field. + metric_group_group_attributes: HashMap, +} + +/// Represents a collection of ids (attribute or metric ids). +#[derive(Debug, Default)] +struct GroupIds { + /// The semantic convention origin (path or URL) where the group id is + /// defined. This is used to report errors. + origin: String, + /// The collection of ids (attribute or metric ids). + ids: HashSet, +} + +/// A semantic convention specification. +/// +/// See [here](https://github.com/open-telemetry/build-tools/blob/main/semantic-conventions/syntax.md) +/// the syntax of the semantic convention YAML file. +#[derive(Serialize, Deserialize, Debug, Validate, Clone)] +#[serde(deny_unknown_fields)] +pub struct SemConvSpec { + /// A collection of semantic convention groups. + #[validate] + pub groups: Vec, +} + +/// The configuration of the resolver. +#[derive(Debug, Default)] +pub struct ResolverConfig { + error_when_attribute_ref_not_found: bool, + keep_specs: bool, +} + +impl ResolverConfig { + /// Returns a config instructing the resolver to keep + /// the semantic convention group specs after the resolution. + pub fn with_keep_specs() -> Self { + Self { + keep_specs: true, + ..Default::default() + } + } +} + +/// A wrapper for a resolver error that is considered as a warning +/// by configuration. +#[derive(Debug)] +pub struct ResolverWarning { + /// The error that occurred. + pub error: Error, +} + +/// Structure to keep track of the source of the attribute to resolve. +struct AttributeToResolve { + /// The provenance of the attribute. + /// Path or URL of the semantic convention asset. + path_or_url: String, + /// The group id of the attribute. + group_id: String, + /// The attribute reference. + r#ref: String, +} + +/// Structure to keep track of the source of the metric to resolve. +struct MetricToResolve { + path_or_url: String, + group_id: String, + r#ref: String, +} + +impl SemConvSpecs { + /// Load and add a semantic convention file to the semantic convention registry. + pub fn load_from_file + Clone>(&mut self, path: P) -> Result<(), Error> { + let spec = SemConvSpec::load_from_file(path.clone())?; + if let Err(e) = spec.validate() { + return Err(Error::InvalidCatalog { + path_or_url: path.as_ref().display().to_string(), + line: None, + column: None, + error: e.to_string(), + }); + } + self.specs.push(SemConvSpecWithProvenance { + spec, + provenance: path.as_ref().display().to_string(), + }); + Ok(()) + } + + /// Loads and returns the semantic convention spec from a file. + pub fn load_sem_conv_spec_from_file( + sem_conv_path: &Path, + ) -> Result<(String, SemConvSpec), Error> { + let spec = SemConvSpec::load_from_file(sem_conv_path)?; + if let Err(e) = spec.validate() { + return Err(Error::InvalidCatalog { + path_or_url: sem_conv_path.display().to_string(), + line: None, + column: None, + error: e.to_string(), + }); + } + Ok((sem_conv_path.display().to_string(), spec)) + } + + /// Downloads and returns the semantic convention spec from an URL. + pub fn load_sem_conv_spec_from_url(sem_conv_url: &str) -> Result<(String, SemConvSpec), Error> { + let spec = SemConvSpec::load_from_url(sem_conv_url)?; + if let Err(e) = spec.validate() { + return Err(Error::InvalidCatalog { + path_or_url: sem_conv_url.to_string(), + line: None, + column: None, + error: e.to_string(), + }); + } + Ok((sem_conv_url.to_string(), spec)) + } + + /// Returns the number of semantic convention assets added in the semantic convention registry. + pub fn asset_count(&self) -> usize { + self.asset_count + } + + /// Append a list of semantic convention specs to the semantic convention registry. + pub fn append_sem_conv_specs(&mut self, specs: Vec) { + self.specs.extend(specs); + } + + /// Append a semantic convention spec to the semantic convention registry. + pub fn append_sem_conv_spec(&mut self, spec: SemConvSpecWithProvenance) { + self.specs.push(spec); + self.asset_count += 1; + } + + /// Resolves all the references present in the semantic convention registry. + /// + /// The `config` parameter allows to customize the resolver behavior + /// when a reference is not found. By default, the resolver will emit an + /// error when a reference is not found. This behavior can be changed by + /// setting the `error_when_<...>_ref_not_found` to `false`, in which case + /// the resolver will record the error in a warning list and continue. + /// The warning list is returned as a list of warnings in the result. + pub fn resolve(&mut self, config: ResolverConfig) -> Result, Error> { + let mut warnings = Vec::new(); + let mut attributes_to_resolve = Vec::new(); + let mut metrics_to_resolve = HashMap::new(); + + // Add all the attributes with an id to the semantic convention registry. + for SemConvSpecWithProvenance { spec, provenance } in self.specs.clone().into_iter() { + for group in spec.groups.iter() { + // Process attributes + match group.r#type { + group::ConvTypeSpec::AttributeGroup + | group::ConvTypeSpec::Span + | group::ConvTypeSpec::Resource + | group::ConvTypeSpec::Metric + | group::ConvTypeSpec::Event + | group::ConvTypeSpec::MetricGroup => { + let attributes_in_group = self.process_attributes( + provenance.clone(), + group.id.clone(), + group.prefix.clone(), + group.attributes.clone(), + &mut attributes_to_resolve, + )?; + + let group_attributes = match group.r#type { + group::ConvTypeSpec::AttributeGroup => { + Some(&mut self.attr_grp_group_attributes) + } + group::ConvTypeSpec::Span => Some(&mut self.span_group_attributes), + group::ConvTypeSpec::Resource => { + Some(&mut self.resource_group_attributes) + } + group::ConvTypeSpec::Metric => Some(&mut self.metric_group_attributes), + group::ConvTypeSpec::Event => Some(&mut self.event_group_attributes), + group::ConvTypeSpec::MetricGroup => { + Some(&mut self.metric_group_group_attributes) + } + _ => None, + }; + + if let Some(group_attributes) = group_attributes { + let prev_group_ids = group_attributes.insert( + group.id.clone(), + GroupIds { + origin: provenance.clone(), + ids: attributes_in_group.clone(), + }, + ); + Self::detect_duplicated_group( + provenance.clone(), + group.id.clone(), + prev_group_ids, + )?; + } + } + _ => { + eprintln!( + "Warning: group type `{:?}` not implemented yet", + group.r#type + ); + } + } + + // Process metrics + match group.r#type { + group::ConvTypeSpec::Metric => { + let metric_name = if let Some(metric_name) = group.metric_name.as_ref() { + metric_name.clone() + } else { + return Err(Error::InvalidMetric { + path_or_url: provenance.clone(), + group_id: group.id.clone(), + error: "Metric without name".to_string(), + }); + }; + let instrument = if let Some(instrument) = group.instrument.as_ref() { + instrument.clone() + } else { + return Err(Error::InvalidMetric { + path_or_url: provenance.clone(), + group_id: group.id.clone(), + error: "Metric without instrument definition".to_string(), + }); + }; + + let prev_val = self.all_metrics.insert( + metric_name.clone(), + MetricSpecWithProvenance { + metric: MetricSpec { + name: metric_name.clone(), + brief: group.brief.clone(), + note: group.note.clone(), + attributes: group.attributes.clone(), + instrument, + unit: group.unit.clone(), + }, + provenance: provenance.clone(), + }, + ); + if prev_val.is_some() { + return Err(Error::DuplicateMetricName { + path_or_url: provenance.clone(), + name: metric_name.clone(), + }); + } + + if let Some(r#ref) = group.extends.as_ref() { + let prev_val = metrics_to_resolve.insert( + metric_name.clone(), + MetricToResolve { + path_or_url: provenance.clone(), + group_id: group.id.clone(), + r#ref: r#ref.clone(), + }, + ); + if prev_val.is_some() { + return Err(Error::DuplicateMetricName { + path_or_url: provenance.clone(), + name: r#ref.clone(), + }); + } + } + } + group::ConvTypeSpec::MetricGroup => { + eprintln!("Warning: group type `metric_group` not implemented yet"); + } + _ => { + // No metrics to process + } + } + } + } + + // Resolve all the attributes with a reference. + for attr_to_resolve in attributes_to_resolve.into_iter() { + let resolved_attr = self.all_attributes.get(&attr_to_resolve.r#ref); + + if resolved_attr.is_none() { + let err = Error::InvalidAttribute { + path_or_url: attr_to_resolve.path_or_url.clone(), + group_id: attr_to_resolve.group_id.clone(), + error: format!("Attribute reference '{}' not found", attr_to_resolve.r#ref), + }; + if config.error_when_attribute_ref_not_found { + return Err(err); + } else { + warnings.push(ResolverWarning { error: err }); + } + } + } + + // Resolve all the metrics with an `extends` field. + for (metric_name, metric_to_resolve) in metrics_to_resolve { + let attribute_group = self.attr_grp_group_attributes.get(&metric_to_resolve.r#ref); + if let Some(attr_grp) = attribute_group { + if let Some(metric) = self.all_metrics.get_mut(&metric_name) { + let mut inherited_attributes = vec![]; + for attr_id in attr_grp.ids.iter() { + if let Some(attr) = self.all_attributes.get(attr_id) { + // Note: we only keep the last attribute definition for attributes that + // are defined multiple times in the group. + inherited_attributes.push(attr.attribute.clone()); + } + } + metric + .metric + .attributes + .extend(inherited_attributes.iter().cloned()); + } else { + return Err(Error::InvalidMetric { + path_or_url: metric_to_resolve.path_or_url, + group_id: metric_to_resolve.group_id, + error: format!("The metric '{}' doesn't exist", metric_name), + }); + } + } else { + warnings.push(ResolverWarning { + error: Error::InvalidMetric { + path_or_url: metric_to_resolve.path_or_url, + group_id: metric_to_resolve.group_id, + error: format!("The reference `{}` specified in the `extends` field of the '{}' metric could not be resolved", metric_to_resolve.r#ref, metric_name), + } + }); + } + } + + if !config.keep_specs { + self.specs.clear(); + } + + Ok(warnings) + } + + /// Returns the number of unique attributes defined in the semantic convention registry. + pub fn attribute_count(&self) -> usize { + self.all_attributes.len() + } + + /// Returns the number of unique metrics defined in the semantic convention registry. + pub fn metric_count(&self) -> usize { + self.all_metrics.len() + } + + /// Returns an attribute definition from its reference or `None` if the + /// reference does not exist. + pub fn attribute(&self, attr_ref: &str) -> Option<&AttributeSpec> { + self.all_attributes + .get(attr_ref) + .map(|attr| &attr.attribute) + } + + /// Returns an attribute definition and its provenance from its reference + /// or `None` if the reference does not exist. + pub fn attribute_with_provenance( + &self, + attr_ref: &str, + ) -> Option<&AttributeSpecWithProvenance> { + self.all_attributes.get(attr_ref) + } + + /// Returns a map id -> attribute definition from an attribute group reference. + /// Or an error if the reference does not exist. + pub fn attributes( + &self, + r#ref: &str, + r#type: group::ConvTypeSpec, + ) -> Result, Error> { + let mut attributes = HashMap::new(); + let group_ids = match r#type { + group::ConvTypeSpec::AttributeGroup => self.attr_grp_group_attributes.get(r#ref), + group::ConvTypeSpec::Span => self.span_group_attributes.get(r#ref), + group::ConvTypeSpec::Event => self.event_group_attributes.get(r#ref), + group::ConvTypeSpec::Metric => self.metric_group_attributes.get(r#ref), + group::ConvTypeSpec::MetricGroup => self.metric_group_group_attributes.get(r#ref), + group::ConvTypeSpec::Resource => self.resource_group_attributes.get(r#ref), + group::ConvTypeSpec::Scope => panic!("Scope not implemented yet"), + }; + if let Some(group_ids) = group_ids { + for attr_id in group_ids.ids.iter() { + if let Some(attr) = self.all_attributes.get(attr_id) { + // Note: we only keep the last attribute definition for attributes that + // are defined multiple times in the group. + _ = attributes.insert(attr_id, &attr.attribute); + } + } + } else { + return Err(Error::AttributeNotFound { + r#ref: r#ref.to_string(), + }); + } + Ok(attributes) + } + + /// Returns an iterator over all the groups defined in the semantic convention registry. + pub fn groups(&self) -> impl Iterator { + self.specs + .iter() + .flat_map(|SemConvSpecWithProvenance { spec, .. }| &spec.groups) + } + + /// Returns an iterator over all the groups defined in the semantic convention registry. + /// Each group is associated with its provenance (path or URL). + pub fn groups_with_provenance(&self) -> impl Iterator + '_ { + self.specs + .iter() + .flat_map(|SemConvSpecWithProvenance { spec, provenance }| { + spec.groups.iter().map(|group| GroupSpecWithProvenance { + spec: group.clone(), + provenance: provenance.clone(), + }) + }) + } + + /// Returns an iterator over all the attributes defined in the semantic convention registry. + pub fn attributes_iter(&self) -> impl Iterator { + self.all_attributes.values().map(|attr| &attr.attribute) + } + + /// Returns an iterator over all the metrics defined in the semantic convention registry. + pub fn metrics_iter(&self) -> impl Iterator { + self.all_metrics.values().map(|metric| &metric.metric) + } + + /// Returns a metric definition from its name or `None` if the + /// name does not exist. + pub fn metric(&self, metric_name: &str) -> Option<&MetricSpec> { + self.all_metrics + .get(metric_name) + .map(|metric| &metric.metric) + } + + /// Returns a metric definition and its provenance from its name + pub fn metric_with_provenance(&self, metric_name: &str) -> Option<&MetricSpecWithProvenance> { + self.all_metrics.get(metric_name) + } + + /// Returns an error if prev_group_ids is not `None`. + fn detect_duplicated_group( + path_or_url: String, + group_id: String, + prev_group_ids: Option, + ) -> Result<(), Error> { + if let Some(group_ids) = prev_group_ids.as_ref() { + return Err(Error::DuplicateGroupId { + path_or_url, + id: group_id, + origin: group_ids.origin.clone(), + }); + } + Ok(()) + } + + /// Processes a collection of attributes passed as a parameter (`attrs`), + /// adds attributes fully defined to the semantic convention registry, adds attributes with + /// a reference to the list of attributes to resolve and returns a + /// collection of attribute ids defined in the current group. + fn process_attributes( + &mut self, + path_or_url: String, + group_id: String, + prefix: String, + attrs: Vec, + attributes_to_resolve: &mut Vec, + ) -> Result, Error> { + let mut attributes_in_group = HashSet::new(); + for mut attr in attrs.into_iter() { + match &attr { + AttributeSpec::Id { id, .. } => { + // The attribute has an id, so add it to the semantic convention registry + // if it does not exist yet, otherwise return an error. + // The fully qualified attribute id is the concatenation + // of the prefix and the attribute id (separated by a dot). + let fq_attr_id = if prefix.is_empty() { + id.clone() + } else { + format!("{}.{}", prefix, id) + }; + if let AttributeSpec::Id { id, .. } = &mut attr { + *id = fq_attr_id.clone(); + } + let prev_val = self.all_attributes.insert( + fq_attr_id.clone(), + AttributeSpecWithProvenance { + attribute: attr, + provenance: path_or_url.clone(), + }, + ); + if let Some(prev_val) = prev_val { + return Err(Error::DuplicateAttributeId { + origin_path_or_url: prev_val.provenance.clone(), + path_or_url: path_or_url.clone(), + id: fq_attr_id.clone(), + }); + } + let _ = attributes_in_group.insert(fq_attr_id.clone()); + } + AttributeSpec::Ref { r#ref, .. } => { + // The attribute has a reference, so add it to the + // list of attributes to resolve. + attributes_to_resolve.push(AttributeToResolve { + path_or_url: path_or_url.clone(), + group_id: group_id.clone(), + r#ref: r#ref.clone(), + }); + let _ = attributes_in_group.insert(r#ref.clone()); + } + } + } + Ok(attributes_in_group) + } +} + +impl SemConvSpec { + /// Load a semantic convention semantic convention registry from a file. + pub fn load_from_file>(path: P) -> Result { + let path_buf = path.as_ref().to_path_buf(); + + // Load and deserialize the semantic convention semantic convention registry + let catalog_file = File::open(path).map_err(|e| Error::CatalogNotFound { + path_or_url: path_buf.as_path().display().to_string(), + error: e.to_string(), + })?; + let catalog: SemConvSpec = + serde_yaml::from_reader(BufReader::new(catalog_file)).map_err(|e| { + Error::InvalidCatalog { + path_or_url: path_buf.as_path().display().to_string(), + line: e.location().map(|loc| loc.line()), + column: e.location().map(|loc| loc.column()), + error: e.to_string(), + } + })?; + Ok(catalog) + } + + /// Load a semantic convention semantic convention registry from a URL. + pub fn load_from_url(semconv_url: &str) -> Result { + // Create a content reader from the semantic convention URL + let reader = ureq::get(semconv_url) + .call() + .map_err(|e| Error::CatalogNotFound { + path_or_url: semconv_url.to_string(), + error: e.to_string(), + })? + .into_reader(); + + // Deserialize the telemetry schema from the content reader + let catalog: SemConvSpec = + serde_yaml::from_reader(reader).map_err(|e| Error::InvalidCatalog { + path_or_url: semconv_url.to_string(), + line: e.location().map(|loc| loc.line()), + column: e.location().map(|loc| loc.column()), + error: e.to_string(), + })?; + Ok(catalog) + } +} + +#[cfg(test)] +mod tests { + use std::vec; + + use super::*; + + /// Load multiple semantic convention files in the semantic convention registry. + /// No error should be emitted. + #[test] + fn test_load_catalog() { + let yaml_files = vec![ + "data/client.yaml", + "data/cloud.yaml", + "data/cloudevents.yaml", + "data/database.yaml", + "data/database-metrics.yaml", + "data/exception.yaml", + "data/faas.yaml", + "data/faas-common.yaml", + "data/faas-metrics.yaml", + "data/http.yaml", + "data/http-common.yaml", + "data/http-metrics.yaml", + "data/jvm-metrics.yaml", + "data/media.yaml", + "data/messaging.yaml", + "data/network.yaml", + "data/rpc.yaml", + "data/rpc-metrics.yaml", + "data/server.yaml", + "data/source.yaml", + "data/trace-exception.yaml", + "data/url.yaml", + "data/user-agent.yaml", + "data/vm-metrics-experimental.yaml", + "data/tls.yaml", + ]; + + let mut catalog = SemConvSpecs::default(); + for yaml in yaml_files { + let result = catalog.load_from_file(yaml); + assert!(result.is_ok(), "{:#?}", result.err().unwrap()); + } + } + + /// Test the resolver with a semantic convention semantic convention registry that contains + /// multiple references to resolve. + /// No error or warning should be emitted. + #[test] + fn test_resolve_catalog() { + let yaml_files = vec![ + "data/http-common.yaml", + "data/http-metrics.yaml", + "data/network.yaml", + "data/server.yaml", + "data/url.yaml", + ]; + + let mut catalog = SemConvSpecs::default(); + for yaml in yaml_files { + let result = catalog.load_from_file(yaml); + assert!(result.is_ok(), "{:#?}", result.err().unwrap()); + } + + let result = catalog.resolve(ResolverConfig { + error_when_attribute_ref_not_found: false, + ..Default::default() + }); + + match result { + Ok(warnings) => { + if !warnings.is_empty() { + dbg!(&warnings); + } + assert!(warnings.is_empty()); + } + Err(e) => { + panic!("{:#?}", e); + } + } + } +} diff --git a/crates/weaver_semconv/src/metric.rs b/crates/weaver_semconv/src/metric.rs new file mode 100644 index 00000000..bab42b67 --- /dev/null +++ b/crates/weaver_semconv/src/metric.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Metric specification. + +use crate::attribute::AttributeSpec; +use crate::group::InstrumentSpec; +use serde::{Deserialize, Serialize}; + +/// A metric specification. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct MetricSpec { + /// Metric name. + pub name: String, + /// Brief description of the metric. + pub brief: String, + /// Note on the metric. + pub note: String, + /// Set of attribute ids attached to the metric. + #[serde(default)] + pub attributes: Vec, + /// Type of the metric (e.g. gauge, histogram, ...). + pub instrument: InstrumentSpec, + /// Unit of the metric. + pub unit: Option, +} + +impl MetricSpec { + /// Returns the name of the metric. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the brief description of the metric. + pub fn brief(&self) -> &str { + &self.brief + } + + /// Returns the note on the metric. + pub fn note(&self) -> &str { + &self.note + } + + /// Returns the unit of the metric. + pub fn unit(&self) -> Option<&str> { + self.unit.as_deref() + } +} diff --git a/crates/weaver_semconv/src/stability.rs b/crates/weaver_semconv/src/stability.rs new file mode 100644 index 00000000..00917690 --- /dev/null +++ b/crates/weaver_semconv/src/stability.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Stability specification. + +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +/// The level of stability for a definition. +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] +#[serde(rename_all = "snake_case")] +pub enum StabilitySpec { + /// A deprecated definition. + Deprecated, + /// An experimental definition. + Experimental, + /// A stable definition. + Stable, +} + +/// Implements a human readable display for the stability. +impl Display for StabilitySpec { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + StabilitySpec::Deprecated => write!(f, "deprecated"), + StabilitySpec::Experimental => write!(f, "experimental"), + StabilitySpec::Stable => write!(f, "stable"), + } + } +} diff --git a/crates/weaver_template/Cargo.toml b/crates/weaver_template/Cargo.toml new file mode 100644 index 00000000..11c7dd27 --- /dev/null +++ b/crates/weaver_template/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "weaver_template" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true +edition.workspace = true + +[dependencies] +weaver_logger = { path = "../weaver_logger" } +weaver_resolver = { path = "../weaver_resolver" } +weaver_schema = { path = "../weaver_schema" } +weaver_cache = { path = "../weaver_cache" } + +tera = "1.19.1" +textwrap = "0.16.0" +glob = "0.3.1" +convert_case = "0.6.0" +thread_local = "1.1.7" + +thiserror.workspace = true +serde.workspace = true +serde_yaml.workspace = true +rayon.workspace = true diff --git a/crates/weaver_template/src/config.rs b/crates/weaver_template/src/config.rs new file mode 100644 index 00000000..a69d4e1e --- /dev/null +++ b/crates/weaver_template/src/config.rs @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Configuration for the template crate. + +use std::cell::RefCell; +use std::collections::HashMap; +use std::path::Path; + +use convert_case::{Case, Casing}; +use serde::Deserialize; +use thread_local::ThreadLocal; + +use crate::Error; +use crate::Error::InvalidConfigFile; + +/// Case convention for naming of functions and structs. +#[derive(Deserialize, Debug)] +#[allow(clippy::enum_variant_names)] +pub enum CaseConvention { + #[serde(rename = "lowercase")] + LowerCase, + #[serde(rename = "UPPERCASE")] + UpperCase, + #[serde(rename = "PascalCase")] + PascalCase, + #[serde(rename = "camelCase")] + CamelCase, + #[serde(rename = "snake_case")] + SnakeCase, + #[serde(rename = "SCREAMING_SNAKE_CASE")] + ScreamingSnakeCase, + #[serde(rename = "kebab-case")] + KebabCase, + #[serde(rename = "SCREAMING-KEBAB-CASE")] + ScreamingKebabCase, +} + +/// Language specific configuration. +#[derive(Deserialize, Debug, Default)] +pub struct LanguageConfig { + /// Case convention used to name a file. + #[serde(default)] + pub file_name: CaseConvention, + /// Case convention used to name a function. + #[serde(default)] + pub function_name: CaseConvention, + /// Case convention used to name a function argument. + #[serde(default)] + pub arg_name: CaseConvention, + /// Case convention used to name a struct. + #[serde(default)] + pub struct_name: CaseConvention, + /// Case convention used to name a struct field. + #[serde(default)] + pub field_name: CaseConvention, + /// Type mapping for language specific types (OTel types -> Target language types). + #[serde(default)] + pub type_mapping: HashMap, +} + +/// Dynamic global configuration. +#[derive(Debug, Default)] +pub struct DynamicGlobalConfig { + /// File name for the current generated code. + pub file_name: ThreadLocal>>, +} + +impl Default for CaseConvention { + /// Default case convention is PascalCase + fn default() -> Self { + CaseConvention::PascalCase + } +} + +impl CaseConvention { + pub fn convert(&self, text: &str) -> String { + let text = text.replace('.', "_"); + match self { + CaseConvention::LowerCase => text.to_case(Case::Lower), + CaseConvention::UpperCase => text.to_case(Case::Upper), + CaseConvention::PascalCase => text.to_case(Case::Pascal), + CaseConvention::CamelCase => text.to_case(Case::Camel), + CaseConvention::SnakeCase => text.to_case(Case::Snake), + CaseConvention::ScreamingSnakeCase => text.to_case(Case::ScreamingSnake), + CaseConvention::KebabCase => text.to_case(Case::Kebab), + CaseConvention::ScreamingKebabCase => text.to_case(Case::Cobol), + } + } +} + +impl LanguageConfig { + pub fn try_new(lang_path: &Path) -> Result { + let config_file = lang_path.join("config.yaml"); + if config_file.exists() { + let reader = + std::fs::File::open(config_file.clone()).map_err(|e| InvalidConfigFile { + config_file: config_file.clone(), + error: e.to_string(), + })?; + serde_yaml::from_reader(reader).map_err(|e| InvalidConfigFile { + config_file: config_file.clone(), + error: e.to_string(), + }) + } else { + Ok(LanguageConfig::default()) + } + } +} + +impl DynamicGlobalConfig { + /// Set the file name for the current generated code. + /// This method uses a thread local variable to store the file name. + pub fn set(&self, file_name: &str) { + self.file_name + .get_or(|| RefCell::new(None)) + .borrow_mut() + .replace(file_name.to_string()); + } + + /// Get the file name for the current generated code. + /// This method uses a thread local variable to store the file name. + pub fn get(&self) -> Option { + self.file_name + .get_or(|| RefCell::new(None)) + .borrow() + .clone() + } + + /// Reset the file name for the current generated code. + /// This method uses a thread local variable to store the file name. + pub fn reset(&self) { + self.file_name + .get_or(|| RefCell::new(None)) + .borrow_mut() + .take(); + } +} diff --git a/crates/weaver_template/src/filters.rs b/crates/weaver_template/src/filters.rs new file mode 100644 index 00000000..bb62416a --- /dev/null +++ b/crates/weaver_template/src/filters.rs @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Custom Tera filters + +use std::collections::{BTreeMap, HashMap}; + +use tera::{try_get_value, Filter, Result, Value}; +use textwrap::{wrap, Options}; + +use crate::config::CaseConvention; + +/// Case converter filter. +pub struct CaseConverter { + filter_name: &'static str, + case: CaseConvention, +} + +impl CaseConverter { + /// Create a new case converter filter. + pub fn new(case: CaseConvention, filter_name: &'static str) -> Self { + CaseConverter { filter_name, case } + } +} + +/// Filter to convert a string to a specific case. +impl Filter for CaseConverter { + /// Convert a string to a specific case. + fn filter(&self, value: &Value, _: &HashMap) -> Result { + let text = try_get_value!(self.filter_name, "value", String, value); + Ok(Value::String(self.case.convert(&text))) + } +} + +/// Filter to normalize instrument name. +pub fn instrument(value: &Value, _: &HashMap) -> Result { + if let Value::String(metric_type) = value { + match metric_type.as_str() { + "counter" | "gauge" | "histogram" => Ok(Value::String(metric_type.clone())), + "updowncounter" => Ok(Value::String("up_down_counter".to_string())), + _ => Err(tera::Error::msg(format!( + "Filter instrument: unknown metric instrument {}", + metric_type + ))), + } + } else { + Err(tera::Error::msg(format!( + "Filter instrument: expected a string, got {:?}", + value + ))) + } +} + +/// Filter to deduplicate attributes from a list of values containing attributes. +/// The optional parameter `recursive` can be set to `true` to recursively search for attributes. +/// The default value is `false`. +/// +/// The result is a list of unique attributes sorted by their id or an empty list if no attributes +/// are found. +pub fn unique_attributes(value: &Value, ctx: &HashMap) -> Result { + let mut unique_attributes = BTreeMap::new(); + + let recursive = match ctx.get("recursive") { + Some(Value::Bool(v)) => *v, + _ => false, + }; + + fn visit_attributes( + value: &Value, + unique_attributes: &mut BTreeMap, + levels_to_visit: usize, + ) { + match value { + Value::Array(values) => { + if levels_to_visit == 0 { + return; + } + for value in values { + visit_attributes(value, unique_attributes, levels_to_visit - 1); + } + } + Value::Object(obj) => { + if levels_to_visit == 0 { + return; + } + for (field, value) in obj.iter() { + if field.eq("attributes") { + if let Value::Array(attrs) = value { + for attr in attrs { + if let Value::Object(map) = attr { + let id = map.get("id"); + if let Some(Value::String(id)) = id { + if unique_attributes.contains_key(id) { + // attribute already exists + continue; + } + unique_attributes.insert(id.clone(), attr.clone()); + } + } + } + } + } + visit_attributes(value, unique_attributes, levels_to_visit - 1); + } + } + _ => {} + } + } + + visit_attributes( + value, + &mut unique_attributes, + if recursive { usize::MAX } else { 1 }, + ); + let mut attributes = vec![]; + for attribute in unique_attributes.into_values() { + attributes.push(attribute); + } + Ok(Value::Array(attributes)) +} + +/// Filter out attributes that are not required. +pub fn required(value: &Value, _: &HashMap) -> Result { + let mut required_values = vec![]; + match value { + Value::Array(values) => { + for value in values { + match value { + Value::Object(map) => { + if let Some(Value::String(req_level)) = map.get("requirement_level") { + if req_level == "required" { + required_values.push(value.clone()); + } + } + } + _ => required_values.push(value.clone()), + } + } + } + _ => return Ok(value.clone()), + } + Ok(Value::Array(required_values)) +} + +/// Filter out attributes that are required. +pub fn not_required(value: &Value, _: &HashMap) -> Result { + let mut required_values = vec![]; + match value { + Value::Array(values) => { + for value in values { + match value { + Value::Object(map) => { + if let Some(Value::String(req_level)) = map.get("requirement_level") { + if req_level != "required" { + required_values.push(value.clone()); + } + } else { + required_values.push(value.clone()); + } + } + _ => required_values.push(value.clone()), + } + } + } + _ => return Ok(value.clone()), + } + Ok(Value::Array(required_values)) +} + +/// Transform a value into a quoted string, a number, or a boolean depending on the value type. +pub fn value(value: &Value, _: &HashMap) -> Result { + Ok(match value { + Value::Bool(v) => Value::String(v.to_string()), + Value::Number(v) => Value::String(v.to_string()), + Value::String(v) => Value::String(format!("\"{}\"", v)), + _ => value.clone(), + }) +} + +/// Filter out attributes without value. +pub fn with_value(value: &Value, _: &HashMap) -> Result { + let mut with_values = vec![]; + match value { + Value::Array(values) => { + for value in values { + match value { + Value::Object(map) => { + if map.get("value").is_some() { + with_values.push(value.clone()); + } + } + _ => with_values.push(value.clone()), + } + } + } + _ => return Ok(value.clone()), + } + Ok(Value::Array(with_values)) +} + +/// Filter out attributes with value. +pub fn without_value(value: &Value, _: &HashMap) -> Result { + let mut without_values = vec![]; + match value { + Value::Array(values) => { + for value in values { + match value { + Value::Object(map) => { + if map.get("value").is_none() { + without_values.push(value.clone()); + } + } + _ => without_values.push(value.clone()), + } + } + } + _ => return Ok(value.clone()), + } + Ok(Value::Array(without_values)) +} + +/// Retain only attributes with a valid enum type. +pub fn with_enum(value: &Value, _: &HashMap) -> Result { + let mut with_enums = vec![]; + match value { + Value::Array(attributes) => { + for attr in attributes { + match attr { + Value::Object(fields) => { + if let Some(Value::Object(type_value)) = fields.get("type") { + if type_value.get("members").is_some() { + with_enums.push(attr.clone()); + } + } + } + _ => with_enums.push(attr.clone()), + } + } + } + _ => return Ok(value.clone()), + } + + Ok(Value::Array(with_enums)) +} + +/// Retain only attributes without a enum type. +pub fn without_enum(value: &Value, _: &HashMap) -> Result { + let mut without_enums = vec![]; + match value { + Value::Array(attributes) => { + for attr in attributes { + match attr { + Value::Object(fields) => { + if let Some(Value::Object(type_value)) = fields.get("type") { + if type_value.get("members").is_none() { + without_enums.push(attr.clone()); + } + } else { + without_enums.push(attr.clone()); + } + } + _ => without_enums.push(attr.clone()), + } + } + } + _ => return Ok(value.clone()), + } + + Ok(Value::Array(without_enums)) +} + +/// Filter to map an OTel type to a language type. +pub struct TypeMapping { + pub type_mapping: HashMap, +} + +impl Filter for TypeMapping { + /// Map an OTel type to a language type. + fn filter(&self, value: &Value, ctx: &HashMap) -> Result { + match value { + Value::String(otel_type) => { + match self.type_mapping.get(otel_type) { + Some(language_type) => Ok(Value::String(language_type.clone())), + None => Err(tera::Error::msg(format!("Filter type_mapping: could not find a conversion for {}. To resolve this, create or extend the type_mapping in the config.yaml file.", otel_type))) + } + } + Value::Object(otel_enum) => { + if !otel_enum.contains_key("members") { + return Err(tera::Error::msg(format!("Filter type_mapping: expected an enum with a members array, got {:?}", value))) + } + let enum_name = match ctx.get("enum") { + Some(Value::String(v)) => v.clone(), + Some(_) => return Err(tera::Error::msg(format!("Filter type_mapping: expected a string for the enum parameter, got {:?}", ctx.get("enum")))), + _ => return Err(tera::Error::msg("Filter type_mapping: expected an enum parameter".to_string())) + }; + Ok(Value::String(enum_name)) + } + _ => Err(tera::Error::msg(format!("Filter type_mapping: expected a string or an object, got {:?}", value))) + } + } +} + +/// Creates a multiline comment from a string. +/// The `value` parameter is a string. +/// The `prefix` parameter is a string. +pub fn comment(value: &Value, ctx: &HashMap) -> Result { + fn wrap_comment(comment: &str, prefix: &str, lines: &mut Vec) { + wrap(comment.trim_end(), Options::new(80)) + .into_iter() + .map(|s| format!("{}{}", prefix, s.trim_end())) + .for_each(|s| lines.push(s)); + } + + let prefix = match ctx.get("prefix") { + Some(Value::String(prefix)) => prefix.clone(), + _ => "".to_string(), + }; + + let mut lines = vec![]; + match value { + Value::String(value) => wrap_comment(value, "", &mut lines), + Value::Array(values) => { + for value in values { + match value { + Value::String(value) => wrap_comment(value, "", &mut lines), + Value::Array(values) => { + for value in values { + if let Value::String(value) = value { + wrap_comment(value, "- ", &mut lines) + } + } + } + _ => {} + } + } + } + _ => {} + } + + let mut comments = String::new(); + for (i, line) in lines.into_iter().enumerate() { + if i > 0 { + comments.push_str(format!("\n{}", prefix).as_ref()); + } + comments.push_str(line.as_ref()); + } + Ok(Value::String(comments)) +} diff --git a/crates/weaver_template/src/functions.rs b/crates/weaver_template/src/functions.rs new file mode 100644 index 00000000..358db48a --- /dev/null +++ b/crates/weaver_template/src/functions.rs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Custom Tera functions + +use std::collections::HashMap; +use std::sync::Arc; + +use tera::Result; +use tera::{Function, Value}; + +use crate::config::DynamicGlobalConfig; + +#[derive(Debug)] +pub struct FunctionConfig { + config: Arc, +} + +impl FunctionConfig { + pub fn new(config: Arc) -> Self { + FunctionConfig { config } + } +} + +impl Function for FunctionConfig { + fn call(&self, args: &HashMap) -> Result { + if let Some(file_name) = args.get("file_name") { + self.config.set(file_name.as_str().unwrap()); + } + Ok(Value::Null) + } + + fn is_safe(&self) -> bool { + false + } +} diff --git a/crates/weaver_template/src/lib.rs b/crates/weaver_template/src/lib.rs new file mode 100644 index 00000000..d33efe3c --- /dev/null +++ b/crates/weaver_template/src/lib.rs @@ -0,0 +1,86 @@ +use std::path::PathBuf; + +mod config; +mod filters; +mod functions; +pub mod sdkgen; +mod testers; + +/// An error that can occur while generating a client SDK. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Invalid config file. + #[error("Invalid config file `{config_file}`: {error}")] + InvalidConfigFile { + /// Config file. + config_file: PathBuf, + /// Error message. + error: String, + }, + + /// Language not found. + #[error( + "Language `{0}` is not supported. Use the command `languages` to list supported languages." + )] + LanguageNotSupported(String), + + /// Invalid template directory. + #[error("Invalid template directory: {0}")] + InvalidTemplateDirectory(PathBuf), + + /// Invalid template file. + #[error("Invalid template file: {0}")] + InvalidTemplateFile(PathBuf), + + /// Invalid template. + #[error("{error}")] + InvalidTemplate { + /// Template directory. + template: PathBuf, + /// Error message. + error: String, + }, + + /// Invalid telemetry schema. + #[error("Invalid telemetry schema {schema}: {error}")] + InvalidTelemetrySchema { + /// Schema file. + schema: PathBuf, + /// Error message. + error: String, + }, + + /// Write generated code failed. + #[error("Writing of the generated code {template} failed: {error}")] + WriteGeneratedCodeFailed { + /// Template path. + template: PathBuf, + /// Error message. + error: String, + }, + + /// Internal error. + #[error("Internal error: {0}")] + InternalError(String), + + /// Template file name undefined. + #[error("File name undefined in the template `{template}`. To resolve this, use the function `config(file_name = )` to set the file name.")] + TemplateFileNameUndefined { + /// Template path. + template: PathBuf, + }, +} + +/// General configuration for the generator. +pub struct GeneratorConfig { + template_dir: PathBuf, +} + +impl Default for GeneratorConfig { + /// Create a new generator configuration with default values. + fn default() -> Self { + Self { + template_dir: PathBuf::from("templates"), + } + } +} diff --git a/crates/weaver_template/src/sdkgen.rs b/crates/weaver_template/src/sdkgen.rs new file mode 100644 index 00000000..4775df19 --- /dev/null +++ b/crates/weaver_template/src/sdkgen.rs @@ -0,0 +1,536 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Client SDK generator + +use std::error::Error; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::{fs, process}; + +use glob::{glob, Paths}; +use rayon::iter::IntoParallelIterator; +use rayon::iter::ParallelIterator; +use tera::{Context, Tera}; +use weaver_cache::Cache; + +use weaver_logger::Logger; +use weaver_resolver::SchemaResolver; +use weaver_schema::event::Event; +use weaver_schema::metric_group::MetricGroup; +use weaver_schema::span::Span; +use weaver_schema::univariate_metric::UnivariateMetric; +use weaver_schema::TelemetrySchema; + +use crate::config::{DynamicGlobalConfig, LanguageConfig}; +use crate::Error::{ + InternalError, InvalidTelemetrySchema, InvalidTemplate, InvalidTemplateDirectory, + InvalidTemplateFile, LanguageNotSupported, TemplateFileNameUndefined, WriteGeneratedCodeFailed, +}; +use crate::{filters, functions, testers, GeneratorConfig}; + +/// Client SDK generator +pub struct ClientSdkGenerator { + /// Language path + lang_path: PathBuf, + + /// Tera template engine + tera: Tera, + + /// Global configuration + config: Arc, +} + +/// A pair {template, object} to generate code for. +enum TemplateObjectPair<'a> { + Metric { + template: String, + metric: &'a UnivariateMetric, + }, + MetricGroup { + template: String, + metric_group: &'a MetricGroup, + }, + Event { + template: String, + event: &'a Event, + }, + Span { + template: String, + span: &'a Span, + }, + Other { + template: String, + relative_path: PathBuf, + object: &'a TelemetrySchema, + }, +} + +impl ClientSdkGenerator { + /// Create a new client SDK generator for the given language + /// or return an error if the language is not supported. + pub fn try_new(language: &str, config: GeneratorConfig) -> Result { + // Check if the language is supported + // A language is supported if a template directory exists for it. + let lang_path = config.template_dir.join(language); + + if !lang_path.exists() { + return Err(LanguageNotSupported(language.to_string())); + } + + let lang_dir_tree = match lang_path.to_str() { + None => { + return Err(InvalidTemplateDirectory(lang_path)); + } + Some(dir) => { + format!("{}/**/*.tera", dir) + } + }; + + let mut tera = match Tera::new(&lang_dir_tree) { + Ok(tera) => tera, + Err(e) => { + return Err(InvalidTemplate { + template: lang_path, + error: format!("{}", e), + }); + } + }; + + let lang_config = LanguageConfig::try_new(&lang_path)?; + + let config = Arc::new(DynamicGlobalConfig::default()); + + // Register custom filters + tera.register_filter( + "file_name", + filters::CaseConverter::new(lang_config.file_name, "file_name"), + ); + tera.register_filter( + "function_name", + filters::CaseConverter::new(lang_config.function_name, "function_name"), + ); + tera.register_filter( + "arg_name", + filters::CaseConverter::new(lang_config.arg_name, "arg_name"), + ); + tera.register_filter( + "struct_name", + filters::CaseConverter::new(lang_config.struct_name, "struct_name"), + ); + tera.register_filter( + "field_name", + filters::CaseConverter::new(lang_config.field_name, "field_name"), + ); + tera.register_filter("unique_attributes", filters::unique_attributes); + tera.register_filter("instrument", filters::instrument); + tera.register_filter("required", filters::required); + tera.register_filter("not_required", filters::not_required); + tera.register_filter("value", filters::value); + tera.register_filter("with_value", filters::with_value); + tera.register_filter("without_value", filters::without_value); + tera.register_filter("with_enum", filters::with_enum); + tera.register_filter("without_enum", filters::without_enum); + tera.register_filter("comment", filters::comment); + tera.register_filter( + "type_mapping", + filters::TypeMapping { + type_mapping: lang_config.type_mapping, + }, + ); + + // Register custom functions + tera.register_function("config", functions::FunctionConfig::new(config.clone())); + + // Register custom testers + tera.register_tester("required", testers::is_required); + tera.register_tester("not_required", testers::is_not_required); + + Ok(Self { + lang_path, + tera, + config, + }) + } + + /// Generate a client SDK for the given schema + pub fn generate( + &self, + log: impl Logger + Clone + Sync, + schema_path: PathBuf, + output_dir: PathBuf, + ) -> Result<(), crate::Error> { + let cache = Cache::try_new().unwrap_or_else(|e| { + log.error(&e.to_string()); + std::process::exit(1); + }); + + let schema = SchemaResolver::resolve_schema_file(schema_path.clone(), &cache, log.clone()) + .map_err(|e| InvalidTelemetrySchema { + schema: schema_path.clone(), + error: format!("{}", e), + })?; + + // Process recursively all files in the template directory + let mut lang_path = self.lang_path.to_str().unwrap_or_default().to_string(); + let paths = if lang_path.is_empty() { + glob("**/*.tera").map_err(|e| InternalError(e.to_string()))? + } else { + lang_path.push_str("/**/*.tera"); + glob(lang_path.as_str()).map_err(|e| InternalError(e.to_string()))? + }; + + // Build the list of all {template, object} pairs to generate code for + // and process them in parallel. + // All pairs are independent from each other so we can process them in parallel. + self.list_all_templates(&schema, paths)? + .into_par_iter() + .try_for_each(|pair| { + match pair { + TemplateObjectPair::Metric { template, metric } => self.process_metric( + log.clone(), + &template, + &schema_path, + metric, + &output_dir, + ), + TemplateObjectPair::MetricGroup { + template, + metric_group, + } => self.process_metric_group( + log.clone(), + &template, + &schema_path, + metric_group, + &output_dir, + ), + TemplateObjectPair::Event { template, event } => { + self.process_event(log.clone(), &template, &schema_path, event, &output_dir) + } + TemplateObjectPair::Span { template, span } => { + self.process_span(log.clone(), &template, &schema_path, span, &output_dir) + } + TemplateObjectPair::Other { + template, + relative_path, + object, + } => { + // Process other templates + let context = &Context::from_serialize(object).map_err(|e| { + InvalidTelemetrySchema { + schema: schema_path.clone(), + error: format!("{}", e), + } + })?; + + log.loading(&format!("Generating file {}", template)); + let generated_code = self.generate_code(log.clone(), &template, context)?; + let relative_path = relative_path.to_path_buf(); + let generated_file = + Self::save_generated_code(&output_dir, relative_path, generated_code)?; + log.success(&format!("Generated file {:?}", generated_file)); + Ok(()) + } + } + })?; + + Ok(()) + } + + /// Lists all {template, object} pairs derived from a template directory and a given + /// schema specification. + fn list_all_templates<'a>( + &self, + schema: &'a TelemetrySchema, + paths: Paths, + ) -> Result>, crate::Error> { + let mut templates = Vec::new(); + if let Some(schema_spec) = &schema.schema { + for entry in paths { + if let Ok(tmpl_file_path) = entry { + if tmpl_file_path.is_dir() { + continue; + } + let relative_path = tmpl_file_path.strip_prefix(&self.lang_path).unwrap(); + let tmpl_file = relative_path + .to_str() + .ok_or(InvalidTemplateFile(tmpl_file_path.clone()))?; + + if tmpl_file.ends_with(".macro.tera") { + // Macro files are not templates. + // They are included in other templates. + // So we skip them. + continue; + } + + match tmpl_file_path.file_stem().and_then(|s| s.to_str()) { + Some("metric") => { + if let Some(resource_metrics) = schema_spec.resource_metrics.as_ref() { + for metric in resource_metrics.metrics.iter() { + templates.push(TemplateObjectPair::Metric { + template: tmpl_file.into(), + metric, + }) + } + } + } + Some("metric_group") => { + if let Some(resource_metrics) = schema_spec.resource_metrics.as_ref() { + for metric_group in resource_metrics.metric_groups.iter() { + templates.push(TemplateObjectPair::MetricGroup { + template: tmpl_file.into(), + metric_group, + }) + } + } + } + Some("event") => { + if let Some(events) = schema_spec.resource_events.as_ref() { + for event in events.events.iter() { + templates.push(TemplateObjectPair::Event { + template: tmpl_file.into(), + event, + }) + } + } + } + Some("span") => { + if let Some(spans) = schema_spec.resource_spans.as_ref() { + for span in spans.spans.iter() { + templates.push(TemplateObjectPair::Span { + template: tmpl_file.into(), + span, + }) + } + } + } + _ => { + // Remove the `tera` extension from the relative path + let mut relative_path = relative_path.to_path_buf(); + relative_path.set_extension(""); + + templates.push(TemplateObjectPair::Other { + template: tmpl_file.into(), + relative_path, + object: schema, + }) + } + } + } else { + return Err(InvalidTemplateDirectory(self.lang_path.clone())); + } + } + } + Ok(templates) + } + + /// Generate code. + fn generate_code( + &self, + log: impl Logger, + tmpl_file: &str, + context: &Context, + ) -> Result { + let generated_code = self.tera.render(tmpl_file, context).unwrap_or_else(|err| { + log.newline(1); + log.error(&format!("{}", err)); + let mut cause = err.source(); + while let Some(e) = cause { + log.error(&format!("- caused by: {}", e)); + cause = e.source(); + } + process::exit(1); + }); + + Ok(generated_code) + } + + /// Save the generated code to the output directory. + fn save_generated_code( + output_dir: &Path, + relative_path: PathBuf, + generated_code: String, + ) -> Result { + // Create all intermediary directories if they don't exist + let output_file_path = output_dir.join(relative_path); + if let Some(parent_dir) = output_file_path.parent() { + if let Err(e) = fs::create_dir_all(parent_dir) { + return Err(WriteGeneratedCodeFailed { + template: output_file_path.clone(), + error: format!("{}", e), + }); + } + } + + // Write the generated code to the output directory + fs::write(output_file_path.clone(), generated_code).map_err(|e| { + WriteGeneratedCodeFailed { + template: output_file_path.clone(), + error: format!("{}", e), + } + })?; + + Ok(output_file_path) + } + + /// Process an univariate metric. + fn process_metric( + &self, + log: impl Logger + Clone, + tmpl_file: &str, + schema_path: &Path, + metric: &UnivariateMetric, + output_dir: &Path, + ) -> Result<(), crate::Error> { + if let UnivariateMetric::Metric { name, .. } = metric { + let context = &Context::from_serialize(metric).map_err(|e| InvalidTelemetrySchema { + schema: schema_path.to_path_buf(), + error: format!("{}", e), + })?; + + // Reset the config + self.config.reset(); + + log.loading(&format!("Generating code for univariate metric `{}`", name)); + let generated_code = self.generate_code(log.clone(), tmpl_file, context)?; + + // Retrieve the file name from the config + let relative_path = { + match &self.config.get() { + None => { + return Err(TemplateFileNameUndefined { + template: PathBuf::from(tmpl_file), + }); + } + Some(file_name) => PathBuf::from(file_name.clone()), + } + }; + + // Save the generated code to the output directory + let generated_file = + Self::save_generated_code(output_dir, relative_path, generated_code)?; + log.success(&format!("Generated file {:?}", generated_file)); + } + + Ok(()) + } + + /// Process a metric group (multivariate). + fn process_metric_group( + &self, + log: impl Logger + Clone, + tmpl_file: &str, + schema_path: &Path, + metric: &MetricGroup, + output_dir: &Path, + ) -> Result<(), crate::Error> { + let context = &Context::from_serialize(metric).map_err(|e| InvalidTelemetrySchema { + schema: schema_path.to_path_buf(), + error: format!("{}", e), + })?; + + // Reset the config + self.config.reset(); + + log.loading(&format!( + "Generating code for multivariate metric `{}`", + metric.name + )); + let generated_code = self.generate_code(log.clone(), tmpl_file, context)?; + + // Retrieve the file name from the config + let relative_path = { + match self.config.get() { + None => { + return Err(TemplateFileNameUndefined { + template: PathBuf::from(tmpl_file), + }); + } + Some(file_name) => PathBuf::from(file_name.clone()), + } + }; + + // Save the generated code to the output directory + let generated_file = Self::save_generated_code(output_dir, relative_path, generated_code)?; + log.success(&format!("Generated file {:?}", generated_file)); + + Ok(()) + } + + /// Process an event. + fn process_event( + &self, + log: impl Logger + Clone, + tmpl_file: &str, + schema_path: &Path, + event: &Event, + output_dir: &Path, + ) -> Result<(), crate::Error> { + let context = &Context::from_serialize(event).map_err(|e| InvalidTelemetrySchema { + schema: schema_path.to_path_buf(), + error: format!("{}", e), + })?; + + // Reset the config + self.config.reset(); + + log.loading(&format!("Generating code for log `{}`", event.event_name)); + let generated_code = self.generate_code(log.clone(), tmpl_file, context)?; + + // Retrieve the file name from the config + let relative_path = { + match self.config.get() { + None => { + return Err(TemplateFileNameUndefined { + template: PathBuf::from(tmpl_file), + }); + } + Some(file_name) => PathBuf::from(file_name.clone()), + } + }; + + // Save the generated code to the output directory + let generated_file = Self::save_generated_code(output_dir, relative_path, generated_code)?; + log.success(&format!("Generated file {:?}", generated_file)); + + Ok(()) + } + + /// Process a span. + fn process_span( + &self, + log: impl Logger + Clone, + tmpl_file: &str, + schema_path: &Path, + span: &Span, + output_dir: &Path, + ) -> Result<(), crate::Error> { + let context = &Context::from_serialize(span).map_err(|e| InvalidTelemetrySchema { + schema: schema_path.to_path_buf(), + error: format!("{}", e), + })?; + + // Reset the config + self.config.reset(); + + log.loading(&format!("Generating code for span `{}`", span.span_name)); + let generated_code = self.generate_code(log.clone(), tmpl_file, context)?; + + // Retrieve the file name from the config + let relative_path = { + match self.config.get() { + None => { + return Err(TemplateFileNameUndefined { + template: PathBuf::from(tmpl_file), + }); + } + Some(file_name) => PathBuf::from(file_name.clone()), + } + }; + + // Save the generated code to the output directory + let generated_file = Self::save_generated_code(output_dir, relative_path, generated_code)?; + log.success(&format!("Generated file {:?}", generated_file)); + + Ok(()) + } +} diff --git a/crates/weaver_template/src/testers.rs b/crates/weaver_template/src/testers.rs new file mode 100644 index 00000000..fbbe4c8f --- /dev/null +++ b/crates/weaver_template/src/testers.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Custom testers + +use tera::Value; + +pub fn is_required(value: Option<&Value>, _args: &[Value]) -> tera::Result { + if let Some(Value::Object(map)) = value { + if let Some(Value::String(req_level)) = map.get("requirement_level") { + if req_level == "required" { + return Ok(true); + } + } + } + Ok(false) +} + +pub fn is_not_required(value: Option<&Value>, _args: &[Value]) -> tera::Result { + if let Some(Value::Object(map)) = value { + if let Some(Value::String(req_level)) = map.get("requirement_level") { + if req_level == "required" { + return Ok(false); + } + } + } + Ok(true) +} diff --git a/crates/weaver_version/Cargo.toml b/crates/weaver_version/Cargo.toml new file mode 100644 index 00000000..611c3b89 --- /dev/null +++ b/crates/weaver_version/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "weaver_version" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +license.workspace = true +publish.workspace = true +edition.workspace = true + +[dependencies] +serde.workspace = true +serde_yaml.workspace = true +thiserror.workspace = true + +semver = {version = "1.0.21", features = ["serde"]} diff --git a/crates/weaver_version/data/app_versions.yaml b/crates/weaver_version/data/app_versions.yaml new file mode 100644 index 00000000..049aa2f1 --- /dev/null +++ b/crates/weaver_version/data/app_versions.yaml @@ -0,0 +1,35 @@ +# Override the transformations defined in a parent versions file. + +versions: + 1.22.0: + spans: + changes: + - rename_attributes: + attribute_map: + messaging.kafka.client_id: messaging.client.id + 1.8.0: + logs: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: database.name + spans: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: database.name + resources: + changes: + - rename_attributes: + attribute_map: + db.cassandra.db: database.name + metrics: + changes: + - rename_metrics: + m2: metric2 + 1.7.1: + spans: + changes: + - rename_attributes: + attribute_map: + db.cassandra.table: database.table diff --git a/crates/weaver_version/data/parent_versions.yaml b/crates/weaver_version/data/parent_versions.yaml new file mode 100644 index 00000000..1ee0c794 --- /dev/null +++ b/crates/weaver_version/data/parent_versions.yaml @@ -0,0 +1,147 @@ +versions: + 1.21.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3336 + - rename_attributes: + attribute_map: + messaging.kafka.client_id: messaging.client_id + messaging.rocketmq.client_id: messaging.client_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3402 + - rename_attributes: + attribute_map: + # net.peer.(name|port) attributes were usually populated on client side + # so they should be usually translated to server.(address|port) + # net.host.* attributes were only populated on server side + net.host.name: server.address + net.host.port: server.port + # was only populated on client side + net.sock.peer.name: server.socket.domain + # net.sock.peer.(addr|port) mapping is not possible + # since they applied to both client and server side + # were only populated on server side + net.sock.host.addr: server.socket.address + net.sock.host.port: server.socket.port + http.client_ip: client.address + # https://github.com/open-telemetry/opentelemetry-specification/pull/3426 + - rename_attributes: + attribute_map: + net.protocol.name: network.protocol.name + net.protocol.version: network.protocol.version + net.host.connection.type: network.connection.type + net.host.connection.subtype: network.connection.subtype + net.host.carrier.name: network.carrier.name + net.host.carrier.mcc: network.carrier.mcc + net.host.carrier.mnc: network.carrier.mnc + net.host.carrier.icc: network.carrier.icc + # https://github.com/open-telemetry/opentelemetry-specification/pull/3355 + - rename_attributes: + attribute_map: + http.method: http.request.method + http.status_code: http.response.status_code + http.scheme: url.scheme + http.url: url.full + http.request_content_length: http.request.body.size + http.response_content_length: http.response.body.size + metrics: + changes: + # https://github.com/open-telemetry/semantic-conventions/pull/53 + - rename_metrics: + process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization + 1.20.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3272 + - rename_attributes: + attribute_map: + net.app.protocol.name: net.protocol.name + net.app.protocol.version: net.protocol.version + 1.19.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3209 + - rename_attributes: + attribute_map: + faas.execution: faas.invocation_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3188 + - rename_attributes: + attribute_map: + faas.id: cloud.resource_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3190 + - rename_attributes: + attribute_map: + http.user_agent: user_agent.original + resources: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3190 + - rename_attributes: + attribute_map: + browser.user_agent: user_agent.original + 1.18.0: + 1.17.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2957 + - rename_attributes: + attribute_map: + messaging.consumer_id: messaging.consumer.id + messaging.protocol: net.app.protocol.name + messaging.protocol_version: net.app.protocol.version + messaging.destination: messaging.destination.name + messaging.temp_destination: messaging.destination.temporary + messaging.destination_kind: messaging.destination.kind + messaging.message_id: messaging.message.id + messaging.conversation_id: messaging.message.conversation_id + messaging.message_payload_size_bytes: messaging.message.payload_size_bytes + messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes + messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key + messaging.kafka.message_key: messaging.kafka.message.key + messaging.kafka.partition: messaging.kafka.destination.partition + messaging.kafka.tombstone: messaging.kafka.message.tombstone + messaging.rocketmq.message_type: messaging.rocketmq.message.type + messaging.rocketmq.message_tag: messaging.rocketmq.message.tag + messaging.rocketmq.message_keys: messaging.rocketmq.message.keys + messaging.kafka.consumer_group: messaging.kafka.consumer.group + 1.16.0: + 1.15.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2743 + - rename_attributes: + attribute_map: + http.retry_count: http.resend_count + 1.14.0: + 1.13.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2614 + - rename_attributes: + attribute_map: + net.peer.ip: net.sock.peer.addr + net.host.ip: net.sock.host.addr + 1.12.0: + 1.11.0: + 1.10.0: + 1.9.0: + 1.8.0: + logs: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: db.name + db.hbase.namespace: db.name + spans: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: db.name + db.hbase.namespace: db.name + metrics: + changes: + - rename_metrics: + m1: metric_1 + m2: metric_2 + 1.7.0: + 1.6.1: + 1.5.0: + 1.4.0: \ No newline at end of file diff --git a/crates/weaver_version/src/lib.rs b/crates/weaver_version/src/lib.rs new file mode 100644 index 00000000..f0cf36a8 --- /dev/null +++ b/crates/weaver_version/src/lib.rs @@ -0,0 +1,766 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! The specification of the changes to apply to the schema for different versions. + +#![deny(missing_docs)] +#![deny(clippy::print_stdout)] +#![deny(clippy::print_stderr)] + +use std::collections::{BTreeMap, HashMap}; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +use serde::{Deserialize, Serialize}; + +use crate::logs_change::LogsChange; +use crate::logs_version::LogsVersion; +use crate::metrics_change::MetricsChange; +use crate::metrics_version::MetricsVersion; +use crate::resource_change::ResourceChange; +use crate::resource_version::ResourceVersion; +use crate::spans_change::SpansChange; +use crate::spans_version::SpansVersion; + +pub mod logs_change; +pub mod logs_version; +pub mod metrics_change; +pub mod metrics_version; +pub mod resource_change; +pub mod resource_version; +pub mod spans_change; +pub mod spans_version; + +/// An error that can occur while loading or resolving version changes. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// The `versions` file was not found. + #[error("Versions {path_or_url:?} not found\n{error:?}")] + VersionsNotFound { + /// The path or URL of the `versions` file. + path_or_url: String, + /// The error that occurred. + error: String, + }, + + /// The `versions` file is invalid. + #[error("Invalid versions {path_or_url:?}\n{error:?}")] + InvalidVersions { + /// The path or URL of the `versions` file. + path_or_url: String, + /// The line number where the error occurred. + line: Option, + /// The column number where the error occurred. + column: Option, + /// The error that occurred. + error: String, + }, +} + +/// List of versions with their changes. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(transparent)] +pub struct Versions { + versions: BTreeMap, +} + +/// An history of changes to apply to the schema for different versions. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub struct VersionSpec { + /// The changes to apply to the metrics specification for a specific version. + pub metrics: Option, + /// The changes to apply to the logs specification for a specific version. + pub logs: Option, + /// The changes to apply to the spans specification for a specific version. + pub spans: Option, + /// The changes to apply to the resource specification for a specific version. + pub resources: Option, +} + +/// The changes to apply to rename attributes and metrics for +/// a specific version. +#[derive(Default)] +pub struct VersionChanges { + metric_old_to_new_names: HashMap, + metric_old_to_new_attributes: HashMap, + resource_old_to_new_attributes: HashMap, + log_old_to_new_attributes: HashMap, + span_old_to_new_attributes: HashMap, +} + +/// A trait to get the new name of an attribute of a resource, log or span. +pub trait VersionAttributeChanges { + /// Returns the new name of the given attribute or the given name if the attribute + /// has not been renamed. + fn get_attribute_name(&self, name: &str) -> String; +} + +impl Versions { + /// Loads a `versions` file and returns an instance of `Versions` if successful + /// or an error if the file could not be loaded or deserialized. + pub fn load_from_file>(path: P) -> Result { + /// Versions has a transparent serde representation so we need to define a top-level + /// struct to deserialize the `versions` file. + #[derive(Serialize, Deserialize, Debug)] + struct TopLevel { + versions: Versions, + } + + let path_buf = path.as_ref().to_path_buf(); + + // Load and deserialize the telemetry schema + let versions_file = File::open(path).map_err(|e| Error::VersionsNotFound { + path_or_url: path_buf.as_path().display().to_string(), + error: e.to_string(), + })?; + let top_level: TopLevel = + serde_yaml::from_reader(BufReader::new(versions_file)).map_err(|e| { + Error::InvalidVersions { + path_or_url: path_buf.as_path().display().to_string(), + line: e.location().map(|loc| loc.line()), + column: e.location().map(|loc| loc.column()), + error: e.to_string(), + } + })?; + Ok(top_level.versions) + } + + /// Returns the most recent version or None if there are no versions. + pub fn latest_version(&self) -> Option<&semver::Version> { + self.versions.keys().last() + } + + /// Returns a vector of tuples containing the versions and their corresponding changes + /// in ascending order. + pub fn versions_asc(&self) -> Vec<(&semver::Version, &VersionSpec)> { + self.versions.iter().collect() + } + + /// Returns a vector of tuples containing the versions and their corresponding changes + /// in descending order. + pub fn versions_desc(&self) -> Vec<(&semver::Version, &VersionSpec)> { + self.versions.iter().rev().collect() + } + + /// Returns a vector of tuples containing the versions and their corresponding changes + /// in ascending order from the given version. + pub fn versions_asc_from( + &self, + version: &semver::Version, + ) -> Vec<(&semver::Version, &VersionSpec)> { + self.versions.range(version.clone()..).collect() + } + + /// Returns a vector of tuples containing the versions and their corresponding changes + /// in descending order from the given version. + pub fn versions_desc_from( + &self, + version: &semver::Version, + ) -> Vec<(&semver::Version, &VersionSpec)> { + self.versions.range(..=version.clone()).rev().collect() + } + + /// Returns the changes to apply for the given version including the changes + /// of the previous versions. + /// The current supported changes are: + /// - Renaming of attributes (for resources, logs and spans) + /// - Renaming of metrics + pub fn version_changes_for(&self, version: &semver::Version) -> VersionChanges { + let mut resource_old_to_new_attributes: HashMap = HashMap::new(); + let mut metric_old_to_new_names: HashMap = HashMap::new(); + let mut metric_old_to_new_attributes: HashMap = HashMap::new(); + let mut log_old_to_new_attributes: HashMap = HashMap::new(); + let mut span_old_to_new_attributes: HashMap = HashMap::new(); + + for (_, spec) in self.versions_desc_from(version) { + // Builds a map of old to new attribute names for the attributes that have been renamed + // in the different versions of the resources. + if let Some(resources) = spec.resources.as_ref() { + resources + .changes + .iter() + .flat_map(|change| change.rename_attributes.attribute_map.iter()) + .for_each(|(old_name, new_name)| { + if !resource_old_to_new_attributes.contains_key(old_name) { + resource_old_to_new_attributes + .insert(old_name.clone(), new_name.clone()); + } + }); + } + + // Builds a map of old to new metric names that have been renamed + // in the different versions. + if let Some(metrics) = spec.metrics.as_ref() { + metrics + .changes + .iter() + .flat_map(|change| change.rename_metrics.iter()) + .for_each(|(old_name, new_name)| { + if !metric_old_to_new_names.contains_key(old_name) { + metric_old_to_new_names.insert(old_name.clone(), new_name.clone()); + } + }); + } + + // Builds a map of old to new attribute names for the attributes that have been renamed + // in the different versions of the metrics. + if let Some(metrics) = spec.metrics.as_ref() { + metrics + .changes + .iter() + .flat_map(|change| change.rename_attributes.attribute_map.iter()) + .for_each(|(old_name, new_name)| { + if !metric_old_to_new_attributes.contains_key(old_name) { + metric_old_to_new_attributes.insert(old_name.clone(), new_name.clone()); + } + }); + } + + // Builds a map of old to new attribute names for the attributes that have been renamed + // in the different versions of the logs. + if let Some(logs) = spec.logs.as_ref() { + logs.changes + .iter() + .flat_map(|change| change.rename_attributes.attribute_map.iter()) + .for_each(|(old_name, new_name)| { + if !log_old_to_new_attributes.contains_key(old_name) { + log_old_to_new_attributes.insert(old_name.clone(), new_name.clone()); + } + }); + } + + // Builds a map of old to new attribute names for the attributes that have been renamed + // in the different versions of the spans. + if let Some(spans) = spec.spans.as_ref() { + spans + .changes + .iter() + .flat_map(|change| change.rename_attributes.attribute_map.iter()) + .for_each(|(old_name, new_name)| { + if !span_old_to_new_attributes.contains_key(old_name) { + span_old_to_new_attributes.insert(old_name.clone(), new_name.clone()); + } + }); + } + } + + VersionChanges { + resource_old_to_new_attributes, + metric_old_to_new_attributes, + metric_old_to_new_names, + log_old_to_new_attributes, + span_old_to_new_attributes, + } + } + + /// Update the current `Versions` to include the transformations of the parent `Versions`. + /// Transformations of the current `Versions` take precedence over the parent `Versions`. + pub fn extend(&mut self, parent_versions: Versions) { + for (version, spec) in parent_versions.versions.into_iter() { + match self.versions.get_mut(&version) { + Some(current_spec) => { + current_spec.extend(spec); + } + None => { + self.versions.insert(version.clone(), spec); + } + } + } + } + + /// Returns true if the `Versions` is empty. + pub fn is_empty(&self) -> bool { + self.versions.is_empty() + } + + /// Returns the number of versions. + pub fn len(&self) -> usize { + self.versions.len() + } +} + +impl VersionSpec { + /// Update the current `VersionSpec` to include the transformations of the parent `VersionSpec`. + /// Transformations of the current `VersionSpec` take precedence over the parent `VersionSpec`. + pub fn extend(&mut self, parent_spec: VersionSpec) { + // Process resources + if let Some(resources) = parent_spec.resources { + let mut resource_change = ResourceChange::default(); + for change in resources.changes { + 'next_parent_renaming: for (old, new) in change.rename_attributes.attribute_map { + for local_change in self + .resources + .get_or_insert_with(ResourceVersion::default) + .changes + .iter() + { + if local_change + .rename_attributes + .attribute_map + .contains_key(&old) + { + // renaming already present in local changes, skip it + continue 'next_parent_renaming; + } + } + // renaming not found in local changes, add it + resource_change + .rename_attributes + .attribute_map + .insert(old, new); + } + } + if !resource_change.rename_attributes.attribute_map.is_empty() { + if self + .resources + .get_or_insert_with(ResourceVersion::default) + .changes + .is_empty() + { + self.resources + .get_or_insert_with(ResourceVersion::default) + .changes + .push(resource_change); + } else { + self.resources + .get_or_insert_with(ResourceVersion::default) + .changes[0] + .rename_attributes + .attribute_map + .extend(resource_change.rename_attributes.attribute_map); + } + } + } + + // Process metrics + if let Some(metrics) = parent_spec.metrics { + let mut metrics_change = MetricsChange::default(); + for change in metrics.changes { + 'next_parent_renaming: for (old, new) in change.rename_metrics { + for local_change in self + .metrics + .get_or_insert_with(MetricsVersion::default) + .changes + .iter() + { + if local_change.rename_metrics.contains_key(&old) { + // renaming already present in local changes, skip it + continue 'next_parent_renaming; + } + } + // renaming not found in local changes, add it + metrics_change.rename_metrics.insert(old, new); + } + } + if !metrics_change.rename_metrics.is_empty() { + if self + .metrics + .get_or_insert_with(MetricsVersion::default) + .changes + .is_empty() + { + self.metrics + .get_or_insert_with(MetricsVersion::default) + .changes + .push(metrics_change); + } else { + self.metrics + .get_or_insert_with(MetricsVersion::default) + .changes[0] + .rename_metrics + .extend(metrics_change.rename_metrics); + } + } + } + + // Process logs + if let Some(logs) = parent_spec.logs { + let mut logs_change = LogsChange::default(); + for change in logs.changes { + 'next_parent_renaming: for (old, new) in change.rename_attributes.attribute_map { + for local_change in self + .logs + .get_or_insert_with(LogsVersion::default) + .changes + .iter() + { + if local_change + .rename_attributes + .attribute_map + .contains_key(&old) + { + // renaming already present in local changes, skip it + continue 'next_parent_renaming; + } + } + // renaming not found in local changes, add it + logs_change.rename_attributes.attribute_map.insert(old, new); + } + } + if !logs_change.rename_attributes.attribute_map.is_empty() { + if self + .logs + .get_or_insert_with(LogsVersion::default) + .changes + .is_empty() + { + self.logs + .get_or_insert_with(LogsVersion::default) + .changes + .push(logs_change); + } else { + self.logs.get_or_insert_with(LogsVersion::default).changes[0] + .rename_attributes + .attribute_map + .extend(logs_change.rename_attributes.attribute_map); + } + } + } + + // Process spans + if let Some(spans) = parent_spec.spans { + let mut spans_change = SpansChange::default(); + for change in spans.changes { + 'next_parent_renaming: for (old, new) in change.rename_attributes.attribute_map { + for local_change in self + .spans + .get_or_insert_with(SpansVersion::default) + .changes + .iter() + { + if local_change + .rename_attributes + .attribute_map + .contains_key(&old) + { + // renaming already present in local changes, skip it + continue 'next_parent_renaming; + } + } + // renaming not found in local changes, add it + spans_change + .rename_attributes + .attribute_map + .insert(old, new); + } + } + if !spans_change.rename_attributes.attribute_map.is_empty() { + if self + .spans + .get_or_insert_with(SpansVersion::default) + .changes + .is_empty() + { + self.spans + .get_or_insert_with(SpansVersion::default) + .changes + .push(spans_change); + } else { + self.spans.get_or_insert_with(SpansVersion::default).changes[0] + .rename_attributes + .attribute_map + .extend(spans_change.rename_attributes.attribute_map); + } + } + } + } +} + +/// Wrapper around `VersionChanges` to get the new name of an attribute of resources. +pub struct ResourcesVersionAttributeChanges<'a> { + version_changes: &'a VersionChanges, +} + +impl<'a> VersionAttributeChanges for ResourcesVersionAttributeChanges<'a> { + /// Returns the new name of the given resource attribute or the given name if the attribute + /// has not been renamed. + fn get_attribute_name(&self, name: &str) -> String { + self.version_changes.get_resource_attribute_name(name) + } +} + +/// Wrapper around `VersionChanges` to get the new name of an attribute of metrics. +pub struct MetricsVersionAttributeChanges<'a> { + version_changes: &'a VersionChanges, +} + +impl<'a> VersionAttributeChanges for crate::MetricsVersionAttributeChanges<'a> { + /// Returns the new name of the given metric attribute or the given name if the attribute + /// has not been renamed. + fn get_attribute_name(&self, name: &str) -> String { + self.version_changes.get_metric_attribute_name(name) + } +} + +/// Wrapper around `VersionChanges` to get the new name of an attribute of logs. +pub struct LogsVersionAttributeChanges<'a> { + version_changes: &'a VersionChanges, +} + +impl<'a> VersionAttributeChanges for crate::LogsVersionAttributeChanges<'a> { + /// Returns the new name of the given log attribute or the given name if the attribute + /// has not been renamed. + fn get_attribute_name(&self, name: &str) -> String { + self.version_changes.get_log_attribute_name(name) + } +} + +/// Wrapper around `VersionChanges` to get the new name of an attribute of spans. +pub struct SpansVersionAttributeChanges<'a> { + version_changes: &'a VersionChanges, +} + +impl<'a> VersionAttributeChanges for crate::SpansVersionAttributeChanges<'a> { + /// Returns the new name of the given span attribute or the given name if the attribute + /// has not been renamed. + fn get_attribute_name(&self, name: &str) -> String { + self.version_changes.get_span_attribute_name(name) + } +} + +impl VersionChanges { + /// Returns the attribute changes to apply to the resources. + pub fn resource_attribute_changes(&self) -> impl VersionAttributeChanges + '_ { + ResourcesVersionAttributeChanges { + version_changes: self, + } + } + + /// Returns the attribute changes to apply to the metrics. + pub fn metric_attribute_changes(&self) -> impl VersionAttributeChanges + '_ { + MetricsVersionAttributeChanges { + version_changes: self, + } + } + + /// Returns the attribute changes to apply to the logs. + pub fn log_attribute_changes(&self) -> impl VersionAttributeChanges + '_ { + LogsVersionAttributeChanges { + version_changes: self, + } + } + + /// Returns the attribute changes to apply to the spans. + pub fn span_attribute_changes(&self) -> impl VersionAttributeChanges + '_ { + SpansVersionAttributeChanges { + version_changes: self, + } + } + + /// Returns the new name of the given resource attribute or the given name if the attribute + /// has not been renamed. + pub fn get_resource_attribute_name(&self, name: &str) -> String { + if let Some(new_name) = self.resource_old_to_new_attributes.get(name) { + new_name.clone() + } else { + name.to_string() + } + } + + /// Returns the new name of the given metric attribute or the given name if the attribute + /// has not been renamed. + pub fn get_metric_attribute_name(&self, name: &str) -> String { + if let Some(new_name) = self.metric_old_to_new_attributes.get(name) { + new_name.clone() + } else { + name.to_string() + } + } + + /// Returns the new name of the given metric or the given name if the metric + /// has not been renamed. + pub fn get_metric_name(&self, name: &str) -> String { + if let Some(new_name) = self.metric_old_to_new_names.get(name) { + new_name.clone() + } else { + name.to_string() + } + } + + /// Returns the new name of the given log attribute or the given name if the attribute + /// has not been renamed. + pub fn get_log_attribute_name(&self, name: &str) -> String { + if let Some(new_name) = self.log_old_to_new_attributes.get(name) { + new_name.clone() + } else { + name.to_string() + } + } + + /// Returns the new name of the given span attribute or the given name if the attribute + /// has not been renamed. + pub fn get_span_attribute_name(&self, name: &str) -> String { + if let Some(new_name) = self.span_old_to_new_attributes.get(name) { + new_name.clone() + } else { + name.to_string() + } + } +} + +#[cfg(test)] +mod tests { + use crate::Versions; + + #[test] + fn test_ordering() { + let versions: Versions = Versions::load_from_file("data/parent_versions.yaml").unwrap(); + let mut version = None; + + for (v, _) in versions.versions_asc() { + if version.is_some() { + assert!(v > version.unwrap()); + } + version = Some(v); + } + + let mut version = None; + + for (v, _) in versions.versions_desc() { + if version.is_some() { + assert!(v < version.unwrap()); + } + version = Some(v); + } + } + + #[test] + fn test_version_changes_for() { + let versions: Versions = Versions::load_from_file("data/parent_versions.yaml").unwrap(); + let changes = versions.version_changes_for(versions.latest_version().unwrap()); + + // Test renaming of resource attributes + assert_eq!( + "user_agent.original", + changes.get_resource_attribute_name("browser.user_agent") + ); + + // Test renaming of metric names + assert_eq!( + "process.runtime.jvm.cpu.recent_utilization", + changes.get_metric_name("process.runtime.jvm.cpu.utilization") + ); + + // Test renaming of span attributes + assert_eq!( + "user_agent.original", + changes.get_span_attribute_name("http.user_agent") + ); + assert_eq!( + "http.request.method", + changes.get_span_attribute_name("http.method") + ); + assert_eq!("url.full", changes.get_span_attribute_name("http.url")); + assert_eq!( + "net.protocol.name", + changes.get_span_attribute_name("net.app.protocol.name") + ); + assert_eq!( + "cloud.resource_id", + changes.get_span_attribute_name("faas.id") + ); + assert_eq!( + "db.name", + changes.get_span_attribute_name("db.hbase.namespace") + ); + assert_eq!( + "db.name", + changes.get_span_attribute_name("db.cassandra.keyspace") + ); + assert_eq!("metric_1", changes.get_metric_name("m1")); + assert_eq!("metric_2", changes.get_metric_name("m2")); + } + + #[test] + fn test_override() { + let parent_versions = Versions::load_from_file("data/parent_versions.yaml").unwrap(); + let mut app_versions = Versions::load_from_file("data/app_versions.yaml").unwrap(); + + // Update `app_version` to extend `parent_versions` + app_versions.extend(parent_versions); + + // Transformations defined in `app_versions.yaml` overriding or + // complementing `parent_versions.yaml` + let v1_22 = app_versions + .versions + .get(&semver::Version::parse("1.22.0").unwrap()) + .unwrap(); + let observed_value = v1_22.spans.as_ref().unwrap().changes[0] + .rename_attributes + .attribute_map + .get("messaging.kafka.client_id"); + assert_eq!(observed_value, Some(&"messaging.client.id".to_string())); + + let v1_8 = app_versions + .versions + .get(&semver::Version::parse("1.8.0").unwrap()) + .unwrap(); + let observed_value = v1_8.spans.as_ref().unwrap().changes[0] + .rename_attributes + .attribute_map + .get("db.cassandra.keyspace"); + assert_eq!(observed_value, Some(&"database.name".to_string())); + let observed_value = v1_8.spans.as_ref().unwrap().changes[0] + .rename_attributes + .attribute_map + .get("db.cassandra.keyspace"); + assert_eq!(observed_value, Some(&"database.name".to_string())); + let observed_value = v1_8.spans.as_ref().unwrap().changes[0] + .rename_attributes + .attribute_map + .get("db.hbase.namespace"); + assert_eq!(observed_value, Some(&"db.name".to_string())); + let observed_value = v1_8.logs.as_ref().unwrap().changes[0] + .rename_attributes + .attribute_map + .get("db.cassandra.keyspace"); + assert_eq!(observed_value, Some(&"database.name".to_string())); + let observed_value = v1_8.logs.as_ref().unwrap().changes[0] + .rename_attributes + .attribute_map + .get("db.hbase.namespace"); + assert_eq!(observed_value, Some(&"db.name".to_string())); + let observed_value = v1_8.metrics.as_ref().unwrap().changes[0] + .rename_metrics + .get("m1"); + assert_eq!(observed_value, Some(&"metric_1".to_string())); + let observed_value = v1_8.metrics.as_ref().unwrap().changes[0] + .rename_metrics + .get("m2"); + assert_eq!(observed_value, Some(&"metric2".to_string())); + + let v1_7_1 = app_versions + .versions + .get(&semver::Version::parse("1.7.1").unwrap()) + .unwrap(); + let observed_value = v1_7_1.spans.as_ref().unwrap().changes[0] + .rename_attributes + .attribute_map + .get("db.cassandra.table"); + assert_eq!(observed_value, Some(&"database.table".to_string())); + + // Transformations inherited from `parent_versions.yaml` and + // initially not present in `app_versions.yaml` + let v1_21 = app_versions + .versions + .get(&semver::Version::parse("1.21.0").unwrap()) + .unwrap(); + let observed_value = v1_21.metrics.as_ref().unwrap().changes[0] + .rename_metrics + .get("process.runtime.jvm.cpu.utilization"); + assert_eq!( + observed_value, + Some(&"process.runtime.jvm.cpu.recent_utilization".to_string()) + ); + let observed_value = v1_21.spans.as_ref().unwrap().changes[0] + .rename_attributes + .attribute_map + .get("messaging.kafka.client_id"); + assert_eq!(observed_value, Some(&"messaging.client_id".to_string())); + + let changes = app_versions.version_changes_for(app_versions.latest_version().unwrap()); + assert_eq!("metric_1", changes.get_metric_name("m1")); + assert_eq!("metric2", changes.get_metric_name("m2")); + } +} diff --git a/crates/weaver_version/src/logs_change.rs b/crates/weaver_version/src/logs_change.rs new file mode 100644 index 00000000..3c2cb0e8 --- /dev/null +++ b/crates/weaver_version/src/logs_change.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Changes to apply to the logs for a specific version. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Changes to apply to the logs for a specific version. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct LogsChange { + /// A collection of rename operations to apply to the log attributes. + pub rename_attributes: RenameAttributes, +} + +/// A collection of rename operations to apply to the log attributes. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct RenameAttributes { + /// A collection of rename operations to apply to the log attributes. + pub attribute_map: HashMap, +} diff --git a/crates/weaver_version/src/logs_version.rs b/crates/weaver_version/src/logs_version.rs new file mode 100644 index 00000000..96357e66 --- /dev/null +++ b/crates/weaver_version/src/logs_version.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Logs version. + +use crate::logs_change::LogsChange; +use serde::{Deserialize, Serialize}; + +/// Changes to apply to the logs for a specific version. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct LogsVersion { + /// Changes to apply to the logs for a specific version. + pub changes: Vec, +} diff --git a/crates/weaver_version/src/metrics_change.rs b/crates/weaver_version/src/metrics_change.rs new file mode 100644 index 00000000..fcb7d7b1 --- /dev/null +++ b/crates/weaver_version/src/metrics_change.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Metrics change definitions. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Changes to apply to the metrics for a specific version. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct MetricsChange { + /// A collection of rename operations to apply to the metric attributes. + #[serde(default)] + pub rename_attributes: RenameAttributes, + /// A collection of rename operations to apply to the metric names. + #[serde(default)] + pub rename_metrics: HashMap, +} + +/// A collection of rename operations to apply to the metric attributes. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct RenameAttributes { + /// A collection of rename operations to apply to the metric attributes. + pub attribute_map: HashMap, + /// A collection of metric references. + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub apply_to_metrics: Vec, +} diff --git a/crates/weaver_version/src/metrics_version.rs b/crates/weaver_version/src/metrics_version.rs new file mode 100644 index 00000000..da16cc36 --- /dev/null +++ b/crates/weaver_version/src/metrics_version.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Metrics version. + +use crate::metrics_change::MetricsChange; +use serde::{Deserialize, Serialize}; + +/// Changes to apply to the metrics for a specific version. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct MetricsVersion { + /// Changes to apply to the metrics for a specific version. + pub changes: Vec, +} diff --git a/crates/weaver_version/src/resource_change.rs b/crates/weaver_version/src/resource_change.rs new file mode 100644 index 00000000..caf22c30 --- /dev/null +++ b/crates/weaver_version/src/resource_change.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Changes to apply to the resources for a specific version. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Changes to apply to the resources for a specific version. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct ResourceChange { + /// Changes to apply to the resource attributes for a specific version. + pub rename_attributes: RenameAttributes, +} + +/// Changes to apply to the resource attributes for a specific version. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct RenameAttributes { + /// A collection of rename operations to apply to the resource attributes. + pub attribute_map: HashMap, +} diff --git a/crates/weaver_version/src/resource_version.rs b/crates/weaver_version/src/resource_version.rs new file mode 100644 index 00000000..f3e4e20f --- /dev/null +++ b/crates/weaver_version/src/resource_version.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Resource version. + +use crate::resource_change::ResourceChange; +use serde::{Deserialize, Serialize}; + +/// Changes to apply to the resource for a specific version. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct ResourceVersion { + /// Changes to apply to the resource for a specific version. + pub changes: Vec, +} diff --git a/crates/weaver_version/src/spans_change.rs b/crates/weaver_version/src/spans_change.rs new file mode 100644 index 00000000..ba2946ca --- /dev/null +++ b/crates/weaver_version/src/spans_change.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Spans change specification. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Changes to apply to the spans specification for a specific version. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct SpansChange { + /// Changes to apply to the span attributes for a specific version. + pub rename_attributes: RenameAttributes, +} + +/// Changes to apply to the span attributes for a specific version. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct RenameAttributes { + /// A collection of rename operations to apply to the span attributes. + pub attribute_map: HashMap, +} diff --git a/crates/weaver_version/src/spans_version.rs b/crates/weaver_version/src/spans_version.rs new file mode 100644 index 00000000..93ed2257 --- /dev/null +++ b/crates/weaver_version/src/spans_version.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Changes to apply to the spans specification for a specific version. + +use crate::spans_change::SpansChange; +use serde::{Deserialize, Serialize}; + +/// Changes to apply to the spans specification for a specific version. +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[serde(deny_unknown_fields)] +pub struct SpansVersion { + /// Changes to apply to the spans specification for a specific version. + pub changes: Vec, +} diff --git a/data/app-telemetry-schema-1.yaml b/data/app-telemetry-schema-1.yaml new file mode 100644 index 00000000..75b7b09a --- /dev/null +++ b/data/app-telemetry-schema-1.yaml @@ -0,0 +1,73 @@ +file_format: 1.2.0 +# Inherit from the OpenTelemetry schema v1.21.0 +parent_schema_url: https://opentelemetry.io/schemas/1.21.0 +# Current schema url +schema_url: https://mycompany.com/schemas/1.0.0 + +# Semantic Convention Imports +semantic_conventions: + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/url.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/http-common.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/client.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/exception.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/server.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/network.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/http.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/database-metrics.yaml + +# The section below outlines the telemetry schema for a service or application. +# This schema details all the metrics, logs, and spans specifically generated +# by that service or application. +# +# Note: Frameworks or libraries linked with the application that produce OTel +# telemetry data might also have their own telemetry schema, detailing the +# metrics, logs, and spans they generate locally. +schema: + # Attributes inherited by all resource types + resource: + attributes: + - ref: service.name + value: "my-service" + - ref: service.version + value: "{{SERVICE_VERSION}}" + + # Metrics declaration + resource_metrics: + # Common attributes shared across univariate and multivariate metrics + attributes: + - id: environment + type: string + brief: The environment in which the service is running + tag: sensitive-information + requirement_level: required + # Declaration of all the univariate metrics + metrics: + - ref: http.server.request.duration + attributes: + - ref: server.address + - ref: server.port + - ref: http.request.method + - ref: http.response.status_code + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + - ref: db.client.connections.usage + attributes: + - ref: server.address + # Declaration of all the multivariate metrics + metric_groups: + - name: http # name of a multivariate metrics group + attributes: + - ref: server.address + - ref: server.port + - ref: http.request.method + - ref: http.response.status_code + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + metrics: # metrics sharing the same attributes + - ref: http.server.request.duration + - ref: http.server.active_requests + - ref: http.server.request.size + - ref: http.server.response.size + - ref: db.client.connections.usage diff --git a/data/app-telemetry-schema-2.yaml b/data/app-telemetry-schema-2.yaml new file mode 100644 index 00000000..792a3f56 --- /dev/null +++ b/data/app-telemetry-schema-2.yaml @@ -0,0 +1,161 @@ +file_format: 1.2.0 +parent_schema_url: data/open-telemetry-schema.1.22.0.yaml +# Current schema url +schema_url: https://mycompany.com/schemas/1.0.0 + +# The section below outlines the telemetry schema for a service or application. +# This schema details all the metrics, logs, and spans specifically generated +# by that service or application. +# +# Note: Frameworks or libraries linked with the application that produce OTel +# telemetry data might also have their own telemetry schema, detailing the +# metrics, logs, and spans they generate locally. +schema: + # Resource attributes + # Only used when a Client SDK is generated + resource: + attributes: + - ref: service.name + value: "my-service" + - ref: service.version + requirement_level: required + - id: service.instance.id + type: string + brief: The unique identifier of the service instance + + # Instrumentation library + instrumentation_library: + name: "my-service" + version: "1.0.0" + # schema url? + + # Metrics declaration + resource_metrics: + # Common attributes shared across univariate and multivariate metrics + attributes: + - id: environment + type: string + brief: The environment in which the service is running + tag: sensitive-information + requirement_level: required + # Declaration of all the univariate metrics + metrics: + - ref: jvm.thread.count + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + requirement_level: required + - ref: jvm.class.loaded + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + requirement_level: required + tags: + sensitivity: PII + tags: + sensitivity: PII + - ref: jvm.cpu.recent_utilization + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + requirement_level: required + - ref: jvm.gc.duration + # Declaration of all the multivariate metrics + metric_groups: + - name: http # name of a metrics group + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + - id: url.host + type: string + brief: The host of the request + requirement_level: required + metrics: # metrics sharing the same attributes + - ref: jvm.thread.count + # Note: u64 or f64 must be defined at this level + - ref: jvm.class.loaded + - ref: jvm.cpu.recent_utilization + + # Events declaration + resource_events: + events: + - event_name: request + domain: http + attributes: + - ref: server.address + - ref: server.port + - ref: http.request.method + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + - id: url.host + type: string + brief: The host of the request + requirement_level: required + - event_name: response + domain: http + attributes: + - ref: server.address + - ref: server.port + - ref: http.request.method + - ref: http.response.status_code + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + - id: url.host + type: string + brief: The host of the request + requirement_level: required + + # Spans declaration + resource_spans: + spans: + - span_name: http.request # name of a specific tracer + attributes: + - ref: server.address + - ref: server.port + - ref: client.address + - ref: client.port + - ref: url.scheme + - id: url.host + type: string + brief: The host of the request + requirement_level: required + events: + - event_name: error + attributes: + - ref: exception.type + - ref: exception.message + requirement_level: required + - ref: exception.stacktrace + # links: + - span_name: database.query + attributes: + - ref: server.address + - ref: server.port + - ref: client.address + - ref: client.port + - ref: url.scheme + requirement_level: required + - id: url.host + type: string + brief: The host of the request + requirement_level: required + events: + - event_name: error + attributes: + - ref: exception.type + - ref: exception.message + - ref: exception.stacktrace \ No newline at end of file diff --git a/data/app-telemetry-schema-events.yaml b/data/app-telemetry-schema-events.yaml new file mode 100644 index 00000000..035bfcac --- /dev/null +++ b/data/app-telemetry-schema-events.yaml @@ -0,0 +1,57 @@ +file_format: 1.2.0 +parent_schema_url: https://opentelemetry.io/schemas/1.21.0 +schema_url: https://mycompany.com/schemas/1.0.0 +schema: + resource: + attributes: + - ref: service.name + value: "my-service" + - ref: service.version + value: "1.1.1" + + instrumentation_library: + name: "my-service" + version: "1.0.0" + + resource_events: + events: + - event_name: request + domain: http + attributes: + - ref: http.request.method + - ref: network.protocol.name + - ref: network.protocol.version + - ref: http.route + tags: + sensitivity: PII + - ref: server.address + - ref: server.port + - ref: url.scheme + requirement_level: required + - id: mycompany.com.env + type: string + brief: The environment in which the service is running + requirement_level: required + - event_name: response + domain: http + attributes: + - attribute_group_ref: attributes.http.common + - ref: http.response.status_code + requirement_level: required + note: Required status code for HTTP response events. + - ref: http.route + tags: + sensitivity: PII + - ref: server.address + - ref: server.port + - ref: url.scheme + requirement_level: required + - id: mycompany.com.env + type: string + brief: The environment in which the service is running + requirement_level: required + - event_name: error + domain: server + attributes: + - attribute_group_ref: server + - attribute_group_ref: error diff --git a/data/app-telemetry-schema-metrics.yaml b/data/app-telemetry-schema-metrics.yaml new file mode 100644 index 00000000..546e9331 --- /dev/null +++ b/data/app-telemetry-schema-metrics.yaml @@ -0,0 +1,49 @@ +file_format: 1.2.0 +parent_schema_url: data/open-telemetry-schema.1.22.0.yaml +schema_url: https://mycompany.com/schemas/1.0.0 + +schema: + resource: + attributes: + - resource_ref: os + + instrumentation_library: + name: "my-service" + version: "1.0.0" + + resource_metrics: + # Common attributes shared across univariate and multivariate metrics + attributes: + - id: environment + type: string + brief: The environment in which the service is running + tag: sensitive-information + requirement_level: required + metrics: + - ref: http.server.request.duration +# attributes: +# - attribute_group_ref: server +# - ref: http.request.method +# - ref: http.response.status_code +# - ref: network.protocol.name +# - ref: network.protocol.version +# - ref: url.scheme + metric_groups: + - name: http +# attributes: +# - attribute_group_ref: server +# - ref: http.request.method +# - ref: http.response.status_code +# - ref: network.protocol.name +# - ref: network.protocol.version +# - ref: url.scheme + metrics: + - ref: http.server.request.duration + - ref: http.server.request.body.size + - ref: http.server.response.body.size +# - id: another_http +# metrics: +# - ref: http.server.request.duration +# - ref: http.server.active_requests +# - ref: http.server.request.body.size +# - ref: http.server.response.body.size diff --git a/data/app-telemetry-schema-simple.yaml b/data/app-telemetry-schema-simple.yaml new file mode 100644 index 00000000..e5cde22f --- /dev/null +++ b/data/app-telemetry-schema-simple.yaml @@ -0,0 +1,35 @@ +# This file describes the main sections of the schema file v1.2.0. +# This schema is not intended to be used directly, but rather to be used +# for documentation purposes. + +file_format: 1.2.0 +parent_schema_url: # e.g. https://opentelemetry.io/schemas/1.22.0 +schema_url: +semantic_conventions: # list of semantic conventions to import +schema: + resource: + attributes: # attributes (ref. to semconv or local def.) + instrumentation_library: + name: + version: + resource_metrics: + metrics: # metrics def. (ref. to semconv or local def.) + metric_groups: + - id: + attributes: # attributes (ref. to semconv or local def.) + metrics: # metrics def. (ref. to semconv or local def.) + resource_events: + events: + - event_name: + domain: + attributes: # attributes (ref. to semconv or local def.) + resource_spans: + spans: + - span_name: + attributes: # attributes (ref. to semconv or local def.) + events: # event definitions + links: # link definitions +versions: # version definitions + + + diff --git a/data/app-telemetry-schema-spans.yaml b/data/app-telemetry-schema-spans.yaml new file mode 100644 index 00000000..7140f1b8 --- /dev/null +++ b/data/app-telemetry-schema-spans.yaml @@ -0,0 +1,41 @@ +file_format: 1.2.0 +parent_schema_url: https://opentelemetry.io/schemas/1.21.0 +schema_url: https://mycompany.com/schemas/1.0.0 +semantic_conventions: + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/os.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/client.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/exception.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/server.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/feature-flag.yaml + +schema: + resource: + attributes: + - resource_ref: os + + instrumentation_library: + name: "my-service" + version: "1.0.0" + + resource_spans: + spans: + - span_name: client.http.request + attributes: + - attribute_group_ref: client + events: + - event_name: error + attributes: + - span_ref: exception + - event_name: feature_flag + attributes: + - event_ref: feature_flag + - span_name: server.http.request + attributes: + - attribute_group_ref: server + events: + - event_name: error + attributes: + - span_ref: exception + - event_name: feature_flag + attributes: + - event_ref: feature_flag diff --git a/data/app-telemetry-schema-traces.yaml b/data/app-telemetry-schema-traces.yaml new file mode 100644 index 00000000..f645ec62 --- /dev/null +++ b/data/app-telemetry-schema-traces.yaml @@ -0,0 +1,65 @@ +file_format: 1.2.0 +# Inherit from the OpenTelemetry schema v1.21.0 +parent_schema_url: https://opentelemetry.io/schemas/1.21.0 +# Current schema url +schema_url: https://mycompany.com/schemas/1.0.0 + +# Semantic Convention Imports +semantic_conventions: + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/url.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/http-common.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/client.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/exception.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/server.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/network.yaml + - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/http.yaml + +# The section below outlines the telemetry schema for a service or application. +# This schema details all the metrics, logs, and spans specifically generated +# by that service or application. +# +# Note: Frameworks or libraries linked with the application that produce OTel +# telemetry data might also have their own telemetry schema, detailing the +# metrics, logs, and spans they generate locally. +schema: + # Resource attributes + # Only used when a Client SDK is generated + resource: + attributes: + - ref: service.name + value: "my-service" + - ref: service.version + value: "{{SERVICE_VERSION}}" + # schema url? + + # Instrumentation library + instrumentation_library: + name: "my-service" + version: "1.0.0" + # schema url? + + # Spans declaration + resource_spans: + spans: + - span_name: http.request # name of a specific tracer + attributes: + - ref: server.address + - ref: server.port + - ref: server.socket.address + - ref: server.socket.port + - ref: client.address + - ref: client.port + - ref: client.socket.address + - ref: client.socket.port + - ref: url.scheme + - id: url.host + type: string + brief: The host of the request + requirement_level: required + events: + - event_name: error + attributes: + - ref: exception.type + - ref: exception.message + - ref: exception.stacktrace + # links: \ No newline at end of file diff --git a/data/app-telemetry-schema.yaml b/data/app-telemetry-schema.yaml new file mode 100644 index 00000000..afd53507 --- /dev/null +++ b/data/app-telemetry-schema.yaml @@ -0,0 +1,90 @@ +file_format: 1.2.0 +# Inherit from the OpenTelemetry schema v1.21.0 +parent_schema_url: ../../data/open-telemetry-schema.1.22.0.yaml +# Current schema url +schema_url: https://mycompany.com/schemas/1.0.0 + +# The section below outlines the telemetry schema for a service or application. +# This schema details all the metrics, logs, and spans specifically generated +# by that service or application. +# +# Note: Frameworks or libraries linked with the application that produce OTel +# telemetry data might also have their own telemetry schema, detailing the +# metrics, logs, and spans they generate locally. +schema: + tags: + sensitive_data: true + + # Resource attributes + # Only used when a Client SDK is generated + resource: + attributes: + - id: service.name + type: string + brief: The name of the service + value: "my-service" + - id: service.version + type: string + brief: The version of the service + value: "{{SERVICE_VERSION}}" + # schema url? + + # Instrumentation library + instrumentation_library: + name: "my-service" + version: "1.0.0" + # schema url? + + # Metrics declaration + resource_metrics: + # Common attributes shared across univariate and multivariate metrics + attributes: + - id: environment + type: string + brief: The environment in which the service is running + tag: sensitive-information + requirement_level: required + # Declaration of all the univariate metrics + metrics: + - ref: http.server.request.duration + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + # Declaration of all the multivariate metrics + metric_groups: + - name: http # name of a multivariate metrics group + metrics: # metrics sharing the same attributes + - ref: http.server.request.duration + - ref: http.server.request.body.size + - ref: http.server.response.body.size + + + # Events declaration + resource_events: + events: + - event_name: http # name of a specific meter + domain: http + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + + # Spans declaration + resource_spans: + spans: + - span_name: http.request # name of a specific tracer + attributes: + - ref: server.address + - ref: server.port + - ref: client.address + - ref: client.port + events: + - event_name: error + attributes: + - ref: exception.type + - ref: exception.message + - ref: exception.stacktrace + # links: \ No newline at end of file diff --git a/data/open-telemetry-schema.1.22.0.yaml b/data/open-telemetry-schema.1.22.0.yaml new file mode 100644 index 00000000..0b6b97e2 --- /dev/null +++ b/data/open-telemetry-schema.1.22.0.yaml @@ -0,0 +1,353 @@ +file_format: 1.1.0 +schema_url: https://opentelemetry.io/schemas/1.21.0 + +# Semantic conventions can be loaded from a list of URLs or from a git repository. +# The git repository approach is usually faster and download all the semantic +# conventions files dynamically without the need to update the schema file. +semantic_conventions: + - git_url: https://github.com/open-telemetry/semantic-conventions.git + path: model +#semantic_conventions: +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/client.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/destination.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/error.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/exception.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/faas-common.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/general.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/http-common.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/network.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/server.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/session.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/source.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/url.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/user-agent.yaml +# # logs +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/events.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/general.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/log-exception.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/log-feature_flag.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/media.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/mobile-events.yaml +# #metrics +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/database-metrics.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/faas-metrics.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/http.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/jvm-metrics-experimental.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/jvm-metrics.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/rpc-metrics.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/system-metrics.yaml +# # registry +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/registry/http.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/registry/url.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/registry/network.yaml +# # resources +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/android.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/browser.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/container.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/deployment_environment.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/device.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/faas.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/host.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/k8s.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/oci.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/os.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/process.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/service.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/service_experimental.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/telemetry.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/telemetry_experimental.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/webengine.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/aws/ecs.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/aws/eks.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/aws/logs.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/gcp/cloud_run.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/gcp/gce.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/heroku.yaml +# # scope +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/scope/exporter/exporter.yaml +# # trace +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/cloudevents.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/compatibility.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/database.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/faas.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/feature-flag.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/http.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/messaging.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/rpc.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/trace-exception.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/aws/lambda.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/exporter/exporter.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/instrumentation/aws-sdk.yml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/instrumentation/graphql.yml + +versions: + 1.22.0: + metrics: + changes: + # https://github.com/open-telemetry/semantic-conventions/pull/229 + - rename_attributes: + attribute_map: + messaging.message.payload_size_bytes: messaging.message.body.size + # https://github.com/open-telemetry/semantic-conventions/pull/224 + - rename_metrics: + http.client.duration: http.client.request.duration + http.server.duration: http.server.request.duration + # https://github.com/open-telemetry/semantic-conventions/pull/241 + - rename_metrics: + process.runtime.jvm.memory.usage: jvm.memory.usage + process.runtime.jvm.memory.committed: jvm.memory.committed + process.runtime.jvm.memory.limit: jvm.memory.limit + process.runtime.jvm.memory.usage_after_last_gc: jvm.memory.usage_after_last_gc + process.runtime.jvm.gc.duration: jvm.gc.duration + # also https://github.com/open-telemetry/semantic-conventions/pull/252 + process.runtime.jvm.threads.count: jvm.thread.count + # also https://github.com/open-telemetry/semantic-conventions/pull/252 + process.runtime.jvm.classes.loaded: jvm.class.loaded + # also https://github.com/open-telemetry/semantic-conventions/pull/252 + process.runtime.jvm.classes.unloaded: jvm.class.unloaded + # also https://github.com/open-telemetry/semantic-conventions/pull/252 + # and https://github.com/open-telemetry/semantic-conventions/pull/60 + process.runtime.jvm.classes.current_loaded: jvm.class.count + process.runtime.jvm.cpu.time: jvm.cpu.time + process.runtime.jvm.cpu.recent_utilization: jvm.cpu.recent_utilization + process.runtime.jvm.memory.init: jvm.memory.init + process.runtime.jvm.system.cpu.utilization: jvm.system.cpu.utilization + process.runtime.jvm.system.cpu.load_1m: jvm.system.cpu.load_1m + # https://github.com/open-telemetry/semantic-conventions/pull/253 + process.runtime.jvm.buffer.usage: jvm.buffer.memory.usage + # https://github.com/open-telemetry/semantic-conventions/pull/253 + process.runtime.jvm.buffer.limit: jvm.buffer.memory.limit + process.runtime.jvm.buffer.count: jvm.buffer.count + # https://github.com/open-telemetry/semantic-conventions/pull/20 + - rename_attributes: + attribute_map: + type: jvm.memory.type + pool: jvm.memory.pool.name + apply_to_metrics: + - jvm.memory.usage + - jvm.memory.committed + - jvm.memory.limit + - jvm.memory.usage_after_last_gc + - jvm.memory.init + - rename_attributes: + attribute_map: + name: jvm.gc.name + action: jvm.gc.action + apply_to_metrics: + - jvm.gc.duration + - rename_attributes: + attribute_map: + daemon: thread.daemon + apply_to_metrics: + - jvm.threads.count + - rename_attributes: + attribute_map: + pool: jvm.buffer.pool.name + apply_to_metrics: + - jvm.buffer.usage + - jvm.buffer.limit + - jvm.buffer.count + # https://github.com/open-telemetry/semantic-conventions/pull/89 + - rename_attributes: + attribute_map: + state: system.cpu.state + cpu: system.cpu.logical_number + apply_to_metrics: + - system.cpu.time + - system.cpu.utilization + - rename_attributes: + attribute_map: + state: system.memory.state + apply_to_metrics: + - system.memory.usage + - system.memory.utilization + - rename_attributes: + attribute_map: + state: system.paging.state + apply_to_metrics: + - system.paging.usage + - system.paging.utilization + - rename_attributes: + attribute_map: + type: system.paging.type + direction: system.paging.direction + apply_to_metrics: + - system.paging.faults + - system.paging.operations + - rename_attributes: + attribute_map: + device: system.device + direction: system.disk.direction + apply_to_metrics: + - system.disk.io + - system.disk.operations + - system.disk.io_time + - system.disk.operation_time + - system.disk.merged + - rename_attributes: + attribute_map: + device: system.device + state: system.filesystem.state + type: system.filesystem.type + mode: system.filesystem.mode + mountpoint: system.filesystem.mountpoint + apply_to_metrics: + - system.filesystem.usage + - system.filesystem.utilization + - rename_attributes: + attribute_map: + device: system.device + direction: system.network.direction + protocol: network.protocol + state: system.network.state + apply_to_metrics: + - system.network.dropped + - system.network.packets + - system.network.errors + - system.network.io + - system.network.connections + - rename_attributes: + attribute_map: + status: system.processes.status + apply_to_metrics: + - system.processes.count + # https://github.com/open-telemetry/semantic-conventions/pull/247 + - rename_metrics: + http.server.request.size: http.server.request.body.size + http.server.response.size: http.server.response.body.size + 1.21.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3336 + - rename_attributes: + attribute_map: + messaging.kafka.client_id: messaging.client_id + messaging.rocketmq.client_id: messaging.client_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3402 + - rename_attributes: + attribute_map: + # net.peer.(name|port) attributes were usually populated on client side + # so they should be usually translated to server.(address|port) + # net.host.* attributes were only populated on server side + net.host.name: server.address + net.host.port: server.port + # was only populated on client side + net.sock.peer.name: server.socket.domain + # net.sock.peer.(addr|port) mapping is not possible + # since they applied to both client and server side + # were only populated on server side + net.sock.host.addr: server.socket.address + net.sock.host.port: server.socket.port + http.client_ip: client.address + # https://github.com/open-telemetry/opentelemetry-specification/pull/3426 + - rename_attributes: + attribute_map: + net.protocol.name: network.protocol.name + net.protocol.version: network.protocol.version + net.host.connection.type: network.connection.type + net.host.connection.subtype: network.connection.subtype + net.host.carrier.name: network.carrier.name + net.host.carrier.mcc: network.carrier.mcc + net.host.carrier.mnc: network.carrier.mnc + net.host.carrier.icc: network.carrier.icc + # https://github.com/open-telemetry/opentelemetry-specification/pull/3355 + - rename_attributes: + attribute_map: + http.method: http.request.method + http.status_code: http.response.status_code + http.scheme: url.scheme + http.url: url.full + http.request_content_length: http.request.body.size + http.response_content_length: http.response.body.size + metrics: + changes: + # https://github.com/open-telemetry/semantic-conventions/pull/53 + - rename_metrics: + process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization + 1.20.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3272 + - rename_attributes: + attribute_map: + net.app.protocol.name: net.protocol.name + net.app.protocol.version: net.protocol.version + 1.19.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3209 + - rename_attributes: + attribute_map: + faas.execution: faas.invocation_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3188 + - rename_attributes: + attribute_map: + faas.id: cloud.resource_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3190 + - rename_attributes: + attribute_map: + http.user_agent: user_agent.original + resources: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3190 + - rename_attributes: + attribute_map: + browser.user_agent: user_agent.original + 1.18.0: + 1.17.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2957 + - rename_attributes: + attribute_map: + messaging.consumer_id: messaging.consumer.id + messaging.protocol: net.app.protocol.name + messaging.protocol_version: net.app.protocol.version + messaging.destination: messaging.destination.name + messaging.temp_destination: messaging.destination.temporary + messaging.destination_kind: messaging.destination.kind + messaging.message_id: messaging.message.id + messaging.conversation_id: messaging.message.conversation_id + messaging.message_payload_size_bytes: messaging.message.payload_size_bytes + messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes + messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key + messaging.kafka.message_key: messaging.kafka.message.key + messaging.kafka.partition: messaging.kafka.destination.partition + messaging.kafka.tombstone: messaging.kafka.message.tombstone + messaging.rocketmq.message_type: messaging.rocketmq.message.type + messaging.rocketmq.message_tag: messaging.rocketmq.message.tag + messaging.rocketmq.message_keys: messaging.rocketmq.message.keys + messaging.kafka.consumer_group: messaging.kafka.consumer.group + 1.16.0: + 1.15.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2743 + - rename_attributes: + attribute_map: + http.retry_count: http.resend_count + 1.14.0: + 1.13.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2614 + - rename_attributes: + attribute_map: + net.peer.ip: net.sock.peer.addr + net.host.ip: net.sock.host.addr + 1.12.0: + 1.11.0: + 1.10.0: + 1.9.0: + 1.8.0: + spans: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: db.name + db.hbase.namespace: db.name + 1.7.0: + 1.6.1: + 1.5.0: + 1.4.0: \ No newline at end of file diff --git a/data/resolved-schema-events.yaml b/data/resolved-schema-events.yaml new file mode 100644 index 00000000..753bee7e --- /dev/null +++ b/data/resolved-schema-events.yaml @@ -0,0 +1,573 @@ +file_format: 1.2.0 +parent_schema_url: https://opentelemetry.io/schemas/1.21.0 +schema_url: https://mycompany.com/schemas/1.0.0 +schema: + resource: + attributes: + - ref: service.name + value: my-service + - ref: service.version + value: 1.1.1 + instrumentation_library: + name: my-service + version: 1.0.0 + resource_events: + events: + - event_name: request + domain: http + attributes: + - id: http.route + type: string + brief: | + The matched route (path template in the format used by the respective server framework). See note below + examples: + - /users/:userID? + - '{controller}/{action}/{id?}' + requirement_level: + conditionally_required: If and only if it's available + note: | + 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. + tags: + sensitivity: PII + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: required + note: '' + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: http.request.method + type: + allow_custom_values: true + members: + - id: connect + value: CONNECT + brief: CONNECT method. + note: null + - id: delete + value: DELETE + brief: DELETE method. + note: null + - id: get + value: GET + brief: GET method. + note: null + - id: head + value: HEAD + brief: HEAD method. + note: null + - id: options + value: OPTIONS + brief: OPTIONS method. + note: null + - id: patch + value: PATCH + brief: PATCH method. + note: null + - id: post + value: POST + brief: POST method. + note: null + - id: put + value: PUT + brief: PUT method. + note: null + - id: trace + value: TRACE + brief: TRACE method. + note: null + - id: other + value: _OTHER + brief: Any HTTP method that the instrumentation has no prior knowledge of. + note: null + brief: HTTP request method. + examples: + - GET + - POST + - HEAD + requirement_level: required + note: | + 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) + and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html). + + 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 + (this list MUST be a full override of the default known method, 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. + - id: mycompany.com.env + type: string + brief: The environment in which the service is running + requirement_level: required + note: '' + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - event_name: response + domain: http + attributes: + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - id: error.type + type: + allow_custom_values: true + members: + - id: other + value: _OTHER + brief: A fallback error value to be used when the instrumentation does not define a custom value for it. + note: null + brief: Describes a class of error the operation ended with. + examples: + - timeout + - java.net.UnknownHostException + - server_certificate_invalid + - '500' + requirement_level: recommended + note: | + The `error.type` SHOULD be predictable and SHOULD have low cardinality. + Instrumentations SHOULD document the list of errors they report. + + The cardinality of `error.type` within one instrumentation library SHOULD be low, but + 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 codes (such as HTTP or gRPC status codes), + it's RECOMMENDED to use a domain-specific attribute and also set `error.type` to capture + all errors, regardless of whether they are defined within the domain-specific set or not. + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + - id: http.response.status_code + type: int + brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).' + examples: + - 200 + requirement_level: required + note: Required status code for HTTP response events. + - id: http.route + type: string + brief: | + The matched route (path template in the format used by the respective server framework). See note below + examples: + - /users/:userID? + - '{controller}/{action}/{id?}' + requirement_level: + conditionally_required: If and only if it's available + note: | + 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. + tags: + sensitivity: PII + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: required + note: '' + - id: mycompany.com.env + type: string + brief: The environment in which the service is running + requirement_level: required + note: '' + - id: http.request.method + type: + allow_custom_values: true + members: + - id: connect + value: CONNECT + brief: CONNECT method. + note: null + - id: delete + value: DELETE + brief: DELETE method. + note: null + - id: get + value: GET + brief: GET method. + note: null + - id: head + value: HEAD + brief: HEAD method. + note: null + - id: options + value: OPTIONS + brief: OPTIONS method. + note: null + - id: patch + value: PATCH + brief: PATCH method. + note: null + - id: post + value: POST + brief: POST method. + note: null + - id: put + value: PUT + brief: PUT method. + note: null + - id: trace + value: TRACE + brief: TRACE method. + note: null + - id: other + value: _OTHER + brief: Any HTTP method that the instrumentation has no prior knowledge of. + note: null + brief: HTTP request method. + examples: + - GET + - POST + - HEAD + requirement_level: required + note: | + 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) + and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html). + + 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 + (this list MUST be a full override of the default known method, 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. + - event_name: error + domain: server + attributes: + - id: server.socket.address + type: string + brief: Server address of the socket connection - IP address or Unix domain socket name. + examples: + - 10.5.3.2 + requirement_level: + recommended: If different than `server.address`. + note: | + When observed from the client side, this SHOULD represent the immediate server peer address. + When observed from the server side, this SHOULD represent the physical server address. + - id: error.type + type: + allow_custom_values: true + members: + - id: other + value: _OTHER + brief: A fallback error value to be used when the instrumentation does not define a custom value for it. + note: null + brief: Describes a class of error the operation ended with. + examples: + - timeout + - java.net.UnknownHostException + - server_certificate_invalid + - '500' + requirement_level: recommended + note: | + The `error.type` SHOULD be predictable and SHOULD have low cardinality. + Instrumentations SHOULD document the list of errors they report. + + The cardinality of `error.type` within one instrumentation library SHOULD be low, but + 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 codes (such as HTTP or gRPC status codes), + it's RECOMMENDED to use a domain-specific attribute and also set `error.type` to capture + all errors, regardless of whether they are defined within the domain-specific set or not. + - id: server.socket.domain + type: string + brief: Immediate server peer's domain name if available without reverse DNS lookup + examples: + - proxy.example.com + requirement_level: + recommended: If different than `server.address`. + note: Typically observed from the client side, and represents a proxy or other intermediary domain name. + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: server.socket.port + type: int + brief: Server port number of the socket connection. + examples: + - 16456 + requirement_level: + recommended: If different than `server.port`. + note: | + When observed from the client side, this SHOULD represent the immediate server peer port. + When observed from the server side, this SHOULD represent the physical server port. +versions: + 1.4.0: + metrics: null + logs: null + spans: null + resources: null + 1.5.0: + metrics: null + logs: null + spans: null + resources: null + 1.6.1: + metrics: null + logs: null + spans: null + resources: null + 1.7.0: + metrics: null + logs: null + spans: null + resources: null + 1.8.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: db.name + db.hbase.namespace: db.name + resources: null + 1.9.0: + metrics: null + logs: null + spans: null + resources: null + 1.10.0: + metrics: null + logs: null + spans: null + resources: null + 1.11.0: + metrics: null + logs: null + spans: null + resources: null + 1.12.0: + metrics: null + logs: null + spans: null + resources: null + 1.13.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + net.host.ip: net.sock.host.addr + net.peer.ip: net.sock.peer.addr + resources: null + 1.14.0: + metrics: null + logs: null + spans: null + resources: null + 1.15.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + http.retry_count: http.resend_count + resources: null + 1.16.0: + metrics: null + logs: null + spans: null + resources: null + 1.17.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + messaging.destination: messaging.destination.name + messaging.rocketmq.message_keys: messaging.rocketmq.message.keys + messaging.message_payload_size_bytes: messaging.message.payload_size_bytes + messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes + messaging.message_id: messaging.message.id + messaging.kafka.tombstone: messaging.kafka.message.tombstone + messaging.protocol_version: net.app.protocol.version + messaging.destination_kind: messaging.destination.kind + messaging.conversation_id: messaging.message.conversation_id + messaging.kafka.message_key: messaging.kafka.message.key + messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key + messaging.protocol: net.app.protocol.name + messaging.temp_destination: messaging.destination.temporary + messaging.rocketmq.message_type: messaging.rocketmq.message.type + messaging.consumer_id: messaging.consumer.id + messaging.rocketmq.message_tag: messaging.rocketmq.message.tag + messaging.kafka.partition: messaging.kafka.destination.partition + messaging.kafka.consumer_group: messaging.kafka.consumer.group + resources: null + 1.18.0: + metrics: null + logs: null + spans: null + resources: null + 1.19.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + faas.execution: faas.invocation_id + - rename_attributes: + attribute_map: + faas.id: cloud.resource_id + - rename_attributes: + attribute_map: + http.user_agent: user_agent.original + resources: + changes: + - rename_attributes: + attribute_map: + browser.user_agent: user_agent.original + 1.20.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + net.app.protocol.version: net.protocol.version + net.app.protocol.name: net.protocol.name + resources: null + 1.21.0: + metrics: + changes: + - rename_attributes: + attribute_map: {} + rename_metrics: + process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + messaging.kafka.client_id: messaging.client_id + messaging.rocketmq.client_id: messaging.client_id + - rename_attributes: + attribute_map: + http.client_ip: client.address + net.host.name: server.address + net.sock.peer.name: server.socket.domain + net.host.port: server.port + net.sock.host.addr: server.socket.address + net.sock.host.port: server.socket.port + - rename_attributes: + attribute_map: + net.host.carrier.icc: network.carrier.icc + net.protocol.version: network.protocol.version + net.host.connection.type: network.connection.type + net.host.carrier.mnc: network.carrier.mnc + net.host.carrier.mcc: network.carrier.mcc + net.host.connection.subtype: network.connection.subtype + net.protocol.name: network.protocol.name + net.host.carrier.name: network.carrier.name + - rename_attributes: + attribute_map: + http.method: http.request.method + http.url: url.full + http.response_content_length: http.response.body.size + http.scheme: url.scheme + http.status_code: http.response.status_code + http.request_content_length: http.request.body.size + resources: null diff --git a/data/resolved-schema-metrics.yaml b/data/resolved-schema-metrics.yaml new file mode 100644 index 00000000..779172ce --- /dev/null +++ b/data/resolved-schema-metrics.yaml @@ -0,0 +1,587 @@ +file_format: 1.2.0 +parent_schema_url: data/open-telemetry-schema.1.22.0.yaml +schema_url: https://mycompany.com/schemas/1.0.0 +schema: + resource: + attributes: + - id: os.build_id + type: string + brief: Unique identifier for a particular build or compilation of the operating system. + examples: + - TQ3C.230805.001.B2 + - '20E247' + - '22621' + requirement_level: recommended + note: '' + - id: os.name + type: string + brief: Human readable operating system name. + examples: + - iOS + - Android + - Ubuntu + requirement_level: recommended + note: '' + - id: os.description + type: string + brief: | + Human readable (not intended to be parsed) OS version information, like e.g. reported by `ver` or `lsb_release -a` commands. + examples: + - Microsoft Windows [Version 10.0.18363.778] + - Ubuntu 18.04.1 LTS + requirement_level: recommended + note: '' + - id: os.type + type: + allow_custom_values: true + members: + - id: windows + value: windows + brief: Microsoft Windows + note: null + - id: linux + value: linux + brief: Linux + note: null + - id: darwin + value: darwin + brief: Apple Darwin + note: null + - id: freebsd + value: freebsd + brief: FreeBSD + note: null + - id: netbsd + value: netbsd + brief: NetBSD + note: null + - id: openbsd + value: openbsd + brief: OpenBSD + note: null + - id: dragonflybsd + value: dragonflybsd + brief: DragonFly BSD + note: null + - id: hpux + value: hpux + brief: HP-UX (Hewlett Packard Unix) + note: null + - id: aix + value: aix + brief: AIX (Advanced Interactive eXecutive) + note: null + - id: solaris + value: solaris + brief: SunOS, Oracle Solaris + note: null + - id: z_os + value: z_os + brief: IBM z/OS + note: null + brief: The operating system type. + requirement_level: required + note: '' + - id: os.version + type: string + brief: | + The version string of the operating system as defined in [Version Attributes](/docs/resource/README.md#version-attributes). + examples: + - 14.2.1 + - 18.04.1 + requirement_level: recommended + note: '' + instrumentation_library: + name: my-service + version: 1.0.0 + resource_metrics: + attributes: + - id: environment + type: string + brief: The environment in which the service is running + tag: sensitive-information + requirement_level: required + note: '' + metrics: + - name: http.server.request.duration + brief: Duration of HTTP server requests. + note: '' + attributes: + - id: error.type + type: + allow_custom_values: true + members: + - id: other + value: _OTHER + brief: A fallback error value to be used when the instrumentation does not define a custom value for it. + note: null + brief: Describes a class of error the operation ended with. + examples: + - timeout + - java.net.UnknownHostException + - server_certificate_invalid + - '500' + requirement_level: recommended + note: | + The `error.type` SHOULD be predictable and SHOULD have low cardinality. + Instrumentations SHOULD document the list of errors they report. + + The cardinality of `error.type` within one instrumentation library SHOULD be low, but + 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 codes (such as HTTP or gRPC status codes), + it's RECOMMENDED to use a domain-specific attribute and also set `error.type` to capture + all errors, regardless of whether they are defined within the domain-specific set or not. + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + instrument: histogram + unit: s + metric_groups: + - id: http + attributes: + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: error.type + type: + allow_custom_values: true + members: + - id: other + value: _OTHER + brief: A fallback error value to be used when the instrumentation does not define a custom value for it. + note: null + brief: Describes a class of error the operation ended with. + examples: + - timeout + - java.net.UnknownHostException + - server_certificate_invalid + - '500' + requirement_level: recommended + note: | + The `error.type` SHOULD be predictable and SHOULD have low cardinality. + Instrumentations SHOULD document the list of errors they report. + + The cardinality of `error.type` within one instrumentation library SHOULD be low, but + 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 codes (such as HTTP or gRPC status codes), + it's RECOMMENDED to use a domain-specific attribute and also set `error.type` to capture + all errors, regardless of whether they are defined within the domain-specific set or not. + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + metrics: + - name: http.server.request.duration + brief: Duration of HTTP server requests. + note: '' + attributes: [] + instrument: histogram + unit: s + - name: http.server.request.body.size + brief: Size of HTTP server request bodies. + note: | + 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. + attributes: [] + instrument: histogram + unit: By + - name: http.server.response.body.size + brief: Size of HTTP server response bodies. + note: | + 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. + attributes: [] + instrument: histogram + unit: By +versions: + 1.4.0: + metrics: null + logs: null + spans: null + resources: null + 1.5.0: + metrics: null + logs: null + spans: null + resources: null + 1.6.1: + metrics: null + logs: null + spans: null + resources: null + 1.7.0: + metrics: null + logs: null + spans: null + resources: null + 1.8.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: db.name + db.hbase.namespace: db.name + resources: null + 1.9.0: + metrics: null + logs: null + spans: null + resources: null + 1.10.0: + metrics: null + logs: null + spans: null + resources: null + 1.11.0: + metrics: null + logs: null + spans: null + resources: null + 1.12.0: + metrics: null + logs: null + spans: null + resources: null + 1.13.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + net.host.ip: net.sock.host.addr + net.peer.ip: net.sock.peer.addr + resources: null + 1.14.0: + metrics: null + logs: null + spans: null + resources: null + 1.15.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + http.retry_count: http.resend_count + resources: null + 1.16.0: + metrics: null + logs: null + spans: null + resources: null + 1.17.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + messaging.rocketmq.message_type: messaging.rocketmq.message.type + messaging.destination_kind: messaging.destination.kind + messaging.conversation_id: messaging.message.conversation_id + messaging.rocketmq.message_keys: messaging.rocketmq.message.keys + messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes + messaging.kafka.tombstone: messaging.kafka.message.tombstone + messaging.temp_destination: messaging.destination.temporary + messaging.destination: messaging.destination.name + messaging.consumer_id: messaging.consumer.id + messaging.protocol: net.app.protocol.name + messaging.message_id: messaging.message.id + messaging.message_payload_size_bytes: messaging.message.payload_size_bytes + messaging.kafka.message_key: messaging.kafka.message.key + messaging.kafka.partition: messaging.kafka.destination.partition + messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key + messaging.rocketmq.message_tag: messaging.rocketmq.message.tag + messaging.protocol_version: net.app.protocol.version + messaging.kafka.consumer_group: messaging.kafka.consumer.group + resources: null + 1.18.0: + metrics: null + logs: null + spans: null + resources: null + 1.19.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + faas.execution: faas.invocation_id + - rename_attributes: + attribute_map: + faas.id: cloud.resource_id + - rename_attributes: + attribute_map: + http.user_agent: user_agent.original + resources: + changes: + - rename_attributes: + attribute_map: + browser.user_agent: user_agent.original + 1.20.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + net.app.protocol.name: net.protocol.name + net.app.protocol.version: net.protocol.version + resources: null + 1.21.0: + metrics: + changes: + - rename_attributes: + attribute_map: {} + rename_metrics: + process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + messaging.kafka.client_id: messaging.client_id + messaging.rocketmq.client_id: messaging.client_id + - rename_attributes: + attribute_map: + net.host.name: server.address + net.sock.peer.name: server.socket.domain + net.sock.host.addr: server.socket.address + http.client_ip: client.address + net.host.port: server.port + net.sock.host.port: server.socket.port + - rename_attributes: + attribute_map: + net.protocol.name: network.protocol.name + net.host.carrier.icc: network.carrier.icc + net.host.carrier.mnc: network.carrier.mnc + net.host.carrier.name: network.carrier.name + net.host.carrier.mcc: network.carrier.mcc + net.protocol.version: network.protocol.version + net.host.connection.type: network.connection.type + net.host.connection.subtype: network.connection.subtype + - rename_attributes: + attribute_map: + http.url: url.full + http.request_content_length: http.request.body.size + http.response_content_length: http.response.body.size + http.status_code: http.response.status_code + http.scheme: url.scheme + http.method: http.request.method + resources: null + 1.22.0: + metrics: + changes: + - rename_attributes: + attribute_map: + messaging.message.payload_size_bytes: messaging.message.body.size + rename_metrics: {} + - rename_attributes: + attribute_map: {} + rename_metrics: + http.client.duration: http.client.request.duration + http.server.duration: http.server.request.duration + - rename_attributes: + attribute_map: {} + rename_metrics: + process.runtime.jvm.buffer.usage: jvm.buffer.memory.usage + process.runtime.jvm.classes.unloaded: jvm.class.unloaded + process.runtime.jvm.classes.current_loaded: jvm.class.count + process.runtime.jvm.system.cpu.utilization: jvm.system.cpu.utilization + process.runtime.jvm.memory.usage: jvm.memory.usage + process.runtime.jvm.gc.duration: jvm.gc.duration + process.runtime.jvm.memory.limit: jvm.memory.limit + process.runtime.jvm.system.cpu.load_1m: jvm.system.cpu.load_1m + process.runtime.jvm.cpu.recent_utilization: jvm.cpu.recent_utilization + process.runtime.jvm.buffer.limit: jvm.buffer.memory.limit + process.runtime.jvm.cpu.time: jvm.cpu.time + process.runtime.jvm.memory.usage_after_last_gc: jvm.memory.usage_after_last_gc + process.runtime.jvm.buffer.count: jvm.buffer.count + process.runtime.jvm.classes.loaded: jvm.class.loaded + process.runtime.jvm.memory.committed: jvm.memory.committed + process.runtime.jvm.memory.init: jvm.memory.init + process.runtime.jvm.threads.count: jvm.thread.count + - rename_attributes: + attribute_map: + pool: jvm.memory.pool.name + type: jvm.memory.type + apply_to_metrics: + - jvm.memory.usage + - jvm.memory.committed + - jvm.memory.limit + - jvm.memory.usage_after_last_gc + - jvm.memory.init + rename_metrics: {} + - rename_attributes: + attribute_map: + name: jvm.gc.name + action: jvm.gc.action + apply_to_metrics: + - jvm.gc.duration + rename_metrics: {} + - rename_attributes: + attribute_map: + daemon: thread.daemon + apply_to_metrics: + - jvm.threads.count + rename_metrics: {} + - rename_attributes: + attribute_map: + pool: jvm.buffer.pool.name + apply_to_metrics: + - jvm.buffer.usage + - jvm.buffer.limit + - jvm.buffer.count + rename_metrics: {} + - rename_attributes: + attribute_map: + cpu: system.cpu.logical_number + state: system.cpu.state + apply_to_metrics: + - system.cpu.time + - system.cpu.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + state: system.memory.state + apply_to_metrics: + - system.memory.usage + - system.memory.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + state: system.paging.state + apply_to_metrics: + - system.paging.usage + - system.paging.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + direction: system.paging.direction + type: system.paging.type + apply_to_metrics: + - system.paging.faults + - system.paging.operations + rename_metrics: {} + - rename_attributes: + attribute_map: + direction: system.disk.direction + device: system.device + apply_to_metrics: + - system.disk.io + - system.disk.operations + - system.disk.io_time + - system.disk.operation_time + - system.disk.merged + rename_metrics: {} + - rename_attributes: + attribute_map: + state: system.filesystem.state + device: system.device + type: system.filesystem.type + mode: system.filesystem.mode + mountpoint: system.filesystem.mountpoint + apply_to_metrics: + - system.filesystem.usage + - system.filesystem.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + protocol: network.protocol + direction: system.network.direction + device: system.device + state: system.network.state + apply_to_metrics: + - system.network.dropped + - system.network.packets + - system.network.errors + - system.network.io + - system.network.connections + rename_metrics: {} + - rename_attributes: + attribute_map: + status: system.processes.status + apply_to_metrics: + - system.processes.count + rename_metrics: {} + - rename_attributes: + attribute_map: {} + rename_metrics: + http.server.response.size: http.server.response.body.size + http.server.request.size: http.server.request.body.size + logs: null + spans: null + resources: null diff --git a/data/resolved-schema-spans.yaml b/data/resolved-schema-spans.yaml new file mode 100644 index 00000000..82d82543 --- /dev/null +++ b/data/resolved-schema-spans.yaml @@ -0,0 +1,490 @@ +file_format: 1.2.0 +parent_schema_url: https://opentelemetry.io/schemas/1.21.0 +schema_url: https://mycompany.com/schemas/1.0.0 +schema: + resource: + attributes: + - id: os.version + type: string + brief: | + The version string of the operating system as defined in [Version Attributes](/docs/resource/README.md#version-attributes). + examples: + - 14.2.1 + - 18.04.1 + requirement_level: recommended + note: '' + - id: os.build_id + type: string + brief: Unique identifier for a particular build or compilation of the operating system. + examples: + - TQ3C.230805.001.B2 + - '20E247' + - '22621' + requirement_level: recommended + note: '' + - id: os.name + type: string + brief: Human readable operating system name. + examples: + - iOS + - Android + - Ubuntu + requirement_level: recommended + note: '' + - id: os.type + type: + allow_custom_values: true + members: + - id: windows + value: windows + brief: Microsoft Windows + note: null + - id: linux + value: linux + brief: Linux + note: null + - id: darwin + value: darwin + brief: Apple Darwin + note: null + - id: freebsd + value: freebsd + brief: FreeBSD + note: null + - id: netbsd + value: netbsd + brief: NetBSD + note: null + - id: openbsd + value: openbsd + brief: OpenBSD + note: null + - id: dragonflybsd + value: dragonflybsd + brief: DragonFly BSD + note: null + - id: hpux + value: hpux + brief: HP-UX (Hewlett Packard Unix) + note: null + - id: aix + value: aix + brief: AIX (Advanced Interactive eXecutive) + note: null + - id: solaris + value: solaris + brief: SunOS, Oracle Solaris + note: null + - id: z_os + value: z_os + brief: IBM z/OS + note: null + brief: The operating system type. + requirement_level: required + note: '' + - id: os.description + type: string + brief: | + Human readable (not intended to be parsed) OS version information, like e.g. reported by `ver` or `lsb_release -a` commands. + examples: + - Microsoft Windows [Version 10.0.18363.778] + - Ubuntu 18.04.1 LTS + requirement_level: recommended + note: '' + instrumentation_library: + name: my-service + version: 1.0.0 + resource_spans: + spans: + - span_name: client.http.request + attributes: + - id: client.socket.address + type: string + brief: Client address of the socket connection - IP address or Unix domain socket name. + examples: + - /tmp/my.sock + - 127.0.0.1 + requirement_level: + recommended: If different than `client.address`. + note: | + When observed from the server side, this SHOULD represent the immediate client peer address. + When observed from the client side, this SHOULD represent the physical client address. + - id: client.address + type: string + brief: Client address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - /tmp/my.sock + - 10.1.2.80 + requirement_level: recommended + note: | + When observed from the server side, and when communicating through an intermediary, `client.address` SHOULD represent the client address behind any intermediaries (e.g. proxies) if it's available. + - id: client.socket.port + type: int + brief: Client port number of the socket connection. + examples: + - 35555 + requirement_level: + recommended: If different than `client.port`. + note: | + When observed from the server side, this SHOULD represent the immediate client peer port. + When observed from the client side, this SHOULD represent the physical client port. + - id: client.port + type: int + brief: Client port number. + examples: + - 65123 + requirement_level: recommended + note: | + When observed from the server side, and when communicating through an intermediary, `client.port` SHOULD represent the client port behind any intermediaries (e.g. proxies) if it's available. + events: + - event_name: error + attributes: + - id: exception.message + type: string + brief: The exception message. + examples: + - Division by zero + - Can't convert 'int' object to str implicitly + requirement_level: recommended + note: '' + - id: exception.type + type: string + brief: | + 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. + examples: + - java.net.ConnectException + - OSError + requirement_level: recommended + note: '' + - id: exception.stacktrace + type: string + brief: | + 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. + 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)' + requirement_level: recommended + note: '' + - event_name: feature_flag + attributes: + - id: feature_flag.variant + type: string + brief: | + SHOULD be a semantic identifier for a value. If one is unavailable, a stringified version of the value can be used. + examples: + - red + - 'true' + - on + requirement_level: recommended + note: |- + 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`. + + A stringified version of the value can be used in situations where a + semantic identifier is unavailable. String representation of the value + should be determined by the implementer. + - id: feature_flag.provider_name + type: string + brief: The name of the service provider that performs the flag evaluation. + examples: + - Flag Manager + requirement_level: recommended + note: '' + - id: feature_flag.key + type: string + brief: The unique identifier of the feature flag. + examples: + - logo-color + requirement_level: required + note: '' + - span_name: server.http.request + attributes: + - id: server.socket.address + type: string + brief: Server address of the socket connection - IP address or Unix domain socket name. + examples: + - 10.5.3.2 + requirement_level: + recommended: If different than `server.address`. + note: | + When observed from the client side, this SHOULD represent the immediate server peer address. + When observed from the server side, this SHOULD represent the physical server address. + - id: server.socket.domain + type: string + brief: Immediate server peer's domain name if available without reverse DNS lookup + examples: + - proxy.example.com + requirement_level: + recommended: If different than `server.address`. + note: Typically observed from the client side, and represents a proxy or other intermediary domain name. + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: server.socket.port + type: int + brief: Server port number of the socket connection. + examples: + - 16456 + requirement_level: + recommended: If different than `server.port`. + note: | + When observed from the client side, this SHOULD represent the immediate server peer port. + When observed from the server side, this SHOULD represent the physical server port. + events: + - event_name: error + attributes: + - id: exception.stacktrace + type: string + brief: | + 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. + 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)' + requirement_level: recommended + note: '' + - id: exception.type + type: string + brief: | + 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. + examples: + - java.net.ConnectException + - OSError + requirement_level: recommended + note: '' + - id: exception.message + type: string + brief: The exception message. + examples: + - Division by zero + - Can't convert 'int' object to str implicitly + requirement_level: recommended + note: '' + - event_name: feature_flag + attributes: + - id: feature_flag.key + type: string + brief: The unique identifier of the feature flag. + examples: + - logo-color + requirement_level: required + note: '' + - id: feature_flag.variant + type: string + brief: | + SHOULD be a semantic identifier for a value. If one is unavailable, a stringified version of the value can be used. + examples: + - red + - 'true' + - on + requirement_level: recommended + note: |- + 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`. + + A stringified version of the value can be used in situations where a + semantic identifier is unavailable. String representation of the value + should be determined by the implementer. + - id: feature_flag.provider_name + type: string + brief: The name of the service provider that performs the flag evaluation. + examples: + - Flag Manager + requirement_level: recommended + note: '' +versions: + 1.4.0: + metrics: null + logs: null + spans: null + resources: null + 1.5.0: + metrics: null + logs: null + spans: null + resources: null + 1.6.1: + metrics: null + logs: null + spans: null + resources: null + 1.7.0: + metrics: null + logs: null + spans: null + resources: null + 1.8.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + db.hbase.namespace: db.name + db.cassandra.keyspace: db.name + resources: null + 1.9.0: + metrics: null + logs: null + spans: null + resources: null + 1.10.0: + metrics: null + logs: null + spans: null + resources: null + 1.11.0: + metrics: null + logs: null + spans: null + resources: null + 1.12.0: + metrics: null + logs: null + spans: null + resources: null + 1.13.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + net.peer.ip: net.sock.peer.addr + net.host.ip: net.sock.host.addr + resources: null + 1.14.0: + metrics: null + logs: null + spans: null + resources: null + 1.15.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + http.retry_count: http.resend_count + resources: null + 1.16.0: + metrics: null + logs: null + spans: null + resources: null + 1.17.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + messaging.conversation_id: messaging.message.conversation_id + messaging.consumer_id: messaging.consumer.id + messaging.destination: messaging.destination.name + messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key + messaging.message_id: messaging.message.id + messaging.kafka.message_key: messaging.kafka.message.key + messaging.message_payload_size_bytes: messaging.message.payload_size_bytes + messaging.rocketmq.message_tag: messaging.rocketmq.message.tag + messaging.kafka.partition: messaging.kafka.destination.partition + messaging.protocol: net.app.protocol.name + messaging.rocketmq.message_type: messaging.rocketmq.message.type + messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes + messaging.protocol_version: net.app.protocol.version + messaging.kafka.tombstone: messaging.kafka.message.tombstone + messaging.kafka.consumer_group: messaging.kafka.consumer.group + messaging.temp_destination: messaging.destination.temporary + messaging.destination_kind: messaging.destination.kind + messaging.rocketmq.message_keys: messaging.rocketmq.message.keys + resources: null + 1.18.0: + metrics: null + logs: null + spans: null + resources: null + 1.19.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + faas.execution: faas.invocation_id + - rename_attributes: + attribute_map: + faas.id: cloud.resource_id + - rename_attributes: + attribute_map: + http.user_agent: user_agent.original + resources: + changes: + - rename_attributes: + attribute_map: + browser.user_agent: user_agent.original + 1.20.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + net.app.protocol.name: net.protocol.name + net.app.protocol.version: net.protocol.version + resources: null + 1.21.0: + metrics: + changes: + - rename_attributes: + attribute_map: {} + rename_metrics: + process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + messaging.kafka.client_id: messaging.client_id + messaging.rocketmq.client_id: messaging.client_id + - rename_attributes: + attribute_map: + net.sock.host.addr: server.socket.address + net.sock.host.port: server.socket.port + http.client_ip: client.address + net.sock.peer.name: server.socket.domain + net.host.name: server.address + net.host.port: server.port + - rename_attributes: + attribute_map: + net.host.carrier.icc: network.carrier.icc + net.protocol.name: network.protocol.name + net.host.connection.type: network.connection.type + net.host.carrier.mcc: network.carrier.mcc + net.host.carrier.name: network.carrier.name + net.protocol.version: network.protocol.version + net.host.connection.subtype: network.connection.subtype + net.host.carrier.mnc: network.carrier.mnc + - rename_attributes: + attribute_map: + http.status_code: http.response.status_code + http.url: url.full + http.request_content_length: http.request.body.size + http.response_content_length: http.response.body.size + http.method: http.request.method + http.scheme: url.scheme + resources: null diff --git a/data/resolved_schema.yaml b/data/resolved_schema.yaml new file mode 100644 index 00000000..2dc0011b --- /dev/null +++ b/data/resolved_schema.yaml @@ -0,0 +1,561 @@ +file_format: 1.2.0 +parent_schema_url: data/open-telemetry-schema.1.22.0.yaml +schema_url: https://mycompany.com/schemas/1.0.0 +semantic_conventions: +- url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/url.yaml +- url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/http-common.yaml +- url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/client.yaml +- url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/general.yaml +- url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/exception.yaml +- url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/server.yaml +- url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/network.yaml +- url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/http.yaml +- url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/jvm-metrics.yaml +schema: + resource: + attributes: + - ref: service.name + requirement_level: recommended + note: '' + value: my-service + - ref: service.version + requirement_level: recommended + note: '' + value: '{{SERVICE_VERSION}}' + instrumentation_library: + name: my-service + version: 1.0.0 + resource_metrics: + attributes: + - id: environment + type: string + brief: The environment in which the service is running + examples: null + tag: sensitive-information + requirement_level: required + note: '' + metrics: + - name: jvm.thread.count + brief: Number of executing platform threads. + note: '' + attributes: + - id: thread.daemon + type: boolean + brief: Whether the thread is daemon or not. + examples: null + requirement_level: recommended + note: '' + instrument: updowncounter + unit: '{thread}' + - name: jvm.class.loaded + brief: Number of classes loaded since JVM start. + note: '' + attributes: [] + instrument: counter + unit: '{class}' + - name: jvm.cpu.recent_utilization + brief: Recent CPU utilization for the process as reported by the JVM. + note: | + 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()). + attributes: [] + instrument: gauge + unit: '1' + - name: http.server.request.duration + brief: Measures the duration of inbound HTTP requests. + note: '' + attributes: + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: http.response.status_code + type: int + brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).' + examples: + - 200 + requirement_level: + conditionally_required: If and only if one was received/sent. + note: '' + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: recommended + note: '' + instrument: histogram + unit: s + metric_groups: + - id: http + attributes: + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: http.response.status_code + type: int + brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).' + examples: + - 200 + requirement_level: + conditionally_required: If and only if one was received/sent. + note: '' + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: recommended + note: '' + - id: url.host + type: string + brief: The host of the request + examples: null + requirement_level: required + note: '' + metrics: + - name: jvm.thread.count + brief: Number of executing platform threads. + note: '' + attributes: + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: http.response.status_code + type: int + brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).' + examples: + - 200 + requirement_level: + conditionally_required: If and only if one was received/sent. + note: '' + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: recommended + note: '' + - id: url.host + type: string + brief: The host of the request + examples: null + requirement_level: required + note: '' + instrument: updowncounter + unit: '{thread}' + - name: jvm.class.loaded + brief: Number of classes loaded since JVM start. + note: '' + attributes: + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: http.response.status_code + type: int + brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).' + examples: + - 200 + requirement_level: + conditionally_required: If and only if one was received/sent. + note: '' + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: recommended + note: '' + - id: url.host + type: string + brief: The host of the request + examples: null + requirement_level: required + note: '' + instrument: counter + unit: '{class}' + - name: jvm.cpu.recent_utilization + brief: Recent CPU utilization for the process as reported by the JVM. + note: | + 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()). + attributes: + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: http.response.status_code + type: int + brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).' + examples: + - 200 + requirement_level: + conditionally_required: If and only if one was received/sent. + note: '' + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: recommended + note: '' + - id: url.host + type: string + brief: The host of the request + examples: null + requirement_level: required + note: '' + instrument: gauge + unit: '1' + resource_events: + events: + - event_name: http + domain: http + attributes: + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: http.response.status_code + type: int + brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).' + examples: + - 200 + requirement_level: + conditionally_required: If and only if one was received/sent. + note: '' + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client used has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: recommended + note: '' + - id: url.host + type: string + brief: The host of the request + examples: null + requirement_level: required + note: '' + resource_spans: + spans: + - span_name: http.request + attributes: + - id: server.address + type: string + brief: Server address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - example.com + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.address` SHOULD represent + the server address behind any intermediaries (e.g. proxies) if it's available. + - id: server.port + type: int + brief: Server port number + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + When observed from the client side, and when communicating through an intermediary, `server.port` SHOULD represent the server port behind any intermediaries (e.g. proxies) if it's available. + - id: server.socket.address + type: string + brief: Server address of the socket connection - IP address or Unix domain socket name. + examples: + - 10.5.3.2 + requirement_level: + recommended: If different than `server.address`. + note: | + When observed from the client side, this SHOULD represent the immediate server peer address. + When observed from the server side, this SHOULD represent the physical server address. + - id: server.socket.port + type: int + brief: Server port number of the socket connection. + examples: + - 16456 + requirement_level: + recommended: If different than `server.port`. + note: | + When observed from the client side, this SHOULD represent the immediate server peer port. + When observed from the server side, this SHOULD represent the physical server port. + - id: client.address + type: string + brief: Client address - domain name if available without reverse DNS lookup, otherwise IP address or Unix domain socket name. + examples: + - /tmp/my.sock + - 10.1.2.80 + requirement_level: recommended + note: | + When observed from the server side, and when communicating through an intermediary, `client.address` SHOULD represent the client address behind any intermediaries (e.g. proxies) if it's available. + - id: client.port + type: int + brief: Client port number. + examples: + - 65123 + requirement_level: recommended + note: | + When observed from the server side, and when communicating through an intermediary, `client.port` SHOULD represent the client port behind any intermediaries (e.g. proxies) if it's available. + - id: client.socket.address + type: string + brief: Client address of the socket connection - IP address or Unix domain socket name. + examples: + - /tmp/my.sock + - 127.0.0.1 + requirement_level: + recommended: If different than `client.address`. + note: | + When observed from the server side, this SHOULD represent the immediate client peer address. + When observed from the client side, this SHOULD represent the physical client address. + - id: client.socket.port + type: int + brief: Client port number of the socket connection. + examples: + - 35555 + requirement_level: + recommended: If different than `client.port`. + note: | + When observed from the server side, this SHOULD represent the immediate client peer port. + When observed from the client side, this SHOULD represent the physical client port. + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: recommended + note: '' + - id: url.host + type: string + brief: The host of the request + examples: null + requirement_level: required + note: '' + events: + - event_name: error + attributes: + - id: exception.type + type: string + brief: | + 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. + examples: + - java.net.ConnectException + - OSError + requirement_level: recommended + note: '' + - id: exception.message + type: string + brief: The exception message. + examples: + - Division by zero + - Can't convert 'int' object to str implicitly + requirement_level: recommended + note: '' + - id: exception.stacktrace + type: string + brief: | + 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. + 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)' + requirement_level: recommended + note: '' diff --git a/demo/alternative-resolved-schema.yaml b/demo/alternative-resolved-schema.yaml new file mode 100644 index 00000000..3d19e741 --- /dev/null +++ b/demo/alternative-resolved-schema.yaml @@ -0,0 +1,657 @@ +file_format: 1.0.0 +schema_url: https://mycompany.com/schemas/1.0.0 + +catalog: + attributes: + - id: exception.message + type: string + brief: The exception message. + examples: + - Division by zero + - Can't convert 'int' object to str implicitly + requirement_level: required + note: '' + - id: exception.stacktrace + type: string + brief: | + 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. + 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)' + requirement_level: recommended + note: '' + - id: exception.type + type: string + brief: | + 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. + examples: + - java.net.ConnectException + - OSError + requirement_level: recommended + note: '' + - id: client.address + type: string + brief: Client address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - client.example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: client.port + type: int + brief: Client port number. + examples: + - 65123 + requirement_level: recommended + note: | + 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. + stability: stable + - id: service.instance.id + type: string + brief: The unique identifier of the service instance + examples: null + requirement_level: recommended + note: '' + - id: service.name + type: string + brief: | + Logical name of the service. + examples: + - shoppingcart + requirement_level: required + note: | + 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`. + value: my-service + - id: service.version + type: string + brief: | + The version string of the service API or implementation. The format is not defined by these conventions. + examples: + - 2.0.0 + - a01dbef8a + requirement_level: required + note: '' + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + stability: stable + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + stability: stable + - id: server.address + type: string + brief: Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: server.port + type: int + brief: Server port number. + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + 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. + stability: stable + - id: url.scheme.2 + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: required + note: '' + stability: stable + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: required + note: '' + stability: stable + tags: + sensitivity: PII + - id: jvm.thread.daemon + type: boolean + brief: Whether the thread is daemon or not. + examples: null + requirement_level: recommended + note: '' + - id: jvm.thread.state + type: + allow_custom_values: false + members: + - id: new + value: new + brief: A thread that has not yet started is in this state. + note: null + - id: runnable + value: runnable + brief: A thread executing in the Java virtual machine is in this state. + note: null + - id: blocked + value: blocked + brief: A thread that is blocked waiting for a monitor lock is in this state. + note: null + - id: waiting + value: waiting + brief: A thread that is waiting indefinitely for another thread to perform a particular action is in this state. + note: null + - id: timed_waiting + value: timed_waiting + brief: A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state. + note: null + - id: terminated + value: terminated + brief: A thread that has exited is in this state. + note: null + brief: State of the thread. + examples: + - runnable + - blocked + requirement_level: recommended + note: '' + - id: action + type: string + brief: Name of the garbage collector action. + examples: + - end of minor GC + - end of major GC + requirement_level: recommended + note: | + 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()). + - id: name + type: string + brief: Name of the garbage collector. + examples: + - G1 Young Generation + - G1 Old Generation + requirement_level: recommended + note: | + 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()). + metrics: + - name: jvm.thread.count + brief: Number of executing platform threads. + note: '' + attributes: + - ref: network.protocol.name + - ref: network.protocol.version + - ref: server.address + - ref: server.port + - ref: url.scheme + - ref: jvm.thread.daemon + - ref: jvm.thread.state + instrument: updowncounter + unit: '{thread}' + - name: jvm.class.loaded + brief: Number of classes loaded since JVM start. + note: '' + attributes: + - ref: network.protocol.name + - ref: network.protocol.version + - ref: server.address + - ref: server.port + - ref: url.scheme.2 + instrument: counter + unit: '{class}' + tags: + sensitivity: PII + - name: jvm.cpu.recent_utilization + brief: Recent CPU utilization for the process as reported by the JVM. + note: | + 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()). + attributes: + - ref: network.protocol.name + - ref: network.protocol.version + - ref: server.address + - ref: server.port + - ref: url.scheme + instrument: gauge + unit: '1' + - name: jvm.gc.duration + brief: Duration of JVM garbage collection actions. + note: '' + attributes: + - ref: action + - ref: name + instrument: histogram + unit: s + +schema: + resource: + attributes: + - ref: service.instance.id + - ref: service.name + - ref: service.version + instrumentation_library: + name: my-service + version: 1.0.0 + resource_metrics: + metrics: + - ref: jvm.thread.count + - ref: jvm.class.loaded + - ref: jvm.cpu.recent_utilization + - ref: jvm.gc.duration + metric_groups: + - name: http + attributes: + - ref: network.protocol.name + - ref: network.protocol.version + - ref: server.address + - ref: url.host + - ref: url.scheme + - ref: server.port + metrics: + - ref: jvm.class.loaded + - ref: jvm.cpu.recent_utilization + brief: null + note: null + resource_events: + events: + - event_name: request + domain: http + attributes: + - ref: network.protocol.name + - ref: network.protocol.version + - ref: server.address + - ref: server.port + - ref: url.host + - ref: url.scheme + brief: null + note: null + - event_name: response + domain: http + attributes: + - ref: network.protocol.name + - ref: network.protocol.version + - ref: server.address + - ref: server.port + - ref: url.host + - ref: url.scheme + brief: null + note: null + resource_spans: + spans: + - span_name: http.request + attributes: + - ref: client.address + - ref: client.port + - ref: server.address + - ref: server.port + - ref: url.host + - ref: url.scheme + events: + - event_name: error + attributes: + - ref: exception.message + - ref: exception.stacktrace + - ref: exception.type + brief: null + note: null + brief: null + note: null + - span_name: database.query + attributes: + - ref: client.address + - ref: client.port + - ref: server.address + - ref: server.port + - ref: url.host + - ref: url.scheme + events: + - event_name: error + attributes: + - ref: exception.message + - ref: exception.stacktrace + - ref: exception.type + brief: null + note: null + brief: null + note: null +versions: + 1.4.0: + metrics: null + logs: null + spans: null + resources: null + 1.5.0: + metrics: null + logs: null + spans: null + resources: null + 1.6.1: + metrics: null + logs: null + spans: null + resources: null + 1.7.0: + metrics: null + logs: null + spans: null + resources: null + 1.8.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: db.name + db.hbase.namespace: db.name + resources: null + 1.9.0: + metrics: null + logs: null + spans: null + resources: null + 1.10.0: + metrics: null + logs: null + spans: null + resources: null + 1.11.0: + metrics: null + logs: null + spans: null + resources: null + 1.12.0: + metrics: null + logs: null + spans: null + resources: null + 1.13.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + net.host.ip: net.sock.host.addr + net.peer.ip: net.sock.peer.addr + resources: null + 1.14.0: + metrics: null + logs: null + spans: null + resources: null + 1.15.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + http.retry_count: http.resend_count + resources: null + 1.16.0: + metrics: null + logs: null + spans: null + resources: null + 1.17.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + messaging.conversation_id: messaging.message.conversation_id + messaging.protocol_version: net.app.protocol.version + messaging.temp_destination: messaging.destination.temporary + messaging.message_payload_size_bytes: messaging.message.payload_size_bytes + messaging.rocketmq.message_type: messaging.rocketmq.message.type + messaging.rocketmq.message_keys: messaging.rocketmq.message.keys + messaging.destination: messaging.destination.name + messaging.protocol: net.app.protocol.name + messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes + messaging.kafka.consumer_group: messaging.kafka.consumer.group + messaging.kafka.tombstone: messaging.kafka.message.tombstone + messaging.kafka.partition: messaging.kafka.destination.partition + messaging.message_id: messaging.message.id + messaging.consumer_id: messaging.consumer.id + messaging.destination_kind: messaging.destination.kind + messaging.kafka.message_key: messaging.kafka.message.key + messaging.rocketmq.message_tag: messaging.rocketmq.message.tag + messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key + resources: null + 1.18.0: + metrics: null + logs: null + spans: null + resources: null + 1.19.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + faas.execution: faas.invocation_id + - rename_attributes: + attribute_map: + faas.id: cloud.resource_id + - rename_attributes: + attribute_map: + http.user_agent: user_agent.original + resources: + changes: + - rename_attributes: + attribute_map: + browser.user_agent: user_agent.original + 1.20.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + net.app.protocol.name: net.protocol.name + net.app.protocol.version: net.protocol.version + resources: null + 1.21.0: + metrics: + changes: + - rename_attributes: + attribute_map: {} + rename_metrics: + process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + messaging.rocketmq.client_id: messaging.client_id + messaging.kafka.client_id: messaging.client_id + - rename_attributes: + attribute_map: + http.client_ip: client.address + net.host.port: server.port + net.sock.host.addr: server.socket.address + net.host.name: server.address + net.sock.peer.name: server.socket.domain + net.sock.host.port: server.socket.port + - rename_attributes: + attribute_map: + net.protocol.name: network.protocol.name + net.protocol.version: network.protocol.version + net.host.connection.type: network.connection.type + net.host.carrier.name: network.carrier.name + net.host.carrier.mcc: network.carrier.mcc + net.host.carrier.icc: network.carrier.icc + net.host.connection.subtype: network.connection.subtype + net.host.carrier.mnc: network.carrier.mnc + - rename_attributes: + attribute_map: + http.response_content_length: http.response.body.size + http.method: http.request.method + http.url: url.full + http.status_code: http.response.status_code + http.scheme: url.scheme + http.request_content_length: http.request.body.size + resources: null + 1.22.0: + metrics: + changes: + - rename_attributes: + attribute_map: + messaging.message.payload_size_bytes: messaging.message.body.size + rename_metrics: {} + - rename_attributes: + attribute_map: {} + rename_metrics: + http.server.duration: http.server.request.duration + http.client.duration: http.client.request.duration + - rename_attributes: + attribute_map: {} + rename_metrics: + process.runtime.jvm.classes.unloaded: jvm.class.unloaded + process.runtime.jvm.memory.init: jvm.memory.init + process.runtime.jvm.buffer.count: jvm.buffer.count + process.runtime.jvm.system.cpu.load_1m: jvm.system.cpu.load_1m + process.runtime.jvm.memory.usage: jvm.memory.usage + process.runtime.jvm.classes.loaded: jvm.class.loaded + process.runtime.jvm.memory.limit: jvm.memory.limit + process.runtime.jvm.cpu.recent_utilization: jvm.cpu.recent_utilization + process.runtime.jvm.buffer.usage: jvm.buffer.memory.usage + process.runtime.jvm.buffer.limit: jvm.buffer.memory.limit + process.runtime.jvm.threads.count: jvm.thread.count + process.runtime.jvm.cpu.time: jvm.cpu.time + process.runtime.jvm.memory.usage_after_last_gc: jvm.memory.usage_after_last_gc + process.runtime.jvm.gc.duration: jvm.gc.duration + process.runtime.jvm.memory.committed: jvm.memory.committed + process.runtime.jvm.system.cpu.utilization: jvm.system.cpu.utilization + process.runtime.jvm.classes.current_loaded: jvm.class.count + - rename_attributes: + attribute_map: + pool: jvm.memory.pool.name + type: jvm.memory.type + apply_to_metrics: + - jvm.memory.usage + - jvm.memory.committed + - jvm.memory.limit + - jvm.memory.usage_after_last_gc + - jvm.memory.init + rename_metrics: {} + - rename_attributes: + attribute_map: + action: jvm.gc.action + name: jvm.gc.name + apply_to_metrics: + - jvm.gc.duration + rename_metrics: {} + - rename_attributes: + attribute_map: + daemon: thread.daemon + apply_to_metrics: + - jvm.threads.count + rename_metrics: {} + - rename_attributes: + attribute_map: + pool: jvm.buffer.pool.name + apply_to_metrics: + - jvm.buffer.usage + - jvm.buffer.limit + - jvm.buffer.count + rename_metrics: {} + - rename_attributes: + attribute_map: + state: system.cpu.state + cpu: system.cpu.logical_number + apply_to_metrics: + - system.cpu.time + - system.cpu.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + state: system.memory.state + apply_to_metrics: + - system.memory.usage + - system.memory.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + state: system.paging.state + apply_to_metrics: + - system.paging.usage + - system.paging.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + direction: system.paging.direction + type: system.paging.type + apply_to_metrics: + - system.paging.faults + - system.paging.operations + rename_metrics: {} + - rename_attributes: + attribute_map: + device: system.device + direction: system.disk.direction + apply_to_metrics: + - system.disk.io + - system.disk.operations + - system.disk.io_time + - system.disk.operation_time + - system.disk.merged + rename_metrics: {} + - rename_attributes: + attribute_map: + mountpoint: system.filesystem.mountpoint + state: system.filesystem.state + device: system.device + type: system.filesystem.type + mode: system.filesystem.mode + apply_to_metrics: + - system.filesystem.usage + - system.filesystem.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + device: system.device + direction: system.network.direction + state: system.network.state + protocol: network.protocol + apply_to_metrics: + - system.network.dropped + - system.network.packets + - system.network.errors + - system.network.io + - system.network.connections + rename_metrics: {} + - rename_attributes: + attribute_map: + status: system.processes.status + apply_to_metrics: + - system.processes.count + rename_metrics: {} + - rename_attributes: + attribute_map: {} + rename_metrics: + http.server.request.size: http.server.request.body.size + http.server.response.size: http.server.response.body.size + logs: null + spans: null + resources: null \ No newline at end of file diff --git a/demo/app-telemetry-schema.yaml b/demo/app-telemetry-schema.yaml new file mode 100644 index 00000000..9e7fb7b7 --- /dev/null +++ b/demo/app-telemetry-schema.yaml @@ -0,0 +1,149 @@ +file_format: 1.2.0 +parent_schema_url: demo/root-telemetry-schema.1.22.0.yaml +# Current schema url +schema_url: https://mycompany.com/schemas/1.0.0 + +# The section below outlines the telemetry schema for a service or application. +# This schema details all the metrics, logs, and spans specifically generated +# by that service or application. +# +# Note: Frameworks or libraries linked with the application that produce OTel +# telemetry data might also have their own telemetry schema, detailing the +# metrics, logs, and spans they generate locally. +schema: + # Resource attributes + # Only used when a Client SDK is generated + resource: + attributes: + - ref: service.name + value: "my-service" + - ref: service.version + requirement_level: required + - id: service.instance.id + type: string + brief: The unique identifier of the service instance + + # Instrumentation library + instrumentation_library: + name: "my-service" + version: "1.0.0" + # schema url? + + # Metrics declaration + resource_metrics: + # Declaration of all the univariate metrics + metrics: + - ref: jvm.thread.count + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + requirement_level: required + - ref: jvm.class.loaded + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + requirement_level: required + tags: + sensitivity: PII + tags: + sensitivity: PII + - ref: jvm.cpu.recent_utilization + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + requirement_level: required + - ref: jvm.gc.duration + # Declaration of all the multivariate metrics + metric_groups: + - name: http # name of a metrics group + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + - id: url.host + type: string + brief: The host of the request + requirement_level: required + metrics: # metrics sharing the same attributes + - ref: jvm.class.loaded + - ref: jvm.cpu.recent_utilization + + # Events declaration + resource_events: + events: + - event_name: request + domain: http + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + - id: url.host + type: string + brief: The host of the request + requirement_level: required + - event_name: response + domain: http + attributes: + - ref: server.address + - ref: server.port + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + - id: url.host + type: string + brief: The host of the request + requirement_level: required + + # Spans declaration + resource_spans: + spans: + - span_name: http.request # name of a specific tracer + attributes: + - ref: server.address + - ref: server.port + - ref: client.address + - ref: client.port + - ref: url.scheme + - id: url.host + type: string + brief: The host of the request + requirement_level: required + events: + - event_name: error + attributes: + - ref: exception.type + - ref: exception.message + requirement_level: required + - ref: exception.stacktrace + # links: + - span_name: database.query + attributes: + - ref: server.address + - ref: server.port + - ref: client.address + - ref: client.port + - ref: url.scheme + requirement_level: required + - id: url.host + type: string + brief: The host of the request + requirement_level: required + events: + - event_name: error + attributes: + - ref: exception.type + - ref: exception.message + - ref: exception.stacktrace \ No newline at end of file diff --git a/demo/resolved-schema.yaml b/demo/resolved-schema.yaml new file mode 100644 index 00000000..82ae1c67 --- /dev/null +++ b/demo/resolved-schema.yaml @@ -0,0 +1,980 @@ +file_format: 1.2.0 +parent_schema_url: demo/root-telemetry-schema.1.22.0.yaml +schema_url: https://mycompany.com/schemas/1.0.0 +schema: + resource: + attributes: + - id: service.instance.id + type: string + brief: The unique identifier of the service instance + examples: null + requirement_level: recommended + note: '' + - id: service.name + type: string + brief: | + Logical name of the service. + examples: + - shoppingcart + requirement_level: required + note: | + 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`. + value: my-service + - id: service.version + type: string + brief: | + The version string of the service API or implementation. The format is not defined by these conventions. + examples: + - 2.0.0 + - a01dbef8a + requirement_level: required + note: '' + instrumentation_library: + name: my-service + version: 1.0.0 + resource_metrics: + metrics: + - name: jvm.thread.count + brief: Number of executing platform threads. + note: '' + attributes: + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + stability: stable + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + stability: stable + - id: server.address + type: string + brief: Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: server.port + type: int + brief: Server port number. + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + 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. + stability: stable + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: required + note: '' + stability: stable + - id: jvm.thread.daemon + type: boolean + brief: Whether the thread is daemon or not. + examples: null + requirement_level: recommended + note: '' + - id: jvm.thread.state + type: + allow_custom_values: false + members: + - id: new + value: new + brief: A thread that has not yet started is in this state. + note: null + - id: runnable + value: runnable + brief: A thread executing in the Java virtual machine is in this state. + note: null + - id: blocked + value: blocked + brief: A thread that is blocked waiting for a monitor lock is in this state. + note: null + - id: waiting + value: waiting + brief: A thread that is waiting indefinitely for another thread to perform a particular action is in this state. + note: null + - id: timed_waiting + value: timed_waiting + brief: A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state. + note: null + - id: terminated + value: terminated + brief: A thread that has exited is in this state. + note: null + brief: State of the thread. + examples: + - runnable + - blocked + requirement_level: recommended + note: '' + instrument: updowncounter + unit: '{thread}' + - name: jvm.class.loaded + brief: Number of classes loaded since JVM start. + note: '' + attributes: + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + stability: stable + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + stability: stable + - id: server.address + type: string + brief: Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: server.port + type: int + brief: Server port number. + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + 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. + stability: stable + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: required + note: '' + stability: stable + tags: + sensitivity: PII + instrument: counter + unit: '{class}' + tags: + sensitivity: PII + - name: jvm.cpu.recent_utilization + brief: Recent CPU utilization for the process as reported by the JVM. + note: | + 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()). + attributes: + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + stability: stable + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + stability: stable + - id: server.address + type: string + brief: Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: server.port + type: int + brief: Server port number. + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + 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. + stability: stable + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: required + note: '' + stability: stable + instrument: gauge + unit: '1' + - name: jvm.gc.duration + brief: Duration of JVM garbage collection actions. + note: '' + attributes: + - id: action + type: string + brief: Name of the garbage collector action. + examples: + - end of minor GC + - end of major GC + requirement_level: recommended + note: | + 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()). + - id: name + type: string + brief: Name of the garbage collector. + examples: + - G1 Young Generation + - G1 Old Generation + requirement_level: recommended + note: | + 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()). + instrument: histogram + unit: s + metric_groups: + - name: http + attributes: + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + stability: stable + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + stability: stable + - id: server.address + type: string + brief: Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: url.host + type: string + brief: The host of the request + examples: null + requirement_level: required + note: '' + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: recommended + note: '' + stability: stable + - id: server.port + type: int + brief: Server port number. + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + 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. + stability: stable + metrics: + - name: jvm.class.loaded + brief: Number of classes loaded since JVM start. + note: '' + attributes: [] + instrument: counter + unit: '{class}' + - name: jvm.cpu.recent_utilization + brief: Recent CPU utilization for the process as reported by the JVM. + note: | + 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()). + attributes: [] + instrument: gauge + unit: '1' + brief: null + note: null + resource_events: + events: + - event_name: request + domain: http + attributes: + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + stability: stable + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + stability: stable + - id: server.address + type: string + brief: Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: server.port + type: int + brief: Server port number. + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + 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. + stability: stable + - id: url.host + type: string + brief: The host of the request + examples: null + requirement_level: required + note: '' + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: recommended + note: '' + stability: stable + brief: null + note: null + - event_name: response + domain: http + attributes: + - id: network.protocol.name + type: string + brief: '[OSI application layer](https://osi-model.com/application-layer/) or non-OSI equivalent.' + examples: + - amqp + - http + - mqtt + requirement_level: recommended + note: The value SHOULD be normalized to lowercase. + stability: stable + - id: network.protocol.version + type: string + brief: Version of the protocol specified in `network.protocol.name`. + examples: 3.1.1 + requirement_level: recommended + note: | + `network.protocol.version` refers to the version of the protocol used and might be different from the protocol client's version. If the HTTP client has a version of `0.27.2`, but sends HTTP version `1.1`, this attribute should be set to `1.1`. + stability: stable + - id: server.address + type: string + brief: Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: server.port + type: int + brief: Server port number. + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + 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. + stability: stable + - id: url.host + type: string + brief: The host of the request + examples: null + requirement_level: required + note: '' + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: recommended + note: '' + stability: stable + brief: null + note: null + resource_spans: + spans: + - span_name: http.request + attributes: + - id: client.address + type: string + brief: Client address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - client.example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: client.port + type: int + brief: Client port number. + examples: + - 65123 + requirement_level: recommended + note: | + 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. + stability: stable + - id: server.address + type: string + brief: Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: server.port + type: int + brief: Server port number. + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + 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. + stability: stable + - id: url.host + type: string + brief: The host of the request + examples: null + requirement_level: required + note: '' + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: recommended + note: '' + stability: stable + events: + - event_name: error + attributes: + - id: exception.message + type: string + brief: The exception message. + examples: + - Division by zero + - Can't convert 'int' object to str implicitly + requirement_level: required + note: '' + - id: exception.stacktrace + type: string + brief: | + 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. + 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)' + requirement_level: recommended + note: '' + - id: exception.type + type: string + brief: | + 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. + examples: + - java.net.ConnectException + - OSError + requirement_level: recommended + note: '' + brief: null + note: null + brief: null + note: null + - span_name: database.query + attributes: + - id: client.address + type: string + brief: Client address - domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - client.example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: client.port + type: int + brief: Client port number. + examples: + - 65123 + requirement_level: recommended + note: | + 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. + stability: stable + - id: server.address + type: string + brief: Server domain name if available without reverse DNS lookup; otherwise, IP address or Unix domain socket name. + examples: + - example.com + - 10.1.2.80 + - /tmp/my.sock + requirement_level: recommended + note: | + 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. + stability: stable + - id: server.port + type: int + brief: Server port number. + examples: + - 80 + - 8080 + - 443 + requirement_level: recommended + note: | + 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. + stability: stable + - id: url.host + type: string + brief: The host of the request + examples: null + requirement_level: required + note: '' + - id: url.scheme + type: string + brief: The [URI scheme](https://www.rfc-editor.org/rfc/rfc3986#section-3.1) component identifying the used protocol. + examples: + - https + - ftp + - telnet + requirement_level: required + note: '' + stability: stable + events: + - event_name: error + attributes: + - id: exception.message + type: string + brief: The exception message. + examples: + - Division by zero + - Can't convert 'int' object to str implicitly + requirement_level: recommended + note: '' + - id: exception.stacktrace + type: string + brief: | + 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. + 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)' + requirement_level: recommended + note: '' + - id: exception.type + type: string + brief: | + 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. + examples: + - java.net.ConnectException + - OSError + requirement_level: recommended + note: '' + brief: null + note: null + brief: null + note: null +versions: + 1.4.0: + metrics: null + logs: null + spans: null + resources: null + 1.5.0: + metrics: null + logs: null + spans: null + resources: null + 1.6.1: + metrics: null + logs: null + spans: null + resources: null + 1.7.0: + metrics: null + logs: null + spans: null + resources: null + 1.8.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: db.name + db.hbase.namespace: db.name + resources: null + 1.9.0: + metrics: null + logs: null + spans: null + resources: null + 1.10.0: + metrics: null + logs: null + spans: null + resources: null + 1.11.0: + metrics: null + logs: null + spans: null + resources: null + 1.12.0: + metrics: null + logs: null + spans: null + resources: null + 1.13.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + net.host.ip: net.sock.host.addr + net.peer.ip: net.sock.peer.addr + resources: null + 1.14.0: + metrics: null + logs: null + spans: null + resources: null + 1.15.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + http.retry_count: http.resend_count + resources: null + 1.16.0: + metrics: null + logs: null + spans: null + resources: null + 1.17.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + messaging.conversation_id: messaging.message.conversation_id + messaging.protocol_version: net.app.protocol.version + messaging.temp_destination: messaging.destination.temporary + messaging.message_payload_size_bytes: messaging.message.payload_size_bytes + messaging.rocketmq.message_type: messaging.rocketmq.message.type + messaging.rocketmq.message_keys: messaging.rocketmq.message.keys + messaging.destination: messaging.destination.name + messaging.protocol: net.app.protocol.name + messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes + messaging.kafka.consumer_group: messaging.kafka.consumer.group + messaging.kafka.tombstone: messaging.kafka.message.tombstone + messaging.kafka.partition: messaging.kafka.destination.partition + messaging.message_id: messaging.message.id + messaging.consumer_id: messaging.consumer.id + messaging.destination_kind: messaging.destination.kind + messaging.kafka.message_key: messaging.kafka.message.key + messaging.rocketmq.message_tag: messaging.rocketmq.message.tag + messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key + resources: null + 1.18.0: + metrics: null + logs: null + spans: null + resources: null + 1.19.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + faas.execution: faas.invocation_id + - rename_attributes: + attribute_map: + faas.id: cloud.resource_id + - rename_attributes: + attribute_map: + http.user_agent: user_agent.original + resources: + changes: + - rename_attributes: + attribute_map: + browser.user_agent: user_agent.original + 1.20.0: + metrics: null + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + net.app.protocol.name: net.protocol.name + net.app.protocol.version: net.protocol.version + resources: null + 1.21.0: + metrics: + changes: + - rename_attributes: + attribute_map: {} + rename_metrics: + process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization + logs: null + spans: + changes: + - rename_attributes: + attribute_map: + messaging.rocketmq.client_id: messaging.client_id + messaging.kafka.client_id: messaging.client_id + - rename_attributes: + attribute_map: + http.client_ip: client.address + net.host.port: server.port + net.sock.host.addr: server.socket.address + net.host.name: server.address + net.sock.peer.name: server.socket.domain + net.sock.host.port: server.socket.port + - rename_attributes: + attribute_map: + net.protocol.name: network.protocol.name + net.protocol.version: network.protocol.version + net.host.connection.type: network.connection.type + net.host.carrier.name: network.carrier.name + net.host.carrier.mcc: network.carrier.mcc + net.host.carrier.icc: network.carrier.icc + net.host.connection.subtype: network.connection.subtype + net.host.carrier.mnc: network.carrier.mnc + - rename_attributes: + attribute_map: + http.response_content_length: http.response.body.size + http.method: http.request.method + http.url: url.full + http.status_code: http.response.status_code + http.scheme: url.scheme + http.request_content_length: http.request.body.size + resources: null + 1.22.0: + metrics: + changes: + - rename_attributes: + attribute_map: + messaging.message.payload_size_bytes: messaging.message.body.size + rename_metrics: {} + - rename_attributes: + attribute_map: {} + rename_metrics: + http.server.duration: http.server.request.duration + http.client.duration: http.client.request.duration + - rename_attributes: + attribute_map: {} + rename_metrics: + process.runtime.jvm.classes.unloaded: jvm.class.unloaded + process.runtime.jvm.memory.init: jvm.memory.init + process.runtime.jvm.buffer.count: jvm.buffer.count + process.runtime.jvm.system.cpu.load_1m: jvm.system.cpu.load_1m + process.runtime.jvm.memory.usage: jvm.memory.usage + process.runtime.jvm.classes.loaded: jvm.class.loaded + process.runtime.jvm.memory.limit: jvm.memory.limit + process.runtime.jvm.cpu.recent_utilization: jvm.cpu.recent_utilization + process.runtime.jvm.buffer.usage: jvm.buffer.memory.usage + process.runtime.jvm.buffer.limit: jvm.buffer.memory.limit + process.runtime.jvm.threads.count: jvm.thread.count + process.runtime.jvm.cpu.time: jvm.cpu.time + process.runtime.jvm.memory.usage_after_last_gc: jvm.memory.usage_after_last_gc + process.runtime.jvm.gc.duration: jvm.gc.duration + process.runtime.jvm.memory.committed: jvm.memory.committed + process.runtime.jvm.system.cpu.utilization: jvm.system.cpu.utilization + process.runtime.jvm.classes.current_loaded: jvm.class.count + - rename_attributes: + attribute_map: + pool: jvm.memory.pool.name + type: jvm.memory.type + apply_to_metrics: + - jvm.memory.usage + - jvm.memory.committed + - jvm.memory.limit + - jvm.memory.usage_after_last_gc + - jvm.memory.init + rename_metrics: {} + - rename_attributes: + attribute_map: + action: jvm.gc.action + name: jvm.gc.name + apply_to_metrics: + - jvm.gc.duration + rename_metrics: {} + - rename_attributes: + attribute_map: + daemon: thread.daemon + apply_to_metrics: + - jvm.threads.count + rename_metrics: {} + - rename_attributes: + attribute_map: + pool: jvm.buffer.pool.name + apply_to_metrics: + - jvm.buffer.usage + - jvm.buffer.limit + - jvm.buffer.count + rename_metrics: {} + - rename_attributes: + attribute_map: + state: system.cpu.state + cpu: system.cpu.logical_number + apply_to_metrics: + - system.cpu.time + - system.cpu.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + state: system.memory.state + apply_to_metrics: + - system.memory.usage + - system.memory.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + state: system.paging.state + apply_to_metrics: + - system.paging.usage + - system.paging.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + direction: system.paging.direction + type: system.paging.type + apply_to_metrics: + - system.paging.faults + - system.paging.operations + rename_metrics: {} + - rename_attributes: + attribute_map: + device: system.device + direction: system.disk.direction + apply_to_metrics: + - system.disk.io + - system.disk.operations + - system.disk.io_time + - system.disk.operation_time + - system.disk.merged + rename_metrics: {} + - rename_attributes: + attribute_map: + mountpoint: system.filesystem.mountpoint + state: system.filesystem.state + device: system.device + type: system.filesystem.type + mode: system.filesystem.mode + apply_to_metrics: + - system.filesystem.usage + - system.filesystem.utilization + rename_metrics: {} + - rename_attributes: + attribute_map: + device: system.device + direction: system.network.direction + state: system.network.state + protocol: network.protocol + apply_to_metrics: + - system.network.dropped + - system.network.packets + - system.network.errors + - system.network.io + - system.network.connections + rename_metrics: {} + - rename_attributes: + attribute_map: + status: system.processes.status + apply_to_metrics: + - system.processes.count + rename_metrics: {} + - rename_attributes: + attribute_map: {} + rename_metrics: + http.server.request.size: http.server.request.body.size + http.server.response.size: http.server.response.body.size + logs: null + spans: null + resources: null diff --git a/demo/root-telemetry-schema.1.22.0.yaml b/demo/root-telemetry-schema.1.22.0.yaml new file mode 100644 index 00000000..6ab7886e --- /dev/null +++ b/demo/root-telemetry-schema.1.22.0.yaml @@ -0,0 +1,354 @@ +file_format: 1.2.0 +schema_url: https://opentelemetry.io/schemas/1.21.0 + +# Semantic conventions can be loaded from a list of URLs or from a git repository. +# The git repository approach is usually faster and download all the semantic +# conventions files dynamically without the need to update the schema file. +semantic_conventions: + - git_url: https://github.com/open-telemetry/semantic-conventions.git + path: model +#semantic_conventions: +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/client.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/destination.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/error.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/exception.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/faas-common.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/general.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/http-common.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/network.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/server.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/session.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/source.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/url.yaml +# # logs +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/events.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/general.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/log-exception.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/log-feature_flag.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/media.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/logs/mobile-events.yaml +# #metrics +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/database-metrics.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/faas-metrics.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/http.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/jvm-metrics-experimental.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/jvm-metrics.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/rpc-metrics.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/metrics/system-metrics.yaml +# # registry +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/registry/http.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/registry/network.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/registry/rpc.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/registry/url.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/registry/user-agent.yaml +# # resources +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/android.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/browser.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/container.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/deployment_environment.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/device.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/faas.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/host.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/k8s.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/oci.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/os.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/process.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/service.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/service_experimental.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/telemetry.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/telemetry_experimental.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/webengine.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/aws/ecs.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/aws/eks.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/aws/logs.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/gcp/cloud_run.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/gcp/gce.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/resource/cloud_provider/heroku.yaml +# # scope +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/scope/exporter/exporter.yaml +# # trace +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/cloudevents.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/compatibility.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/database.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/faas.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/feature-flag.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/http.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/messaging.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/rpc.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/trace-exception.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/aws/lambda.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/exporter/exporter.yaml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/instrumentation/aws-sdk.yml +# - url: https://raw.githubusercontent.com/open-telemetry/semantic-conventions/main/model/trace/instrumentation/graphql.yml + +versions: + 1.22.0: + metrics: + changes: + # https://github.com/open-telemetry/semantic-conventions/pull/229 + - rename_attributes: + attribute_map: + messaging.message.payload_size_bytes: messaging.message.body.size + # https://github.com/open-telemetry/semantic-conventions/pull/224 + - rename_metrics: + http.client.duration: http.client.request.duration + http.server.duration: http.server.request.duration + # https://github.com/open-telemetry/semantic-conventions/pull/241 + - rename_metrics: + process.runtime.jvm.memory.usage: jvm.memory.usage + process.runtime.jvm.memory.committed: jvm.memory.committed + process.runtime.jvm.memory.limit: jvm.memory.limit + process.runtime.jvm.memory.usage_after_last_gc: jvm.memory.usage_after_last_gc + process.runtime.jvm.gc.duration: jvm.gc.duration + # also https://github.com/open-telemetry/semantic-conventions/pull/252 + process.runtime.jvm.threads.count: jvm.thread.count + # also https://github.com/open-telemetry/semantic-conventions/pull/252 + process.runtime.jvm.classes.loaded: jvm.class.loaded + # also https://github.com/open-telemetry/semantic-conventions/pull/252 + process.runtime.jvm.classes.unloaded: jvm.class.unloaded + # also https://github.com/open-telemetry/semantic-conventions/pull/252 + # and https://github.com/open-telemetry/semantic-conventions/pull/60 + process.runtime.jvm.classes.current_loaded: jvm.class.count + process.runtime.jvm.cpu.time: jvm.cpu.time + process.runtime.jvm.cpu.recent_utilization: jvm.cpu.recent_utilization + process.runtime.jvm.memory.init: jvm.memory.init + process.runtime.jvm.system.cpu.utilization: jvm.system.cpu.utilization + process.runtime.jvm.system.cpu.load_1m: jvm.system.cpu.load_1m + # https://github.com/open-telemetry/semantic-conventions/pull/253 + process.runtime.jvm.buffer.usage: jvm.buffer.memory.usage + # https://github.com/open-telemetry/semantic-conventions/pull/253 + process.runtime.jvm.buffer.limit: jvm.buffer.memory.limit + process.runtime.jvm.buffer.count: jvm.buffer.count + # https://github.com/open-telemetry/semantic-conventions/pull/20 + - rename_attributes: + attribute_map: + type: jvm.memory.type + pool: jvm.memory.pool.name + apply_to_metrics: + - jvm.memory.usage + - jvm.memory.committed + - jvm.memory.limit + - jvm.memory.usage_after_last_gc + - jvm.memory.init + - rename_attributes: + attribute_map: + name: jvm.gc.name + action: jvm.gc.action + apply_to_metrics: + - jvm.gc.duration + - rename_attributes: + attribute_map: + daemon: thread.daemon + apply_to_metrics: + - jvm.threads.count + - rename_attributes: + attribute_map: + pool: jvm.buffer.pool.name + apply_to_metrics: + - jvm.buffer.usage + - jvm.buffer.limit + - jvm.buffer.count + # https://github.com/open-telemetry/semantic-conventions/pull/89 + - rename_attributes: + attribute_map: + state: system.cpu.state + cpu: system.cpu.logical_number + apply_to_metrics: + - system.cpu.time + - system.cpu.utilization + - rename_attributes: + attribute_map: + state: system.memory.state + apply_to_metrics: + - system.memory.usage + - system.memory.utilization + - rename_attributes: + attribute_map: + state: system.paging.state + apply_to_metrics: + - system.paging.usage + - system.paging.utilization + - rename_attributes: + attribute_map: + type: system.paging.type + direction: system.paging.direction + apply_to_metrics: + - system.paging.faults + - system.paging.operations + - rename_attributes: + attribute_map: + device: system.device + direction: system.disk.direction + apply_to_metrics: + - system.disk.io + - system.disk.operations + - system.disk.io_time + - system.disk.operation_time + - system.disk.merged + - rename_attributes: + attribute_map: + device: system.device + state: system.filesystem.state + type: system.filesystem.type + mode: system.filesystem.mode + mountpoint: system.filesystem.mountpoint + apply_to_metrics: + - system.filesystem.usage + - system.filesystem.utilization + - rename_attributes: + attribute_map: + device: system.device + direction: system.network.direction + protocol: network.protocol + state: system.network.state + apply_to_metrics: + - system.network.dropped + - system.network.packets + - system.network.errors + - system.network.io + - system.network.connections + - rename_attributes: + attribute_map: + status: system.processes.status + apply_to_metrics: + - system.processes.count + # https://github.com/open-telemetry/semantic-conventions/pull/247 + - rename_metrics: + http.server.request.size: http.server.request.body.size + http.server.response.size: http.server.response.body.size + 1.21.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3336 + - rename_attributes: + attribute_map: + messaging.kafka.client_id: messaging.client_id + messaging.rocketmq.client_id: messaging.client_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3402 + - rename_attributes: + attribute_map: + # net.peer.(name|port) attributes were usually populated on client side + # so they should be usually translated to server.(address|port) + # net.host.* attributes were only populated on server side + net.host.name: server.address + net.host.port: server.port + # was only populated on client side + net.sock.peer.name: server.socket.domain + # net.sock.peer.(addr|port) mapping is not possible + # since they applied to both client and server side + # were only populated on server side + net.sock.host.addr: server.socket.address + net.sock.host.port: server.socket.port + http.client_ip: client.address + # https://github.com/open-telemetry/opentelemetry-specification/pull/3426 + - rename_attributes: + attribute_map: + net.protocol.name: network.protocol.name + net.protocol.version: network.protocol.version + net.host.connection.type: network.connection.type + net.host.connection.subtype: network.connection.subtype + net.host.carrier.name: network.carrier.name + net.host.carrier.mcc: network.carrier.mcc + net.host.carrier.mnc: network.carrier.mnc + net.host.carrier.icc: network.carrier.icc + # https://github.com/open-telemetry/opentelemetry-specification/pull/3355 + - rename_attributes: + attribute_map: + http.method: http.request.method + http.status_code: http.response.status_code + http.scheme: url.scheme + http.url: url.full + http.request_content_length: http.request.body.size + http.response_content_length: http.response.body.size + metrics: + changes: + # https://github.com/open-telemetry/semantic-conventions/pull/53 + - rename_metrics: + process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization + 1.20.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3272 + - rename_attributes: + attribute_map: + net.app.protocol.name: net.protocol.name + net.app.protocol.version: net.protocol.version + 1.19.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3209 + - rename_attributes: + attribute_map: + faas.execution: faas.invocation_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3188 + - rename_attributes: + attribute_map: + faas.id: cloud.resource_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3190 + - rename_attributes: + attribute_map: + http.user_agent: user_agent.original + resources: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3190 + - rename_attributes: + attribute_map: + browser.user_agent: user_agent.original + 1.18.0: + 1.17.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2957 + - rename_attributes: + attribute_map: + messaging.consumer_id: messaging.consumer.id + messaging.protocol: net.app.protocol.name + messaging.protocol_version: net.app.protocol.version + messaging.destination: messaging.destination.name + messaging.temp_destination: messaging.destination.temporary + messaging.destination_kind: messaging.destination.kind + messaging.message_id: messaging.message.id + messaging.conversation_id: messaging.message.conversation_id + messaging.message_payload_size_bytes: messaging.message.payload_size_bytes + messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes + messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key + messaging.kafka.message_key: messaging.kafka.message.key + messaging.kafka.partition: messaging.kafka.destination.partition + messaging.kafka.tombstone: messaging.kafka.message.tombstone + messaging.rocketmq.message_type: messaging.rocketmq.message.type + messaging.rocketmq.message_tag: messaging.rocketmq.message.tag + messaging.rocketmq.message_keys: messaging.rocketmq.message.keys + messaging.kafka.consumer_group: messaging.kafka.consumer.group + 1.16.0: + 1.15.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2743 + - rename_attributes: + attribute_map: + http.retry_count: http.resend_count + 1.14.0: + 1.13.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2614 + - rename_attributes: + attribute_map: + net.peer.ip: net.sock.peer.addr + net.host.ip: net.sock.host.addr + 1.12.0: + 1.11.0: + 1.10.0: + 1.9.0: + 1.8.0: + spans: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: db.name + db.hbase.namespace: db.name + 1.7.0: + 1.6.1: + 1.5.0: + 1.4.0: \ No newline at end of file diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..1e04147a --- /dev/null +++ b/deny.toml @@ -0,0 +1,160 @@ +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +targets = [ +] + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url(s) of the advisory databases to use +db-urls = ["https://github.com/rustsec/advisory-db"] +# The lint level for security vulnerabilities +vulnerability = "deny" +# The lint level for unmaintained crates +unmaintained = "warn" +# The lint level for crates that have been yanked from their source registry +yanked = "warn" +# The lint level for crates with security notices. Note that as of +# 2019-12-17 there are no security notice advisories in +# https://github.com/rustsec/advisory-db +notice = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +# +# e.g. "RUSTSEC-0000-0000", +ignore = [ +] + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +unlicensed = "deny" +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +allow = [ + "MIT", + "MIT-0", + "Apache-2.0", + "BSD-3-Clause", + "MPL-2.0", + "Unicode-DFS-2016", + "CC0-1.0", + "ISC", + "OpenSSL", + "zlib-acknowledgement", + "Zlib", +] +exceptions = [ + { allow = [ + "Unlicense", + ], name = "measure_time" }, +] +# List of explicitly disallowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +deny = [ +] +# Lint level for licenses considered copyleft +copyleft = "deny" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +# * both - The license will be approved if it is both OSI-approved *AND* FSF +# * either - The license will be approved if it is either OSI-approved *OR* FSF +# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF +# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved +# * neither - This predicate is ignored and the default lint level is used +allow-osi-fsf-free = "neither" +# Lint level used when no other predicates are matched +# 1. License isn't in the allow or deny lists +# 2. License isn't copyleft +# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" +default = "deny" +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries. +# To see how to mark a crate as unpublished (to the official registry), +# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. +ignore = true + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "warn" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# The default lint level for `default` features for crates that are members of +# the workspace that is being checked. This can be overridden by allowing/denying +# `default` on a crate-by-crate basis if desired. +workspace-default-features = "allow" +# The default lint level for `default` features for external crates that are not +# members of the workspace. This can be overridden by allowing/denying `default` +# on a crate-by-crate basis if desired. +external-default-features = "allow" +# List of crates that are allowed. Use with care! +allow = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# List of crates to deny +deny = [ + # We use gix + { name = "git2" }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "deny" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "deny" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [ + "https://github.com/rust-lang/cargo", +] + +[sources.allow-org] +# 1 or more github.com organizations to allow git sources for +github = [] + +[[licenses.clarify]] +name = "ring" +expression = "MIT AND ISC AND OpenSSL" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 }, +] + +[[licenses.clarify]] +name = "measure_time" +version = "*" +expression = "Unlicense" +license-files = [ + { path = "LICENSE", hash = 0x39f8ad31 } +] \ No newline at end of file diff --git a/docs/component-telemetry-schema-proposal.yaml b/docs/component-telemetry-schema-proposal.yaml new file mode 100644 index 00000000..4277ece1 --- /dev/null +++ b/docs/component-telemetry-schema-proposal.yaml @@ -0,0 +1,65 @@ +# This file describes the structure and format of a component telemetry schema. +# A component telemetry schema can be used for three types of purposes: +# - defining the telemetry schema for an application or a service +# - defining the telemetry schema for a library +# - defining a semantic convention registry +file_format: 1.2.0 +schema_url: +# This optional field allows to specify the parent schema of the current schema. +# The parent schema is a resolved schema. +parent_schema_url: + +# This optional section allows for importing a semantic convention registry +# from a git repository containing a set of semantic convention files. It is +# also possible to import file by file. +semantic_conventions: + - git_url: + path: + - url: + +# The resource field is defined when the component schema is that of an +# application (as opposed to that of a library). The resource field contains a +# list of local references to attributes defined in the shared catalog within +# this file. +resource: + # attributes defined locally or inherited from the parent schema (if any) or + # from the semantic conventions (if any). + attributes: # attribute definitions + +# This optional section defines the instrumentation library, its version, and +# the schema of OTel entities reported by this instrumentation library ( +# representing an application or a library component). +# This section is not defined if the current component telemetry schema is only +# used to represent a semantic convention registry. +instrumentation_library: + name: + version: + # The schema details all the metrics, logs, and spans specifically generated + # by that instrumentation library. + schema: + # Declaration of all the univariate metrics + resource_metrics: + - metric_name: + # attributes defined locally or inherited from the parent schema (if any) or + # from the semantic conventions (if any). + attributes: # attribute definitions + # ... + # other metric fields + # ... + tags: + : + + # Declaration of all the spans + resource_spans: + - span_name: + # attributes defined locally or inherited from the parent schema (if any) or + # from the semantic conventions (if any). + attributes: # attribute definitions + # ... + # other span fields + # ... + tags: + : + +# Reuse the same versioning mechanism already defined in the telemetry schema v1.1.0 +versions: # see telemetry schema v1.1.0. \ No newline at end of file diff --git a/docs/component-telemetry-schema.md b/docs/component-telemetry-schema.md new file mode 100644 index 00000000..f6ae570f --- /dev/null +++ b/docs/component-telemetry-schema.md @@ -0,0 +1,139 @@ +# Component Telemetry Schema + +The Component Telemetry Schema is a developer-friendly format for defining an +application's or library's telemetry schema. Authors of applications or +libraries can enhance an existing Resolved Telemetry Schema by overriding or +adding new elements, importing Semantic Convention Registries, defining +resource attributes (only for applications), defining properties of the +instrumentation library, and defining the telemetry signals an application or +library can produce. They can also use the versioning mechanism from OTEP 0152. +The base schema is typically the official Telemetry Schema (Resolved), which +links to the OpenTelemetry Semantic Convention Registry. The final schema in +this system details all the signals produced by a specific application or +library. + +Although there is no direct lineage between these systems, a similar approach +was designed and deployed by Facebook to address the same type of problem but in +a proprietary context (refer to +this [positional paper](https://research.facebook.com/publications/positional-paper-schema-first-application-telemetry/) +for more information). + +The following diagram shows how a Component Telemetry Schema is structured. + +![Telemetry Schema](./images/0240-otel-weaver-component-schema.svg) + +> Note 1: Each signal definition, where possible, reuses the existing syntax and +> semantics defined by the semantic conventions. Each signal definition is also +> identified by a unique name (or ID), making schemas addressable, easy to +> traverse, validate, and diff. +> +> Note 2: This hierarchy of telemetry schemas helps large organizations in +> collaborating on the Component Telemetry Schema. It enables different +> aspects of a Component Telemetry Schema to be managed by various teams. +> +> Note 3: For all the elements that make up the Component Telemetry Schema, a +> general mechanism of annotation or tagging will be integrated in order to +> attach additional traits, characteristics, or constraints, allowing vendors +> and companies to extend the definition of concepts defined by OpenTelemetry. +> +> Note 4: Annotations and Tags can also be employed to modify schemas for +> diverse audiences. For example, the public version of a schema can exclude all +> signals or other metadata labeled as private. Similarly, elements can be +> designated as exclusively available for beta testers. These annotations can +> also identify attributes as PII (Personally Identifiable Information), and +> privacy policy enforcement can be implemented at various levels (e.g., in the +> generated client SDK or in a proxy). +> +> Note 5: This +> recent [paper](https://arxiv.org/pdf/2311.07509.pdf#:~:text=The%20results%20of%20the%20benchmark%20provide%20evidence%20that%20supports%20our,LLM%20without%20a%20Knowledge%20Graph) +> from [data.world](https://data.world/home/), along with +> the [MetricFlow framework](https://docs.getdbt.com/docs/build/about-metricflow) +> which underpins the [dbt Semantic Layer](https://www.getdbt.com/product/semantic-layer), +> highlights the significance of adopting a schema-first approach in data +> modeling, especially for Generative AI-based question answering systems. Tools +> like Observability Query Assistants ( +> e.g. [Elastic AI Assistant](https://www.elastic.co/fr/blog/introducing-elastic-ai-assistant) +> and [Honeycomb Query Assistant](https://www.honeycomb.io/blog/introducing-query-assistant?utm_source=newswire&utm_medium=link&utm_campaign=query_assistant)) +> are likely to become increasingly prevalent and efficient in the near future, +> thanks to the adoption of a schema-first approach. + +Several OTEPs will be dedicated to the precise definition of the structure and +the format of this/these schema(s). The rules for resolving overrides +(inheritance), external references, and conflicts will also be described in +these OTEPs. See the Roadmap section for a comprehensive list of these OTEPs. + +## Structure and Format + +The structure and format of the component telemetry schema is given here as an +example and corresponds to the author's vision (not yet validated) of what the +structure/format of a component telemetry schema could be. The definitive +structure and format of this component schema will be discussed and finalized +later in a dedicated OTEP. + +```yaml +# This file describes the structure and format of a component telemetry schema. +# A component telemetry schema can be used for three types of purposes: +# - defining the telemetry schema for an application or a service +# - defining the telemetry schema for a library +# - defining a semantic convention registry +file_format: 1.2.0 +schema_url: +# This optional field allows to specify the parent schema of the current schema. +# The parent schema is a resolved schema. +parent_schema_url: + +# This optional section allows for importing a semantic convention registry +# from a git repository containing a set of semantic convention files. It is +# also possible to import file by file. +semantic_conventions: + - git_url: + path: + - url: + +# The resource field is defined when the component schema is that of an +# application (as opposed to that of a library). The resource field contains a +# list of local references to attributes defined in the shared catalog within +# this file. +resource: + # attributes defined locally or inherited from the parent schema (if any) or + # from the semantic conventions (if any). + attributes: # attribute definitions + +# This optional section defines the instrumentation library, its version, and +# the schema of OTel entities reported by this instrumentation library ( +# representing an application or a library component). +# This section is not defined if the current component telemetry schema is only +# used to represent a semantic convention registry. +instrumentation_library: + name: + version: + # The schema details all the metrics, logs, and spans specifically generated + # by that instrumentation library. + schema: + # Declaration of all the univariate metrics + resource_metrics: + - metric_name: + # attributes defined locally or inherited from the parent schema (if any) or + # from the semantic conventions (if any). + attributes: # attribute definitions + # ... + # other metric fields + # ... + tags: + : + + # Declaration of all the spans + resource_spans: + - span_name: + # attributes defined locally or inherited from the parent schema (if any) or + # from the semantic conventions (if any). + attributes: # attribute definitions + # ... + # other span fields + # ... + tags: + : + +# Reuse the same versioning mechanism already defined in the telemetry schema v1.1.0 +versions: # see telemetry schema v1.1.0. +``` \ No newline at end of file diff --git a/docs/contribution.md b/docs/contribution.md new file mode 100644 index 00000000..ed8191e5 --- /dev/null +++ b/docs/contribution.md @@ -0,0 +1,10 @@ +# How to contribute + +## Add support for a new language +### Via Tera templates +### Via WASM plugin + +## Other WASM plugins +### Schema validation plugin +### Schema export plugin +### Variable resolver plugin \ No newline at end of file diff --git a/docs/dependencies.md b/docs/dependencies.md new file mode 100644 index 00000000..5b219162 --- /dev/null +++ b/docs/dependencies.md @@ -0,0 +1,8 @@ +# Internal crates interdependencies + +## Overview + +![Dependencies](./images/dependencies.svg) + +> To update this diagram, run `cargo depgraph --workspace-only | dot -Tsvg > docs/images/dependencies.svg` from the +> root of the repository. \ No newline at end of file diff --git a/docs/images/0240-otel-weaver-component-schema.svg b/docs/images/0240-otel-weaver-component-schema.svg new file mode 100644 index 00000000..589be2e8 --- /dev/null +++ b/docs/images/0240-otel-weaver-component-schema.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/0240-otel-weaver-resolved-schema.svg b/docs/images/0240-otel-weaver-resolved-schema.svg new file mode 100644 index 00000000..1424b913 --- /dev/null +++ b/docs/images/0240-otel-weaver-resolved-schema.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/0240-otel-weaver-use-cases.svg b/docs/images/0240-otel-weaver-use-cases.svg new file mode 100644 index 00000000..77597df1 --- /dev/null +++ b/docs/images/0240-otel-weaver-use-cases.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/dependencies.svg b/docs/images/dependencies.svg new file mode 100644 index 00000000..672a5008 --- /dev/null +++ b/docs/images/dependencies.svg @@ -0,0 +1,186 @@ + + + + + + + + + +0 + +weaver_cache + + + +1 + +weaver_logger + + + +2 + +weaver_resolved_schema + + + +3 + +weaver_semconv + + + +2->3 + + + + + +4 + +weaver_version + + + +2->4 + + + + + +5 + +weaver_resolver + + + +5->0 + + + + + +5->1 + + + + + +5->2 + + + + + +5->3 + + + + + +5->4 + + + + + +6 + +weaver_schema + + + +5->6 + + + + + +6->3 + + + + + +6->4 + + + + + +7 + +weaver_template + + + +7->0 + + + + + +7->1 + + + + + +7->5 + + + + + +7->6 + + + + + +8 + +weaver + + + +8->0 + + + + + +8->1 + + + + + +8->3 + + + + + +8->5 + + + + + +8->6 + + + + + +8->7 + + + + + diff --git a/docs/images/otel-schema-v1.2.0.png b/docs/images/otel-schema-v1.2.0.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc80eed13a06c72c832a7a22a7013e01b2d4ec0 GIT binary patch literal 172747 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
a7qv))tv( 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 z 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

    2JSPjZbjS9VR$wLB83Fa0G*-;6t_Tqp)Xd9pIF zkLCt{(AbJB4^+d4-q%3RM3t4w_H=R+F9Y za!Yj}hL~{%5gV?@Z=#V*<^86_4{@>;%W*t<`X|#B|k!deL|}HEK*dU%2g+`i=w@{Zd5-#{FLWs_5oyc-+Y6{XB?n zA7n|vXzz88yvf0u&G``(^7z0A+4<}<O@6^A_EUFl`;|MWr9~%>Q7pnPeUMUU$8mZNmV?)D_7Kk~h4kT7 z#t`aSnvCoR){`0z7J8GO;i;(u%t<&Ke&A?;#<_Q!W`sc^W@gT^cZ43rnK@_OjzssQPIvu!s+Ma@(89yIX0h^7 zwN1+jgj#KLqmdz3m*R?nzH!TfY(_a_@#UM#jrBV|Tk+&gSVAdhbU&(w7w#%v5?UYr z08QhTWl>irlnUM4I}xV%_C)#I=^37Wc%MeeG_z!K`#{@NpKKvzc#pvvPMU&Bb-20( z<%kKv2}@j5d!w_8oTqHp+irH5rTwri&G4o$p}xed8{}f@KRoi3J^box#Re|3Z>z9C zBC%26^W7}3X_u*yfQqb5q-m=c8ryN*mjcVK#CtW0KrxQiAt0;9>MjocSoc;gSo-&2ucuEKkq184Gp5{`@-1=p{H~;@#}?5^OG9_` zY5f6g(3rPQSM+HYy%m=|f&JvJQO?zHDX*VMPnpIaNC_rx&&0pW#NVkQ?LS2q={?Bbo?4@f^OMnzue9h+kDgDYOhs zMH?2xf^15Aw|83v>#qZX z4l3HAl!n`cRTJl%-yAxfl6*GE3w}9vblFclR2>HHWg;l9S8VsJLvZn!tXq)BX2Efl z?E+>9GtCb4&!2i%BpZ*OBTWYN508d!cNM!T<(CU(oamNKPpso z@hGq6nX0|%%s90IceMq+vL^a>Hd#v<*Jgd(o>eS_Vu{^5Vx?7N$(<9FbzYPK#gTMW zTxXzSlPt7c!$(Rk7qWL)itj~~Jk%-=K~J_ed)3d(zHN&eNF~T7n9Vl%uL-U9SbE1F zy^}~x+sxjqEs%2+TZe@<74*n*Ds*o1lQe0fV%s@dC|{+}+w*2s_439XIdT}&t7wJQXE&XSDpro z<1=0}@tfKsLpR+|AJjtk@LQ@D$5G3pH%5Kh$HdABsEZ>FuUEZc#kx#mvORX1xX2TY zNy`HjMZB@Hum~e@uczwn)L9L746VqY4d5zqD)V->SB2)WFw(hkG276~TQxG;$UbxH z!o8#2Z z07;mq zTM)R$Z3-VjX;dYC@{ z6bGvcO&iRE%~i82g}mc|`HCumi?VB5!J8}6Qa&E()3@H;nnA~^%vV}jepRa2CGNaa zq+jEW)2$p}j5n%zb^8|lQ(j)qflJar1Adq3;o;%3xoo{qOzt96-q=DQXL{&CHs_dpwX zIM^%~Cz*weji`0cdaGVhTdOj0^2p1n!9;@(5pPP62GT!J46SchmI)Ct@3dzYt?XHQ z=DQ|esf9D@z)x6TyxlBK%0^kqpEoA520fVjpSjT1*Jt2p)s7~$;Bp*tA>ldyBA z@%t{8s&Z8@o^fp4#eUmM59&c zy5wkD#cpdy@psc%{jHPhaqykkdSv}l(Kg83CJ!x5I1Lx^&6|w`n_d*lV;Y0+OLe9q zSA$*+pd``2sh*r4l#?W|M#t=n{Yz{;LF%75Zhz>=!ZxF8deMT#cH6d!-q!kdPQDl5 z3*fUPb`epBv|)SbUxS&iFCHU+aDGzzTPa*fVq$tXrfy~H+0FX8;cdnFKW&MOh)Nd( z+8IpzdiyI)u_cKGiU`=Io)zcS8A6`nyx#7JY;WGohUQ``r3V^69o@It_o+e6()vDl z4kU7++Eb;_#@r!rkBdw7iZ>tCwxd~-+6!^dBxk8C;V0GVuVuk(LjSMST_t!@V9tln z{OOzApR)^S=K%AnXL=P0=YY6MuaJhA+`Z?`HgfisInMeCAmaq$4A0__!C%)s+Dr{}Z{}jV4^IOmBo-7w%GCOimrwjzwDcN%)GF8J|j4;lG+_a1GdR ziRYB;KNZ1JiBG`=NhPkGoi2Bqe`fCST{EK9=Ju(fOB|4cv57L(%lvh_dBz4!L=KzL zD`oSvPhnoQl8*jE4;8$rh-l^4ZjQLiYKCw^B8Ts);hpcQ{h_?VFy$(d#&{=SKu->| zg_p|WJkub@nb zSy_242D-!pIsUZZyG}Fpp|(QVy#M2KlSl1-T+8a~>kCsT@^Dm*RrH0*KTyZclUbrr?qlQL*jG(3lSPMIG#E zswHGcc#PzUcB zl4oY19JnUl%B&LP2U3$D_P4f|+D@&duR1uqz2~fxeQ|5sxo0k~k3U!0|47UMh1y*o zot)BT)iC&l*{lJzOP4jmhq1S?y9WOR8_Ss&L5WFLr(L0JST^!QB9pRgLPsPT5YUl% z(ZsjHS4Fa&b0;nf2qeSi$f-A#ea()DIeJsQ(@P<8;zQ!bM@ z=t4Dz@Li3zX}&P~gGrcbv9p^QC-)uY+gf+Am zUeJO$6da~(rhU0w<;6$5)L z92k{DYo{%;j(OcO#HHlf>YFa2skl40ig=$3PwqMKk{^?qo&lFKW>vC|v?9F#*ap0~`g3)p>xnRW>7(Zi~QhSth> zl={c+mv+JK^{7)9%Fg^#@;6MI%=wR0VP*H2_n4Eeg`zQpP{-f1`UlSm=oQ{bO-*(G z=KLYp&8|IleAOL)x2eF*&v0FHM8(X4Y3r!!BwH=eHMCi8t-uX=;d61WwBGer`kjVgaLF$;touPgk^p+}d1DcZeWww8N|TdqyvBo0ZM| z{A%e7CZ@|gVts}xcrl2<;9_Ss@tT7;_>H@Hv>(0VY+qbBrI)PkE-Fl?8i*Ajc1BhI z1^Pq@V7a3|1ZUk-xaR>TnQG}v32RvRa1R`KcC9-!qkz^9bD-?uEjQkb8QWXO)$u8L z=^C_MmNQzh+mGjSj_z81M~H<5USsj-9^NLMa_#Bp_p>552Whvosk0>+fTLIvNeKbg zEgFDx*TmfMYvC{aOc#{a6=4kP_bI0HEw+w24ib7U**tPqAw7-QBz&;+Jq&?FFT+df z%cL@S7qiyr_6Na=#-8#O{%9>Plc>o7`FZ?C^R@1ba+&Kq|8KCX*e71eHwNZg5A&`5 zc68vK1TeM$al+gkbh)BC&(Pi?F32C@0Q4^>1+)iWIWuOwOTK(}{Y!0^SRvxz`TSd+ zjKoXJ<&R41y!@W$UixNF{sO|I1qVs2N&!&dh`OEhp{D(481x(;0}P8%~)J?HwWbV!GJ5udDS76skofk$5+U1 zsaMX0J+ZM9Qg`!)cDQD$ylkhA>t|-t{tTS;Rj5;1NI;ABPh9Y$LG5EfktwT8nAy zRM({?_S%R;Gomy`_iPKM=<%~~Z=vLv+>pq(H~q&oZmQK>EpbTOW-XAE zxMQ$oCR5r3zC{)K{WQMil;s11C{3isuce_0xS|~G6bHn@8aFJ#`q&9Yo;sCrH-q0D zd-9pIu#;pPJGK%LTYJyGQPr1hzUz(50+`dEgK4@a@;D)%Y>Y_Rkn4iL6$)?Wbfo+Q z`h%kj{;{|=4qARb#xoN@c_#BcOQOJDg%j_du9o`Hl*DR#tATgr;VKBpMg+A7VTgk!iT^smVuUnj`xgOM=JB%wF*zLthn@9MT74EixBC}VG zpQYx5tawH^zd8 z_)(b=Ct$wE!;d3UE^n@#pIVt?2GB(&C|q`x^0Z}%L)qa{)Q@4~%=TC3-&f`HiAf1t zA+4_^S{r%1)GR()+EzkLppH|VXX{(;*IFV|0<+ax#z6GfF&hwH&9yaoF#Z`O`0{k} z+r)*8?Zn{;50#Uo=Z(kS)2bsetCJcpv{-E&15-PGYf%CxJ)y25&#ny|6gNO!J~BM= zj7sX9ik_NqC~k9}#T}7Ek8h~euaBjKh(I8(aZLt|X}I=^O%KNwPc4gBF?lELInX>?|0|x<6`~2@ zaqJrqa=mlBTree)*gI3rONr2hd0eCcJ07ntdeKK7R|5!EUucqYU2TzU-Li;@5E`}Y zY-SJP>v~Sa-z)dH8cW&L=q8$AL=K-ldG{L9pidBSdVfv2!vW)kxA)M8CyrT5B@Wdi z?NVx$R{C4HUN&NOvrZlV3axjV*VrEDrlw&$J6_L{RZlPcI5P?7?QJ^wCBuk(H%(*6 z-mUsYyJZKr8^}w%#g{->^{2rFtPD?*$g8IgWUz>Lavku+q}$U= zk9YOn8EM4+AU8gpy{z*Ol<^+fL0{gqa7SkC=D*auX6FP~*H~qKiO}+QOB3t8ygbX$ zAgmmgF{n~Xf4;O(3XU0ncx71p#m_a*Z_KU?TTRu{B3T&w6Mpj{174zb;DlSqX?+a3 zbXd7n80eoGS@q890Snb>&<2y#)I-HSW<1nh6RH}e-ZL{P<%S#HH<;i>9(avWEnlWr zkKdlTb_(Kpe2deRg{+h~>?FkN785geDfrUmm~eP2v9QRq&%`0s(26wVB0tBAFtn6D z^xIhh7KId>paQ!kEW0q7-O&%@3u-JKVU@_f=1& zgyE6+YNoX=T;iNcCscQ-bm!o}QTxX3nDtOAk_J{fnWXh%m9r%Ps}~<6h>_j@Yz;GT zjDGHOO8BjGExy;XUHoCsw|TE8N3Gc{w#OI#X{P=b?*gcPy;AuqwW^74|DBd(iJl;K z5D@Xz)ibO-#`pSFLS++Xb}!C46)kmG0+y{3`~aE7m#<@a`jX;9l@BMPW?ogR$F@0wwXR*Y#1d(sz5+Y7HP+zxJuEk))bV3Ne=jOvc^c|xZT;#x{=E{FJHl4y(eeLKt;%_vYaXMFbS;BcQ>9q-U2M=+TPYTUq+$l z!WAq1?W_k|t1GFM%HcLRfxL)#PKMkXs8S%Y+Ve4VoyzvrPiAa*qmfu=k52n14RZ;? zV`xP3Unk9SoHjfrBD%~@ua1Lh@X~fNiV8sMLw8J|J;*7qi>-JaqNNMl-UEuF5uHx< zGO!+#2}7y=&br?3hklA!&=cU{aDKL3FeL18xC#q<>HT|KV~P^h4%n(D8D&mV4Nh{? z(--tPnx9>}vJYrXCWvDd*nepDzsD=JZIYeTn| zH!ZfTd@dGn>SyXQ^&00yqVj%~q!JS#T1~X>GdZ1MG#wFSg3RLKcL4q*TLibcZufbOJ8)pzsQY8kz-=uK z_?Uk~SCzM@gVw`2`S9<3Hh*g^Ii9NryY*buF+YlZI3rJVs2@iBiV5GX#;^1Q7p40{ zvqmg{>FZ&jx%kAxUJo{3E$xop*Vk(T(Lrz%ByLV3UR~n4+`Ma|Z8XkivU5dbE&PVO zyPaKQG&J$4R066?5z)DQD73u|K9?sf`jJx)i0FBV4x!!Wc3k31&bPJkaRcOxv-m210cCgvpfT7TwwWhgtKb< zmBgoFOBDjy*gmo)cuI#t_3~0tsCmTA6EMmL4|^Nf{2yS+5MG(|YK*^H879P{9){IvX+(us zxWl$x#Uirv94uD)9h@~&`}oV5XRNPFg^y??nlv#iNbSiaHTx<0Zr_IQA}rbCEwyqs zI1b|>Zi?98I#p|P{bhL{$#$h5WsxZusi$&f35;yH3ski~d!~E|Z+F>BeiU3jUHmDd z1$#|OI8?%LT`w@!&HfTBqqlwA2!LyJ=e4BX6g2k7&0KThlWpGb`z#!Cws~J49+O>Z z<9g15V-kn$YM5tA-c{}I4RLdZx(DveYOOi5z1Y#KmVF*hzn7-q*3jS;_j)*w-OueQ zhVe!CuO}~l6So_eLV&zf+Vf))@+Nk@u9sMUa3staOU9dMlp#nLd&39kK>?N6zzPfQAK3)q3xuUN+Ys; ztedD2$dI2s5r?euU^|u8`Fu23a>t$OUaW}h8Zhi*c@r3>3?A5c@xK4g3{mt5>| zg%ZwDecs0CDkt;_)^B#gV=tLLg&PyFI(@#_poseQ1(Cl>rUX zCe2ZL*n`9G&YSJhD+^VSX1d*01Tpy4oc&)%6ef?F4o|y`LRonOe9`q@Z^X9(>hi;n zmF3%#=4&w-U12lr&^x=*P6}I5CeDucP=kz;>f9+D;rDEkeMN%-+73uZ2JAT(`WZan zBQ1s*A^bHB=n{O|Jd9me*nclrb!>Q`nZHyeIjsFHRBs%$Anr_p@o?8nYm9nFEL7Pt zV~OvU=7CUb!PZzJ5Eig#QvlXMjvDAsyZF(`vOb@<*b~u=3i3+v=6=@t*QCyyYH&PH&U&9)_pH zMJ8pEjV@7l6$1IYuq<+yi_yyLX<8PpdkMtAzGaM7*ZpT=M^S0yZo$BZF2Y-0; zvF+ycN%8Y%#(zHtq7V${7*KYT8jgyfqz>OOwK1vimW5G?-t`L4v^VfMUS&rMpNp(< zqwb?_VOS*}8Qope(uQnfW3Ac|=Xu}C0!7K(`}Cy=kVB}|$zu*hV8Pom&);q(Q@YCv z6)IITrl~wuA(2SrwoeNHrchEmtP7rtDv~O={fAUw6I7hpGfeHC$GDGF;No z@(KuIo5kZfs{lRAC@Hw3;vUz|M*cyW^Qb`5LeAY!s&8mF%O47nR z$R=-Dxfz0cMPv;6Xx(N}>&d)gaN^VR)e$AyKdf%xGY6Evl=nPt_2y)weiLw6H9OsJ zQW6at%$FLAGl1xHZD zE0U2t$JT-zwuKAE+1L$Ud}C%29vS32stCZ zqOvrE@zsv!V?{nb{e1s6XLha2_j%HM6IJ=P8mDW*J`8#!1gZzSpZUNB*Y$Yy(@%I* zPCd5#H-S&ND-Co#EOvy97|45;a8bfVCSr~;_o^-52B}_?SP25lj+2Mi=uMi(ZU*Qq z^DPcM88bCUd_-Y#a}|^7tF9opfs+lYeU+oueF!bm2-w7yk@}(+ku@@4NkEkyDB1$T z+R5XJhK=|pi)pC#Ti{Fo@%98WgrfFGK>p^e?rF-S4$Tlw@S~fKS zdGxxlt10!#S6eR3LmuWS$Mc&fevhKa0L8I{lo2Vma0I8&mZab_2T1! zT>{Vjw{GpUh_A22%UE$m_YHW0pmqGT_=WXYoWiXetT_**VQ>0_L}}JX7Iq8-vY2I7 zOU3$yyv+|+Y(A*mhF`Q<#-2y`Z(b_!b_mpM6VKhI70g!BfwTR2Fmj-w&E>wGDtG(D zzS?%+9L0a7sYEX=v-Vk8-A<)QJ$}D`ywblU;T@e0+DAOU92#As6M|Z(p=?gKfO!$< z8Q# ziNO0^>D19wwB3GNKs&wZZ!*q~Prd=Vn$Cr8l*c~uqFH}lS4X>LYqr8d(ZE-H2I_~J zn_w9|M}OUA`!szx>$9L&l{5aJea5$=uF(C;IjiC55hft5>mu+m1*7nuP~0%?q+tl% z6=d<`)Rf9y%lt`k%@3E0I_RxS+9L$-xayYKXG5NC0~zWYZZsfR8VdZ9spVn`62gJ! z+J#-`xt1>jG@sQb)wy7phU){9OQT<BZ~ zrJ7-DDdiQKhp1ofYx)m}L#zLpmv@w5DgzgFOK6^{S*v&)VzJ9ZFc>^Fb9byp!UxW) zIu&TP$hv37j>L>vIdnk>Jov)pjw68X9vYcf9bvi-d2{=#+JVJCNX)W`rBF+u z0AsK!KXP?habPyxo zpsII28D+&rkF$@+E@VZ*r0y&zth{qvqhB z&|Ucm{@(V&=?pv3Uu?cAbgjm8s>Z*OEiFF+b+jV?;(r6xG5#ZjVtzUBPxx0A2t=+3 z1R27_B&sIvtOy{ToZZiWx>jt=k7wKpnIg`4{^j}6zM~d{5)7#o?lq{`jM8yy`#qmE z7xz}8l~xWNUi-rr!Oe{$-JTl^J=0zFH4+DX6gwWxP`KV_EuUd+bULG?1$*mkvKHSE z;yS3i845aO58&=3@X#)AXN%Hf4EYFnBmnRLJP~&GGU6>eG8o>ht#$#>+~GUTbj`Xi zg6}oZ=|tKWk$|0?{^u@kERm{Xb#W7wJ|jT)uS8~SWos7q!PP{tS^;50(;E8}(oD|w z(f4%dJp2|aeWE<@GuuNqO`?au)zPBPxt zragM=4lgf)n--P*9tOPgo~UM2(#997)t3Kn4imNH7?3PyD10xXb!VwjFfWU$pb7SktcFPs0im9T@0w z&668*0_FYUmg%Y+bN+QH?FV0D?_E$?iGiGa?Ym*D<#r}7zgbu%;k%vZ6YlY@5+MqD zgXY-EI`j+xQX*YD)_Z{Ogq~n1G_RWj?+JmtP|;Hhcr^3PaH_7^A!+TK@opG!lCiO6 zEwgR%&oiS&13;b2_6t>1#@K#PA&pl#mcJtYPLU?Pr!J2HqzE@=FFw_fhYi#5_b}TN zwgc2#lU)NttZlf4^Jq-Pd`;=PNJJ^4>?m|5gvD@NikkiLG>J_Mo)T!`|Foj0DUsf6tVJTHGqSj6t%OkU6igUud|;U0@dC5ObZT(~|tOLv~w zeLU+kFXj4of35d0Y;Y^3kZ7aU>~v>-u`p=9KhVwDp7@b^5hBTviX&A)Cb?;nj~NOq z(z^}K(71CUXWnS*!x4|#jvP6ajyVm4K9P%sB6yaMaOpS``fbOUU6qO)u_b^t zorqYuJfWUV30W?VUcc`_TYFZ&>>A{6T&MF^yX&G9hCTj zAm0y>lR7&rNveVYZtHmeVO&eQ{&Zkl>2r}WrrESORe&YIA$PFrcgMq?M0;1!KN>nm z18yZnH$x%|;)ypt#i(w{Y!Yp{k3v@42QIG6O_WMOc-G9NuM}Gzf9hL5^7^=a{3gs)KY|P>KkfwBW#sYZbvr*&>7An>!MdJzT7Z^0Vxhgl z%>jO<)WEsb;KP`yWX}D#PZb`%)y<%XK6GbU1}vS7p}4cA7{@HT#k+D30jU1awkG~< zSWpG!Mr=E?Tl~OUYHlqk;=`7RgNHR&iVukuY_MBa@o50iQh2dkacdjY;=N^} zJ0l6U-a)Qtye%5<*w}dZ>=>)S#x&4A9Hd4zFV9Z&RA!RACJ>ivR8Q&KYi z2Ot*rvaemMQ3LU3LRWU6ZAQs(7cT0p@tqeZwz!}4t5z?E57@p;`9ATTujSJ~h#N$A7!UHS_`-mnvts+2ms}9l_@md1l=o+vQ*Z2nW z%4igzCy&qq6%vq;zoV~qo041^pauYm%WR-cWb!z)So~zVj+!knCUVEasAHU^_u@G zscYBi8=m1C0AvMslYPU%p`foXK1yki-e{gYzozgYh&>R)Yr57H-4o}i)dyyjT)&$n zbJgi6KIU|2-i6r{E?^4Qi7!iO;yI=EKgd{a{}z`?T5ez~I6MP+W&bLC>)E8l0N4Y7WnWMGINJQbH%WT}CwsFt zpKz$83K{R&RhsTKT}fZ-tE7YHNI3y6qMpbGv(0DH8mH=a@PchCXj>St zNm7he-MzpZKz&ri+gsQCdD>RqdqAGa${>U-g*mIili1gyju}ZEO+QoKNX$EbJ8Xc` z>po@hVUbSXqx0zDXV*K*mf6E#VIi>xJuaG35=~Am=}RS6O9EwjWe;hz)cI9zu7u4O zdqqR_QgTY)=fqLgU~%U?gnZem?Ecr-40`To(MD?JIhOr?yUikASFjUHhYvFhe&ul}uhTl}j`!YoN;(`vl&oXq(&n*V>Hm#~`9< zA$>|Ix8}bhUKuSvpWVy~*eIw4`V~l|TW=M)Z1t6;FU=B*S|n_?^+Sdb0H|Xqf|2npPAK8`c&MA~ z0Md;2;Nh7A#}Un2-=3Iu4dRE=mab^bhpst9UO9P&@1RjbURG4QP_{a*Vm(37#@hN` zm^UzN@HMwW1sZ$xJCH^Px!uLkS%k&QY`(lR-+r% za{z^uhbe$>1@n`@|L+jnEmZH-bHAnDb^Dm9XPFYr-(uHhuA2GF+UOEcv0Z(`=LRkj z63fW|A7a1ccK!E34VUQ(0cg89$Qd-?F&eGRrM|0l@ zTRa|L>;r_J9k072AO7zn;3PHu?$>x7C7S#1e*WKoiSIW^#Q=qvTa*sN;J{fJd$%JC zer$@kfS=&`to-7I!n5SIM;XKRfT#WYu}Iw%qf?8k%&y`;-yU!Jv!y<0AnOFK8|<)m zPyoozbcDe~o2S2RS2aKq00nykzJIaeSEG#K{J=eVm{o19nexq<0ABt7$B;g4%~*$|3oxUr$gBhM6UooU1KRN7Nks=rPZs;IEdspa<$_D@cnaAXW{6y7 z{(-0n4j6UAN*_^ClRSv43CfT!{B7y`R|X8WYSFq1d9BaF*Syh8LL)6u zj1~sblPwDgbaH*0K05%64dV&{C3Yj!1Cu`r5(i+G6G<_&o0m(?F51*sQZtiCfXoCF z9nO5J{gtiZ0+>k`vME4Vb=j(0NaTsnY|XR~s%SafTQoN1a>Y8_#b)W!^MDO&lN;|G zQ-|*$Rl#nsUt9s|o@H}EG5b$nm7NkozN4LOL1B_ro0c^be&&q(hSlW!>%*4I_#qw2 z=|Id7ce|W=sp?kHuaAerXJ@>ls92DnCM}Zjplro?t14RSJy_Et0rgI(_CZcJ+WX<* zLw4}X1(~QnH#XZoE_8}_Ugvo?Ccyr29xGp|f~BgZ5ha(eH_0Vyj$bRXSZ3-sUj@yd zfWg~k4b>mMz2dAXEHH9Tm;HVqTd`M>fqf_pDMed8ODP!rZRIt&nazLl@*9aea@kvm zb#G~65bnO*a!)dS-v86yJfT3h5?c%|Se5thZhTT0x1HzF7c2}ay)bLH9NcHoZ zP=yX*G*l@ucAEp7q7qHzJSPds=GujlTN|g(VjpCn(5O1#0^246uYrb&G->w#17Rx+ zKo?~;z}1O2Epo5U-vd)GsG-1SD9!Uw(Y4?TZ^02Tjfii4P<$;|h6JR5ykWk~`z?!u zMMJBCKhGF@d>Vy^^y3aH)VSpdQr$2U)2^Z`^s z1@o)Z%c=xq2}-GmAWpT>w|fN|mzec{36};`TqV0U0Eo^ws`zCwAzRl92rYnpkUQwA z*p>U<-|8kJ#Q*Wud$5oT7dPDhtSHkF!`94LtTZls0<6GE!VYVhzV>VTZrfU4RTvQw zFsscI%V1;gN87`o-)pJcKNuaZE}{DRhQCq|s#5_792?7szgTrp;@j4LZ6k->2~lC) zA2S=RiLY)AXza&2{-32}$^F7g7A_GhhSyrytH$`*Q-yoz7BlW&kuS0RT3u+_X0TV$ zG`m4ZzG`g7Dt!06)ODlO;cG}$v5q}MA+@b+gLWOe1ngJpp&g zs-@Dacayy*xia9#^B&dwI{SZ)mMyYf@?$n!Izl^sld6yWCGJTS@?6;R0j(wphm~&T zdyWoBj2yzf`<*NL`C^$*PT2)hE7@1|WvgxQgJ{&~lkU}vKAi^*Ev4KSHaxdYAj%K7 z96`K;Kt2vfY&h3TMmR22d@+(SQ`rA(BX$ftf!J?v74wi$HIP6+Vrv0w?bScdwN42` z6%OWH(iIBxjM0kdv088P-I)%o0Oap|b@Y*zl*;2iHeR6TJ%m2=sI7tiRPdNq0XvhP zv_km_!T@ny53+Fp83mLr6$N60O#m>0M%EY!z;0i;x;oxU_{Wev1!0MRFaf2n7tp6( z^m)*n2EFi6wH3+_C}(lX3v_d_aVSx}1XVMq?t9bJg?Va%R#%rhG*ei*=$-Q@@Z}Lo z4`w|s54eu?>UXAqTd#-dqoqB!%b%&YxMfo#-j(=A+y#VNFjdl{MRweF*K!`PAK>!5 zBMOvu0znNCs*z3PxPSAsv!?d`b2xD-j<2eu@&2DDMi<9l)t4fX@`EJKm-%^IkWbw} zb%V52WiMo87S7bX*m5289AhN@*CV>V0y1&j^fY?3)ZBw#14>_CpXwS{u~fNptxx*o zTS9J=CE2mmGNC~(b@)1@a5{ax;)^JJ@Uv;L4`^zLl-_BNzlPxM82Z8g6e_<~cOUAgC=m#7B<0d9G1Ny%MFI+z_xz%({# zL)G$}V#>tlK6RlX$w!P$w{zh| zmH=(e5Ul7imrn$%YJ%bXLx_$~(kL=>6_2zVM9u=OAIL1_K)M~vORW*7OosNfIwh)sZHBqh1(^BA5jH~DU+g}9&~2Sg0}L>CEK&m#!Uh9)`BBD} zptre^@J>d#di!6{=>B*hTU!bMshe1zN8|G2E$iX}{3u6n%^}kg_A@;YmdX`W#V@XJeU9rjsuSBj|c4}&%D#{ zGONd_ig|Ro;;CLe;8_DNlA*1BLxx7vYb+FNnC zVEjhN&p6+OU-#!*#(~|#xC`4-yW4QR?2j-drb4_fG*5x)VZyNrRDaW}3W`+MadaFDrO4(|KO z*0b@Vh}D5$xqwTy6ppcLntuU zdcd4kqDalVws#_C1ur%RAc5;;3qV4{0CWVdYSJ43w_Jg=AZ_+~2q^*fi}qu$>v8_* z=cW2b{w)so-QoVw)>s%h}YJn5iy#6w@YH(&YcqEV)-35D^xlu{Ex(c zy|M_h=D0|Av43GUXWRZsO`K+S^p!(Z08*A7w|tw*#6b`}hPyT>VlVK;1{ z1^b-B!j_& z!+w`^d*KoDet$(OqY+enCYx@)ROP`|!wq&WY6j<-1=4vidG$~vd?2}^HbnHwQpbYM29Tif!+;F^pfTbq z0Z5)egZKFt%ls%Xv~W_sVmFXdajtPqHaMNl*0uvF?^!aa2RmHFwpMikkgAry*<k55PzbexcVXF&P z5jOpjFFd^_XjwY6>0h}M(TL8TJb(+E57T*MVG~{i82-PWhtPq!sF=5cQ3btewsgmI zIB1fB)B@-mtgbq6H1Wj;-DuTD6vE~^%~o%*PyYZeU|6o$augLfv#iNFhzD16$f44~ex&xa2kJ=(;aP1nR->L)0BfD00 zTsA@juKnk4Puf2Gc0`c3*bz6x>Lf)J_}>q;DzKJ!S3+yM!LE=x77oAk`vm+c zzc)BDDOcVE?T}EC@k4&A)kjUW?!ofcz``;n;DzqdOru?K z_mpt0fI?6KAW;BD8wRe*TJ7qLF7Rj7-_!?Y1uDQZGqa<~gjs`K?83YV$aI=rLeX9# zoxiQZqbNYW1q{B~wZ9S>f5>d4kWWwP|WBU9~0- z#;Cq&pNXmn{({(8IQ`{IXhnG18UCsOC{(rEhO`X;VxXbk#bkkGl;r}{U%Ef-)?c{A4m}IL@dTls{#KSWZ{v zpGO$$JmdbSEx+5jX##RswH0*JXiWbQ2iSq~!JN50V!j|%piDU1lTXE{2W@X%yF4uo8!QH_+|RsstBEyWT0`u4ewg7g?>6qWStjX za=8po(rg8qNqpe!73Tbc>|on%tSXSp>3*|=yhG~RtxC9wyTgq%szANn2PrY#&22E^ zPY+$Wj2H1RM;`IR{RaAMkd3-SH-1kMPGW*|+W@@*$oM4b3TONnl}|S^Oe~i6-FGbU zF+vlpexzFmZ>eJ!!2>)e?WjHIyd{E)7Bep_(<-2Wc!Hu`|2UcPa7DUSCMO+6ZeO{$gnxX7PbDiY=3%$GU@&7xPTJ~&3K9B_GUv~Vc=wXlF{3KguCMO;#GNS~}_!!%w4+g!O z-r(+LEhnVkx5xWpKnE`rbPrts_UtOhjz57GH?*L%^BEwIYTj0&T6TK;dUk2U#&^da{8uZ0jfKaZK&=IS>ifSLldVbD$ZCW#9Tl z(>eEGyYoOgQ>zfZGuUv|%d1>4r@emZ=j*at?SZglp!!Uc4*xH?aF_(3S!=804W$>0)|lL%6`f!<*5Gv40U-rt5DkRp5{HVfk@0n-%2nB zNqoRV_Avzlre=WqS^)N+pR(0S)P;|(@}WWFD}hXkaHD|^_Vij5i0q=9@whI)6vO&q zt85j%Q4(9m4^$fL`l|oPpNJnGzLTqlBnzL0{Ca8D&t`Z4F5a`mAyhqij0(uC{`h-ch%)lRYwBGnO-kTXU# z1;Y1cjHO_{EbU`Q9to)94-XC5D=e;z+>%79g;5JtaLvhW#*E~M=oF*LgMTWmgK?lf zVf42ZDiH6Z-Ve<%rop|PsI9h~S`^S-CsJ?Id@IZK;j)Ehu7Rm>4#FPV7Ir_U%W89K zP}2=CNg__ef$^zvO&KM-ko!`KD)fAZTr?66Y-Si_)kWv5x1(uf7|v7b*;z>l_UDv3 zP~hEy%7yqk;FT6Rhfq(*%lG4|9VM@NYwrWPZ5u!)L&p%x0u;w*w?88S_Pxtcf(MCH ze}ip(_Lx4KkFy?ux(CMG%?#@!MPkyGxGC4o^>0SD(vl^h@Ty%UaCaEHd(S-8Bn7$BpW~m4bemBv~3|^Qwr!=k+HLTz66CR&@T;+ z^&!bJIsGFd>#@&*PZc=((KW7pr~hhLQh z_mr571#_a=n6Rk8I4@GFS~9r86_hi?9V$4M2fGDvV?p%B2!Id}<2W5u#RGw^2HFP# z1smh*oFPH<*yK|s&xePB8*JvoAu9<*W<3!I2 zgmVGNI;b>}xkrLq zvg6ujs*DE>S6eQ7qV z)x#F5n<0KV$k(V`(MyH{!2ACjy72O7>Q}??A)XKw%Tz4NagTbT#34Yz6j8JyB@;Q`4d90Lt z>iV1Kn=&av`YU2Il0aJ;Gq&t!E*pFDP(@a|1?tm%-9>9=Igrq*04v?Bw37yRNN_?v zRlNy#$D!Wf9cw;rr5Pl%hOdA-#zbq^q<*`6HlKw;)ooR=qH82e6kLY8ZTg$LAg#?- zBTGAfWBru5O`2+)%0}Iop2{QcPQ>m++>`_)F`d~Yv_*4(JZ6B(jqKm96x)<}^?fvR zF7I|sj}h4FK)lE3aw}iZh!c6nR;&x0`_@<03sZ!?jl$M~lj)~px9QRjxT2D8er%ZS z4ZB1f=>HpVC-<_YND@|y<|Rat3Rid(*CQb*&1^9tj!cN>F{gg10fz|*)HfJ&1pHDk zu(!t3|I(72={<+`(@^y#ZH`oP>q|$;-&lKum>zD}=_~5+B&CEFAlXGP@41eJ4&H`g zfZYoYfVt6je^stI$3GE$O+$XzQP$vZU{E`@34mfcqYj{^<&Pn2dyNHxSQ7{LDxTo` zRSrAf>J|~Yq1yxN4{J#eeq{^(;6oGjGUy;`hNFZrkMh#fHM$+=a-%b2oeKR5Zq4QU z#PZ{@?#ruveU-71JBRK9%hrhwZ-m!kZu4YV)NOh)t018ls`U*-r~d$+v3)nRSq+Mj z)(dr&_&^(=SoQ6Wf)X5hM%yD)XqrN%OpP_8)5(6vdrzg}pS=IC^-ILq?PkRUtg~LbJqzp7}q@@H4#QkO`ut)C&+#g~v)6J#9zX?jO!4BB#1EoU_>S z@fTS+!KJ}Bo&0#&#v!6$P3jsWN;l)l^o$jnc4Z4e4P(I!e&ck`ZrV6=Xh4x$&49uhxp*{$IPTsjCS z>M?=(Z$mY=7;EjMstT2Nwy3I&Ln-kfCjh959}uZv<-yt8f+It8d z{XsZg_&XniZZV>dLz1AfSb+X@Ji@o`O7NkF-Yv$Q=*4eiv8CKgTT8U|SH7L8e*3S2 zubXmGAZ&{E3}NORCru$#((>?R?;%%k@ayw~YVKgrkGggLQsBX7O!<->l`N|%sG)!n zkddGj^}S4a!aY5Z;qM}{=R`JRXG;?Tm=^Vv1~M0Fqbq7&e9X`{5pi$$=(t)r0fpipw%_wlkD4 z?&w>;Xhjia&F`Ukb`M@FN(;zX(Fh)q=-bA0>oj+kwn}@>(3$o^Y6Cz8@-DW=XrN|c zN=N6*%2?;ZVHqxeFY5&{-OJ7?L-f@UIT3?0GXCq_#$V$&ca{!PiIQY%y?>!$+iQKIuUIuCt^cV zLCy+%O*|-P!g1h~w3)ihi=&NXIH;fM5f`{e=rMU|+&z7}2x7%Vw_o+ZK{$9z&mRFt zN=3dX(kQ$?byd{=yrFn%oE@G9&!ssyY`QI53`TOr$b{f3x%v`bE$*vW%?1-}J6P;_ z9+`xrCu^!;H2cGZe&$dvZvl$ZrTF{%D?r^KymbvJ#CZ20-GBMPv+zWkpbU9RUNCSV zyxK2zdTgF{3E(E|0k<)ie(U?3DCObgcsA5WykwBv?2e2B{!d3pB~S zbrP17ME8;W<*0}DzRK4T*yo{fGCcJUJ~!AFJdUMqZ5f@*k0B`4?9B@5Ad4gt&sfeu zFqTLcjxwF7^2NaloNE0hq(_$2J3mvCV0nX@zQPk#8Z73aA>(7;D5yQ((} zL6r7$_05%nqJ^jo3AU=^0#Sybgp6*azEwJR)$|k|w(gqnfAjSS763?stPnN;DpWzq zJ~%VjF#a`b4#n0^WEfiGE|<+>)r0rJv5?&!BEjG~4ppQV5j+HQinazVKqPd?R=_41 zwePqHIr>~Vx5r{P0fd4;t<6RN4YR)MzWD?Yh_1t{s{+w4ju$~@5b_~x^U`yOVj$3C zGfJL}<CIrGv4;sp5LTR8Uq-yecd0Z;(*xg%a_(6w)dIawcN@O zt$`kVN>BuT!fwc0S9%h9h84C7O4nEE6AV^akF!FQFVk9SXR~ky6!-Pq^k+jBY5?p+ z&OWVz#%C;S%a){d8T~&OsX*8wCx2lES$hY4_Q^&3=0smiAB^)K!fjen<+t& z*rF-$+-V)L&5o%WDas_crbNJ00}PHphE;0O`w2G}@z@39SM8l^l273uy_>-;x#E7m5W#lH)wYcXM>W zS$OGdDMmKhxq%LG`3IkChSp|wkOyqqp$v!?VT`2!7nqMId6D%?{*&I`O+ zA{dF+fUnQE%1VpP8cAJ8&xMsFt({oZ8FpqsTj1j*97v)6rhdW0ooqufh+`2_2#g3? zfo>`9+1)U@wgKiHwB|Ccl3@tW`i+F=c94h>WPo%#cp&!jkPTIaweD#K%S5AyHQT{@>YZ}mx@X}8x>ijYr)?$p6YUzX;j57AvH3fGa7AjS zN{G}gJRyrT8GkqjlxNIXk=vs$bedn0yqRTho9*nFl_Dg4s2Lh_Z($L`0E;B;f!EKo zB{MT{M>(YE=zWpobmzQ94e-^@vb$7Fx`KO zeOCl2tXU6)H?%%MH45l)B;kAWrO_wHWmHne zeqt;5>U;tD&CGy>S`U+ieFBgHgLW#{+m20}5OyEb>KZ|!9-F@bN4`NXyF@$^1_Xsg zoOH{R0P-M=>Z9he66mAlU4{-w$13MWcz{npeI{r}yC5a>_emV1p?pc<$FT_I(X$Psv`3OKT9 ztAGBC)SGv)wcVKJc1`?BO4}2WEz1xEc9GwHki0@hBK`0mbcLr0R@&l2A_qi^$ZvC|Na+iT@Yv7 zaH_@ESo@3v9pW?f?=b{`qwc#Fe*_OgU{IL*-%C#6J$8$PK@w@Zw_3oxk4vVwUVa&U z-&%RJ-LHrrBZ3@8I&i0#>Mh^uRW7TY0FVIUo?Gvd#i@=vPIHe-%>}()NXyy-oPvD? zZoAuRuol$LFr`P-8vrW^vxJ0>&s3w6Xe8qu78*&ZTEWbo2L0vM{3c0QGsTA6k=TLl zaDcr0bIB>hY*(1s_4TCkJBUBo$L3;ghDr&aK=P_-Rjc%tGpaiaV*FKq&_&;k`$@NA zHUBp;LrFkeI3D`nWk6nAUJMkRtg}TjCy*)`;COeMRLR`@8@vt?Wkj}O$Fr-1vzZp# zJ4Aw4D(xiO$+{Bvbijh6(EMNe;5D*$57xno4Mn z)*Kil-wR{I1T6uPg&i^iGY!c#F-_my90mOZQ*i4oXSJ~En4s!_={BB+^#58;7!f|H z`rT2vw3EHP9bc67NvwuOXs9^C6QHaGqScIRfGz{2}pa&pfuY%0i9TY`@lj!71%K*zNlU!^mKmr^}>uhe6Y|1}W8OicRJoVu18Q*~k zqM+1}=k6mZ6_+jF+1jBcBScJuN0i+I)a|I5&jr@h;ghG1lc z_ZG#Y__?>qIKeS`Nm5d*F@ZoGB2wvjTl%ugB$0)nK~)vw!ZF*lvI_vjf)YsG8+%Ap zn*_I9Wl2J$<&JCEU7Cbw_k}kJS6@QO9e~98}xdVq!k^{e8zOans!$$13{|V7ANs<)>#m zPMNO(BOS$6dwZ_AZKJq77jO5LLm~s}D$ifNiE<>15h|&rxwSD%x^KUs2BTX1(p2l( zrQ>~|VIUP75KW801O#gE&q=HFr!Rrgm-z)E>+u8t*;3mCDl))93e41yk{hNO3g9Dz z{z>~uu1G;1poRonF*I~c3S>4|A$GZ51458gEdUt@Uc8oba z(mlD`)o{n8L1xQIXRuJ~;d|PT41!fV1fDGwyJJ;-(EOL!rAl>e47Lv&&2#IdD|j`A zto!Hi$H$?j>is$5)9HR~&X8|G;M-ePRwi1sk!Sb_zh5KG@$KZV)2yBGK5O6p{CPxR z5d{|gSxVIDQ~|%YO8}*ToU^7gFAs&O5=a1Wa%YJY?Uxi=?B6DcLSsibkj%9iTfPKP2DuCR;-|N+wBSGK@`bxR-S~KKhEfajWYfKBEIW2=OP*q07_4+WMZA51QJKhLa=7sRt~ljDkyDyy=_&- zRIr=5hyaULobjqV4H6Vm@0I%QXD{FdJeb09txas_Rz;|zo*lsxkM&ek4f}bo?GhOo znP;zm;%~V>kR5asKlU&w!1~?d5-Km9-W_2}p0oPP5cu>Y!0)HNgBC&dZVV#k*SQij z{F~d$ShfjpSm&fFjt?7I*xwDyX}#K{_3!(W(75q)3N^-pIxOM>JoT?Hdel~AoUXw-wTNP z{^I5oU?d3=KU&Lf2XmQW5Qwdbi`}ic!X+s0w>!SvxxxeH*YUZP{$?qquZwBY+|8B#CA#Ka_LY5jX6$lGJ6)kiB;*JMmm&<@4Um)4Y( zm0xM+kY1-9qURIt2d2vy*AwAqqsU*E^*tq3eLs~z$#wM85tZ>Q(BP5sUN7(AF!Mg9 zEr2QDg8GToOiLvId_^2k!`Z^nR99y=zG_(h30aHd*yl9c58&qFM{msVW8wag79E#9 z`>ickJglg=>+}Jn4P38!tc(LR7dHvhFY$jMYypU+Xsrb{sy~}Q|B0&MoTs9Zefw^6 z;W8!JD|$H4o{O{++#}|3(>abY&U-$c*4C$%AcZ>3))!z|W`GVQGYx-%1rv zV7OfLac5=KcFk!;4UMm;w6xBhY|RQNBVz73ljBHDb)BG6MdJz5yI1IC#oF8j!!by% z8CZ)J_$sMiuUI-3!YJLw!#)rVCF4MB2PX`nD9Eah;CcJ+ha5ru?mD3m z#W!4DmzJ}{gU%+Q=^RG?RS#Yt)UY~5q+oSenFh3qzH)@s#(DM|#bD6YuAvrVIi_j& z5nFo%bVQL3m&n49S#h}xj63El8J$UAx(mcHI6JyJv~rnN``i6i8NE<%n6000@LE*S z!O^uMA)u7-nelAhg}3oNxh7C$5qd&7s-#LOn1J8P-MbS%q9`vfetdG-vsEJv)r!bv z5BXL(5eY$St`qmb(nGMlSPb|l0h0wBiSodA>~U`DP*PWpj}x{)76{}a+0J)PR&UiF zHC48IX7qY6Pj@V`lPTuCP@$xNJ%%7PAH-~h`!eO)^BGiLFFm0P>n$2hc<12kZDpSs z^R*=J#J#wFQNJ^OC*Hp0MuQBvKj(_BD7;K-+^%vtjZ;9hHSDR9Q8erpmnzN0c$VJs zCse3iS}8gSgo^m-0@spQMN${~%dajNQ6V$IyMXs*{j&byQ9eT_6bH`1p(!h1;N+|m zpU&XvS|7M-*ibZwTA-M2??kL)CaM7;Q}3tBIRml-<%it%WTeCQzkHs0xt^`gph6-! zVH4%_l}?X!zeKH7=2?^l(h1Bu3E!I71trpec>6@#?#LSJtM=jj724W;FMJN@b>I)q zZOhro#vO`2Wt$JvQSpxF+BnQR)y}qkLVJzWqD0@2QJgtVprWXF;akm%EXcqSzQGkj zbMZHAV|fd!#zm@I$P)anAi?(S+6`J2GYc5)^7Q6SW_4_pJ$-(2ZaG^)(I`;-3iiy! za8smp99#jnQWcrBTxrF&KyeIWN0PFT8kb1=DTU8L`J^Rdmqz)r^l?lmDYy9`VV>%yW@$d%JJ zIP03d-1y-PtMBmutU-D_&1|hoc9&sG*rwT-fY2_^N7)cMO4Qa22XoZ4+Ti$u)|_g+ zI`x1K6;e?{4RN zWb`-+746`*U9LwKRy!MYKwZknfz|7cAW#_GfnYDnp?4Anw%5pQCdwS-M=23oqPX_# zL>vi`?%@JMp6c22iaZbGH@>e!nnr^IEd;Lm~9Q+SjzM%qQ!b%q)S(AnD*_#0W>y#(L`QBYn+>eOG z3Ttmkx`LMQSzyjZ>}pW?EG^Qx<9fCuA^YKi!n5J>y>oEDxj?7-b`*k9hgYYL{WW|4 z>7#gTs85NO|NQmfr>?=H&{7y%EW-gM{r9PVJ<_NiPn`DujyT10K*R~)_2;`3AUukM z3myL~7%@nuXRBrzGbkXTuER_BRbE=^6M5*{*7a_mp|bhA?4s#^cIh|Rt^{0<68M_B zpYeNed-dy};1DTX2dSKF;?F6tYv=VLl^gl&L<1Ocw1W21!CV$YgZnPBr1ZTuS5nUx$YS?m=Ai^^M>x5 z&-mdA{@;*Co_~0dR4(G%%puByb9Szp>o6RHmdK>Y5@P#$qnMoBXXYHFnKqsw->l9F z4L0D_Nk+(njVj_F`k`>Dub0len_Zr=xZTtIJaJ4K)XIP;9i6{UcfE7>LkAQl<*t)K`>EL0P`XWs?B~1K zH_Ho8n%3dy1qdB_Z9KA{`ZlRVt7PS}pLv9wrxJ{pzg~$svWOH}P$fif+Fmc4>~T@v z^36PiFr*^MklZRfyvl>&@_KGORk47Heyy#WyMX>x`jiDi#z5sCLoz1#O(29Mes!PT zs~Nk#NX$>fDbt#v8SS1xc2*r>D0#lA0K7;L`g0ZCqSyF6GnVI)5g#t$+a`wLOys82)!lb5)%p_|Tb3Ih41r6pK8$+VP&0Ri# zp(n0$!q{97kFkebj5=RdY|HLwU1;JM18k1hgR8!lYC?s&EoFu~s$y3|b`hrL;n}#I z&UDb=<)1 z0nicnT2GxFa|d((07bG4@)F(HpSiYQd)nYvQM`wm`=?bz*(ZgKA9|JAOGd zpX}GK()(Jm2nWFX`tdU5g5TxZP$k)<{j)B9^Kd`q<>lj$zP*Tj3Ma2t?s8hF+ta~M z3RXZN6yFJ-Ki9e(w*QK+>Qc&Tb~gS-sBtW;X&aR`ft2=w=fKW}L78$Z}iFZ3-MqHCA%*em-8kRjuF~%T_z67Z|50Td)!dl_v()6|QAyAbO+L1Y zydTc5roMjUJ}G&aXAmy5Kb+;->yEe%Y)9lwF?GZ1+8~wv(SFF^Q>2N~G&CLS$y49~ ze}BOFsv_Ig*j9vw@EWfqavtv^j_|$QeJSk(USs=N=)iRSaLJ%je{GsFW{FymuVc+G zJn*(%KJa$N7j-+i_agT9?8B^%*6457a@SuxJ&@Wu)(z1sQmJcCkpEu)xM1$g6nds4 zhO&@eN;r93ZafE*bbIuS^__&A-Q1Uv#M~}pEq!6;-3fPx4Yn&|=IEOBUG+Xv1(ql@ zI}}m-2Dxs{l=Rosm-R<2?ls1-uUy-GZ>Oyj+H0-?3yj+H8LF;Qmtqu!MHi7rWxvYG9k4cuLe)}sjCYNz19tc2-(HqzFZlBP@w z$_)r%=w@3sd~jtpqRi|Q$JL}ONjEvqOX~bE_{{N`b8Sm3NQgV+%%LauL---pd|q2O zyQmma+5}{AA(fiEtNf3 zATFpcVQ~HN!%L&*?mGDNc25yEktw5F3^B(9)|y0(B+r%|%~pFcluFaL93E3J72(_Q z=TDG@uPt4H)fa^$`Db`v#qj3`YCav<`E$(5HEe-eLB_C>t9nLLhK zBTgr<(7Am4cxnEL3LA5UffspxO*R^mmBVZkR=Q)mxBTd(#}{bty^xeD@kU21a=PF{ zlZ`C2qLlsb5%sXilTm(x<=vl>cH1dewgPN(M&-nLN2%%?4?>R=*AM{IL(6SZtrDli zJ0lxAP-VB8&3>?ChEGvxFUK^=9JIVhRf% zN|oOALo&t3CR?!$MTMKP+z_TW8C6mf#QIvT+f)ZVG}@SqOxlX!2yNDWz-Obl*PFbX z>p9j&^eKt$`@H$(9!>qF$+k9DuBDOE%G1Ycq!RrK1pQ(QD(*XAgp)n8w!SuQ?GDmk z-p$0lwe5%^bYvY~`Q1Lg6`fr3ve3sMfT+~;YBKSz<(2 ziwY~3EDO|Dy(%-|Ne4eg+74!O3xlZpNqQGp&?p*?yvI*GHa5OKb*R+ah`sb}^5Z-= zhtWy{)##H1muEbYEwmE4gCE=J0(7(Uk2+X7U)eHLSKyN&|0duSHpx?@bJ+#Qhjsfv zOD`g>FlPOpWZ2~R_7S>yXu3t2fVn?qlqAVlX|^KbSi0w~Gp|W`#J1llpmM$(y?cD% zI?{^dfPaqx+mDQPoigrQ=)08yvK8S5iaadw(7$@fF^9*t6n;sgCckNCgM2~v2RpIo zP0lNc!5jvSY{b_-N zU71OMvP_TpaHJR13)UicROlQ|ND;IqyZR7)*u33RF%7}%`V}_ zWuGF}WNdK9x4@3;`dT;VY@>@@#-Q=wk|;}XW1$5W+qaIDK4FpHv6*x<=@iBIxZPZu zPUyCcTA1so|Kz|$t5Bz@R@`pVs8}Sxm93a&D8gkPiwC?`5LbBCEccTwR6#1JF#@#^SnY%<$sTgxpNMF_ z85)ZAKX($9KZXdt0Tb9^P)8<4@7t% z*Lv-~d=Fh5 zt>Yr&82AX75@TS&Y3718U6l!EWF5;P_Kl=UE~Q8d#A=7=uu)VLg!{n-a*i7HnT$ny;n5>=*?D?z0^R(^rz4hFWy z7bR^k3Qhb3Fp!D-_q$a$A3~SMDdDvY3!-cKm?2qy&id6gl7KV5dInfWl0|0XBDeF{tAJX5 z%+e4X*}d~vBTY!5a2j+mYpT>tak4bqsj-fNF%pbQ5a(++LI8Gh?1^mg2AcurH0d8K(cG3z)cuPIY(q=#_#$Z*Y4rJ-cUJ>Brx{M`QEsP2RUJusu{*cSVzJ!l4|8_J$>W;GNh zGNKR4NUnYRJCY)OHPIkAMmTn${KhG4y@1ssjWU*L{zrm?=FMr+;$}nMaKob%j#-~I zvUa1Thiak9T+r=|7>1j3l?rl<={?wl`&;jMvyOtP|Dh$9VQx_~@S0k7;?J!hAVpZ(!`ap()~B*NZkjaqB-~UXJ#(rr>fVRs#AhOOsFbvQ04oi z``$mxtZ|Y#e9}M)^%%oiE3jnwP@acW**^du&j#IpOv2cS=huSxegL7e+=g*0L z@BW48q)%iIPLB%b`8ty-aJ2A`EHiE=0si?4N|wG!QK3u1(v`u=xI*!OUJf)nk*?_% z9K8ZdJS-8?sRDW^11rTSK|`s8R-{WXy?f4mQ$8h>GT!eelnY8fq)p7#?kgFd5~yAg zjH9@yP9N|wK+n}rbC=|efz%DzFQ)Iq7Zkl}WAHH2kssWk=ydgY_TaPth70UZ(%wZW zJN^(3Ayd?HWjm_2qbS#5ACSm%*)A$DWzn|8DL6<0?FM`ND)h^#(1*cHJRhlay3$`q z3_oWuv1UcVX4m2y0TF;rg;C~~HfTWtT@M$|X_`mFj~J;~(R@*+{amNA^o z-_?{f$if}(jme4N%mFpj_P0S|=|{tTi(I@sj8vbi;Lj_jmh+pE;znm5HQ&uuov(&= zgSXIPmN%SfqU3Cuh=Qh^N7T zK;~Uk!fp*yDt2B=tf)U9rThKAH4o=In- zTIWbng?kt&J?z(<%AnrxM-1orhqT1yfD>M5%Z(;1i1sfDytcFP-V1XNpKt%P-ZzaVqi~=* z*T$!<;Z|{HyaSr?_Doaa>4l>wS0Ss!bP>T~N3`;W__HM~`~*&r=Gy=NNsa@0iah;qp^&Hr7T08ZYIUOZv)}J6UAANR;}$DP@C^-C_?*kSmD&&TTTC za7*IhzgUu6&0eTIT)OZaYhBGuV38C1xpU`;r7VB^YW2~RpmaKun_IDQyXO&Sx_jBE z<&XM$r_I&4(pULQuM(B3;&zUDqu_xTC}j*O&a-HH2+Ew>{MaKDEY>I^%3E@GEvySiuLSdLG`kA)JvRvp=xQGxS`fQPN83u8z2G zTGBbC3-D*}A-*D7^3dw^*w;+HNB?-5iq8|j=qA*&(>a>`<38MQuGikAxPzSt*BmFl z`$*kDC3S6Yv?{F>mm8Y(Xz41vL4LaU+}rlIqPDvaJ+@6TADWF`YXX)Ei0ecj**MN9AxCVVHxG-GraNnrrIoYt+-%Pp841krh9V zSRzwb8x^$IR2cb5n!bl+2zA(y%M|+9cS+=GgHlf#DoXH;GAq-SD_5utBEx-a^vce% zjh4AjkCLDk$&6buZSH^R)5(!BcDlGKdHwoiHQ$yT z@5r2>d-``Kv}-BOF!G&mj-r$INIA3B&K5O`wu1GTU=OMeSwim*ErQ2#*~z)lUpGH` z5$i7yPj_RUmsnWWKHGQ-&yT-1d;XWT{KEIM#Xd!TU$;FedB(Z=QDVqe%}?8}}vwVlC- zNvM5o5u{l)CQk8M5iTYXiNQR|(`U-73?7m#oe#u+N-TIBZjzUXdh%=1!^+N1p1uiZ z4`ZUbOQC!NeqM?H!0vRVS0E*ek&tCb^>XOz*Np0s4h@$tCkad>#BE86i_q2lB-D?f zoW70{wk>ny{RrdAc+UQek>#dTb(2!&ch|h#m!uo-%dNW8GO|wel8Jp6Bg%C`7P#w@ z-FJ(I-=A!vg=D3riAF5Pr51B@bC0-Q;Z#9gg?I10Gjc#?|C-TQTqY&$hMeIIW!9U7 zFGEe?!};e;7T!ne)*Pgz1LhdMZk>97&T(S9K~}MQZm+7PMwO7+7brQ%hlPp#wI`a) zxTi807b}3?#j7XSY=VBkOlj#pcq6$>3^0-)ZkBo?pZIOMM+!LdjiA5ihp_he^zYTr zBI6`JZ(%gN`o&1+$HOl;s<53e6%PvLlKpfZ!N}s@c=76~YL3S2#^_!kRwoT2wtRxy zTH$Owx=8N)D^vR}Jc#Rc$*6w^J29>@I3_U7Povdk&w@TY!i6))j2#%DQS z&`9>MayYvHdA-KmNV*R{fM>ipP0lir|C3c-&5m)YBauvzJWBBzGW)LXZql=rKLxtx zv8rgC;o8>TsPxGeV&xHJ*A^Qw5_#375o}?~om%!Tb!9QHRJG;4zymX`qHZ){#GUXl z@DJmON%_;r{Z|P6=Z~c;&#>Z1(}fi;Pf@7DD~x|tR0Y0ya}T!WslGn`<|-;NvKMz` zD%|R#(leeHc45&J(LWfFw-uA|T=?6GUPUvfVsGH~f}72CB-2lNP@UAjj0d0lpD?ZZ zQTBcsJLN8YQ$dFN&Jn!&q(t?K{RwjW_98QWozmfz`gdk$w5^j82$E~z-usTGx`6@J zn2AzG53(gmae3UmMgLoZxl25-DM5B4f|iU@R#lB~bg^Z|9x<=GuHdORsFvd|BIia~ z;r+E#+*?9=ScplY9M+u$K|GJ_M@9di=yI8b*NVWydO+GiL*yzebG7J-r!q59p` zXUUYrPgnnR!jlZH;Y?>uaSzh7r^nsTZ5VySD5<+%RYp!nyzymbp-U*zCXCeG6;aq{ zYxYB?nY0rl(ksw8@A~J?gvd4KbkcL@#K^jYG+19H3bi*%id8V@HDb>A$Nx^a2K1%Y zoVk6Xt^gyIJTrT;gk7U!_PDvVSwNoKnRqLCSSlhh(?{lSBt8&2WreS~Su{zvQD zNYmgiT|cA^?a&R!*6xl~QSTh-FxsbkSfEiR)U))o-EqtH2H|7kaHqjFPAr9QA1wF@ z`onc+WcbFE(F~_c7(Txs=;U;XIK^3Zu_7Cdu8$G@`!aLS3o`;AJy1TkA=|3?raq=` zLip-EJ35Wa$O^bAN_8HRl9}0oK_9=tF+Cg5sGwj zVwkNX=~CG{#ajP2Z%%EGI}{2Y!7WBr6Q$7j)8)(|vQYUsvt_Y+H-$<)c9_k6pSItsgqQW zR`Fu}c_Hodm4B+*aO~n~zIlxokyV#uWwV@CeY1rdd*FJVQzaJWb0sghPgkhgMHr#T z6BqY)8UFeNbDsBfO5jj8z5|JD3aqYJDBUN>nQ{E|MoLxuMA4h0YtqOb2Zh3->j{fx z$4oz7ESAa6FV?k5pnKtyPq82l543h=1kFkqXk(y&HmNRngkC@ih>UxePL%=nZPAZQ2_DN*D46c1WPI5(y4?Fv1R=vU) z27Xc`qnG&7K7D(Bm!4>qNMuK*CP88=%h7iPpKjd%igadMs!h zzKpecg*Ck0SZ?t-&ibzC1&V&^$n0LLpZtMWKF_+E`-CC~z0owUGDzyqrT54J1NL

    @yk!(7ZG-1aTX0Y_!zO82>v2Q)b&vrSf!jPwNhG!2a6$c%`Of@l15IhMK%I_kpj+ zE9nGM=CUFO?{}TrD8gxA>;Z-O%P)6&g|DzW5Qg0<O5Ig032O$m(Yx^<=Iu-5oR`j?SsFc#Cm~7uRz5uOC|=UEme0$wQPg#D@%JwD zmZo>WAT z<*TP+M9Ry1AYTTdPpV#c1wyMIxc$B9ExladUdDPs)YmRn08=uV$H^#0hxD=Z!5M@9XJ8$Nw| zEp;xy z4PM_AsnWrFZcZv+gOr~3J85G);~9D79cYX+Af%_>!4+4s5Mm^fHsq%}?ukjaUNI`u zVzK2Dxa+>V^}WI9@%qk}n5z=wZ{>V@MP1DU@4rQNkbJ{DP zX-{T~5FS&y1D}ALS}rv-x50l*ybX zH3Soi^?7MS2TOBh2$LS8KXm1}c|t zg!J3v9XOn|Z;$N%UtTgYs!wudvJH!*UEWVs0wbfc)CN9*VZj>}u9q=G}iz^bF7OxeT>&H0n!J(;}#sQj``0?zc*P`bcAIPUqfF5^8_C!5a+bP{H&LY;y|vzq0Qc%N%1y`B;a7 z(L3Uef!~<7NwpDskx|R4xaNGu&s8URS~UzdTH@K#GX`j_DOJ<)OCPwx8@w{V+}U&# z(%`Z9v123isK^^#z7uR+dX=5BUsL7PdcZABNObM=xfq~$icBzxwYSHxi1U?LTKLlm7%403C~e_1 z)=*JHrCtIX9ZC5ME^Brs5|uHE${}Aj=P%#=J8k2gg%ciN^go}|ZoJ&h$ZJYp9-Bi^ zS9Hq|rhk@=<5LH2Z!#kdvA(*z|6#rPx@qnUf_JETdlmJu%pJbA(TT%%rZmGzPhVus z$IQr6EMptaq<;~)z+uPT>+5wX6<0c4q-|IWw9IZ^TpN6ZMyvhIg2fijbR#x%Z&vWb zEd??G!(K6b%vr4Y2WqjHX8#v3#Hq@M(Rp{*A0;HV4CG?wS?aPEj&CeMU z^-=Tq4+_lb=~Z9&D&u@}1AOxWTFZoKpPV8PlmES0T9A^OML2jjDr))V#*UKe-o%u| zZodN+)k~Qk+M?1^-u_hTMleTR&Y!H5ORnU=?Y{>cPSF3P+D7(Xe9NHc);Pv{)|KW> zQ{!je;~!rnU*#6WU$(eiY`sSlsJKj$lD3=)O#F#a>#wHIN0_L*#k($j4|;X2MR|Tq zwt}XtDt_lJ8!At+ts&3U=%um6>BFP{v}c|@+TUc9h1~Q#ZH;F|S;BA)t1xyTIL8V2 zRb+J3M*(AR6w_7D6uHP(AwE6c5#+=|d%Hnbm3F&kmbfmcY5;)|wa6&~VOiLb9;Ly| zX(Q`R_5lBn=%6e+fA*e}%e9bZDWSemw9(x3uZJqr&^GO~{?GbJV9C7hS*LZ}FuAsP zf%*BgaMu0&VqUhR!X@-xSnI4W8|`}ctP{1CJooJ2%6I+Upp3KZbRWM?)fhlGr&ou_ z#qWOqSfeGL%GgKp7s@eznHD6aA3n{?`4xuQ`WBHd4;+8s&z{ClQXY4#HNLFZ8M!VB zF8>p&_5w+5k(tiIYO1c7-u~6;q~jOFf-Fzx+r02mdGoqxnEj>$|MeLe0z5t4(ZRvM zdS6URs^+H&iEpK+<6CxY6HSSW03Y;O|t zx_a(WxNu5|^rm}0Y1`vu7belv_fa7NqeeUP1(FxI_(xy$iT>) z?cD-rlb%Eyet|jObllnQdetP<+whKX#^?qGt&xULH|?7y)zZI5MjKemr?gG%Hqe2z zeOi7MMGx`rW(&h<1d`Ts>3a}RarepE+B9~oAll^oSOboe&^TKdn6j~H%EQHoB#$kD z?+_uhvE=ynI8Z$PP~Kpe3xy{b| z7#p~sTwkeTVz!@>GxSL&N!{S5qDILp|51_Exdp>kBfYL)Gkv#X;s$4rJ;o}z7wgwG zGXyeUZl58T)`>A?wOt`lp8*BDYoN7F#p8` zO10164RcMJxA}(q5+$Tkwrcs&+;L&UwZe{1zF=fQ$1MM+$!{LDB+~ce<|ppC>kF0lDym8~ zAMAcxiX0JgC+QhVHfhVx#-4UZvB!Q!F)7jxEaubyRF_eHTQn>$ryAf@^AwTFiQH8e z$2#EDXMP#`KTLgjJe2SEJ}I)LLZ~dIA_`e6A&Hbd`%d;5Ap5?PeW@gA z>}$44c4HfZG3Ixk>izltyk7m0W#)PA`+m;3&UIbqO!qbMESis($O@+YveyjlIJsd1 z?L=&-+`nro5WnZ5i(F53tV$Qo)jP$%k~H>A8ZgkQXgf4DG<63J?f+=$o+s^^LH_ix zC&^g-y*4SvlVO>WvI|af{XgBUBbgb>EwZAUeo8`O`z4t9w4VGAU>E)@lRnVfOHnnt zw5`GPac1Vn$7j`Ub4LY?Yo0t~5&QxFon2nu-1T4mAbky}6=buc#iOSbcRYV@*j1JDOYqyb5G(~Mv@g>`&sbh9Mn@PS| z!ZhyQLS{tB>p?((ZWP*TG7mhP%IkLbT`=C`nzzp_*^9NIa3(GByHF`>%4w8x6(E@< zo#)I=r=GM*-CywNJ9=Z)60nc(e$VNW$$TcBRSg#O*DJ51qOJvhT|DVmJmo%BO6@6c zFkIT_W1?>ecq;@JeQf4?jorKAFA1KLvYW#igZjfYGo8`#j@1eA!x+`>UHNMB@zXI% zhUi82#9@xyQ=d~?LoAd>E}yyl(oJm<_P+FGlo&tHPSuPJUW(7?mSvG_lx@<&1CPld zdKcE3@9*E>N9_wE72MEQ!cWPC91QsJ44WQdtj$s+UUz9U>qpBmgsB!DW z=Hi8Z6bCm^a9CQ)<^7vs{5|DUpg>`S9dW^eU!Jo5*PaM?1#2C-yMZ{ z?EU-qLw4=}_e+v~IlLF`^Y-jTm*oCuJof-W#8G~YaHQQum7vNno)Jja-CZ@3o6;y7 z<>#JIs)&3@wua5OM&GjftrufEv83i;m?(Ph)r#It-c38W!)n~-wE1wBx5#MYizOTR z=O>la2{#~Y290Cpqb~}Iz%a*)YK>bHJq-_dqsTYh#{+lo&`THHdWS9Gr4g@Rzt!pf z>2pn*r{lK(!()pMtJc4(Err%)ial8xcKq^Em!bfYt{)rEk#tpeI57g7FleI3>{>f3 zc{kvS$8nXb4h=NzC1)zozXFWT>kS^eNJi#oT+{!DEEdiFXz;HJJ|1rwt7nA|(}8EP zCup^5yy5%CE@aPYY;=~oXuy$W-(TlSCWAr?Y~Kdy^CdVanp}vfJ~MN%wfO_W+7!s-BS$>qElZp@kY>c>b?7&YR1j zxl`~&fB*hg;_^C^%8p#)qqN@7-RF^INw30=lo;%(|V0TWWI<%)eo%^l;)>@duVsiJ!2XbVWtqzIh-^ zGS+2r27tH4k_(4eKc3vDQF3>?g5~kfUT^m}B_QqQ5DB{ar-B9B?nC2p^8xw!9@2V* zTW8UJylC3f2QUh8#Yb8@yb!)tx3Ta2c89w|+2*RNXUT~msEk9K{H9F<$ zik#`^>6*ipgKRWVXI3xlwWU`D5I;^l>239<UQeh zlO^C64sGra<9MfCv)iwwYYw}Bd(yBAkyjUOYPI!Ch^&q@!0B4EYN=;pbJ@~*rYSo9 zq)>fMstNwXkFc7bEptExh2;L^$Qg0j`oOXxXJ+j2QYap;q+R1wmMVm1Gl)&8bnzCv zz%%na`6QD!sX#_ih8CrvtI4kA?vX-z`(d$&;B@*8+z(SHi|PFv$F}Y=9dK!=JC{wo z)Kwc101uNJh32i?P2_ZokFjXTVY;hT?!OoKlL7qMnQJ?`26`N;8x1v$Gq!6MVE3pT zv=k_;_F(;$mx?T9I~su8xy0UZ*J-T$5+9{ZcE3Q7Hx{7e&uwjn#BmbZNy>unI`xwo zsWYchNa0u#3`B^i-`kIgo!0AGrzL#Op25``6t`XZ>aQyaxK!lu*RSigP7K_r{5jy3 zS_$X5nbWLbTLmsqgpU{`AlWo%5AFI*7s&My}+uV-p7mNun>I^tff>|7fLB zN>V8pIYKFd>pD>r@k^a=61+UB+)ItZItKNbWb*1pGiO#*6 zM*o`CGn;&LnVSgtOqQ^IWLZs~z>Q)MTXh9NyJcDKLpY+yZqE}Szi zhZak+re>6HIu%Z-%IOn=vsq_}JI@+#bK<)gG0o1LG%5IB;jb>8iFv{_8!NshMSbx|LA@@%g(9N_;uM5b?$^l3xzqs?48S>iWxHK<} z9)<18;Cb@;g5gw-^H}$I2-L@N*9lN#U)I_#Sb5M7HH%VQgip>o;KpwFIbc1t;6XDA zIi2_LUQc+&`1w5?OlXLc4Pz~H3VG|^jV^FLk;-OK?&(ym#naX4EKb_EcTkqXOVLs_ zo%~*9ShI+Q3~}ac)c!8t9u+17$Otzq5GlYA-=aP0w+uYy0Z#OQBUo$G9Hs1YA7n{d8tgdQS05^RGueQ?Khh@t@E#i{DC;MN{}yb??zkK5L=&Q8n`66o zI^D+7955bIOaipeXqF)n8%+p&9*_(seE3C7gQlntjp1FJf7H@Z8QPi63ATw+o-Rn4 zeN~rtdLYuJK|pN7SjFS*yUZFIs8mCu9*RQl!xYs|Zc9q?u*lS$rkY?c8u)JN4^i5+ zwXTWW7l;rDLTX#_0C|}v9!LguaX1JcFUNYkPe+NnqdmNTGxPFq>-jV`<_8SVNXFr% zJAE6MR9w)N)5NMLm}$r}#72XqZNq`at+n2$`?w1E7U{$RvTT63NGW{D?&k_>Nkfeg z#KvnL3@dST^VCf=XMqJ7oWAP$wudU56kUl`fOa}K{Z5A6ZU;6#b#(g1QiM&&ZE`%0|UQ#qBz^d*yAmq`(q?Y+S6_ zcng|lunh_JqY@P|_P=~z4B(mc1Ein;L{Lz#%UHv-)Kpzn9iB&X;!(o!`R;zZd|`i~ zHr4)_Ka$Cne~fad=x5cFbSIJzmxzBBcm~Dt3D>(*GYnFX)=FOnrrfcTzj6#faq5?@ zCI+78u%T;La^9P*3U|)yz_U%UFV}#F4S9ypgs78AkEvs_e!zAdb`1<5Nk|6s=v9m` zY$g9%DatM24KH`1#nufnQr=O*G*R#mnh{=m2J?6$<^7eg^pFpu1_{CIoH636tY>uF zRfm{5O@FOd1BX$(_9m`mE=h*+?R5ei(UzGP^(MFO33KSZ|5PR&0dEQk#B%0WLhd)F zi)%K_G!%xwh|Brh)u`WJV>=vQ|FXpxFY}x6oRSjey2;)JJLUk5NmYlGFUWU$GvN(Z zemyMu0Y$}r$EpTk7pKBTCgYY)b_(6Jl7DZ5(o(yZnQ3DNv|31&Y%cH^Osc?X9~=_z zOqOIout^h&VR`iD`p&IL)z$qs9&bPYLV5V|(U{dT0te1J0;rBZwdE6G*aJD~(7@0Q zJJjzQV}Uc5{YbB!f{27$^U|7?NuC~+N2LH7yP%z8rWdiq_AFzFQ<|qv;kI`NBToFj*b<#f>~S5(SKN{!q=N=zGr>*cuJR>GU`(dH z@DN4QjBgXLZI{ks`RcYGOln5~#Uo82QHOt1Dz4JFbG21gNvafoXqGrWIe~xkN|GJ8 z%A=dw9T4k*MsIb}9OUWt<=yemxazT$V%?Y=dIJjtVbADS|A~V-kcGwsLMwF{u{ z>mp@;4%Qydyfoo<>;Uu6+(V1Zo3fVj2TC74DA_7B-G9>I=u!+p*ftA-h>y2$d1waTwMKY7sPbgUtWGa3%+Jii#4D5v(a|`#<+E@Pcs%?g$uB()jME&lo zl9&fe&0m5j<9g;Cm2zD0xD_ZE!91ca z@l}dDJMuio??`W7_nLsphgz);j>t9Uy1H2BQ95}I<@ykahSzpKJpBCin{3?}$KmR^ z%cFd#i;U-Hws)P%hOcQa65rHsPPui9BO2aRB``T0J%fUI(pxJr80ywbr5(utV8kq)PNy|2*t)Dkybfsp z)*gE(xp(ASob%|(Vp4Q)GTG!ZGe1Sy&W_23wDPDSRz+8?C|#c zxoP~+{KQm<_cOo9UN3AOh>O&mo}KQGwA&d^-Uc;6-is7jR**S*j(fr3V%@rlISKSY zIj;+{TC&E+y1spDXWl&JD4tkc#9Hvxh@p1ggU%2&AoW^G@?y>D-rwd9n*3jIU@B@VdT>#c$;4u8$A)UoGS@;Itz0 z=*v*g5>Jlb#|Q>jp~~+YHxatW2Tot}kAdW}a{R}V#}t9Vmcis@x1`Rs_{b#0hbMph zpRs`uT zJOP?8$Zp@1)v^M3W4d{-ST-X*c`q8*1ZSU z4{($2D7PKrWRf0_j?dQ*;{~$2+}=nu#@K%6rg4O;)Kvg+uF``>p6_yI`gay>wQ;yc zVcGsi^(};UN)@sufJrTLPx%_@?fNm^v;pCuOuKH|-}k6|2V&2()meC!Eyi0UF9BMf ztEq?Jt;w@ndQSC7l()5qMGCM%uelspqp(*!^10Ih3(d7E|F zDM=pP<7E0?ZS^gO6O?cHCaQ5NR3;59EI{gDSxFw$(%sf#8|0={rfAZZN#-}ox-tCp z0`CQJ?P<+&mwo$WXrmv@TPCg4Q|zOP-bn;ZYd%|=S@R_;w=fwHQBR#pc^yE5BN|?; z?2fqRkU^DBPrrc=flGPX6a8yzwCRM5@(sGF`{ZQg{ZG}B@mPPmNo~Rq?7mw zh=m!b0b6ck3F6KJ>nL)N{dxRcZnlvrWQEGjfklCPlFu#|-WHw5en3w2^9?b`b5m|* zx>uks=`tD&5H&ANodF3Vow(8R!%%EAMO&8C*M1efHx2mL>&LK6yzwh?5d0)i`AS;5 zO`~R#9^k~e$#nalp;T1yt&>Ze2x;4NsC=3+FR{lZzTn3a)^ULi^vA@V)gsYebyNPg zRVHSJOtY?#{rT?@7`00;t?%Z-y&b>WeYL45q=?x5`1@)3b(Y|3`}qvdl@7Af=}%gL zDACy##`pLOCCyfE-pS-i#cc1to=qw|nH>?Twyi>{3Ne44@bM(_mrjHPA^7czH6)5p_TarWc`?@OD7mKn6V z?Q7YL!0NH$_XC0DzcVRYow{h|I>sl|ahF1QAAIG=0t>X+w5M2stXSYGvCc6u&+|JR zT?w;n>V6$}Tk&G{eO5WA&{nXdk6JgWo*mU6YMVs4jDFoUX%23~ zLELZh4Gx}{xM3?@GT}S^${D?Vz=^+(KEm#a8!(Of8_16S3uH%b$G=y0suq<j&G?raH)Xy%U#QD-Z&{nWwJbii*BkMv^&IFenKKhW^1tbgxa^)L zkge1n@^~>FujJtGCjREgunl@k6u9p7hDU#!~1E=)hp@g7t&ds?O%M%SJ&R*KK;{@{62Ql z8Dho8s>06G))1b9Sp=6V%jWb)haYqu|G+)UV80}1C3_|S##S+^GI&Tjm4B(`zI27J zK@xNc*|uRFqzVk#F_`kC(XXBXXqd@Y{xp2iH)?>3%%wD(2xu^*urLk({#hnIF7(6K zkYhp2->~*RFJIoW9~T&HOLo3Cw_Yz-**R8YXQ3YZD1yUbpIJ?IV2v7(D7;8k9OYQX zJ}Sjl!3xQ;kJ?N1{#xI$+<2WczH?Z`l4w#eczE7|%syUk)Bm;e=wi+jbVBl@?{*^5 zP=c5I6eeiV$vGTWa;ZH^|LwNwCb2a!zXk>CxVkyM6y!L}dhssTDCHbZ5bBno`Kkn~ zbkjNFwkTm}&u7t1=yLxMp&?R*7CU(k5M0lv;=gw9Xv*Q+&-acCi5?&5%F8?}d0SIo zV|=FLr+u_G>F6aNYS$F}hp%W;U-Rq9>MIT3J=0akZ^i8D2e-s5fdaK{bZW9o=40G_ zOoCbAzJh4RC&JA}4MV(G_uA2EVdf!`qk15EMFUL?Km#*9(jOB6GHeg30l1P^aj^%{ zAg8~I^*iah?Z*F^((kYa=@ji3L>$R;&)dkg$+cw8vy$}=@F1x3vuG&J;?ByKZ>CNa z#`AI0=52}GZ5J18q2=;EeWh5ZM11&Z+;%O zF5H<`5s&A8Rn+{sdofMt2B| z*}H2ZR=bM-VP&Syultj!UN-D^eiz)GdkeW}VEq_Ar_^XP@Bgy%3;>!Qx&2^dBVpIr zol7X+L|yND*1WzoeR=bKsIts}W=AwKf5Mkhy;8^E-$Vv&orAA$Qvv5K&i8n}eu;X? z_rr>$hNYPO;zEQYHiGiDCGi&aumaHC=#isK`SVI4KDEcgio*wL%tF$55rKdxp6%== z^fdN4-5sve4sYl`Zb}gAqs+<$`$33hvFneazP#jL zqXC?}X(fJ-fDA)&Z{Uu42pbhwwVt!)PR2#FeQ&$(}FFFiQTzX7@ea+V?aV{bzMG6vGY>lIZeAC z$TS95x9*~dAYS`W9tCPKJfYd_x|F8wL;yGs9n7s34+;U7)tpJG}J8?-LYOEyInhin?nrjB#9IQmr%b+Ub$xpt(Gz z(kX0)dQPK+z^|!m1^&Y}AZ$VkuiL!p4;msIojf#<>AA;?n&`u5gHI0AADfms;Zuow zzPyJRIYZ0;^G&DAF0AzcgZ27=-^};w>k=0@m!Mwnnm;$0hxr({_w`%*M+nddgB)~% z`$3abLl0Ip7#UQ{=n}s{k`G}+yghI#E^FvAuCM|yRp&|lhF+lAk)FSOS*m+wm|6!M zTZS&Ck+`C3{B>i#wLY#B&QplnNY87gUYaLcdrG>UpH?>V_KCrw36`|x6mTSgb&SR( z@#@dx=tiqY{O@F6!^iScXA!7|pa@q9w)pzuKuW|ShU!PZH6YTkk)`rZ>xK1e;62E| zjw1F#_fPqOGgpc| zs{DDijT1F?+`#yREZ(6?oo#h1ubSf!dH!1V5gbc9L!hc|n6EmKqgSDH3x#H$5^kZJt~_$W1$PoLcWG zzcS1D!3jUZ>x%awWFQ(Gczf7fNl@p~%zGcw)|u5A+5z%@*{?h|KbLtbC|2|y6Ne#D zQVx&vPj-%@g^Tht^<^rsN$C3SMpJQ%VJgzs-;Qqx{U(Xq7*nrS3_6OS->%poGqu`X z3Zk$2fg{0DX}pNTtY?&1l%A?>tggW{=2{X>=x*FRm8vRw_Y%_3MMO3*JA zpI^docMno$0i-e<+n+P??rDp-i~htVNuI&Au2hXu*;oZRTLW#wFx$yDLRZvSbwF8U z@HKzr>aY9k)v7*Yu(`%NRJ`j;C(Js6%FA7=3@ZhxKQc>{53tnZ+CP^Z;0G17cnZgo zQ3c+won7{?mjc1Bf^xQj^M>$n)-#h(m|mO0frOc{VByeVmoBft`A$%~vA)W#RJK-u zP*RHTB5ZAf!#5az|IE3c@as>i*vT)L zcs=V^neznTBR;hOPCn37sl?RY@c>_6`lJ`4o7J4KorB#mRlH0$lS@@=bo_-n7mHUq z=iJVmQ))HMQXL2;ZkL@?KBgL%v_c(RPZ~<~_APJFwf=?Sql?$kG!BNvgkq7pdMRPS znQq>@&-)8+_-QHcY1CP&#Ljb4r^W|yUDjphz(L5#jH83WFWCg>EFwz0P!8l^2nbIB zLm~bew-Ys2C7<*{DgArf*hBcimspYDUS@e}hr@?om^yKP$0*%w)&#k$$dJFFYfe#l zZ_uoe)Aqpk=SIy_9cnUg3k<`bcf2R&7vo5!dsc|5dktciw2T6N&;Xw|WNhu&tdIlj z^OIhOD2~Tzyss-&Di4J`cC3HA+=kh z{W=?wZ1%X=I;!`P>^$S*mnBEt8y#%fu`lbNj_($EOsi;Fp6;P+Ezty+l;&#jpEaCS z+>sZNwEi*x852i>jkH1QI%euP;kcD<4f;I1zt8nxn>XN+deV#AR}w*w145kz-0i`i z0<@3C9QZ1CTkD91PU=pV4F1l{JKpdtB47aD5za7sG+^mD!uBA8#FJlF%J*sR^&4yZ z@W_K+hrb}=?(KdBMbTWEQec2mRamH>7X-!vD(wBrMMK8NT-QX!q~59N9_XUk5t9&8 zbXgBjB`Mv@vyA&h*+Lk84~&0sFF*g6RWJ?2SxAFu;?pwX%l27yud=G*U=}9?jR7vM zmDYao-MOUdr3W4dtJ-y_c)}=4;pib4C88ovEH4aW=#^DW^WGLse%&)r)k3b`%}~^Q zO$*&9i1~VKyr?e!j2Y)LvBt89=d9S+jEg?+&0`_DOlK|41-VoAT)Re89V&7f9kV(^ zdcL6_(qvUth(2A+y+UXlW_!8)S(*Xve~i4Pw80(_Kx4M6^3C7CiOrQDqFO$&^Ib^P zqwI;)fxX`vtr(YnPqBK!7d|I~srSueiUm1^FE?e$79z#S*V3qA(R;9w)>OofWlonW zB`V%Nj+mzz^#m5gkFghgA>FV>WlzeGpVe_Xb``=mn5vbk82lHOIw&&R$J_oSm_#W` zs54gbTM^UpfXWGXf)7yNY;S8T`1rqwnLf)eI>UGMG*n-a^!B(4tI?A#rF)LRAJ@H9 zsNM>&(G(NwbKt;lFAq(HRzySS$Zib(uQ%srx4^h_+vATN>E!E(r($f?{ud@82qIlH zcwKllz>0bGTV>k2dU|;1#Zj(OXFiNBL5m6O zWnhOp`2;vxQ>Ar5&2Ssl1H*2MlR8LApKMfa%9`JAaD|Z7}v3Q{?7#qV}ofHP`SVew*9|45bY@8)l?FBwX z`w2{<)R_(-E~zPp0PuhiL<2AO!PzNUiS(t|p#yIsH{wD2;<>ViTDpb!6_2jTvIj@Ki6q< zqGx*XIzV8oL)4EvOjW`-Ngroi01(Q{H$0W#v);HCI#80yAYON*(n3WcG@Og9o??qg zFf~9GjgNjLhz>vW+n#Hy27~aF!+S7VA*O!vUPP;#lb#19>LB4>RKZ>nCe|PGV3bFJ~god1(hDv2KLs=tKSX-CY=d=e0iYQ>LXYF2AG8qongQ7;5>gAUz?zQVFSW{U&gqEQe`RXrA_h@l%`B}XwGKsS1p%u^128_R|$G&NAT zIJSSy3pXjjkb`(r%lXt*++#6L0PB01{ht9K>j~8$@3rEQb;!*_eDp{ogGHMaVuy#N zOuL8IZJ99vfEuF4@kOBaBwayF3KI0e``-#^7nIW}pex>9)!?~yI}Q?0Pf86wOf~@2 z?jr!rP9SQ`+@~1G0&&aKRp9enr4Gr%oHG35IKxbg7pZt0Ee8?&?ZSf}xty4fpx1=(pad#7)``EV* zY{9F5v_eXRYO66mtbK9tIPXv5@NMW$>QtfMS{^c=LV>xsC!A>A308NZZ2FC4y?nq1 z5JdDDv&?|M1HqMyEPIF;>+D_y6zSTw zqmnFzpOES|uNe1y$Kx2LxQy7nc1mDfJGmB+`eyH0P3Jb)h!t(k+rgUug+HzvtF9UmDJn=ft^l%qPcL|L)M0W@ROyE-tLzL~Wa|&~H z+^Ly0xfs+6xa@Cu8D5Se2acS_=o7T5SPZ#R`HPwhD@1VoTxYK@%rx+FJak==zzSd= zhdMWlazVr_sFt@+^1Nnm1@P-T;O=5K$JXp>v)iEBXYXoLrOGX_%h8c|1gWp-KbpxL0+|L9 zM?+K(zMVq@qxKCX=Z`rR?;707MvTN|y2!1G1r{;g$xJt8EWFOy1USUJ!K}0HbO}yu}2U~m1 za_SLS8sFiRW2`Gs(6r?$)l+IY93E(92$^HU+Y(Jt!52S!AK_?ylk)^AOdZ8fELYFO zwZsf4RJEruR^DW~^3RVu>x`}7t4zpwE8#Mg)HBml`#35aGgSnFvH4{^kBVcl0j4*T z28*X6{WaOToH_a43Y^$NOqb=#zO7+ZY|gi=idE2V#A;`h$#P6`B zQ#S2TB-MXO@(IA^DSc_#R8VQJy|eC7)~0G)NGn@~&=$5_VHN+Hoo^E5q0p<<&A(~l zFZ=m7(rp4gJgWF#Rno>9MuTGjjp*vd9a_(vQ~Y0IT>}`t?m@5#pWO zAe2KNh}bTx2p+cyE(v-cWB!Cj-pRG0T;P=OQfserw#PtR{+u9mCA;!wBJiIHRrreB zvASSS8o_rLKt|-RQ!e{rT{bnvgMOBa`9{Bf?2XCOadFilq47CcjDgnRCbGHK?E)9yNjsps5BUh) zA^NAVd|0(E8%`&7s3*LVtXB)tf}>x&+uY`jJDMhY2CyI8AQJ?#Q*Y3a4!{rBG{&!E zzsZueTAQHXYyB;<#zF?I+_gs7^~pq=r{58Uo_EhLP97aI*$G!az(?FByB z?o2q`-@$(VdvAF05?KY--tGOEkc$R7=(j=JXnwidHbv$D8g5mzQ4V%|1&n(K>W#Il z-ldU02lv9UaqprWZz))lBlR@$;0%hB*}D87tCMM5{BU>xG7%zp-+w9n+>oOd)C%{A!r-8e9rOera{xkzz}CZ@KLh(;Xv#tPBMm?oJ=p0}5S$$SrDyNVoUKs$vqgV&!BrSTp4G%0WVn_!Bqu<3N*x!8B zU||8;HQBCCB_zcxynnSd{YLkRn|kzD@L{ZiaW|z+>-Aw(dsH^~K&1>f) z=@W98=2>!xEM?8F5u3#J_q>q8UuM8SQ9OF?tXUuAX7Kz_Aw+Krxcjs;3@aZXOsa|` z|E=~JWtXhB^axsS0xfKkSm@Ne!!tTu8JvJ?*oxs7gt}mv2*vCC@u&-3hI1Qm=`Bb1 zMAwocLi1yQv^SNi7oTl5y^h_y4`l@6K8Od=KNWRP6!IMZ@hGuQ!!1p@iMc*(AGA5R zeU=)!$24DK?O|47LTZ7B0lW?L0i=Ff|64(3JCn57>cKwY;$g?kNOHhqRPM@yVC{qG zFvsCStH2v+3MYcDNXAFjUE9EiQb&5dbZ*HW^nSz$(TSCXdpeO&bX-f#^-QyMS@3jj zqOf&TQf3A?p{#O_IB>%0&w+vE?R9V6}b$LAd=MhR9K18rf^k>>bZy`EO`oTBPO!+5a67m8jQF_CA#GfYSLGrr?G(s>BpzBvaF> zRMuQ3Af~dS#k#d-7yh;`D^=G+OjX?S0bA{T;C|scrO+5Cd=an8%zHx3MaKF);0OH# zYzmxX>Sffrzx8(?)O{%S$Y8e11O;o;AH=`%f(2Z73B2ZE~CTI7kDw)nb}G2p1pUU8a^kK zx(3FdsI6hS4?J|7hL$YFjzk;Wk1!gz57nf6IcRwRYg(*MdFS6oMu^v+rr!*s_DoKE z!P+jO{PLv~ow@UrE5VDLb6v1l>#@ENItgAKPgIqcbB^a6;iX0ydps6FN2 z1=c_bcjMu*GSQTP)GuHh1!7K&vb8^1q3p5(7d#iw-2@c2+ z86>C5qQ=`A>j~+6SPNt8-xE;mBVOPr`i`9fD3BMyV}!A})*L;%&i?hQa6e->w6jU8 z+GIrjv^sU=b^2z^ZWKTA_>=yrn%!LYb4#d|{Cyal`D!>Y*Kl6y>t--*72(}Oj@G_q zC)V68O5;@*Fb+@1RUZ@3?JJi*cM|IzU>fz@2N$f}($o@zskh(4b+pXGWwtOzPZ_km zTPk}u|G*<&3*$P^lQF(&i2R>k3!%|-3$>WC7f1}q0g#? z=@2})u)r{QmBR_muKk#snmTd?8pZvj334aGctuZ_V1G%=Y*2Q1Ts0t2qr`>=O;6LP zrdJUu%uk$D86Dj#zSde2N&i|}N%R6mVyC#-jrm+xfm&Bb&YjjB4wX@P;pJY82N~HP zU%$#J@b&loCtg5{NGgOhXc$)T0lV6cZQpguk_P?Pl@bl6y0R2AZ&fnDd@?{bkHWvNdlXP>V)|2yG zABt@=0!bSNC1_-ln!*3Qm+TyXgd5f#s_Iefp!DV?s)?G-9;mGgXmo2BoBs`dv2L^9 zvCYEAKljw42bM~O>Sm=`t|kgJlBx82w+^qgixe)H|Wi$8uQg#JW$qDz8=zMFTTpyec(j^6L~M;Xdut=PzbJFkVj|z(-K*{y~XM_gO~ND7p&M1x`9rSu|hZ zPQBQ@+u!K>ou&p;!R_x=+W#yZxo)=U8&r<)*@mBYB9ci@5*h=Y6a2%QE!h8sf(wSJKq{ugr$?d_@1o!mo?~)(%3}QY}qh zA~MY)=tIUb?d3rO!Nr*Dn?zPAh1^VnTFe;cA&EmGA#%CICmQ~)n`>(R8il84oHgll zq7LcLj{Y~~g)&+x{4tF8(Sa5bWWmaKPUH41EGw~e>6qH#I*+ z>V_@xy4`us0%sbyGWxp)vI6g9U?f#&M8wDUud&@8YcKwt$l_1)4>ovN=<%9b2HJzf zITeP?iM|yX$tgz*GsJJu^7AhKK>5IuoHCtT(7d%!F|5b`O$$Tz;PngRLnE_lHa|7W z2S`!3TA#HxeHy8kx?4!JhoAC~!KJLmwhY>&;$i=TIS$0x;68cthh;^wc`oTJ zM!Er46BfD#^ny~u-bG)UZevXr9qC&$pMaOhCeeyB`4^Tk7x3hDduMW7lA?iTX}_J@ zX$_$!B*kM!$c(o~5&s5noG$%9b;5*2)wE_;HZSp5G`K%CBgRZo$AnM-mS4O{vcM^w z>cK&Os;%?YX%Q!&7J$Srt+DH4$nKYmc+yLe_k?l}>P1-eN#vAnp^G$h^(j9tDH!bv zaeWFgkBoN3S3lzL+QZ7atmMKNos|wVTwv-2xQabq%1h$`8+$B_y^>Oy_|5qE@d-hb z*%tZ1E4*Vf=1_#m_?&|2jUQnW9Eoo}jsM$ce~A%^HPYPNT=RCM$mMbeT5y*!+%tPt zX7RaeDwK16c8Rc?7x2q2O4l+e_5*x&{q4pLy(-kzP5{BGuZfJhHI)@)T*-HetQu`SezhQ< zmqj&7ORpY6J)^rAGRB201*goYVEj7#zs~ju)&q&1$0`Tiz!9>t2L*&kJ_qGIVNBih z#NlF9n8*e5dH#*3qQhuvFuA>T!tJ7JR?}@z`vL*Gqn2H)$E}mA1ToM$3hDn;or=o| z^%|##P31rhop?D&&XtYf>LQpx`)4Kxl^#Wg2XNbJgg>L_9XoqhDQz2{L=Qv;jGf2J4*#zKf-MPmh}n(v+OY z;h5(%VZlZ1$}OgO$1ic`;P2OtNk_TYj#c-e4vn$Ab=?#_un{pN1Mz9T@%cuyrLN-< zW-1{cF%}r3r(ui4hyP7N3vEdU(Wuq~(ZqZwk=EyUq@gP5QiragEs@#3McNb;f{($}wcdcVsqdWpJ5Ze;7<*4jvE9 z4ob30XP;YFU2m#E?Nxg2PuH=Khf!wgAh)j|_&aJ}(5tiZ?gr@@O!1Sh$3}C#HZV4x zW-q@>+DtC2lsoq69@#T^0s~eVB<&6~Sn1lYGGsZ7nF?+QI7IE#2FSZ>kUlpO4KNpL zRF*h%Ca&Bhp3s;GCH@&mC&zSXu(l^DfTT!VWvK3;+-UzTSi3_8fTQ2ebfYDc(H|F`Edl>{X1z^O1U||EAjQ3Z6 zNk3PG!5d_%Q`yY7XST(|it5G=FFt_=L*5G(p-cUg8de@Ippr;s7yFn(s~!}0<7EX% zpBoS}!8ks~px6K|r6lW0gSbxXpwXqx{UvpIxp_PqW6o7Gx9D3sa&xH|o9@}d3iA3r zrsucA3Eo7}V^d;1&Bi3-twd7nP!uIX`~H+lGV$+nxQrw~iXmef%1DxqDK&-r};tzJ*dUNhD!h_}^?D?ptSmzRBXKs$ruT z2gBk@_a}E$vf4jE2s$B}1Kl{0j&&FTkZ|at??ccAp`#sv(;$~Ud+{Xh*7F)Vq=uQJ z7!IrXNv>Ta$F5TZ@<;oOD|DhmYu_UvPwBkP#uBe8o)v5)Ls%Lu_cgN(vFt;k{CQEB z1lwwQC^qeL^F*=GE)y(Z^E5;DzI)T1V(szUi2;|Dm_GH;nx79pdicv)a$_2A@&vip z@AG-N0(s(y{k&U=$x-;|r=!9B{iL5iexz3ss3t0Ma}()J=b%9d#f_Eq+gmz@v+8TB z8FL0r%-sDC2;Y&Zmwq|h)_Auj048jX_7i*|>ot20Yg<@4K{=83DC}QyNIQI9kiQvJ zXSu5w#PqW8<{Z+D-Y-~Wnwyt{7!c`)ci}|AEGfF%URPk`g2dr#huObZN#?I(A9%3( zV1UMB71?|s_h=3Z(~qF4wlp~QnUzNudzrpgGYy(2gLca9KsyUTjOh2s5sRmE0x@H} zycZ62lnNoH(-t}C{mL_b66$lhr?#}_!t)Fr23%2kVe;bhXV2Di(gkRBaxl!w)9RF^ zuoRU3p{)$eveVD6}>8CuE95;ck25|-t{vD zymzjvC;iP67-c=Rfcuayt6Pd8nzC?phapq7Fik{#Ucmgn>Xmmg4g- zMgRc>zCmOvA2I?EoV#%FO?Ik5dk1gviNjpX+S8l5z8f!uUL`>B1tuO*PKV4p4@8BJ z8bEu_$M|wLqh#*`AeMhX+}C?XEJ(3t7heVLPph+>56NT?Lyq&-zpaOc`39cpRm?B| z0n~n0Nr7I?`nu8SmVa7Sxf6)zybV@4IpH(FG_PzEfE|Kd=H9wWRt&A`tw;43wAIMOM&*I)*MpJC%JZD%JBJM(T_Ajy3Wy}d83`xM>9A~{rJc#qG_YV$0-|m|^96U)Ry~ahi zj8(Bhp@92=OllVTH+r_uCOmBt8~|MPUX$kKvU2U4Bybr;D3E}Hzj=JUAptmBz4gNy2d=Iyn$D~Eww^=dvhjd3uEHCi zWZCS4n-{T{X(#!<7?F6Xt)q*kGW!!QN7&}L?Tm5qE-Q%vrk|ljNI*b!NN^1uH{9%Z zk_n3a|AX~?x4=E~n({iBh3~_>v6@&fXoYPqN;Y}cm>-~j+zc5Tpl{My`AfmfAK(-g;rR*VBBTZ^)8E!J z#(+YPBu#5f(o+~cW8j5`zEn0Hh9epFf+Dc5U!*j*S4MV*^xo^xt*KdZd5bSC#F=9%$L1h0L+R%jGIl7TvY2Y+D5v4ou5T|d-V@WnLo zZ7Iz=1=B>5MIYJwwMn;B_e$8rCD_-%4>vEsxJFBHTUwNpe&q>*`Xn&=vpe zat;TAt_;rrB|d{znqum?mi{1Zj>s#dcJo5&=ht)6xWaxFuG49}EFz+eatbC0y&(#d zLLh`Qj{jCv7H87Z{W!XWG`=GKu@-eZ016=0cdqQ-AYGk^)j0{Bp|>>U>Urv%#dnX} z|IvVvXbB?|ti7aRUEjgbmaQ7YgcJ0bTfd+g1R6nW0_PxTb$8dCzAoeEX6JjDq(Qpf%4 zX4l_9UkC`4&{-&vQnLfSW=F<0k^XbgxXf{Q0UMi}BSPwP+6=e`v?p1|l81withSss z%xJ21yZy+?_UO@&sFsy(wqklPM0P-1NSgoTjTyGLE|QOwzKMiJ5L55fc;ehh4WXW- z{nwkY&|h1=&tSZ_PdWzg*MIo^PSps*noHcs)4K$Wy_wXxfsoYO7KgE{5$S#W`6j<0 z`S+>4l^G=)9FA%FC}4+H+-w}+k}eC{Mk>Gn#94Xz+w56xs6u@N`p9A6ewyJ!&7g9F zRX~0Thjd3A#lM2-Q0N048O+DHb#4Fn-^p0KsHD?~3CDAcq^HQIk*Ez6y3+!pi%K;j z9gIQ$kF56qr$Uea#}6`+QK3Xg>gJ*%BiTC*A$w(%LPE*jTZmLBvPbq_$qJ!VvR8Jp z9a|jdod5fy?)UrqKL4KkJon~uobeg&_v`%{xJ{EU{cQq(s3UOsq*j6=5dDB}=y1P@)tG+;IN@13xDcmjA32)AE_%_&q5%Cb3XURW(A@Zdf|WJe1Yfxkb1CvbjDU#R_C{u*5na3;gF6 z#n$Pg=Sn^4)8|XyPE_%wEwSC$N~Eh-J9tg#Rl65_DjHBXjXXH0rME>h6Usr?Vy?#= zYwhm+FYKg9tN!4}G^~+yAEy+Y4A{)hIlAs}!u_VN)zdR6KBD^o`W_3r;6)r?aB!Fg zeK-#>8v}S}S`gVAxoOSY4EbmEy+pfU%Kp>@;{@q*W-2aU=Ug zFvl|I$;@{JOPsW*{>IrolM|r-7$#WOy1w~(w~?WpKqBomvmYjX21npbf|GYZQ_V;V zDoCJ_fchkZ9p+~vyLgtyt%6+c%fKEpZ#y$$1C%5^=OL))0yyU_pttvfX|aC=Iqn#> z4#b&QMX-a3Be+y1x3Gf`==eI2Z87U~qRLF}&36*#?T)!!B5RKv^SW@8arbaQ5l}ry zy&v=|)3C<0Cn3SphMx5sw#Jt03UbXo!9}$DD^m`5Llrhsms2sXbt5M7Ym0c{5{JoA zt@B!bw>J+>@=t^e&}1XrE^%Oij^kma?Ygymhy``=U&sBy)-6o^)Hr_FLM?0y;v7ED0* z|82v-IXTtJ;}xi6$L~?i&?dy~j}8*d99ev{e_9+g$V<1G6c5!lgKiQoorF3?t*_vy zfV7Q?xPTXY#}lD|jGX3ZEzJ&c9nH9*!5fh6MYjC<$UJjs|GvNi)ajI(O-`7qzCSE^ zHi)D$;KO6$m?%cxl&^+oo)Eyg%bOX>lkH~gp@bLr{8INpL{s8b4EibEBKa2`f>tNL zZyU+@kmLcij+jC{KMt|o;uy@Ipbm8FXYO4HCQ*i{T;m3v4&x2_4Bv_haODZ`G0{7{ zWe04p=)mXr#(c0S-t)bH&}kWt_*6Wr!fC1BzYl>+9c zse|ByPJ8gZw$kPTMFa92BmK@Q@hwpp5p9*PU!@bPXQS3t=`omfhf96Nk{IJL(f5mTSa$T^l&3#*DpTzWY&zrnTZz| z3gpMlecjA@CHR>AX)}Gdx~*2ynH0O|EsR7Be>6aUJED4uH`SMUMMRE-q^xWEsIo`s z{`XT8FTTH!I3&6iza(%Rn=|b)x+Hzp@c$jvoD{tNG`N*Mbq-dykeb2;33kh3yLIp? z`G@8`9DHN3^C6@eZP_K42ZPmP^oISD*x>dv>;slkK-A_WFpG5Eb?VoF*&`Hdcn=Tl z6c85*fe6ys0%%g1wO%?*@WCrBa0yJa0-%Fygg2`J)LG33RPl$=wg6KQn1S=T(Gm>v z4+LU)4`k;A(##r|Kk9H5{pT!LwmEriytZ=%=Q=4buv&k{Lxsf-u53w_4F|&skIwzh zA^A&%X8bR-j`n0R<$IJZSubgykXZTH@$zb%aQhakmAlm*6X#1Nth>AwTnr(dq)WOZ! zl`Iym=$ahupliSir=7U3B?3*9A`}TyT}W35$0v}f8wT%6Uirjz)`L(h>R5SGRMM;W z`0Q<9wIcV7eKB=l{L*M@11`g*6HYM5gJz&%ZVoK;%hvJdxcQW>*{9wOa5w}j61=P= zA`JdwAPb2XwZPJAJ%n06st<4M*Zbf<;`mJ8sqAo@pH~k$gqh&3t9 z=*Cfv2|RUMSv6?S2Q7vBe;{0DXcGqB+HFEZonG-I`(RLPZni4`I|-gYZMxR(OLj4C ziqPLF-r93g9H>?Zq*eW1?Ez44?>S~RBnO3(1rf?0f+I12Gs_?)&>(Sx^*{i|tFy^e zY$0XT{fH2<;ScAgp&%T`8G-K}$dZH#o}U1tEIZG=TC^N!906VCS_9zJ(XfeyHX*5T z&O;rXDle4SpCnZK`LL9)<{!=mb@%M-%Ikc>Fk7Ej!An`RBQzo&aJzoTdB`j#I&W!o z7gi1$tK%*zWoPF^FlgP_Dv-#&!Jws!B^hHEX`WL=sY-rdK33kLc|XIdeRNZ+dglsH zhl~U|wqGIG;=sxQO1NlG(Xxp#USu+gsQ&S<_C<3Un2a_;A;pge4@d2bXR|@S9@1}b zKK+xru{C#z;0JqLzD=&cY!jSQ8fKwVyuAk+UXOsv7!G48(@FRa)RKX@4iHb+$ES6X z&f!0-XYhs|>zy{eW@y=`=%vD6c0bg=f>hTXKGAOQJ0}}??6-YSn=iGv; za9lzQY>g=;@_`)!JP_rscc0}mq2oi|sdv;Qn|4)oLr9H_N23y$xyxtRxlyS=_QH%c zyfVQa28I^T;+=kf7XM-+6qf!?L&&)ulKB`r1r(z^`F469h_=lc+1i&sqSPbDk&^S@|D2%A=1Ku zfuaT+J$h8W3j+lXwHLt4LHh!DLoLx;gj2L(F)>m8!nq@v>MJ8(dKlr@-t&o)_ytVH zMmUC#jBI@!EkMoleP==nuN3qCP95!qBR2}baei(N1N@PT3#J?Jj(kbLVL=vget~J@ zQ;fDr%~lZEC|pdfEHM+$4|)|Hz@(8p+Hk`1N8kFWrZ=UICHBZvZAB1!EL_c)G4SF} z-Zum4(KHRKxf}P zkmY(6)_S%#cpe9J!Ygdp0-In$66Hbe5Ynm6(;CU+%0Td><$1&Lpn!|z`seuYsP^hG zunlI?{zd7^>krpP!|Lwn6{$5SVKv&fQQ@==F53R@m%M29bTA&k)&31_Ho_AENe4hQ z>EEW(9+T*Kywkkw-o1Yj3vhk%W|v^v_zrl2Fgxh+z+d>S)|ow(8k98^jqpQ65$x6E zxzVc&{5i&?=m5OARJpJ6z1zWgrs|NVPid*{kQ@vwT%R%WP}2N3{B~5R$#etW7MKUm z-LxkC5u#CH`_rT6wK93PwCmQ{oTK#c82=^Nq(~iebIGtn=LhH@fM0ssH31Y)7SA>H zprLN>?27{=0&yBUMP}f7134J-tY^L?_HkXJ!q(*|bVUdR|I67)Nf}&i5MSCqxAE*1 zl=ASOk=i&Z`zirGQ^1KRfi%ut{Q%0y9SI^-S?I6;B%ZD^?++TtO6}$4R4rx_JJQ3B zq_aJ9ep3t9^j0{r>1J||stNLd1U~1}xs2I2un!G}tx89I+e1v}g72K(Kn(Nq?C`Us_46O#jetKJAHd6| za|nvKz@%UQQoqbD{^wPJ6+4X{281U7wU33+z3LFbC4aK->efx-#@R~dCyY+2u4_paV8sWAP`QG{jx|$6VgVihw<;Kwls23T6b1g z9u_?7>Hi<^BGL27&&zC;(<tue)$M9P_|7=V%_FB0(E)!88HELBA>$rC_C$kGd5+ict)pz*; z=WCl_0o5ma(Deat7lYM^F4EGg}@=x#FL8>qsnyk&;Ssc?g+ra&eS}BaR~bSxkFOK6aRV_M2(;@ zpF?gcV82pbwIkr-fYv`!bdtF_B?G||p0ve%OOf>*`G38%lh?};vI{@k8z1y%re*oe z8O)Qoj=YQqb+f1kj0`QLmf)>JsW3?UITCvA-erYB>p*U*2ff05!`yGcEiM9d?<}_{ zk^d;CdkxQo65v{}cvhMRdpbY-D>KtqwD0m^q&esu8ioFOcIP2j;?|euJ{i<;1$ZBw z|B_^2bhby<e! zXL4!}U$!U%Cb;FtNjsz_YgpceyT%`sadi8`)4Yd}Qg_+GLd%*CfG;*A+R)aaBprQ` zb|_UMc?@KyFrYAPbMtym(F087>pJsGp>WPbB4uw1YJ8DzbD&EoDZ4$)xnK#Ud(T3n z4OsbyU?}>5I*81dJz_8aEt@w$ow^SmawydCOpW${Ov!2h*aR0kA@v1PR=pQ=8ms3JE#X_*PPVAaP^Zv#`w= zlmK(A@e4y`lUTrHC4aUBpS*UYewE(S#rP`9w`=8rKsB4++O$t0)6d%4)`MT&OG*2z zCS&hm@*g!g*_03Ne=M3hWENj!dYn>saKI8GouseK&j~`=AjFR*4AH?P^;l;3Yox>f z+J5oV8IZreFwZSR`+*+i+j4Pg%>mpfO(#sC;(GDN_dMv4!-qzX~DV$ zOvW1;#IYZND$0DJDO}^syd7|IP1QufP-Q~a4FmJ7=H$=RvnRYwMj61n(bI7>^^lij z@dvWM={Mk6cn)Mmey|-tgAjsX*KhZLL(k&*4TN)n>oqTE=fLV1F^`=I31AhQWm0KH zNJ*S1JF30k_WmESP{99{pCwh0R^IY) zVy_%$J**?IOCWL7GcjUpxbUdwSRRQ-NXm%|eR#CA53eo70Yzk(5H@H`TJH^8pyR}U z>Lqn5_&><0c6qw*)-#IUn%DcYi3_!t$+C}Mz6ye<17vSuC}K%&WsAy(@fkH7NMI&V z>{6~i79Sf{&90}lZPx}y8L-Y>8%LC~9nR}kf9W*Af>&dwqbC?KO>cp0g>J=hjdL5Q zkBA1)pjP)8EFr;E!19@Xngt;xSZnzPI25;YxSm|VcC|0LF(Q+Yan}Mm_T~JAbNaVp zo?dzJt!}fQb9P=3w_P+~-RVOImegu$y2pFX6_elUs2DNoZvBX{M4P!-&DPvu^y;s` z9e&>v(R`&##0u+?kq-{HJOW|K=H7k09KVIUka zqvCMzihE&^!gVwi98R9JiXE}7K#*DV z2mvPG7Q21*!7#8|meOD3Lng-SmoA>8%C3P$nQyl<;! z8dVlkFEW({Vd6gF2qeOQOAG4g*XB$;HRj+k0Z@@J({pI^%Ov{qiU+LGVU)DDb&y>l zRGZe@^WaEX1ny?YyJvQjlA#i8ri-6d5YZ5Br^HnAt|J|Y9xlN5A9`$Igg<`Hh+v{S zGzYbxp zy>K&y_g_f;-jU71b0E9KiW`n~J(ZjYvvn&0!rBY``2$&Pp70g~=ibSP*w>0fO`Q<` z|FBe3e7ND%pqE}b{=ZU)2w0A@cp$={=r+TI*KD0K>tHz3Fnd?N)k?VvSmwz87%sSO zcFBF9o|~2eybEMg$AFK~2veu3H5$b-d%@k&7a_yOpj+e_^MoyPN9Bz#=Ze^f=I$2yt&+=vK%V|EfsP@0(Q=|3ViX#&td^fPA^&n z*@1*bO?y=U=4#uym#yODSB`<;1TI1m^xzB*Ef41u*x1%{j)_O8I*;beFhUhp5ODfv?!i_k%Q7VA+?ty%j1;r_a(O0)w-%M;E)i{gEFV<@=cuL z2+_FO`A~T7eC~gVzHvMf9;dKGMLgGrR~fWK$M?%9w;#A=P>R7UJIre5AXaxrQuAj7 zSi+w}Abz8`-fEwuNypBO-A~rn3PCA=ADBY@nk<;P5JUdjoReqm3-%2*O32O$i7q(2lcu=lC2a9JH6KAj7Q8( zltvyEa|to#+bjc$c2!Mw5`Tt`Z(oBi6ssZN^80O`O!hDK`y}3e^WOW>lP7a5H$s`w zvpE@v?z=yKxKk9)@Z#l-8eYU{Uwi{!qbNfQP{60B=P5;xo2x6xXBhZZ@Wv$^cg95E zXHVw5TC8xUW@u`n!YL=OB2LMlP*WfyMJl@1SN(lM75tv^m8Lr#?y~2gD1ygrl_Ny; znEN~=j@C5lrr?-8ZTxz!qNj24EThLuJOADm6gg}GU!X#d6wn~VkFZkoCnYTM#fy~e zlo79zV%MKQF+gC~k&#Vc#g$jN!1dDG74hTjn{N*aHeljbJnHc)nW5qNhTksnu5LW{ z4p>Vc_+tbC@-*1wj`iyn1BA9(N_0K|JjOM6gry%^(1$aLZbi_*U`~KPD+bKO4nfUH zWPoYG{!Za3UoeMi>iLO#R?JmxqH(p&K2 zG->#mg#uskHz{7$F5rBCybl64Q=mjdK97}}X{=|w1V{zavH_5twq+_kLkT<@!?RpvYu~{3M?=R4t8s_QDktZ(r*QvWmil3FazmSvkX6f zLwc0Ka9XAgBwX%404@|ry_A|gVFkxpdx2~JniKL7M*e1Z$?^IKFoo>2iyNYVo+3p& z^j|4|qVu{})l4DZ6TUdJ?e=Gpm8U;QnE{m{Srapd?%RO^0T*Mzs4M@v0|qNWW{*H0 zfH~AF@|;0PBsDYzW!<^#O~3Jp1?$U*f-qcu4BkFI@CyML%EqC@0`oUP%7U(!VP%OD zMJcQp)fHBAFkS&_Ros&JO`^%2gNZW&5>9YF2#Nmv9EFri;~%uN6%Y=&ywfPW(~)QD zC50smo5M=XpgE+sXMj49+K|Sq!=$HQtv$rgLsL&36_YTJMJ5dD>NUkc4(U;$dIGI% zz|Xlue+J{;zy>T8$}HRB5a)BVnKNFe=$j=k+j(T|AJvn34d5k|7-Q^F%LGIS8GYBl z9u){H^5axn5A*@!hIG%P`)(mIjtT|?r>y^jV|!|uBf@e$^eBS}h?vOngC-`U^9eS0?c!N9FS6)vVJT;agK`n~31-{@Fbss)2F2)d^CX&J>L_>v z!`fqmff<|=XQo`LISXp&)Qqzo+?t?TMXXE@Pc6-LjmA8%OtcZjc6GFhAV846nriiq~QCx6bT1(KXn6Wc_x}i){T0*7$aMFWnji zc;(91u3x|Y{S4Jp|8n2>USsC0@ZUYt1_7L(pb&*sl)tU3eerldgoBkqZscXWAylal z3mgKy5-A24#FxR;{MT&&Frz7bDCD@S009!rO!zY`h7wd0y&~wtf#eARABDJ_b~Qcq zbMA)la!|O-^Q1G4@pl3N)wFtI7yjhodeTNK`1DtY+{E zta9b{*3QMcfG!#4gHx)#XQY60X!Pv{)P!l90KGk?YZZ{OTq6j15SK9w@<)FD-K9%A9vXqWAxB18OkwiYaih zLXR4$tEU$LA&gUgLH(Ell+H?E4Fk0!r$-IOSAP>Q{;K3k6sx7a&CWbde5dAj4{0qy zK>e|(F{&SWe~4;1EOm=EmpC_DCVLj-65y~Goi3x3;OYtC#$GJT zm#*5a*XqECg1dIg5k)dKY`h279tsnZEZmA?% zyQY3ZU0SjCEfO|;_%BsAgi5%7lcT-b``Zo3!5%((PInm1fp%i9O}jOgDUr@5)8)+v z6e%aN^jc;XDB5>rd4Kjmw~oEhp$uaWqkcb{KRVt}z{L>yCF9Hum=_SC;KreLkmrRP zr61pOkEH7;Uu_%1e?jBmpRNFV;ceC0OWu1YPYX+)cm@C^ILtf+{9_Buq`0 zqiF$rZVA&HBG4Ym_n3mzw7+UL-g|a`cvF(E=?7>G|FZT{VH2`sBP%Be!3?DE%WTus zBp3W&2{%);3u&U~Y97Fq6c{~==zPFQ zK!!RELv@4++t{9-o(G`|*^B@HF~5ioAF!XkB7L7QpKBq7&TfP}5_;MrP`+XZcdfP9T?}IfXo6gOhfryQ53z>aA80b`DSUi8(++~n{Ojtx^rp-=czYp z+J>`*?CP2Aq4(G^0OI~rUyu&|L<1d&*93+m6e1z9g>S2fiTs!7iCaa9*PZT2ERc4!jAD>5G7o81mzAw)|QUP{DdkH`x;l z(c8vOM#5ELPZtV$7X64lVe7ThF1By)Br4_+3Kzt~BMCO(2#AJkFy1AL)&eUBsnM_N zwbvpqEtJ5JJ5u^@rJmWo?rW7Y#1_8q&b_sBXs<$ zo#A#d>#BxXCUVh-hOM(kQ$hoDG`2TR=*0$=#~2S7h+J8Z6>x zr9=#2id=YSfEsy?FK@yw>oOz)uDt@;P~VM$;3=p>RRZPnp8X8_d=21eo8Gu!If1)F zFM-?$A|i%jpMOJA_PKx4grg^;{EQ_?N5D{{-ZcjPziA^|XA8^+sQR>Q=k8U43E>IR zygA2rk4KL>=w*Zb7X9xZJbR>2TB--z8i_e@z9PWMl|M{fX=oBkvOnSNV7r6@OZksi zF+Q-ZLBkqwXIp_F4_Kb3hVwV3$6{^;tSM{LnHzsX9#Fvjds8a*2yRF;;PPNqR4;@) zAeND7?0{Yf1hz3!G@0W(Z3)S@u(f{KaI3(H!AXlDlU^u5HgG6Ct77?E;l^n{5Rd^r z$M=H*V$1H^IEAgJ_L%Dd5e7<7GuYgm^owmH*SLnhK)@mTKs!Bs2OX1~yxd?6*;#&n z@yrJ4061zt->lOd871q+2S7OJ35SunwKkr3N_ln0g#kUD>a*Y&tfpoJ6X1|i4k5AA zDr-ebwN4o6>QTgcO>WN~A?QPh!xsKitK-!iX+F2=Oxs^71*0>>;RWFJk>$4|IW(in ze^;)QrM1}8L2PZy#nGk4@Zy4p<|B2cQ(W|}JXzq)OSnCC{tyFMQ*+bUp9EQ@g^!My zU*{vP!-FJGFG*1j)j5t9Y`DbIJ;j4LI+MS4n&IwOCr!8$P&2}%Vr zRnaJy{0vQrN89@<(=^}_q0mQ(l?4tVov3t5Qo2IcH|yD4kQJl# z`;rdh$MtexJA@-1)V94*25FHJw0CJA+-gKfg0t%pIVElAUqy9)ox%MX=PB1W)9WC- zbo|59@xx!l5VsO$+QP!RS5%>IriqDSQ&R%M0@+QLafI_&r{tUxdp^ijkkJqc}RFUMHRB{>!|rpxPv3v=S5=55Yz1 zSQROmD}!EC}tveyS)se_lN=FF+@`0 zO5Ake_GDf1qP2J}$a|OROI;|3pC4vpwyJ1+&oUoS@>IJHM3*N?lMEk!@c2ZoE+hGh zH%sjf*y_g7W#^zN^G?x8)E3dHWkM__ABzL&PXel1!2y_dxDVbrNpa~G;4B4_U9n2i z`vuEaHXzP>#GGT{_E!`bCn$OcypXiMt9JKTd;bO_je4MUDz z8+!GnaMFI|e+g_2wfpB~)L%ZU4TPETu*fImKnGc%a-HGOJniLRTHHD&+YCOu zFnB^S!eBdW5f}>%tq|E|{Wh7rVs#Bh(t-C68>ef)iu?;HZR^>lowr_g?N`)(mqJxz1>a0{%DC5i>YSw!R}P zV@vLk*S8JRjG480h7DGq5l=OKG8>ZL>wcxaa~NOh>>#sC@TRj8YRT7^h!7{n_E=mV z2A+fKm=@TNWI(elEggmptw6^1H(ug@a^UCI+>z%%j4?j7y)lb(M{`loM6DbfO+Ii;H$no&=-`D*W!nbgBXFZ~ z^}Pf&%bvx5r7;}YA+~zitk1r8q3}U+XPt$fY?E!63poGYKN;m*Aldsf1Ad_T=H>D7ko#qTjH#a@fkxpgoZ%(aE(JpkCQI-3wSCn*(-&NxGg+DDRW&g zwX_3ES#(H%$%q_y3{v%t!#M*E-LoZMy0;b}s&o7vKL8{kx4S%;VS74i9=USCO$nx` zU@_7Fw9`{G0&pKA4C(7u2bJE8=nrgJ3U{YNB|sDQM7$T=P5!VP(ba0VjC^V%rE|WQ z#9w0(_`FuTK)n0b4iCnMt& zhwb@Uf?wp(Z%*S1MzrN%{b|bYfQ#NjIhm$O8u!Ihq*|cRgwCiL_E$edTqHKEb%aOB zUlE@A{JPU4jM#GpdXse15ip2FbyRT0&_f7;gTY_@|=l$sca`74Kcx*2&P+&-%a;h*jUw9 z5c@fQY>C4r{Mwgpn&nz>S>!up!G;Amjf4Ur9EF4sY#q(ozjb=!#NfNYK52ff#}hp} z**SQ2&YMp9qhxn^!$6g0>ua`U!={^zj714h%OY-~BblOq6PqS*n1F@Q@-Il!UE^CBgs(W^{Qd!rGjX zZZ(1k`8@QS6bF&z%DQI`@%HKC`vCwHptH=nJ@&`PN=BVgp3?!F_$_0SIX=pE!wgPP zmhP(fugr(mEgr>Vxosq8=7=Vip!*t{~Hi1C4|>DkPQ zp)tH^x+<}-YQ!yiO!?)=Wpzf&k9}~@EoE7-+g2cZG(?y;#boBCziV&n$i}pTCbn~W zlilJ)bb?P%qBGr|Y43C$lx)s!*hl39)(PfPa~D>&eyYc$UmcAZyuE5Q4pO`BlEtrI zzI}kuNM+uCDNAbBbU=hcTR03OPpQN$pl2ZK*G5&TAk-n#oQ;NzlY{!Xz(cR49N|<* za`Mor*iNo~M5$idSyil1(Kc0L+{bco3V#Vw)%HeJC@6<)X&$iFiXq9N@_m<6>yA=q$ zU4NV&QC2-NN%%WL!q#YB23NdAW`uU2Bs^5>1$nJI{$=)fcBU&Y82|K`hWdKt=_P z9YoOo@Wv#2ZHSk^h1`I-4EEzPb_P+=37j;Sv2ccF&4m-Rn1CFMaLX}>Mv9hde&6|i zFdDq^w3)|9NXgP6$V&bdq-vfp+(fio;Kh;3X%>H8Oa>WRGa7PJsk;!3kmEE)8$wna zvVVZ5mFX*KfVNUh&ms|LS4fcL6ad+Q8z8TBeE~+!eavZXiDT^Br~c`H15tw+4k`$0=I*Hr+JU- zmt|T-pQA91A$bhR5Kmsx7Vs4`S$@VtM-S(DLre6ZbYd{`$Z>E;hemtwth2{)1M4vXIqMQc)&3I4TdpsnGyNP60r=dls8O^4^0R z!dK=rn2p;&gb-j}n+*(~blgp1QdI@yw8?=0ha`gb*HsKk${ZKd(shq3G?T`>xioC? zNyHQ$IiL&KBV%77G>)4un$9tqpjr@&;6p`<|7;DupH}>^@%#lj;wIlekS`!y_s7R& zpQgzM!H7sH>hbvExkSg3JyxE@_jlhr!q)Q6@ZV8>b63sC{8U)}COx-1tg_Cl75XF@ z94hdtz77nGydCR$QpGa9jzrgfobS5sQtab~LRW-IyKXZukt$LC$SG29s7$C|WiaJ0 zS=g@WiGZC%z!oAT)0m??yo0_ly@*h*zYkP12eO}#nYsThC?K3ny|aqtj+?{_PiGX? zM%#g~;B__)FRt4E!kak21hZ9&H!=&fYw}Pd5be<$(&|pt?K;oYKq3%F z?eZx*f3s|&rq>gM9yZV=cd9ya>e$(VbzNwRMP^dRdI6`DIe4o$RhbPfxm{; zaFNuM{iR0pSpD_zMU->*qa9+#g7VG-U#srdgSV`zR!eU1hLd<4*W>7D^?nax7o*?5 z0jO8t%N==GuC)`Z=;5IS%o?&8w&f|w8I^LZkX&y#sGEfAD1#{;E)D3hEKACCzXhw? z+g;*bO+Zm)sx^jB&m%jEE5=Z*Y+0qFs9a8+C8(l!A=@T-h4*U(7*Q?>YU2W z_XII+(+HU@_}}?=6}-z=UV8{=liA+1f&0tfZkHtqOe7~xQRYrsix@`YxsDfXe`C1O z9w&A>lbRvrL~MuE96>tfTtdqyKXHDWIm4Wzpl0nzdRU7W(!alT71EQkbK_`>H7sDU zalc6bUYRDQ_`FsS_}W8m?-jP3foMQ6ARydt4K_xn3=nG6MI|APlV#yqz}MZzBOv*R zlDKzMtxf816y@7*uhQK(VDCZ4OGv!%LtQ(EAW!lTw9vs0PutgO8FhYn>EI4{re6`hg( zmS}{&C43Xyu*eyTs1%KxmA%WOZW4cT5^@m7*MVq;h-VqoC{tmkyj3m<@U;Vt2V8e~ zU$=!_xfV0>&jDx4fM<6|_)>k1HplZNUd1LP8@Q+oO&z8%%72&N zx@@l~sHpz^N4GaB{y{Vh2A6#F>EMPV>QGDva&d)fm5$W}M*t+jy`6TW3=E8V-Y%Po z4zBOXISOl^lWUv%cz6NjkUAzgmnW;vhne~mbzp8WqTO`1KxH-w+GJ$v@RpVJWYr>n zX9ieQ$h)Q=%-JK6Non(AUthgH)Cc(h)sjM5*s|7I%CtbsdKrFSJo{K34TR5hc(B_- z5r@!LNI#~X^}H$~(=oFZtp+KlAZAjc)NakEOYNh}q1gXAqhziD9X&T{X8+nGCu z7mi-?%I8g6Te~MzNxiQ915jHks)vhk)+H9YqPw+4wQi>h4m*=po)NLbA4i`6K34?T z0FH4Vy@Pqr-@rCL+Ak0mMBVt(;~kIRF=))ywbBj#B++ReJg*N{_(^Wy$rH3LQseL= zWSLz(?cWfy9YWm5_H9j?a&;I2At^xWNq!C-m_d0rHr@I%D8yAsZXEylrHy~5nD!`DKnaHwz~c`ec8}} z;-9ciB!*67)wwRGSS%g?c>_A=EDe5~#E3%T zoUkMpkGyG0IcZHYmsc{iLQrh$BbN!*O7p_1Jt2l z5GzK2#32pb2<)OkrrZoW}b*_#`!YGhv>a=Eit{2;=M-npf#_WtZVd3FTB{RV1oyCRV*3qur$U;CD~;aFtbt-mCv1Y*!XvX} z=)b4ls`!X?I(BOcOv-mcnaM6TpHNzMqcJn{K47#sBh~lhfYH5&EAEtF84v%!YKNXF z&96Dk@X$x);P_l1IQsyRksn`arwJO{^UGPy4_*$~Ii#To^xRx#m9~+%$hl7=5SPSo zL)xME8v{GfdEWh%lCS#MZ9-D0hNw@N3`+pW#iEH6#$YGe2xJkka`hgTV#FXw(_eTD zb%K^^M!gwe{2<2n00mv!)es@Z9`R??)?l8MRFY~WsF}$`-43EfkkVm1`)y##dyK#T zWOwOcn$1=q9qUq-9c#YG#x0|iwDz@*&KMGIeXW5f6rjC#Jn$9(wcq|%0C7OD>a05m z=uq)0H53kGC7OAYbeN2SeqY66a!-0jbciL&&+6X`Qg~}h@9%GpB4)wZUoXZx$zX5z>z81rgfo?K3|97I ztyfA}-dB4Z(|~j*O^)%UjnQ1s{TNc--1`9o>pa;xoEsq30ynw(yCwL-YQt%0#2llggL^81yEuVc)R|t{MSWCZ+IO8r%>v;2$5eC zP$ge-c7k(O=fcJ+NQFO@LqkrHG_uBITLsPFOa*QC42*!i+>YDsG@=#FYtgOw3*0da%oE|UgE~G8Ran4ClPWGJ^_vvJ~)nUf;&S zxUvBUfshYT4<-LH_1rZ@Q4biG`6JM48O-yU%Kz=wi4ia>+&J@8jZ#RV#ID*FHV~j? zD5DNpw!@jc0_ln{N=!BwuYr#cQLBADmjHwg=)!f<53Ujtfr7B~j;9_Z5i=#mJwq-3 zh?Y(l2wU)3Hf1KoC<;OGOEvfTVn~Ya@MG#!wIdJVCi(MmMvrgL)#84JV(lt3E`I<0 zVqS2?mVx4z4|xe;T9=owY0l<77SlE;y|}hZsEZ-6K4;*vza%*)uK4iwbj90cl_^WZ z_=^-6{`>kofrA5!$ur$Q+$q3HSharr^>&xQ;%CF_-<~XmJRkY8uN!=g7AeEHdIN!QlzlXc>^&c0cy^NOs&-n((piefgmpW~3? z)tD0b5}xgsydI5nh&zFWcz4B4tGePA{}4WBskIh+N_X-Jgv(TJ17Hc4$9<`CbuKE_ zEKK17zGH|0q6^4RZtv0c9>KdA@7RGyH`i*cBA9CcJ;4VsvG)5z<=aN%ZaGKq_Ax+m zm*025YRT9=CoXBMiQg<6?NW7sOmbK(s^rdP9Q#_gK0vKmmDmL?wzCf+k1sP{0eEEp zXevan2*>mRtfPGq-KrM#x2-~hDWZs%dP9Eot|ipr+`KpJR9HXPT4FG&+%ewpMUPwO z<|au@28gl#KNWI}qiWA@?b@y0d$g&Xc1y%?O=?s#Ve`jD0n_02+d+d5WXacSraAcZ zhMrnrACJ$*I8DBP5s&e7<{;t*Sa&w=#qU&TJgw3C+??@*m-DI0wU3#RT#?d~R}Bs9 zXje0C$X@+y9z1`UGE@=lIEYU*wZPmZv|&y_K)?<>VBQZm)Qe$EO@^rg2cLlK?ZdwJ z_?#Hl0qp07xshZCa7;+C6C<1JjSMhFO7g`=ibA3in~xr*e?ZZJtiNX?&auAu=MB&R z!vS2(aKRWpuUQUDW)atgRZ*gu&U&La0U>*fn+At|8BD9lp&YOLlJ;JQ7jF-xr-k zTiR;|ceHN7Y#Mkdmw_L)UIa8^p~|h){VzNS-(p1-MVIAk27SNevw6|C&Hq!Z4}&U% zk=00U7?bF8A+a2U5=PQhQZwll>WMICf~LbQ?&K8EGN>wily>q#aH=CJ2^rfdBqr)( zw`_4<03+HxQ?zmooGmQlk>rA|4`Q1RxB!R;QpdqGfKQ2hPC z*I%oiW%EnlzD`8Faqf>(JQwn<+#Bu#uj&g8OQ&V))GayktI z1))1sz6a_&e;J3ER@{p5OFwWyZdfSA+j+Bn^hyM_&b;dGn1NKdGW!=DgQ^o`F-8|C zUNpBfn(g1pq9t_Re}4???ygCl%@EM=&Yn&5Q7-A!5RJE6q&e>ELJUmbT`IFz2S%A*NSIl8u2eR9yryjjKLFFrS{vo?QfRbaE z8fTDkgeCchr#B9E9momYf+w7jlbgd^oP!eQc-jonWL#Y?r@s~R77n$P@Wx=ECK4{Ba zN|hFHt>8mkfz(^(O?-l_MEE;qgEx=)0-m3s5_Onh4yo|8(3KQyJ`$8A`0G_d!H7Wh zY|TZ|&ZUsm3yoV(^m9`6w)|h31f(beaPZ*_M@_okxP2QXtKI7FLj`B7g?jrYQY(nL zxSgIHn;7@cyS|9EP23|>WA(O zA1x~@9~-_%NORkGGjIFOKmgx;%wDK*PRYy9XX&=qeKCyGhOOAQGS=FW(fpK@iW;8J zZFVUshHGo(MYY;a256D1k0I)-d~*U>Ld-o6r+IbgK5488xrgJVa}?Zjnd)jC{>ivL z6L(E_C$(`P;A^7R)pnN))Enu|8Fzn_?%WU(vNF~d>=nVBYact~VqwI~e}>}Il#j#1 zSGQ!0*nD+@dZuI@!1YYW+##^{Nw7r{KeZ2zMO#Nlr|&@SJh*Tfglk7R%!wa}xs`hC zgu7Q`Td?Z&)!zcQ4(_YodGAncr_dg;TIacXx{mC4q&9Cps0#0<|8yGt{o<}gD!dAN zcViy$-a%DL2MF*^%h1tVS(L5|e(#A-yKI-%HQO}g<=MgY>hSH*MxZk_-^R_5t5rPk zENE|-V<@(l474P_bk|tB`(Bg+>)e7xZb4LncsyHSpJsCMwz{VP4vlD_^**p{xI&vvONsD974wKJDzRrDzM zEMfXrS+(8XG@Tf%d(YSsalC1vE#z6O6g%W$KjVMI$TEL_{IR1mt;?@(d}^A0Y*Hun z?IrRsh6eW!+`|6@?_+48X%)i|4=ZzEN#8%Rh?2_*?-z*p`&^~ROy2bt7gM5EpVb;h6M|Z8T zh5+dmciVYGVc8tgmGNS1;glb3)dT9D{r-fjS5*2WXV-d0^STz=o_O6alWVR`pgKym z9dl$n)NA)^swC5t`R^{lh>5pOQkl9z3`|p1K0kiECR9jvS}Ar+FvWU}5~s_ig^6r- z85vjCoIij0{6Q7#uI5^cUsIJBv@XbM@Lw<-CKR5aiq+8*sJie}??@QIuT~CPOf2 zFnCo{@U-nqr)2V2+0ov~W&>;^{03dVBUHZL`fg86jgd|$m^27#PnHXWoes{~hf1-u zQ9X1*WugzqEadCx#txIWr8@oM^0*$>vK)_VpNOk*T#)Rx_r0tU%7E?*?k}nz&l6P> zvUot9vh~lmBeQ)wLgHfSEh|OZeCMh&&x>#!!kqYQ73Dj3^xj<)&++uuZ?VCj2K1DJ zBMvyM?YvPpxj~a3x&;PfPk)^b?4B}ps40A3fn>xTGU@pi`A!Y5#0t+9)yrE>skph{!+H|?Y>2st^?)Ob>rs8&*A7^JjzH-{uW*g%tL(s#jhD#BlUF! z+vF$nv}bF~i*z41Dt$DNetDzz)k{IP7xw1`+*NBg8(iV;mKn)?mzgD&Z}B1_VuTUVB#~TUHrc|Rx zuk|j)l9VgwXgM9r3%|QNo)sk2rw*#H?nKM@T~lrNl#ZP3tF4+ydovr07j{!&y5%~@ z`DN#WI}OL7r;X;LE;VvZgyHm8f>`RN=F{9e7&yTkM&EjQY#&o->F4Khp~;vw4uoG0 z{~7jcm-llUslL7E_m@uf74-h-)%SSU_4WpHLSa&V z;P6pCh~PY1C%e`}fqwbUS9#siU5Yni4{7eTchB5fJSoWgwvR|x`S?Rr|B6;omKSBP zS4kOP9pT8wwRdYrE!n$lYj=*L1qW}-ITq*Eg_DCeW3hq-KYEiu=^g&fB@qb4N6_os7){xV{F)Mc=zU%;89iks?g&Yn&50 zl^*d#_R;fuQ<+e>6+V`w5PM_vySAF7ZnThIQMcJUYJiykO8d4qX6kz1@Le;^J>t?$SR!oQYW?fGZPnV|$GMYBg}0<~^%k`s zZ%25hy#Km|J-6YU__2S&uCE}m3AXCX%l#j3X`E(bRjppuvHbnz(%R!jvx!~!zKL6L>T#ZH-;LNOp;ECd|HKg*U7%o)!)213@g?{8*Uw3?PXc}J<=qw(AwEW5qc+_R(~yBx&0EC$k_Hno#Fu{a+K{?GAx-Z?haDi;f_ zAFn>WC4Gfv(rP2G>GL<8f-0lzVw=ZI`?jai$~&wS-tg11;w_V0rvDo0EpGnMmK{hwXyfMVer(!)izxNg|N`FcB9M4tD@2MhqtaahZAoF2r^6-#cF{Nb$D2TrCgf zF?p?sN&J#5P_y2lE2R}KaVwEQ!ATew_Ux@>G&BC>hTXzTQ@)8m$(=R)bhnI)f-1fh zDBQ#68WwO)LpyPR|F{kddrcn^wX5uf>nW0?_tt;yK|#B!NSk2Lms{y}4c%F}s@Ad9 zf}xutssnl-g+=(9P23ZVO0433XgQntZaI)J*|scK40-62WE`gES#<_mUYB7dmeqEs z{3}S@=7q1;uS0Gc-c&d>;JO>-R&y52P4N-4(S!3y?=#upVQ|}#8EQ>^yi{Rq9l<4* z>k^hX>uomczURq0+jxj{rq4H*I@mFa=VcB4c!HHJM`|}WXY?@1t1$iT?&)Q@9}@K5 zqyGAZ9<8>+-eP>iT%_GH51pTf?oXmtpknIg$P0}`Ni13T&+BqgwY0WUsG$J5{eOfn+k;xSjyhZy$PYY^DH|9Iwfbi z^3~w%fYdO(??)nc(jVg9dEg!A8)Q@43mSD+_D>O$hu{mC(EK*=fkvG%>8O?Uu5~KS zPrjR_c54FqFEIZ%kU0=+yo{|SW4IlfRHv0a>wddD=XCf`x?2N5gde)}oGs(C`yCKN zKsz5Y`$Jd{#bmG)L)#Zr=Ei6HD%~V>7XLdQ$Ov?W9i2#IWp>$(k>r#c|H0q8ri72Q~nQxI=KQqd}cY(9=^)1Tz zXN(-%LPGm(r`CMmxfXYw$c4o(U*F~X&aALSw0HMr#wIX=KZQ~)^om7-|sju zQs~uCTc`GJ+!cF^kozW-r=Qf-Umcbol0?U#I9(6?83NW%`}^Lz5#3-Co=DS__zAg& zQ%lq3&#~&W*hkt43<{!q=Ryf*PsK+y^87fje>mZuODy4jT#;fw!mUKF!|;cz?%=Oi z;${QNA;O7`mPOujLN!rZ%M^@l*XI@BAiB#C7Pk{G!=JCl*&g{*8;e11DlB* zDG{kCK4H$j!CJfx63IFncZ0ulZ=l4Ct|vLk{ng9KY_@+%yP3(>xpX#1qs}8rmF8G# z9s1(NWt)e?4r{&~%2pEA?DP*(bd3c?P0BRw*12n6n9F>0g_By%4_aYspXe%rJ=v3_ z5>z5_Hnu|gZTu@f=#*-|Sajq@)R^~T+eQ5N-pui$tvv2u527*ou5ZOU;iI|oz@Adu zUY)e3C4GX9M(WJTox`n(EtS=`geBijbnf7dRD0~)k6?O|`!j55*h#k@?Tm-A=bRT$U23f)Y#T3Dt+qSObIbR)b5>OcfC&HQb9F946A0lDbt?4u ziK_!3D?Ei%>(-9m<$$3=-30&jI7CV?CqCEtER~Dl_V=eM>pO|p|61aWpsbM|&a^nq z{;8Lp5?M<2E~PUyAzKC89^C#P+6B9-6S*%B_ufVtB}#}yXw32+Wd$p{&(7#>!4p?Z z*^CtQ%PZ@)?(Duv9bIIrwpFiOJEA*W!`lSF#Oi3ZKJzBqb33E@91pfwKOH&FeUBJi4AL*L!fEf8GkW*|T37%6oxEmm=#J(1*Zr6H%h`(F^t8(C9r< zWHk^yy7Kk}8>cSud|HUqR!>iFaP$0RpRxMS9|cWW(`MSlTWCE}J2v*Nf#JCm zhGywSwo?k9HK(ToO5T)*3G7g|15f}QSXk_gxU(^yt<)e15K~C;>)`;@x-5@y) z2Z9Aq5wWJi1_53hvw9(5M};Z8df^mC1OPE!wf5q&21ayQrRc2{;g0X}53Wl2nnCubvFS{pyVq zA+W^Os(s9!ibb6F)jl;94wfv*rmQ^5et1Gr?hFp{uX{P!CaMnkosS_vF(dGBapN1U zYINLPJ`UN0jyOS@dRxQ`Qk>xlD^!x?~M5nEY38ywKTT@V%YPZ!I|vBV)T^s_Zd;_0A;4mmjFdA9C-j>i9__e#*R~MuniO{ zO46tbDI`*TQu-pGd~vq(y=8u8gW=Hu@jKlle-L3y&o=8M%CJTb5DA1{L^y6*3%i}; z1FIrCyhG;Y{R0Q7xUt>v#64SETWGh9zKqFk;5Zm0^46!@xh0+zGhUaWo&(tdPb0mr z1BdR{mig|@pZd0K@9V1=tFRwgW1~?=IpFHg&D0|dP=%A)I;Gx6LT?*287E?o!g~te z-?6~>wJpj{lMuA=7P%C5#evDKs7WV8XNW|n`kl*g7H``&w)=MlxtdKTWVh;HIZm+Y z9~Z1$JRYv1=h&_F&TI%_YLdhU#>$TfL;%A%l4RC(S=nUt!CYI4;fZ3d)1Y51?LP`fmYehlukYd+Iyi+CthV@GHEPB!3iZ2Ld6xb(YkJhvOx-^*9#42ZAcJ1~%-{CuXvR z@AD0QIKE(sjq z9=y+^)(_oxwIZtdXnq>ox`{}xqSV;vesPq4M0eM^7`wl>u7e~(H$FAQw47k#;VTxq z*1<=*wXWA<4e+Ccs)PkrGVT{o#>7Q_l^k>@L49wZKgcEIJl%R$bRf(va$}dOgU%-W z^R;Ef%!F<+{)PDYz82#dX1V%ME^^VN$aO7BQd;Nj7n!-Mi&2u@)3fWPYm)GzYQZ>@ zdiJq;OlXRAY+UlE&h3=xicRjGmEGF+i@f0>n0m311nnfvpZqt4e&sKbRLk6v5TfZj zN53unNvdsQX}=%^zxj9&S&%|b5vI0rJ*PrWs7_a1pIeNpM-hYE?A`e>W90QAk>h6P zLxoJ9gqhTnGnN@M+z(x*?c>Z*>15GY<~sfh@oG$wyMi1G*3+%qh;$rzP6btU5xX68 zzXcqElk&FPGK_ro6xe%_++>RYeSdUuWga-d{io+yVJ{^bMKj#e(jqE3Z!D}J&mqcn zb?q%Fj)isxknPLcJ|Qf+4N$|(qeWfY7PQQFxWP5F%)A>jEoefe!|#U%mJameCwd-^ znVw}!74-suv#cSo;nVgUU^D>S5r~}3gKnKYdf%s-v8W3uG6L-cI)T7Y4K}F7Z2An; zNLDh3#H-e8`)kJFVoZ0rVX#uba&Zm69zj9eMq%@GLH4)ePagXrA0kh%o~f#SN-sb9 zcIw=YbOvd`X~M&?pMAuz8Vk1{e}9xpeKJ`tfAnEWwfG5B@21;4rbd$kL^PTGhjFtb zhIx-jxsL`UBIP3E40vY_yvD0$i}5)d56thTr$#IbystA4_B$Sqpw8KVp#=zyCxVQMYYZKYzg8@RP*;$IH~e zjqkhy<|&BliX)r%iQmU92qTZOErLQ(;=wFjBNLv3o&HC&vE1yurpD=8bvO4_c&%vt z&=((jDOto)1RS6K)zkJV45oqterj=1_TgT-%Hg~4BA*0Np6jdscoc9}t4om{mxMWB zISsQkeos9pET+=dxIPrlt`po>H6bbG7kLLCf@>`r1h)g_BtZuNG)uAqTgox2kKdqn zznKm6huPw-0}c~lHEB=6m76@%457KCxmRR3SXW>|mSsb>%NIQmkpPZOPs~$wFJ7qx zbOQnMTWBCEkv1@Uc?-P#de*v}U9{iv;c;6ilG_FHJwcUCI;e6k-nqpXr5T7a3jb$wwooy7&dtx0}A*bx`(Gjhj{Z~(G+PBkS;-Q$g<)&^g zDMDJU;-H2{LaiDp0s}@O#M8bsU74U@RKWQ`NAH_RRI;LscHzRikAO;#Hab4-d6ByEqtovIoTmoP2;I8X?!ct?NYbd`#krvbP{utt0O&0N0hOgb z4LYM*zq8lnoFL{PYZ3x_2U|| zQvy#v@d4{NGrLosxBD-5FSh^2z=lO!%~H5KukYO=n;&! zxDq(>%H0zsVJr~_8q7fWVuBGyU6${1NA*Z8!{I%p>t@9xZ7*!*qEj~*tuUj`l@6?F zk>rYf_}|>`LEwYapRXrJM!t^J;OZV^=zEi^v)Q@Ng@&JjM68IF&2C5236}dqC7V&~mJqlQkaaqF_)cEY?rePYXb$|1w`mlu zF@PiAi~FDCVa=+~yvrOvPZ=@3(FxXX(Chd(^fu$<#Fz zO!VgJCq`B6-K64%pICwG&Fg_~-zBkRa3o1A>l z2YIgVAkEE>Dhu7{sYezI19OE=%y?40ev-y=@`n-wi#}i zDdxu3l)C5x0Z2zpPvJ}-5JX3VtOFq9YGZrNc)g>5jv8yM9kyc>nnBE_9iQ(|z9um< z|2N8y*#rv?uz#(d$Sr{`cp%Mb z&fJeY7tbd=u}j`x1#6DUAi1q*R9=9d?f%46-ND%UMA>Deh*-Ye^#swv%UvoqyUm3g zZ%tQ$bysZ{W$86zS3RFHXi$0MIIEaL7r~T^u@W0y9y&HrF}{Xm2-NXBbF?V2()iGq z?pua5!k7iBOen}`z*f1tB9BzNh0apY+}e{*-6d`l1atqmAZleEvt}?KjzNLJnB=0@ zk=W~dcWS8>;uP6RI*O^(saA@J^D6S8U8^pGWp9f@Ve$*4OgXiLBZd$nMx{mHX$OL5O6Wn+B>x!TNn4Uh#Dj${k9AVd9s$}_=&E<|x<;JJ zZW1m&up%$R^O%?ko}R=NzKwtM>th*bss?*XD3%qUAce+4_VPHRS`3>(66t4x)m~z@ z5mD7b>K_hTlpzEi%w3kspJF6tl{Zxc9IAv9Dcr_ijRD*Y1U~fQGoK#bzJ&$La^DGro(z4Ful96WuxlJW8~&!=m_zgl#c~nBR#Y$p2Gu%fr6oQ@aDsBL}1hQhhIa__@XZ_ zVQW^ux;OVW-pV$IW;1*dQ9Cb^T{Va$3hx>q>5G`XYb()Yb3YQ0>ROH2u18 z;@)VmL>5MsEIv81zFY)6vOlKUL;s;aO5%7amtWy|iv8*_2%_rtockh)70-<$@(AVt zFBW|$eOdV~(%Ldsw8pFaehq513Zzo;`J&u|1iQ0!%yf~BmA8G$5m&&yKMdzHd3{y< zsB-05(Zb16&nG3J-)y|Cp6iGBzH+;wU0vKcK|Onvnp8}5{(bFhm6B-fA@k{O-b#S( z`}r$tzFMEran<9FX3R}PDrYIxV;&QDCvThIz;NB69cI+MVuq!aHJpj~j2%k_2n7A| zYd3BN#M1e}pX*>8WD)18(;-e)hKVtm#pbNW=Yv4I25cW^rK!07m~XGwF9{u=q7CVi zY0$kOGaW7o1DhNCFa+7J0+?oGV!vK86BV)=Kc*yPuVi;~1q)}Z>tDa;w+7?s#=MJNv5!@73Jb46o}2^(PyNmTSA^>4ej1Phrsv7OR-O`{9vs>`DI% zft$x4f{e~6p9q`-Mh`^o!TBYMFEf9xVO4&|>z#*UmP6UJ%rypciAg(R6+(T9m5sa& zDyD)9mN3zvhP;|v1tlrIBZvEgry!DAu6pbFV0?Wdt5mFC@8N75%-j33c*lLQ8JC)w z^<#KAL&%DmLVwy^)YbGHl_s{IvY}pNyYT=~*1G?2ue!>6s2&UNrtNc9?`!KbEo;rW zxi-#vVl4*6TuIw@hMLj6S2VYyG;Xk*9o@i44L1Ve7xUBm!$tVw@2%=(C4S{HW&gEm z{M~cBCn&5_@4fO0Sc+(A?V_N{z1gSD{qSPeG>w#b+mBHCPXf|^a&noErG^3Rz>7sT z3f~oLv>d=08tN(t{V$SO(0DLO=Y~4^h8I)03EzAxeJ}&U3H#;c#Zr5fC$PD*Jw#HT zRh<46$i&waIPkfqs*0r$4TTR{b4b0x#7s3% zh2?Q4Ja^Yd)dJQpJl*;tVPHEC(|7(~r#I%9Mk%XspsQY+Yxv>WB>hWhaa_?>?WF*Y zyaBK9xGxa_azFjqMUkV)tWE^mT_q+vY*^!G+T4R{>w9y^I$tD0w*m`ZAA%;#h2ruY zHPp~#ef1pDqvo^>@vZ)3zJHP=UWP(WgjUgLEdKZ1$u00&`1z|)@T)^Z)mPKG+!+G8 zR`eO8Ri`$Cl{Qp=1X9~~Y6iZ~s6yY@mSkJdS{k%9Yb+2-0_v;=K10>Jn1{T4l!Y5( z^NaH4mTNz!az%Z%c;+!@QD#rPVe- z-t5>)^h94Vwzq5P)e3WCBeMW;L&XnjE`=Y&I!P-#`qOrK4 zquM0(TF|TsuBwMFJ>ywl)vx3Umw7G&_QLKLAeQqv`TVp1jF1~pGW>x=o|k>&K`K+g z>ziU4H%HO8SJiAZBwT)s8T~im~=2hD!1;w?;As%OOx^yil<>eRdSLS`24(T^o>${Crre6UqK#txm3E^$!aR zr#iuOcfsE?2V5URD`#P4Yui?h*%5P_Zg6k9wKZ3jqZ&e-ed=|c)&o(D6j$xH1==oX znXCk+O32^Y>`I6ksQ>o$R6HEki(p4^32&AI_CbXbU zn!`c`M3O>aJ6}#{#QRLTK1oS0?+?~H_oOCM_B-oWxcZXES_5zDr>^-DBnUna0Gw~`emN5xsac035yf!6BNx?t~&Sh zr%L;V9iX4jy4fMuSE471HR+NX>)S{oUe5qLK~M4%ydSGZA#gmTUvuOv-y{pQgn(71 z@vP|a!QmF^ZXvI^v(xKIe({m~+ z?~O{o;d+P>3o&ZY#k%tu#7i4P$44b8^FwFNqbJ?c4JRuYO5Wm(!P*@I}IX^#C3iO;!U8;U3 z^UR)9@*R`6y(EI3$qJK;v>t(LC^T`{#$mz4kC)7wTG2EXWi^|p^cFNQrSb|H0v+t> z=WyOqPLdIIDBIlJDOflQdzeAq=b0zU%{BVSO>-L9@O_fr*7a?s&+PE^UTy#n$1!pa z6XMBgvFr?H_}5tJ1(V+c_#8s@$_pHfjVGFc&yZVP!KKo&#*5hwk)EvDKOf1J%^-%G z4@{+Luu5i_%ol=hqx1K$Q2?3bF>|VP8hqAgubM{G;b%uL(Agi>tpehtHVaHU(1cvU>I?QweME()_zAb zm*Wv-bR?@q~MsVa<=?zXk-; z?1m`qK+kWlOYh8-D_xOs8vtBwNUQaQ zC?t>@%mB0`M->1lyXFBZ0M~Jl$E52)E5Qix7?-oQUP0WAV&Qv>;l~c6Yd6n65Sl5_ zuv*vt{&pi^a+dJqv~#?$#>j%wi-!ewHg0qgKuOUXh0g(o?r{G;Gq=d^gWqJzBlzuJ z5GH?u61TM;5n=4v7(KW6Jt+Ek$i$Oi=*)e})rKw9U?_fid;fiok%QSaVs(Dw6>$-T z5#h|E`LSr~FdBw%x45tYF-G@&#=Ena^WN2BVrlGDFfpD!8xt`kGtfn7U0nFMGS_x5BX`6; zrq#gV@b3i$pwDb-+!n?Lu$iTrCQ*WLuUsJ!=y{ms4__yV{kTaMYX^3%A#k2ZpE{1J}-p6{)@2g$^G0nZ1` zPeu~23L@`qGX)=^Z)0IyUP4nwsmsbiBtx?+ySAm1tHXBye5S(ME&%s$!W{z&C9J2o z#TvG{ZhW^PR&HF8>6;~yj>#+K*|ExRexYZfC#IXJ!K-JBfF7`;hr~^0@c>2g5sm(? z9T%uhqM@M{7g+yvlFq*J;3}{t`bK>*W7j1Pcwv8Ri27W`bjs9~Vzy!Tdva>Hr z(h;`oK_Qzc(D=5G!n{rA;`d|zVR~tYzl8T9ejohQ+n|Hq30M8eR?g+H5x*LfBsC5p zo#2}zcUry+mVg0(rv*Tw#N2Ivy&5pswrVz_%QEo5qg`^gF*VZ*{+y&{G?ikkZmh3jiXc5@4XOM0A@aSwZognJbZ z-_&2jEz>RL;!k=SH1YL1X>vTl1Q?dvp{;5s!WAvDYHPWVqZ`-3J{Yv6eJ00Ra zU#CKl6MBSB{FS@i3kh{~l5dm6ea+|N7T;|;KD$g}p2Wu_(xc5tiRcGop1i~6 zC(JCR#Bee~Qy?w^soTEHu3zw=9e%#i(5}{eWcO1yq!ih192J|V|3m%GO`lGb$MCFP zsR7st!`^vU3UvOc_B%+?H(6@ZdW`x)1n+3n2n8%NP6gmsFQ;wFBHyV#*P9XgLK1fr zxwNllz&Wa;nS22iB7bbpjeM7F=p*fnqN(@#I^rT^#$K>q_4VQ=_|N8Gu!+$+M$^16 z0#JTdz)5Twh+)G>`9!a&RLC%Qbw!>xWY3WBWrz8;-(p0YkrCOQ`-VWSmq+P(d7BYJ z;tQf+gq5Ul-E-wm0~gn1D*C(3u^u#Q)9{2&^=)7>7}dJq=;p7)TUSi1pZ7u&K`~kn zbo%+h`IGq?M$cU`Wy;mB)Em*CwG!1^b}zns73-ZMgccY-s$+Rl{l zR@@l6Sqj?bfN2U#%w)Jx$I{t_#Km+7Y5RvhU%txRd14`zo5;IlF9CHtBO_Z=39Jaq zFIdMdT|!AOqa?9hdnT71UJ2Ec#_FeqJm&yUfBE|X4%dv;^wM(f!RA*G5!3JHP;@_`|t^e+`DFd~ll-j={X9{;? zdgZxoWeU$ASO+W2@A@z3W4+*cMAzYMLrNB9VN~vZi3PQ#pe1i(VqT6xg?m*-ONM1UxbQ|jE%?Cg`==6B~8+3~R^~Ij9y(yI6 zQ%vzrqa>7C6)ZT6`Y^ImVU@3fWK05FA1#3xk_D*(Ws&%o;{=Z1fi}Ix+1S zDPV`q=XNMbg}}(4Lzbl&$z`?S<<{Yt@ntMd+vkpO_vH)bsF9fWJ}C`RqCUm&b{aP& zy=mu+%P4T2Luk4Qo*>4BbAhKqBQ@$MYFHzIm!QO5#7Jp|7>{KK7(Fgf0NaK0LH^4$gxENj$o&QSgj8$r$N z;#Jmwd?dCO(9is6e*8gLrY)z5$n60GY-;er|EilBr!c-A+yt7brLK`W+m$weXWWyz zrGI?>SzHB?-U>ZT7vvdmKeV=x8p-#VIo zjjWZ~D4gbo+!8)vB!k<$fX_d}@ca(UGqbdm$Ek9pbraoka(Mp}wok3xex_LcWz;u2 z4}%ksXc}x`ftwhFI8X402)_qlJK*Doy`31*Brb0~hPE%)%a=a#@CLDxKzN~*o#I;=PCA0+fE*D@DC%roap7c4Nwd8b|b zzE>VF(g?EMS3xuajLKyhlJ$9D&*i38gd<<>)@7{m|3PWonVQbh`Z%2%nMt9vLJ$uf>)+zY~i;cCn&JsMIq~ZpJPBVk;G30?w~CAVvb zCN3mCdPB)pAG%Ca`a|{&?8yQcF30owUm36?lM?w)-(pKM6I7mg4vMjjg%h zxI2?OsIKWMJoRH2$TfhcQPMIV@-Z5K8EVp~a!YNIWZXWUvZiNq=sDVn%AUvpBdamLt35oq#+6&2w)*J46A~?;cvu_wa zfM0=B1IUsBkPrENg14iH-)rP1K5p~>Z8J;%7etY?Sr8s}4c_;tc-s>WARGmTNHVL6N%k;+%U%n|llgQ)kn*qD1-(xB6Y zo67N29#M$09bi{VJA%GKWJC5H8i@D2hxU*T;3Pz_or$l~FBNFX(0_4;b0!ORx+YDb z3cCpbz8BNa<~;oPUvg-gYHre>UeMXop9CWi zm47+Cw<4%NmKyo3*Ls1UMmJ7H?b^Wwu!T!bkm;$+95QL}FZ!jTKQ60bKAlXLYPmvS zY}UZ%e^aww3>%gRdja}3H}M&E(w!QBXz~{>U!@B#;M0>rZr3) ztcgM3@o*v(r36slm2_Jcu|*AH8mI_j6+;`Fa@q|(IkSLklR9nPfbfv z0(TxqfF*7(=2TF-lD#x^fAD5U?X*ozYarIhCnej5kf|{K^s8-aPzAkhr+PtUp!gSx z%$ogKp9PyMt=l<&A0G$?g1tP+y`>(Udu(e4cq*j2Z(j;@Sw2rHTDiGu7I@p8c;v2` zE6#nhHV*x-w>z=t;JO(W(q=1AJqu&#x_OCJXLE>*#k6!S$?)sQ0)->h$cPH*!+gm~ z54UsxdjM&n^_bU*b6oXo9Gv3F#Z#rvBY`0~VKF~=sB1DM%BeJ{C9B=}FXb%Q^ z`8vqA-FVC6v3;#tvU3_nH?Fc?wnO%$l4!Ym1Ke?XKJI(EKLA$nwQ*ls#OeSQ=0`F0 z#d?-kt8MeY-b4~<#yAvh@;%{e-Vvrck_I`E#n@Vf_2A)`YJ-N^x_TFcW7rx)tM8m7 z>Kl-ebM{Y`G|p40L+2bj_5Awl-Og7)Gh#gr69~ArR*R zSR#m)5BLtpT%r>CK$^+IkSa5|19S^87`M$>V0|E3v_tRbqnkpHdav~}^n0CFi6ty9 zo3hRCx|_LI>NI!*F;*E+0JRH7S2DAjZ|>iW_Z09w$up-js$`&m2GM zhF!G_ReFDZN-=T%x*8#dQdbDg^gD;5Gisvg_9-}152XGCb{fVPG%wy0hZ_WA{g-Mt zZjmaRH|CO`By zJ2h|qJgET{Fy{-uxrrhv(#~A9_LX?WXO$T~FRQCp&%Gz{&ZbeNimL|5GvnC>aj!LM zexs$ekpnZ<2nw4cPp0aNaR5=jRVBZYq~|)9R|0SCqZ5i*1e`LYd>uvJn%+HR%x`t{ zKKWy(#b!k>Y**R5b3FB5xeOoB|LmxNgV^cS*t+gD4ZiA9#PQq|9@^Kvlg0T_yaU`D z=m6)G>XUxt2JL03;nY<%KqZHQ7`K7rgJy0bG!fy28n92KTV%K5j?{)PXnSi zfQ(Vx8~#}j!D;N8`>#JVk+NPab?@!tN>EA`rvEQ%jLaFk4PN;!;rP)H#u!FuM{KDL zeU2u@1sErAjH;?tFDaUiz%#6fL2y1EA9!{I`HNVyH{e~V*X}RvH_MsF@K|QrJd+3)7ik@ z;wj7!7#UTq^R-cDxqXk#-^`5#0WQL|0K4*PpKOwx$*R3xNWOKrFzUgym4>eJT%-O8<>FiGLDzzk zZ@#{D@1nP=7J*pr^m%*+#Ne%fhRNVY0E&yayq*$AmMsw5Ddf6U(f$wDf#Pdbhpwz;b;m4TyAO7M7qJffMISz*-XyiL}<`x01p6ZHgbYW z05+m>l<4c<7z(b1Ap!C-0r^W7WWjXCA)&NzTl?s{N=Cf!+amtKouoXCK@LHGmXBcF4O6GZvigBI6F@3!_>46y6xYnY5`lpG77W=_h zc+5pd9e}1gpe9-MDhUF0V|=F(>_>$rfv42;=TkL0lq&Dy5Ki$xQPkhIB z3PnxS4`^R!kw|fg>H~T`a4O&M@}?j;dm|KDzvB2>tJr599GA zc5<1b0t5wwE4v4NamJcbHGiFNb2b{+&F?LIPzPGdOrP@s!@VkRQZG8jN0f4ylyYky244Cyjf^}o2S5`od z37C!Z8Q&nud%{;uq^&>GLCCZ-w(T+p+;(GuQ2|G8=&+G;#!gY)g46nw?M-0Yn)OGy zra@p3-shrvHoVnyP%~qzB=LLFKx=wIsLpzp$qeY(hcl zg=7iy_Pk9*uId9L4$ApIr(bt8N8QM99OlOv-KGTi{fY_E7clTX zm90A^1doQHTDak)t>HVcK3U2HVg`PvQm~tdo&p)6Pwhy|ONtQ4nFC7%^9kdp`}>F9 zkAs^jM~6YX_k-F(&%u~})ujGt1c*0ZlF4il@y(At%P+KP2Ia_jg%{1k8+Pm|Y^=wP zMS|YW^w%TTu7?7#BhU$ma-^Nz+Snp;tJ()_Co%?=V;0UY&dV2jB(r zw_^YeaYHkHj0u!t=i?*ZXO7!}q~v8#6++e9zV9a)R38llM>Q90IC~mN#f*Aq3>4v25}Kn-OXdGo-fNYrQtjGSnazz(6zSjg9ns@ z1mP`!lfPCzvr^krl~dg?MUiw!mse=BhB^RllDrRek!U=+`dh(wg_E?Gdyw>xRpT8N;o~l}}_3{D}3eH1+9*5OhILj583ioGRA-MtwT3W5q zv#Dv(4Y>?<9U?G#m>q*#iWrg@B*?EFmxWe{bO@d5T6-Gya$3>Y9D25$8(tw%LAgw1 z9K|KN*@UVZlb$5R1tUE%Yt)L5`wH9!=fmoOGojB_t#!c$2L6ez+?5IqEO1|8?(0s>{$}<;{BbQ< zdF87nsQ%?Ch;&BpY*dA^J&?fBs=;MXC{v0?g zudiA+hD<)tTuSBjgoowjl`KIspg?*QPh|1XdyEiX0Kr=i9DnL1Jh!h8`7Q&x@{D3S zgjDn(AMzl&F8NoE0MxLAhyjXSp1IYwnetpU-^J&8RBO#=<6%e#{UxRsDAx6qRBsnH zbCr7eL*ta%)K#||TY5+32gX2YR1!{E%@0CTC!A0$p-r`f zMey+CXYS2O{NUYP@Tef_@@fWm+QNX=>tACoaFV>LCYg8#q5?{;F1o)WCv_R7Qb2{z zfrEF$V+}-|y~@w|!zXr?Sff+>H`I%!1Au6b?vD%5gm|zv`o1t=FtOa@coGDXO8|QU z?|XzW1|x)_v%czSV5lmuN?^1b2x-y??!x_*ia4tDdK=3@BDn`lvH{3u{o;#P= zwJ!>SMCTxhrvFRLrWNzDPaM8YSD3D9|kZA&!l5$-_hR%*6!8iCy7TG;M-wTkQ1`6fMy z8nX{u!P7yymAX>}Eu}aJOP9)&U3ch1#8i`4D+wh*7M{Oaj)qhST@7pSF9~C>NL*us z7Qp{Y_2uUo7icT<^MA^Y<{=}pet!NhC~b?YRmjL|4~8QB22unkK!d@#00lA`vi{qk z1QDcD%=Z!{3oAMC)PyKBj8*JeL5`a?K9Fa=4w8!-e*}}hUi$^rer?-{0r6&Vm6h3P z-NrW+nTOZP!9EGDEb95g{l`qL(i^M>RF61*2?Su|KnA=E^4&q?mV|C3Q(rAU@J84B z83rkG@(P{jfOhk>^@K}}{?q0;pyAw|8=8bqX3eAmFh+L5rbJBZUOhzmLrHf(Nd2Jv z$E&mR7f9%(^B|QVd+ZGu&<7$s3jrt$B7d#7%TD!harYvk`6=RDOw%r*$?)yW{v<2I!Qr9}7 zi4!K3i` z2b>aN3p7wKts`cqdlh*?)SZ&6otqc|#hFu9cb}zXd7*q`;*U6(%vR+T)FPU>4C2R{ z%Ilfo%OD;{&wEMktnGHEiUElP6C@m58i3Pt=5NwwwzbzGfg>WFBVIl5`R%IZE^Kt0 zuA&1hVZaQ0om+*YNqS;%Te2$hkUGtGlUB7th#Cm}vQhxeWe}-am|ndC+tS-Rp%wimy)JEu6g? zgYOcRPljWN=K)CDzT)BdbIiVp1%uM8(8Luoa!95(5LcD^@mpNy%=~)+lpBK2U{`|M z@bdf$r{w$X7YnCZcim^aaarpG?a1XZ>lOXr8xC8PS`u`t>x#=CKrw`VO5O78FYEel zMoSCQ0$K&4aatNL)5k%L+8}yK^^Q+Yk=BBHN1xilDy9a_&AMOrqNn-kMNxN8A@g{? zTe4}E5p3E!QQY7AiUNtJf^myO-=n29`b3)8AAzdxeav-;@<2&-r^5P*)@df+__LxN zBbrTDe&%jCQd9pkR<*MmXzxx&yjmvKYR%)m#F6xJ9nu7x9J$| zv!0OSs@~`2puq&)M?SeXMd?NVp4EwHu9`eM0L`J7>l1oP!s?Aw^(CN6sP{Mf(a5kk z=y-=H%cBGkJ`Vf3!vNJbyh_h@n4fuO;w(KQ$7P05P0t`X`9f$Dt|&`jGB2)Nz*TIp?8UJicz!t4<@U-Qx1*xJo; zN&^rRs69~Ay)jaNvkVHftPQapfPE(|9uzW8AUkZ;q-?(0kY)=2$b^XP0vkXkU?fRC zEKcrr$_m&I(&)K47ggR%o98U}0vIccY<6L&CuM5Go9_7~YVQE=LcT1^nB=}4<)0=A*q1Q&2HE> z1t$jgce>_CHmL5;Ep^*6LmKV2%$6Xr^qoHPO?zxAJAch;Du5B=wJ~HK>_aU?*#?R2 zLut1Qo3HrF{-p)CicL|8&}Q_lS2L>z`xe1QANs zhdFcVq)1Ol>odm_#+9^;?78$L;vU^VptcOLXv;r@zB8k{9gmqVkoN(rWG*zF=ULJ? z5Q~bX2U&@Cn0+n^wC>2dn_;H9Jg|Kj-J7-hX6BDJ*&)8BN(PB;8{>3vvJf^E=Yc(w z#a#lv2BwPmjl=C%Iz%XMdZZD>d%yFhINze+z}jKE>2%kQ$E6={7CiLn_=ZK=-Su~pZ#T8Kk0wn?m-feq!pr??PXw@}QFcWUV0<2RI)fXC}-{0Ta=v^5!vz0@tvp zNBWhWuu2><-@GP0Q5E2Ml3kP#Vri4&4;W6m23&QWO9CLG3pN+4xs#q;tt6@ZsQMeq zlf0|l6Y$kW5{p&)2VutJI7G~X0QwdL9b|s;0NaDHfvB{&C)DNEJg5hz#H2UcTXMET63!vlyGba?V%+R@=mVGqitZ^K+ z_sl=Y(u*j2KABnjTypd4B3Hsd6LB>hp^v~FizXnzLpD(c(CN14G6ye%A0xxW!q@H6 zKE$PmH^C+wZe2a3gyIS<_*BZ7U8YTq}A~y8RT~aIeo9m$`t?WdRD5WBg#79F6LJfN_s1u085%jwe)j+q<(X{%)5plp9x|NR}GCyeXk<4qU7JahnJ6HzOj<9x?8fV2XOfi+ax2rFyDgpDhuJ1H-x3AP*WAT=^J>L!k0&&i} zNWdev1pYljZX3DH)^f(gj=B$29qfX|X?W%7gR-eE?d6IZ9}PQM*xIBX^ulh}*53rw z4@9oJCh(?6Hm(4^7xAMI-?2K%UZOs&EWvF$_x(U>Rn7yME%avl2mKz|J5$OB*YBd? zrQ}YmHahzPEzxj&HS)^frY00_F$63@ofMA3Wq=^>w-`?um0mf>S!^U+wmglJdsoFtjgAjUS_@r7VP|`++HkG0>zJJI3b#NU`MI3s@@|ljX!rjGe0Y z0S|vuCd^85SCTYSRs@&?gaL0TxmV9#@o}@l>d$*8N>VDB5bT)#{_n*#x=(b@RX1fb zL`%^p)770>yayjx<8n!?*%)v#F5a+@7?MHTRI4-3g10uqpe$DTk${C1vgRUTS;~7Kn!$iQ<1sU*V{0rd$Yc| z8Xr3I2`K1(NhP_(b-3mW=!T#xg%S=*Q@Ds-uXT~Ul%F9fHM9!QD0gR$`8P_)9ygFq z$Qu(eiG%0@HdJ*j&8|ZT;QP0~xIg{Onf9J}fR@Jd1aws^)DM{UVHu>H`!(=X)nH7$1*6eTKfdW8OW03?_4QOFW?lm(0pt-ndl?KnoS~9C{TfaaniY2&` z5}utateNs%0vHK-)hGZ6r>l|*!8?UekVd^?IarB1k=E(orMtDyws@yI1y<&8mrN6w zg--ZQ8r9z*Lf#;aT1%?bB>OF~gO!IBes85;Ioc}yH$}QBs{;6r57Scm;TrY*mLuHr zFtA#gkIe!$4-nVO-sisjgC1YFcO^&iFIZNfkuZZt-QvE*Vt(&9J{!_K8UIm18$aN? z%|oBB3~m6R_*7v5eIIjjX9uD_dev#b4yIaPekMUyH%QtXtK}BAS#cS~Cf)}*?(Z!uHz%H@3s|ka##65jfhs^GbDL+A;puDu5L>;?6nfOrWSKd&FkP+C z#nd>vfj5%Q@JHy(zwr`M`f)?cfEodl4@2qKI8{n4GChEGc2`qzX-cO*Y>Pq&^7oI+ z48*o?R%gZlhg zvR`WXZrF#SpC1K0W`RWuL*qyjL>#)YH=GC1h$!-Vck?yy0MJ<88H>&R!OMi!fF1!} zqCrtKFq{#QaBjSm_UAXZ~+MGfU<`TmaXm zu@UDUc6N4Xh(I@%S%b1YWZTD#g$0DTp1B8sB|@xTKUVHF6>5}k6Fhka(MzMh51h=L zywEJn<*tg&&G3Ek60I-sI+OP{PHx%>-5kJ{os6adM zlv-(A6+V133P;I2Yj)QTuhwlQ3X3CIX2WURPywp|DAYOIero;+?LTW0AXc^c>AhCm zf1;;n)2a*V=ZC3%4;MDOL5DN{cgh!oK`9$xPAT`NDSd;IofHe(bjg_VQcShvzx_Z?mAES%gb7tWaG15${u+$_V7U8*A4;G*={%QOSy}lar66*+wbO0 zd^RT4ZU5!Zuggs4$+A{wV2kl)ilO`RKm^X}X~n+W@jaaorpDD4(-JOL9^#f5|IwCU zt&(UuU->RmNn(!0CiIJ{RlTWj@X~0zk{5xbaqz6M=YVm?H<~vK6K~%`I;V0vTK=~A`hdHRyE!=e;Fa!@*3%tkMH{_;tPkzQIZ_eo z4JZ048q)LE7N!j2d1BD*3R^POz2^-|Qy70#Lf?rDGwir8@}Hn8Ku%&Ozzkcz z&Qc2Ppy#jLI|}{$>X*1I?I?)o3NmCG3?zP#UIo{-r=>>v_^|`j{08;ZmciS?c!w9T z&l&ny2<_6($_Y5X>au;ToPI|rmU~C?zUNT*x4ttvVc?kbLc+9=Ydv#c{S{}m)YS-l zZCR3~yZNhq!mwd;5u0}&EyC6H6}SSvGOpHsI$cw&_5`O>fl(#T4G~RdJPy0GJOvRK zjIevzPeZSQUkLuAJz<{!z<$FZK1!XDBWWer+B!*+lA6Hq=tj9&ny2!ExPenSo{`@k zYqf|z9BNqSN5O70E!vRzPmj-s2Cxy2qv?oFKzj}dOWpm4V22i0O}Zzcf=VuL^9r_M zLar41f(!|2pMoKy+LzCm5(u>?)eU!vr|<{>DZxH35fE6Lc2-bZ(`bV)`0_%>S(snN zi7Bc2AazaMj4%_gs4f+d{nD8|I*AS_FrS)JQM-9m{F(t%4Abq&uF6|_o zJ-fkv2_cMcS@W?p?ZE2zKsU%Nt%HU@i{LxF18J zoeo3oAk-VBsnpKsR9BcUjxc9%(Owd}h28l2QAb+jRt`8YJ&*j9U*NinU^WA*s#*Oh zh92@Ur9E?C5VgXlupBc`K+Hy|jv+XAuQ{0hU@&;;69CM!RF@&`N&_*Ljz{}y)h^K8 zQb6l4E;e({NfJ_Lk0v;09Ve>p(^5Ap_@m`FYSjpSSV1oQ$!1;1pT`p33~E9fyIp4Q z5H(VN3bBt*ZNWD~prQdr2p4<{5d&N|9xWqd%ymCV#s zkU6dBIHN`#xQOY`E~eY;c6OHpb}jB>``e>BmPYd9y?vEYBlkQW`bfNigDOOqZ{HYR zHDvs}i>6)V-)UWSu^3M4^JCe~-b|%kdPD2-gX(Nw*T2K8*x%4zu|V^`kHJbZheHeN zYG&`uCfx;Mfm^bCyXM3--`${XMu}C>51^_HCNRLyeJ^xeOYt>4`f!o&d++AB-*j9o zB)1RBZB$$ZM-0OPpV8^lCa0uLxllS8E_GO8+0kC{yP*1`UIEeLAAtBYe{QD^ZY&Vt zhQT#O@?g56UN1FnR3gFgR9t@RG@OJ4weETS8HiWzcX*^5ykUWdRG$ig_zX_jPCMp+ zVEg1;;rT*43S!hf{F@0bL}H()J|r&l-E%M37FA}y(=oeK0>~$v3tPtZt+2UWL?SG? zc-(v;s1yEOA%e%d07_@;A=5InAA`BHOBb*h@47wH^@B1@>_GDv<5ZXHcUzm`{$mb05AAUDzRvrHL>^IC@6HEJd^yC zF765n(<3i6ZRERxkw83_C;PRT10x11E&$t9|D^z8smz5{kaI&#rNt^1=qO3siqTht zk^{yFe}M66%o{$j>xG-}hD@zI>u`WwVD5W!d?xz=+hZAQ$@=F&$Q=Wa=l!|IEhVyd zQ`Xhmp<*39&Bo;H=cVrB*K;iM>y_D6R%fV68JNr0Ey=F%H{Gdg6#6g^;@x+aZ2BT` z>yl4E8z6J73mS>(JZ1`yz>hs{ecbC{z@4!A$17%My>eIqesxOLRpuh-)8({RT~D0$ zWrAAi9M)UjxY_+uZP9?+{wtQKl>*I>H`4B393U64$VnMhEL~wU*n+4OU}|~{73}*( zKLhg}qO*pnqC4Bt(R$;Ry~FHAcI~V!iS|>_ar*mr!VX}ZDoF}dd!O4E>bH}hJt~;* z9elcd{q+L--5Kpy=G9yXg(r7%2wce?0;K)TOwprD->A#_13WY2^9WI?o+MiD_{aBG z2y8#R|92K(C0Sp&4sUP~^r|PIdWi7?wX14=@jaYrmJ{}$vYgr3QMCSx=4e*A9__^L zGdNQT6;_3qX7eJ~Yt3?y*`(?|r=U8Y(p&6v7LyNfAp5{_D3F>?X+WuM$xCyf6Ga5I z5E1B|O4=yyN)lG?unpz!2HI>E{TU$EAl*p8<2T*Ak-)IGZr}cCoc&)NVQm~Uac&-@ z?yB)ca!`ot!jyDYu_YsAFbK$n%jK6j|FGNg{> zpD4b|YtVf*rlac$YOsxK17PahPzl7nW&=@DU0O;PxdLpM6d;3#W)RiUB+k+u?0bN^ zJ$?JIB_MrD@^>ul0-6_qTs$g^uLlguZ}vJx zdw{ek&0`kmKA=-qVbxLe%7M}=$0pcTw2pvyOkl?fphTa9Gzj77j6tvD-6OBa^op0| zhmS4jbv{BhTt4dZ>H@Ey&pE*?zl+#Ven&ZY(Y&9pAtEF6^`Gl9U-| zjeF)2S#~^9zsN%IJFZ5GXh|RSa~aGF6bAaQ@2vG*r7v~8`V{gk1)^kNu3s`_Isxf= zpk)j=WBxEw|FT-vfI$}=z&&8=hlh+jYGrVIDNJ&1@h4o z>=xlv>~1b0`%RUQbk@62vR-1JhaoS^z}NvMSWW(tN{&J=cQ$OSuwC#-=T@@_#}p=%aAT>@75UXPR5v^N!xE9?U~Ni;bzjeBGhnQ0KC-X3l%@;-g8$oH5UYbvdEE&CmrVoI#X@zT=|jxl;N#Zk_pE}f!#4?XxR1cKJQ6d zLIXlS4*pJaLjm`q1St(y5Dh{EWZ@QVLYF~rBAv4=y0*~Q#=}-L`f?hSfnKh)3YnUy zEH_*x6U_iU(;__jgNDLCUI@B2l*agN(!PaW56!xlt&4z`Y#o7mImky;!Fvt_eRZyE z6KB453EBH8*Tfv&JE#^oKF@q_HUf-mA+3&vVhg)j-|n3fSdA&7m{#u1bBj0c84EK;lCn@1IQgBrnWb8{Q2 zsw?$wqf@pA4!zko4y-{&<_v+n z6ViflICD7n))hfQ!PcO3XJGRdr5fr@f{m+?^hT*;KTYSjpg-U!$XqTlZeu_mSamO$ zx#y$bxUK<~p@o9pki&ztVaB;cKKhWC`B>NGE5(zp>Eq__d-A}Ps^)dUh`4B*#1Bei zOtM{*eJ9J9P!be2-g=urijC*^t#P}&Stx}eW45$$7`|aq1(pbc%`k=?0#_c|=G=-w zD=$bN@)9;>Q7R?63$hSTA?{iH3?A*(9j3|PpOR4~s50|oW>zi6#68ui*ta-(z=vwM z(%75{o+n0bae&~xst0aTf_RT#I z)*Qp$#sOBYCm?R&!=TYj>9of2Q*StD4^B2lK`i zy0F6M=Aa7%njVfDh)%5r-5FQdDtXCQHn-+Wmo^US52VfxA4Mv7m{`9{rhEnUZ1qpX z7@k2rEg)}9gC1#L5uxvM*|`@r$jdna7vma_L?U@$TK$5~z5HIxc~@FxK7E7I6M`|> z(X%s-dm@1Ov?MFG>Z@u$*f4&q$`HbO85ce8)p_8p>auPsI#p~3L952PB9PF0aAD9) zk^zk@2t}2%J8N5i{m=;c$E#kDN42INq8{5WY-@z0%2gS?%a8$*o6k;$Au;;}mAQRA z9Z^~qrPEg$c#}Tai{*4N+d|Sv-2KbJ=pm@fsd`>d#xA^7Bh^D8632}B-z=UZ6~&WC=>m$Y@tp=gMHj+6fSuh{QaH)b_5Ba0$_F65Y!kHwR9)AUPm3DZ(1xm7^vM(6+suc*F(oPC)(8WaYx%{MA)?mPKI`(>6JuUuzFMy10=V1Kj5VTU=~1?@?Gu6Ih{==(}*0 zehDdAWRP-B*S;DVc3IrQb{WlUkZq zk2QVlwo1x&j~+o@kWje`={q#B`>=g*W{jy@;T@N9i2e`&UZcLarOFgm-hQD2OYfi% zSx5HuuqjzYSaC3knw=kz@NteXblIcLF1{w*!bsrGJRw%QuX-)E=rI9n(3p^Fdh0D5 z-tR>RK`9|%=x9QZjlDak=n*JDLJ?Uh|IeSs)(wXuI0irUb$*s)uiu8)1k{TtgtDee z`#pv_14hS>PgS4E9hVBA?FLnO-Qv;<&f0lvt;yAb^M_>poD~ePYeB)N zY|YpwVUYfl6U@tIj%loo`9562KeB(aa#}Z9yfzRIE=P+OA<;!7bbA8geDS zEx@%m{BnEKxwHX0@1E^3OO>@fJ0S1#*L@ng6KOng>$ILYA>ATm6hg}t67$@73b1BO z^@QQpd)F3eeoL8bA;6{8DxUT`xoJUYNn0Gw^?}mzUxX(I;di@QW5Y zNTmS^uYHgnrQ-?Ufz1YbkbLeFFTQ-rAxbbPJuUw8nq@B}?O>w>ruA<|=fi(=zqMI|nmo|gXUW-=vm1WY{#O-}Oti_j;WQqdN+8dpj#DSd9%`D;C2yM7t6 z=_dyihtHe=zpLPxT{Qoh@6Hs#Mum=(cqVi0wD}G&UA%wB`F~F`<-8?qM-0!zF-|>J z>dpO50b?9qs72HwD?v`@uQsNFSfp|a2ORO*c{L{9J9br*R*|(G;MgLh*=WB|w@V@S zIdsWGE(&Z^ijt>-|PFxfoN=5cI2{j^vmF%p5V|xacIuJ=apKNBzvg;pblcN-+kDDqF3QTumR)jjFSek?s=O2@T98c08BZcONf*=|*?( z*27@Z?DOP*&yx|JC-_HVph6M?b8JmM8S*DOWpe^%Dsk{2{xf6a1m}g{ZKrQKxRyaW z91B|zy%C?{6J(xC{L(kq6_cz4+LNw%aOwTYe3`Bdwjy}+r#imCL;iJdeOtbf@M|#b zRhS*IKuUqgT|HhS(bc5M?IBRlK`+5ne?8x3$LYembIZx*OZEdFnPk=l!R`DnW$bbz z)(lpLm<r%@d}Zt-m3+msPB(x$jQteoEjZIXd<>;v(ac{dUb(%?n&xe1Uw_ zjv4mbcztMsck>>1^{q8jQf6KW3{W_T+uKAntj)xHkPw zK|E{%B`>QOp=hs^zSph+H7WO{e_kJ7JEc}_G3E?Mvd;$1tq;5AZDp~$C&rI zD;XOJCd5oa@e%0RY^W)I{BbrI-w{rFWeD>SPR%3WJMSQy;dq=02bPmAhQ^FXB?K66 z-5Ir*+2FOh+O+hfc=3H7Zw*MzPUVE<4XA;f>nSKu6nv&gZ}du{!P(p%|Dyfg#==wU ze85EtC&LF}s;7AeR9lA*`5<9cg16y{`+}-DDs*}|^>4=E2vAxb z4ysQkdntw|AA6qO_^Fdc+6U{*Au>bxH9xwj(%wmYGN8y?xf<@|bq3w80>Ut=a1xkJds%(~OQXH=Rf95c1Sc&YL4(Xe|s9i_; z=s2$6i+k_vAp?#}K6NmkI?Gkw^bL9B#cLm{fas?-MoIx(WgZh96otiaDs+pL4V=4KgJAa zX7LKb)^Xmr(AxTVQ2UkF7Z4yJ3myM(&Z&c*#EIwow*4D@%7i6qgdCW?&NPSdA_xR>tg(vi_4o#t>F#m-r;QB)tw3Oih(p+-I-AYKR2#VN4 zvv%$GKXwDO8y5k*L9i>G0Ede;`DvK-Wf0-vg2rLjrfuz{-1#vv5%@#kNyntfE0-&< zn}h!Pd}tvfbOZz2HZLOyA*lfwI|NwJ98-zH=2GsbiWeVid|Kz;LJH9T8cLbPVX+0_ z2}mXf1R5fu9)YIT0T*LkVC2V8N*$svYgr9(%cB;&ja_BU%^APQ z+BlaE@cQ~pb;isd2`GIxU$X17+|37ON(!>a&)9S{?z`y0yGSq%JwMC4>iY&4bqeYN z0rz|8m@b$HhGpP3smLo-mk0tW2vNe4!%5{@VeO>n6N5ucwD>#YB8!raZdKKilPxQ12QL_qJ z_lQIsfaXzwd(dr9!hf$uS*HKG|9_F&CT098ccDId9|FkL^RCMs&X{FMIsYi%h!X*U zs2A~Fe*W8aUpkaggX<}dDh50wt}=C}w*Kxy7Ni`a0*l{OW!8#+HTAD-S*py^I(HBJ z&@HroODlBRaekIJvhlw1xN2(w&xg9WZZ>j+wWAv5s) zecwOw#{tam$1E(&W%+nHPN#dqG&N0|9{pXd^(!9tKgI$|z-@KcA&z%nUOT}ZLl$b_ zgA)0AJp`EhT8o^AeQ%W(33LWW74f>VvB#AJC46+-dBzjFp+~evG#Tq zWgy7R85euaM^s`3EI;_@MQi7wIK;zNLUViK5GJdsh}e|+0k5jd+Ruy3koe^=CLWJg z_i>Lk5)YJq=%uL;eAqu|`l8n@x4xgwj=6plTEE=QpU)OTs|uVln8rz9+5*Q}jkUl9 znKhuI;}`dA=RoaMKzoja2Cy8j0ANoON-Uu@7{wmVEId=2yAaO}8fQMwvB9S#sSy>z zS4{ky{ZWf6v((=$*xjD8qD?rOY32!Du3H3bX$#=Iva|sw74Dt+$?FZCsBgrm*n`_6 zGfPJd8keb^R}&$<0FaQhn37x_PWnCvr>d!v&Hh|9dD=nZa_p{|=GUhgAaKL<2aN)| zH@B|@pgdr0D`Dc+Ea0```7Zp4Cu;wm*{U~OJ8YZU^<&NDM(~mhB$FU3rK!zrT`1UV zY~qvEH9dKG!GZ$0+5V9QXe_SHX1s9y$o^Tj)J^CK@Y|@{HM!bsI3{Qo!>a~m#>tST zj{2^&`FTuWMHd!TBuAj|*u4?!+KsKwVW`~LHwFgj~pv^F11J-}W9MS#h!Wp$d>h^tml zgzbees&n2@3qr1SHWg(Azf5JE>Y)Tgzg1{f8R&!*oG+W*j6FgCEKecc3cbcGw2eK` zb|(KzeI(QL_pL+T`pGgDjy5BIFcHU03V3(dF9i_;2+P z1KSQC9Al!~{4=<}%Icl^^PSG8xbApxdCQpMnvW{XI7n9+ABC~0Yd*Bg0}Whur1xsE z813|y^Fj!A=&G7rl_LA`%MUDlzlxZ~vw+}4)SC02@5WIdB(!6TY7`IMdR&`2yaX93 z{_j~EUQr(|qRy&!Kb@g*lp+30z45y_Cj==nIky9jX_KV2Mi{VwU&+Lx*FwJ`Y1Iua zSp`4pXJa2j%6=ORFB%L_fUN)kc~_$QM4x{8j7&~wS0Q_f+8r^!!I?umz=I28F>uTZ z^{H^pLcbj9T7p(BnDgf-$x9!GG*;(-*vRbOZeicuyB*c}Wn;kcsxMabDc z)R%D9ukw*N+I_G4L0*2SiD_h8-yR5S%JkxkHfc^o znTrs~;eN^B4Jbb#1XB-H=Eo`_@Ku@g^gyNpkhfj(f5N+OVHK_Rt&0G&{+MoVcrxnt zn{%AW=WLH^br$*ju)3}rqRmF4mWD!}0T-~W->J5%v55|2k~pqRK!EY9@i`9HGt=KEAOnX?4dg>24v)i0 zT+YAQfhlH3zwfo%2;&aPK)xf5?>Wt2CbBO3|1(5z1tx#^s8q3xYsp)I3obyCGV8Mf z?{KJd^Bn$zs806*8h^&N$9T~j(uzFmYD{7geRlxuAND9}$(Q?^wgEh?1(|O(e?6+e zP^@(kPJn+reU%X;Cwk;VW?d%zy+PD0CQe3g9d_h8SLiWV^*sja4%8)ox)n;utmG~5 zbg6T3|5_=o$R-Bff-)gX?{jlJz%20H9UarDhr3cJ#9!sRH%8i;=cBb1S=_IoHD`aN zz9C0w1%9L24s`dd(6a#|l=^9iJyrT8Q~aZ6c^Qhu%jDKX?g)jhas52qr?@xKv&4s5 z=+YIZPwd~^6R>vnqX)DWxxjDb52o;<8EUWlGKyfSzcj4&Xk%cSeb&^aq8}i-@N3U@ z8mLF#pW;=rMK;M-)$vfHX5~X`jfLtFj0Lnw)ndSU55-ev_5DDoJ|Yn3O1vWU<^5-i z9}_m2SIZEX&iy?bOoJD^&KH3DjuD5GHVp6A;QJ=wm0Jh0s(QSkG9Hc z?5-7IW2UFSq&i|GqlE%i-41%)2!)D%H}(xXq>3iXUhxVnOy4dNh5Z1GV+{XF&l5)N zZ`6@1H{@d!MS9w+*rUgx?aRsYTN77i>-vAqY5n{5f5r+Jga;tjQfYyo`DAh559=G^ z(diaye?4V~EKl>`kWi8TlO?hT&SOm!AX!m99A7ozo^?8==#o#{r2_o^C6P2mn zoU9n#)YQZnUGe0Jlwx}}noue_p{ct?1U#+&|Ng;*x|_PcfZHMT!5-V(Q9;oqH?ldY ze*e>;LWqUuZbNUUcw@I=-}OeyQ{cHdFH}9J~5T^Vkz69AC?iI>YjhGY+n?0hy;^YWlK z@po~+Q3b80)x-QDs=+?j;I_2vX}fz@`ctq+eO@$zkN1vj(`duMg=`@M~({ zVKtMv@pp_J-P8T&$GNhYRrHLT2_(<D>iHHlRpK0>(kZ%jfX+<_fnx<~qC z7S8?4Mz)+|o?|rFrx*>TFqiJ=!+`yUcEUC6{^v?U&LgWGig=fEg9Blmc)`y{--Y@- zr#`@zNtp=CTKtGL_?%Ulc!$7u@8qB+dgggK2nAr=TQaL4vZM8~N9UVc){n=^pmiB) z$;A{2{lk^bqU=&~^EIdt zNM_F^`ox|^I~N_CH!v)x(=E6Z7IqLcW{9o#z+|)F>cikkvip+OUn_A)qS{PYLW1J{ zSM1Pnj-4T$oBM7*u8D{?i-KUsYY4ZFWFMSJsRsqDf@c!&k>DcPSM97Qa(XOKRXg$5 zJD(|n0lM5^wDNW6xMRQr2;S=_dvz1sCw#01(W$gAoq-r4UFQ0}a=%r&C_RY^uvE%f z{_0V7u0nf)k%cA3>7u~2cXk#}_Zc=}l#sNwqgZ%fJc4&qg%{fxxWSk-n`nv3GlW zXD1B?Tt^v@tgv1B#UNYR(B1Mh-aE+Hw+CqpaaW=vH^x~m6^$mGvbkPE8C3J zJ_q|q!K|}+Vw-bPtu6C3d^hrO2#@!9y&}iYPs<*miU$UJ_}nYb-?T4YcK8C~gD3#L z5Qq5%cUyfJzxnzl6>D}PzlrChs-f=#HQr0v&PS`iLH8*ZI9mLFcIPj^$ZfIs3>BaD zP%oz|e04oJVTHkXHV7V`Trd*;uPgK0gX_a?&Mz+%XSoH3hN{Q}@T4~=)PIdUkpCEe zqCFcO;~vy_kDpCgqqxy0I^tg{7q1;3$lGs~CcSyY`^KNy#@0p1E;i^`9P-2BqEGHf% zu3Dw~KXc_hdC=5!Tz7uj;KD@#i=`F0&rw&HNq1nONukN0v>z_&FdW$^e?Oz}?C(Pb z_kau8&Y!a;;nSf8Z$P(4GXXtfABgU6DRWjAp^3p2`RAjsdsstq4N&lv3}f~c*OP0^ z3g~LB)D8>F(H^<^+~UBUV8>g6{HYfo`V_1F=b{h{a5%dAIu|hKZ*qa`H3sIfIl7;;9@Z3;CO!2GnpS=vt^gnZ;uLAY1uU)0o#tko0Uar0=~QE zEBr4IUKw2}Z#9Ix-uchu-pw#r`3|Ica=4CxAxAHlcJ1ihxARwa01z2|0h#!oSoODm zF60-sMHn1&lfW{0pEup?K2hn^9(pQ^kBf&VZvr`%a~Jnr(J>V(nz`%#cGck+d^eXC z?}NVSFD>$0x3=Ip+U&VTR#{r(G8Xv!ZC!cT%2{AJQ#-$HirSd>^V7fl0GjQrI}#1{ zG)#MtWgi}6vG_JFLQe}m*=hNBPCLWU0wCVs5ibI8LA$3Pvs9+a@I_1ET+AyYdA&mx zZl4PA3~EhEf^gAM28Tmc~9`ja*YV7Ff3>MYk_XWM$>; z&X@QBs((qD|7hs&-~qz^{mP!pW~cfc_FN_~oqBStN!!fyP@pAege79}2dRkBiw^i^ zAGF(VB+aeY#h5cytes6*pghG--we2jJ6&n8r9P0%x~@7*kYOQpSYz@7;6)|r!7>QU zRQw|iZo32Pim33+AbyQM&3`TO5Jvp?v9s+J58*zkBMY?qpQdN3;9lr%xz3o-Rc+w+ zW9CwY#zOID(F=XH1;;Li93W`AOS!w-Xe5|1xn-&l(N+Zb-;ea*B=BKL`MQt5(4;J?kIM;>02cJ0zIyP$>4;g`$iN!gXplZCp_HxOdX85*?qQ>KW=QPWBHSH z2QS(adJs#BYa>$m_eFt4Z$|hWo$GGY+S?inY+@&5KD~K&#c5FfReNH<6*oXb2hiWVHZ9&^L-whhGA*TAm!;A zgCtR5?>`3zonde+^K&?!Gjo~uTMRr!W^|KR&}aW>xzc}&^H)wDwJ`+#>mOTF4`ZI6 zdy^xEgd*x_F=p9W5j3K!nqpyf-`~QwZHqUk`37zHLSeV{U`&3#YPg5Qu17u=c6+~3 z7oPTUw>Ir>Ll_;}d&fWivt_t5MwBof{FX7>f#oFt#2v=1|1+N+<7qTesiqeUJkm8+ zP_%)r%x?jH?F?47`l~PKI_l~dR)I*nT}k@kF$MPVI&nx5x72jT=i_6EM4upcg7_-~ zFH@YU&(vXFZR&$I#vbvmuFD8W>yW-lotASGnntELJieQ*mB-VnM0)EV*d z2&M1-;B`b-f{WRELGUe+@1=p1du|QB2h0WTX88G-*AfS)B-Wqe3^J?S1~4JOLFw^* zqOs#o`*c{-S}tKPVvo#^-7UYju10y-K_H;rS|aU9#J^>g4_O$ET10y`|2STS%SwORwc)n}1xw5*41B0nnCE>VH*SemU7*g|G^n z13l__#n||JpWGU_N;>pD{x;HvD)TFdMwx$yAE~PjEO`jy!Mbezw4dACMJeDz;dZ>^KYAotFXSH28mOmqslh-8; zAp>doR@J~-3)`X!Ve)@(LcDzFfSzXsjdb-d*f|)2hiX$--W`OuK;m9F|H|*R5vlD4 z=VhL+d$WU~6?R8{WpDvzbk_>MzcrHa{RAGKq>PPev1(-(GMO=y-lql2;#Bb^T>nMG zWY+q55B_9`vSbvgmL4IZfPeCu%>_q{&7rn3x^OgU)96hRNb<)M;5THd+;4Z0A{ z-0yEOX+O*5o*diRR#)$%m=?F|=BY^ZBUdJNRMz=S?D4R|m4pHA)xCsGsK%vzL?j$-Ahs-@^9W#T|DH7rI}3;PdlG@(o!y6)NN8yXt2 z>eF?Oz7;u@aeM#OF2-Ar7N3bFTMDBMrx{k1N;3QBRz_p*idn}x9WbzR^@g29E-i0{ z_tECbe;@F8>b{9ctqH~pTk4gDV(}f+^BWIZIFEwD%#%dzr$RUM*&iBV5<#8Es4IK< zB+DIO*7w^QNkQwd&D@kGXTyotqnFi9h5SbC_gwB*+#C96^B^XC-&&FK*zgjVuCEwn zN2etjME&#ZX@s_b1xsTr54(O3miegxGmhO4qt|Lknz zBdl?A@mO}pD+^idm)#i7=g$rf=YoZWWYNDRdv5bKr;#2UHcsrvZ>Ir+dCY&Kn8hbZ z*xlk#pjwRvQx5Erl{-2*q92FA0B@-of^o4n5EE};WU9bR7+vl4C)D8MDg%y22`Rpf zMjw@*wrKsR@g_fspomSS?AN1%&dC8<4Dz@%clLn)|BO0z>zNGbww2z=_LzKNWTUS)iWCg}&N~&{*tioHscaNv%mVSx zstj?Gdf)h;ws;B=>y5E|BsX(fBGrn56$RVDflIefY#pK=jA8BL0ROumQQ5n1K8h}$ z`M*1J^GuFtfW!U^UA~&~gq8*0LeZ+_=yo_N{~L6&0ky`0EY*4ti_RvT((cA^? zl9+qc(&VWvy=x@Ia7W?0b0AYMvT?ZuRRFXFns@_ zq&+3c}ZqF*stkf5Qt;ZrpBpMVuo@7nkC~=Ais91#9;`J%4ae@bN*fLo~esgYcCNjpnhliDw zcdPs45m~>)hSA|0(!W}_Ryim`Z7rQ1I*lXl?u1uddc2ors>qb?GF!I`ksDKt;+%Cf zIYrA=jxYV+62+=%YPzp!24=M?G;-Bsow+wNegyNT+h%i>9%G~MiCFWq;jeG<5`i3@ zgb}wJy<2YNHZ|?qB~NC-&zh2d@kYxndzsw(SU&%e<)C!VV%01MOI>Yr-t?B=(a?^H z3eWm!&tE-TtGnuXrBiBu#Gk3wadA{A+=#Tf+~ZuQI&LU6AWz;*NpmDVy*4a=ZFls( zC`HjbSCTs?jv2my4+Qsu6rj+G6FZ;#eMeVBDVq>SJ1yp7^~Vh+@m7B`b;fkV3LZsX z_zY*_0xr^G;vVG7{^X;uFkvDM2Ju6$?pwIVEiac${QaxZ4~a-NL+r>6a055zDXH-h zjmbV~GMJOplV8wsEhAfvcimloJ+aclOn;rEpU_Mu2dvL$txNa5ZxoY+C#Wef&GBxO z=LPw)KDt30t2NDPm>SB01T98K>=W^P;_aKxn@iwae@gGRJBIV|89B0_oeqIO5TQ&R zkKDexv=b94 z3qzVR+RLrcI}QU`NUTD}_(nZjk#%9uYe`pSzo7)sw^i;N`S3EEp~Ta}qb{1VY3B3ETz=7vXY7S1!T$WXR@lr;f5Gx2fg}9GL3F$)yT{)-;y$ZH z%XMz8{@NE6-P!570~=o7FnYIOdUPvNM9eWlAjuCO&{rT4{ph6H#%;R^1;@%q`*uH$ ztv{HEPtve&HlMw+qS^w73R%7cLL9iQo_O6a~^O+@Vhls>9v*6Q@db6e_mpd z@dhSP8O?eOhIsh7*HNAfT}o+mRkZKmv8PAZ2Yl8B$V;F5oU0G{F8_U0I_Q7yIPYA; zMKPMd<6VnUNBG@-o3Gb()4P%htr2ATGTb$>CqQe3^>BXx1f;HM(F z(E26T%VKge`BLKLdAxziJ%73RKW&-%YY`;V=AwD~vx;?AaBf;@sooNHsvc|LSFq{K3KPM6ThjD8(Fe34b3L($!itb z^x#(tm$={(+viE4|EIm{jA|-t*KrtiMnuL6DrH29fC_>Ff&m>-3?LZ=3<44n5Rnq; zB?hcW6%7PLA*e`^s`Qpnq$mi8^p*$_2uUCyga9GQ-3N8P`~A50*ZpYiR^v+v@&+|BS=k~r`-&E3GmHn~Q_6D~UzotW z(^#wxf`jVb#M5C3sS3*kewH8d$5;2+4G8h;-0Z<6g@YzdU^So zxoT2E;Yj&J_J;G?%};GGvg(;Ko8LMyOUiCEZ0~sMjcO;$)rU?H zsrls-Ugc$8-NlwE)IoNcw_&x}8tAD1q)xW9hj%8Vbkbd5HBXdsHXS~`CA>%%wy|Z26Fpt-n(4^M`*RcN4-K3b zas(XHq#N|-ZOHONbNT+%C#yKeB=SraE>$>sCizeIzoyxGE%ly%M-hjAv$8GmvSbX! zAG~O*alzRc1w9pl3x_%Ukz4FR!8~in=gXcSw6SRtyPotRRxz%KKb;tTQey&C z&C$=1YJfMtKE@|IIL~9UdmwxqYCjkQoVlBb)hF|lZ5 +}HAkW@A$tTrv*I;5i?U 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 literal 0 HcmV?d00001 diff --git a/docs/resolved-telemetry-schema-proposal.yaml b/docs/resolved-telemetry-schema-proposal.yaml new file mode 100644 index 00000000..a66de7e9 --- /dev/null +++ b/docs/resolved-telemetry-schema-proposal.yaml @@ -0,0 +1,136 @@ +# This file describes the general logical structure envisioned to date for a +# resolved telemetry schema. The precise definition of the structure and format +# of a resolved telemetry schema is the subject of a dedicated OTEP, which does +# not yet exist at the time of writing the current OTEP. The format used here +# is YAML, but another format such as JSON, Protobuf, or another could +# ultimately be chosen based on considerations of efficiency and ease of +# integration. +# The telemetry metadata described in this file is self-contained and describes +# the entirety of telemetry metadata for either: +# - one or more semantic convention registries +# - an application or a service +# - a library +file_format: 1.2.0 +schema_url: + +catalog: + # The attribute catalog is the place where the fields of attributes are defined + # precisely. The other sections of the Resolved Telemetry Schema refer to the + # catalog when they need to "attach" one or more attributes to an OTel entity + # (e.g., resource, metric, span, ...). Within the catalog, each attribute + # definition is unique. This does not mean that the id of the attributes is + # unique within the catalog. It means that the set of fields that make up an + # attribute is unique. The fact that the id of an attribute is not unique in a + # resolved schema is related to the overload mechanism supported by the + # telemetry schema component. This process is ensured at the time of the schema + # resolution process. + # Note: A reference to an attribute defined in this catalog is defined in terms + # of the numerical position of the corresponding attribute in the catalog. + attributes: + # Array of fully resolved and qualified attributes + - name: # id of the most recent version + type: + # ... + # other attributes fields + # ... + + # This field is used when versioning has been implemented for this + # attribute. It is calculated by the resolution process to simplify the + # exploitation of versioning by consumers of resolved telemetry schemas. + versions: + : + rename_to: + + +# This optional section contains one or more semantic convention registries of +# attributes, spans, metrics, etc., groups. This section only exists when the +# schema resolution process has been applied to one or more registries (e.g., +# the official and standard OTel registry, and an internal registry of a +# company complementing that of OTel). The `ref` and 'extend' clauses present +# in the initial registry are all resolved and should therefore no longer +# appear here. Only internal references to the attribute catalog are used. +registries: + # Registry definition + - registry_url: + groups: + - id: + type: + attributes: + - # position in the catalog of attributes + - <...> + # ... + # other group fields (except `ref` and `extends` which have been + # resolved) + # ... + + # This optional field tracks the provenance and the various + # transformations that have been applied to the attribute during the + # resolution process within the current group. If originally the + # attribute was defined by a reference with some fields locally + # overridden, the provenance and override operations will be defined + # by the lineage. If the attribute comes from an extends clause, then + # the lineage will contain the provenance and the reference of the + # extends. The exact definition of the lineage field will be detailed + # in the OTEP describing the format of the resolved schemas. + # This field is present only upon request during the resolution process. + # By default, this field is not present. + lineage: + provenance: + attributes: # will be defined in a future OTEP + +# The resource field is defined when the resolved schema is that of an +# application (as opposed to that of a library). The resource field contains a +# list of local references to attributes defined in the shared catalog within +# this file. +resource: + attributes: + - # position in the catalog of attributes + +# This optional section defines the instrumentation library, its version, and +# the schema of OTel entities reported by this instrumentation library. This +# section is mandatory if the origin of this resolved schema is a telemetry +# schema component (i.e. application or library). Therefore, this section does +# not exist for the resolution of a registry. +instrumentation_library: + name: + version: + # The schema details all the metrics, logs, and spans specifically generated + # by that instrumentation library. + schema: + # Declaration of all the univariate metrics + resource_metrics: + - metric_name: + attributes: + - # position in the catalog of attributes + # ... + # other metric fields + # ... + tags: + : + versions: # optional versioning for the metric. + + # Declaration of all the spans + resource_spans: + - span_name: + attributes: + - # position in the catalog of attributes + # ... + # other span fields + # ... + tags: + : + versions: # optional versioning for the span. + +# This optional section contains the definition of the resolved telemetry +# schema for each dependency of the currently instrumented component +# (application or library). The schema resolution process collects the resolved +# telemetry schemas of the component's dependencies, merges the attribute +# catalog, and adds the instrumentation library of the dependency with all the +# definitions of metrics, logs, spans it contains while adapting the attribute +# references to point to the local catalog. +dependencies: + - name: + version: + # The schema details all the metrics, logs, and spans specifically generated + # by that instrumentation library (i.e. a dependency in this context). + schema: # same structure as the instrumentation_library section (see above) \ No newline at end of file diff --git a/docs/resolved-telemetry-schema.md b/docs/resolved-telemetry-schema.md new file mode 100644 index 00000000..b8eb2b25 --- /dev/null +++ b/docs/resolved-telemetry-schema.md @@ -0,0 +1,189 @@ +# Resolved Telemetry Schema + +A Resolved Telemetry Schema is the outcome of the schema resolution process. +This process involves taking the entire hierarchy of Telemetry Schemas and +Semantic Convention Registries and applying a set of rules to resolve overrides +and eliminate external references. The key design principles to be followed in +the definition of the Resolved Telemetry Schema are: + +* **Self-contained**: No external references are allowed. This artifact contains + everything required to determine what an application or a library produces in + terms of telemetry. +* **Easy to exchange**: This artifact must be easily accessible from a web + server via a URL. This artifact must be small and avoid the repetition of + definitions. +* **Easy to parse**: A widespread and well-defined format should be preferred. + JSON is an example of such a format. +* **Easy to interpret**: The internal structure of this artifact must be + straightforward to avoid any misinterpretation and must be efficient. +* **Platform- and Language-agnostic**: This artifact must be independent of any + platform architectures and programming languages. + +The following diagram describes two main use cases for the Resolved Telemetry +Schema. The key points to remember are: 1) both use cases result in a Resolved +Telemetry Schema, 2) Resolved Telemetry Schemas serve as the mechanism for +distributing Telemetry Schemas throughout the entire ecosystem, and 3) Resolved +Telemetry Schemas would replace/augment existing SchemaURL. + +![Use cases](./images/0240-otel-weaver-use-cases.svg) + +The main components of a Resolved Telemetry Schema are illustrated in the +diagram below. The 'OTel Weaver' tool is used to create these schemas. It can +also extend an existing schema or import a Semantic Convention Registry. +Resolved Telemetry Schema serves as a key mechanism for interoperability, +feeding various external tools, including SDK generators, documentation +generators, policy enforcers, and more. + +![Resolved Telemetry Schema](./images/0240-otel-weaver-resolved-schema.svg) + +The internal catalog is used to define all the attributes and metrics in this +artifact. This design allows for the reuse of the same attributes or metrics +multiple times in different signals and different instrumentation libraries. It +is expected to be a very common pattern to reuse the same subset of attributes +or metrics across several signals and libraries. + +## Structure + +The structure of the resolved telemetry schema is given here as an example and +corresponds to the author's vision (not yet validated) of what the structure of +a resolved telemetry schema could be. The definitive structure and format of +this resolved schema will be discussed and finalized later in a dedicated OTEP. + +```yaml +# This file describes the general logical structure envisioned to date for a +# resolved telemetry schema. The precise definition of the structure and format +# of a resolved telemetry schema is the subject of a dedicated OTEP, which does +# not yet exist at the time of writing the current OTEP. The format used here +# is YAML, but another format such as JSON, Protobuf, or another could +# ultimately be chosen based on considerations of efficiency and ease of +# integration. +# The telemetry metadata described in this file is self-contained and describes +# the entirety of telemetry metadata for either: +# - one or more semantic convention registries +# - an application or a service +# - a library +file_format: 1.2.0 +schema_url: + +catalog: + # The attribute catalog is the place where the fields of attributes are defined + # precisely. The other sections of the Resolved Telemetry Schema refer to the + # catalog when they need to "attach" one or more attributes to an OTel entity + # (e.g., resource, metric, span, ...). Within the catalog, each attribute + # definition is unique. This does not mean that the id of the attributes is + # unique within the catalog. It means that the set of fields that make up an + # attribute is unique. The fact that the id of an attribute is not unique in a + # resolved schema is related to the overload mechanism supported by the + # telemetry schema component. This process is ensured at the time of the schema + # resolution process. + # Note: A reference to an attribute defined in this catalog is defined in terms + # of the numerical position of the corresponding attribute in the catalog. + attributes: + # Array of fully resolved and qualified attributes + - name: # id of the most recent version + type: + # ... + # other attributes fields + # ... + + # This field is used when versioning has been implemented for this + # attribute. It is calculated by the resolution process to simplify the + # exploitation of versioning by consumers of resolved telemetry schemas. + versions: + : + rename_to: + + +# This optional section contains one or more semantic convention registries of +# attributes, spans, metrics, etc., groups. This section only exists when the +# schema resolution process has been applied to one or more registries (e.g., +# the official and standard OTel registry, and an internal registry of a +# company complementing that of OTel). The `ref` and 'extend' clauses present +# in the initial registry are all resolved and should therefore no longer +# appear here. Only internal references to the attribute catalog are used. +registries: + # Registry definition + - registry_url: + groups: + - id: + type: + attributes: + - # position in the catalog of attributes + - <...> + # ... + # other group fields (except `ref` and `extends` which have been + # resolved) + # ... + + # This optional field tracks the provenance and the various + # transformations that have been applied to the attribute during the + # resolution process within the current group. If originally the + # attribute was defined by a reference with some fields locally + # overridden, the provenance and override operations will be defined + # by the lineage. If the attribute comes from an extends clause, then + # the lineage will contain the provenance and the reference of the + # extends. The exact definition of the lineage field will be detailed + # in the OTEP describing the format of the resolved schemas. + # This field is present only upon request during the resolution process. + # By default, this field is not present. + lineage: + provenance: + attributes: # will be defined in a future OTEP + +# The resource field is defined when the resolved schema is that of an +# application (as opposed to that of a library). The resource field contains a +# list of local references to attributes defined in the shared catalog within +# this file. +resource: + attributes: + - # position in the catalog of attributes + +# This optional section defines the instrumentation library, its version, and +# the schema of OTel entities reported by this instrumentation library. This +# section is mandatory if the origin of this resolved schema is a telemetry +# schema component (i.e. application or library). Therefore, this section does +# not exist for the resolution of a registry. +instrumentation_library: + name: + version: + # The schema details all the metrics, logs, and spans specifically generated + # by that instrumentation library. + schema: + # Declaration of all the univariate metrics + resource_metrics: + - metric_name: + attributes: + - # position in the catalog of attributes + # ... + # other metric fields + # ... + tags: + : + versions: # optional versioning for the metric. + + # Declaration of all the spans + resource_spans: + - span_name: + attributes: + - # position in the catalog of attributes + # ... + # other span fields + # ... + tags: + : + versions: # optional versioning for the span. + +# This optional section contains the definition of the resolved telemetry +# schema for each dependency of the currently instrumented component +# (application or library). The schema resolution process collects the resolved +# telemetry schemas of the component's dependencies, merges the attribute +# catalog, and adds the instrumentation library of the dependency with all the +# definitions of metrics, logs, spans it contains while adapting the attribute +# references to point to the local catalog. +dependencies: + - name: + version: + # The schema details all the metrics, logs, and spans specifically generated + # by that instrumentation library (i.e. a dependency in this context). + schema: # same structure as the instrumentation_library section (see above) +``` diff --git a/docs/telemetry-schema-v1.2.0.md b/docs/telemetry-schema-v1.2.0.md new file mode 100644 index 00000000..718b65a3 --- /dev/null +++ b/docs/telemetry-schema-v1.2.0.md @@ -0,0 +1,179 @@ +# Telemetry Schema v1.2.0 + +```yaml +# This annotated YAML document describes version 1.2.0 of the OpenTelemetry +# telemetry schema structure. This version 1.2.0 is backward compatible with +# version 1.1.0. Version 1.2.0 introduces many new concepts aimed at +# describing: +# - the telemetry signals that an application, service, device, or library can +# produce (either authored by a developer or produced following a reference +# resolution mechanism to produce self-contained version). +# - a resolved catalog of attributes and signals defined in a semantic +# convention registry in an easily exchangeable and consumable form by other +# tools. +file_format: 1.2.0 +schema_url: +# Optional field used to define an inheritance relationship between two +# schemas. The current schema extends the one in the following URL. +# Presence: This field does not exist when the current schema is a resolved +# schema. +extends: + +# Optional section used to import one or several semantic convention +# registries. +# Presence: This section does not exist when the current schema is a resolved +# schema. +import_semantic_convention_registries: + # A semantic convention registry can be imported from a git repository + - git_url: + # Optional path to the directory containing the semantic convention + # registry. If not specified, the root of the git repository is used. + path: + # A semantic convention registry can also be imported from one or several + # files composing a registry. + - url: + +# Optional section used to describe the attributes of an OpenTelemetry +# resource. This section applies only if the current schema is used to describe +# the signals of a component using a client SDK. +# This section must not contain any external references if the current schema +# is resolved. +# Presence: This section does not exist when the current schema does not belong +# to a deployable component such as an application or a service. +resource: + # attributes defined locally or inherited from the parent schema (if any) or + # from the semantic conventions (if any). + attributes: # List of attribute definitions + # A new attribute definition + - id: + # other field definitions, see semantic convention file format. + # A attribute definition that overrides a previously defined attribute + - ref: + # other field definitions, see semantic convention file format. + # A reference to an attribute defined in the shared catalog within this + # file. + - lref: + +# Section used to define the instrumentation library, its version, and the +# schema of OpenTelemetry signals reported by an application, a service, a +# device, or a library. +# Presence: This section doesn't exist when the current schema does not belong +# to an application, a service, a device, or a library. +instrumentation_library: + name: + version: + # Section describing the telemetry signals produced by the current component + # (i.e. metrics, logs, events, and spans). + schema: + # Declaration of all the univariate metrics + resource_metrics: + - metric_name: + # attributes defined locally or inherited from the parent schema (if any) or + # from the semantic conventions (if any). + attributes: # attribute definitions + # ... + # other metric fields + # ... + tags: + : + versions: # optional versioning for the metric. + + # Declaration of all the spans + resource_spans: + - span_name: + # attributes defined locally or inherited from the parent schema (if any) or + # from the semantic conventions (if any). + attributes: # attribute definitions + # ... + # other span fields + # ... + tags: + : + versions: # optional versioning for the metric. + +catalog: + # The attribute catalog is the place where the fields of attributes are defined + # precisely. The other sections of the Resolved Telemetry Schema refer to the + # catalog when they need to "attach" one or more attributes to an OTel entity + # (e.g., resource, metric, span, ...). Within the catalog, each attribute + # definition is unique. This does not mean that the id of the attributes is + # unique within the catalog. It means that the set of fields that make up an + # attribute is unique. The fact that the id of an attribute is not unique in a + # resolved schema is related to the overload mechanism supported by the + # telemetry schema component. This process is ensured at the time of the schema + # resolution process. + # Note: A reference to an attribute defined in this catalog is defined in terms + # of the numerical position of the corresponding attribute in the catalog. + attributes: + # Array of fully resolved and qualified attributes + - name: # id of the most recent version + type: + # ... + # other attributes fields + # ... + + # This field is used when versioning has been implemented for this + # attribute. It is calculated by the resolution process to simplify the + # exploitation of versioning by consumers of resolved telemetry schemas. + versions: + : + rename_to: + + +# This optional section contains one or more semantic convention registries of +# attributes, spans, metrics, etc., groups. This section only exists when the +# schema resolution process has been applied to one or more registries (e.g., +# the official and standard OTel registry, and an internal registry of a +# company complementing that of OTel). The `ref` and 'extend' clauses present +# in the initial registry are all resolved and should therefore no longer +# appear here. Only internal references to the attribute catalog are used. +registries: + # Registry definition + - registry_url: + groups: + - id: + type: + attributes: + - # position in the catalog of attributes + - <...> + # ... + # other group fields (except `ref` and `extends` which have been + # resolved) + # ... + + # This optional field tracks the provenance and the various + # transformations that have been applied to the attribute during the + # resolution process within the current group. If originally the + # attribute was defined by a reference with some fields locally + # overridden, the provenance and override operations will be defined + # by the lineage. If the attribute comes from an extends clause, then + # the lineage will contain the provenance and the reference of the + # extends. The exact definition of the lineage field will be detailed + # in the OTEP describing the format of the resolved schemas. + # This field is present only upon request during the resolution process. + # By default, this field is not present. + lineage: + provenance: + attributes: # will be defined in a future OTEP + +# This optional section contains the definition of the resolved telemetry +# schema for each dependency of the currently instrumented component +# (application or library). The schema resolution process collects the resolved +# telemetry schemas of the component's dependencies, merges the attribute +# catalog, and adds the instrumentation library of the dependency with all the +# definitions of metrics, logs, spans it contains while adapting the attribute +# references to point to the local catalog. +dependencies: + - name: + version: + # The schema details all the metrics, logs, and spans specifically generated + # by that instrumentation library (i.e. a dependency in this context). + schema: # same structure as the instrumentation_library section (see above) + +# Optional section used to define a list of transformations to apply between +# versions. +# Presence: This section doesn't exist when the current schema is a resolved +# schema. +versions: + # Same structure as in telemetry schema v1.1.0. +``` \ No newline at end of file diff --git a/docs/template.md b/docs/template.md new file mode 100644 index 00000000..b86df8ac --- /dev/null +++ b/docs/template.md @@ -0,0 +1,51 @@ +# Template + +## Description + +Based on the template engine [Tera](https://keats.github.io/tera/) (inspired by Jinja2 and Django templates). + +## Custom Filters + +format filters + +### file_name +Format the value as a file name based on the config.yaml file +defined in the root of the template directory for a given language. + +### function_name +Format the value as a function name based on the config.yaml file +defined in the root of the template directory for a given language. + +### arg_name +Format the value as a function argument name based on the config.yaml file +defined in the root of the template directory for a given language. + +### struct_name +Format the value as a struct name based on the config.yaml file +defined in the root of the template directory for a given language. + +### field_name +Format the value as a field name based on the config.yaml file +defined in the root of the template directory for a given language. + +### unique_attributes + +ToDo attributes(recursive=true, required=true, unique=true, not_required=true, with_value=true, without_value=true) + +### instrument +### required +### not_required +### value +### with_value +### without_value +### comment +### type_mapping + +## Custom Functions + +config + +## Customer Testers + +required +not_required diff --git a/examples/ex1.rs b/examples/ex1.rs new file mode 100644 index 00000000..0b65a7ba --- /dev/null +++ b/examples/ex1.rs @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Example 1 + +// use crate::otel::meter::{http, HttpAttrs, HttpMetrics, JvmThreadCountAttrs}; +// use crate::otel::tracer::{HttpRequestAttrs, HttpRequestEvent, HttpRequestOptAttrs, Status}; + +// mod otel; + +fn main() { + // // Starts a new span with the required attributes. + // // todo 2 start impl: 1) with, 2) without required attributes + // let mut span1 = otel::tracer::start_http_request( + // HttpRequestAttrs { + // url_host: "localhost".to_string(), + // }); + // + // // Specifies some optional attributes. + // span1.attr_url_scheme("https".to_string()); + // span1.attr_client_port(443); + // + // // Add an event to the span. + // span1.event(HttpRequestEvent::Error { + // exception_type: None, + // exception_message: Some("an error message".into()), + // exception_stacktrace: None, + // }); + // + // // Set the status of the span. + // span1.status(Status::Ok); + // // End the span. After this call, the span is not longer + // // accessible. + // span1.end(); + // + // // At this point, any reference to the span1 will result in a compiler + // // error. + // + // // ======================================================================== + // // Starts a new span with the required attributes. + // let mut span2 = otel::tracer::start_http_request( + // HttpRequestAttrs { + // url_host: "localhost".to_string(), + // }); + // span2.event(HttpRequestEvent::Error { + // exception_type: None, + // exception_message: None, + // exception_stacktrace: None, + // }); + // span2.status(Status::Ok); + // // End the span with optional attributes. + // span2.end_with_opt_attrs(HttpRequestOptAttrs { + // url_scheme: Some("https".to_string()), + // client_port: Some(443), + // ..Default::default() + // }); + // + // // ======================================================================== + // // Reports an HTTP Request event. + // otel::eventer::event_http_request(otel::eventer::HttpRequestAttrs { + // server_address: Some("localhost".to_string()), + // server_port: Some(443), + // network_protocol_name: Some("http".to_string()), + // network_protocol_version: None, + // url_scheme: None, + // url_host: "".to_string(), + // }); + // // ======================================================================== + // // Reports an HTTP Response event. + // otel::eventer::event_http_response(otel::eventer::HttpResponseAttrs { + // server_address: Some("localhost".to_string()), + // server_port: Some(443), + // http_response_status_code: Some(200), + // network_protocol_name: Some("http".to_string()), + // network_protocol_version: None, + // url_scheme: None, + // url_host: "".to_string(), + // }); + // + // + // // ======================================================================== + // // Example of univariate metrics. + // // todo otel::meter::new_jvm_thread_count + // let mut jvm_thread_count = otel::meter::jvm_thread_count_u64(); + // jvm_thread_count.add(10, JvmThreadCountAttrs { thread_daemon: Some(true) }); + // + // let mut http_server = otel::meter::http_server_request_duration_f64(); + // http_server.record(10.0, otel::meter::HttpServerRequestDurationAttrs { + // server_address: Some("localhost".to_string()), + // server_port: Some(443), + // http_response_status_code: Some(200), + // network_protocol_name: Some("http".to_string()), + // network_protocol_version: None, + // url_scheme: None, + // }); + // + // // ======================================================================== + // // Example of multivariate metrics. + // // todo otel::meter::new_http + // // todo check concept of metric_group + // let mut http = otel::meter::http(); + // http.report( + // HttpMetrics { + // jvm_thread_count: 10, + // jvm_class_loaded: 50, + // jvm_cpu_recent_utilization: 60, + // }, + // HttpAttrs { + // server_address: Some("localhost".into()), + // server_port: Some(8080), + // http_response_status_code: None, + // network_protocol_name: None, + // network_protocol_version: None, + // url_scheme: None, + // url_host: "".to_string(), + // }, + // ); +} diff --git a/justfile b/justfile new file mode 100644 index 00000000..c12a126a --- /dev/null +++ b/justfile @@ -0,0 +1,20 @@ +default: pre-push + +install: + cargo install cargo-machete + cargo install cargo-depgraph + cargo install cargo-edit + +pre-push-check: + cargo update + cargo machete + cargo fmt --all + cargo clippy --workspace --all-features --all-targets -- -D warnings --allow deprecated + cargo test --all + cargo doc --workspace --all-features --no-deps --document-private-items + +pre-push: pre-push-check + cargo depgraph --workspace-only | dot -Tsvg > docs/images/dependencies.svg + +upgrade: + cargo upgrade diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..a996f270 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly" +components = [ "rustfmt" ] \ No newline at end of file diff --git a/schemas/telemetry-schema-1.yml b/schemas/telemetry-schema-1.yml new file mode 100644 index 00000000..3c5241fe --- /dev/null +++ b/schemas/telemetry-schema-1.yml @@ -0,0 +1,229 @@ +file_format: 1.1.0 +# Inherit from the OpenTelemetry schema v1.21.0 +parent_schema_url: https://opentelemetry.io/schemas/1.21.0 +# Current schema url +schema_url: https://mycompany.com/schemas/1.21.0 + +# Semantic Convention Imports +semantic_conventions: + - url: https://github.com/open-telemetry/semantic-conventions/blob/main/model/http-common.yaml + - url: https://github.com/open-telemetry/semantic-conventions/blob/main/model/server.yaml + - url: https://github.com/open-telemetry/semantic-conventions/blob/main/model/network.yaml + +schema: + # Attributes inherited by all resource types + resource: + attributes: + - ref: service.name + value: "my-service" + - ref: service.version + value: "{{SERVICE_VERSION}}" + + # Section instrumentation library + # TBD + + # Metrics declaration + resource_metrics: + attributes: + - id: environment + type: string + brief: The environment in which the service is running + tag: sensitive-information + requirement_level: required + metrics: + - ref: metric.http.server.duration + attributes: + - ref: server.address + - ref: server.port + - ref: http.request.method + - ref: http.response.status_code + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + metrics_groups: + - id: http # name of a group of metrics + attributes: + - ref: server.address + - ref: server.port + - ref: http.request.method + - ref: http.response.status_code + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + metrics: # metrics sharing the same attributes + - ref: metric.http.server.duration + - ref: metric.http.server.active_requests + - ref: metric.http.server.request.size + - ref: metric.http.server.response.size + + # Events declaration + resource_events: + events: + http: # name of a specific meter + attributes: + - ref: server.address + - ref: server.port + - ref: http.request.method + - ref: http.response.status_code + - ref: network.protocol.name + - ref: network.protocol.version + - ref: url.scheme + + # Spans declaration + resource_spans: + spans: + http.request: # name of a specific tracer + attributes: + - ref: server.address + - ref: server.port + - ref: server.socket.address + - ref: server.socket.port + - ref: client.address + - ref: client.port + - ref: client.socket.address + - ref: client.socket.port + - ref: url.schema + events: + error: + attributes: + - ref: exception.type + - ref: exception.message + - ref: exception.stacktrace + # links: + +versions: + 1.21.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3336 + - rename_attributes: + attribute_map: + messaging.kafka.client_id: messaging.client_id + messaging.rocketmq.client_id: messaging.client_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3402 + - rename_attributes: + attribute_map: + # net.peer.(name|port) attributes were usually populated on client side + # so they should be usually translated to server.(address|port) + # net.host.* attributes were only populated on server side + net.host.name: server.address + net.host.port: server.port + # was only populated on client side + net.sock.peer.name: server.socket.domain + # net.sock.peer.(addr|port) mapping is not possible + # since they applied to both client and server side + # were only populated on server side + net.sock.host.addr: server.socket.address + net.sock.host.port: server.socket.port + http.client_ip: client.address + # https://github.com/open-telemetry/opentelemetry-specification/pull/3426 + - rename_attributes: + attribute_map: + net.protocol.name: network.protocol.name + net.protocol.version: network.protocol.version + net.host.connection.type: network.connection.type + net.host.connection.subtype: network.connection.subtype + net.host.carrier.name: network.carrier.name + net.host.carrier.mcc: network.carrier.mcc + net.host.carrier.mnc: network.carrier.mnc + net.host.carrier.icc: network.carrier.icc + # https://github.com/open-telemetry/opentelemetry-specification/pull/3355 + - rename_attributes: + attribute_map: + http.method: http.request.method + http.status_code: http.response.status_code + http.scheme: url.scheme + http.url: url.full + http.request_content_length: http.request.body.size + http.response_content_length: http.response.body.size + metrics: + changes: + # https://github.com/open-telemetry/semantic-conventions/pull/53 + - rename_metrics: + process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization + 1.20.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3272 + - rename_attributes: + attribute_map: + net.app.protocol.name: net.protocol.name + net.app.protocol.version: net.protocol.version + 1.19.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3209 + - rename_attributes: + attribute_map: + faas.execution: faas.invocation_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3188 + - rename_attributes: + attribute_map: + faas.id: cloud.resource_id + # https://github.com/open-telemetry/opentelemetry-specification/pull/3190 + - rename_attributes: + attribute_map: + http.user_agent: user_agent.original + resources: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/3190 + - rename_attributes: + attribute_map: + browser.user_agent: user_agent.original + 1.18.0: + 1.17.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2957 + - rename_attributes: + attribute_map: + messaging.consumer_id: messaging.consumer.id + messaging.protocol: net.app.protocol.name + messaging.protocol_version: net.app.protocol.version + messaging.destination: messaging.destination.name + messaging.temp_destination: messaging.destination.temporary + messaging.destination_kind: messaging.destination.kind + messaging.message_id: messaging.message.id + messaging.conversation_id: messaging.message.conversation_id + messaging.message_payload_size_bytes: messaging.message.payload_size_bytes + messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes + messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key + messaging.kafka.message_key: messaging.kafka.message.key + messaging.kafka.partition: messaging.kafka.destination.partition + messaging.kafka.tombstone: messaging.kafka.message.tombstone + messaging.rocketmq.message_type: messaging.rocketmq.message.type + messaging.rocketmq.message_tag: messaging.rocketmq.message.tag + messaging.rocketmq.message_keys: messaging.rocketmq.message.keys + messaging.kafka.consumer_group: messaging.kafka.consumer.group + 1.16.0: + 1.15.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2743 + - rename_attributes: + attribute_map: + http.retry_count: http.resend_count + 1.14.0: + 1.13.0: + spans: + changes: + # https://github.com/open-telemetry/opentelemetry-specification/pull/2614 + - rename_attributes: + attribute_map: + net.peer.ip: net.sock.peer.addr + net.host.ip: net.sock.host.addr + 1.12.0: + 1.11.0: + 1.10.0: + 1.9.0: + 1.8.0: + spans: + changes: + - rename_attributes: + attribute_map: + db.cassandra.keyspace: db.name + db.hbase.namespace: db.name + 1.7.0: + 1.6.1: + 1.5.0: + 1.4.0: \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 00000000..a84aac26 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Manage command line arguments + +use crate::gen_client::GenClientCommand; +use crate::languages::LanguagesParams; +use crate::resolve::ResolveCommand; +use crate::search::SearchCommand; +use clap::{Parser, Subcommand}; + +/// Command line arguments. +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + /// Turn debugging information on + #[arg(short, long, action = clap::ArgAction::Count)] + pub debug: u8, + + /// List of supported commands + #[command(subcommand)] + pub command: Option, +} + +/// Supported commands. +#[derive(Subcommand)] +pub enum Commands { + /// Resolve a semantic convention registry or a telemetry schema + Resolve(ResolveCommand), + /// Generate a client SDK or client API + GenClient(GenClientCommand), + /// List all supported languages + Languages(LanguagesParams), + /// Search in a semantic convention registry or a telemetry schema + Search(SearchCommand), +} diff --git a/src/gen_client.rs b/src/gen_client.rs new file mode 100644 index 00000000..378e32a1 --- /dev/null +++ b/src/gen_client.rs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Command to generate a client SDK. + +use std::path::PathBuf; + +use clap::Parser; + +use weaver_logger::Logger; +use weaver_template::sdkgen::ClientSdkGenerator; +use weaver_template::GeneratorConfig; + +/// Parameters for the `gen-client-sdk` command +#[derive(Parser)] +pub struct GenClientCommand { + /// Schema file to resolve + #[arg(short, long, value_name = "FILE")] + schema: PathBuf, + + /// Language to generate the client SDK for + #[arg(short, long)] + language: String, + + /// Output directory where the client API will be generated + #[arg(short, long, value_name = "DIR")] + output_dir: PathBuf, +} + +/// Generate a client SDK (application) +pub fn command_gen_client(log: impl Logger + Sync + Clone, params: &GenClientCommand) { + log.loading(&format!( + "Generating client SDK for language {}", + params.language + )); + let generator = match ClientSdkGenerator::try_new(¶ms.language, GeneratorConfig::default()) + { + Ok(gen) => gen, + Err(e) => { + log.error(&format!("{}", e)); + std::process::exit(1); + } + }; + + generator + .generate( + log.clone(), + params.schema.clone(), + params.output_dir.clone(), + ) + .map_err(|e| { + log.error(&format!("{}", e)); + std::process::exit(1); + }) + .unwrap(); + + log.success("Generated client SDK"); +} diff --git a/src/languages.rs b/src/languages.rs new file mode 100644 index 00000000..aa91bb42 --- /dev/null +++ b/src/languages.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Command to list the supported languages + +use clap::Parser; +use std::path::PathBuf; + +use weaver_logger::Logger; + +/// Parameters for the `languages` command +#[derive(Parser)] +pub struct LanguagesParams { + /// Template root directory + #[arg(short, long, default_value = "templates")] + templates: PathBuf, +} + +/// List of supported languages +pub fn command_languages(log: impl Logger + Sync + Clone, params: &LanguagesParams) { + // List all directories in the templates directory + log.log("List of supported languages:"); + let template_dir = match std::fs::read_dir(¶ms.templates) { + Ok(dir) => dir, + Err(e) => { + log.error(&format!("Failed to read templates directory: {}", e)); + std::process::exit(1); + } + }; + for entry in template_dir { + if let Ok(entry) = entry { + if entry.file_type().is_ok() { + log.indent(1); + log.log(&format!("- {}", entry.file_name().to_str().unwrap())); + } + } else { + log.error("Failed to read template directory entry"); + std::process::exit(1); + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000..e9883783 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,34 @@ +use clap::Parser; + +use weaver_logger::ConsoleLogger; + +use crate::cli::{Cli, Commands}; +use crate::gen_client::command_gen_client; +use crate::resolve::command_resolve; + +mod cli; +mod gen_client; +mod languages; +mod resolve; +mod search; + +fn main() { + let cli = Cli::parse(); + let log = ConsoleLogger::new(cli.debug); + + match &cli.command { + Some(Commands::Resolve(params)) => { + command_resolve(log, params); + } + Some(Commands::GenClient(params)) => { + command_gen_client(log, params); + } + Some(Commands::Languages(params)) => { + languages::command_languages(log, params); + } + Some(Commands::Search(params)) => { + search::command_search(log, params); + } + None => {} + } +} diff --git a/src/resolve.rs b/src/resolve.rs new file mode 100644 index 00000000..e774d368 --- /dev/null +++ b/src/resolve.rs @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Command to resolve a schema file, then output and display the results on the console. + +use clap::{Args, Subcommand}; +use std::path::PathBuf; +use std::process::exit; +use weaver_cache::Cache; + +use weaver_logger::Logger; +use weaver_resolver::SchemaResolver; +use weaver_schema::SemConvImport; +use weaver_semconv::ResolverConfig; + +/// Specify the `resolve` command +#[derive(Args)] +pub struct ResolveCommand { + /// Define the sub-commands for the `resolve` command + #[clap(subcommand)] + pub command: ResolveSubCommand, +} + +/// Sub-commands for the `resolve` command +#[derive(Subcommand)] +pub enum ResolveSubCommand { + /// Resolve a semantic convention registry + Registry(ResolveRegistry), + /// Resolve a telemetry schema + Schema(ResolveSchema), +} + +/// Parameters for the `resolve registry` sub-command +#[derive(Args)] +pub struct ResolveRegistry { + /// Registry to resolve + pub registry: String, + + /// Optional path in the git repository where the semantic convention + /// registry is located + pub path: Option, + + /// Output file to write the resolved schema to + /// If not specified, the resolved schema is printed to stdout + #[arg(short, long)] + pub output: Option, +} + +/// Parameters for the `resolve schema` sub-command +#[derive(Args)] +pub struct ResolveSchema { + /// Schema file to resolve + pub schema: PathBuf, + + /// Output file to write the resolved schema to + /// If not specified, the resolved schema is printed to stdout + #[arg(short, long)] + pub output: Option, +} + +/// Resolve a schema file and print the result +pub fn command_resolve(log: impl Logger + Sync + Clone, command: &ResolveCommand) { + let cache = Cache::try_new().unwrap_or_else(|e| { + log.error(&e.to_string()); + std::process::exit(1); + }); + match command.command { + ResolveSubCommand::Registry(ref command) => { + let mut registry = SchemaResolver::semconv_registry_from_imports( + &[SemConvImport::GitUrl { + git_url: command.registry.clone(), + path: command.path.clone(), + }], + ResolverConfig::with_keep_specs(), + &cache, + log.clone(), + ) + .unwrap_or_else(|e| { + log.error(&e.to_string()); + exit(1); + }); + + let resolved_schema = + SchemaResolver::resolve_semantic_convention_registry(&mut registry, log.clone()) + .unwrap_or_else(|e| { + log.error(&e.to_string()); + exit(1); + }); + match serde_yaml::to_string(&resolved_schema) { + Ok(yaml) => { + if let Some(output) = &command.output { + log.loading(&format!( + "Saving resolved registry to {}", + output + .to_str() + .unwrap_or("") + )); + if let Err(e) = std::fs::write(output, &yaml) { + log.error(&format!( + "Failed to write to {}: {}", + output.to_str().unwrap(), + e + )); + exit(1) + } + log.success(&format!( + "Saved resolved registry to '{}'", + output + .to_str() + .unwrap_or("") + )); + } else { + log.log(&yaml); + } + } + Err(e) => { + log.error(&format!("{}", e)); + exit(1) + } + } + } + ResolveSubCommand::Schema(ref command) => { + let schema = command.schema.clone(); + let schema = SchemaResolver::resolve_schema_file(schema, &cache, log.clone()); + + match schema { + Ok(schema) => match serde_yaml::to_string(&schema) { + Ok(yaml) => { + if let Some(output) = &command.output { + log.loading(&format!( + "Saving resolved schema to {}", + output + .to_str() + .unwrap_or("") + )); + if let Err(e) = std::fs::write(output, &yaml) { + log.error(&format!( + "Failed to write to {}: {}", + output.to_str().unwrap(), + e + )); + exit(1) + } + log.success(&format!( + "Saved resolved schema to '{}'", + output + .to_str() + .unwrap_or("") + )); + } else { + log.log(&yaml); + } + } + Err(e) => { + log.error(&format!("{}", e)); + exit(1) + } + }, + Err(e) => { + log.error(&format!("{}", e)); + exit(1) + } + } + } + } +} diff --git a/src/search/mod.rs b/src/search/mod.rs new file mode 100644 index 00000000..ea826ffa --- /dev/null +++ b/src/search/mod.rs @@ -0,0 +1,765 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Command to generate a client SDK. + +use std::io; +use std::path::PathBuf; + +use clap::{Args, Subcommand}; +use crossterm::event::DisableMouseCapture; +use crossterm::event::EnableMouseCapture; +use crossterm::{ + event::{self, KeyCode, KeyEventKind}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::layout::{Constraint, Direction, Layout}; +use ratatui::prelude::{CrosstermBackend, Span, Terminal}; +use ratatui::style::{Color, Style, Stylize}; +use ratatui::text::Line; +use ratatui::widgets::Cell; +use ratatui::widgets::{Block, Borders, Paragraph, Row, Table, TableState, Wrap}; +use ratatui::Frame; +use tantivy::collector::TopDocs; +use tantivy::query::QueryParser; +use tantivy::schema::{Field, Schema, STORED, TEXT}; +use tantivy::{Index, IndexWriter, ReloadPolicy}; +use tui_textarea::TextArea; + +use theme::ThemeConfig; +use weaver_cache::Cache; +use weaver_logger::Logger; +use weaver_resolver::attribute::AttributeCatalog; +use weaver_resolver::registry::{resolve_registry, unresolved_registry_from_specs}; +use weaver_resolver::SchemaResolver; +use weaver_schema::attribute::Attribute; +use weaver_schema::TelemetrySchema; + +use crate::search::schema::{attribute, metric, metric_group, resource, span}; + +mod schema; +mod semconv; +mod theme; + +type Err = Box; +type Result = std::result::Result; + +/// Parameters for the `search` command +#[derive(Debug, Args)] +pub struct SearchCommand { + /// Define the sub-commands for the `search` command + #[clap(subcommand)] + pub command: SearchSubCommand, +} + +/// Sub-commands for the `search` command +#[derive(Debug, Subcommand)] +pub enum SearchSubCommand { + /// Search in a semantic convention registry + Registry(SearchRegistry), + /// Search in a semantic convention registry [WIP, todo] + Registry2(SearchRegistry2), + /// Search in a telemetry schema + Schema(SearchSchema), +} + +/// Parameters for the `search registry` sub-command +#[derive(Debug, Args)] +pub struct SearchRegistry { + /// Git URL of the semantic convention registry + pub registry: String, + + /// Optional path in the git repository where the semantic convention + /// registry is located + pub path: Option, + + /// The telemetry schema containing the versions (url or file) + #[arg(short, long)] + schema: Option, +} + +/// Parameters for the `search registry` sub-command [WIP, todo] +#[derive(Debug, Args)] +pub struct SearchRegistry2 { + /// Git URL of the semantic convention registry + pub registry: String, + + /// Optional path in the git repository where the semantic convention + /// registry is located + pub path: Option, + + /// The telemetry schema containing the versions (url or file) + #[arg(short, long)] + schema: Option, +} + +/// Parameters for the `search schema` sub-command +#[derive(Debug, Args)] +pub struct SearchSchema { + /// Schema file to search + pub schema: PathBuf, +} + +pub struct SearchApp<'a> { + schema: TelemetrySchema, + search_area: TextArea<'a>, + + results: StatefulResults, + + searcher: tantivy::Searcher, + query_parser: QueryParser, + current_query: Option, + + should_quit: bool, + + theme: ThemeConfig, +} + +/// A result item +pub struct ResultItem { + path: String, + brief: String, +} + +/// A stateful list of items +pub struct StatefulResults { + state: TableState, + // ListState, + items: Vec, +} + +/// A struct representing all the fields in an indexed document. +pub struct DocFields { + path: Field, + brief: Field, + note: Field, + tag: Field, +} + +impl StatefulResults { + /// Creates a new stateful list of items + fn new() -> StatefulResults { + StatefulResults { + state: TableState::default(), // ListState::default(), + items: vec![], + } + } + + /// Selects the next item in the list + fn next(&mut self) { + if self.items.is_empty() { + return; + } + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + /// Selects the previous item in the list + fn previous(&mut self) { + if self.items.is_empty() { + return; + } + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + /// Unselects the current selection + fn unselect(&mut self) { + self.state.select(None); + } + + /// Clears the results + fn clear(&mut self) { + self.unselect(); + self.items.clear(); + } +} + +/// Search for attributes and metrics in a schema file +pub fn command_search(log: impl Logger + Sync + Clone, command: &SearchCommand) { + let cache = Cache::try_new().unwrap_or_else(|e| { + log.error(&e.to_string()); + std::process::exit(1); + }); + + match &command.command { + SearchSubCommand::Registry(args) => search_registry_command(log, &cache, args), + SearchSubCommand::Registry2(args) => search_registry_command2(log, &cache, args), + SearchSubCommand::Schema(args) => search_schema_command(log, &cache, args), + } +} + +/// Search semantic convention registry command [todo, WIP]. +fn search_registry_command2( + log: impl Logger + Sync + Clone + Sized, + cache: &Cache, + registry_args: &SearchRegistry2, +) { + let semconv_specs = SchemaResolver::load_semconv_registry( + registry_args.registry.clone(), + registry_args.path.clone(), + cache, + log.clone(), + ) + .unwrap_or_else(|e| { + log.error(&format!("{}", e)); + std::process::exit(1); + }); + + let mut attr_catalog = AttributeCatalog::default(); + let resolved_registry = resolve_registry( + unresolved_registry_from_specs(®istry_args.registry, &semconv_specs), + &mut attr_catalog, + ) + .unwrap_or_else(|e| { + log.error(&format!("{}", e)); + std::process::exit(1); + }); + + dbg!(resolved_registry); + //dbg!(attr_catalog); + + // let schema = if let Some(schema) = ®istry_args.schema { + // let mut schema = + // SchemaResolver::resolve_schema(schema, cache, log.clone()).unwrap_or_else(|e| { + // log.error(&format!("{}", e)); + // std::process::exit(1); + // }); + // schema.semantic_convention_registry = semconv_registry; + // schema + // } else { + // TelemetrySchema { + // file_format: "".to_string(), + // parent_schema_url: None, + // schema_url: "".to_string(), + // semantic_conventions: vec![], + // schema: None, + // versions: None, + // parent_schema: None, + // semantic_convention_registry: semconv_registry, + // } + // }; + // + // search_schema_tui(log, schema); +} + +/// Search semantic convention registry command. +fn search_registry_command( + log: impl Logger + Sync + Clone + Sized, + cache: &Cache, + registry_args: &SearchRegistry, +) { + let semconv_registry = SchemaResolver::resolve_semconv_registry( + registry_args.registry.clone(), + registry_args.path.clone(), + cache, + log.clone(), + ) + .unwrap_or_else(|e| { + log.error(&format!("{}", e)); + std::process::exit(1); + }); + + let schema = if let Some(schema) = ®istry_args.schema { + let mut schema = + SchemaResolver::resolve_schema(schema, cache, log.clone()).unwrap_or_else(|e| { + log.error(&format!("{}", e)); + std::process::exit(1); + }); + schema.semantic_convention_registry = semconv_registry; + schema + } else { + TelemetrySchema { + file_format: "".to_string(), + parent_schema_url: None, + schema_url: "".to_string(), + semantic_conventions: vec![], + schema: None, + versions: None, + parent_schema: None, + semantic_convention_registry: semconv_registry, + } + }; + + search_schema_tui(log, schema); +} + +/// Search schema command. +fn search_schema_command( + log: impl Logger + Sync + Clone + Sized, + cache: &Cache, + schema_args: &SearchSchema, +) { + let schema = + SchemaResolver::resolve_schema_file(schema_args.schema.clone(), cache, log.clone()) + .unwrap_or_else(|e| { + log.error(&format!("{}", e)); + std::process::exit(1); + }); + + search_schema_tui(log, schema); +} + +fn search_schema_tui(log: impl Logger + Sync + Clone + Sized + Sized, schema: TelemetrySchema) { + let semconv_registry = schema.semantic_convention_catalog(); + + let mut schema_builder = Schema::builder(); + let fields = DocFields { + path: schema_builder.add_text_field("path", TEXT | STORED), + brief: schema_builder.add_text_field("brief", TEXT | STORED), + note: schema_builder.add_text_field("note", TEXT), + tag: schema_builder.add_text_field("tag", TEXT), + }; + + let index_schema = schema_builder.build(); + let index = Index::create_in_ram(index_schema.clone()); + let mut index_writer: IndexWriter = index + .writer(15_000_000) + .expect("Failed to create index writer"); + + attribute::index_semconv_attributes( + semconv_registry.attributes_iter(), + "semconv", + &fields, + &mut index_writer, + ); + metric::index_semconv_metrics( + semconv_registry.metrics_iter(), + "semconv", + &fields, + &mut index_writer, + ); + resource::index(&schema, &fields, &mut index_writer); + metric::index_schema_metrics(&schema, &fields, &mut index_writer); + metric_group::index(&schema, &fields, &mut index_writer); + schema::event::index(&schema, &fields, &mut index_writer); + span::index(&schema, &fields, &mut index_writer); + + index_writer + .commit() + .expect("Failed to commit index writer"); + let reader = index + .reader_builder() + .reload_policy(ReloadPolicy::OnCommit) + .try_into() + .expect("Failed to create reader"); + let searcher = reader.searcher(); + let DocFields { + path, + brief, + note, + tag, + } = fields; + let query_parser = QueryParser::for_index(&index, vec![path, brief, note, tag]); + + let theme = ThemeConfig { + title: Color::Rgb(238, 238, 238), + border: Color::Rgb(85, 109, 89), + label: Color::Rgb(128, 208, 163), + value: Color::Rgb(204, 204, 204), + }; + + let mut search_area = TextArea::default(); + search_area.set_cursor_line_style(Style::default()); + search_area.set_placeholder_text("Enter search terms, operators, or use path:, brief:, tag:, or note: prefixes to target specific fields."); + search_area.set_block( + Block::default() + .borders(Borders::TOP) + .border_style(Style::default().fg(theme.border)) + .title("Search (press `Esc` or `Ctrl-C` to stop running) ") + .title_style(Style::default().fg(theme.title)), + ); + + // application state + let mut app = SearchApp { + schema, + search_area, + results: StatefulResults::new(), + searcher, + query_parser, + current_query: None, + should_quit: false, + theme, + }; + + search_tui(&mut app).unwrap_or_else(|e| { + log.error(&format!("{}", e)); + std::process::exit(1); + }); +} + +fn search_tui(app: &mut SearchApp<'_>) -> Result<()> { + // Startup + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + enable_raw_mode()?; + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut term = Terminal::new(backend)?; + + let status = run(app); + + // Shutdown + disable_raw_mode()?; + execute!( + term.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + term.show_cursor()?; + + status?; + Ok(()) +} + +fn ui(app: &mut SearchApp, frame: &mut Frame<'_>) { + let empty_search_box = app.search_area.is_empty(); + app.search_area.lines().iter().for_each(|query| { + if let Some(current_query) = app.current_query.as_ref() { + if current_query == query { + return; + } + } + app.current_query = Some(query.to_string()); + match app.query_parser.parse_query(query) { + Ok(query) => { + app.results.clear(); + let top_docs = app + .searcher + .search(&query, &TopDocs::with_limit(100)) + .expect("Failed to search"); + for (_score, doc_address) in top_docs { + let retrieved_doc = app + .searcher + .doc(doc_address) + .expect("Failed to retrieve document"); + let values = retrieved_doc.field_values(); + let path = values[0].value().as_text().unwrap_or_default(); + let brief = values[1].value().as_text().unwrap_or_default(); + + app.results.items.push(ResultItem { + path: path.to_string(), + brief: brief.to_string(), + }); + } + app.results.next(); + } + Err(_e) => { + app.results.clear(); + } + } + }); + + let selected_style = Style::default() + .bg(Color::Rgb(106, 47, 47)) + .fg(app.theme.title); + let normal_style = Style::default(); + let header_cells = ["Path:", "Brief:"] + .iter() + .map(|h| Cell::from(*h).style(Style::default().fg(app.theme.title))); + let header = Row::new(header_cells) + .style(normal_style) + .height(1) + .bottom_margin(0); + let rows: Vec = app + .results + .items + .iter() + .map(|item| { + let cells = vec![ + Cell::from(item.path.clone()).fg(app.theme.label), + Cell::from(item.brief.clone()).fg(app.theme.value), + ]; + Row::new(cells).height(1).bottom_margin(0) + }) + .collect(); + + let outer_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Length(2)]) + .split(frame.size()); + + let inner_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Percentage(70), Constraint::Percentage(30)]) + .split(outer_layout[0]); + + let content = Table::new(rows, [Constraint::Max(50), Constraint::Max(120)]) + .header(header) + .block( + Block::default() + //.borders(Borders::TOP.union(Borders::RIGHT)) + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.border)) + .title("Search results ") + .title_style(Style::default().fg(app.theme.value)), + ) + .highlight_style(selected_style) + .highlight_symbol(">> "); + + frame.render_stateful_widget(content, inner_layout[1], &mut app.results.state); + + // Detail area + let item = match app.results.state.selected() { + Some(i) => app.results.items.get(i), + None => None, + }; + if empty_search_box { + frame.render_widget(summary_area(app), inner_layout[0]); + } else { + frame.render_widget(detail_area(app, item), inner_layout[0]); + } + frame.render_widget(app.search_area.widget(), outer_layout[1]); +} + +fn summary_area<'a>(app: &'a SearchApp<'a>) -> Paragraph<'a> { + let area_title = "Summary"; + let semconv_catalog = app.schema.semantic_convention_catalog(); + let text = vec![ + Line::from(""), + Line::from("Telemetry schema:"), + Line::from(format!("- URL: {}", app.schema.schema_url)), + Line::from(format!("- Parent schema URL: {}", app.schema.parent_schema_url.clone().unwrap_or_default())), + Line::from(format!("- {} metrics", app.schema.metrics_count())), + Line::from(format!("- {} metric groups", app.schema.metric_groups_count())), + Line::from(format!("- {} events", app.schema.events_count())), + Line::from(format!("- {} spans", app.schema.spans_count())), + Line::from(format!("- {} versions", app.schema.version_count())), + Line::from(""), + Line::from(vec![ + Span::raw("Semantic convention catalog:"), + ]), + Line::from(vec![ + Span::raw(format!("- {} files.", semconv_catalog.asset_count())), + ]), + Line::from(vec![ + Span::raw(format!("- {} attributes.", semconv_catalog.attribute_count())), + ]), + Line::from(vec![ + Span::raw(format!("- {} metrics.", semconv_catalog.metric_count())), + ]), + Line::from(""), + Line::from(""), + Line::from(">> Enter search terms, operators, or use path:, brief:, tag:, or note: prefixes to target specific fields."), + ]; + + let paragraph = Paragraph::new(text).style(Style::default().fg(app.theme.value)); + + paragraph + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.border)) + .title(format!("{} ", area_title)) + .title_style(Style::default().fg(app.theme.title)) + .style(Style::default()), + ) + .wrap(Wrap { trim: true }) +} + +fn detail_area<'a>(app: &'a SearchApp<'a>, item: Option<&'a ResultItem>) -> Paragraph<'a> { + let mut area_title = "Details"; + let paragraph = if let Some(item) = item { + let path = item.path.as_str().split('/').collect::>(); + + match path[..] { + ["semconv", "attr", id] => { + area_title = "Semantic Convention Attribute"; + semconv::attribute::widget( + app.schema + .semantic_convention_catalog() + .attribute_with_provenance(id), + &app.theme, + ) + } + ["semconv", "metric", id] => { + area_title = "Semantic Convention Metric"; + semconv::metric::widget( + app.schema + .semantic_convention_catalog() + .metric_with_provenance(id), + &app.theme, + ) + } + ["schema", "resource", "attr", attr_id] => { + area_title = "Schema Resource Attribute"; + if let Some(resource) = app.schema.resource() { + attribute::widget( + resource.attributes.iter().find(|attr| { + if let Attribute::Id { id, .. } = attr { + id.as_str() == attr_id + } else { + false + } + }), + app.schema.schema_url.as_str(), + &app.theme, + ) + } else { + Paragraph::new(vec![Line::default()]) + } + } + ["schema", "metric", id] => { + area_title = "Schema Metric"; + metric::widget( + app.schema.metric(id), + app.schema.schema_url.as_str(), + &app.theme, + ) + } + ["schema", "metric", metric_id, "attr", attr_id] => { + area_title = "Schema Metric Attribute"; + attribute::widget( + app.schema + .metric(metric_id) + .iter() + .flat_map(|m| m.attribute(attr_id)) + .next(), + app.schema.schema_url.as_str(), + &app.theme, + ) + } + ["schema", "metric_group", id] => { + area_title = "Schema Metric Group"; + metric_group::widget( + app.schema.metric_group(id), + app.schema.schema_url.as_str(), + &app.theme, + ) + } + ["schema", "metric_group", metric_group_id, "attr", attr_id] => { + area_title = "Schema Metric Group"; + attribute::widget( + app.schema + .metric_group(metric_group_id) + .iter() + .flat_map(|m| m.attribute(attr_id)) + .next(), + app.schema.schema_url.as_str(), + &app.theme, + ) + } + ["schema", "event", id] => { + area_title = "Schema Event"; + schema::event::widget( + app.schema.event(id), + app.schema.schema_url.as_str(), + &app.theme, + ) + } + ["schema", "event", event_id, "attr", attr_id] => { + area_title = "Schema Event Attribute"; + attribute::widget( + app.schema + .event(event_id) + .iter() + .flat_map(|m| m.attribute(attr_id)) + .next(), + app.schema.schema_url.as_str(), + &app.theme, + ) + } + ["schema", "span", id] => { + area_title = "Schema Span"; + span::widget( + app.schema.span(id), + app.schema.schema_url.as_str(), + &app.theme, + ) + } + ["schema", "span", span_id, "attr", attr_id] => { + area_title = "Schema Span Attribute"; + attribute::widget( + app.schema + .span(span_id) + .iter() + .flat_map(|m| m.attribute(attr_id)) + .next(), + app.schema.schema_url.as_str(), + &app.theme, + ) + } + _ => Paragraph::new(vec![Line::default()]), + } + } else { + Paragraph::new(vec![Line::default()]) + }; + + paragraph + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(app.theme.border)) + .title(format!("{} ", area_title)) + .title_style(Style::default().fg(app.theme.title)) + //.padding(Padding::new(1,0,0,0)) + .style(Style::default()), + ) + .wrap(Wrap { trim: true }) +} + +fn update(app: &mut SearchApp) -> Result<()> { + if event::poll(std::time::Duration::from_millis(250))? { + let event = event::read()?; + if let event::Event::Key(key) = event { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Esc => { + app.should_quit = true; + return Ok(()); + } + KeyCode::Up => app.results.previous(), + KeyCode::Down => app.results.next(), + KeyCode::Enter => {} + _ => { + app.search_area.input(event); + } + } + } + } + // app.search_area.input(event); + } + + Ok(()) +} + +fn run(app: &mut SearchApp<'_>) -> Result<()> { + // ratatui terminal + let mut t = Terminal::new(CrosstermBackend::new(std::io::stderr()))?; + + loop { + // application render + t.draw(|f| { + ui(app, f); + })?; + + // application update + update(app)?; + + // application exit + if app.should_quit { + break; + } + } + + Ok(()) +} diff --git a/src/search/schema/attribute.rs b/src/search/schema/attribute.rs new file mode 100644 index 00000000..f05e3b0f --- /dev/null +++ b/src/search/schema/attribute.rs @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Utility functions to index and render attributes. + +use crate::search::schema::tags; +use crate::search::semconv::examples; +use crate::search::theme::ThemeConfig; +use crate::search::DocFields; +use ratatui::style::Style; +use ratatui::text::{Line, Span}; +use ratatui::widgets::Paragraph; +use tantivy::{doc, IndexWriter}; +use weaver_schema::attribute::Attribute; + +/// Build index for semantic convention attributes. +pub fn index_semconv_attributes<'a>( + attributes: impl Iterator, + path: &str, + fields: &DocFields, + index_writer: &mut IndexWriter, +) { + for attr in attributes { + index_writer + .add_document(doc!( + fields.path => format!("{}/attr/{}", path, attr.id()), + fields.brief => attr.brief(), + fields.note => attr.note(), + fields.tag => attr.tag().unwrap_or_default().as_str(), + )) + .expect("Failed to add document"); + } +} + +/// Build index for schema attributes. +pub fn index_schema_attribute<'a>( + attributes: impl Iterator, + path: &str, + fields: &DocFields, + index_writer: &mut IndexWriter, +) { + for attr in attributes { + if let Attribute::Id { + id, + brief, + note, + tags, + .. + } = attr + { + let tags: String = tags.as_ref().map_or("".to_string(), |tags| { + tags.iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", ") + }); + + index_writer + .add_document(doc!( + fields.path => format!("{}/attr/{}", path, id), + fields.brief => brief.clone(), + fields.note => note.clone(), + fields.tag => tags.as_str(), + )) + .expect("Failed to add document"); + } + } +} + +/// Render an attribute details. +pub fn widget<'a>( + attribute: Option<&'a Attribute>, + provenance: &'a str, + theme: &ThemeConfig, +) -> Paragraph<'a> { + match attribute { + Some(Attribute::Id { + id, + r#type, + brief, + examples, + tag, + requirement_level, + sampling_relevant, + note, + stability, + deprecated, + tags, + value, + }) => { + let mut text = vec![ + Line::from(vec![ + Span::styled("Id : ", Style::default().fg(theme.label)), + Span::raw(id), + ]), + Line::from(vec![ + Span::styled("Type : ", Style::default().fg(theme.label)), + Span::raw(r#type.to_string()), + ]), + ]; + + // Tag + if let Some(tag) = tag { + text.push(Line::from(vec![ + Span::styled("Tag : ", Style::default().fg(theme.label)), + Span::raw(tag), + ])); + } + + // Brief + if !brief.trim().is_empty() { + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Brief: ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(brief.as_str())); + } + + // Note + if !note.trim().is_empty() { + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Note : ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(note.as_str())); + } + + // Requirement Level + text.push(Line::from("")); + text.push(Line::from(vec![ + Span::styled("Requirement Level: ", Style::default().fg(theme.label)), + Span::raw(format!("{}", requirement_level)), + ])); + + if let Some(sampling_relevant) = sampling_relevant { + text.push(Line::from(vec![ + Span::styled("Sampling Relevant: ", Style::default().fg(theme.label)), + Span::raw(sampling_relevant.to_string()), + ])); + } + + if let Some(stability) = stability { + text.push(Line::from(vec![ + Span::styled("Stability: ", Style::default().fg(theme.label)), + Span::raw(format!("{}", stability)), + ])); + } + + if let Some(deprecated) = deprecated { + text.push(Line::from(vec![ + Span::styled("Deprecated: ", Style::default().fg(theme.label)), + Span::raw(deprecated.to_string()), + ])); + } + + if let Some(examples) = examples { + examples::append_lines(examples, &mut text, theme); + } + + if let Some(value) = value { + text.push(Line::from(vec![ + Span::styled("Value: ", Style::default().fg(theme.label)), + Span::raw(format!("{}", value)), + ])); + } + + tags::append_lines(tags.as_ref(), &mut text, theme); + + // Provenance + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Provenance: ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(provenance)); + + Paragraph::new(text).style(Style::default().fg(theme.value)) + } + None => Paragraph::new(vec![Line::default()]), + _ => Paragraph::new(vec![Line::from("Attribute not resolved!")]), + } +} diff --git a/src/search/schema/attributes.rs b/src/search/schema/attributes.rs new file mode 100644 index 00000000..6499338e --- /dev/null +++ b/src/search/schema/attributes.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! List of attributes rendering. + +use crate::search::theme::ThemeConfig; +use ratatui::style::Style; +use ratatui::text::{Line, Span}; +use weaver_schema::attribute::Attribute; +use weaver_semconv::attribute::{BasicRequirementLevelSpec, RequirementLevelSpec}; + +/// Append attributes to the text. +pub fn append_lines(attributes: &[Attribute], text: &mut Vec, theme: &ThemeConfig) { + if !attributes.is_empty() { + text.push(Line::from(Span::styled( + "Attributes: ", + Style::default().fg(theme.label), + ))); + for attr in attributes.iter() { + if let Attribute::Id { + id, + r#type, + requirement_level, + tags, + value, + .. + } = attr + { + let mut properties = vec![format!("type={}", r#type)]; + if let RequirementLevelSpec::Basic(BasicRequirementLevelSpec::Required) = + requirement_level + { + properties.push("required".to_string()); + } + if let Some(tags) = tags { + if !tags.is_empty() { + let mut pairs = vec![]; + for (k, v) in tags.iter() { + pairs.push(format!("{}={}", k, v)); + } + properties.push(format!("tags=[{}]", pairs.join(","))); + } + } + if let Some(value) = value { + properties.push(format!("value={}", value)); + } + let properties = if properties.is_empty() { + String::new() + } else { + format!(" ({})", properties.join(", ")) + }; + text.push(Line::from(Span::raw(format!("- {}{}", id, properties)))); + } + } + } +} diff --git a/src/search/schema/event.rs b/src/search/schema/event.rs new file mode 100644 index 00000000..c17ffe74 --- /dev/null +++ b/src/search/schema/event.rs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Utility functions to index and render events. + +use crate::search::schema::{attribute, attributes, tags}; +use crate::search::theme::ThemeConfig; +use crate::search::DocFields; +use ratatui::prelude::{Line, Style}; +use ratatui::text::Span; +use ratatui::widgets::Paragraph; +use tantivy::{doc, IndexWriter}; +use weaver_schema::TelemetrySchema; + +/// Build index for events. +pub fn index(schema: &TelemetrySchema, fields: &DocFields, index_writer: &mut IndexWriter) { + for event in schema.events() { + let tags: String = event.tags.clone().map_or("".to_string(), |tags| { + tags.iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", ") + }); + + index_writer + .add_document(doc!( + fields.path => format!("schema/event/{}", event.event_name), + fields.brief => "", + fields.note => "", + fields.tag => tags.as_str(), + )) + .expect("Failed to add document"); + attribute::index_schema_attribute( + event.attributes.iter(), + &format!("schema/event/{}", event.event_name), + fields, + index_writer, + ); + } +} + +/// Render a span details. +pub fn widget<'a>( + event: Option<&'a weaver_schema::event::Event>, + provenance: &'a str, + theme: &'a ThemeConfig, +) -> Paragraph<'a> { + match event { + Some(event) => { + let mut text = vec![ + Line::from(vec![ + Span::styled("Type : ", Style::default().fg(theme.label)), + Span::raw("Event (schema)"), + ]), + Line::from(vec![ + Span::styled("Name : ", Style::default().fg(theme.label)), + Span::raw(&event.event_name), + ]), + Line::from(vec![ + Span::styled("Domain : ", Style::default().fg(theme.label)), + Span::raw(&event.domain), + ]), + ]; + + attributes::append_lines(event.attributes.as_slice(), &mut text, theme); + tags::append_lines(event.tags.as_ref(), &mut text, theme); + + // Provenance + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Provenance: ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(provenance)); + + Paragraph::new(text).style(Style::default().fg(theme.value)) + } + None => Paragraph::new(vec![Line::default()]), + } +} diff --git a/src/search/schema/metric.rs b/src/search/schema/metric.rs new file mode 100644 index 00000000..14a0fafa --- /dev/null +++ b/src/search/schema/metric.rs @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Utility functions to index and render metrics. + +use ratatui::prelude::{Line, Style}; +use ratatui::text::Span; +use ratatui::widgets::Paragraph; +use tantivy::{doc, IndexWriter}; + +use weaver_schema::univariate_metric::UnivariateMetric; +use weaver_schema::TelemetrySchema; + +use crate::search::schema::{attribute, attributes, tags}; +use crate::search::theme::ThemeConfig; +use crate::search::DocFields; + +/// Build index for semantic convention metrics. +pub fn index_semconv_metrics<'a>( + metrics: impl Iterator, + path: &str, + fields: &DocFields, + index_writer: &mut IndexWriter, +) { + for metric in metrics { + index_writer + .add_document(doc!( + fields.path => format!("{}/metric/{}", path, metric.name), + fields.brief => metric.brief(), + fields.note => metric.note(), + fields.tag => "", + )) + .expect("Failed to add document"); + } +} + +/// Build index for schema metrics. +pub fn index_schema_metrics( + schema: &TelemetrySchema, + fields: &DocFields, + index_writer: &mut IndexWriter, +) { + for metric in schema.metrics() { + let tags: String = metric.tags().map_or("".to_string(), |tags| { + tags.iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", ") + }); + + index_writer + .add_document(doc!( + fields.path => format!("schema/metric/{}", metric.name()), + fields.brief => metric.brief(), + fields.note => metric.note(), + fields.tag => tags.as_str(), + )) + .expect("Failed to add document"); + if let UnivariateMetric::Metric { attributes, .. } = metric { + attribute::index_schema_attribute( + attributes.iter(), + &format!("schema/metric/{}", metric.name()), + fields, + index_writer, + ); + } + } +} + +/// Render a metric details. +pub fn widget<'a>( + metric: Option<&'a UnivariateMetric>, + provenance: &'a str, + theme: &'a ThemeConfig, +) -> Paragraph<'a> { + match metric { + Some(metric) => { + let mut text = vec![Line::from(vec![ + Span::styled("Type : ", Style::default().fg(theme.label)), + Span::raw("Metric (schema)"), + ])]; + + if let UnivariateMetric::Metric { + name, + brief, + note, + attributes, + instrument, + unit, + tags, + } = metric + { + text.push(Line::from(vec![ + Span::styled("Name : ", Style::default().fg(theme.label)), + Span::raw(name), + ])); + text.push(Line::from(vec![ + Span::styled("Brief : ", Style::default().fg(theme.label)), + Span::raw(brief), + ])); + text.push(Line::from(vec![ + Span::styled("Note : ", Style::default().fg(theme.label)), + Span::raw(note), + ])); + + text.push(Line::from(vec![ + Span::styled("Instrument: ", Style::default().fg(theme.label)), + Span::raw(format!("{:?}", instrument)), + ])); + + if let Some(unit) = unit { + text.push(Line::from(vec![ + Span::styled("Unit : ", Style::default().fg(theme.label)), + Span::raw(unit), + ])); + } + + attributes::append_lines(attributes.as_slice(), &mut text, theme); + + tags::append_lines(tags.as_ref(), &mut text, theme); + + // Provenance + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Provenance: ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(provenance)); + } + Paragraph::new(text).style(Style::default().fg(theme.value)) + } + None => Paragraph::new(vec![Line::default()]), + } +} diff --git a/src/search/schema/metric_group.rs b/src/search/schema/metric_group.rs new file mode 100644 index 00000000..488b2b8a --- /dev/null +++ b/src/search/schema/metric_group.rs @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Utility functions to index and render metric groups. + +use ratatui::prelude::{Line, Style}; +use ratatui::text::Span; +use ratatui::widgets::Paragraph; +use tantivy::{doc, IndexWriter}; + +use weaver_schema::metric_group::{Metric, MetricGroup}; +use weaver_schema::TelemetrySchema; + +use crate::search::schema::{attributes, tags}; +use crate::search::theme::ThemeConfig; +use crate::search::DocFields; + +/// Build index for metrics. +pub fn index(schema: &TelemetrySchema, fields: &DocFields, index_writer: &mut IndexWriter) { + for metric_group in schema.metric_groups() { + let tags: String = metric_group.tags().map_or("".to_string(), |tags| { + tags.iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", ") + }); + + index_writer + .add_document(doc!( + fields.path => format!("schema/metric_group/{}", metric_group.name()), + fields.brief => "", + fields.note => "", + fields.tag => tags.as_str(), + )) + .expect("Failed to add document"); + } +} + +/// Render a metric details. +pub fn widget<'a>( + metric_group: Option<&'a MetricGroup>, + provenance: &'a str, + theme: &'a ThemeConfig, +) -> Paragraph<'a> { + match metric_group { + Some(metric_group) => { + let mut text = vec![Line::from(vec![ + Span::styled("Type : ", Style::default().fg(theme.label)), + Span::raw("Metric Group (schema)"), + ])]; + + text.push(Line::from(vec![ + Span::styled("Name : ", Style::default().fg(theme.label)), + Span::raw(metric_group.name.clone()), + ])); + + attributes::append_lines(metric_group.attributes.as_slice(), &mut text, theme); + + if !metric_group.metrics.is_empty() { + text.push(Line::from(Span::styled( + "Metrics : ", + Style::default().fg(theme.label), + ))); + for metric in metric_group.metrics.iter() { + if let Metric::Metric { + name, + instrument, + unit, + tags, + .. + } = metric + { + let mut properties = vec![]; + properties.push(format!("instrument={:?}", instrument)); + if let Some(unit) = unit { + properties.push(format!("unit={}", unit)); + } + if let Some(tags) = tags { + if !tags.is_empty() { + let mut pairs = vec![]; + for (k, v) in tags.iter() { + pairs.push(format!("{}={}", k, v)); + } + properties.push(format!("tags=[{}]", pairs.join(","))); + } + } + let properties = if properties.is_empty() { + String::new() + } else { + format!(" ({})", properties.join(", ")) + }; + text.push(Line::from(Span::raw(format!("- {}{}", name, properties)))); + } + } + } + + tags::append_lines(metric_group.tags.as_ref(), &mut text, theme); + + // Provenance + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Provenance: ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(provenance)); + + Paragraph::new(text).style(Style::default().fg(theme.value)) + } + None => Paragraph::new(vec![Line::default()]), + } +} diff --git a/src/search/schema/mod.rs b/src/search/schema/mod.rs new file mode 100644 index 00000000..7b1765e2 --- /dev/null +++ b/src/search/schema/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Renderers for schema objects. +pub mod attribute; +pub mod attributes; +pub mod event; +pub mod metric; +pub mod metric_group; +pub mod resource; +pub mod span; +pub mod tags; diff --git a/src/search/schema/resource.rs b/src/search/schema/resource.rs new file mode 100644 index 00000000..2d15cd1d --- /dev/null +++ b/src/search/schema/resource.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Utility functions to index and render resources. + +use crate::search::DocFields; +use tantivy::{doc, IndexWriter}; +use weaver_schema::attribute::Attribute; +use weaver_schema::TelemetrySchema; + +/// Build index for resources. +pub fn index(schema: &TelemetrySchema, fields: &DocFields, index_writer: &mut IndexWriter) { + if let Some(resource) = schema.resource() { + for attr in resource.attributes() { + if let Attribute::Id { + id: the_id, + brief: the_brief, + note: the_note, + tag: the_tag, + .. + } = attr + { + index_writer + .add_document(doc!( + fields.path => format!("schema/resource/attr/{}", the_id), + fields.brief => the_brief.as_str(), + fields.note => the_note.as_str(), + fields.tag => the_tag.as_ref().unwrap_or(&"".to_string()).as_str(), + )) + .expect("Failed to add document"); + } + } + } +} diff --git a/src/search/schema/span.rs b/src/search/schema/span.rs new file mode 100644 index 00000000..323cb8ea --- /dev/null +++ b/src/search/schema/span.rs @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Utility functions to index and render spans. + +use crate::search::schema::{attribute, attributes, tags}; +use crate::search::theme::ThemeConfig; +use crate::search::DocFields; +use ratatui::prelude::{Line, Style}; +use ratatui::text::Span; +use ratatui::widgets::Paragraph; +use tantivy::{doc, IndexWriter}; +use weaver_schema::TelemetrySchema; + +/// Build index for spans. +pub fn index(schema: &TelemetrySchema, fields: &DocFields, index_writer: &mut IndexWriter) { + for span in schema.spans() { + let tags: String = span.tags.clone().map_or("".to_string(), |tags| { + tags.iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", ") + }); + + index_writer + .add_document(doc!( + fields.path => format!("schema/span/{}", span.span_name), + fields.brief => "", + fields.note => "", + fields.tag => tags.as_str(), + )) + .expect("Failed to add document"); + attribute::index_schema_attribute( + span.attributes.iter(), + &format!("schema/span/{}", span.span_name), + fields, + index_writer, + ); + for event in span.events.iter() { + let tags: String = event.tags.clone().map_or("".to_string(), |tags| { + tags.iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", ") + }); + + index_writer + .add_document(doc!( + fields.path => format!("schema/span/{}/event/{}", span.span_name, event.event_name), + fields.brief => "", + fields.note => "", + fields.tag => tags.as_str(), + )) + .expect("Failed to add document"); + attribute::index_schema_attribute( + event.attributes.iter(), + &format!("schema/span/{}/event/{}", span.span_name, event.event_name), + fields, + index_writer, + ); + } + } +} + +/// Render a span details. +pub fn widget<'a>( + span: Option<&'a weaver_schema::span::Span>, + provenance: &'a str, + theme: &'a ThemeConfig, +) -> Paragraph<'a> { + match span { + Some(span) => { + let mut text = vec![ + Line::from(vec![ + Span::styled("Type : ", Style::default().fg(theme.label)), + Span::raw("Span (schema)"), + ]), + Line::from(vec![ + Span::styled("Name : ", Style::default().fg(theme.label)), + Span::raw(&span.span_name), + ]), + ]; + + if let Some(kind) = span.kind.as_ref() { + text.push(Line::from(vec![ + Span::styled("Kind : ", Style::default().fg(theme.label)), + Span::raw(format!("{:?}", kind)), + ])); + } + + attributes::append_lines(span.attributes.as_slice(), &mut text, theme); + + if !span.events.is_empty() { + text.push(Line::from(Span::styled( + "Events : ", + Style::default().fg(theme.label), + ))); + for event in span.events.iter() { + text.push(Line::from(Span::raw(format!("- {} ", event.event_name)))); + } + } + + if !span.links.is_empty() { + text.push(Line::from(Span::styled( + "Links : ", + Style::default().fg(theme.label), + ))); + for link in span.links.iter() { + text.push(Line::from(Span::raw(format!("- {} ", link.link_name)))); + } + } + + tags::append_lines(span.tags.as_ref(), &mut text, theme); + + // Provenance + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Provenance: ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(provenance)); + + Paragraph::new(text).style(Style::default().fg(theme.value)) + } + None => Paragraph::new(vec![Line::default()]), + } +} diff --git a/src/search/schema/tags.rs b/src/search/schema/tags.rs new file mode 100644 index 00000000..60046d70 --- /dev/null +++ b/src/search/schema/tags.rs @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Tags rendering. + +use crate::search::theme::ThemeConfig; +use ratatui::prelude::{Line, Span, Style}; +use weaver_schema::tags::Tags; + +/// Append tags to the text. +pub fn append_lines<'a>(tags: Option<&'a Tags>, text: &mut Vec, theme: &'a ThemeConfig) { + if let Some(tags) = tags { + if tags.is_empty() { + return; + } + text.push(Line::from(Span::styled( + "Tags : ", + Style::default().fg(theme.label), + ))); + for (k, v) in tags.iter() { + text.push(Line::from(Span::raw(format!(" - {}={} ", k, v)))); + } + } +} diff --git a/src/search/semconv/attribute.rs b/src/search/semconv/attribute.rs new file mode 100644 index 00000000..92564904 --- /dev/null +++ b/src/search/semconv/attribute.rs @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Render semantic convention attributes. + +use ratatui::prelude::{Line, Span, Style}; +use ratatui::widgets::Paragraph; + +use crate::search::theme::ThemeConfig; +use weaver_semconv::attribute::AttributeSpec; +use weaver_semconv::AttributeSpecWithProvenance; + +use crate::search::semconv::examples; + +pub fn widget<'a>( + attribute: Option<&'a AttributeSpecWithProvenance>, + theme: &'a ThemeConfig, +) -> Paragraph<'a> { + match attribute.as_ref() { + Some(AttributeSpecWithProvenance { + attribute: + AttributeSpec::Id { + id, + r#type, + brief, + examples, + tag, + requirement_level, + sampling_relevant, + note, + stability, + deprecated, + }, + provenance, + }) => { + let mut text = vec![ + Line::from(vec![ + Span::styled("Id : ", Style::default().fg(theme.label)), + Span::raw(id), + ]), + Line::from(vec![ + Span::styled("Type : ", Style::default().fg(theme.label)), + Span::raw(format!("{}", r#type)), + ]), + ]; + + // Tag + if let Some(tag) = tag { + text.push(Line::from(vec![ + Span::styled("Tag : ", Style::default().fg(theme.label)), + Span::raw(tag), + ])); + } + + // Brief + if !brief.trim().is_empty() { + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Brief: ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(brief.as_str())); + } + + // Note + if !note.trim().is_empty() { + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Note : ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(note.as_str())); + } + + // Requirement Level + text.push(Line::from("")); + text.push(Line::from(vec![ + Span::styled("Requirement Level: ", Style::default().fg(theme.label)), + Span::raw(format!("{}", requirement_level)), + ])); + + if let Some(sampling_relevant) = sampling_relevant { + text.push(Line::from(vec![ + Span::styled("Sampling Relevant: ", Style::default().fg(theme.label)), + Span::raw(sampling_relevant.to_string()), + ])); + } + + if let Some(stability) = stability { + text.push(Line::from(vec![ + Span::styled("Stability: ", Style::default().fg(theme.label)), + Span::raw(format!("{}", stability)), + ])); + } + + if let Some(deprecated) = deprecated { + text.push(Line::from(vec![ + Span::styled("Deprecated: ", Style::default().fg(theme.label)), + Span::raw(deprecated.to_string()), + ])); + } + + if let Some(examples) = examples { + examples::append_lines(examples, &mut text, theme); + } + + // Provenance + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Provenance: ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(provenance.as_str())); + + Paragraph::new(text).style(Style::default().fg(theme.value)) + } + _ => Paragraph::new(vec![Line::from("Attribute not resolved!")]), + } +} diff --git a/src/search/semconv/attributes.rs b/src/search/semconv/attributes.rs new file mode 100644 index 00000000..a181f5f3 --- /dev/null +++ b/src/search/semconv/attributes.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Attribute rendering. + +use crate::search::theme::ThemeConfig; +use ratatui::style::Style; +use ratatui::text::{Line, Span}; +use weaver_semconv::attribute::{AttributeSpec, BasicRequirementLevelSpec, RequirementLevelSpec}; + +/// Append attributes to the text. +pub fn append_lines(attributes: &[AttributeSpec], text: &mut Vec, theme: &ThemeConfig) { + if !attributes.is_empty() { + text.push(Line::from(Span::styled( + "Attributes: ", + Style::default().fg(theme.label), + ))); + for attr in attributes.iter() { + if let AttributeSpec::Id { + id, + r#type, + requirement_level, + .. + } = attr + { + let mut properties = vec![format!("type={}", r#type)]; + if let RequirementLevelSpec::Basic(BasicRequirementLevelSpec::Required) = + requirement_level + { + properties.push("required".to_string()); + } + let properties = if properties.is_empty() { + String::new() + } else { + format!(" ({})", properties.join(", ")) + }; + text.push(Line::from(Span::raw(format!("- {}{}", id, properties)))); + } + } + } +} diff --git a/src/search/semconv/examples.rs b/src/search/semconv/examples.rs new file mode 100644 index 00000000..0fef6387 --- /dev/null +++ b/src/search/semconv/examples.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Render examples + +use crate::search::theme::ThemeConfig; +use ratatui::prelude::{Line, Span, Style}; +use weaver_semconv::attribute::ExamplesSpec; + +/// Append examples to the text. +pub fn append_lines(examples: &ExamplesSpec, text: &mut Vec, theme: &ThemeConfig) { + text.push(Line::from(Span::styled( + "Examples: ", + Style::default().fg(theme.label), + ))); + match examples { + ExamplesSpec::Int(v) => text.push(Line::from(Span::raw(format!("- {}", v)))), + ExamplesSpec::Double(v) => text.push(Line::from(Span::raw(format!("- {}", v)))), + ExamplesSpec::Bool(v) => text.push(Line::from(Span::raw(format!("- {}", v)))), + ExamplesSpec::String(v) => text.push(Line::from(Span::raw(format!("- {}", v)))), + ExamplesSpec::Ints(vals) => { + for v in vals.iter() { + text.push(Line::from(Span::raw(format!("- {}", v)))); + } + } + ExamplesSpec::Doubles(vals) => { + for v in vals.iter() { + text.push(Line::from(Span::raw(format!("- {}", v)))); + } + } + ExamplesSpec::Bools(vals) => { + for v in vals.iter() { + text.push(Line::from(Span::raw(format!("- {}", v)))); + } + } + ExamplesSpec::Strings(vals) => { + for v in vals.iter() { + text.push(Line::from(Span::raw(format!("- {}", v)))); + } + } + } +} diff --git a/src/search/semconv/metric.rs b/src/search/semconv/metric.rs new file mode 100644 index 00000000..bdc2d4cf --- /dev/null +++ b/src/search/semconv/metric.rs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Render semantic convention attributes. + +use ratatui::prelude::{Line, Span, Style}; +use ratatui::widgets::Paragraph; + +use crate::search::theme::ThemeConfig; +use weaver_semconv::MetricSpecWithProvenance; + +use crate::search::semconv::attributes; + +pub fn widget<'a>( + metric: Option<&'a MetricSpecWithProvenance>, + theme: &'a ThemeConfig, +) -> Paragraph<'a> { + match metric { + Some(MetricSpecWithProvenance { metric, provenance }) => { + let mut text = vec![ + Line::from(vec![ + Span::styled("Name : ", Style::default().fg(theme.label)), + Span::raw(metric.name.clone()), + ]), + Line::from(vec![ + Span::styled("Instrument: ", Style::default().fg(theme.label)), + Span::raw(format!("{:?}", metric.instrument)), + ]), + Line::from(vec![ + Span::styled("Unit : ", Style::default().fg(theme.label)), + Span::raw(metric.unit.clone().unwrap_or_default()), + ]), + ]; + + // Brief + if !metric.brief.trim().is_empty() { + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Brief : ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(metric.brief.as_str())); + } + + // Note + if !metric.note.trim().is_empty() { + text.push(Line::from("")); + text.push(Line::from(Span::styled( + "Note : ", + Style::default().fg(theme.label), + ))); + text.push(Line::from(metric.note.as_str())); + } + + attributes::append_lines(metric.attributes.as_slice(), &mut text, theme); + + // Provenance + text.push(Line::from("")); + text.push(Line::from(vec![ + Span::styled("Provenance: ", Style::default().fg(theme.label)), + Span::raw(provenance.to_string()), + ])); + + Paragraph::new(text).style(Style::default().fg(theme.value)) + } + None => Paragraph::new(vec![Line::default()]), + } +} diff --git a/src/search/semconv/mod.rs b/src/search/semconv/mod.rs new file mode 100644 index 00000000..3ddc81a8 --- /dev/null +++ b/src/search/semconv/mod.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Renderers for semantic convention objects. + +pub mod attribute; +pub mod attributes; +pub mod examples; +pub mod metric; diff --git a/src/search/theme.rs b/src/search/theme.rs new file mode 100644 index 00000000..a0f1af79 --- /dev/null +++ b/src/search/theme.rs @@ -0,0 +1,13 @@ +use ratatui::prelude::Color; + +/// Theme configurations +pub struct ThemeConfig { + /// Color of the titles (i.e. block titles) + pub title: Color, + /// Color of the borders + pub border: Color, + /// Color of the labels (i.e. field names) + pub label: Color, + /// Color of the values (i.e. field values) + pub value: Color, +} diff --git a/templates/go/config.yaml b/templates/go/config.yaml new file mode 100644 index 00000000..452e1bf4 --- /dev/null +++ b/templates/go/config.yaml @@ -0,0 +1,15 @@ +file_name: snake_case +function_name: PascalCase +arg_name: camelCase +struct_name: PascalCase +field_name: PascalCase + +type_mapping: + int: int64 + double: double + boolean: bool + string: string + "int[]": "[]int64" + "double[]": "[]double" + "boolean[]": "[]bool" + "string[]": "[]string" \ No newline at end of file diff --git a/templates/go/optional_attrs.macro.tera b/templates/go/optional_attrs.macro.tera new file mode 100644 index 00000000..89fcf653 --- /dev/null +++ b/templates/go/optional_attrs.macro.tera @@ -0,0 +1,52 @@ +{% macro attr_type(prefix) -%}Optional{{prefix}}Attribute{% endmacro attr_type %} + +{% macro declare_attrs(prefix="", marker, attrs) -%} +{% set not_require_attrs = attrs | not_required | without_value %} +{%- if not_require_attrs | length > 0 -%} +// =============================================== +// ====== Definition of optional attributes ====== +// =============================================== + +// Optional{{marker}}Attribute is an interface implemented by all optional attributes of the {{marker}}. +type Optional{{marker}}Attribute interface { + Attribute() otel_attr.KeyValue + {{ marker | function_name }}Marker() +} + +{% for attr in not_require_attrs | without_enum %} +// {{prefix}}{{attr.id | struct_name}}OptAttr represents an optional attribute. +// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix="// ") }} +func {{prefix}}{{attr.id | struct_name}}OptAttr(v {{ attr.type | type_mapping(enum=attr.id | struct_name) }}) {{prefix}}{{attr.id | struct_name}}OptAttrWrapper { return {{prefix}}{{attr.id | struct_name}}OptAttrWrapper{v} } +// {{prefix}}{{attr.id | struct_name}}OptAttrWrapper is a wrapper for the attribute `{{attr.id}}`. +// Use the function {{attr.id | struct_name}}OptAttr(value) to create an instance. +type {{prefix}}{{attr.id | struct_name}}OptAttrWrapper struct { {{ attr.type | type_mapping(enum=attr.id | struct_name) }} } +func (w {{prefix}}{{attr.id | struct_name}}OptAttrWrapper) Attribute() otel_attr.KeyValue { + return attribute.{{ attr.id | field_name }}Key.{{ attr.type | type_mapping(enum="String" | struct_name) | function_name }}(w.{{ attr.type | type_mapping(enum="string" | struct_name) }}) +} +func (w {{prefix}}{{attr.id | struct_name}}OptAttrWrapper) {{marker}}Marker() {} + +{% endfor %} + +{% for attr in not_require_attrs | with_enum %} +// {{prefix}}{{attr.id | struct_name}}OptAttr represents an optional attribute. +// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix="// ") }} +func {{prefix}}{{attr.id | struct_name}}OptAttr(v {{ attr.type | type_mapping(enum=attr.id | struct_name) }}) {{prefix}}{{attr.id | struct_name}}OptAttrWrapper { return {{prefix}}{{attr.id | struct_name}}OptAttrWrapper{v} } +// {{prefix}}{{attr.id | struct_name}}OptAttrWrapper is a wrapper for the attribute `{{attr.id}}`. +// Use the function {{attr.id | struct_name}}OptAttr(value) to create an instance. +type {{prefix}}{{attr.id | struct_name}}OptAttrWrapper struct { {{ attr.type | type_mapping(enum=attr.id | struct_name) }} } +func (w {{prefix}}{{attr.id | struct_name}}OptAttrWrapper) Attribute() otel_attr.KeyValue { + return attribute.{{ attr.id | field_name }}Key.String(string(w.{{ attr.type | type_mapping(enum=attr.id | struct_name) }})) +} +func (w {{prefix}}{{attr.id | struct_name}}OptAttrWrapper) {{marker}}Marker() {} + +type {{attr.id | struct_name}} string +const ( +{% for variant in attr.type.members %} + // {{variant.id | struct_name}} is a possible value of {{attr.id | struct_name}}. + // {{ [variant.brief, variant.note] | comment(prefix=" // ") }} + {{variant.id | struct_name}} {{attr.id | struct_name}} = "{{variant.id}}" +{%- endfor %} +) +{% endfor %} +{%- endif -%} +{% endmacro declare_attrs %} \ No newline at end of file diff --git a/templates/go/otel/attribute/attrs.go.tera b/templates/go/otel/attribute/attrs.go.tera new file mode 100644 index 00000000..ec1fd2f3 --- /dev/null +++ b/templates/go/otel/attribute/attrs.go.tera @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 + +package attribute + +import ( + "go.opentelemetry.io/otel/attribute" +) + +{% set attrs = schema | unique_attributes(recursive=true) -%} +// Declaration of all attribute keys. +var ( +{%- for attr in attrs %} + {{ attr.id | field_name }}Key = attribute.Key("{{attr.id}}") +{%- endfor %} +) \ No newline at end of file diff --git a/templates/go/otel/client.go.tera b/templates/go/otel/client.go.tera new file mode 100644 index 00000000..7ec1ba53 --- /dev/null +++ b/templates/go/otel/client.go.tera @@ -0,0 +1,181 @@ +{% import "required_attrs.macro.tera" as required %} +{% import "optional_attrs.macro.tera" as optional %} +package otel + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "go.opentelemetry.io/otel" + otel_attr "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/metric" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" + + "go_test/pkg/otel/attribute" +) + +const ( + InstrumentationName = "{{ schema.instrumentation_library.name }}" + InstrumentationVersion = "{{ schema.instrumentation_library.version }}" +) + +var ( + Meter = otel.GetMeterProvider().Meter( + InstrumentationName, + metric.WithInstrumentationVersion(InstrumentationVersion), + metric.WithSchemaURL("{{ schema_url }}"), + ) + Tracer = otel.GetTracerProvider().Tracer( + InstrumentationName, + trace.WithInstrumentationVersion(InstrumentationVersion), + trace.WithSchemaURL("{{ schema_url }}"), + ) +) + +// ClientHandler is a handler for the OTel Weaver client. +type ClientHandler struct { + ctx context.Context + metricShutdown func(context.Context) error + traceShutdown func(context.Context) error +} + +{%- set required_attrs = schema.resource.attributes | required | without_value -%} +{%- set not_required_attrs = schema.resource.attributes | not_required | without_value %} + +{{ required::declare_attrs(attrs=required_attrs) }} +{{ optional::declare_attrs(marker="Resource", attrs=not_required_attrs) }} + +// ================== +// ===== Client ===== +// ================== + +// Client returns a OTel client (generated by OTel Weaver). +// It uses a context initialized with `context.Background()`. +func Client( + {{- required::declare_args(attrs=required_attrs) }} + {% if not_required_attrs | length > 0 -%}optionalAttributes ...OptionalResourceAttribute,{% endif %} +) *ClientHandler { + return ClientWithContext( + context.Background(), + {%- for attr in required_attrs %} + {{attr.id | arg_name}}, + {%- endfor %} + {% if not_required_attrs | length > 0 -%}optionalAttributes...,{% endif %} + ) +} + +// ClientWithContext returns a OTel client with a given context (generated by OTel Weaver). +func ClientWithContext( + ctx context.Context, + {{- required::declare_args(attrs=required_attrs) }} + {% if not_required_attrs | length > 0 -%}optionalAttributes ...OptionalResourceAttribute,{% endif %} +) *ClientHandler { + metricShutdown, traceShutdown, err := installExportPipeline( + {%- for attr in required_attrs %} + {{attr.id | arg_name}}, + {%- endfor %} + {% if not_required_attrs | length > 0 -%}optionalAttributes...,{% endif %} + ) + if err != nil { + log.Fatal(err) + } + + return &ClientHandler{ + ctx: ctx, + metricShutdown: metricShutdown, + traceShutdown: traceShutdown, + } +} + +func (o *ClientHandler) Shutdown() { + metricErr := o.metricShutdown(o.ctx) + traceErr := o.traceShutdown(o.ctx) + + mustExit := false + if metricErr != nil { + log.Println(metricErr) + mustExit = true + } + if traceErr != nil { + log.Println(traceErr) + mustExit = true + } + if mustExit { + os.Exit(1) + } +} + +func resourceBuilder( + {%- for attr in required_attrs %} + {{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, + {%- endfor %} + {% if not_required_attrs | length > 0 -%}optionalAttributes ...OptionalResourceAttribute,{% endif %} +) *resource.Resource { + attrs := []otel_attr.KeyValue { + {%- for attr in schema.resource.attributes | with_value %} + attribute.{{ attr.id | field_name }}Key.{{ attr.type | type_mapping(enum=attr.id | struct_name) | function_name }}({{ attr.value | value }}), + {%- endfor %} + {%- for attr in required_attrs %} + {{attr.id | arg_name}}.Attribute(), + {%- endfor %} + } + {% if not_required_attrs | length > 0 -%} + for _, attr := range optionalAttributes { + attrs = append(attrs, attr.Attribute()) + } + {% endif %} + return resource.NewWithAttributes("{{ schema_url }}", attrs...) +} + +func installExportPipeline( + {%- for attr in required_attrs %} + {{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, + {%- endfor %} + {% if not_required_attrs | length > 0 -%}optionalAttributes ...OptionalResourceAttribute,{% endif %} +) (metricShutdown func(context.Context) error, traceShutdown func(context.Context) error, err error) { + metricExporter, err := stdoutmetric.New(stdoutmetric.WithPrettyPrint()) + if err != nil { + err = fmt.Errorf("creating metric stdout exporter: %w", err) + return + } + + traceExporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) + if err != nil { + err = fmt.Errorf("creating trace stdout exporter: %w", err) + return + } + + metricProvider := sdkmetric.NewMeterProvider( + sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter, sdkmetric.WithInterval(3*time.Second))), + sdkmetric.WithResource(resourceBuilder( + {%- for attr in required_attrs %} + {{attr.id | arg_name}}, + {%- endfor %} + {% if not_required_attrs | length > 0 -%}optionalAttributes...,{% endif %} + )), + ) + otel.SetMeterProvider(metricProvider) + + tracerProvider := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(traceExporter), + sdktrace.WithResource(resourceBuilder( + {%- for attr in required_attrs %} + {{attr.id | arg_name}}, + {%- endfor %} + {% if not_required_attrs | length > 0 -%}optionalAttributes...,{% endif %} + )), + ) + otel.SetTracerProvider(tracerProvider) + + metricShutdown = metricProvider.Shutdown + traceShutdown = tracerProvider.Shutdown + return +} \ No newline at end of file diff --git a/templates/go/otel/eventer/event.tera b/templates/go/otel/eventer/event.tera new file mode 100644 index 00000000..5e489334 --- /dev/null +++ b/templates/go/otel/eventer/event.tera @@ -0,0 +1,57 @@ +{% import "required_attrs.macro.tera" as required %} +{% import "optional_attrs.macro.tera" as optional %} +{# Define the file name for the generated code #} +{%- set file_name = event_name | file_name -%} +{{- config(file_name="otel/eventer/event_" ~ file_name ~ "/event.go") -}} +// SPDX-License-Identifier: Apache-2.0 + +package {{ event_name | file_name }} + +import ( + "context" + + otel_attr "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + client "go_test/pkg/otel" + "go_test/pkg/otel/attribute" +) + +// Event records a new `{{ event_name }}` event with +// the given required attributes. +func Event( + {{- required::declare_args(attrs=attributes) }} + optionalAttributes ...{{ optional::attr_type(prefix="Span") }}, +) { + EventWithContext( + context.TODO(), + {%- for attr in attributes | required | without_value %} + {{attr.id | arg_name}}, + {%- endfor %} + optionalAttributes..., + ) +} + +// EventWithContext records a new `{{ event_name }}` event with +// the given context and required attributes. +func EventWithContext( + ctx context.Context, + {{- required::declare_args(attrs=attributes) }} + optionalAttributes ...{{ optional::attr_type(prefix="Span") }}, +) { + + ctx, span := client.Tracer.Start(ctx, "{{ event_name }}", + {%- for attr in attributes | with_value %} + trace.WithAttributes(attribute.{{ attr.id | function_name }}Key.{{attr.type | type_mapping(enum=attr.id) | function_name}}({{attr.value | value}})), + {%- endfor %} + {%- for attr in attributes | required | without_value %} + trace.WithAttributes({{ attr.id | arg_name }}.Attribute()), + {%- endfor %} + ) + for _, opt := range optionalAttributes { + span.SetAttributes(opt.Attribute()) + } +} + +{{ required::declare_attrs(attrs=attributes) }} +{{ optional::declare_attrs(marker="Span", attrs=attributes) }} diff --git a/templates/go/otel/meter/metric.tera b/templates/go/otel/meter/metric.tera new file mode 100644 index 00000000..4e2a5cc7 --- /dev/null +++ b/templates/go/otel/meter/metric.tera @@ -0,0 +1,462 @@ +{% import "required_attrs.macro.tera" as required %} +{% import "optional_attrs.macro.tera" as optional %} +{# Define the file name for the generated code #} +{%- set file_name = name | file_name -%} +{{- config(file_name="otel/meter/metric_" ~ file_name ~ "/metric.go") -}} +// SPDX-License-Identifier: Apache-2.0 + +package {{ name | file_name }} + +import ( + "context" + + otel_attr "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + + client "go_test/pkg/otel" + "go_test/pkg/otel/attribute" +) + +type Int64Observer func() (int64, {% for attr in attributes | required | without_value %}{{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}[]OptionalMetricAttribute, error) +type Float64Observer func() (float64, {% for attr in attributes | required | without_value %}{{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}[]OptionalMetricAttribute, error) + +{% if instrument == "counter" %} +// ===== Synchronous Counter Declaration ===== +type Int64Counter_ struct { + ctx context.Context + counter metric.Int64Counter +} + +type Float64Counter_ struct { + ctx context.Context + counter metric.Float64Counter +} + +func Int64Counter() (*Int64Counter_, error) { + return Int64CounterWithContext(context.TODO()) +} + +func Int64CounterWithContext(ctx context.Context) (*Int64Counter_, error) { + counter, err := client.Meter.Int64Counter( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + ) + if err != nil { + return nil, err + } + return &Int64Counter_{ + ctx: ctx, + counter: counter, + }, nil +} + +func (g *Int64Counter_) Add(incr uint64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.AddOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.counter.Add(g.ctx, int64(incr), options...) +} + +func (g *Int64Counter_) AddWithContext(ctx context.Context, incr uint64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.AddOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.counter.Add(ctx, int64(incr), options...) +} + +func Float64Counter() (*Float64Counter_, error) { + return Float64CounterWithContext(context.TODO()) +} + +func Float64CounterWithContext(ctx context.Context) (*Float64Counter_, error) { + counter, err := client.Meter.Float64Counter( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + ) + if err != nil { + return nil, err + } + return &Float64Counter_{ + ctx: ctx, + counter: counter, + }, nil +} + +func (g *Float64Counter_) Add(incr float64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.AddOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.counter.Add(g.ctx, incr, options...) +} + +func (g *Float64Counter_) AddWithContext(ctx context.Context, incr float64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.AddOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.counter.Add(ctx, incr, options...) +} + +// ============================================ +// ===== Asynchronous Counter Declaration ===== +// ============================================ + +func Int64ObservableCounter(observer Int64Observer) error { + _, err := client.Meter.Int64ObservableCounter( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + metric.WithInt64Callback(func(ctx context.Context, otelObserver metric.Int64Observer) error { + v, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}}, {% endfor %}optAttrs, err := observer() + if err != nil { + return err + } + options := []metric.ObserveOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + otelObserver.Observe(v, options...) + return nil + })) + if err != nil { + return err + } + return nil +} + +func Float64ObservableCounter(observer Float64Observer) error { + _, err := client.Meter.Float64ObservableCounter( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + metric.WithFloat64Callback(func(ctx context.Context, otelObserver metric.Float64Observer) error { + v, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}}, {% endfor %}optAttrs, err := observer() + if err != nil { + return err + } + options := []metric.ObserveOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + otelObserver.Observe(v, options...) + return nil + })) + if err != nil { + return err + } + return nil +} + +{% elif instrument == "updowncounter" %} +// ===== Synchronous UpDownCounter Declaration ===== +type Int64UpDownCounter_ struct { + ctx context.Context + counter metric.Int64UpDownCounter +} + +type Float64UpDownCounter_ struct { + ctx context.Context + counter metric.Float64UpDownCounter +} + +func Int64UpDownCounter() (*Int64UpDownCounter_, error) { + return Int64UpDownCounterWithContext(context.TODO()) +} + +func Int64UpDownCounterWithContext(ctx context.Context) (*Int64UpDownCounter_, error) { + counter, err := client.Meter.Int64UpDownCounter( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + ) + if err != nil { + return nil, err + } + return &Int64UpDownCounter_{ + ctx: ctx, + counter: counter, + }, nil +} + +func (g *Int64UpDownCounter_) Add(incr uint64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.AddOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.counter.Add(g.ctx, int64(incr), options...) +} + +func (g *Int64UpDownCounter_) AddWithContext(ctx context.Context, incr uint64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.AddOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.counter.Add(ctx, int64(incr), options...) +} + +func Float64UpDownCounter() (*Float64UpDownCounter_, error) { + return Float64UpDownCounterWithContext(context.TODO()) +} + +func Float64UpDownCounterWithContext(ctx context.Context) (*Float64UpDownCounter_, error) { + counter, err := client.Meter.Float64UpDownCounter( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + ) + if err != nil { + return nil, err + } + return &Float64UpDownCounter_{ + ctx: ctx, + counter: counter, + }, nil +} + +func (g *Float64UpDownCounter_) Add(incr float64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.AddOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.counter.Add(g.ctx, incr, options...) +} + +func (g *Float64UpDownCounter_) AddWithContext(ctx context.Context, incr float64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.AddOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.counter.Add(ctx, incr, options...) +} + +// ============================================ +// ===== Asynchronous UpDownCounter Declaration ===== +// ============================================ + +func Int64ObservableUpDownCounter(observer Int64Observer) error { + _, err := client.Meter.Int64ObservableUpDownCounter( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + metric.WithInt64Callback(func(ctx context.Context, otelObserver metric.Int64Observer) error { + v, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}}, {% endfor %}optAttrs, err := observer() + if err != nil { + return err + } + options := []metric.ObserveOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + otelObserver.Observe(v, options...) + return nil + })) + if err != nil { + return err + } + return nil +} + +func Float64ObservableUpDownCounter(observer Float64Observer) error { + _, err := client.Meter.Float64ObservableUpDownCounter( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + metric.WithFloat64Callback(func(ctx context.Context, otelObserver metric.Float64Observer) error { + v, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}}, {% endfor %}optAttrs, err := observer() + if err != nil { + return err + } + options := []metric.ObserveOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + otelObserver.Observe(v, options...) + return nil + })) + if err != nil { + return err + } + return nil +} + +{% elif instrument == "gauge" %} +// ========================================== +// ===== Asynchronous Gauge Declaration ===== +// ========================================== + +func Int64ObservableGauge(observer Int64Observer) error { + _, err := client.Meter.Int64ObservableGauge( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + metric.WithInt64Callback(func(ctx context.Context, otelObserver metric.Int64Observer) error { + v, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}}, {% endfor %}optAttrs, err := observer() + if err != nil { + return err + } + options := []metric.ObserveOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + otelObserver.Observe(v, options...) + return nil + })) + if err != nil { + return err + } + return nil +} + +func Float64ObservableGauge(observer Float64Observer) error { + _, err := client.Meter.Float64ObservableGauge( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + metric.WithFloat64Callback(func(ctx context.Context, otelObserver metric.Float64Observer) error { + v, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}}, {% endfor %}optAttrs, err := observer() + if err != nil { + return err + } + options := []metric.ObserveOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + otelObserver.Observe(v, options...) + return nil + })) + if err != nil { + return err + } + return nil +} + +{% elif instrument == "histogram" %} +// ============================================= +// ===== Synchronous Histogram Declaration ===== +// ============================================= + +type Int64Histogram_ struct { + ctx context.Context + histogram metric.Int64Histogram +} + +type Float64Histogram_ struct { + ctx context.Context + histogram metric.Float64Histogram +} + +func Int64Histogram() (*Int64Histogram_, error) { + return Int64HistogramWithContext(context.TODO()) +} + +func Int64HistogramWithContext(ctx context.Context) (*Int64Histogram_, error) { + histogram, err := client.Meter.Int64Histogram( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + ) + if err != nil { + return nil, err + } + return &Int64Histogram_{ + ctx: ctx, + histogram: histogram, + }, nil +} + +func (g *Int64Histogram_) Record(incr uint64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.RecordOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.histogram.Record(g.ctx, int64(incr), options...) +} + +func (g *Int64Histogram_) RecordWithContext(ctx context.Context, incr uint64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.RecordOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.histogram.Record(ctx, int64(incr), options...) +} + +func Float64Histogram() (*Float64Histogram_, error) { + return Float64HistogramWithContext(context.TODO()) +} + +func Float64HistogramWithContext(ctx context.Context) (*Float64Histogram_, error) { + histogram, err := client.Meter.Float64Histogram( + "{{name}}", + metric.WithDescription("{{brief}}"), + metric.WithUnit("{{unit}}"), + ) + if err != nil { + return nil, err + } + return &Float64Histogram_{ + ctx: ctx, + histogram: histogram, + }, nil +} + +func (g *Float64Histogram_) Record(incr float64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.RecordOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.histogram.Record(g.ctx, incr, options...) +} + +func (g *Float64Histogram_) RecordWithContext(ctx context.Context, incr float64, {% for attr in attributes | required | without_value %}{{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}optAttrs ...OptionalMetricAttribute) { + options := []metric.RecordOption { + {% for attr in attributes | required | without_value %}metric.WithAttributes({{attr.id | arg_name}}.Attribute()),{% endfor %} + } + for _, opt := range optAttrs { + options = append(options, metric.WithAttributes(opt.Attribute())) + } + g.histogram.Record(ctx, incr, options...) +} + +{% endif %} + +{{ required::declare_attrs(attrs=attributes) }} +{{ optional::declare_attrs(marker="Metric", attrs=attributes) }} diff --git a/templates/go/otel/meter/metric_group.tera b/templates/go/otel/meter/metric_group.tera new file mode 100644 index 00000000..900dbad2 --- /dev/null +++ b/templates/go/otel/meter/metric_group.tera @@ -0,0 +1,20 @@ +{% import "required_attrs.macro.tera" as required %} +{% import "optional_attrs.macro.tera" as optional %} +{# Define the file name for the generated code #} +{%- set file_name = name | file_name -%} +{{- config(file_name="otel/meter/metric_group_" ~ file_name ~ "/metric_group.go") -}} +// SPDX-License-Identifier: Apache-2.0 + +package {{ name | file_name }} + +import ( + otel_attr "go.opentelemetry.io/otel/attribute" + + "go_test/pkg/otel/attribute" +) + +type Int64Observer func() (int64, {% for attr in attributes | required | without_value %}{{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}[]OptionalMetricAttribute, error) +type Float64Observer func() (float64, {% for attr in attributes | required | without_value %}{{attr.id | struct_name}}ReqAttrWrapper, {% endfor %}[]OptionalMetricAttribute, error) + +{{ required::declare_attrs(attrs=attributes) }} +{{ optional::declare_attrs(marker="Metric", attrs=attributes) }} diff --git a/templates/go/otel/tracer/span.tera b/templates/go/otel/tracer/span.tera new file mode 100644 index 00000000..0c4fd4ce --- /dev/null +++ b/templates/go/otel/tracer/span.tera @@ -0,0 +1,160 @@ +{% import "required_attrs.macro.tera" as required %} +{% import "optional_attrs.macro.tera" as optional %} +{# Define the file name for the generated code #} +{%- set file_name = span_name | file_name -%} +{{- config(file_name="otel/tracer/" ~ file_name ~ "/span.go") -}} +// SPDX-License-Identifier: Apache-2.0 + +package {{ span_name | file_name }} + +import ( + "context" + + otel_attr "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + + client "go_test/pkg/otel" + "go_test/pkg/otel/attribute" +) + +{%- if attributes | required | without_value | length > 0 %} +// Start starts a new `{{ span_name }}` span with +// the given required attributes. +func Start( + {{- required::declare_args(attrs=attributes) }} + optionalAttributes ...{{ optional::attr_type(prefix="Span") }}, +) *{{span_name | struct_name}}Span { + return StartWithContext( + context.TODO(), + {%- for attr in attributes | required | without_value %} + {{attr.id | arg_name}}, + {%- endfor %} + optionalAttributes..., + ) +} + +// StartWithContext starts a new `{{ span_name }}` span with +// the given required attributes and context. +func StartWithContext( + ctx context.Context, + {{- required::declare_args(attrs=attributes) }} + optionalAttributes ...{{ optional::attr_type(prefix="Span") }}, +) *{{span_name | struct_name}}Span { + ctx, span := client.Tracer.Start(ctx, "{{ span_name }}", + {%- for attr in attributes | with_value %} + trace.WithAttributes(attribute.{{ attr.id | function_name }}Key.{{attr.type | type_mapping(enum=attr.id) | function_name}}({{attr.value | value}})), + {%- endfor %} + {%- for attr in attributes | required | without_value %} + trace.WithAttributes({{ attr.id | arg_name }}.Attribute()), + {%- endfor %} + ) + for _, opt := range optionalAttributes { + span.SetAttributes(opt.Attribute()) + } + return &{{span_name | struct_name}}Span { + ctx: ctx, + span: span, + } +} +{%- else %} +// Start starts a new named `{{ span_name }}` span. +func Start{{ span_name | function_name }}(ctx context.Context, optionalAttributes ...{{ optional::attr_type(prefix="Span") }}) *{{span_name | struct_name}}Span { + ctx, span := client.Tracer.Start(ctx, "{{ span_name }}") + for _, opt := range optionalAttributes { + span.SetAttributes(opt.Attribute()) + } + return &{{span_name | struct_name}}Span { + ctx: ctx, + span: span, + } +} +{%- endif %} + +{{ required::declare_attrs(attrs=attributes) }} +{{ optional::declare_attrs(marker="Span", attrs=attributes) }} + +// {{span_name | struct_name}}Span is a span for `{{ span_name }}`. +type {{span_name | struct_name}}Span struct { + ctx context.Context + span trace.Span +} + +{% if events | length > 0 -%} +// {{ span_name | struct_name }}Event is interface implemented by all events for `{{ span_name }}`. +type {{ span_name | struct_name }}Event interface { + EventOptions() []trace.EventOption +} + +{% for event in events -%} + +{% set event_name = event.event_name | function_name -%} +{{ required::declare_attrs(prefix="Event" ~ event_name, attrs=event.attributes) }} +{{ optional::declare_attrs(prefix="Event" ~ event_name, marker="Event" ~ event_name, attrs=event.attributes) }} + +// Event adds an event to the span. +func (s *{{span_name | struct_name}}Span) Event{{ event.event_name | function_name }}( + {%- for attr in event.attributes | required %} + {{attr.id | field_name}} Event{{ event.event_name | function_name }}{{attr.id | struct_name}}ReqAttrWrapper, + {%- endfor %} + optionalAttributes ...{{ optional::attr_type(prefix="Event" ~ event_name) }}, +) *{{span_name | struct_name}}Span { + eventOptions := []trace.EventOption{ + {%- for attr in event.attributes | with_value %} + trace.WithAttributes(attribute.{{ attr.id | function_name }}Key.{{attr.type | type_mapping(enum=attr.id) | function_name}}({{attr.value | value}})), + {%- endfor %} + {%- for attr in event.attributes | required %} + trace.WithAttributes({{ attr.id | field_name }}.Attribute()), + {%- endfor %} + } + for _, opt := range optionalAttributes { + eventOptions = append(eventOptions, trace.WithAttributes(opt.Attribute())) + } + s.span.AddEvent("{{ event.event_name }}", eventOptions...) + return s +} +{% endfor %} + +{%- endif %} + +{% for attr in attributes | not_required | without_value %} +// Attr{{ attr.id | function_name }} sets the optional attribute `{{ attr.id }}` for the span. +// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix="// ") }} +func (s *{{span_name | struct_name}}Span) {{attr.id | function_name}}OptAttr(value {{ attr.type | type_mapping(enum=attr.id | struct_name) }}) *{{span_name | struct_name}}Span { + s.span.SetAttributes(attribute.{{ attr.id | field_name }}Key.{{ attr.type | type_mapping(enum=attr.id) | function_name }}(value)) + return s +} +{% endfor %} + +func (s *{{span_name | struct_name}}Span) StatusOk() *{{span_name | struct_name}}Span { + s.span.SetStatus(codes.Ok, "") + return s +} + +// Error sets the error for the span. +func (s *{{span_name | struct_name}}Span) Error(err error, description string) *{{span_name | struct_name}}Span { + s.span.SetStatus(codes.Error, description) + s.span.RecordError(err) + return s +} + +// Context returns the context of the current span. +func (s *{{span_name | struct_name }}Span) Context() context.Context { return s.ctx } + +// End ends the span with status OK. +func (s *{{span_name | struct_name}}Span) EndWithOk() { + s.span.SetStatus(codes.Ok, "") + s.span.End() +} + +// End ends the span with status Error and a given description. +func (s *{{span_name | struct_name}}Span) EndWithError(err error, description string) { + s.span.SetStatus(codes.Error, description) + s.span.RecordError(err) + s.span.End() +} + +// End ends the span. +func (s *{{span_name | struct_name}}Span) End() { + s.span.End() +} diff --git a/templates/go/required_attrs.macro.tera b/templates/go/required_attrs.macro.tera new file mode 100644 index 00000000..793f628b --- /dev/null +++ b/templates/go/required_attrs.macro.tera @@ -0,0 +1,54 @@ +{% macro declare_args(attrs) -%} + {%- for attr in attrs | required | without_value %} + {{attr.id | arg_name}} {{attr.id | struct_name}}ReqAttrWrapper, + {%- endfor %} +{% endmacro declare_args %} + + +{% macro declare_attrs(prefix="", attrs) -%} +{% set require_attrs = attrs | required | without_value %} +{% if require_attrs | length > 0 -%} +// =============================================== +// ====== Definition of required attributes ====== +// =============================================== + +{% for attr in require_attrs | without_enum %} +// {{prefix}}{{attr.id | struct_name}}ReqAttr is a wrapper for a required attribute. +// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix="// ") }} +func {{prefix}}{{attr.id | struct_name}}ReqAttr(v {{ attr.type | type_mapping(enum=attr.id | struct_name) }}) {{prefix}}{{attr.id | struct_name}}ReqAttrWrapper { + return {{prefix}}{{attr.id | struct_name}}ReqAttrWrapper{v} +} +// {{prefix}}{{attr.id | struct_name}}ReqAttrWrapper is a wrapper for the attribute `{{attr.id}}`. +// Use the function {{prefix}}{{attr.id | struct_name}}ReqAttr(value) to create an instance. +type {{prefix}}{{attr.id | struct_name}}ReqAttrWrapper struct { {{ attr.type | type_mapping(enum=attr.id | struct_name) }} } +func (w {{prefix}}{{attr.id | struct_name}}ReqAttrWrapper) Attribute() otel_attr.KeyValue { + return attribute.{{ attr.id | field_name }}Key.String(w.{{ attr.type | type_mapping(enum=attr.id | struct_name) }}) +} + +{% endfor %} + +{% for attr in require_attrs | with_enum %} +// {{prefix}}{{attr.id | struct_name}}ReqAttr is a wrapper for a required attribute. +// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix="// ") }} +func {{prefix}}{{attr.id | struct_name}}ReqAttr(v {{ attr.type | type_mapping(enum=attr.id | struct_name) }}) {{prefix}}{{attr.id | struct_name}}ReqAttrWrapper { + return {{prefix}}{{attr.id | struct_name}}ReqAttrWrapper{v} +} +// {{prefix}}{{attr.id | struct_name}}ReqAttrWrapper is a wrapper for the attribute `{{attr.id}}`. +// Use the function {{prefix}}{{attr.id | struct_name}}ReqAttr(value) to create an instance. +type {{prefix}}{{attr.id | struct_name}}ReqAttrWrapper struct { {{ attr.type | type_mapping(enum=attr.id | struct_name) }} } +func (w {{prefix}}{{attr.id | struct_name}}ReqAttrWrapper) Attribute() otel_attr.KeyValue { + return attribute.{{ attr.id | field_name }}Key.String(w.{{ attr.type | type_mapping(enum=attr.id | struct_name) }}) +} + +type {{attr.id | struct_name}} string +const ( +{% for variant in attr.type.members %} + // {{variant.id | struct_name}} is a possible value of {{attr.id | struct_name}}. + // {{ [variant.brief, variant.note] | comment(prefix=" // ") }} + {{variant.id | struct_name}} {{attr.id | struct_name}} = "{{variant.id}}" +{%- endfor %} +) +{% endfor %} + +{%- endif %} +{% endmacro declare_attrs %} \ No newline at end of file diff --git a/templates/rust/config.yaml b/templates/rust/config.yaml new file mode 100644 index 00000000..72595c63 --- /dev/null +++ b/templates/rust/config.yaml @@ -0,0 +1,15 @@ +file_name: snake_case +function_name: snake_case +arg_name: snake_case +struct_name: PascalCase +field_name: snake_case + +type_mapping: + int: i64 + double: f64 + boolean: bool + string: String + "int[]": "[i64]" + "double[]": "[f64]" + "boolean[]": "[bool]" + "string[]": "[String]" \ No newline at end of file diff --git a/templates/rust/eventer/mod.rs.tera b/templates/rust/eventer/mod.rs.tera new file mode 100644 index 00000000..6de050dc --- /dev/null +++ b/templates/rust/eventer/mod.rs.tera @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Generated OTel Client Loggers API. + +{% if schema.resource_events is defined %} +{% for event in schema.resource_events.events %} +/// Events `{{ event.event_name }}` (domain `{{ event.domain }}`) with the given attributes. +pub fn event_{{ event.domain | function_name }}_{{ event.event_name | function_name }}(attrs: {{ event.domain | struct_name }}{{ event.event_name | struct_name }}Attrs) {} + +/// event attributes for `{{ event.event_name }}` (domain `{{ event.domain }}`). +pub struct {{ event.domain | struct_name }}{{ event.event_name | struct_name }}Attrs { + {%- for attr in event.attributes %} + /// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix=" /// ") }} + pub {{attr.id | field_name}}: {% if attr is required %}{{ attr.type | type_mapping }}{% else %}Option<{{ attr.type | type_mapping }}>{% endif %}, + {%- endfor %} +} + +{% endfor %} +{% endif %} \ No newline at end of file diff --git a/templates/rust/meter/mod.rs.tera b/templates/rust/meter/mod.rs.tera new file mode 100644 index 00000000..d8f75f8f --- /dev/null +++ b/templates/rust/meter/mod.rs.tera @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Generated OTel Client Loggers API. + +{% if schema.resource_metrics is defined %} +{% if schema.resource_metrics.metrics is defined %} +{% for metric in schema.resource_metrics.metrics %} +/// Metric `{{ metric.name }}` to report u64 values. +pub fn {{ metric.name | function_name }}_u64() -> {{ metric.name | struct_name }}U64{{ metric.instrument | instrument | struct_name }} { + {{ metric.name | struct_name }}U64{{ metric.instrument | instrument | struct_name }}{} +} + +/// Metric `{{ metric.name }}` to report f64 values. +pub fn {{ metric.name | function_name }}_f64() -> {{ metric.name | struct_name }}F64{{ metric.instrument | instrument | struct_name }} { + {{ metric.name | struct_name }}F64{{ metric.instrument | instrument | struct_name }}{} +} + +pub struct {{ metric.name | struct_name }}U64{{ metric.instrument | instrument | struct_name }} { +} + +pub struct {{ metric.name | struct_name }}F64{{ metric.instrument | instrument | struct_name }} { +} + +impl {{ metric.name | struct_name }}U64{{ metric.instrument | instrument | struct_name }} { + {% if metric.instrument == "counter" %} + pub fn add(&mut self, value: u64, attrs: {{ metric.name | struct_name }}Attrs) {} + {% elif metric.instrument == "updowncounter" %} + pub fn add(&mut self, value: u64, attrs: {{ metric.name | struct_name }}Attrs) {} + {% elif metric.instrument == "gauge" %} + pub fn add(&mut self, value: u64, attrs: {{ metric.name | struct_name }}Attrs) {} + {% elif metric.instrument == "histogram" %} + pub fn record(&mut self, value: u64, attrs: {{ metric.name | struct_name }}Attrs) {} + {% endif %} +} + +impl {{ metric.name | struct_name }}F64{{ metric.instrument | instrument | struct_name }} { + {% if metric.instrument == "counter" %} + pub fn add(&mut self, value: f64, attrs: {{ metric.name | struct_name }}Attrs) {} + {% elif metric.instrument == "updowncounter" %} + pub fn add(&mut self, value: f64, attrs: {{ metric.name | struct_name }}Attrs) {} + {% elif metric.instrument == "gauge" %} + pub fn add(&mut self, value: f64, attrs: {{ metric.name | struct_name }}Attrs) {} + {% elif metric.instrument == "histogram" %} + pub fn record(&mut self, value: f64, attrs: {{ metric.name | struct_name }}Attrs) {} + {% endif %} +} + +/// Metric attributes for `{{ metric.name }}`. +pub struct {{ metric.name | struct_name }}Attrs { + {%- for attr in metric.attributes %} + /// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix=" /// ") }} + pub {{attr.id | arg_name}}: {% if attr is required %}{{ attr.type | type_mapping }}{% else %}Option<{{ attr.type | type_mapping }}>{% endif %}, + {%- endfor %} +} + +{% endfor %} +{% endif %} +{% endif %} + + +{% if schema.resource_metrics is defined %} +{% if schema.resource_metrics.metric_groups is defined %} +{% for metric in schema.resource_metrics.metric_groups %} +/// Multivariate metric `{{ metric.id }}`. +pub fn {{ metric.id | function_name }}() -> {{ metric.id | struct_name }} { + {{ metric.id | struct_name }}{} +} + +pub struct {{ metric.id | struct_name }} { +} + +impl {{ metric.id | struct_name }} { + pub fn report(&mut self, metrics: {{ metric.id | struct_name }}Metrics, attrs: {{ metric.id | struct_name }}Attrs) {} +} + +/// Multivariate metrics for `{{ metric.id }}`. +pub struct {{ metric.id | struct_name }}Metrics { + {%- for metric in metric.metrics %} + /// {{ [metric.brief, metric.note] | comment(prefix=" /// ") }} + pub {{metric.name | arg_name}}: u64, + {%- endfor %} +} + +/// Metric attributes for `{{ metric.id }}`. +pub struct {{ metric.id | struct_name }}Attrs { + {%- for attr in metric.attributes %} + /// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix=" /// ") }} + pub {{attr.id | arg_name}}: {% if attr is required %}{{ attr.type | type_mapping }}{% else %}Option<{{ attr.type | type_mapping }}>{% endif %}, + {%- endfor %} +} + +{% endfor %} +{% endif %} +{% endif %} \ No newline at end of file diff --git a/templates/rust/mod.rs.tera b/templates/rust/mod.rs.tera new file mode 100644 index 00000000..26d821c5 --- /dev/null +++ b/templates/rust/mod.rs.tera @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Generated OTel Client API. + +pub mod meter; +pub mod eventer; +pub mod tracer; \ No newline at end of file diff --git a/templates/rust/span.tera.bak b/templates/rust/span.tera.bak new file mode 100644 index 00000000..9cd38e8b --- /dev/null +++ b/templates/rust/span.tera.bak @@ -0,0 +1,142 @@ +{# Define the file name for the generated code #} +{%- set file_name = id | file_name -%} +{{- config(file_name="tracers/" ~ file_name ~ ".rs") -}} +// SPDX-License-Identifier: Apache-2.0 + +//! Code generated by OTel Weaver to define the span `{{ id }}`. + +pub enum Status { + Unset, + Error, + Ok, +} + +{%- set required_attrs = attributes | required -%} +{%- set not_required_attrs = attributes | not_required -%} +{%- if required_attrs | length > 0 %} +/// Starts a new named `{{ id }}` span with the given required attributes. +pub fn start( + name: &str, + required_attrs: {{id | struct_name}}Attrs, +) -> {{id | struct_name}}Span { + {{id | struct_name}}Span { + {{id | field_name}}_attrs: required_attrs, + {{id | field_name}}_opt_attrs: Default::default(), + events: Vec::new(), + } +} + +/// Starts a new named `{{ id }}` span with the given required attributes +/// and the optional attributes. +pub fn start_with_opt_attrs( + name: &str, + required_attrs: {{id | struct_name}}Attrs, + optional_attrs: {{id | struct_name}}OptAttrs, + ) -> {{id | struct_name}}Span { + {{id | struct_name}}Span { + {{id | field_name}}_attrs: required_attrs, + {{id | field_name}}_opt_attrs: optional_attrs, + events: Vec::new(), + } +} +{%- else %} +/// Starts a new named `{{ id }}` span. +pub fn start(name: &str) -> {{id | struct_name}}Span { + {{id | struct_name}}Span { + {{id | field_name}}_opt_attrs: {{id | struct_name}}OptAttrs::default(), + } +} + +/// Starts a new named `{{ id }}` span with the given optional attributes. +pub fn start_with_opt_attrs( + name: &str, + optional_attrs: {{id | struct_name}}OptAttrs, + ) -> {{id | struct_name}}Span { + {{id | struct_name}}Span { + {{id | field_name}}_opt_attrs: optional_attrs, + } +} +{%- endif %} + +pub struct {{id | struct_name}}Span { +{%- if required_attrs | length > 0 %} + /// Required span attributes for `{{ id }}`. + {{id | field_name}}_attrs: {{id | struct_name}}Attrs, +{%- endif -%} +{%- if not_required_attrs | length > 0 %} + /// Optional span attributes for `{{ id }}`. + {{id | field_name}}_opt_attrs: {{id | struct_name}}OptAttrs, +{%- endif %} +{%- if events | length > 0 -%} + /// Events for `{{ id }}`. + events: Vec, +{%- endif %} +} + +{% if required_attrs | length > 0 -%} +/// Required span attributes for `{{ id }}`. +pub struct {{id | struct_name}}Attrs { +{%- for attr in required_attrs %} + /// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix=" /// ") }} + pub {{attr.id | arg_name}}: {{ attr.type | type_mapping }}, +{%- endfor %} +} +{%- endif %} + +{% if not_required_attrs | length > 0 -%} +/// Optional span attributes for `{{ id }}`. +#[derive(Default)] +pub struct {{id | struct_name}}OptAttrs { +{%- for attr in not_required_attrs %} + /// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix=" /// ") }} + pub {{attr.id | arg_name}}: Option<{{ attr.type | type_mapping }}>, +{%- endfor %} +} +{%- endif %} + +{% if events | length > 0 -%} +pub enum Event { + {% for event in events -%} + {{ event.id | struct_name}} { + {%- for attr in event.attributes %} + /// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix=" /// ") }} + {%- if attr is required %} + {{attr.id | field_name}}: {{ attr.type | type_mapping }}, + {% else %} + {{attr.id | field_name}}: Option<{{ attr.type | type_mapping }}>, + {% endif -%} + {% endfor %} + }, + {%- endfor %} +} +{%- endif %} + + +impl {{id | struct_name}}Span { + {%- for attr in not_required_attrs %} + /// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix=" /// ") }} + pub fn attr_{{attr.id | function_name}}(&mut self, value: {{ attr.type | type_mapping }}) { + self.{{id | field_name}}_opt_attrs.{{attr.id | field_name}} = Some(value); + } + {% endfor %} + + {% if events | length > 0 -%} + /// Adds an event to the span. + pub fn event(&mut self, event: Event) { + self.events.push(event); + } + {%- endif %} + + pub fn status(&self, status: Status) {} + pub fn error(&self, err: &dyn std::error::Error) {} + + /// Ends the span. + pub fn end(self) {} + + {%- if not_required_attrs | length > 0 %} + /// Ends the span with the optional attributes. + pub fn end_with_opt_attrs(mut self, optional_attrs: {{id | struct_name}}OptAttrs) { + self.{{id | field_name}}_opt_attrs = optional_attrs; + } + {%- endif %} +} \ No newline at end of file diff --git a/templates/rust/tracer/mod.rs.tera b/templates/rust/tracer/mod.rs.tera new file mode 100644 index 00000000..2ac39fb3 --- /dev/null +++ b/templates/rust/tracer/mod.rs.tera @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Generated OTel Client Tracers API. + +pub enum Status { + Unset, + Error, + Ok, +} + +{% if schema.resource_spans is defined %} +{% for span in schema.resource_spans.spans %} + +{%- set required_attrs = span.attributes | required -%} +{%- set not_required_attrs = span.attributes | not_required -%} +{%- if required_attrs | length > 0 %} +/// Starts a new named `{{ span.span_name }}` span with the given required attributes. +pub fn start_{{ span.span_name | function_name }}( + required_attrs: {{span.span_name | struct_name}}Attrs, +) -> {{span.span_name | struct_name}}Span { + {{span.span_name | struct_name}}Span { + {{span.span_name | field_name}}_attrs: required_attrs, + {{span.span_name | field_name}}_opt_attrs: Default::default(), + events: Vec::new(), + } +} + +/// Starts a new named `{{ span.span_name }}` span with the given required attributes +/// and the optional attributes. +pub fn start_{{ span.span_name | function_name }}_with_opt_attrs( + required_attrs: {{span.span_name | struct_name}}Attrs, + optional_attrs: {{span.span_name | struct_name}}OptAttrs, +) -> {{span.span_name | struct_name}}Span { + {{span.span_name | struct_name}}Span { + {{span.span_name | field_name}}_attrs: required_attrs, + {{span.span_name | field_name}}_opt_attrs: optional_attrs, + events: Vec::new(), + } +} +{%- else %} +/// Starts a new named `{{ span.span_name }}` span. +pub fn start_{{ span.span_name | function_name }}() -> {{span.span_name | struct_name}}Span { + {{span.span_name | struct_name}}Span { + {{span.span_name | field_name}}_opt_attrs: {{span.span_name | struct_name}}OptAttrs::default(), + } +} + +/// Starts a new named `{{ span.span_name }}` span with the given optional attributes. +pub fn start_{{ span.span_name | function_name }}_with_opt_attrs( + optional_attrs: {{span.span_name | struct_name}}OptAttrs, +) -> {{span.span_name | struct_name}}Span { + {{span.span_name | struct_name}}Span { + {{span.span_name | field_name}}_opt_attrs: optional_attrs, + } +} +{%- endif %} + +/// {{span.span_name | struct_name}}Span is a span for `{{ span.span_name }}`. +pub struct {{span.span_name | struct_name}}Span { + {%- if required_attrs | length > 0 %} + /// Required span attributes for `{{ span.span_name }}`. + {{span.span_name | field_name}}_attrs: {{span.span_name | struct_name}}Attrs, + {%- endif -%} + {%- if not_required_attrs | length > 0 %} + /// Optional span attributes for `{{ span.span_name }}`. + {{span.span_name | field_name}}_opt_attrs: {{span.span_name | struct_name}}OptAttrs, + {%- endif %} + {%- if span.events | length > 0 %} + /// Events for `{{ span.span_name }}`. + events: Vec<{{ span.span_name | struct_name }}Event>, + {%- endif %} +} + +{% if required_attrs | length > 0 -%} +/// Required span attributes for `{{ span.span_name }}`. +pub struct {{span.span_name | struct_name}}Attrs { + {%- for attr in required_attrs %} + /// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix=" /// ") }} + pub {{attr.id | field_name}}: {{ attr.type | type_mapping }}, + {%- endfor %} +} +{%- endif %} + +{% if not_required_attrs | length > 0 -%} +/// Optional span attributes for `{{ span.span_name }}`. +#[derive(Default)] +pub struct {{span.span_name | struct_name}}OptAttrs { + {%- for attr in not_required_attrs %} + /// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix=" /// ") }} + pub {{attr.id | field_name}}: Option<{{ attr.type | type_mapping }}>, + {%- endfor %} +} +{%- endif %} + +{% if span.events | length > 0 -%} +pub enum {{ span.span_name | struct_name }}Event { +{% for event in span.events -%} +{{ event.event_name | struct_name}} { +{%- for attr in event.attributes %} +/// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix=" /// ") }} +{%- if attr is required %} +{{attr.id | field_name}}: {{ attr.type | type_mapping }}, +{% else %} +{{attr.id | field_name}}: Option<{{ attr.type | type_mapping }}>, +{% endif -%} +{% endfor %} +}, +{%- endfor %} +} +{%- endif %} + + +impl {{span.span_name | struct_name}}Span { + {%- for attr in not_required_attrs %} + /// {{ [attr.brief, attr.note, "", "# Examples", attr.examples] | comment(prefix=" /// ") }} + pub fn attr_{{attr.id | function_name}}(&mut self, value: {{ attr.type | type_mapping }}) { + self.{{span.span_name | field_name}}_opt_attrs.{{attr.id | field_name}} = Some(value); + } + {% endfor %} + + {% if span.events | length > 0 -%} + /// Adds an event to the span. + pub fn event(&mut self, event: {{ span.span_name | struct_name }}Event) { + self.events.push(event); + } + {%- endif %} + + pub fn status(&self, status: Status) {} + pub fn error(&self, err: &dyn std::error::Error) {} + + /// Ends the span. + pub fn end(self) {} + + {%- if not_required_attrs | length > 0 %} + /// Ends the span with the optional attributes. + pub fn end_with_opt_attrs(mut self, optional_attrs: {{span.span_name | struct_name}}OptAttrs) { + self.{{span.span_name | field_name}}_opt_attrs = optional_attrs; + } + {%- endif %} +} +{% endfor %} +{% endif %} \ No newline at end of file