Skip to content

Commit 3ac64f6

Browse files
authored
Add metrics implementation (#750)
* Add metrics implementation * Apply cs-fixer * Downgrade to php 7.4 * [TODO] Suppress phan for now * Add basic example * [TODO] Remove outdated prometheus example for now * Add otlp metric converter * Add metric stream tests * Downgrade to php 7.4 - fix asynchronous counter instrument type * Add missing psalm-suppress annotations * Add `ext-gmp` to composer suggest * Fix `Sdk` -> `SDK` * Remove `_bridge.php` * Add array typehints * Add `Interface` suffix to interfaces * Add aggregation / attribute processor / staleness handler tests * Apply rector * Simplify filtered attribute processor * Move instrument deduplication to meter Allows removing view registry dependency from `MetricFactory`. Should ideally turn of staleness handling for asynchronous instruments with permanently registered callbacks (drop all `::onStale()` callbacks and prevent addition of new callbacks). * Allow injecting metric factory * Release `::onStale()` callbacks if permanent observer callback registered * Add `MultiObserver` tests * Add php-doc for exporter temporality * Resolve phan issues * Add note about forward compatibility * Add exemplar tests * Remove special handling for callbacks being registered multiple times Was mainly a side-effect of using `spl_object_id()`; lead to inconsistent behavior between providing same and identical callbacks; reverting back to incrementing index, keyspace is large enough. * Add basic `Meter` / `MeterProvider` tests * Add view `SelectionCriteria` tests * Allow `MetricReader`s to configure default aggregation - move default aggregation handling to metric reader / metric exporter - move view registration to meter provider constructor - move exemplar reservoir creation to aggregation to support user implementations - remove `AttributeProcessor` from view as baggage access was dropped from spec - deduplicate metric streams * Add support for weakening `$this` reference of asynchronous callbacks * Minor improvements - add missing `Interface` suffix - move callback destructors to metric observer to not detach if meter and meter provider are out of scope - simplify `::viewRegistrationRequests()` by readding fallback view * Add OTLP metric exporter * Log export failure * Minor improvements - rename `MetricObserver::weakMap()` to `::destructors()` to better reflect usage` - move `ReferenceCounter::acquire()` call to corresponding `MetricObserver::observe()` call for consistency - cache instrumentation scope id in `Meter` to avoid repeated calls to `serialize()` - remove obsolete instrument type check from `ViewProjection`, leftover from supporting aggregation per type * Suppress `PhanAccessMethodInternal` due to being too strict for our usecase * Add workaround for observer segfault if destruct is initiated by `WeakMap` key going out of scope * Mark internal classes as internal * Add in-memory and stream exporter * Add metric converter test * Add metric observer tests * Add view registry tests * Add metric reader tests * Improve stream test coverage * Improve meter test coverage * Apply rector * Add delayed staleness handler
1 parent d0a600f commit 3ac64f6

File tree

186 files changed

+9105
-1596
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

186 files changed

+9105
-1596
lines changed

.phan/config.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,11 @@
280280

281281
// Add any issue types (such as `'PhanUndeclaredMethod'`)
282282
// to this deny-list to inhibit them from being reported.
283-
'suppress_issue_types' => [],
283+
'suppress_issue_types' => [
284+
'PhanAccessClassInternal',
285+
'PhanAccessMethodInternal',
286+
'PhanAccessPropertyInternal',
287+
],
284288

285289
// A regular expression to match files to be excluded
286290
// from parsing and analysis and will not be read at all.

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,14 @@ smoke-test-exporter-examples: FORCE ## Run (some) exporter smoke test examples
6565
smoke-test-collector-integration: ## Run smoke test collector integration
6666
docker-compose -f docker-compose.collector.yaml up -d --remove-orphans
6767
# This is slow because it's building the image from scratch (and parts of that, like installing the gRPC extension, are slow)
68-
# This can be sped up by switching to the pre-built images hosted on ghcr.io (and referenced in other docker-compose**.yaml files)
68+
# This can be sped up by switching to the pre-built images hosted on ghcr.io (and referenced in other docker-compose**.yaml files)
6969
docker-compose -f docker-compose.collector.yaml run -e OTEL_EXPORTER_OTLP_ENDPOINT=collector:4317 --rm php php ./examples/traces/features/exporters/otlp_grpc.php
7070
docker-compose -f docker-compose.collector.yaml stop
71+
smoke-test-collector-metrics-integration:
72+
docker-compose -f docker-compose.collector.yaml up -d --force-recreate collector
73+
COMPOSE_IGNORE_ORPHANS=TRUE docker-compose -f docker-compose.yaml run --rm php php ./examples/metrics/features/exporters/otlp.php
74+
docker-compose -f docker-compose.collector.yaml logs collector
75+
docker-compose -f docker-compose.collector.yaml stop collector
7176
smoke-test-prometheus-example: metrics-prometheus-example stop-prometheus
7277
metrics-prometheus-example:
7378
@docker-compose -f docker-compose.prometheus.yaml -p opentelemetry-php_metrics-prometheus-example up -d web

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"php-http/mock-client": "^1.5",
8989
"phpbench/phpbench": "^1.2",
9090
"phpmetrics/phpmetrics": "^2.7",
91+
"phpspec/prophecy-phpunit": "^2.0",
9192
"phpstan/phpstan": "^1.1",
9293
"phpstan/phpstan-mockery": "^1.0",
9394
"phpstan/phpstan-phpunit": "^1.0",
@@ -100,6 +101,7 @@
100101
"symfony/http-client": "^5.2"
101102
},
102103
"suggest": {
104+
"ext-gmp": "To support unlimited number of synchronous metric readers",
103105
"ext-grpc": "To use the OTLP GRPC Exporter",
104106
"ext-protobuf": "For more performant protobuf/grpc exporting",
105107
"ext-sockets": "To use the Thrift UDP Exporter for the Jaeger Agent"

examples/metrics/basic.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
// Example based on https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/supplementary-guidelines.md#synchronous-example
4+
declare(strict_types=1);
5+
6+
require_once __DIR__ . '/../../vendor/autoload.php';
7+
8+
use OpenTelemetry\Contrib\Otlp\StreamMetricExporter;
9+
use OpenTelemetry\SDK\Common\Attribute\Attributes;
10+
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeFactory;
11+
use OpenTelemetry\SDK\Common\Time\ClockFactory;
12+
use OpenTelemetry\SDK\Metrics\Aggregation\ExplicitBucketHistogramAggregation;
13+
use OpenTelemetry\SDK\Metrics\Exemplar\ExemplarFilter\WithSampledTraceExemplarFilter;
14+
use OpenTelemetry\SDK\Metrics\MeterProvider;
15+
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
16+
use OpenTelemetry\SDK\Metrics\StalenessHandler\ImmediateStalenessHandlerFactory;
17+
use OpenTelemetry\SDK\Metrics\View\CriteriaViewRegistry;
18+
use OpenTelemetry\SDK\Metrics\View\SelectionCriteria\InstrumentNameCriteria;
19+
use OpenTelemetry\SDK\Metrics\View\ViewTemplate;
20+
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
21+
22+
$clock = ClockFactory::getDefault();
23+
$reader = new ExportingReader(new StreamMetricExporter(STDOUT, /*Temporality::CUMULATIVE*/), $clock);
24+
25+
// Let's imagine we export the metrics as Histogram, and to simplify the story we will only have one histogram bucket (-Inf, +Inf):
26+
$views = new CriteriaViewRegistry();
27+
$views->register(
28+
new InstrumentNameCriteria('http.server.duration'),
29+
ViewTemplate::create()
30+
->withAttributeKeys(['http.method', 'http.status_code'])
31+
->withAggregation(new ExplicitBucketHistogramAggregation([])),
32+
);
33+
34+
$meterProvider = new MeterProvider(
35+
null,
36+
ResourceInfoFactory::emptyResource(),
37+
$clock,
38+
Attributes::factory(),
39+
new InstrumentationScopeFactory(Attributes::factory()),
40+
[$reader],
41+
$views,
42+
new WithSampledTraceExemplarFilter(),
43+
new ImmediateStalenessHandlerFactory(),
44+
);
45+
46+
$serverDuration = $meterProvider->getMeter('io.opentelemetry.contrib.php')->createHistogram(
47+
'http.server.duration',
48+
'ms',
49+
'measures the duration inbound HTTP requests',
50+
);
51+
52+
// During the time range (T0, T1]:
53+
$serverDuration->record(50, ['http.method' => 'GET', 'http.status_code' => 200]);
54+
$serverDuration->record(100, ['http.method' => 'GET', 'http.status_code' => 200]);
55+
$serverDuration->record(1, ['http.method' => 'GET', 'http.status_code' => 500]);
56+
$reader->collect();
57+
58+
// During the time range (T1, T2]:
59+
$reader->collect();
60+
61+
// During the time range (T2, T3]:
62+
$serverDuration->record(5, ['http.method' => 'GET', 'http.status_code' => 500]);
63+
$serverDuration->record(2, ['http.method' => 'GET', 'http.status_code' => 500]);
64+
$reader->collect();
65+
66+
// During the time range (T3, T4]:
67+
$serverDuration->record(100, ['http.method' => 'GET', 'http.status_code' => 200]);
68+
$reader->collect();
69+
70+
// During the time range (T4, T5]:
71+
$serverDuration->record(100, ['http.method' => 'GET', 'http.status_code' => 200]);
72+
$serverDuration->record(30, ['http.method' => 'GET', 'http.status_code' => 200]);
73+
$serverDuration->record(50, ['http.method' => 'GET', 'http.status_code' => 200]);
74+
$reader->collect();
75+
76+
$meterProvider->shutdown();
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
require __DIR__ . '/../../../../vendor/autoload.php';
6+
7+
use GuzzleHttp\Client;
8+
use GuzzleHttp\Psr7\HttpFactory;
9+
use OpenTelemetry\API\Metrics\ObserverInterface;
10+
use OpenTelemetry\Contrib\OtlpHttp\MetricExporter;
11+
use OpenTelemetry\SDK\Common\Attribute\Attributes;
12+
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeFactory;
13+
use OpenTelemetry\SDK\Common\Time\ClockFactory;
14+
use OpenTelemetry\SDK\Metrics\Exemplar\ExemplarFilter\WithSampledTraceExemplarFilter;
15+
use OpenTelemetry\SDK\Metrics\MeterProvider;
16+
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
17+
use OpenTelemetry\SDK\Metrics\StalenessHandler\ImmediateStalenessHandlerFactory;
18+
use OpenTelemetry\SDK\Metrics\View\CriteriaViewRegistry;
19+
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
20+
21+
$clock = ClockFactory::getDefault();
22+
$reader = new ExportingReader(MetricExporter::create(
23+
new Client(),
24+
new HttpFactory(),
25+
new HttpFactory(),
26+
'http://collector:4318/v1/metrics',
27+
), $clock);
28+
29+
$meterProvider = new MeterProvider(
30+
null,
31+
ResourceInfoFactory::defaultResource(),
32+
$clock,
33+
Attributes::factory(),
34+
new InstrumentationScopeFactory(Attributes::factory()),
35+
[$reader],
36+
new CriteriaViewRegistry(),
37+
new WithSampledTraceExemplarFilter(),
38+
new ImmediateStalenessHandlerFactory(),
39+
);
40+
41+
$meter = $meterProvider->getMeter('io.opentelemetry.contrib.php');
42+
$meter
43+
->createObservableUpDownCounter('process.memory.usage', 'By', 'The amount of physical memory in use.')
44+
->observe(static function (ObserverInterface $observer): void {
45+
$observer->observe(memory_get_usage(true));
46+
});
47+
48+
$serverDuration = $meter
49+
->createHistogram('http.server.duration', 'ms', 'measures the duration inbound HTTP requests');
50+
51+
// During the time range (T0, T1]:
52+
$serverDuration->record(50, ['http.method' => 'GET', 'http.status_code' => 200]);
53+
$serverDuration->record(100, ['http.method' => 'GET', 'http.status_code' => 200]);
54+
$serverDuration->record(1, ['http.method' => 'GET', 'http.status_code' => 500]);
55+
$reader->collect();
56+
57+
// During the time range (T1, T2]:
58+
$reader->collect();
59+
60+
// During the time range (T2, T3]:
61+
$serverDuration->record(5, ['http.method' => 'GET', 'http.status_code' => 500]);
62+
$serverDuration->record(2, ['http.method' => 'GET', 'http.status_code' => 500]);
63+
$reader->collect();
64+
65+
// During the time range (T3, T4]:
66+
$serverDuration->record(100, ['http.method' => 'GET', 'http.status_code' => 200]);
67+
$reader->collect();
68+
69+
// During the time range (T4, T5]:
70+
$serverDuration->record(100, ['http.method' => 'GET', 'http.status_code' => 200]);
71+
$serverDuration->record(30, ['http.method' => 'GET', 'http.status_code' => 200]);
72+
$serverDuration->record(50, ['http.method' => 'GET', 'http.status_code' => 200]);
73+
$reader->collect();
74+
75+
$meterProvider->shutdown();

examples/metrics/prometheus/prometheus_metrics_example.php

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44

55
require __DIR__ . '/../../../vendor/autoload.php';
66

7-
use OpenTelemetry\Contrib\Prometheus\PrometheusExporter;
8-
use OpenTelemetry\SDK\Metrics\Counter;
9-
use Prometheus\CollectorRegistry;
107
use Prometheus\Storage\Redis;
118

129
Redis::setDefaultOptions(
@@ -20,10 +17,4 @@
2017
]
2118
);
2219

23-
$counter = new Counter('opentelemetry_prometheus_counter', 'Just a quick measurement');
24-
25-
$counter->increment();
26-
27-
$exporter = new PrometheusExporter(CollectorRegistry::getDefault());
28-
29-
$exporter->export([$counter]);
20+
trigger_error('Prometheus exporter currently not supported', E_USER_WARNING);

files/collector/otel-collector-config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ service:
3030
receivers: [otlp, zipkin]
3131
exporters: [zipkin, logging, newrelic]
3232
processors: [batch]
33+
metrics:
34+
receivers: [otlp]
35+
exporters: [logging]

src/API/Common/Instrumentation/InstrumentationTrait.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
use OpenTelemetry\API\Metrics\MeterInterface;
88
use OpenTelemetry\API\Metrics\MeterProviderInterface;
9-
use OpenTelemetry\API\Metrics\NoopMeter;
9+
use OpenTelemetry\API\Metrics\Noop\NoopMeter;
1010
use OpenTelemetry\API\Trace\NoopTracer;
1111
use OpenTelemetry\API\Trace\TracerInterface;
1212
use OpenTelemetry\API\Trace\TracerProviderInterface;
@@ -177,6 +177,7 @@ private function initDefaults(): void
177177
{
178178
$this->propagator = new NullPropagator();
179179
$this->tracer = new NoopTracer();
180+
/** @phan-suppress-next-line PhanAccessMethodInternal */
180181
$this->meter = new NoopMeter();
181182
$this->logger = new NullLogger();
182183
}

src/API/Metrics/CounterInterface.php

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,18 @@
44

55
namespace OpenTelemetry\API\Metrics;
66

7-
interface CounterInterface extends MetricInterface
8-
{
9-
/**
10-
* Adds value to the counter
11-
*
12-
* @access public
13-
* @param int $value
14-
* @return self
15-
*/
16-
public function add(int $value): CounterInterface;
7+
use OpenTelemetry\Context\Context;
178

18-
/**
19-
* Increments value
20-
*
21-
* @access public
22-
* @return self
23-
*/
24-
public function increment(): CounterInterface;
9+
interface CounterInterface
10+
{
2511

2612
/**
27-
* Gets the value
13+
* @param float|int $amount non-negative amount to increment by
14+
* @param iterable<non-empty-string, string|bool|float|int|array|null> $attributes
15+
* attributes of the data point
16+
* @param Context|false|null $context execution context
2817
*
29-
* @access public
30-
* @return int
18+
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#add
3119
*/
32-
public function getValue(): int;
20+
public function add($amount, iterable $attributes = [], $context = null): void;
3321
}

src/API/Metrics/ExporterInterface.php

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)