adding logs signal (#934)

* initial work on logs signal

* supressing psalm error

* "fixing" 7.4 segfault
through trial and error, I found that it was consistently segfaulting in the sdk autoloader
tests, but by running them in a different order, the failures went away.

* adding logs tests

* rename example per feedback

* move log context injection into processors, per spec

* correctly set context on logs

* update logger name per feedback

* adding variables, map psr3 to otel severity, fix timestamp

* psr-3 loggers

* tests, psr3 v3 fix

* remove LogRecordDate and refactor SDK log record classes

* tests, tidy, improvements

* remove todos

* documentation

* check for valid span context

* removing psr-3 compatibility, per spec and feedback

* use InstrumentationScopeFactoryInterface

* apply attribute limits on readablelogrecord creation
per feedback, this should be more memory efficient for the processors

* group log record by resource/scope

* remove psr3 references from readme

* ignoring psalm error

* add trace context in Logger rather than processors
per feedback, and following the example in java and python SIGs, we
can add trace context to logs once, in the logger, rather than having
processors do it. The Context passed to processors is no longer used,
but retained for spec compliance.
This commit is contained in:
Brett McBride 2023-03-22 23:11:51 +11:00 committed by GitHub
parent c058a646f6
commit 615917f5d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 3570 additions and 27 deletions

View File

@ -81,7 +81,7 @@ Additional packages, demos and tools are hosted or distributed in the [OpenTelem
|---------|--------|---------|
| Traces | Beta | N/A |
| Metrics | Beta | N/A |
| Logs | N/A | N/A |
| Logs | Alpha | N/A |
## Specification conformance
We attempt to keep the [OpenTelemetry Specification Matrix](https://github.com/open-telemetry/opentelemetry-specification/blob/master/spec-compliance-matrix.md) up to date in order to show which features are available and which have not yet been implemented.
@ -357,7 +357,24 @@ Meters must be obtained from a `MeterProvider`
See [basic example](./examples/metrics/basic.php)
## Log signals
_frozen pending delivery of tracing and metrics_
Loggers must be obtained from a `LoggerProvider`.
As logging is a mature and well-established function, the
[OpenTelemetry approach](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/README.md#introduction)
is a little different for this signal than traces or metrics.
The OpenTelemetry logger is not designed to be used directly, but rather to be integrated into existing
logging libraries as a handler. In this way you can choose to have your application logs sent to an
opentelemetry collector and further distributed from there.
The [monolog-otel-integration example](./examples/logs/features/monolog-otel-integration.php) demonstrates
using the popular Monolog logger to send some logs to a stream (in their usual format), as well as sending
some logs to an OpenTelemetry collector.
### Logging examples
See [getting started example](./examples/logs/getting_started.php)
# Versioning

View File

@ -55,6 +55,7 @@
"src/Contrib/Newrelic/_register.php",
"src/Contrib/Zipkin/_register.php",
"src/Extension/Propagator/B3/_register.php",
"src/SDK/Logs/Exporter/_register.php",
"src/SDK/Metrics/MetricExporter/_register.php",
"src/SDK/Propagation/_register.php",
"src/SDK/Trace/SpanExporter/_register.php",

View File

@ -21,3 +21,8 @@ services:
ports:
- 9412:9412
- 16686:16686
collector:
image: otel/opentelemetry-collector-contrib
command: [ "--config=/etc/otel-collector-config.yml" ]
volumes:
- ./files/collector/otel-collector-config.yml:/etc/otel-collector-config.yml

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use OpenTelemetry\API\Common\Log\LoggerHolder;
use OpenTelemetry\API\Common\Signal\Signals;
use OpenTelemetry\API\Logs\EventLogger;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\Contrib\Grpc\GrpcTransportFactory;
use OpenTelemetry\Contrib\Otlp\LogsExporter;
use OpenTelemetry\Contrib\Otlp\OtlpUtil;
use Opentelemetry\Proto\Logs\V1\SeverityNumber;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeFactory;
use OpenTelemetry\SDK\Common\Time\ClockFactory;
use OpenTelemetry\SDK\Logs\LoggerProvider;
use OpenTelemetry\SDK\Logs\LogRecordLimitsBuilder;
use OpenTelemetry\SDK\Logs\Processor\BatchLogsProcessor;
use Psr\Log\LogLevel;
require __DIR__ . '/../../../vendor/autoload.php';
LoggerHolder::set(
new Logger('otel-php', [new StreamHandler(STDOUT, LogLevel::DEBUG)])
);
$transport = (new GrpcTransportFactory())->create('http://collector:4317' . OtlpUtil::method(Signals::LOGS));
$exporter = new LogsExporter($transport);
$loggerProvider = new LoggerProvider(
new BatchLogsProcessor(
$exporter,
ClockFactory::getDefault()
),
new InstrumentationScopeFactory(
(new LogRecordLimitsBuilder())->build()->getAttributeFactory()
)
);
$logger = $loggerProvider->getLogger('demo', '1.0', 'http://schema.url', true, ['foo' => 'bar']);
$eventLogger = new EventLogger($logger, 'my-domain');
$eventLogger->logEvent(
'foo',
(new LogRecord(['foo' => 'bar', 'baz' => 'bat', 'msg' => 'hello world']))
->setSeverityText('INFO')
->setSeverityNumber(SeverityNumber::SEVERITY_NUMBER_INFO)
);
$eventLogger->logEvent(
'foo',
new LogRecord('otel is great')
);
$loggerProvider->shutdown();

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use OpenTelemetry\API\Common\Log\LoggerHolder;
use OpenTelemetry\API\Logs\EventLogger;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\Contrib\Otlp\LogsExporter;
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
use Opentelemetry\Proto\Logs\V1\SeverityNumber;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeFactory;
use OpenTelemetry\SDK\Logs\LoggerProvider;
use OpenTelemetry\SDK\Logs\LogRecordLimitsBuilder;
use OpenTelemetry\SDK\Logs\Processor\SimpleLogsProcessor;
use Psr\Log\LogLevel;
require __DIR__ . '/../../../vendor/autoload.php';
LoggerHolder::set(
new Logger('otel-php', [new StreamHandler(STDOUT, LogLevel::DEBUG)])
);
$transport = (new OtlpHttpTransportFactory())->create('http://collector:4318/v1/logs', 'application/json');
$exporter = new LogsExporter($transport);
$loggerProvider = new LoggerProvider(
new SimpleLogsProcessor(
$exporter
),
new InstrumentationScopeFactory(
(new LogRecordLimitsBuilder())->build()->getAttributeFactory()
)
);
$logger = $loggerProvider->getLogger('demo', '1.0', 'http://schema.url', true, ['foo' => 'bar']);
$eventLogger = new EventLogger($logger, 'my-domain');
$record = (new LogRecord(['foo' => 'bar', 'baz' => 'bat', 'msg' => 'hello world']))
->setSeverityText('INFO')
->setSeverityNumber(SeverityNumber::SEVERITY_NUMBER_INFO);
$eventLogger->logEvent('foo', $record);

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
use OpenTelemetry\API\Logs\EventLogger;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\SDK\Common\Export\Stream\StreamTransportFactory;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeFactory;
use OpenTelemetry\SDK\Common\Time\ClockFactory;
use OpenTelemetry\SDK\Logs\Exporter\ConsoleExporter;
use OpenTelemetry\SDK\Logs\LoggerProvider;
use OpenTelemetry\SDK\Logs\LogRecordLimitsBuilder;
use OpenTelemetry\SDK\Logs\Processor\BatchLogsProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
require __DIR__ . '/../../../vendor/autoload.php';
$loggerProvider = new LoggerProvider(
new BatchLogsProcessor(
new ConsoleExporter(
(new StreamTransportFactory())->create(STDOUT, '')
),
ClockFactory::getDefault()
),
new InstrumentationScopeFactory(
(new LogRecordLimitsBuilder())->build()->getAttributeFactory()
)
);
$tracerProvider = new TracerProvider();
$tracer = $tracerProvider->getTracer('demo-tracer');
//start and activate a span
$span = $tracer->spanBuilder('root')->startSpan();
$scope = $span->activate();
echo 'Trace id: ' . $span->getContext()->getTraceId() . PHP_EOL;
echo 'Span id: ' . $span->getContext()->getSpanId() . PHP_EOL;
//get a logger, and emit a log record from an EventLogger. The active context (trace id + span id) will be
//attached to the log record
$logger = $loggerProvider->getLogger('demo', '1.0', 'http://schema.url', true, ['foo' => 'bar']);
$eventLogger = new EventLogger($logger, 'my-domain');
$record = (new LogRecord(['foo' => 'bar', 'baz' => 'bat', 'msg' => 'hello world']))
->setSeverityText('INFO')
->setSeverityNumber(9);
$eventLogger->logEvent('foo', $record);
$eventLogger->logEvent('bar', (new LogRecord('hello world')));
//end span
$span->end();
$scope->detach();
//shut down logger provider
$loggerProvider->shutdown();

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use OpenTelemetry\API\Common\Instrumentation\Globals;
use OpenTelemetry\API\Logs\EventLogger;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\API\Logs\Map\Psr3;
use Psr\Log\LogLevel;
/**
* This example creates a monolog handler which integrates with opentelemetry, as described in:
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/bridge-api.md#usage
*/
putenv('OTEL_PHP_AUTOLOAD_ENABLED=true');
putenv('OTEL_METRICS_EXPORTER=none');
putenv('OTEL_LOGS_EXPORTER=otlp');
putenv('OTEL_LOGS_PROCESSOR=batch');
putenv('OTEL_EXPORTER_OTLP_PROTOCOL=grpc');
putenv('OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317');
require __DIR__ . '/../../../vendor/autoload.php';
$streamHandler = new StreamHandler(STDOUT, LogLevel::DEBUG);
$tracer = Globals::tracerProvider()->getTracer('monolog-demo');
//otel handler for Monolog v2, which ignores logs < INFO
$otelHandler = new class('demo', 'demo-domain', LogLevel::INFO) extends AbstractProcessingHandler {
private EventLogger $eventLogger;
public function __construct(string $name, string $domain, string $level, bool $bubble = true, ?LoggerProviderInterface $loggerProvider = null)
{
parent::__construct($level, $bubble);
$loggerProvider ??= Globals::loggerProvider();
$this->eventLogger = new EventLogger($loggerProvider->getLogger($name), $domain);
}
protected function write(array $record): void
{
$this->eventLogger->logEvent('foo', $this->convert($record));
}
private function convert(array $record): LogRecord
{
return (new LogRecord($record['message']))
->setSeverityText($record['level_name'])
->setTimestamp((int) (microtime(true) * LogRecord::NANOS_PER_SECOND))
->setObservedTimestamp($record['datetime']->format('U') * LogRecord::NANOS_PER_SECOND)
->setSeverityNumber(Psr3::severityNumber($record['level_name']))
->setAttributes($record['context'] + $record['extra']);
}
};
//start a span so that logs contain span context
$span = $tracer->spanBuilder('foo')->startSpan();
$scope = $span->activate();
$monolog = new Logger('otel-php-monolog', [$otelHandler, $streamHandler]);
$monolog->debug('debug message');
$monolog->info('hello world', ['extra_one' => 'value_one']);
$monolog->alert('foo', ['extra_two' => 'value_two']);
$scope->detach();
$span->end();

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
use OpenTelemetry\API\Logs\EventLogger;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\SDK\Logs\Exporter\ConsoleExporter;
use OpenTelemetry\SDK\Logs\LoggerProvider;
use OpenTelemetry\SDK\Logs\Processor\SimpleLogsProcessor;
use OpenTelemetry\SDK\Trace\TracerProvider;
require __DIR__ . '/../../vendor/autoload.php';
$loggerProvider = new LoggerProvider(
new SimpleLogsProcessor(
new ConsoleExporter()
)
);
$tracerProvider = new TracerProvider();
$tracer = $tracerProvider->getTracer('demo-tracer');
//start and activate a span
$span = $tracer->spanBuilder('root')->startSpan();
$scope = $span->activate();
echo 'Trace id: ' . $span->getContext()->getTraceId() . PHP_EOL;
echo 'Span id: ' . $span->getContext()->getSpanId() . PHP_EOL;
//get a logger, and emit a log record from an EventLogger. The active context (trace id + span id) will be
//attached to the log record
$logger = $loggerProvider->getLogger('demo', '1.0', 'http://schema.url', true, ['foo' => 'bar']);
$eventLogger = new EventLogger($logger, 'my-domain');
$record = (new LogRecord(['foo' => 'bar', 'baz' => 'bat', 'msg' => 'hello world']))
->setSeverityText('INFO')
->setSeverityNumber(9);
$eventLogger->logEvent('foo', $record);
//end span
$span->end();
$scope->detach();

View File

@ -9,7 +9,7 @@ exporters:
zipkin:
endpoint: "http://zipkin:9411/api/v2/spans"
logging:
loglevel: debug
verbosity: detailed
processors:
batch:
@ -21,11 +21,18 @@ extensions:
service:
extensions: [pprof, zpages, health_check]
telemetry:
logs:
level: "debug"
pipelines:
traces:
receivers: [otlp, zipkin]
exporters: [zipkin, logging]
exporters: [logging]
processors: [batch]
metrics:
receivers: [otlp]
exporters: [logging]
logs:
receivers: [ otlp ]
processors: [ batch ]
exporters: [ logging ]

View File

@ -7,6 +7,8 @@ namespace OpenTelemetry\API\Common\Instrumentation;
use ArrayAccess;
use function assert;
use function class_exists;
use OpenTelemetry\API\Logs\LoggerInterface;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\API\Metrics\MeterInterface;
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\API\Trace\TracerInterface;
@ -31,6 +33,8 @@ final class CachedInstrumentation
private ?ArrayAccess $tracers;
/** @var ArrayAccess<MeterProviderInterface, MeterInterface>|null */
private ?ArrayAccess $meters;
/** @var ArrayAccess<LoggerProviderInterface, LoggerInterface>|null */
private ?ArrayAccess $loggers;
public function __construct(string $name, ?string $version = null, ?string $schemaUrl = null, iterable $attributes = [])
{
@ -40,6 +44,7 @@ final class CachedInstrumentation
$this->attributes = $attributes;
$this->tracers = self::createWeakMap();
$this->meters = self::createWeakMap();
$this->loggers = self::createWeakMap();
}
private static function createWeakMap(): ?ArrayAccess
@ -78,4 +83,15 @@ final class CachedInstrumentation
return $this->meters[$meterProvider] ??= $meterProvider->getMeter($this->name, $this->version, $this->schemaUrl, $this->attributes);
}
public function logger(): LoggerInterface
{
$loggerProvider = Globals::loggerProvider();
if ($this->loggers === null) {
//@todo configurable includeTraceContext?
return $loggerProvider->getLogger($this->name, $this->version, $this->schemaUrl, true, $this->attributes);
}
return $this->loggers[$loggerProvider] ??= $loggerProvider->getLogger($this->name, $this->version, $this->schemaUrl, true, $this->attributes);
}
}

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace OpenTelemetry\API\Common\Instrumentation;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\API\Logs\NoopLoggerProvider;
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\API\Metrics\Noop\NoopMeterProvider;
use OpenTelemetry\API\Trace\NoopTracerProvider;
@ -25,6 +27,7 @@ final class Configurator implements ImplicitContextKeyedInterface
private ?TracerProviderInterface $tracerProvider = null;
private ?MeterProviderInterface $meterProvider = null;
private ?TextMapPropagatorInterface $propagator = null;
private ?LoggerProviderInterface $loggerProvider = null;
private function __construct()
{
@ -47,6 +50,7 @@ final class Configurator implements ImplicitContextKeyedInterface
->withTracerProvider(new NoopTracerProvider())
->withMeterProvider(new NoopMeterProvider())
->withPropagator(new NoopTextMapPropagator())
->withLoggerProvider(new NoopLoggerProvider())
;
}
@ -68,6 +72,9 @@ final class Configurator implements ImplicitContextKeyedInterface
if ($this->propagator !== null) {
$context = $context->with(ContextKeys::propagator(), $this->propagator);
}
if ($this->loggerProvider !== null) {
$context = $context->with(ContextKeys::loggerProvider(), $this->loggerProvider);
}
return $context;
}
@ -93,6 +100,13 @@ final class Configurator implements ImplicitContextKeyedInterface
$self = clone $this;
$self->propagator = $propagator;
return $self;
}
public function withLoggerProvider(?LoggerProviderInterface $loggerProvider): Configurator
{
$self = clone $this;
$self->loggerProvider = $loggerProvider;
return $self;
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace OpenTelemetry\API\Common\Instrumentation;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\API\Trace\TracerProviderInterface;
use OpenTelemetry\Context\Context;
@ -44,4 +45,14 @@ final class ContextKeys
return $instance ??= Context::createKey(TextMapPropagatorInterface::class);
}
/**
* @return ContextKeyInterface<LoggerProviderInterface>
*/
public static function loggerProvider(): ContextKeyInterface
{
static $instance;
return $instance ??= Context::createKey(LoggerProviderInterface::class);
}
}

