Skip to content

feat(sdk): implement OTel SDK self-observability metrics#1987

Merged
bobstrecansky merged 14 commits into
open-telemetry:mainfrom
intuibase:feat/sdk-self-observability-metrics
Jun 23, 2026
Merged

feat(sdk): implement OTel SDK self-observability metrics#1987
bobstrecansky merged 14 commits into
open-telemetry:mainfrom
intuibase:feat/sdk-self-observability-metrics

Conversation

@intuibase

Copy link
Copy Markdown
Contributor

This PR implements OTel SDK self-observability metrics as defined in the OTel specification, replacing the old non-standard internal metrics with semconv-compliant instrument names.

Signals covered

Component Metrics added
Traces otel.sdk.span.started, otel.sdk.span.live
Trace BatchSpanProcessor otel.sdk.processor.span.queue.capacity, otel.sdk.processor.span.queue.size, otel.sdk.processor.span.processed, otel.sdk.exporter.span.inflight, otel.sdk.exporter.span.exported
Logs otel.sdk.log.created
Log BatchLogRecordProcessor otel.sdk.processor.log.queue.capacity, otel.sdk.processor.log.queue.size, otel.sdk.processor.log.processed, otel.sdk.exporter.log.inflight, otel.sdk.exporter.log.exported
Metric ExportingReader otel.sdk.metric_reader.collection.duration, otel.sdk.exporter.metric.data_point.inflight, otel.sdk.exporter.metric.data_point.exported

Old non-standard metrics (otel.trace.span_processor.*, otel.logs.log_processor.*) are removed.

Wiring

A MeterProvider can be injected into:

  • TracerProvider / TracerProviderBuilder::setMeterProvider()
  • LoggerProvider / LoggerProviderBuilder::setMeterProvider()
  • BatchSpanProcessor (constructor, 8th param)
  • BatchLogRecordProcessor (constructor, 8th param)
  • ExportingReader (constructor, 2nd param)

The YAML/file-config path (OpenTelemetrySdk component provider) wires the SDK-internal MeterProvider automatically into all processors.

Notable implementation details

  • Span live counter (otel.sdk.span.live) tracks recording spans between startSpan() and end(), attributed by otel.span.sampling_result.
  • Processor dropped spans/logs are reported via the processed counter with error.type=_OTHER (semconv catch-all).
  • Export errors (exception thrown) are handled via try/finally in ExportingReader to guarantee the inflight counter is always decremented.
  • Instance naming uses spl_object_id() for unique, stable otel.component.name values without global static state.
  • Context::withMeterProvider() added to allow updating the config context after MeterProvider is constructed, enabling proper wiring into batch processors in the file-config path.
  • Logger::isEnabled() now consistently returns false after shutdown, matching the behaviour of emit() and logRecordBuilder().

Test plan

  • BatchSpanProcessor — metrics names, processed/exported counter values, dropped span increments error.type=_OTHER
  • BatchLogRecordProcessor — same as above for logs
  • ExportingReaderdata_point.exported success and _OTHER error paths
  • All existing tests pass (make test-unit)

Replace legacy otel.trace.span_processor.* and otel.logs.log_processor.*
metrics with the standardized otel.sdk.* names from the semconv spec.
Add new metrics: otel.sdk.span.started, otel.sdk.span.live,
otel.sdk.processor.span.{queue.capacity,queue.size,processed},
otel.sdk.exporter.span.{inflight,exported}, otel.sdk.log.created,
otel.sdk.processor.log.{queue.capacity,queue.size,processed},
otel.sdk.exporter.log.{inflight,exported}.

Wire MeterProvider into TracerProvider, TracerProviderBuilder,
LoggerProvider, LoggerProviderBuilder and the config-based
OpenTelemetrySdk so all signals are instrumented automatically.
Add otel.sdk.metric_reader.collection.duration (Histogram),
otel.sdk.exporter.metric_data_point.inflight (UpDownCounter) and
otel.sdk.exporter.metric_data_point.exported (Counter) to ExportingReader.

ExportingReader accepts an optional MeterProvider to avoid a circular
dependency — pass a separate provider instance for self-observability.
- Add hasShutdown() guard in Logger::emit() before incrementing log counter
- Fix ExportingReader::countDataPoints() to use count() for arrays instead
  of iterator_count() which would exhaust generators
- Fix ExportingReader::doCollect() to record collection duration before
  export, not after, matching the spec intent
- Replace all literal attribute/metric name strings with OtelIncubatingAttributes
  and OtelIncubatingMetrics constants across TracerSharedState, LoggerSharedState,
  SpanBuilder, Span, BatchSpanProcessor, BatchLogRecordProcessor, ExportingReader
Replace iterator_count() fallback in ExportingReader::countDataPoints()
with 0 to avoid exhausting generators before export. Use '_OTHER' (semconv
catch-all) instead of 'export_failure' for metric export failures where no
exception class is available. Track dropped spans/logs in the processed counter
with error.type='_OTHER' to restore observability of queue-full drops. Add
tests verifying counter values after successful export and after queue-full
drops for BatchSpanProcessor, BatchLogRecordProcessor and ExportingReader.
- ExportingReader: wrap export() in try/finally so dataPointInflightCounter
  is always decremented even if the exporter throws
- ExportingReader: fix countDataPoints() to use is_countable() instead of
  is_array() (dataPoints is typed iterable, not array) and remove dead isset()
- ExportingReader/BatchSpanProcessor/BatchLogRecordProcessor: move
  ReflectionClass call inside the meterProvider non-null branch to avoid
  unnecessary reflection when self-observability is disabled
