PHPackages                             timewave/logger - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Logging &amp; Monitoring](/categories/logging)
4. /
5. timewave/logger

ActiveLibrary[Logging &amp; Monitoring](/categories/logging)

timewave/logger
===============

Logging library

v0.6.0(4d ago)013.5k↑32.2%[2 PRs](https://github.com/Timewave-AB/package-logger/pulls)MITPHPPHP ^7.4 || ^8.0CI passing

Since Oct 11Pushed 3d ago1 watchersCompare

[ Source](https://github.com/Timewave-AB/package-logger)[ Packagist](https://packagist.org/packages/timewave/logger)[ RSS](/packages/timewave-logger/feed)WikiDiscussions main Synced yesterday

READMEChangelog (4)Dependencies (3)Versions (16)Used By (0)

Timewave\\Logger
================

[](#timewavelogger)

Custom logger package for PHP applications with opinionated log levels.

Usage
-----

[](#usage)

There will always be output to `stdout`. If Open Telemetry is configured, it will be pushed there to.

### Basic usage:

[](#basic-usage)

```
use Timewave\Logger\Classes\Logger;
use Timewave\Logger\Classes\OtlpSender;

$log = new Logger(
    'my-app-name',
    'debug',
    'text',
    "\t",
    new OtlpSender('http://localhost:4318')
);
$log->info('Something happened', ['key' => 'value']);
```

If you don't pass an `OtlpSender`, the logger only writes to stdout — OTLP is opt-in by construction. Wiring is constructor-only; the sender cannot be swapped on a live logger.

### Usage with spans

[](#usage-with-spans)

```
$log = new Logger('auth-4', 'debug', 'text', "\t", new OtlpSender('http://localhost:4318'));

$requestSpanLog = $log->createSpanLogger('request', ['requestId' => 'Legodalf']);

$requestSpanLog->verbose('Incoming request', ['method' => 'POST', 'path' => '/auth/password']);

$loginSpanLog = $requestSpanLog->createSpanLogger('login', ['username' => 'siv']);

$loginSpanLog->info('User is trying to login');
$userId = User::login('siv');
$loginSpanLog->verbose('User is logged in');

// The underlying Span is reachable for callers that need the id or trace id
// (e.g. to set on a response header):
$traceId = $loginSpanLog->getSpan()->traceId;

$loginSpanLog->endSpan();

$requestSpanLog->debug('Request is over');

$requestSpanLog->endSpan();
```

### Linking to an incoming trace (`traceparent`)

[](#linking-to-an-incoming-trace-traceparent)

When a reverse-proxy (e.g. nginx `ngx_otel_module` with `otel_trace_context propagate`) injects a W3C `traceparent` header, root your request span on it so PHP spans join the proxy's trace instead of starting a detached one:

```
$requestSpanLog = $log->createSpanLoggerFromTraceparent(
    'request',
    $_SERVER['HTTP_TRACEPARENT'] ?? null,
    ['requestId' => 'Legodalf']
);
```

The header (`version-traceId-spanId-flags`) is parsed and validated (32-hex trace-id, 16-hex span-id, both non-zero). On a valid header the span adopts the incoming trace-id and uses the proxy's span-id as its parent. A missing or malformed header falls back to a fresh trace without throwing. Nested `createSpanLogger` calls inherit the parent's trace-id, so the whole request shares one trace.

Log levels
----------

[](#log-levels)

- `error`: Unrecoverable error. Something is so broken the execution of the application can not continue.
- `warning`: Something is wrong, but the application can keep running. Must be addressed.
- `info`: All is well, but this message is important.
- `verbose`: Extra info, likely good in a production environment that is misbehaving.
- `debug`: A lot of detailed logs to debug your application \[default\]. Do not use in production.

Log formats
-----------

[](#log-formats)

- `json`: Outputs a string of a JSON object
- `text`: Outputs a simple string \[default\]

Open Telemetry Collector endpoint
---------------------------------

[](#open-telemetry-collector-endpoint)

`OtlpSender` takes a DSN string in its constructor — e.g. `new OtlpSender('http://localhost:4318')`. The target must be an OTLP/HTTP endpoint. Payloads are sent JSON-encoded (`Content-Type: application/json`); most collectors (e.g. otelcol's HTTP receiver) accept this on `:4318` alongside the protobuf encoding.

**The library expects a low-latency OTLP collector — typically `otelcol` running on the same VM/pod as the application.** That local collector handles batching, retries, and outbound wire traffic. This assumption shapes the design choices below.

If OTLP is enabled, sender wiring is mandatory: construct one in your composition root and pass it into every `Logger` (and any directly-constructed `Span`) that needs OTLP. There is no library-level singleton or implicit lookup — sharing is the caller's responsibility, which keeps lifetimes explicit. One sender keeps one cURL handle, queue, and shutdown-hook entry, so reusing a single instance across the request keeps host resolution + TLS state warm and bounds resource use in long-running workers.

### OTLP sending is fire-and-forget

[](#otlp-sending-is-fire-and-forget)

Every call to `OtlpSender::http()` (and every `Span::end()` / log line emitted via `Logger`) appends the payload to an in-memory queue rather than blocking on the collector. The queue is drained either:

- automatically at process shutdown via a single process-wide `register_shutdown_function` hook, or
- explicitly by calling `OtlpSender::flushAll()` (drain every sender that has queued items) or `$sender->flush()` (drain one).

Practical consequences:

- **No call ever blocks the request path on OTLP I/O.** Even if the collector is slow or hung, `http()` returns immediately. The actual cURL POST happens during the flush at shutdown.
- **PHP-FPM**: call `OtlpSender::flushAll()` *before* `fastcgi_finish_request()` if you want OTLP delivered before the response goes out; otherwise the response ships first and the flush runs during worker idle time. `fastcgi_finish_request()` exists only in the FPM SAPI.
- **Queue cap**: the queue is capped at `OtlpSender::MAX_QUEUE_SIZE` (10 000) items per sender. If the collector is dead and the queue fills, new entries are dropped and one `OTLP ERROR: queue full…` line is written to stdout until the queue drains.
- **Hard process kill (SIGKILL, OOM-killer)**: the shutdown hook does not run, so in-flight items are lost. With a local collector this gap is small; if it matters to you, call `OtlpSender::flushAll()` at critical points.
- **Forgotten `end()`**: a `Span` that is destroyed without `end()` is invisible to the collector. The destructor writes one stderr warning per dropped span (`Span 'name' destroyed without end() — span not POSTed to OTLP`) so the omission is observable.

### OTLP stopwatch (per-call latency)

[](#otlp-stopwatch-per-call-latency)

Every `send()` measures its own latency, but only writes a record to stdout when the call took longer than `OtlpSender::STOPWATCH_THRESHOLD_MS` (200 ms). That gives you a production-safe signal for slow OTLP without flooding the log stream on every span.

When the threshold is exceeded the sender writes a JSON line:

```
{"level":"WARNING","name":"otlp_stopwatch","path":"/v1/traces","latencyMs":287,"thresholdMs":200}
```

For a healthy local collector this should be effectively silent; sustained stopwatch lines mean the local collector pipeline is misbehaving.

Local development
-----------------

[](#local-development)

Everything runs through Docker via `docker-compose.yml`; no host-side PHP or composer needed.

Install dependencies:

```
docker compose run --rm composer install
```

Run the test suite (PHP 7.4):

```
docker compose run --rm phpunit
```

Run against PHP 8.3 and 8.5 as well (the package supports `^7.4 || ^8.0`):

```
docker compose run --rm phpunit-8.3
docker compose run --rm phpunit-8.5
```

Ad-hoc PHP invocations (e.g. trying a snippet, running a single test file):

```
docker compose run --rm phpunit vendor/bin/phpunit --filter SpanOtlpTest
docker compose run --rm php php -r 'echo PHP_VERSION;'
```

All image tags are pinned to exact patch versions in `docker-compose.yml`; bump them deliberately rather than relying on rolling tags.

`composer.json` also pins `config.platform.php = "7.4"` so dependency resolution always targets the lowest supported PHP. Without that pin, running `composer install` under PHP 8 (as the `composer:2.9.8` image does) would pull dev deps that drop PHP 7.4 support — e.g. `doctrine/instantiator` ≥ 2.x uses PHP 8.3 typed-constant syntax and silently breaks the PHPUnit run on 7.4.

Register an autoloader, or explicitly require the PHP files in `src/`, to consume the library from another project.

Releasing
---------

[](#releasing)

The package is published on [Packagist](https://packagist.org/packages/timewave/logger) as `timewave/logger`. The version is taken from the git tag — there is no `version` field in `composer.json`.

To cut a release:

1. Add a changelog entry below for the new version.
2. Create a [GitHub release](https://github.com/Timewave-AB/package-logger/releases/new) with a semver tag `vX.Y.Z` (e.g. `v0.6.0`). On `0.x`, bump the minor for breaking changes and the patch otherwise.

A GitHub webhook notifies Packagist on every push, so the new version appears within a minute or two — no manual step.

The webhook is already configured; this is only documented in case it needs re-creating (repo → Settings → Webhooks): payload URL `https://packagist.org/api/github?username=timewave`, content type `application/json`, secret = the `timewave` Packagist account's API token, subscribed to the `push` event.

Changelog
---------

[](#changelog)

### 0.6.0

[](#060)

- Declare `createSpanLoggerFromTraceparent()` on `LoggerInterface` so consumers type-hinted against the interface can call it.

### 0.5.0

[](#050)

- Link spans to an incoming W3C `traceparent` header via `Logger::createSpanLoggerFromTraceparent()` so PHP spans join the reverse-proxy's trace instead of a detached one.
- `Span` accepts an optional `traceId`; nested `createSpanLogger` spans now inherit their parent's trace-id.

### 0.4.0

[](#040)

- Correct OTLP span timestamps to sub-second precision and add a per-call OTLP stopwatch (warns over a 200 ms threshold).
- OTLP sending is always fire-and-forget — the queue drains at shutdown or via `OtlpSender::flushAll()`.
- **Breaking:** dropped the OTLP singleton; an `OtlpSender` must be constructed and injected explicitly.
- **Breaking:** renamed `CustomLogger` to `Logger` and extracted `OtlpLogRecord`.

### 0.3.0

[](#030)

- PHP 7.4 compatibility (`^7.4 || ^8.0`) and a Docker-based test workflow.

### 0.2.2

[](#022)

- Fixed span-logger config propagation (log level/format were passed by the wrong enum member).

### 0.2.1

[](#021)

- Fixed missing `otlpHttpHost` on `Span`.

### 0.2.0

[](#020)

- Added OpenTelemetry (OTLP) logging.

### 0.1.0

[](#010)

- Initial release: stdout logger with opinionated log levels and `text`/`json` formats.

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance99

Actively maintained with recent releases

Popularity25

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor2

2 contributors hold 50%+ of commits

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~89 days

Recently: every ~149 days

Total

8

Last Release

4d ago

PHP version history (2 changes)0.1.0PHP ^8.0

0.3.0PHP ^7.4 || ^8.0

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/3755795?v=4)[timewave](/maintainers/timewave)[@timewave](https://github.com/timewave)

---

Top Contributors

[![martinsiden](https://avatars.githubusercontent.com/u/15065802?v=4)](https://github.com/martinsiden "martinsiden (9 commits)")[![lilleman-tw](https://avatars.githubusercontent.com/u/142297505?v=4)](https://github.com/lilleman-tw "lilleman-tw (8 commits)")[![renovate[bot]](https://avatars.githubusercontent.com/in/2740?v=4)](https://github.com/renovate[bot] "renovate[bot] (7 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/timewave-logger/health.svg)

```
[![Health](https://phpackages.com/badges/timewave-logger/health.svg)](https://phpackages.com/packages/timewave-logger)
```

###  Alternatives

[psr/log

Common interface for logging libraries

10.4k1.2B11.5k](/packages/psr-log)[open-telemetry/api

API for OpenTelemetry PHP.

1941.5M276](/packages/open-telemetry-api)[open-telemetry/sdk

SDK for OpenTelemetry PHP.

2328.5M343](/packages/open-telemetry-sdk)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
