diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..53e8c5bf Binary files /dev/null and b/.DS_Store differ diff --git a/apps/opentelemetry_api_experimental/ARCHITECTURE.md b/apps/opentelemetry_api_experimental/ARCHITECTURE.md new file mode 100644 index 00000000..1e9563c9 --- /dev/null +++ b/apps/opentelemetry_api_experimental/ARCHITECTURE.md @@ -0,0 +1,40 @@ +opentelemetry_api_experimental – Architecture (Metrics API) +========================================================== + +Purpose: Describe the instrumentation surface and how it delegates to the SDK so non-Erlang reviewers can reason about behavior. + +API surface +----------- +- Erlang macros in `include/otel_meter.hrl` provide a minimal, ergonomic API: + - Instrument creation: `?create_counter`, `?create_updown_counter`, `?create_histogram`, and observable counterparts. + - Recording: `?counter_add`, `?updown_counter_add`, `?histogram_record`. + - Observables: `?register_callback` for one or many instruments. +- Macros resolve the “current meter” automatically from the module’s application scope and call underlying modules (`otel_meter`, `otel_counter`, `otel_histogram`, etc.). + +Delegation to SDK +----------------- +- Instrument creation and recording calls are forwarded to the SDK’s MeterProvider (`otel_meter_server`). +- The SDK applies views, aggregations, temporality, and export; the API does not implement these rules. + +Elixir wrapper modules +---------------------- +- `lib/open_telemetry/*.ex` expose equivalent APIs for Elixir users, delegating to the same underlying functions. + +Conceptual flow +--------------- +1) Application code includes `otel_meter.hrl` and uses macros to create instruments and record measurements. +2) Macros capture the current context and meter, and call the appropriate API module. +3) The API module calls into the SDK MeterProvider, which routes to streams and aggregations. + +Interoperability notes +---------------------- +- Attributes are passed through as maps; filtering happens in the SDK via Views. +- Units and descriptions are provided at creation and propagated to the SDK. +- Callback lifecycles for observables are controlled by the SDK’s Readers during collection. + +See also +-------- +- `SPEC_COMPLIANCE.md` for spec crosswalk and code anchors. +- Package README for quickstart and macro cheat sheet. + + diff --git a/apps/opentelemetry_api_experimental/GLOSSARY.md b/apps/opentelemetry_api_experimental/GLOSSARY.md new file mode 100644 index 00000000..a5763788 --- /dev/null +++ b/apps/opentelemetry_api_experimental/GLOSSARY.md @@ -0,0 +1,15 @@ +Glossary (API) +============== + +- Instrument: A Counter, UpDownCounter, Histogram, or Observable variant used to capture measurements. +- Measurement: A numeric value plus optional attributes recorded against an instrument. +- Attributes: Key/value pairs attached to a measurement for dimensionality. +- Meter: Factory for instruments within an instrumentation scope; resolved implicitly by macros. +- Instrumentation scope: Name/version/schema of the library emitting telemetry. +- View: SDK rule that selects instruments, filters attributes, and chooses an aggregation. +- Aggregation: Sum, Last Value, Explicit Bucket Histogram, or Drop. +- Temporality: Delta (per-interval) or Cumulative (running total), selected by the Reader. +- Reader: SDK component that collects and exports metrics. +- Exporter: Component that serializes and sends metrics (Console, OTLP). + + diff --git a/apps/opentelemetry_api_experimental/README.md b/apps/opentelemetry_api_experimental/README.md index d7be7122..1b7f5990 100644 --- a/apps/opentelemetry_api_experimental/README.md +++ b/apps/opentelemetry_api_experimental/README.md @@ -164,3 +164,13 @@ AtomGauge = ?create_observable_gauge(AtomCountName, #{description => <<"Number o The callbacks are run when the Metric Reader collects metrics for export. See the Experimental SDK's `README.md` for more details on Metric Readers and their configuration. + +## For non-Erlang reviewers + +- **What to read first**: See `SPEC_COMPLIANCE.md` in this folder for a plain-language crosswalk of the Metrics API to the OpenTelemetry spec and where behaviors are implemented. +- **How to validate quickly**: + - Ensure the experimental SDK app `opentelemetry_experimental` is included and configured with a console reader (see that app's README). + - Use the macros in `include/otel_meter.hrl` to create an instrument and record a value. + - Expect to see aggregated metric points printed by the SDK's console exporter. +- **Where defaults live**: Aggregation and temporality are SDK concerns; see `../opentelemetry_experimental/SPEC_COMPLIANCE.md` for defaults and reader behavior. + diff --git a/apps/opentelemetry_api_experimental/REVIEW_GUIDE.md b/apps/opentelemetry_api_experimental/REVIEW_GUIDE.md new file mode 100644 index 00000000..def3f655 --- /dev/null +++ b/apps/opentelemetry_api_experimental/REVIEW_GUIDE.md @@ -0,0 +1,81 @@ +Review Guide (Metrics API) +========================== + +Audience: Validate instrumentation behavior using macros, without reading Erlang. + +Prerequisites +------------- +- Ensure `opentelemetry_experimental` (SDK) is included and configured with a console reader (see SDK REVIEW_GUIDE/README). +- Include the API header in your module: `-include_lib("opentelemetry_api_experimental/include/otel_meter.hrl").` + +1) Create instruments +--------------------- +Goal: Instruments can be created with name, unit, and description. + +Example (Counter): +```erlang +?create_counter(app_counter, #{description => <<"Requests">>, unit => '1'}). +``` + +Expect: No error on creation; SDK will create streams on first record. + +2) Record synchronous measurements +---------------------------------- +Goal: Recording calls forward context, meter, and attributes to SDK. + +Counter: +```erlang +?counter_add(app_counter, 5, #{host => <<"a">>}). +``` + +UpDownCounter and Histogram similarly: +```erlang +?updown_counter_add(app_load, -2, #{}). +?histogram_record(latency_ms, 37, #{path => <<"/ping">>}). +``` + +Expect (with console reader): Sum/Histogram points printed periodically. + +3) Observable instruments +------------------------- +Goal: Callback-based measurements appear only on collection. + +Single instrument with callback at creation: +```erlang +?create_observable_gauge(proc_count, + fun(_) -> [{erlang:system_info(process_count), #{}}] end, + [], + #{description => <<"Proc count">>, unit => '1'}). +``` + +Multiple instruments with one callback: +```erlang +G1 = ?create_observable_gauge(proc_count, #{}), +G2 = ?create_observable_gauge(atom_count, #{}), +?register_callback([G1, G2], + fun(_) -> + [{proc_count, [{erlang:system_info(process_count), #{}}]}, + {atom_count, [{erlang:system_info(atom_count), #{}}]}] + end, []). +``` + +Expect: Values printed each collection tick; nothing in between. + +4) Attribute filtering via SDK Views +------------------------------------ +Goal: Attributes are forwarded intact by API; filtering happens in SDK. +- Configure a View in SDK with `attribute_keys` limited to `[host]`. +- Record with extra attributes; expect only `host` to remain. + +5) Negative value handling +-------------------------- +Goal: API forwards values; SDK enforces monotonicity. +- Add negative values to a Counter; the SDK will discard (see SDK REVIEW_GUIDE step 1). + +Pointers +-------- +- Architecture: `ARCHITECTURE.md` +- Spec crosswalk: `SPEC_COMPLIANCE.md` +- SDK validation: `../opentelemetry_experimental/REVIEW_GUIDE.md` + + diff --git a/apps/opentelemetry_api_experimental/SPEC_COMPLIANCE.md b/apps/opentelemetry_api_experimental/SPEC_COMPLIANCE.md new file mode 100644 index 00000000..f2568316 --- /dev/null +++ b/apps/opentelemetry_api_experimental/SPEC_COMPLIANCE.md @@ -0,0 +1,108 @@ +opentelemetry_api_experimental – Spec Compliance Crosswalk +========================================================== + +Audience: Reviewers validating OpenTelemetry Metrics API semantics without Erlang knowledge. + +Scope: Instrument types, recording semantics, observables, attributes, scope propagation, and how these map to SDK behavior. + +How to read code anchors: Blocks below cite file, start:end lines. They are illustrative; exact line numbers may drift slightly across commits. + +1) Instrument kinds supported +----------------------------- +- Counter, UpDownCounter, Histogram +- ObservableCounter, ObservableUpDownCounter, ObservableGauge + +API surface (macros) maps to implementation functions. The macros resolve the current meter automatically and call the appropriate module. + +```41:52:apps/opentelemetry_api_experimental/include/otel_meter.hrl +-define(counter_add(Name, Number, Attributes), + otel_counter:add(otel_ctx:get_current(), ?current_meter, Name, Number, Attributes)). + +-define(histogram_record(Name, Number, Attributes), + otel_histogram:record(otel_ctx:get_current(), ?current_meter, Name, Number, Attributes)). + +-define(register_callback(Instruments, Callback, CallbackArgs), + otel_meter:register_callback(?current_meter, Instruments, Callback, CallbackArgs)). +``` + +Where implemented (Elixir shims): +- `lib/open_telemetry/*.ex` provide Elixir wrappers matching the same instrument set. + +2) Instrument creation +---------------------- +Macros to create instruments delegate to `otel_meter` (API) which communicates with the SDK meter provider. + +```10:35:apps/opentelemetry_api_experimental/include/otel_meter.hrl +-define(create_counter(Name, Opts), + otel_meter:create_counter(?current_meter, Name, Opts)). +-define(create_histogram(Name, Opts), + otel_meter:create_histogram(?current_meter, Name, Opts)). +-define(create_updown_counter(Name, Opts), + otel_meter:create_updown_counter(?current_meter, Name, Opts)). +-define(create_observable_counter(Name, Callback, CallbackArgs, Opts), + otel_meter:create_observable_counter(?current_meter, Name, Callback, CallbackArgs, Opts)). +``` + +Behavioral expectations (from spec): +- Instrument name uniqueness per meter. +- Observable instruments tie to callbacks; callbacks are invoked on collection. + +Where enforced (SDK): see SDK crosswalk (Streams/View matching and reader callback execution). + +3) Recording semantics +---------------------- +Synchronous instruments record via macros which capture the current context and meter. + +```41:49:apps/opentelemetry_api_experimental/include/otel_meter.hrl +-define(counter_add(Name, Number, Attributes), + otel_counter:add(otel_ctx:get_current(), ?current_meter, Name, Number, Attributes)). +-define(updown_counter_add(Name, Number, Attributes), + otel_updown_counter:add(otel_ctx:get_current(), ?current_meter, Name, Number, Attributes)). +``` + +Spec alignment notes: +- Attributes: arbitrary key/value map is forwarded; filtering is performed by SDK views. +- Units/descriptions are provided at creation time and carried to SDK. + +4) Observable semantics +----------------------- +Observable instruments can be created with a callback, or callbacks can later be registered for multiple instruments. + +```50:52:apps/opentelemetry_api_experimental/include/otel_meter.hrl +-define(register_callback(Instruments, Callback, CallbackArgs), + otel_meter:register_callback(?current_meter, Instruments, Callback, CallbackArgs)). +``` + +Spec alignment notes: +- Callback returns a list of measurements with attributes per instrument. +- Callbacks are run when the configured Metric Reader collects (see SDK crosswalk). + +5) Scope and resource propagation +--------------------------------- +The API resolves a “current meter” tied to the application/module scope. The SDK attaches instrumentation scope and resource to exported metrics. + +```7:9:apps/opentelemetry_api_experimental/include/otel_meter.hrl +-define(current_meter, opentelemetry_experimental:get_meter( + opentelemetry:get_application_scope(?MODULE))). +``` + +6) Defaults (aggregation and temporality) +----------------------------------------- +Defaults are an SDK concern. API defers to SDK’s per-instrument defaults and per-reader temporality. See SDK SPEC_COMPLIANCE.md for exact mapping. + +Verification steps (manual) +--------------------------- +1. Configure SDK reader with console exporter and 1s interval (see SDK README). +2. Create each instrument via macros. +3. Record values; observe expected aggregation on console. +4. Register an observable callback; confirm values appear only on collection cycles. +5. Add a view in SDK that filters attributes; confirm only allowed keys appear. + +Related code/tests +------------------ +- API macros: `apps/opentelemetry_api_experimental/include/otel_meter.hrl` +- Erlang modules: `apps/opentelemetry_api_experimental/src/otel_*` +- Elixir wrappers: `apps/opentelemetry_api_experimental/lib/open_telemetry/*.ex` +- Tests: `apps/opentelemetry_api_experimental/test/otel_metrics_test.exs` + + diff --git a/apps/opentelemetry_api_experimental/TEST_COVERAGE.md b/apps/opentelemetry_api_experimental/TEST_COVERAGE.md new file mode 100644 index 00000000..8346c337 --- /dev/null +++ b/apps/opentelemetry_api_experimental/TEST_COVERAGE.md @@ -0,0 +1,24 @@ +Test Coverage (API) +=================== + +Files +----- +- `test/otel_metrics_test.exs` – Elixir tests covering API instrumentation semantics. + +Spec areas exercised +-------------------- +- Instrument creation via macros. +- Recording of counters, histograms, and updown counters with attributes. +- Observable callback behavior when paired with SDK readers. + +How to run +---------- +- Using Mix (from umbrella root with Elixir available): + - `mix test apps/opentelemetry_api_experimental` + +Gaps to consider +---------------- +- Negative value propagation relies on SDK enforcement; see SDK tests for monotonic handling. +- Per-reader temporality behavior belongs to SDK tests. + + diff --git a/apps/opentelemetry_experimental/ARCHITECTURE.md b/apps/opentelemetry_experimental/ARCHITECTURE.md new file mode 100644 index 00000000..fee6dc3b --- /dev/null +++ b/apps/opentelemetry_experimental/ARCHITECTURE.md @@ -0,0 +1,108 @@ +opentelemetry_experimental – Architecture (Metrics SDK) +====================================================== + +Purpose: Explain how the experimental Metrics SDK is structured so non-Erlang reviewers can validate behavior against the OpenTelemetry spec. + +Key components +-------------- +- MeterProvider: `otel_meter_server` + - Creates meters, registers views, accepts instruments and callbacks, and orchestrates per-reader streams and aggregations. +- View engine: `otel_view` + - Compiles selection criteria (instrument name/kind/unit, meter scope) to ETS match specifications; matches instruments to views; supplies attribute filtering and aggregation overrides. +- Readers: `otel_metric_reader` + - Runs collection (periodic or pull), applies temporality, exports via configured exporter. +- Aggregations: `otel_aggregation_sum`, `otel_aggregation_last_value`, `otel_aggregation_histogram_explicit`, `otel_aggregation_drop` + - Maintain and collect metric state per stream; enforce spec semantics. +- Exporters: `otel_metric_exporter_console`, `otel_exporter_metrics_otlp` + - Serialize and send metrics to stdout or OTLP backends. +- Observables: `otel_observables` + - Executes callbacks on collection; supports multi-instrument callbacks. +- Exemplars: `otel_metric_exemplar_*` + - Select and store exemplars according to filter/reservoir configuration. + +Data model and storage +---------------------- +ETS tables (per MeterProvider instance): +- `instruments_tab`: Registry of created instruments (per meter, per name). +- `callbacks_tab`: Registered observable callbacks keyed by reader id. +- `streams_tab`: View matches per reader; each stream holds aggregation config, temporality, attribute filter, exemplar reservoir. +- `metrics_tab`: Aggregation state bucketed by stream name and attribute set. +- `exemplars_tab`: Exemplar storage per stream. + +Supervision and processes +------------------------- +- `otel_meter_server` is a `gen_server` registered as the global MeterProvider. +- Each `otel_metric_reader` is a `gen_server` with an optional periodic timer. +- Exporters are invoked synchronously from readers during `collect`. + +Recording flow (synchronous instruments) +--------------------------------------- +1) Instrument creation inserts into `instruments_tab` and computes streams by matching views per reader. +2) Application code records a measurement; MeterProvider finds streams for the instrument and updates aggregations. +3) Negative values for monotonic instruments are discarded. + +```mermaid +sequenceDiagram + participant App + participant MeterServer as otel_meter_server + participant Streams as streams_tab + participant Metrics as metrics_tab + App->>MeterServer: record(Name, Value, Attributes) + MeterServer->>Streams: lookup streams for Name + MeterServer->>Metrics: aggregation update per stream + Note right of MeterServer: Reject negative for Counter/Histogram +``` + +Collection flow (observables and export) +---------------------------------------- +1) Reader wakes (timer) or is triggered (pull) and runs `collect`. +2) Reader fetches callbacks for its id and executes them, populating metrics for observable instruments. +3) Reader iterates streams for its id, collects aggregated data (respecting temporality), and calls exporter with resource and scope. + +```mermaid +sequenceDiagram + participant Reader as otel_metric_reader + participant MeterServer as otel_meter_server + participant Callbacks as callbacks_tab + participant Streams as streams_tab + participant Metrics as metrics_tab + participant Exporter + Reader->>MeterServer: register_with_server (startup) + Reader->>Callbacks: get callbacks for ReaderId + Reader->>MeterServer: run_callbacks(...) + Reader->>Streams: iterate streams for ReaderId + Reader->>Metrics: collect per stream (temporality) + Reader->>Exporter: export(metrics, resource) +``` + +Views and streams +----------------- +- Adding a view triggers recomputation of streams for all existing instruments. +- Streams are per reader; a single instrument may yield multiple streams (one per reader, plus per matching view), each with its own aggregation and temporality. +- Default behavior when no view matches: create a stream using the instrument defaults. + +Temporality and forgetting +-------------------------- +- Readers hold a mapping from instrument kind to temporality (delta or cumulative). +- Streams with delta temporality reset aggregation state after collection ("forget"). +- Observable instruments use "forget" semantics on each collection. + +Configuration surface +--------------------- +- Readers: `{module, config}` with keys: + - `export_interval_ms`: integer (periodic) or undefined (pull). + - `exporter`: `{Module, #{...}}` passed to exporter `init/1`. + - Optional: default aggregation and temporality mappings. +- Views: list of maps with `selector`, optional `name`, `description`, `attribute_keys`, `aggregation_module`, `aggregation_options` (e.g., histogram bucket boundaries). + +Responsibility boundaries +------------------------- +- API (`opentelemetry_api_experimental`) provides instrumentation macros and delegates to the SDK. +- SDK (`opentelemetry_experimental`) enforces aggregation rules, views, temporality, and export. + +See also +-------- +- `SPEC_COMPLIANCE.md` for spec crosswalk and code anchors. +- Package README for configuration examples. + + diff --git a/apps/opentelemetry_experimental/CONFIG_RECIPES.md b/apps/opentelemetry_experimental/CONFIG_RECIPES.md new file mode 100644 index 00000000..e48c1632 --- /dev/null +++ b/apps/opentelemetry_experimental/CONFIG_RECIPES.md @@ -0,0 +1,68 @@ +Configuration Recipes (Metrics SDK) +=================================== + +Console reader (periodic) +------------------------- +```erlang +{opentelemetry_experimental, + [{readers, [#{module => otel_metric_reader, + config => #{export_interval_ms => 1000, + exporter => {otel_metric_exporter_console, #{}}}}]}]}. +``` + +Drop all measurements +--------------------- +```erlang +ViewCriteria = #{instrument_name => '*'}, +ViewConfig = #{aggregation_module => otel_aggregation_drop}, +ok = otel_meter_server:add_view(ViewCriteria, ViewConfig). +``` + +Attribute allow-list (keep `host` only) +--------------------------------------- +```erlang +ViewCriteria = #{instrument_name => app_request_count}, +ViewConfig = #{attribute_keys => [host]}, +ok = otel_meter_server:add_view(ViewCriteria, ViewConfig). +``` + +Explicit histogram buckets +-------------------------- +```erlang +ViewCriteria = #{instrument_name => latency_ms}, +ViewConfig = #{aggregation_module => otel_aggregation_histogram_explicit, + aggregation_options => #{explicit_bucket_boundaries => [0, 5, 10, 25, 50, 100, 250, 500]}}, +ok = otel_meter_server:add_view(ViewCriteria, ViewConfig). +``` + +Two readers with different temporalities +---------------------------------------- +Example: one delta, one cumulative (merge with defaults as needed). +```erlang +Readers = [ + #{module => otel_metric_reader, + config => #{export_interval_ms => 1000, + exporter => {otel_metric_exporter_console, #{}}}}, + #{module => otel_metric_reader, + config => #{export_interval_ms => 1000, + default_temporality_mapping => #{counter => ?TEMPORALITY_DELTA}, + exporter => {otel_metric_exporter_console, #{}}}} +], +{opentelemetry_experimental, [{readers, Readers}]}. +``` + +OTLP exporter (metrics) +----------------------- +```erlang +{opentelemetry_experimental, + [{readers, [#{module => otel_metric_reader, + config => #{export_interval_ms => 1000, + exporter => {otel_exporter_metrics_otlp, #{endpoint => <<"http://localhost:4317">>}}}}]}]}. +``` + +Notes +----- +- Views can be configured via application env as a list under `{opentelemetry_experimental, views, [...]}`. +- Temporality mapping keys follow instrument kinds; unspecified kinds use defaults. + + diff --git a/apps/opentelemetry_experimental/GLOSSARY.md b/apps/opentelemetry_experimental/GLOSSARY.md new file mode 100644 index 00000000..e4a78965 --- /dev/null +++ b/apps/opentelemetry_experimental/GLOSSARY.md @@ -0,0 +1,15 @@ +Glossary (SDK) +============== + +- MeterProvider: SDK service that creates meters, manages views and readers. +- Instrument: Counter, UpDownCounter, Histogram, ObservableCounter, ObservableUpDownCounter, ObservableGauge. +- Stream: A per-Reader aggregation pipeline for an instrument (and view), with its own temporality and aggregation. +- View: Selection and processing configuration for instruments (criteria, attribute keys, aggregation). +- Aggregation: Implementation module that maintains/collects metric data (sum, last value, histogram, drop). +- Temporality: Strategy for reporting (delta or cumulative). Streams may reset (forget) after collection. +- Reader: Collector that triggers callbacks, iterates streams, and exports metrics. +- Exporter: Sends serialized metrics to a backend (console or OTLP). +- Exemplars: Sampled measurements attached to metrics for rich diagnostics. +- ETS: Erlang Term Storage used for in-memory tables (instruments, streams, metrics, exemplars). + + diff --git a/apps/opentelemetry_experimental/README.md b/apps/opentelemetry_experimental/README.md index 7db077f2..8e23d6fd 100644 --- a/apps/opentelemetry_experimental/README.md +++ b/apps/opentelemetry_experimental/README.md @@ -138,3 +138,14 @@ configured to export to the console every five seconds would look like: config => #{export_interval_ms => 5000, exporter => {otel_metric_exporter_console, #{}}}}]}]}, ``` + +## For non-Erlang reviewers + +- **What to read first**: See `SPEC_COMPLIANCE.md` in this folder for a plain-language crosswalk to the OpenTelemetry Metrics spec with code anchors. +- **How the pieces fit**: Instruments created via the API are matched to Views here to produce per-Reader Streams. Readers collect and export (periodic or pull). Aggregations and temporality are enforced per Reader. +- **Quick validation**: + - Add a console Reader (example above) and start the app. + - Create a Counter and record a value via the API macros. + - Observe a sum metric printed by the console exporter. + - Add a View to filter attributes or change histogram buckets; observe effect on output. + diff --git a/apps/opentelemetry_experimental/REVIEW_GUIDE.md b/apps/opentelemetry_experimental/REVIEW_GUIDE.md new file mode 100644 index 00000000..ff3fc2da --- /dev/null +++ b/apps/opentelemetry_experimental/REVIEW_GUIDE.md @@ -0,0 +1,111 @@ +Review Guide (Metrics SDK) +========================== + +Audience: Validate spec-correct behavior without reading Erlang. + +Prerequisites +------------- +- Include `opentelemetry_experimental` and `opentelemetry_api_experimental` apps. +- Configure a periodic console Reader (1s) via application env: + +```erlang +{opentelemetry_experimental, + [{readers, [#{module => otel_metric_reader, + config => #{export_interval_ms => 1000, + exporter => {otel_metric_exporter_console, #{}}}}]}]}. +``` + +1) Counter monotonicity +----------------------- +Goal: Negative values are discarded; only increases are summed. +- Create Counter `app_counter`; record `+5`, then `-3`. +- Expect: Console shows only positive increments; negative is ignored. + +Code anchor: +```386:401:apps/opentelemetry_experimental/src/otel_meter_server.erl +maybe_init_aggregate(_, _Meter, Value, #instrument{kind=Kind} = Instrument, _Stream, _Attributes) + when Value < 0, Kind == ?KIND_COUNTER orelse Kind == ?KIND_HISTOGRAM -> + ?LOG_INFO("Discarding negative value for instrument ~s of type ~s", [Instrument#instrument.name, Kind]), + ok; +``` + +2) UpDownCounter accepts negatives +---------------------------------- +Goal: UpDownCounter supports positive and negative adds. +- Create UpDownCounter `app_gauge`; record `+3`, `-2`. +- Expect: Sum reflects net value. + +3) Histogram with explicit buckets +---------------------------------- +Goal: View overrides buckets. +- Add View with `aggregation_module=otel_aggregation_histogram_explicit` and `aggregation_options` with custom `explicit_bucket_boundaries`. +- Record values across ranges. +- Expect: Bucket counts match boundaries. + +Example: +```erlang +ViewCriteria = #{instrument_name => my_histogram}, +ViewConfig = #{aggregation_module => otel_aggregation_histogram_explicit, + aggregation_options => #{explicit_bucket_boundaries => [0, 10, 50, 100]}}, +ok = otel_meter_server:add_view(ViewCriteria, ViewConfig). +``` + +4) Attribute filtering via Views +-------------------------------- +Goal: Only allowed attributes remain. +- Add View `attribute_keys => [host]` for an instrument. +- Record with attributes `host`, `user`. +- Expect: Exported point contains only `host`. + +5) Observable instruments and callbacks +--------------------------------------- +Goal: Callbacks run on collection and can serve multiple instruments. +- Create two observable gauges; register a single callback returning both values. +- Expect: Values appear each collect tick; nothing emitted between collects. + +Callback execution anchor: +```249:256:apps/opentelemetry_experimental/src/otel_metric_reader.erl +run_callbacks(ReaderId, CallbacksTab, StreamsTab, MetricsTab, ExemplarsTab) -> + try ets:lookup_element(CallbacksTab, ReaderId, 2) of + Callbacks -> otel_observables:run_callbacks(Callbacks, ReaderId, StreamsTab, MetricsTab, ExemplarsTab) + catch error:badarg -> [] end. +``` + +6) Temporality per Reader (delta vs cumulative) +----------------------------------------------- +Goal: Different readers expose different temporalities. +- Configure two readers: one delta, one cumulative. +- Record successive Counter adds. +- Expect: Delta reader reports per-interval increments; cumulative reports running total. + +Reader collection/export anchor: +```172:207:apps/opentelemetry_experimental/src/otel_metric_reader.erl +collect_(State=...) -> + Metrics = run_collection(...), + otel_exporter_metrics:export(Exporter, Metrics, Resource), + ... +``` + +7) Drop via View +---------------- +Goal: Measurements can be dropped. +- Add View with `aggregation_module=otel_aggregation_drop` selecting an instrument. +- Record values. +- Expect: No points exported for that instrument. + +8) Resource and scope propagation +--------------------------------- +Goal: Exported metrics include resource and scope. +- Expect: Console output shows scope (instrumentation scope) and metrics under configured resource. + +If something fails +------------------ +- Ensure the SDK app is started and the reader is configured. +- Force a one-time collection: call the reader’s `call_collect/1` if using pull mode. + +Pointers +-------- +- Architecture: `ARCHITECTURE.md` +- Spec crosswalk: `SPEC_COMPLIANCE.md` + + diff --git a/apps/opentelemetry_experimental/SPEC_COMPLIANCE.md b/apps/opentelemetry_experimental/SPEC_COMPLIANCE.md new file mode 100644 index 00000000..dd5b5a54 --- /dev/null +++ b/apps/opentelemetry_experimental/SPEC_COMPLIANCE.md @@ -0,0 +1,152 @@ +opentelemetry_experimental – Spec Compliance Crosswalk (Metrics SDK) +=================================================================== + +Audience: Reviewers validating OpenTelemetry Metrics SDK behavior without Erlang knowledge. + +Scope: MeterProvider, Views, Streams, Aggregations, Temporality, Readers, Observables, Exporters, Exemplars, Resource/Scope. + +How to read code anchors: Blocks below cite file, start:end lines. They are illustrative; exact line numbers may drift slightly across commits. + +1) MeterProvider responsibilities +--------------------------------- +Creates meters, manages instruments, registers views, and wires readers. Matching instruments to views produces per-reader streams. + +```33:52:apps/opentelemetry_experimental/src/otel_meter_server.erl +%% MeterProvider responsibilities and API surface +``` + +Streams creation on instrument add and when views/readers change: + +```333:359:apps/opentelemetry_experimental/src/otel_meter_server.erl +update_streams_(Instrument, CallbacksTab, StreamsTab, Views, Readers, ExemplarsEnabled, ExemplarFilter) -> + ViewMatches = otel_view:match_instrument_to_views(Instrument, Views, ExemplarsEnabled, ExemplarFilter), + lists:foreach(fun(Reader=#reader{id=ReaderId}) -> + Matches = per_reader_aggregations(Reader, Instrument, ViewMatches), + [true = otel_metrics_tables:insert_stream(StreamsTab, Meter, Name, M) || M <- Matches], + case {Instrument#instrument.callback, Instrument#instrument.callback_args} of + {undefined, _} -> ok; + {Callback, CallbackArgs} -> + otel_metrics_tables:insert_callback(CallbacksTab, ReaderId, Callback, CallbackArgs, Instrument) + end + end, Readers). +``` + +2) View selection and defaults +------------------------------ +Views select instruments by name/kind/unit and meter scope; can filter attributes and override aggregation. + +```64:91:apps/opentelemetry_experimental/src/otel_view.erl +new(Criteria, Config) -> ... %% builds view with matchspec and config +``` + +Wildcard rule (named wildcard forbidden): + +```69:76:apps/opentelemetry_experimental/src/otel_view.erl +new(Name, #{instrument_name := '*'}, _Config) -> + ?LOG_INFO("Wildacrd Views can not have a name, discarding view ~s", [Name]), + {error, named_wildcard_view}; +``` + +Matching; default when no view matched yields a stream with instrument defaults: + +```92:139:apps/opentelemetry_experimental/src/otel_view.erl +match_instrument_to_views(Instrument, Views, ExemplarsEnabled, ExemplarFilter) -> + case lists:filtermap(...) of + [] -> [{undefined, #stream{name=InstrumentName, ...}}]; + Aggs -> Aggs + end. +``` + +3) Aggregations and temporality +------------------------------- +Default aggregation mapping per instrument kind; per-reader temporality mapping. + +```366:379:apps/opentelemetry_experimental/src/otel_meter_server.erl +aggregation_module(Instrument, View, Reader) -> ... %% view override or reader default +``` + +Reader collects with set temporality and calls exporter: + +```172:207:apps/opentelemetry_experimental/src/otel_metric_reader.erl +collect_(State=...) -> + Metrics = run_collection(...), + otel_exporter_metrics:export(Exporter, Metrics, Resource), + ... +``` + +4) Recording path and validation +-------------------------------- +Measurement handling updates per-stream aggregations. Negative values are rejected for monotonic instruments per spec. + +```386:405:apps/opentelemetry_experimental/src/otel_meter_server.erl +maybe_init_aggregate(_, _Meter, Value, #instrument{kind=Kind} = Instrument, _Stream, _Attributes) + when Value < 0, Kind == ?KIND_COUNTER orelse Kind == ?KIND_HISTOGRAM -> + ?LOG_INFO("Discarding negative value for instrument ~s of type ~s", [Instrument#instrument.name, Kind]), + ok; +``` + +5) Observables +-------------- +Callbacks registered per reader; executed during collection; supports multi-instrument callbacks. + +```249:256:apps/opentelemetry_experimental/src/otel_metric_reader.erl +run_callbacks(ReaderId, CallbacksTab, StreamsTab, MetricsTab, ExemplarsTab) -> + try ets:lookup_element(CallbacksTab, ReaderId, 2) of + Callbacks -> otel_observables:run_callbacks(Callbacks, ReaderId, StreamsTab, MetricsTab, ExemplarsTab) + catch error:badarg -> [] end. +``` + +6) Readers (periodic vs pull) +----------------------------- +- `export_interval_ms` undefined → pull-based; `collect/1` or `call_collect/1` triggers. +- Set interval → periodic collection with rescheduled timer. + +```88:107:apps/opentelemetry_experimental/src/otel_metric_reader.erl +init([ReaderId, ProviderSup, Config]) -> + Exporter = otel_exporter_metrics:init(...), + ExporterIntervalMs = maps:get(export_interval_ms, Config, undefined), + TRef = case ExporterIntervalMs of undefined -> undefined; _ -> erlang:send_after(ExporterIntervalMs, self(), collect) end, +``` + +7) Exporters +------------ +Console exporter and OTLP exporter supported for metrics; logs exporters present but out of scope for metrics review. + +Files: `otel_metric_exporter_console.erl`, `otel_exporter_metrics_otlp.erl`. + +8) Exemplars +------------ +Reservoirs and filters implemented; per-stream exemplar reservoir chosen based on instrument kind and filter. + +```98:107:apps/opentelemetry_experimental/src/otel_view.erl +ExemplarReservoir = otel_metric_exemplar:reservoir(Kind, ExemplarsEnabled, ExemplarFilter), +``` + +9) Resource and scope propagation +--------------------------------- +Reader exports include resource; metric scope set from meter/instrumentation scope. + +```288:294:apps/opentelemetry_experimental/src/otel_metric_reader.erl +metric(#instrument{meter={_, Meter=#meter{}}}, Name, Description, Unit, Data) -> + #metric{scope=otel_meter_default:scope(Meter), name=Name, description=Description, unit=Unit, data=Data}. +``` + +10) Verification steps (manual) +------------------------------- +1. Configure a periodic reader with console exporter (1s interval). +2. Create instruments; record values (API macros). Expect default aggregations. +3. Add a view filtering to attribute allow-list; confirm attributes filtered. +4. Override histogram bucket boundaries; confirm bucket counts change. +5. Configure two readers with different temporalities; confirm delta vs cumulative behavior. +6. Register observable callback for multiple instruments; values exported on each collect. + +Related code/tests +------------------ +- MeterProvider: `src/otel_meter_server.erl` +- Views: `src/otel_view.erl` +- Reader: `src/otel_metric_reader.erl` +- Aggregations: `src/otel_aggregation_*.erl` +- Exporters: `src/otel_metric_exporter_console.erl`, `src/otel_exporter_metrics_otlp.erl` +- Observables: `src/otel_observables.erl` + + diff --git a/apps/opentelemetry_experimental/TEST_COVERAGE.md b/apps/opentelemetry_experimental/TEST_COVERAGE.md new file mode 100644 index 00000000..90d75ec6 --- /dev/null +++ b/apps/opentelemetry_experimental/TEST_COVERAGE.md @@ -0,0 +1,28 @@ +Test Coverage (SDK) +=================== + +Files +----- +- `test/otel_metrics_SUITE.erl` – Core metrics SDK behaviors (views, aggregations, temporality). +- `test/otel_exporter_metrics_otlp_SUITE.erl` – OTLP exporter integration. +- `test/simple_metric_producer.erl` – Example producer integration. + +Spec areas exercised +-------------------- +- View selection and default stream creation when no view matches. +- Aggregations: sum, last value, explicit histogram; drop behavior. +- Temporality: delta vs cumulative behavior per reader. +- Observable callbacks executed at collection time. +- Export pipeline to OTLP and console (serialization and send). + +How to run +---------- +- Using rebar3 (from umbrella root): + - `rebar3 as test eunit apps=opentelemetry_experimental` + +Gaps to consider +---------------- +- End-to-end exemplar sampling scenarios may need expanded coverage. +- Multi-reader configurations with divergent temporality and aggregation mappings can be extended. + +