View File

@ -7,6 +7,7 @@ namespace OpenTelemetry\API\Common\Instrumentation;
use function assert;
use Closure;
use const E_USER_WARNING;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\API\Trace\TracerProviderInterface;
use OpenTelemetry\Context\Context;
@ -27,14 +28,17 @@ final class Globals
private TracerProviderInterface $tracerProvider;
private MeterProviderInterface $meterProvider;
private TextMapPropagatorInterface $propagator;
private LoggerProviderInterface $loggerProvider;
public function __construct(
TracerProviderInterface $tracerProvider,
MeterProviderInterface $meterProvider,
LoggerProviderInterface $loggerProvider,
TextMapPropagatorInterface $propagator
) {
$this->tracerProvider = $tracerProvider;
$this->meterProvider = $meterProvider;
$this->loggerProvider = $loggerProvider;
$this->propagator = $propagator;
}
@ -53,6 +57,11 @@ final class Globals
return Context::getCurrent()->get(ContextKeys::propagator()) ?? self::globals()->propagator;
}
public static function loggerProvider(): LoggerProviderInterface
{
return Context::getCurrent()->get(ContextKeys::loggerProvider()) ?? self::globals()->loggerProvider;
}
/**
* @param Closure(Configurator): Configurator $initializer
*
@ -92,10 +101,11 @@ final class Globals
$tracerProvider = $context->get(ContextKeys::tracerProvider());
$meterProvider = $context->get(ContextKeys::meterProvider());
$propagator = $context->get(ContextKeys::propagator());
$loggerProvider = $context->get(ContextKeys::loggerProvider());
assert(isset($tracerProvider, $meterProvider, $propagator));
assert(isset($tracerProvider, $meterProvider, $loggerProvider, $propagator));
return self::$globals = new self($tracerProvider, $meterProvider, $propagator);
return self::$globals = new self($tracerProvider, $meterProvider, $loggerProvider, $propagator);
}
/**
@ -104,5 +114,6 @@ final class Globals
public static function reset(): void
{
self::$globals = null;
self::$initializers = [];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Logs;
class EventLogger implements EventLoggerInterface
{
private LoggerInterface $logger;
private string $domain;
public function __construct(LoggerInterface $logger, string $domain)
{
$this->logger = $logger;
$this->domain = $domain;
}
public function logEvent(string $eventName, LogRecord $logRecord): void
{
$logRecord->setAttributes([
'event.name' => $eventName,
'event.domain' => $this->domain,
]);
$this->logger->logRecord($logRecord);
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Logs;
/**
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/event-api.md#events-api-interface
*/
interface EventLoggerInterface
{
public function logEvent(string $eventName, LogRecord $logRecord): void;
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Logs;
use OpenTelemetry\Context\ContextInterface;
class LogRecord
{
public const NANOS_PER_SECOND = 1_000_000_000;
protected ?int $timestamp = null;
protected ?int $observedTimestamp = null;
protected ?ContextInterface $context = null;
protected int $severityNumber = 0;
protected ?string $severityText = null;
protected $body = null;
protected array $attributes = [];
public function __construct($body = null)
{
$this->body = $body;
}
public function setTimestamp(int $timestamp): self
{
$this->timestamp = $timestamp;
return $this;
}
public function setContext(?ContextInterface $context = null): self
{
$this->context = $context;
return $this;
}
public function setSeverityNumber(int $severityNumber): self
{
$this->severityNumber = $severityNumber;
return $this;
}
public function setSeverityText(string $severityText): self
{
$this->severityText = $severityText;
return $this;
}
public function setAttributes(iterable $attributes): self
{
foreach ($attributes as $name => $value) {
$this->setAttribute($name, $value);
}
return $this;
}
public function setAttribute(string $name, $value): self
{
$this->attributes[$name] = $value;
return $this;
}
public function setBody($body = null): self
{
$this->body = $body;
return $this;
}
public function setObservedTimestamp(int $observedTimestamp = null): self
{
$this->observedTimestamp = $observedTimestamp;
return $this;
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Logs;
interface LoggerInterface
{
public function logRecord(LogRecord $logRecord): void;
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Logs;
/**
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/bridge-api.md#get-a-logger
*/
interface LoggerProviderInterface
{
public function getLogger(
string $name,
?string $version = null,
?string $schemaUrl = null,
bool $includeTraceContext = true,
iterable $attributes = [] //instrumentation scope attributes
): LoggerInterface;
}

40
src/API/Logs/Map/Psr3.php Normal file
View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Logs\Map;
use Psr\Log\LogLevel;
class Psr3
{
/**
* Maps PSR-3 severity level (string) to the appropriate opentelemetry severity number
*
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model-appendix.md#appendix-b-severitynumber-example-mappings
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#field-severitynumber
*/
public static function severityNumber(string $level): int
{
switch (strtolower($level)) {
case LogLevel::DEBUG:
return 5;
case LogLevel::INFO:
return 9;
case LogLevel::NOTICE:
return 10;
case LogLevel::WARNING:
return 13;
case LogLevel::ERROR:
return 17;
case LogLevel::CRITICAL:
return 18;
case LogLevel::ALERT:
return 19;
case LogLevel::EMERGENCY:
return 21;
default:
return 0;
}
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Logs;
use Psr\Log\LoggerTrait;
class NoopLogger implements LoggerInterface
{
use LoggerTrait;
public static function getInstance(): self
{
static $instance;
return $instance ??= new self();
}
/**
* @codeCoverageIgnore
*/
public function logRecord(LogRecord $logRecord): void
{
}
/**
* @codeCoverageIgnore
*/
public function log($level, $message, array $context = []): void
{
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\API\Logs;
class NoopLoggerProvider implements LoggerProviderInterface
{
public static function getInstance(): self
{
static $instance;
return $instance ??= new self();
}
public function getLogger(string $name, ?string $version = null, ?string $schemaUrl = null, bool $includeTraceContext = true, iterable $attributes = []): LoggerInterface
{
return NoopLogger::getInstance();
}
}

View File

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Contrib\Otlp;
use Opentelemetry\Proto\Collector\Logs\V1\ExportLogsServiceRequest;
use Opentelemetry\Proto\Common\V1\InstrumentationScope;
use Opentelemetry\Proto\Common\V1\KeyValue;
use Opentelemetry\Proto\Logs\V1\LogRecord;
use Opentelemetry\Proto\Logs\V1\ResourceLogs;
use Opentelemetry\Proto\Logs\V1\ScopeLogs;
use Opentelemetry\Proto\Resource\V1\Resource as Resource_;
use OpenTelemetry\SDK\Common\Attribute\AttributesInterface;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface;
use OpenTelemetry\SDK\Logs\ReadableLogRecord;
use OpenTelemetry\SDK\Resource\ResourceInfo;
class LogsConverter
{
/**
* @param iterable<ReadableLogRecord> $logs
* @psalm-suppress InvalidArgument
*/
public function convert(iterable $logs): ExportLogsServiceRequest
{
$pExportLogsServiceRequest = new ExportLogsServiceRequest();
$scopeLogs = [];
$resourceLogs = [];
$resourceCache = [];
$scopeCache = [];
foreach ($logs as $log) {
$resource = $log->getResource();
$instrumentationScope = $log->getInstrumentationScope();
$resourceId = $resourceCache[spl_object_id($resource)] ??= serialize([
$resource->getSchemaUrl(),
$resource->getAttributes()->toArray(),
$resource->getAttributes()->getDroppedAttributesCount(),
]);
$instrumentationScopeId = $scopeCache[spl_object_id($instrumentationScope)] ??= serialize([
$instrumentationScope->getName(),
$instrumentationScope->getVersion(),
$instrumentationScope->getSchemaUrl(),
$instrumentationScope->getAttributes()->toArray(),
$instrumentationScope->getAttributes()->getDroppedAttributesCount(),
]);
if (($pResourceLogs = $resourceLogs[$resourceId] ?? null) === null) {
/** @psalm-suppress InvalidArgument */
$pExportLogsServiceRequest->getResourceLogs()[]
= $resourceLogs[$resourceId]
= $pResourceLogs
= $this->convertResourceLogs($resource);
}
if (($pScopeLogs = $scopeLogs[$resourceId][$instrumentationScopeId] ?? null) === null) {
$pResourceLogs->getScopeLogs()[]
= $scopeLogs[$resourceId][$instrumentationScopeId]
= $pScopeLogs
= $this->convertInstrumentationScope($instrumentationScope);
}
$pScopeLogs->getLogRecords()[] = $this->convertLogRecord($log);
}
return $pExportLogsServiceRequest;
}
private function convertLogRecord(ReadableLogRecord $record): LogRecord
{
$pLogRecord = new LogRecord();
$pLogRecord->setBody(AttributesConverter::convertAnyValue($record->getBody()));
$pLogRecord->setTimeUnixNano($record->getTimestamp() ?? 0);
$pLogRecord->setObservedTimeUnixNano($record->getObservedTimestamp() ?? 0);
$spanContext = $record->getSpanContext();
if ($spanContext !== null && $spanContext->isValid()) {
$pLogRecord->setTraceId($spanContext->getTraceIdBinary());
$pLogRecord->setSpanId($spanContext->getSpanIdBinary());
$pLogRecord->setFlags($spanContext->getTraceFlags());
}
$severityNumber = $record->getSeverityNumber();
if ($severityNumber !== null) {
$pLogRecord->setSeverityNumber($severityNumber);
}
$severityText = $record->getSeverityText();
if ($severityText !== null) {
$pLogRecord->setSeverityText($severityText);
}
$this->setAttributes($pLogRecord, $record->getAttributes());
$pLogRecord->setDroppedAttributesCount($record->getAttributes()->getDroppedAttributesCount());
return $pLogRecord;
}
private function convertInstrumentationScope(InstrumentationScopeInterface $instrumentationScope): ScopeLogs
{
$pScopeLogs = new ScopeLogs();
$pInstrumentationScope = new InstrumentationScope();
$pInstrumentationScope->setName($instrumentationScope->getName());
$pInstrumentationScope->setVersion((string) $instrumentationScope->getVersion());
$this->setAttributes($pInstrumentationScope, $instrumentationScope->getAttributes());
$pInstrumentationScope->setDroppedAttributesCount($instrumentationScope->getAttributes()->getDroppedAttributesCount());
$pScopeLogs->setScope($pInstrumentationScope);
return $pScopeLogs;
}
private function convertResourceLogs(ResourceInfo $resource): ResourceLogs
{
$pResourceLogs = new ResourceLogs();
$pResource = new Resource_();
$this->setAttributes($pResource, $resource->getAttributes());
$pResource->setDroppedAttributesCount($resource->getAttributes()->getDroppedAttributesCount());
$pResourceLogs->setResource($pResource);
return $pResourceLogs;
}
/**
* @param Resource_|LogRecord|InstrumentationScope $pElement
*/
private function setAttributes($pElement, AttributesInterface $attributes): void
{
foreach ($attributes as $key => $value) {
/** @psalm-suppress InvalidArgument */
$pElement->getAttributes()[] = (new KeyValue())
->setKey($key)
->setValue(AttributesConverter::convertAnyValue($value));
}
$pElement->setDroppedAttributesCount($attributes->getDroppedAttributesCount());
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Contrib\Otlp;
use OpenTelemetry\API\Behavior\LogsMessagesTrait;
use Opentelemetry\Proto\Collector\Logs\V1\ExportLogsServiceResponse;
use OpenTelemetry\SDK\Common\Export\TransportInterface;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
use OpenTelemetry\SDK\Common\Future\FutureInterface;
use OpenTelemetry\SDK\Logs\LogRecordExporterInterface;
use OpenTelemetry\SDK\Logs\ReadableLogRecord;
use Throwable;
/**
* @psalm-import-type SUPPORTED_CONTENT_TYPES from ProtobufSerializer
*/
class LogsExporter implements LogRecordExporterInterface
{
use LogsMessagesTrait;
private TransportInterface $transport;
private ProtobufSerializer $serializer;
/**
* @psalm-param TransportInterface<SUPPORTED_CONTENT_TYPES> $transport
*/
public function __construct(TransportInterface $transport)
{
$this->transport = $transport;
$this->serializer = ProtobufSerializer::forTransport($transport);
}
/**
* @param iterable<ReadableLogRecord> $batch
*/
public function export(iterable $batch, ?CancellationInterface $cancellation = null): FutureInterface
{
return $this->transport
->send($this->serializer->serialize((new LogsConverter())->convert($batch)), $cancellation)
->map(function (?string $payload): bool {
if ($payload === null) {
return true;
}
$serviceResponse = new ExportLogsServiceResponse();
$this->serializer->hydrate($serviceResponse, $payload);
$partialSuccess = $serviceResponse->getPartialSuccess();
if ($partialSuccess !== null && $partialSuccess->getRejectedLogRecords()) {
self::logError('Export partial success', [
'rejected_logs' => $partialSuccess->getRejectedLogRecords(),
'error_message' => $partialSuccess->getErrorMessage(),
]);
return false;
}
if ($partialSuccess !== null && $partialSuccess->getErrorMessage()) {
self::logWarning('Export success with warnings/suggestions', ['error_message' => $partialSuccess->getErrorMessage()]);
}
return true;
})
->catch(static function (Throwable $throwable): bool {
self::logError('Export failure', ['exception' => $throwable]);
return false;
});
}
public function forceFlush(?CancellationInterface $cancellation = null): bool
{
return $this->transport->forceFlush($cancellation);
}
public function shutdown(?CancellationInterface $cancellation = null): bool
{
return $this->transport->shutdown($cancellation);
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Contrib\Otlp;
use OpenTelemetry\API\Common\Signal\Signals;
use OpenTelemetry\SDK\Common\Configuration\Configuration;
use OpenTelemetry\SDK\Common\Configuration\Defaults;
use OpenTelemetry\SDK\Common\Configuration\Variables;
use OpenTelemetry\SDK\Common\Export\TransportFactoryInterface;
use OpenTelemetry\SDK\Common\Export\TransportInterface;
use OpenTelemetry\SDK\Common\Otlp\HttpEndpointResolver;
use OpenTelemetry\SDK\Logs\LogRecordExporterFactoryInterface;
use OpenTelemetry\SDK\Logs\LogRecordExporterInterface;
use OpenTelemetry\SDK\Registry;
class LogsExporterFactory implements LogRecordExporterFactoryInterface
{
private const DEFAULT_COMPRESSION = 'none';
private ?TransportFactoryInterface $transportFactory;
public function __construct(?TransportFactoryInterface $transportFactory = null)
{
$this->transportFactory = $transportFactory;
}
/**
* @psalm-suppress ArgumentTypeCoercion
*/
public function create(): LogRecordExporterInterface
{
$protocol = Configuration::has(Variables::OTEL_EXPORTER_OTLP_LOGS_PROTOCOL)
? Configuration::getEnum(Variables::OTEL_EXPORTER_OTLP_LOGS_PROTOCOL)
: Configuration::getEnum(Variables::OTEL_EXPORTER_OTLP_PROTOCOL);
return new LogsExporter($this->buildTransport($protocol));
}
/**
* @psalm-suppress UndefinedClass
*/
private function buildTransport(string $protocol): TransportInterface
{
$endpoint = $this->getEndpoint($protocol);
$headers = Configuration::has(Variables::OTEL_EXPORTER_OTLP_LOGS_HEADERS)
? Configuration::getMap(Variables::OTEL_EXPORTER_OTLP_LOGS_HEADERS)
: Configuration::getMap(Variables::OTEL_EXPORTER_OTLP_HEADERS);
$headers += OtlpUtil::getUserAgentHeader();
$compression = $this->getCompression();
$factoryClass = Registry::transportFactory($protocol);
$factory = $this->transportFactory ?: new $factoryClass();
return $factory->create(
$endpoint,
Protocols::contentType($protocol),
$headers,
$compression,
);
}
private function getCompression(): string
{
return Configuration::has(Variables::OTEL_EXPORTER_OTLP_LOGS_COMPRESSION) ?
Configuration::getEnum(Variables::OTEL_EXPORTER_OTLP_LOGS_COMPRESSION) :
Configuration::getEnum(Variables::OTEL_EXPORTER_OTLP_COMPRESSION, self::DEFAULT_COMPRESSION);
}
private function getEndpoint(string $protocol): string
{
if (Configuration::has(Variables::OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)) {
return Configuration::getString(Variables::OTEL_EXPORTER_OTLP_LOGS_ENDPOINT);
}
$endpoint = Configuration::has(Variables::OTEL_EXPORTER_OTLP_ENDPOINT)
? Configuration::getString(Variables::OTEL_EXPORTER_OTLP_ENDPOINT)
: Defaults::OTEL_EXPORTER_OTLP_ENDPOINT;
if ($protocol === Protocols::GRPC) {
return $endpoint . OtlpUtil::method(Signals::LOGS);
}
return HttpEndpointResolver::create()->resolveToString($endpoint, Signals::LOGS);
}
}

View File

@ -5,3 +5,5 @@ declare(strict_types=1);
\OpenTelemetry\SDK\Registry::registerMetricExporterFactory('otlp', \OpenTelemetry\Contrib\Otlp\MetricExporterFactory::class);
\OpenTelemetry\SDK\Registry::registerTransportFactory('http', \OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory::class);
\OpenTelemetry\SDK\Registry::registerLogRecordExporterFactory('otlp', \OpenTelemetry\Contrib\Otlp\LogsExporterFactory::class);

View File

@ -68,6 +68,9 @@ final class AttributesBuilder implements AttributesBuilderInterface
}
$this->attributes[$offset] = $this->normalizeValue($value);
//@todo "There SHOULD be a message printed in the SDK's log to indicate to the user that an attribute was
// discarded due to such a limit. To prevent excessive logging, the message MUST be printed at most
// once per <thing> (i.e., not per discarded attribute)."
}
/**

View File

@ -31,6 +31,7 @@ interface Defaults
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#attribute-limits
*/
public const OTEL_ATTRIBUTE_COUNT_LIMIT = 128;
public const OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT = PHP_INT_MAX;
/**
* Span Limits
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#span-limits-
@ -40,6 +41,11 @@ interface Defaults
public const OTEL_SPAN_LINK_COUNT_LIMIT = 128;
public const OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT = 128;
public const OTEL_LINK_ATTRIBUTE_COUNT_LIMIT = 128;
/**
* LogRecord Limits
*/
public const OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT = PHP_INT_MAX;
public const OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT = 128;
/**
* OTLP Exporter
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#configuration-options
@ -48,18 +54,22 @@ interface Defaults
public const OTEL_EXPORTER_OTLP_ENDPOINT = 'http://localhost:4318';
public const OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:4318';
public const OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://localhost:4318';
public const OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://localhost:4318';
// Insecure
public const OTEL_EXPORTER_OTLP_INSECURE = 'false';
public const OTEL_EXPORTER_OTLP_TRACES_INSECURE = 'false';
public const OTEL_EXPORTER_OTLP_METRICS_INSECURE = 'false';
public const OTEL_EXPORTER_OTLP_LOGS_INSECURE = 'false';
// Timeout (seconds)
public const OTEL_EXPORTER_OTLP_TIMEOUT = 10;
public const OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = 10;
public const OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = 10;
public const OTEL_EXPORTER_OTLP_LOGS_TIMEOUT = 10;
// Protocol
public const OTEL_EXPORTER_OTLP_PROTOCOL = 'http/protobuf';
public const OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'http/protobuf';
public const OTEL_EXPORTER_OTLP_METRICS_PROTOCOL = 'http/protobuf';
public const OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'http/protobuf';
/**
* Zipkin Exporter
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#zipkin-exporter
@ -95,4 +105,5 @@ interface Defaults
public const OTEL_PHP_DETECTORS = 'all';
public const OTEL_PHP_AUTOLOAD_ENABLED = 'false';
public const OTEL_PHP_DISABLED_INSTRUMENTATIONS = [];
public const OTEL_PHP_LOGS_PROCESSOR = 'batch';
}

View File

@ -35,6 +35,12 @@ interface Variables
*/
public const OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT = 'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT';
public const OTEL_ATTRIBUTE_COUNT_LIMIT = 'OTEL_ATTRIBUTE_COUNT_LIMIT';
/**
* LogRecord limits
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#logrecord-limits
*/
public const OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT = 'OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT';
public const OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT = 'OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT';
/**
* Span Limits
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#span-limits-
@ -53,30 +59,37 @@ interface Variables
public const OTEL_EXPORTER_OTLP_ENDPOINT = 'OTEL_EXPORTER_OTLP_ENDPOINT';
public const OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT';
public const OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT';
public const OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT';
// Insecure
public const OTEL_EXPORTER_OTLP_INSECURE = 'OTEL_EXPORTER_OTLP_INSECURE';
public const OTEL_EXPORTER_OTLP_TRACES_INSECURE = 'OTEL_EXPORTER_OTLP_TRACES_INSECURE';
public const OTEL_EXPORTER_OTLP_METRICS_INSECURE = 'OTEL_EXPORTER_OTLP_METRICS_INSECURE';
public const OTEL_EXPORTER_OTLP_LOGS_INSECURE = 'OTEL_EXPORTER_OTLP_LOGS_INSECURE';
// Certificate File
public const OTEL_EXPORTER_OTLP_CERTIFICATE = 'OTEL_EXPORTER_OTLP_CERTIFICATE';
public const OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = 'OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE';
public const OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE = 'OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE';
public const OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE = 'OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE';
// Headers
public const OTEL_EXPORTER_OTLP_HEADERS = 'OTEL_EXPORTER_OTLP_HEADERS';
public const OTEL_EXPORTER_OTLP_TRACES_HEADERS = 'OTEL_EXPORTER_OTLP_TRACES_HEADERS';
public const OTEL_EXPORTER_OTLP_METRICS_HEADERS = 'OTEL_EXPORTER_OTLP_METRICS_HEADERS';
public const OTEL_EXPORTER_OTLP_LOGS_HEADERS = 'OTEL_EXPORTER_OTLP_LOGS_HEADERS';
// Compression
public const OTEL_EXPORTER_OTLP_COMPRESSION = 'OTEL_EXPORTER_OTLP_COMPRESSION';
public const OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = 'OTEL_EXPORTER_OTLP_TRACES_COMPRESSION';
public const OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = 'OTEL_EXPORTER_OTLP_METRICS_COMPRESSION';
public const OTEL_EXPORTER_OTLP_LOGS_COMPRESSION = 'OTEL_EXPORTER_OTLP_LOGS_COMPRESSION';
// Timeout
public const OTEL_EXPORTER_OTLP_TIMEOUT = 'OTEL_EXPORTER_OTLP_TIMEOUT';
public const OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = 'OTEL_EXPORTER_OTLP_TRACES_TIMEOUT';
public const OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = 'OTEL_EXPORTER_OTLP_METRICS_TIMEOUT';
public const OTEL_EXPORTER_OTLP_LOGS_TIMEOUT = 'OTEL_EXPORTER_OTLP_LOGS_TIMEOUT';
// Protocol
public const OTEL_EXPORTER_OTLP_PROTOCOL = 'OTEL_EXPORTER_OTLP_PROTOCOL';
public const OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = 'OTEL_EXPORTER_OTLP_TRACES_PROTOCOL';
public const OTEL_EXPORTER_OTLP_METRICS_PROTOCOL = 'OTEL_EXPORTER_OTLP_METRICS_PROTOCOL';
public const OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = 'OTEL_EXPORTER_OTLP_LOGS_PROTOCOL';
/**
* Zipkin Exporter
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#zipkin-exporter
@ -109,6 +122,7 @@ interface Variables
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#language-specific-environment-variables
*/
public const OTEL_PHP_TRACES_PROCESSOR = 'OTEL_PHP_TRACES_PROCESSOR';
public const OTEL_PHP_LOGS_PROCESSOR = 'OTEL_PHP_LOGS_PROCESSOR';
public const OTEL_PHP_DETECTORS = 'OTEL_PHP_DETECTORS';
public const OTEL_PHP_AUTOLOAD_ENABLED = 'OTEL_PHP_AUTOLOAD_ENABLED';
public const OTEL_PHP_DISABLED_INSTRUMENTATIONS = 'OTEL_PHP_DISABLED_INSTRUMENTATIONS';

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs\Exporter;
use OpenTelemetry\SDK\Common\Export\TransportInterface;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
use OpenTelemetry\SDK\Common\Future\CompletedFuture;
use OpenTelemetry\SDK\Common\Future\FutureInterface;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface;
use OpenTelemetry\SDK\Logs\LogRecordExporterInterface;
use OpenTelemetry\SDK\Logs\ReadableLogRecord;
use OpenTelemetry\SDK\Resource\ResourceInfo;
class ConsoleExporter implements LogRecordExporterInterface
{
private TransportInterface $transport;
public function __construct(TransportInterface $transport)
{
$this->transport = $transport;
}
/**
* @param iterable<mixed, ReadableLogRecord> $batch
*/
public function export(iterable $batch, ?CancellationInterface $cancellation = null): FutureInterface
{
$resource = null;
$scope = null;
foreach ($batch as $record) {
if (!$resource) {
$resource = $this->convertResource($record->getResource());
}
if (!$scope) {
$scope = $this->convertInstrumentationScope($record->getInstrumentationScope());
$scope['logs'] = [];
}
$scope['logs'][] = $this->convertLogRecord($record);
}
$output = [
'resource' => $resource,
'scope' => $scope,
];
$this->transport->send(json_encode($output, JSON_PRETTY_PRINT));
return new CompletedFuture(true);
}
public function forceFlush(?CancellationInterface $cancellation = null): bool
{
return true;
}
public function shutdown(?CancellationInterface $cancellation = null): bool
{
return true;
}
private function convertLogRecord(ReadableLogRecord $record): array
{
$spanContext = $record->getSpanContext();
return [
'timestamp' => $record->getTimestamp(),
'observed_timestamp' => $record->getObservedTimestamp(),
'severity_number' => $record->getSeverityNumber(),
'severity_text' => $record->getSeverityText(),
'body' => $record->getBody(),
'trace_id' => $spanContext !== null ? $spanContext->getTraceId() : '',
'span_id' => $spanContext !== null ? $spanContext->getSpanId() : '',
'trace_flags' => $spanContext !== null ? $spanContext->getTraceFlags() : null,
'attributes' => $record->getAttributes()->toArray(),
'dropped_attributes_count' => $record->getAttributes()->getDroppedAttributesCount(),
];
}
private function convertResource(ResourceInfo $resource): array
{
return [
'attributes' => $resource->getAttributes()->toArray(),
'dropped_attributes_count' => $resource->getAttributes()->getDroppedAttributesCount(),
];
}
private function convertInstrumentationScope(InstrumentationScopeInterface $scope): array
{
return [
'name' => $scope->getName(),
'version' => $scope->getVersion(),
'attributes' => $scope->getAttributes()->toArray(),
'dropped_attributes_count' => $scope->getAttributes()->getDroppedAttributesCount(),
'schema_url' => $scope->getSchemaUrl(),
];
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs\Exporter;
use OpenTelemetry\SDK\Logs\LogRecordExporterFactoryInterface;
use OpenTelemetry\SDK\Logs\LogRecordExporterInterface;
use OpenTelemetry\SDK\Registry;
class ConsoleExporterFactory implements LogRecordExporterFactoryInterface
{
public function create(): LogRecordExporterInterface
{
$transport = Registry::transportFactory('stream')->create('php://stdout', 'application/json');
return new ConsoleExporter($transport);
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs\Exporter;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
use OpenTelemetry\SDK\Common\Future\CompletedFuture;
use OpenTelemetry\SDK\Common\Future\FutureInterface;
use OpenTelemetry\SDK\Logs\LogRecordExporterInterface;
class NoopExporter implements LogRecordExporterInterface
{
public function export(iterable $batch, ?CancellationInterface $cancellation = null): FutureInterface
{
return new CompletedFuture(true);
}
public function forceFlush(?CancellationInterface $cancellation = null): bool
{
return true;
}
public function shutdown(?CancellationInterface $cancellation = null): bool
{
return true;
}
}

View File

@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
\OpenTelemetry\SDK\Registry::registerLogRecordExporterFactory('console', \OpenTelemetry\SDK\Logs\Exporter\ConsoleExporterFactory::class);

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use InvalidArgumentException;
use OpenTelemetry\SDK\Common\Configuration\Configuration;
use OpenTelemetry\SDK\Common\Configuration\Variables;
use OpenTelemetry\SDK\Logs\Exporter\NoopExporter;
use OpenTelemetry\SDK\Registry;
class ExporterFactory
{
public function create(): LogRecordExporterInterface
{
$exporters = Configuration::getList(Variables::OTEL_LOGS_EXPORTER);
if (1 !== count($exporters)) {
throw new InvalidArgumentException(sprintf('Configuration %s requires exactly 1 exporter', Variables::OTEL_TRACES_EXPORTER));
}
$exporter = $exporters[0];
if ($exporter === 'none') {
return new NoopExporter();
}
$factory = Registry::logRecordExporterFactory($exporter);
return $factory->create();
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
interface LogRecordExporterFactoryInterface
{
public function create(): LogRecordExporterInterface;
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
use OpenTelemetry\SDK\Common\Future\FutureInterface;
interface LogRecordExporterInterface
{
/**
* @param iterable<ReadableLogRecord> $batch
*/
public function export(iterable $batch, ?CancellationInterface $cancellation = null): FutureInterface;
public function forceFlush(?CancellationInterface $cancellation = null): bool;
public function shutdown(?CancellationInterface $cancellation = null): bool;
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use OpenTelemetry\SDK\Common\Attribute\AttributesFactoryInterface;
/**
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/sdk.md#logrecord-limits
*/
class LogRecordLimits
{
private AttributesFactoryInterface $attributesFactory;
/**
* @internal Use {@see SpanLimitsBuilder} to create {@see SpanLimits} instance.
*/
public function __construct(
AttributesFactoryInterface $attributesFactory
) {
$this->attributesFactory = $attributesFactory;
}
public function getAttributeFactory(): AttributesFactoryInterface
{
return $this->attributesFactory;
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Configuration\Configuration;
use OpenTelemetry\SDK\Common\Configuration\Variables;
use const PHP_INT_MAX;
class LogRecordLimitsBuilder
{
/** @var ?int Maximum allowed attribute count per record */
private ?int $attributeCountLimit = null;
/** @var ?int Maximum allowed attribute value length */
private ?int $attributeValueLengthLimit = null;
/**
* @param int $attributeCountLimit Maximum allowed attribute count per record
*/
public function setAttributeCountLimit(int $attributeCountLimit): LogRecordLimitsBuilder
{
$this->attributeCountLimit = $attributeCountLimit;
return $this;
}
/**
* @param int $attributeValueLengthLimit Maximum allowed attribute value length
*/
public function setAttributeValueLengthLimit(int $attributeValueLengthLimit): LogRecordLimitsBuilder
{
$this->attributeValueLengthLimit = $attributeValueLengthLimit;
return $this;
}
/**
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/sdk-environment-variables.md#attribute-limits
*/
public function build(): LogRecordLimits
{
$attributeCountLimit = $this->attributeCountLimit
?: Configuration::getInt(Variables::OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT);
$attributeValueLengthLimit = $this->attributeValueLengthLimit
?: Configuration::getInt(Variables::OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT);
if ($attributeValueLengthLimit === PHP_INT_MAX) {
$attributeValueLengthLimit = null;
}
$attributesFactory = Attributes::factory($attributeCountLimit, $attributeValueLengthLimit);
return new LogRecordLimits($attributesFactory);
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use InvalidArgumentException;
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\SDK\Common\Configuration\Configuration;
use OpenTelemetry\SDK\Common\Configuration\KnownValues;
use OpenTelemetry\SDK\Common\Configuration\KnownValues as Values;
use OpenTelemetry\SDK\Common\Configuration\Variables;
use OpenTelemetry\SDK\Common\Time\ClockFactory;
use OpenTelemetry\SDK\Logs\Processor\BatchLogsProcessor;
use OpenTelemetry\SDK\Logs\Processor\NoopLogsProcessor;
use OpenTelemetry\SDK\Logs\Processor\SimpleLogsProcessor;
class LogRecordProcessorFactory
{
public function create(LogRecordExporterInterface $exporter, ?MeterProviderInterface $meterProvider = null): LogRecordProcessorInterface
{
$name = Configuration::getEnum(Variables::OTEL_PHP_LOGS_PROCESSOR);
switch ($name) {
case KnownValues::VALUE_BATCH:
return new BatchLogsProcessor(
$exporter,
ClockFactory::getDefault(),
BatchLogsProcessor::DEFAULT_MAX_QUEUE_SIZE,
BatchLogsProcessor::DEFAULT_SCHEDULE_DELAY,
BatchLogsProcessor::DEFAULT_EXPORT_TIMEOUT,
BatchLogsProcessor::DEFAULT_MAX_EXPORT_BATCH_SIZE,
true,
$meterProvider,
);
case KnownValues::VALUE_SIMPLE:
return new SimpleLogsProcessor($exporter);
case Values::VALUE_NOOP:
case Values::VALUE_NONE:
return NoopLogsProcessor::getInstance();
default:
throw new InvalidArgumentException('Unknown processor: ' . $name);
}
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
interface LogRecordProcessorInterface
{
public function onEmit(ReadWriteLogRecord $record, ?ContextInterface $context = null): void;
public function shutdown(?CancellationInterface $cancellation = null): bool;
public function forceFlush(?CancellationInterface $cancellation = null): bool;
}

39
src/SDK/Logs/Logger.php Normal file
View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use OpenTelemetry\API\Logs\LoggerInterface;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface;
/**
* Note that this logger class is deliberately NOT psr-3 compatible, per spec: "Note: this document defines a log
* backend API. The API is not intended to be called by application developers directly."
*
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/bridge-api.md
*/
class Logger implements LoggerInterface
{
private InstrumentationScopeInterface $scope;
private LoggerSharedState $loggerSharedState;
private bool $includeTraceContext;
public function __construct(LoggerSharedState $loggerSharedState, InstrumentationScopeInterface $scope, bool $includeTraceContext)
{
$this->loggerSharedState = $loggerSharedState;
$this->scope = $scope;
$this->includeTraceContext = $includeTraceContext;
}
public function logRecord(LogRecord $logRecord): void
{
$readWriteLogRecord = new ReadWriteLogRecord($this->scope, $this->loggerSharedState, $logRecord, $this->includeTraceContext);
// @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/sdk.md#onemit
$this->loggerSharedState->getProcessor()->onEmit(
$readWriteLogRecord,
$readWriteLogRecord->getContext(),
);
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use OpenTelemetry\API\Logs\LoggerInterface;
use OpenTelemetry\API\Logs\NoopLogger;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeFactoryInterface;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
class LoggerProvider implements LoggerProviderInterface
{
private LoggerSharedState $loggerSharedState;
private InstrumentationScopeFactoryInterface $instrumentationScopeFactory;
public function __construct(LogRecordProcessorInterface $processor, InstrumentationScopeFactoryInterface $instrumentationScopeFactory, ?ResourceInfo $resource = null)
{
$this->loggerSharedState = new LoggerSharedState(
$resource ?? ResourceInfoFactory::defaultResource(),
(new LogRecordLimitsBuilder())->build(),
$processor
);
$this->instrumentationScopeFactory = $instrumentationScopeFactory;
}
/**
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/sdk.md#logger-creation
*/
public function getLogger(string $name, ?string $version = null, ?string $schemaUrl = null, bool $includeTraceContext = true, iterable $attributes = []): LoggerInterface
{
if ($this->loggerSharedState->hasShutdown()) {
return NoopLogger::getInstance();
}
$scope = $this->instrumentationScopeFactory->create($name, $version, $schemaUrl, $attributes);
return new Logger($this->loggerSharedState, $scope, $includeTraceContext);
}
public function shutdown(CancellationInterface $cancellation = null): bool
{
return $this->loggerSharedState->shutdown($cancellation);
}
public function forceFlush(CancellationInterface $cancellation = null): bool
{
return $this->loggerSharedState->getProcessor()->forceFlush($cancellation);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeFactory;
use OpenTelemetry\SDK\Metrics\MeterProviderInterface;
use OpenTelemetry\SDK\Sdk;
class LoggerProviderFactory
{
public function create(?MeterProviderInterface $meterProvider = null): LoggerProviderInterface
{
if (Sdk::isDisabled()) {
return NoopLoggerProvider::getInstance();
}
$exporter = (new ExporterFactory())->create();
$processor = (new LogRecordProcessorFactory())->create($exporter, $meterProvider);
$instrumentationScopeFactory = new InstrumentationScopeFactory((new LogRecordLimitsBuilder())->build()->getAttributeFactory());
return new LoggerProvider($processor, $instrumentationScopeFactory);
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use OpenTelemetry\API\Logs as API;
interface LoggerProviderInterface extends API\LoggerProviderInterface
{
public function shutdown(): bool;
public function forceFlush(): bool;
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
use OpenTelemetry\SDK\Resource\ResourceInfo;
class LoggerSharedState
{
private ResourceInfo $resource;
private LogRecordProcessorInterface $processor;
private LogRecordLimits $limits;
private ?bool $shutdownResult = null;
public function __construct(
ResourceInfo $resource,
LogRecordLimits $limits,
LogRecordProcessorInterface $processor
) {
$this->resource = $resource;
$this->limits = $limits;
$this->processor = $processor;
}
public function hasShutdown(): bool
{
return null !== $this->shutdownResult;
}
public function getResource(): ResourceInfo
{
return $this->resource;
}
public function getProcessor(): LogRecordProcessorInterface
{
return $this->processor;
}
public function getLogRecordLimits(): LogRecordLimits
{
return $this->limits;
}
/**
* Returns `false` if the provider is already shutdown, otherwise `true`.
*/
public function shutdown(?CancellationInterface $cancellation = null): bool
{
return $this->shutdownResult ?? ($this->shutdownResult = $this->processor->shutdown($cancellation));
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use OpenTelemetry\API\Logs\LoggerInterface;
use OpenTelemetry\API\Logs\NoopLogger;
class NoopLoggerProvider implements LoggerProviderInterface
{
public static function getInstance(): self
{
static $instance;
return $instance ??= new self();
}
public function getLogger(string $name, ?string $version = null, ?string $schemaUrl = null, bool $includeTraceContext = true, iterable $attributes = []): LoggerInterface
{
return NoopLogger::getInstance();
}
public function shutdown(): bool
{
return true;
}
public function forceFlush(): bool
{
return true;
}
}

View File

@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs\Processor;
use InvalidArgumentException;
use OpenTelemetry\API\Behavior\LogsMessagesTrait;
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\API\Metrics\ObserverInterface;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
use OpenTelemetry\SDK\Common\Time\ClockInterface;
use OpenTelemetry\SDK\Logs\LogRecordExporterInterface;
use OpenTelemetry\SDK\Logs\LogRecordProcessorInterface;
use OpenTelemetry\SDK\Logs\ReadWriteLogRecord;
use SplQueue;
use Throwable;
class BatchLogsProcessor implements LogRecordProcessorInterface
{
use LogsMessagesTrait;
public const DEFAULT_SCHEDULE_DELAY = 5000;
public const DEFAULT_EXPORT_TIMEOUT = 30000;
public const DEFAULT_MAX_QUEUE_SIZE = 2048;
public const DEFAULT_MAX_EXPORT_BATCH_SIZE = 512;
private const ATTRIBUTES_PROCESSOR = ['processor' => 'batching'];
private const ATTRIBUTES_QUEUED = self::ATTRIBUTES_PROCESSOR + ['state' => 'queued'];
private const ATTRIBUTES_PENDING = self::ATTRIBUTES_PROCESSOR + ['state' => 'pending'];
private const ATTRIBUTES_PROCESSED = self::ATTRIBUTES_PROCESSOR + ['state' => 'processed'];
private const ATTRIBUTES_DROPPED = self::ATTRIBUTES_PROCESSOR + ['state' => 'dropped'];
private const ATTRIBUTES_FREE = self::ATTRIBUTES_PROCESSOR + ['state' => 'free'];
private LogRecordExporterInterface $exporter;
private ClockInterface $clock;
private int $maxQueueSize;
private int $scheduledDelayNanos;
private int $maxExportBatchSize;
private bool $autoFlush;
private ContextInterface $exportContext;
private ?int $nextScheduledRun = null;
private bool $running = false;
private int $dropped = 0;
private int $processed = 0;
private int $batchId = 0;
private int $queueSize = 0;
/** @var list<ReadWriteLogRecord> */
private array $batch = [];
/** @var SplQueue<list<ReadWriteLogRecord>> */
private SplQueue $queue;
/** @var SplQueue<array{int, string, ?CancellationInterface, bool, ContextInterface}> */
private SplQueue $flush;
private bool $closed = false;
public function __construct(
LogRecordExporterInterface $exporter,
ClockInterface $clock,
int $maxQueueSize = self::DEFAULT_MAX_QUEUE_SIZE,
int $scheduledDelayMillis = self::DEFAULT_SCHEDULE_DELAY,
int $exportTimeoutMillis = self::DEFAULT_EXPORT_TIMEOUT,
int $maxExportBatchSize = self::DEFAULT_MAX_EXPORT_BATCH_SIZE,
bool $autoFlush = true,
?MeterProviderInterface $meterProvider = null
) {
if ($maxQueueSize <= 0) {
throw new InvalidArgumentException(sprintf('Maximum queue size (%d) must be greater than zero', $maxQueueSize));
}
if ($scheduledDelayMillis <= 0) {
throw new InvalidArgumentException(sprintf('Scheduled delay (%d) must be greater than zero', $scheduledDelayMillis));
}
if ($exportTimeoutMillis <= 0) {
throw new InvalidArgumentException(sprintf('Export timeout (%d) must be greater than zero', $exportTimeoutMillis));
}
if ($maxExportBatchSize <= 0) {
throw new InvalidArgumentException(sprintf('Maximum export batch size (%d) must be greater than zero', $maxExportBatchSize));
}
if ($maxExportBatchSize > $maxQueueSize) {
throw new InvalidArgumentException(sprintf('Maximum export batch size (%d) must be less than or equal to maximum queue size (%d)', $maxExportBatchSize, $maxQueueSize));
}
$this->exporter = $exporter;
$this->clock = $clock;
$this->maxQueueSize = $maxQueueSize;
$this->scheduledDelayNanos = $scheduledDelayMillis * 1_000_000;
$this->maxExportBatchSize = $maxExportBatchSize;
$this->autoFlush = $autoFlush;
$this->exportContext = Context::getCurrent();
$this->queue = new SplQueue();
$this->flush = new SplQueue();
if ($meterProvider === null) {
return;
}
$meter = $meterProvider->getMeter('io.opentelemetry.sdk');
$meter
->createObservableUpDownCounter(
'otel.logs.log_processor.logs',
'{logs}',
'The number of log records received by the processor',
)
->observe(function (ObserverInterface $observer): void {
$queued = $this->queue->count() * $this->maxExportBatchSize + count($this->batch);
$pending = $this->queueSize - $queued;
$processed = $this->processed;
$dropped = $this->dropped;
$observer->observe($queued, self::ATTRIBUTES_QUEUED);
$observer->observe($pending, self::ATTRIBUTES_PENDING);
$observer->observe($processed, self::ATTRIBUTES_PROCESSED);
$observer->observe($dropped, self::ATTRIBUTES_DROPPED);
}, true);
$meter
->createObservableUpDownCounter(
'otel.logs.log_processor.queue.limit',
'{logs}',
'The queue size limit',
)
->observe(function (ObserverInterface $observer): void {
$observer->observe($this->maxQueueSize, self::ATTRIBUTES_PROCESSOR);
}, true);
$meter
->createObservableUpDownCounter(
'otel.logs.log_processor.queue.usage',
'{logs}',
'The current queue usage',
)
->observe(function (ObserverInterface $observer): void {
$queued = $this->queue->count() * $this->maxExportBatchSize + count($this->batch);
$pending = $this->queueSize - $queued;
$free = $this->maxQueueSize - $this->queueSize;
$observer->observe($queued, self::ATTRIBUTES_QUEUED);
$observer->observe($pending, self::ATTRIBUTES_PENDING);
$observer->observe($free, self::ATTRIBUTES_FREE);
}, true);
}
public function onEmit(ReadWriteLogRecord $record, ?ContextInterface $context = null): void
{
if ($this->closed) {
return;
}
if ($this->queueSize === $this->maxQueueSize) {
$this->dropped++;
return;
}
$this->queueSize++;
$this->batch[] = $record;
$this->nextScheduledRun ??= $this->clock->now() + $this->scheduledDelayNanos;
if (count($this->batch) === $this->maxExportBatchSize) {
$this->enqueueBatch();
}
if ($this->autoFlush) {
$this->flush();
}
}
public function forceFlush(?CancellationInterface $cancellation = null): bool
{
if ($this->closed) {
return false;
}
return $this->flush(__FUNCTION__, $cancellation);
}
public function shutdown(?CancellationInterface $cancellation = null): bool
{
if ($this->closed) {
return false;
}
$this->closed = true;
return $this->flush(__FUNCTION__, $cancellation);
}
private function flush(?string $flushMethod = null, ?CancellationInterface $cancellation = null): bool
{
if ($flushMethod !== null) {
$flushId = $this->batchId + $this->queue->count() + (int) (bool) $this->batch;
$this->flush->enqueue([$flushId, $flushMethod, $cancellation, !$this->running, Context::getCurrent()]);
}
if ($this->running) {
return false;
}
$success = true;
$exception = null;
$this->running = true;
try {
for (;;) {
while (!$this->flush->isEmpty() && $this->flush->bottom()[0] <= $this->batchId) {
[, $flushMethod, $cancellation, $propagateResult, $context] = $this->flush->dequeue();
$scope = $context->activate();
try {
$result = $this->exporter->$flushMethod($cancellation);
if ($propagateResult) {
$success = $result;
}
} catch (Throwable $e) {
if ($propagateResult) {
$exception = $e;
} else {
self::logError(sprintf('Unhandled %s error', $flushMethod), ['exception' => $e]);
}
} finally {
$scope->detach();
}
}
if (!$this->shouldFlush()) {
break;
}
if ($this->queue->isEmpty()) {
$this->enqueueBatch();
}
$batchSize = count($this->queue->bottom());
$this->batchId++;
$scope = $this->exportContext->activate();
try {
$this->exporter->export($this->queue->dequeue())->await();
} catch (Throwable $e) {
self::logError('Unhandled export error', ['exception' => $e]);
} finally {
$this->processed += $batchSize;
$this->queueSize -= $batchSize;
$scope->detach();
}
}
} finally {
$this->running = false;
}
if ($exception !== null) {
throw $exception;
}
return $success;
}
private function shouldFlush(): bool
{
return !$this->flush->isEmpty()
|| $this->autoFlush && !$this->queue->isEmpty()
|| $this->autoFlush && $this->nextScheduledRun !== null && $this->clock->now() > $this->nextScheduledRun;
}
private function enqueueBatch(): void
{
assert($this->batch !== []);
$this->queue->enqueue($this->batch);
$this->batch = [];
$this->nextScheduledRun = null;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs\Processor;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
use OpenTelemetry\SDK\Logs\LogRecordProcessorInterface;
use OpenTelemetry\SDK\Logs\ReadWriteLogRecord;
class NoopLogsProcessor implements LogRecordProcessorInterface
{
public static function getInstance(): self
{
static $instance;
return $instance ??= new self();
}
/**
* @codeCoverageIgnore
*/
public function onEmit(ReadWriteLogRecord $record, ?ContextInterface $context = null): void
{
}
public function shutdown(?CancellationInterface $cancellation = null): bool
{
return true;
}
public function forceFlush(?CancellationInterface $cancellation = null): bool
{
return true;
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs\Processor;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\SDK\Common\Future\CancellationInterface;
use OpenTelemetry\SDK\Logs\LogRecordExporterInterface;
use OpenTelemetry\SDK\Logs\LogRecordProcessorInterface;
use OpenTelemetry\SDK\Logs\ReadWriteLogRecord;
class SimpleLogsProcessor implements LogRecordProcessorInterface
{
private LogRecordExporterInterface $exporter;
public function __construct(LogRecordExporterInterface $exporter)
{
$this->exporter = $exporter;
}
/**
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/sdk.md#onemit
*/
public function onEmit(ReadWriteLogRecord $record, ?ContextInterface $context = null): void
{
$this->exporter->export([$record]);
}
public function shutdown(?CancellationInterface $cancellation = null): bool
{
return $this->exporter->shutdown($cancellation);
}
public function forceFlush(?CancellationInterface $cancellation = null): bool
{
return $this->exporter->forceFlush($cancellation);
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
class ReadWriteLogRecord extends ReadableLogRecord
{
}

View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\SDK\Logs;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\API\Trace\SpanContextInterface;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\SDK\Common\Attribute\AttributesInterface;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface;
use OpenTelemetry\SDK\Resource\ResourceInfo;
/**
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#log-and-event-record-definition
* "Note: Typically this will be implemented with a new interface or (immutable) value type."
*/
class ReadableLogRecord extends LogRecord
{
private InstrumentationScopeInterface $scope;
private LoggerSharedState $loggerSharedState;
protected AttributesInterface $convertedAttributes;
protected SpanContextInterface $spanContext;
public function __construct(InstrumentationScopeInterface $scope, LoggerSharedState $loggerSharedState, LogRecord $logRecord, bool $includeTraceContext)
{
$this->scope = $scope;
$this->loggerSharedState = $loggerSharedState;
parent::__construct($logRecord->body);
$this->timestamp = $logRecord->timestamp;
$this->observedTimestamp = $logRecord->observedTimestamp;
$this->context = $logRecord->context;
if ($includeTraceContext) {
$context = $this->context ?? Context::getCurrent();
$this->spanContext = Span::fromContext($context)->getContext();
};
$this->severityNumber = $logRecord->severityNumber;
$this->severityText = $logRecord->severityText;
//convert attributes now so that excess data is not sent to processors
$this->convertedAttributes = $this->loggerSharedState
->getLogRecordLimits()
->getAttributeFactory()
->builder($logRecord->attributes)
->build();
}
public function getInstrumentationScope(): InstrumentationScopeInterface
{
return $this->scope;
}
public function getResource(): ResourceInfo
{
return $this->loggerSharedState->getResource();
}
public function getTimestamp(): ?int
{
return $this->timestamp;
}
public function getObservedTimestamp(): ?int
{
return $this->observedTimestamp;
}
public function getContext(): ?ContextInterface
{
return $this->context;
}
public function getSpanContext(): ?SpanContextInterface
{
return $this->spanContext;
}
public function getSeverityNumber(): ?int
{
return $this->severityNumber;
}
public function getSeverityText(): ?string
{
return $this->severityText;
}
/**
* @return mixed|null
*/
public function getBody()
{
return $this->body;
}
public function getAttributes(): AttributesInterface
{
return $this->convertedAttributes;
}
}

View File

@ -6,6 +6,7 @@ namespace OpenTelemetry\SDK;
use OpenTelemetry\Context\Propagation\TextMapPropagatorInterface;
use OpenTelemetry\SDK\Common\Export\TransportFactoryInterface;
use OpenTelemetry\SDK\Logs\LogRecordExporterFactoryInterface;
use OpenTelemetry\SDK\Metrics\MetricExporterFactoryInterface;
use OpenTelemetry\SDK\Trace\SpanExporter\SpanExporterFactoryInterface;
use RuntimeException;
@ -20,6 +21,7 @@ class Registry
private static array $transportFactories = [];
private static array $metricExporterFactories = [];
private static array $textMapPropagators = [];
private static array $logRecordExporterFactories = [];
/**
* @param TransportFactoryInterface|class-string<TransportFactoryInterface> $factory
@ -89,6 +91,25 @@ class Registry
}
self::$metricExporterFactories[$exporter] = $factory;
}
public static function registerLogRecordExporterFactory(string $exporter, $factory, bool $clobber = false): void
{
if (!$clobber && array_key_exists($exporter, self::$logRecordExporterFactories)) {
return;
}
if (!is_subclass_of($factory, LogRecordExporterFactoryInterface::class)) {
trigger_error(
sprintf(
'Cannot register LogRecord exporter factory: %s must exist and implement %s',
is_string($factory) ? $factory : get_class($factory),
LogRecordExporterFactoryInterface::class
),
E_USER_WARNING
);
return;
}
self::$logRecordExporterFactories[$exporter] = $factory;
}
public static function registerTextMapPropagator(string $name, TextMapPropagatorInterface $propagator, bool $clobber = false): void
{
@ -110,6 +131,18 @@ class Registry
return $factory;
}
public static function logRecordExporterFactory(string $exporter): LogRecordExporterFactoryInterface
{
if (!array_key_exists($exporter, self::$logRecordExporterFactories)) {
throw new RuntimeException('LogRecord exporter factory not defined for: ' . $exporter);
}
$class = self::$logRecordExporterFactories[$exporter];
$factory = (is_callable($class)) ? $class : new $class();
assert($factory instanceof LogRecordExporterFactoryInterface);
return $factory;
}
/**
* Get transport factory registered for protocol. If $protocol contains a content-type eg `http/xyz` then
* only the first part, `http`, is used.

View File

@ -10,6 +10,7 @@ use OpenTelemetry\API\Common\Instrumentation\Globals;
use OpenTelemetry\SDK\Common\Configuration\Configuration;
use OpenTelemetry\SDK\Common\Configuration\Variables;
use OpenTelemetry\SDK\Common\Util\ShutdownHandler;
use OpenTelemetry\SDK\Logs\LoggerProviderFactory;
use OpenTelemetry\SDK\Metrics\MeterProviderFactory;
use OpenTelemetry\SDK\Propagation\PropagatorFactory;
use OpenTelemetry\SDK\Trace\ExporterFactory;
@ -47,12 +48,16 @@ class SdkAutoloader
->setSampler((new SamplerFactory())->create())
->build();
$loggerProvider = (new LoggerProviderFactory())->create($meterProvider);
ShutdownHandler::register([$tracerProvider, 'shutdown']);
ShutdownHandler::register([$meterProvider, 'shutdown']);
ShutdownHandler::register([$loggerProvider, 'shutdown']);
return $configurator
->withTracerProvider($tracerProvider)
->withMeterProvider($meterProvider)
->withLoggerProvider($loggerProvider)
->withPropagator($propagator);
});
@ -62,7 +67,7 @@ class SdkAutoloader
/**
* @internal
*/
public static function shutdown(): void
public static function reset(): void
{
self::$enabled = null;
}

View File

@ -31,6 +31,7 @@
},
"files": [
"Common/Util/functions.php",
"Logs/Exporter/_register.php",
"Metrics/MetricExporter/_register.php",
"Propagation/_register.php",
"Trace/SpanExporter/_register.php",

View File

@ -42,7 +42,6 @@ class SpanContextTest extends TestCase
/**
* @group trace-compliance
* @covers ::isValid
*/
public function test_valid_span(): void
{
@ -52,7 +51,6 @@ class SpanContextTest extends TestCase
/**
* @group trace-compliance
* @covers ::isRemote
*/
public function test_context_is_remote_from_restore(): void
{

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace OpenTelemetry\Example\Unit\API\Baggage\Propagation;
namespace OpenTelemetry\Tests\Unit\API\Baggage\Propagation;
use OpenTelemetry\API\Baggage\BaggageBuilderInterface;
use OpenTelemetry\API\Baggage\Metadata;

View File

@ -7,6 +7,8 @@ namespace OpenTelemetry\Tests\Unit\API\Common\Instrumentation;
use OpenTelemetry\API\Common\Instrumentation\CachedInstrumentation;
use OpenTelemetry\API\Common\Instrumentation\Configurator;
use OpenTelemetry\API\Common\Instrumentation\Globals;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\API\Logs\NoopLoggerProvider;
use OpenTelemetry\API\Metrics\MeterInterface;
use OpenTelemetry\API\Metrics\MeterProviderInterface;
use OpenTelemetry\API\Metrics\Noop\NoopMeter;
@ -32,6 +34,7 @@ final class InstrumentationTest extends TestCase
$this->assertInstanceOf(NoopTracerProvider::class, Globals::tracerProvider());
$this->assertInstanceOf(NoopMeterProvider::class, Globals::meterProvider());
$this->assertInstanceOf(NoopTextMapPropagator::class, Globals::propagator());
$this->assertInstanceOf(NoopLoggerProvider::class, Globals::loggerProvider());
}
public function test_globals_returns_configured_instances(): void
@ -39,17 +42,20 @@ final class InstrumentationTest extends TestCase
$tracerProvider = $this->createMock(TracerProviderInterface::class);
$meterProvider = $this->createMock(MeterProviderInterface::class);
$propagator = $this->createMock(TextMapPropagatorInterface::class);
$loggerProvider = $this->createMock(LoggerProviderInterface::class);
$scope = Configurator::create()
->withTracerProvider($tracerProvider)
->withMeterProvider($meterProvider)
->withPropagator($propagator)
->withLoggerProvider($loggerProvider)
->activate();
try {
$this->assertSame($tracerProvider, Globals::tracerProvider());
$this->assertSame($meterProvider, Globals::meterProvider());
$this->assertSame($propagator, Globals::propagator());
$this->assertSame($loggerProvider, Globals::loggerProvider());
} finally {
$scope->detach();
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\API\Logs;
use OpenTelemetry\API\Logs\EventLogger;
use OpenTelemetry\API\Logs\LoggerInterface;
use OpenTelemetry\API\Logs\LogRecord;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\API\Logs\EventLogger
*/
class EventLoggerTest extends TestCase
{
public function test_log_event(): void
{
$logger = $this->createMock(LoggerInterface::class);
$domain = 'some.domain';
$logRecord = $this->createMock(LogRecord::class);
$eventLogger = new EventLogger($logger, $domain);
$logRecord->expects($this->once())->method('setAttributes');
$logger->expects($this->once())->method('logRecord')->with($this->equalTo($logRecord));
$eventLogger->logEvent('some.event', $logRecord);
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\API\Logs;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\Context\Context;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\API\Logs\LogRecord
*/
class LogRecordTest extends TestCase
{
/**
* @dataProvider settersProvider
*/
public function test_setters(string $method, string $propertyName, $value): void
{
$record = new LogRecord();
$record->{$method}($value);
$reflection = new \ReflectionClass($record);
$property = $reflection->getProperty($propertyName);
$property->setAccessible(true);
$this->assertSame($value, $property->getValue($record));
}
public static function settersProvider(): array
{
return [
['setBody', 'body', 'foo'],
['setAttributes', 'attributes', ['foo' => 'bar']],
['setSeverityNumber', 'severityNumber', 5],
['setSeverityText', 'severityText', 'info'],
['setObservedTimestamp', 'observedTimestamp', 999],
['setTimestamp', 'timestamp', 888],
['setContext', 'context', Context::getCurrent()],
];
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\API\Logs\Map;
use OpenTelemetry\API\Logs\Map\Psr3;
use PHPUnit\Framework\TestCase;
use Psr\Log\LogLevel;
/**
* @covers \OpenTelemetry\API\Logs\Map\Psr3
*/
class Psr3Test extends TestCase
{
/**
* @dataProvider levelProvider
*/
public function test_severity_number(string $level): void
{
$this->assertNotNull(Psr3::severityNumber($level));
}
public static function levelProvider(): array
{
return [
[LogLevel::EMERGENCY],
[LogLevel::ALERT],
[LogLevel::CRITICAL],
[LogLevel::ERROR],
[LogLevel::WARNING],
[LogLevel::NOTICE],
[LogLevel::INFO],
[LogLevel::DEBUG],
['unknown'],
];
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\API\Logs;
use OpenTelemetry\API\Logs\NoopLogger;
use OpenTelemetry\API\Logs\NoopLoggerProvider;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\API\Logs\NoopLoggerProvider
*/
class NoopLoggerProviderTest extends TestCase
{
public function test_provides_logger(): void
{
$logger = (new NoopLoggerProvider())->getLogger('foo');
$this->assertInstanceOf(NoopLogger::class, $logger);
}
public function test_get_instance(): void
{
$this->assertInstanceOf(NoopLoggerProvider::class, NoopLoggerProvider::getInstance());
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Example\Unit\API\Logs;
use OpenTelemetry\API\Logs\NoopLogger;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\API\Logs\NoopLogger
*/
class NoopLoggerTest extends TestCase
{
public function test_get_instance(): void
{
$this->assertInstanceOf(NoopLogger::class, NoopLogger::getInstance());
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\Contrib\Otlp;
use OpenTelemetry\API\Trace\SpanContext;
use OpenTelemetry\API\Trace\SpanInterface;
use OpenTelemetry\Context\Context;
use OpenTelemetry\Context\ContextKeys;
use OpenTelemetry\Contrib\Otlp\LogsConverter;
use OpenTelemetry\SDK\Logs\ReadableLogRecord;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\Contrib\Otlp\LogsConverter
*/
class LogsConverterTest extends TestCase
{
private const TRACE_ID_BASE16 = 'ff000000000000000000000000000041';
private const SPAN_ID_BASE16 = 'ff00000000000041';
private const FLAGS = 12;
/** @var ReadableLogRecord&MockObject $record */
private $record;
private LogsConverter $converter;
public function setUp(): void
{
$this->converter = new LogsConverter();
$this->record = $this->createMock(ReadableLogRecord::class);
}
public function test_convert(): void
{
$this->record->method('getBody')->willReturn('body');
$request = $this->converter->convert([$this->record]);
/** @psalm-suppress InvalidArgument */
$row = $request->getResourceLogs()[0]->getScopeLogs()[0]->getLogRecords()[0];
$this->assertSame('body', $row->getBody()->getStringValue());
}
public function test_convert_with_context(): void
{
$spanContext = SpanContext::create(self::TRACE_ID_BASE16, self::SPAN_ID_BASE16, self::FLAGS);
$span = $this->createMock(SpanInterface::class);
$context = Context::getCurrent()->with(ContextKeys::span(), $span);
$span->method('getContext')->willReturn($spanContext);
$this->record->method('getSpanContext')->willReturn($spanContext);
$request = $this->converter->convert([$this->record]);
/** @psalm-suppress InvalidArgument */
$row = $request->getResourceLogs()[0]->getScopeLogs()[0]->getLogRecords()[0];
$this->assertSame(self::TRACE_ID_BASE16, bin2hex($row->getTraceId()));
$this->assertSame(self::SPAN_ID_BASE16, bin2hex($row->getSpanId()));
$this->assertSame(self::FLAGS, $row->getFlags());
}
}

View File

@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\Contrib\Otlp;
use AssertWell\PHPUnitGlobalState\EnvironmentVariables;
use OpenTelemetry\Contrib\Otlp\LogsExporterFactory;
use OpenTelemetry\SDK\Common\Configuration\KnownValues;
use OpenTelemetry\SDK\Common\Configuration\Variables;
use OpenTelemetry\SDK\Common\Export\TransportFactoryInterface;
use OpenTelemetry\SDK\Common\Export\TransportInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use RuntimeException;
/**
* @covers \OpenTelemetry\Contrib\Otlp\LogsExporterFactory
*/
class LogsExporterFactoryTest extends TestCase
{
use EnvironmentVariables;
/** @var TransportFactoryInterface&MockObject $record */
private TransportFactoryInterface $transportFactory;
/** @var TransportInterface&MockObject $record */
private TransportInterface $transport;
public function setUp(): void
{
$this->transportFactory = $this->createMock(TransportFactoryInterface::class);
$this->transport = $this->createMock(TransportInterface::class);
}
public function tearDown(): void
{
$this->restoreEnvironmentVariables();
}
public function test_unknown_protocol_exception(): void
{
$this->expectException(RuntimeException::class);
$this->setEnvironmentVariable(Variables::OTEL_EXPORTER_OTLP_PROTOCOL, 'foo');
$factory = new LogsExporterFactory();
$factory->create();
}
/**
* @dataProvider configProvider
*/
public function test_create(array $env, string $endpoint, string $protocol, string $compression, array $headerKeys = []): void
{
foreach ($env as $k => $v) {
$this->setEnvironmentVariable($k, $v);
}
$factory = new LogsExporterFactory($this->transportFactory);
// @phpstan-ignore-next-line
$this->transportFactory
->expects($this->once())
->method('create')
->with(
$this->equalTo($endpoint),
$this->equalTo($protocol),
$this->callback(function ($headers) use ($headerKeys) {
$this->assertEqualsCanonicalizing($headerKeys, array_keys($headers));
return true;
}),
$this->equalTo($compression)
)
->willReturn($this->transport);
// @phpstan-ignore-next-line
$this->transport->method('contentType')->willReturn($protocol);
$factory->create();
}
public function configProvider(): array
{
$defaultHeaderKeys = ['User-Agent'];
return [
'signal-specific endpoint unchanged' => [
'env' => [
Variables::OTEL_EXPORTER_OTLP_LOGS_PROTOCOL => KnownValues::VALUE_GRPC,
Variables::OTEL_EXPORTER_OTLP_LOGS_ENDPOINT => 'http://collector:4317/foo/bar', //should not be changed, per spec
],
'endpoint' => 'http://collector:4317/foo/bar',
'protocol' => 'application/x-protobuf',
'compression' => 'none',
'headerKeys' => $defaultHeaderKeys,
],
'endpoint has path appended' => [
'env' => [
Variables::OTEL_EXPORTER_OTLP_LOGS_PROTOCOL => KnownValues::VALUE_GRPC,
Variables::OTEL_EXPORTER_OTLP_ENDPOINT => 'http://collector:4317',
],
'endpoint' => 'http://collector:4317/opentelemetry.proto.collector.logs.v1.LogsService/Export',
'protocol' => 'application/x-protobuf',
'compression' => 'none',
'headerKeys' => $defaultHeaderKeys,
],
'protocol' => [
'env' => [
Variables::OTEL_EXPORTER_OTLP_PROTOCOL => KnownValues::VALUE_HTTP_NDJSON,
],
'endpoint' => 'http://localhost:4318/v1/logs',
'protocol' => 'application/x-ndjson',
'compression' => 'none',
'headerKeys' => $defaultHeaderKeys,
],
'signal-specific protocol' => [
'env' => [
Variables::OTEL_EXPORTER_OTLP_PROTOCOL => KnownValues::VALUE_HTTP_JSON,
],
'endpoint' => 'http://localhost:4318/v1/logs',
'protocol' => 'application/json',
'compression' => 'none',
'headerKeys' => $defaultHeaderKeys,
],
'defaults' => [
'env' => [],
'endpoint' => 'http://localhost:4318/v1/logs',
'protocol' => 'application/x-protobuf',
'compression' => 'none',
'headerKeys' => $defaultHeaderKeys,
],
'compression' => [
'env' => [
Variables::OTEL_EXPORTER_OTLP_COMPRESSION => 'gzip',
],
'endpoint' => 'http://localhost:4318/v1/logs',
'protocol' => 'application/x-protobuf',
'compression' => 'gzip',
'headerKeys' => $defaultHeaderKeys,
],
'signal-specific compression' => [
'env' => [
Variables::OTEL_EXPORTER_OTLP_LOGS_COMPRESSION => 'gzip',
],
'endpoint' => 'http://localhost:4318/v1/logs',
'protocol' => 'application/x-protobuf',
'compression' => 'gzip',
'headerKeys' => $defaultHeaderKeys,
],
'headers' => [
'env' => [
Variables::OTEL_EXPORTER_OTLP_HEADERS => 'key1=foo,key2=bar',
],
'endpoint' => 'http://localhost:4318/v1/logs',
'protocol' => 'application/x-protobuf',
'compression' => 'none',
'headerKeys' => array_merge($defaultHeaderKeys, ['key1', 'key2']),
],
'signal-specific headers' => [
'env' => [
Variables::OTEL_EXPORTER_OTLP_LOGS_HEADERS => 'key3=foo,key4=bar',
],
'endpoint' => 'http://localhost:4318/v1/logs',
'protocol' => 'application/x-protobuf',
'compression' => 'none',
'headerKeys' => array_merge($defaultHeaderKeys, ['key3', 'key4']),
],
];
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\Contrib\Otlp;
use OpenTelemetry\Contrib\Otlp\LogsExporter;
use OpenTelemetry\SDK\Common\Export\TransportInterface;
use OpenTelemetry\SDK\Common\Future\CompletedFuture;
use OpenTelemetry\SDK\Common\Future\ErrorFuture;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\Contrib\Otlp\LogsExporter
*/
class LogsExporterTest extends TestCase
{
private MockObject $transport;
private LogsExporter $exporter;
public function setUp(): void
{
$this->transport = $this->createMock(TransportInterface::class);
$this->transport->method('contentType')->willReturn('application/x-protobuf');
$this->exporter = new LogsExporter($this->transport);
}
public function test_export_with_transport_failure(): void
{
$future = new ErrorFuture(new \Exception('foo'));
$this->transport->method('send')->willReturn($future);
$result = $this->exporter->export([]);
$this->assertFalse($result->await());
}
public function test_export_with_invalid_response(): void
{
$future = new CompletedFuture('invalid.grpc.payload');
$this->transport->method('send')->willReturn($future);
$result = $this->exporter->export([]);
$this->assertFalse($result->await());
}
public function test_export_success(): void
{
$future = new CompletedFuture('');
$this->transport->method('send')->willReturn($future);
$result = $this->exporter->export([]);
$this->assertTrue($result->await());
}
public function test_shutdown(): void
{
$this->transport->expects($this->once())->method('shutdown');
$this->exporter->shutdown();
}
public function test_force_flush(): void
{
$this->transport->expects($this->once())->method('forceFlush');
$this->exporter->forceFlush();
}
}

View File

@ -26,7 +26,7 @@ use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\Contrib\Otlp\SpanConverter
*/
class OTLPSpanConverterTest extends TestCase
class SpanConverterTest extends TestCase
{
public function test_convert_span_to_payload(): void
{

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace OpenTelemetry\Example\Unit\SDK\Common\Configuration\Resolver;
namespace OpenTelemetry\Tests\Unit\SDK\Common\Configuration\Resolver;
use OpenTelemetry\SDK\Common\Configuration\Resolver\PhpIniAccessor;
use OpenTelemetry\SDK\Common\Configuration\Resolver\PhpIniResolver;

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace OpenTelemetry\Example\Unit\SDK\Common\Export\Stream;
namespace OpenTelemetry\Tests\Unit\SDK\Common\Export\Stream;
use OpenTelemetry\SDK\Common\Export\Stream\StreamTransportFactory;
use PHPUnit\Framework\TestCase;

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace OpenTelemetry\Example\Unit\SDK;
namespace OpenTelemetry\Tests\Unit\SDK;
use OpenTelemetry\SDK\Common\Export\TransportFactoryInterface;
use OpenTelemetry\SDK\Metrics\MetricExporterFactoryInterface;

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs\Exporter;
use OpenTelemetry\SDK\Logs\Exporter\ConsoleExporter;
use OpenTelemetry\SDK\Logs\Exporter\ConsoleExporterFactory;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\Exporter\ConsoleExporterFactory
*/
class ConsoleExporterFactoryTest extends TestCase
{
public function test_create(): void
{
$factory = new ConsoleExporterFactory();
$this->assertInstanceOf(ConsoleExporter::class, $factory->create());
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs\Exporter;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\SDK\Common\Export\TransportInterface;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface;
use OpenTelemetry\SDK\Logs\Exporter\ConsoleExporter;
use OpenTelemetry\SDK\Logs\LoggerSharedState;
use OpenTelemetry\SDK\Logs\ReadableLogRecord;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\Exporter\ConsoleExporter
* @psalm-suppress UndefinedInterfaceMethod
*/
class ConsoleExporterTest extends TestCase
{
private TransportInterface $transport;
private ConsoleExporter $exporter;
public function setUp(): void
{
$this->transport = $this->createMock(TransportInterface::class);
$this->exporter = new ConsoleExporter($this->transport);
}
public function test_export(): void
{
$batch = [
(new ReadableLogRecord(
$this->createMock(InstrumentationScopeInterface::class),
$this->createMock(LoggerSharedState::class),
(new LogRecord('foo')),
true,
)),
];
$this->transport->expects($this->once())->method('send');
$this->exporter->export($batch);
}
public function test_force_flush(): void
{
$this->assertTrue($this->exporter->forceFlush());
}
public function test_shutdown(): void
{
$this->assertTrue($this->exporter->shutdown());
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs\Exporter;
use OpenTelemetry\SDK\Logs\Exporter\NoopExporter;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\Exporter\NoopExporter
*/
class NoopExporterTest extends TestCase
{
private NoopExporter $exporter;
public function setUp(): void
{
$this->exporter = new NoopExporter();
}
public function test_export(): void
{
$this->assertTrue($this->exporter->export([])->await());
}
public function test_force_flush(): void
{
$this->assertTrue($this->exporter->forceFlush());
}
public function test_shutdown(): void
{
$this->assertTrue($this->exporter->shutdown());
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs;
use AssertWell\PHPUnitGlobalState\EnvironmentVariables;
use OpenTelemetry\SDK\Logs\Exporter\ConsoleExporter;
use OpenTelemetry\SDK\Logs\Exporter\NoopExporter;
use OpenTelemetry\SDK\Logs\ExporterFactory;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\ExporterFactory
*/
class ExporterFactoryTest extends TestCase
{
use EnvironmentVariables;
public function tearDown(): void
{
$this->restoreEnvironmentVariables();
}
/**
* @dataProvider exporterProvider
*/
public function test_create($name, $expected): void
{
$this->setEnvironmentVariable('OTEL_LOGS_EXPORTER', $name);
$exporter = (new ExporterFactory())->create();
$this->assertInstanceOf($expected, $exporter);
}
public static function exporterProvider(): array
{
return [
['console', ConsoleExporter::class],
['none', NoopExporter::class],
];
}
public function test_rejects_multiple(): void
{
$this->setEnvironmentVariable('OTEL_LOGS_EXPORTER', 'one,two');
$this->expectException(\InvalidArgumentException::class);
(new ExporterFactory())->create();
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs;
use OpenTelemetry\SDK\Logs\LogRecordLimitsBuilder;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\LogRecordLimitsBuilder
* @covers \OpenTelemetry\SDK\Logs\LogRecordLimits
*/
class LogRecordLimitsBuilderTest extends TestCase
{
public function test_builder(): void
{
$limits = (new LogRecordLimitsBuilder())
->setAttributeCountLimit(2)
->setAttributeValueLengthLimit(5)
->build();
$attributes = $limits->getAttributeFactory()->builder([
'foo' => 'bar', //allowed, <5 chars
'long' => 'long-attribute-value', //trimmed, >5 chars
'bar' => 'baz', //dropped, exceeds count
])->build();
$this->assertSame(1, $attributes->getDroppedAttributesCount());
$this->assertCount(2, $attributes);
$this->assertSame('long-', $attributes->get('long'));
$this->assertSame('bar', $attributes->get('foo'));
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs;
use AssertWell\PHPUnitGlobalState\EnvironmentVariables;
use OpenTelemetry\SDK\Logs\LogRecordExporterInterface;
use OpenTelemetry\SDK\Logs\LogRecordProcessorFactory;
use OpenTelemetry\SDK\Logs\Processor\BatchLogsProcessor;
use OpenTelemetry\SDK\Logs\Processor\NoopLogsProcessor;
use OpenTelemetry\SDK\Logs\Processor\SimpleLogsProcessor;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\LogRecordProcessorFactory
*/
class LogRecordProcessorFactoryTest extends TestCase
{
use EnvironmentVariables;
public function tearDown(): void
{
$this->restoreEnvironmentVariables();
}
/**
* @dataProvider exporterProvider
*/
public function test_create($name, $expected): void
{
$exporter = $this->createMock(LogRecordExporterInterface::class);
$this->setEnvironmentVariable('OTEL_PHP_LOGS_PROCESSOR', $name);
$processor = (new LogRecordProcessorFactory())->create($exporter);
$this->assertInstanceOf($expected, $processor);
}
public static function exporterProvider(): array
{
return [
['batch', BatchLogsProcessor::class],
['simple', SimpleLogsProcessor::class],
['noop', NoopLogsProcessor::class],
['none', NoopLogsProcessor::class],
];
}
public function test_create_invalid(): void
{
$this->setEnvironmentVariable('OTEL_PHP_LOGS_PROCESSOR', 'baz');
$this->expectException(\InvalidArgumentException::class);
(new LogRecordProcessorFactory())->create($this->createMock(LogRecordExporterInterface::class));
}
public function test_rejects_multiple(): void
{
$this->setEnvironmentVariable('OTEL_PHP_LOGS_PROCESSOR', 'one,two');
$this->expectException(\InvalidArgumentException::class);
(new LogRecordProcessorFactory())->create($this->createMock(LogRecordExporterInterface::class));
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs;
use OpenTelemetry\API\Logs\LoggerProviderInterface;
use OpenTelemetry\SDK\Logs\LoggerProviderFactory;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\LoggerProviderFactory
*/
class LoggerProviderFactoryTest extends TestCase
{
public function test_create(): void
{
$factory = new LoggerProviderFactory();
$this->assertInstanceOf(LoggerProviderInterface::class, $factory->create());
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs;
use OpenTelemetry\API\Logs\NoopLogger;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeFactoryInterface;
use OpenTelemetry\SDK\Logs\Logger;
use OpenTelemetry\SDK\Logs\LoggerProvider;
use OpenTelemetry\SDK\Logs\LogRecordProcessorInterface;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\LoggerProvider
* @psalm-suppress UndefinedInterfaceMethod
* @psalm-suppress PossiblyUndefinedMethod
*/
class LoggerProviderTest extends TestCase
{
/**
* @var LogRecordProcessorInterface|(LogRecordProcessorInterface&\PHPUnit\Framework\MockObject\MockObject)|\PHPUnit\Framework\MockObject\MockObject
*/
private LogRecordProcessorInterface $processor;
private InstrumentationScopeFactoryInterface $instrumentationScopeFactory;
private LoggerProvider $provider;
public function setUp(): void
{
$this->processor = $this->createMock(LogRecordProcessorInterface::class);
$this->instrumentationScopeFactory = $this->createMock(InstrumentationScopeFactoryInterface::class);
$resource = $this->createMock(ResourceInfo::class);
$this->provider = new LoggerProvider($this->processor, $this->instrumentationScopeFactory, $resource);
}
public function test_get_logger(): void
{
$logger = $this->provider->getLogger('name');
$this->assertInstanceOf(Logger::class, $logger);
}
public function test_get_logger_after_shutdown(): void
{
$this->provider->shutdown();
$logger = $this->provider->getLogger('name');
$this->assertInstanceOf(NoopLogger::class, $logger);
}
public function test_shutdown_calls_processor_shutdown(): void
{
$this->processor->expects($this->once())->method('shutdown');
$this->provider->shutdown();
}
public function test_force_flush_calls_processor_force_flush(): void
{
$this->processor->expects($this->once())->method('forceFlush');
$this->provider->forceFlush();
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs;
use OpenTelemetry\SDK\Logs\LoggerSharedState;
use OpenTelemetry\SDK\Logs\LogRecordLimits;
use OpenTelemetry\SDK\Logs\LogRecordProcessorInterface;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\LoggerSharedState
* @psalm-suppress UndefinedInterfaceMethod
*/
class LoggerSharedStateTest extends TestCase
{
private ResourceInfo $resource;
private LogRecordProcessorInterface $processor;
private LogRecordLimits $limits;
private LoggerSharedState $loggerSharedState;
public function setUp(): void
{
$this->resource = $this->createMock(ResourceInfo::class);
$this->processor = $this->createMock(LogRecordProcessorInterface::class);
$this->limits = $this->createMock(LogRecordLimits::class);
$this->loggerSharedState = new LoggerSharedState(
$this->resource,
$this->limits,
$this->processor,
);
}
public function test_get_resource(): void
{
$this->assertSame($this->resource, $this->loggerSharedState->getResource());
}
public function test_get_processor(): void
{
$this->assertSame($this->processor, $this->loggerSharedState->getProcessor());
}
public function test_get_log_record_limits(): void
{
$this->assertSame($this->limits, $this->loggerSharedState->getLogRecordLimits());
}
public function test_shutdown(): void
{
$this->processor->expects($this->once())->method('shutdown')->willReturn(true);
$this->assertFalse($this->loggerSharedState->hasShutdown());
$this->loggerSharedState->shutdown();
$this->assertTrue($this->loggerSharedState->hasShutdown());
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScope;
use OpenTelemetry\SDK\Logs\Logger;
use OpenTelemetry\SDK\Logs\LoggerSharedState;
use OpenTelemetry\SDK\Logs\LogRecordProcessorInterface;
use OpenTelemetry\SDK\Logs\ReadWriteLogRecord;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\Logger
* @psalm-suppress UndefinedInterfaceMethod
*/
class LoggerTest extends TestCase
{
private LoggerSharedState $sharedState;
private LogRecordProcessorInterface $processor;
private InstrumentationScope $scope;
public function setUp(): void
{
$this->sharedState = $this->createMock(LoggerSharedState::class);
$this->processor = $this->createMock(LogRecordProcessorInterface::class);
$this->sharedState->method('getProcessor')->willReturn($this->processor);
$this->scope = new InstrumentationScope('foo', '1.0', 'schema.url', Attributes::create([])); //final
}
public function test_log_record(): void
{
$logger = new Logger($this->sharedState, $this->scope, true);
$record = (new LogRecord())->setContext($this->createMock(ContextInterface::class));
$this->processor->expects($this->once())->method('onEmit')
->with(
$this->isInstanceOf(ReadWriteLogRecord::class),
$this->isInstanceOf(ContextInterface::class)
);
$logger->logRecord($record);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs;
use OpenTelemetry\API\Logs\NoopLogger;
use OpenTelemetry\SDK\Logs\NoopLoggerProvider;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\NoopLoggerProvider
*/
class NoopLoggerProviderTest extends TestCase
{
public function test_get_instance(): void
{
$this->assertInstanceOf(NoopLoggerProvider::class, NoopLoggerProvider::getInstance());
}
public function test_get_logger(): void
{
$this->assertInstanceOf(NoopLogger::class, NoopLoggerProvider::getInstance()->getLogger('foo'));
}
public function test_shutdown(): void
{
$this->assertTrue(NoopLoggerProvider::getInstance()->shutdown());
}
public function test_force_flush(): void
{
$this->assertTrue(NoopLoggerProvider::getInstance()->forceFlush());
}
}

View File

@ -0,0 +1,493 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs\Processor;
use InvalidArgumentException;
use LogicException;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use OpenTelemetry\API\Common\Log\LoggerHolder;
use OpenTelemetry\Context\Context;
use OpenTelemetry\SDK\Common\Attribute\Attributes;
use OpenTelemetry\SDK\Common\Future\CompletedFuture;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeFactory;
use OpenTelemetry\SDK\Common\Time\ClockFactory;
use OpenTelemetry\SDK\Common\Time\ClockInterface;
use OpenTelemetry\SDK\Logs\LogRecordExporterInterface;
use OpenTelemetry\SDK\Logs\LogRecordProcessorInterface;
use OpenTelemetry\SDK\Logs\Processor\BatchLogsProcessor;
use OpenTelemetry\SDK\Logs\ReadWriteLogRecord;
use OpenTelemetry\SDK\Metrics\MeterProvider;
use OpenTelemetry\SDK\Metrics\MetricExporter\InMemoryExporter;
use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader;
use OpenTelemetry\SDK\Metrics\StalenessHandler\ImmediateStalenessHandlerFactory;
use OpenTelemetry\SDK\Metrics\View\CriteriaViewRegistry;
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
use OpenTelemetry\Tests\Unit\SDK\Util\TestClock;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\NullLogger;
/**
* @covers \OpenTelemetry\SDK\Logs\Processor\BatchLogsProcessor
*/
class BatchLogsProcessorTest extends MockeryTestCase
{
private TestClock $testClock;
protected function setUp(): void
{
LoggerHolder::set(new NullLogger());
$this->testClock = new TestClock();
ClockFactory::setDefault($this->testClock);
}
protected function tearDown(): void
{
ClockFactory::setDefault(null);
}
public function test_export_batch_size_met(): void
{
$batchSize = 3;
$queueSize = 5; // queue is larger than batch
$exportDelay = 3;
$logs = [];
$timeout = 3000;
for ($i = 0; $i < $batchSize; $i++) {
$logs[] = $this->createMock(ReadWriteLogRecord::class);
}
$exporter = $this->createMock(LogRecordExporterInterface::class);
$exporter->expects($this->atLeastOnce())->method('export');
$processor = new BatchLogsProcessor(
$exporter,
$this->testClock,
$queueSize,
$exportDelay,
$timeout,
$batchSize
);
foreach ($logs as $record) {
$processor->onEmit($record);
}
}
/**
* @dataProvider scheduledDelayProvider
*/
public function test_export_scheduled_delay(int $exportDelay, int $advanceByNano, bool $expectedFlush): void
{
$batchSize = 2;
$queueSize = 5;
$timeout = 3000;
$logs = [];
for ($i = 0; $i < $batchSize; $i++) {
$logs[] = $this->createMock(ReadWriteLogRecord::class);
}
$exporter = $this->createMock(LogRecordExporterInterface::class);
$exporter->expects($this->exactly($expectedFlush ? 1 : 0))->method('export');
$processor = new BatchLogsProcessor(
$exporter,
$this->testClock,
$queueSize,
$exportDelay,
$timeout,
$batchSize + 1
);
foreach ($logs as $i => $record) {
if (1 === $i) {
$this->testClock->advance($advanceByNano);
}
$processor->onEmit($record);
}
}
public function scheduledDelayProvider(): array
{
return [
'no clock advance' => [1000, 0, false],
'clock advance less than threshold' => [1000, 999 * ClockInterface::NANOS_PER_MILLISECOND, false],
'clock advance equals threshold' => [1000, 1000 * ClockInterface::NANOS_PER_MILLISECOND, false],
'clock advance exceeds threshold' => [1000, 1001 * ClockInterface::NANOS_PER_MILLISECOND, true],
];
}
public function test_export_delay_limit_reached_partially_filled_batch(): void
{
$batchSize = 4;
$queueSize = 5;
$exportDelay = 1;
$timeout = 3000;
$logs = [];
for ($i = 0; $i < $batchSize - 1; $i++) {
$logs[] = $this->createMock(ReadWriteLogRecord::class);
}
$exporter = Mockery::mock(LogRecordExporterInterface::class);
$exporter
->expects('export')
->with(
Mockery::on(
function (array $records) {
$this->assertCount(3, $records);
$this->assertInstanceOf(ReadWriteLogRecord::class, $records[0]);
return true;
}
)
)
->andReturn(new CompletedFuture(0));
$processor = new BatchLogsProcessor(
$exporter,
$this->testClock,
$queueSize,
$exportDelay,
$timeout,
$batchSize
);
foreach ($logs as $idx => $record) {
$processor->onEmit($record);
if (1 === $idx) {
// Advance the clock to force a timeout flush.
$this->testClock->advanceSeconds();
}
}
}
public function test_export_delay_limit_not_reached_partially_filled_batch(): void
{
$batchSize = 3;
$queueSize = 5;
$exportDelay = 2;
$timeout = 3000;
$exporter = $this->createMock(LogRecordExporterInterface::class);
$exporter->expects($this->never())->method('export');
$processor = new BatchLogsProcessor(
$exporter,
$this->testClock,
$queueSize,
$exportDelay,
$timeout,
$batchSize
);
for ($i = 0; $i < $batchSize - 1; $i++) {
$record = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($record);
}
}
public function test_export_includes_force_flush_on_shutdown(): void
{
$batchSize = 3;
$exporter = $this->createMock(LogRecordExporterInterface::class);
$exporter->expects($this->once())->method('export');
$exporter->expects($this->once())->method('shutdown');
$proc = new BatchLogsProcessor($exporter, $this->createMock(ClockInterface::class));
for ($i = 0; $i < $batchSize - 1; $i++) {
$record = $this->createMock(ReadWriteLogRecord::class);
$proc->onEmit($record);
}
$proc->shutdown();
}
public function test_export_after_shutdown(): void
{
$exporter = $this->createMock(LogRecordExporterInterface::class);
$exporter->expects($this->atLeastOnce())->method('shutdown');
$proc = new BatchLogsProcessor($exporter, $this->createMock(ClockInterface::class));
$proc->shutdown();
$record = $this->createMock(ReadWriteLogRecord::class);
$proc->onEmit($record, Context::getCurrent());
$proc->forceFlush();
$proc->shutdown();
}
public function test_force_flush(): void
{
$batchSize = 3;
$queueSize = 3;
$exportDelay = 2;
$timeout = 3000;
$exporter = Mockery::mock(LogRecordExporterInterface::class);
$exporter->expects('forceFlush');
$exporter
->expects('export')
->with(
Mockery::on(
function (array $records) {
$this->assertCount(2, $records);
$this->assertInstanceOf(ReadWriteLogRecord::class, $records[0]);
return true;
}
)
)
->andReturn(new CompletedFuture(0));
$processor = new BatchLogsProcessor(
$exporter,
$this->testClock,
$queueSize,
$exportDelay,
$timeout,
$batchSize
);
for ($i = 0; $i < $batchSize - 1; $i++) {
$record = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($record);
}
$processor->forceFlush();
}
public function test_queue_size_exceeded_drops_spans(): void
{
$exporter = $this->createMock(LogRecordExporterInterface::class);
$processor = new BatchLogsProcessor($exporter, $this->testClock, 5, 5000, 30000, 5);
$exporter->expects($this->exactly(2))->method('export')->willReturnCallback(function (iterable $batch) use ($processor, &$i) {
if ($i) {
$this->assertCount(3, $batch);
} else {
for ($i = 0; $i < 5; $i++) {
$span = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($span);
}
}
return 0;
});
$record = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($record);
$processor->onEmit($record);
$processor->forceFlush();
$processor->forceFlush();
}
public function test_force_flush_applies_only_to_current_logs(): void
{
$exporter = $this->createMock(LogRecordExporterInterface::class);
$processor = new BatchLogsProcessor($exporter, $this->testClock);
$exporter->expects($this->exactly(1))->method('export')->willReturnCallback(function (iterable $batch) use ($processor) {
$this->assertCount(1, $batch);
$record = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($record); //arrives after flush started, so not flushed yet
return 0;
});
$record = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($record);
$processor->forceFlush();
}
public function test_shutdown_shutdowns_exporter(): void
{
$exporter = $this->createMock(LogRecordExporterInterface::class);
$processor = new BatchLogsProcessor($exporter, $this->testClock);
$exporter->expects($this->once())->method('shutdown');
$processor->shutdown();
}
public function test_throwing_exporter_export(): void
{
$exporter = $this->createMock(LogRecordExporterInterface::class);
$exporter->method('forceFlush')->willReturn(true);
$exporter->method('export')->willThrowException(new LogicException());
$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())->method('log')->with(LogLevel::ERROR);
$processor = new BatchLogsProcessor($exporter, $this->testClock);
$record = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($record);
$previousLogger = LoggerHolder::get();
LoggerHolder::set($logger);
try {
$processor->forceFlush();
} finally {
LoggerHolder::set($previousLogger);
}
}
public function test_throwing_exporter_flush(): void
{
$exporter = $this->createMock(LogRecordExporterInterface::class);
$exporter->method('forceFlush')->willThrowException(new LogicException());
$this->expectException(LogicException::class);
$processor = new BatchLogsProcessor($exporter, $this->testClock);
$record = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($record);
$processor->forceFlush();
}
public function test_throwing_exporter_flush_cannot_rethrow_in_original_caller_logs_error(): void
{
$exporter = $this->createMock(LogRecordExporterInterface::class);
$exporter->method('forceFlush')->willReturnCallback(function () use (&$processor) {
/** @var LogRecordProcessorInterface $processor */
$record = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($record);
return $processor->shutdown();
});
$exporter->method('shutdown')->willThrowException(new LogicException());
$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())->method('log')->with(LogLevel::ERROR);
$processor = new BatchLogsProcessor($exporter, $this->testClock);
$record = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($record);
$previousLogger = LoggerHolder::get();
LoggerHolder::set($logger);
try {
$processor->forceFlush();
} finally {
LoggerHolder::set($previousLogger);
}
}
public function test_throwing_exporter_flush_rethrows_in_original_caller(): void
{
$exporter = $this->createMock(LogRecordExporterInterface::class);
$exporter->method('forceFlush')->willReturnCallback(function () use (&$processor) {
/** @var LogRecordProcessorInterface $processor */
$record = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($record);
$processor->shutdown();
throw new LogicException();
});
$exporter->expects($this->once())->method('shutdown');
$this->expectException(LogicException::class);
$processor = new BatchLogsProcessor($exporter, $this->testClock);
$record = $this->createMock(ReadWriteLogRecord::class);
$processor->onEmit($record);
$processor->forceFlush();
}
/**
* @requires PHP >= 8.0
*/
public function test_self_diagnostics(): void
{
$clock = new TestClock();
$metrics = new InMemoryExporter();
$reader = new ExportingReader($metrics, $clock);
$meterProvider = new MeterProvider(
null,
ResourceInfoFactory::emptyResource(),
$clock,
Attributes::factory(),
new InstrumentationScopeFactory(Attributes::factory()),
[$reader],
new CriteriaViewRegistry(),
null,
new ImmediateStalenessHandlerFactory(),
);
$exporter = $this->createMock(LogRecordExporterInterface::class);
$processor = new BatchLogsProcessor(
$exporter,
ClockFactory::getDefault(),
2048,
5000,
30000,
512,
false,
$meterProvider,
);
$reader->collect();
$this->assertEquals(
[
'otel.logs.log_processor.logs',
'otel.logs.log_processor.queue.limit',
'otel.logs.log_processor.queue.usage',
],
array_column($metrics->collect(), 'name'),
);
}
public function test_logs_processor_throws_on_invalid_max_queue_size(): void
{
$this->expectException(InvalidArgumentException::class);
$exporter = $this->createMock(LogRecordExporterInterface::class);
new BatchLogsProcessor($exporter, $this->testClock, -1);
}
public function test_logs_processor_throws_on_invalid_scheduled_delay(): void
{
$this->expectException(InvalidArgumentException::class);
$exporter = $this->createMock(LogRecordExporterInterface::class);
new BatchLogsProcessor($exporter, $this->testClock, 2048, -1);
}
public function test_logs_processor_throws_on_invalid_export_timeout(): void
{
$this->expectException(InvalidArgumentException::class);
$exporter = $this->createMock(LogRecordExporterInterface::class);
new BatchLogsProcessor($exporter, $this->testClock, 2048, 5000, -1);
}
public function test_logs_processor_throws_on_invalid_max_export_batch_size(): void
{
$this->expectException(InvalidArgumentException::class);
$exporter = $this->createMock(LogRecordExporterInterface::class);
new BatchLogsProcessor($exporter, $this->testClock, 2048, 5000, 30000, -1);
}
public function test_logs_processor_throws_on_invalid_max_export_batch_size_exceeding_max_queue_size(): void
{
$this->expectException(InvalidArgumentException::class);
$exporter = $this->createMock(LogRecordExporterInterface::class);
new BatchLogsProcessor($exporter, $this->testClock, 2, 5000, 30000, 3);
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs\Processor;
use OpenTelemetry\SDK\Logs\LogRecordProcessorInterface;
use OpenTelemetry\SDK\Logs\Processor\NoopLogsProcessor;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\Processor\NoopLogsProcessor
*/
class NoopLogsProcessorTest extends TestCase
{
public function test_get_instance(): void
{
$this->assertInstanceOf(LogRecordProcessorInterface::class, NoopLogsProcessor::getInstance());
}
public function test_shutdown(): void
{
$this->assertTrue(NoopLogsProcessor::getInstance()->shutdown());
}
public function test_force_flush(): void
{
$this->assertTrue(NoopLogsProcessor::getInstance()->forceFlush());
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs\Processor;
use OpenTelemetry\SDK\Logs\LogRecordExporterInterface;
use OpenTelemetry\SDK\Logs\Processor\SimpleLogsProcessor;
use OpenTelemetry\SDK\Logs\ReadWriteLogRecord;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\Processor\SimpleLogsProcessor
* @psalm-suppress UndefinedInterfaceMethod
* @psalm-suppress PossiblyUndefinedMethod
*/
class SimpleLogsProcessorTest extends TestCase
{
private SimpleLogsProcessor $processor;
/**
* @var LogRecordExporterInterface|(LogRecordExporterInterface&\PHPUnit\Framework\MockObject\MockObject)|\PHPUnit\Framework\MockObject\MockObject
*/
private LogRecordExporterInterface $exporter;
private ReadWriteLogRecord $readWriteLogRecord;
public function setUp(): void
{
$this->exporter = $this->createMock(LogRecordExporterInterface::class);
$this->readWriteLogRecord = $this->createMock(ReadWriteLogRecord::class);
$this->processor = new SimpleLogsProcessor($this->exporter);
}
public function test_on_emit(): void
{
$this->exporter->expects($this->once())->method('export')->with($this->equalTo([$this->readWriteLogRecord]));
$this->processor->onEmit($this->readWriteLogRecord);
}
public function test_shutdown(): void
{
$this->exporter->expects($this->once())->method('shutdown');
$this->processor->shutdown();
}
public function test_force_flush(): void
{
$this->exporter->expects($this->once())->method('forceFlush');
$this->processor->forceFlush();
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace OpenTelemetry\Tests\Unit\SDK\Logs;
use OpenTelemetry\API\Logs\LogRecord;
use OpenTelemetry\Context\ContextInterface;
use OpenTelemetry\SDK\Common\Attribute\AttributesFactory;
use OpenTelemetry\SDK\Common\Instrumentation\InstrumentationScopeInterface;
use OpenTelemetry\SDK\Logs\LoggerSharedState;
use OpenTelemetry\SDK\Logs\LogRecordLimits;
use OpenTelemetry\SDK\Logs\ReadableLogRecord;
use OpenTelemetry\SDK\Resource\ResourceInfo;
use PHPUnit\Framework\TestCase;
/**
* @covers \OpenTelemetry\SDK\Logs\ReadableLogRecord
*/
class ReadableLogRecordTest extends TestCase
{
public function test_getters(): void
{
$scope = $this->createMock(InstrumentationScopeInterface::class);
$sharedState = $this->createMock(LoggerSharedState::class);
$context = $this->createMock(ContextInterface::class);
$resource = $this->createMock(ResourceInfo::class);
$limits = $this->createMock(LogRecordLimits::class);
$attributeFactory = new AttributesFactory();
$limits->method('getAttributeFactory')->willReturn($attributeFactory); //final
$sharedState->method('getResource')->willReturn($resource);
$sharedState->method('getLogRecordLimits')->willReturn($limits);
$logRecord = (new LogRecord('body'))
->setSeverityNumber(5)
->setSeverityText('info')
->setTimestamp(11)
->setObservedTimestamp(22)
->setAttributes(['foo' => 'bar'])
->setContext($context);
$record = new ReadableLogRecord($scope, $sharedState, $logRecord, true);
$this->assertSame($scope, $record->getInstrumentationScope());
$this->assertSame($resource, $record->getResource());
$this->assertSame(11, $record->getTimestamp());
$this->assertSame(22, $record->getObservedTimestamp());
$this->assertSame($context, $record->getContext());
$this->assertSame(5, $record->getSeverityNumber());
$this->assertSame('info', $record->getSeverityText());
$this->assertSame('body', $record->getBody());
$this->assertEquals(['foo' => 'bar'], $record->getAttributes()->toArray());
}
}

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace OpenTelemetry\Example\Unit\SDK\Metrics;
namespace OpenTelemetry\Tests\Unit\SDK\Metrics;
use AssertWell\PHPUnitGlobalState\EnvironmentVariables;
use OpenTelemetry\API\Common\Log\LoggerHolder;

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace OpenTelemetry\Example\Unit\SDK\Resource\Detectors;
namespace OpenTelemetry\Tests\Unit\SDK\Resource\Detectors;
use OpenTelemetry\SDK\Resource\Detectors\Container;
use OpenTelemetry\SemConv\ResourceAttributes;

View File

@ -7,6 +7,7 @@ namespace OpenTelemetry\Tests\Unit\SDK;
use AssertWell\PHPUnitGlobalState\EnvironmentVariables;
use OpenTelemetry\API\Common\Instrumentation\Globals;
use OpenTelemetry\API\Common\Log\LoggerHolder;
use OpenTelemetry\API\Logs\NoopLoggerProvider;
use OpenTelemetry\API\Metrics\Noop\NoopMeterProvider;
use OpenTelemetry\API\Trace\NoopTracerProvider;
use OpenTelemetry\Context\Propagation\NoopTextMapPropagator;
@ -25,12 +26,12 @@ class SdkAutoloaderTest extends TestCase
public function setUp(): void
{
LoggerHolder::set(new NullLogger());
Globals::reset();
SdkAutoloader::reset();
}
public function tearDown(): void
{
SdkAutoloader::shutdown();
Globals::reset();
$this->restoreEnvironmentVariables();
}
@ -42,13 +43,10 @@ class SdkAutoloaderTest extends TestCase
$this->assertInstanceOf(NoopTextMapPropagator::class, Globals::propagator(), 'propagator not initialized by disabled autoloader');
}
public function test_enabled_by_configuration(): void
public function test_disabled_with_invalid_flag(): void
{
$this->setEnvironmentVariable(Variables::OTEL_PHP_AUTOLOAD_ENABLED, 'true');
SdkAutoloader::autoload();
$this->assertNotInstanceOf(NoopTextMapPropagator::class, Globals::propagator());
$this->assertNotInstanceOf(NoopMeterProvider::class, Globals::meterProvider());
$this->assertNotInstanceOf(NoopTracerProvider::class, Globals::tracerProvider());
$this->setEnvironmentVariable(Variables::OTEL_PHP_AUTOLOAD_ENABLED, 'invalid-value');
$this->assertFalse(SdkAutoloader::autoload());
}
public function test_sdk_disabled_does_not_disable_propagator(): void
@ -61,9 +59,13 @@ class SdkAutoloaderTest extends TestCase
$this->assertInstanceOf(NoopTracerProvider::class, Globals::tracerProvider());
}
public function test_disabled_with_invalid_flag(): void
public function test_enabled_by_configuration(): void
{
$this->setEnvironmentVariable(Variables::OTEL_PHP_AUTOLOAD_ENABLED, 'invalid-value');
$this->assertFalse(SdkAutoloader::autoload());
$this->setEnvironmentVariable(Variables::OTEL_PHP_AUTOLOAD_ENABLED, 'true');
SdkAutoloader::autoload();
$this->assertNotInstanceOf(NoopTextMapPropagator::class, Globals::propagator());
$this->assertNotInstanceOf(NoopMeterProvider::class, Globals::meterProvider());
$this->assertNotInstanceOf(NoopTracerProvider::class, Globals::tracerProvider());
$this->assertNotInstanceOf(NoopLoggerProvider::class, Globals::loggerProvider());
}
}