feat(sdk): implement OTel SDK self-observability metrics#1987
Merged
bobstrecansky merged 14 commits intoJun 23, 2026
Merged
Conversation
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 Report❌ Patch coverage is Additional details and impacted files@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 5 files with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
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
marked this pull request as ready for review
June 19, 2026 14:20
bobstrecansky
left a comment
Contributor
There was a problem hiding this comment.
A couple small suggestions but love the direction.
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.
6 tasks
bobstrecansky
approved these changes
Jun 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
otel.sdk.span.started,otel.sdk.span.liveotel.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.exportedotel.sdk.log.createdotel.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.exportedotel.sdk.metric_reader.collection.duration,otel.sdk.exporter.metric.data_point.inflight,otel.sdk.exporter.metric.data_point.exportedOld non-standard metrics (
otel.trace.span_processor.*,otel.logs.log_processor.*) are removed.Wiring
A
MeterProvidercan 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 (
OpenTelemetrySdkcomponent provider) wires the SDK-internalMeterProviderautomatically into all processors.Notable implementation details
otel.sdk.span.live) tracks recording spans betweenstartSpan()andend(), attributed byotel.span.sampling_result.processedcounter witherror.type=_OTHER(semconv catch-all).try/finallyinExportingReaderto guarantee the inflight counter is always decremented.spl_object_id()for unique, stableotel.component.namevalues without global static state.Context::withMeterProvider()added to allow updating the config context afterMeterProvideris constructed, enabling proper wiring into batch processors in the file-config path.Logger::isEnabled()now consistently returnsfalseafter shutdown, matching the behaviour ofemit()andlogRecordBuilder().Test plan
BatchSpanProcessor— metrics names, processed/exported counter values, dropped span incrementserror.type=_OTHERBatchLogRecordProcessor— same as above for logsExportingReader—data_point.exportedsuccess and_OTHERerror pathsmake test-unit)