- ExportingReader/BatchSpanProcessor/BatchLogRecordProcessor: replace
  static \$instanceCount with spl_object_id(\$this) for stable, test-safe
  instance naming that does not accumulate global state across test runs
- BatchSpanProcessor/BatchLogRecordProcessor: remove dead \$dropped and
  \$processed fields; drops and processed counts are now fully captured
  by the self-observability counters
- Logger: add hasShutdown() check to isEnabled() to match emit() behaviour;
  previously isEnabled() returned true for post-shutdown loggers while
  emit() silently dropped records
- LoggerProviderFactory: forward \$meterProvider to LoggerProvider so that
  factory-created providers emit self-observability metrics
- Context: add withMeterProvider() to allow updating the config context
  after MeterProvider is constructed
- OpenTelemetrySdk: update \$context with the real MeterProvider before
  building span/log processors so that BatchSpanProcessor and
  BatchLogRecordProcessor receive it via \$context->meterProvider
@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.33977% with 82 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.08%. Comparing base (2f1c57f) to head (97b0606).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
.../Config/SDK/ComponentProvider/OpenTelemetrySdk.php 0.00% 43 Missing ⚠️
src/SDK/Trace/TracerSharedState.php 18.75% 13 Missing ⚠️
src/SDK/Logs/LoggerSharedState.php 20.00% 8 Missing ⚠️
src/API/Configuration/Context.php 0.00% 4 Missing ⚠️
src/SDK/Trace/TracerProviderBuilder.php 0.00% 4 Missing ⚠️
src/SDK/Logs/LoggerProviderBuilder.php 25.00% 3 Missing ⚠️
src/SDK/Metrics/Data/Gauge.php 0.00% 2 Missing ⚠️
src/SDK/Metrics/Data/Histogram.php 0.00% 2 Missing ⚠️
src/SDK/Metrics/Data/Sum.php 0.00% 2 Missing ⚠️
src/SDK/Trace/SpanBuilder.php 93.33% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main    #1987      +/-   ##
============================================
+ Coverage     67.87%   68.08%   +0.20%     
- Complexity     3046     3075      +29     
============================================
  Files           459      459              
  Lines          8891     9017     +126     
============================================
+ Hits           6035     6139     +104     
- Misses         2856     2878      +22     
Flag Coverage Δ
8.1 67.77% <68.33%> (+0.20%) ⬆️
8.2 67.93% <68.33%> (+0.12%) ⬆️
8.3 67.97% <68.33%> (+0.16%) ⬆️
8.4 67.90% <68.33%> (+0.13%) ⬆️
8.5 67.89% <68.33%> (+0.15%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/SDK/Logs/Logger.php 90.90% <100.00%> (+0.43%) ⬆️
src/SDK/Logs/LoggerProvider.php 100.00% <100.00%> (ø)
src/SDK/Logs/LoggerProviderFactory.php 85.71% <100.00%> (ø)
src/SDK/Logs/Processor/BatchLogRecordProcessor.php 100.00% <100.00%> (ø)
src/SDK/Metrics/MetricReader/ExportingReader.php 99.04% <100.00%> (+25.61%) ⬆️
src/SDK/Trace/Span.php 93.71% <100.00%> (+0.24%) ⬆️
src/SDK/Trace/SpanProcessor/BatchSpanProcessor.php 100.00% <100.00%> (ø)
src/SDK/Trace/TracerProvider.php 100.00% <100.00%> (ø)
src/SDK/Trace/SpanBuilder.php 91.00% <93.33%> (+0.41%) ⬆️
src/SDK/Metrics/Data/Gauge.php 0.00% <0.00%> (ø)
... and 8 more

... and 5 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2f1c57f...97b0606. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Replace NoopCounter/NoopUpDownCounter/NoopHistogram defaults with
nullable properties and null-safe ?-> operator, eliminating all
deptrac violations where SDK depended on API\Metrics\Noop\*.
Also add Sum instanceof assertions in tests to satisfy PHPStan.
Replace early-return pattern with if/else so Phan correctly sees
each readonly metric property assigned exactly once per branch.
Also replace is_countable/count with is_array/count for dataPoints
to satisfy Phan's type narrowing, and remove unused Metric import.
- Change attribute array PHPDoc types from array<string, string> to
  array<non-empty-string, string> to satisfy psalm metric instrument
  iterable key constraints
- Add @psalm-suppress RedundantCondition for isset($metric->data) which
  guards against uninitialized typed property at runtime
- Fix test code: replace spread operator on iterable with is_array()
  assertion; replace reset()->value chain with explicit variable and
  assert($dp !== false) narrowing
@intuibase
intuibase marked this pull request as ready for review June 19, 2026 14:20
@intuibase
intuibase requested a review from a team as a code owner June 19, 2026 14:20

@bobstrecansky bobstrecansky left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A couple small suggestions but love the direction.

Comment thread src/SDK/Metrics/MetricReader/ExportingReader.php Outdated
Comment thread tests/Unit/SDK/Logs/Processor/BatchLogRecordProcessorTest.php Outdated
Comment thread src/SDK/Logs/Logger.php
Add dataPointCount() to DataInterface so ExponentialHistogram and any future
data type is forced to implement counting instead of silently returning 0.
Extract repeated MeterProvider test setup into MeterProviderFactory helper.
@intuibase
intuibase requested a review from bobstrecansky June 22, 2026 11:53
@intuibase intuibase linked an issue Jun 23, 2026 that may be closed by this pull request
6 tasks
@bobstrecansky
bobstrecansky merged commit 6cb4156 into open-telemetry:main Jun 23, 2026
12 checks passed
@intuibase
intuibase deleted the feat/sdk-self-observability-metrics branch June 23, 2026 14:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement SDK health metrics in OpenTelemetry PHP SDK

2 participants