PHPackages                             thingston/psr3 - 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. thingston/psr3

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

thingston/psr3
==============

A lightweight, dependency-free PSR-3 logger with pluggable local and cloud adapters.

1.0.0(1mo ago)00MITPHPPHP ^8.4CI failing

Since Jul 8Pushed 1mo agoCompare

[ Source](https://github.com/thingston/psr3)[ Packagist](https://packagist.org/packages/thingston/psr3)[ RSS](/packages/thingston-psr3/feed)WikiDiscussions master Synced 1w ago

READMEChangelogDependencies (6)Versions (2)Used By (0)

Thingston PSR3
==============

[](#thingston-psr3)

A lightweight, dependency-free [PSR-3](https://www.php-fig.org/psr/psr-3/) logger for PHP 8.4+, with pluggable adapters for local backends (file, syslog, database) and cloud services (CloudWatch, Google Cloud Logging, Azure Monitor, Papertrail, Slack).

- **One hard dependency**: `psr/log`. Everything else (cloud SDKs, curl) is optional and lazily checked.
- **Composable by design**: fan a record out to multiple backends with `CompositeAdapter`, filter by severity with `MinLevelAdapter`, and swap `LineFormatter`/`JsonFormatter` per adapter.
- **100% test coverage**, PHPStan level 8, PSR-12 coding style.

Table of contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Quick start](#quick-start)
- [Core concepts](#core-concepts)
- [Formatters](#formatters)
- [Composing adapters](#composing-adapters)
    - [CompositeAdapter](#compositeadapter)
    - [MinLevelAdapter](#minleveladapter)
- [Local adapters](#local-adapters)
    - [NullAdapter](#nulladapter)
    - [StreamAdapter](#streamadapter)
    - [RotatingFileAdapter](#rotatingfileadapter)
    - [SyslogAdapter](#syslogadapter)
    - [PdoAdapter](#pdoadapter)
- [Cloud adapters](#cloud-adapters)
    - [CloudWatchLogsAdapter](#cloudwatchlogsadapter)
    - [GoogleCloudLoggingAdapter](#googlecloudloggingadapter)
    - [AzureMonitorAdapter](#azuremonitoradapter)
    - [PapertrailAdapter](#papertrailadapter)
    - [SlackWebhookAdapter](#slackwebhookadapter)
- [Writing your own adapter](#writing-your-own-adapter)
- [PSR-3 message interpolation](#psr-3-message-interpolation)
- [Development](#development)

Installation
------------

[](#installation)

```
composer require thingston/psr3
```

Cloud adapters that wrap a vendor SDK (CloudWatch, Google Cloud Logging) require you to separately install that SDK — see their sections below. Adapters that only need HTTP (Azure, Slack) or raw sockets (Papertrail) work out of the box with `ext-curl`, which ships with most PHP installs.

Quick start
-----------

[](#quick-start)

```
use Thingston\Log\Logger;
use Thingston\Log\Adapter\StreamAdapter;

$logger = new Logger(new StreamAdapter('php://stderr'), channel: 'app');

$logger->info('User {user} logged in', ['user' => 'ana']);
$logger->error('Payment failed', ['orderId' => 123, 'reason' => 'card_declined']);
```

`Logger` implements `Psr\Log\LoggerInterface`, so it's a drop-in wherever a PSR-3 logger is expected (frameworks, libraries, DI containers).

Core concepts
-------------

[](#core-concepts)

Class/interfaceRole`Thingston\Log\Logger`PSR-3 logger. Interpolates `{placeholders}` and hands a `LogRecord` to one `AdapterInterface`.`Thingston\Log\LogRecord`Immutable value object: `level`, `message` (already interpolated), `context`, `channel`, `timestamp`.`Thingston\Log\Level`Backed enum of the eight PSR-3 levels, with syslog-style severity ordering (`emergency` = 0 … `debug` = 7).`Thingston\Log\Adapter\AdapterInterface`The thing you swap: `handle(LogRecord $record): void`.`Thingston\Log\Formatter\FormatterInterface`Turns a `LogRecord` into a string for adapters that write text (files, syslog, sockets).`Logger` only ever talks to a **single** adapter. To send a record to several backends at once, wrap them in a `CompositeAdapter` — see [Composing adapters](#composing-adapters). This keeps `Logger` itself free of routing concerns.

```
$logger = new Logger($adapter, channel: 'worker'); // channel defaults to "app"
```

Formatters
----------

[](#formatters)

Two formatters ship with the package; adapters that produce text default to `LineFormatter` unless noted.

**`LineFormatter`** — plain text, one line per record:

```
use Thingston\Log\Formatter\LineFormatter;

$formatter = new LineFormatter(
    format: "[{timestamp}] {channel}.{level}: {message} {context}\n", // default
    dateFormat: DateTimeInterface::ATOM,                              // default
    includeContext: true,                                             // default
    appendNewline: true,                                              // default
);

// [2026-07-07T12:00:00+00:00] app.ERROR: Payment failed {"orderId":123,"reason":"card_declined"}
```

**`JsonFormatter`** — single-line structured JSON, for backends that want structured payloads:

```
use Thingston\Log\Formatter\JsonFormatter;

$formatter = new JsonFormatter(); // {"timestamp":"...","channel":"app","level":"error","message":"...","context":{...}}
```

Both throw `RuntimeException` if the record can't be encoded (e.g. non-UTF-8 bytes in the message for `JsonFormatter`).

Composing adapters
------------------

[](#composing-adapters)

### CompositeAdapter

[](#compositeadapter)

Fans a record out to multiple adapters — e.g. write to a file *and* alert Slack. A failure in one adapter doesn't stop the others, and doesn't propagate to your application:

```
use Thingston\Log\Adapter\CompositeAdapter;
use Thingston\Log\Adapter\StreamAdapter;
use Thingston\Log\Adapter\Cloud\SlackWebhookAdapter;
use Thingston\Log\Adapter\MinLevelAdapter;

$composite = new CompositeAdapter([
    new StreamAdapter('/var/log/app.log'),
    new MinLevelAdapter(new SlackWebhookAdapter($webhookUrl), minLevel: 'error'),
], onError: function (\Throwable $e, $adapter, $record): void {
    // optional: report the failure elsewhere (e.g. error_log), without
    // losing the record for the adapters that did succeed.
    error_log(sprintf('Adapter %s failed: %s', $adapter::class, $e->getMessage()));
});

$logger = new Logger($composite);
```

### MinLevelAdapter

[](#minleveladapter)

Decorator that filters by PSR-3 severity before delegating, so a noisy channel (e.g. Slack) only receives `error`-and-worse while everything still reaches a file:

```
use Thingston\Log\Adapter\MinLevelAdapter;
use Psr\Log\LogLevel;

$alertsOnly = new MinLevelAdapter($slackAdapter, LogLevel::ERROR);
```

Local adapters
--------------

[](#local-adapters)

All local adapters only need core PHP extensions (`ext-pdo`, syslog functions), no vendor SDKs.

### NullAdapter

[](#nulladapter)

Discards everything. Useful as a default/no-op in tests or config-driven setups:

```
use Thingston\Log\Adapter\NullAdapter;

$logger = new Logger(new NullAdapter());
```

### StreamAdapter

[](#streamadapter)

Writes formatted records to a file path (opened in append mode) or an already-open resource:

```
use Thingston\Log\Adapter\StreamAdapter;

new StreamAdapter('/var/log/app.log');          // file path, opened in "ab" mode
new StreamAdapter('php://stdout');               // any stream wrapper
new StreamAdapter(fopen('php://stderr', 'wb'));   // an existing resource (not closed on destruct)
```

### RotatingFileAdapter

[](#rotatingfileadapter)

Writes to a date-stamped file, rotating automatically when the date changes, and optionally pruning old files:

```
use Thingston\Log\Adapter\RotatingFileAdapter;

$adapter = new RotatingFileAdapter(
    filenamePattern: '/var/log/app-{date}.log', // must contain "{date}"
    maxFiles: 14,                                // keep the 14 most recent files; 0 = unlimited
    dateFormat: 'Y-m-d',                          // default
);
```

### SyslogAdapter

[](#syslogadapter)

Sends records to the system logger via `openlog()`/`syslog()`, mapping each PSR-3 level to its syslog priority:

```
use Thingston\Log\Adapter\SyslogAdapter;

$adapter = new SyslogAdapter(
    ident: 'my-app',
    facility: LOG_USER, // default
    options: LOG_PID,   // default
);
```

### PdoAdapter

[](#pdoadapter)

Inserts each record as a row via PDO. Ships a `createTable()` convenience helper for quick starts/tests (SQLite syntax); production schemas should be managed by your own migrations:

```
use Thingston\Log\Adapter\PdoAdapter;

$pdo = new PDO('mysql:host=localhost;dbname=app', $user, $pass);
$adapter = new PdoAdapter($pdo, table: 'logs'); // table name is validated against ^[A-Za-z_][A-Za-z0-9_]*$

// One-time setup, e.g. in a migration:
// CREATE TABLE logs (
//     id         INTEGER PRIMARY KEY AUTO_INCREMENT,
//     channel    VARCHAR(255) NULL,
//     level      VARCHAR(20)  NOT NULL,
//     message    TEXT         NOT NULL,
//     context    TEXT         NULL,
//     created_at DATETIME     NOT NULL
// );
```

Cloud adapters
--------------

[](#cloud-adapters)

Cloud adapters live under `Thingston\Log\Adapter\Cloud`. None of them are hard dependencies of the package — install what you use.

### CloudWatchLogsAdapter

[](#cloudwatchlogsadapter)

Requires [`aws/aws-sdk-php`](https://github.com/aws/aws-sdk-php):

```
composer require aws/aws-sdk-php
```

```
use Aws\CloudWatchLogs\CloudWatchLogsClient;
use Thingston\Log\Adapter\Cloud\CloudWatchLogsAdapter;

$client = new CloudWatchLogsClient([
    'region' => 'us-east-1',
    'version' => 'latest',
]);

$adapter = new CloudWatchLogsAdapter($client, logGroupName: '/my/app', logStreamName: gethostname());
```

The AWS client is invoked duck-typed (only `putLogEvents()` is called), so the core package never references the SDK's class directly; if `aws/aws-sdk-php` isn't installed, the constructor throws a clear `RuntimeException`instead of a confusing autoload failure.

### GoogleCloudLoggingAdapter

[](#googlecloudloggingadapter)

Requires [`google/cloud-logging`](https://github.com/googleapis/google-cloud-php-logging):

```
composer require google/cloud-logging
```

```
use Google\Cloud\Logging\LoggingClient;
use Thingston\Log\Adapter\Cloud\GoogleCloudLoggingAdapter;

$loggingClient = new LoggingClient(['projectId' => 'my-project']);
$logger = $loggingClient->logger('my-app');

$adapter = new GoogleCloudLoggingAdapter($logger);
```

### AzureMonitorAdapter

[](#azuremonitoradapter)

Talks directly to the [Azure Monitor HTTP Data Collector API](https://learn.microsoft.com/azure/azure-monitor/logs/data-collector-api)— no vendor SDK required, just `ext-curl` (bundled `CurlHttpTransport`):

```
use Thingston\Log\Adapter\Cloud\AzureMonitorAdapter;

$adapter = new AzureMonitorAdapter(
    workspaceId: $workspaceId,   // Log Analytics workspace (customer) ID
    sharedKey: $sharedKeyBase64, // primary or secondary workspace key
    logType: 'MyAppLogs',        // custom log type name; default "PsrLog"
);
```

### PapertrailAdapter

[](#papertrailadapter)

Sends RFC 3164 syslog messages over TCP/TLS. Works with [Papertrail](https://www.papertrail.com/) as well as any other generic syslog-over-TLS endpoint (Loggly, etc.) — just point `host`/`port` at theirs:

```
use Thingston\Log\Adapter\Cloud\PapertrailAdapter;

$adapter = new PapertrailAdapter(
    host: 'logsN.papertrailapp.com',
    port: 12345,
    appName: 'my-app', // default "app"
    useTls: true,       // default true
);
```

### SlackWebhookAdapter

[](#slackwebhookadapter)

Posts to a Slack [Incoming Webhook](https://api.slack.com/messaging/webhooks) URL. Intended for alerting — wrap it in `MinLevelAdapter` so only significant events reach the channel:

```
use Thingston\Log\Adapter\Cloud\SlackWebhookAdapter;
use Thingston\Log\Adapter\MinLevelAdapter;

$slack = new SlackWebhookAdapter(
    webhookUrl: $webhookUrl,
    channel: '#alerts', // optional, overrides the webhook's default channel
    username: 'app-bot', // optional
);

$adapter = new MinLevelAdapter($slack, minLevel: 'error');
```

Both `AzureMonitorAdapter` and `SlackWebhookAdapter` accept an optional `HttpTransportInterface` (defaulting to `CurlHttpTransport`), so you can swap in your own HTTP client (e.g. a PSR-18 bridge) or a test double:

```
use Thingston\Log\Adapter\Cloud\HttpTransportInterface;

final class MyHttpTransport implements HttpTransportInterface
{
    public function send(string $url, string $body, array $headers = []): void
    {
        // ...
    }
}

$adapter = new SlackWebhookAdapter($webhookUrl, transport: new MyHttpTransport());
```

Writing your own adapter
------------------------

[](#writing-your-own-adapter)

Implement the single-method `AdapterInterface`:

```
use Thingston\Log\Adapter\AdapterInterface;
use Thingston\Log\LogRecord;

final class MyAdapter implements AdapterInterface
{
    public function handle(LogRecord $record): void
    {
        // $record->level     e.g. "error"
        // $record->message   already PSR-3 interpolated
        // $record->context   the raw context array
        // $record->channel   e.g. "app"
        // $record->timestamp a DateTimeImmutable
    }
}
```

Throw on failure — wrap with `CompositeAdapter` if you need one broken backend to not affect others.

PSR-3 message interpolation
---------------------------

[](#psr-3-message-interpolation)

`Logger` replaces `{key}` placeholders in the message with the corresponding `context[key]` value, following the PSR-3 reference implementation:

- Scalars (`string`, `int`, `float`, `bool`), `null`, and objects implementing `Stringable` are replaced.
- Arrays and non-`Stringable` objects are left as literal `{key}` text (they can't be safely cast to string).
- Placeholders with no matching context key are left untouched.

```
$logger->warning('User {user} hit rate limit ({count}/{max})', [
    'user' => 'ana',
    'count' => 105,
    'max' => 100,
]);
// "User ana hit rate limit (105/100)"
```

The *raw* `$context` array (not just the interpolated message) is always passed through to `LogRecord::$context`, so structured adapters (`JsonFormatter`, `PdoAdapter`, cloud adapters) can still log it as structured data.

Development
-----------

[](#development)

```
composer install

composer test               # alias for test:unit
composer test:unit          # unit suite: mocks/fakes only, no real extensions or network
composer test:unit:coverage # unit suite with HTML + Clover coverage reports in build/
composer test:integration   # every integration test (skips what isn't configured/available)
composer test:all           # unit + integration
composer phpstan            # static analysis (level 8)
composer cs                 # check coding style (PSR-12)
composer cs:fix             # fix coding style
```

Tests are split into two suites (`tests/Unit`, `tests/Integration`) so mocked behavior and real-resource behavior are never conflated:

### Unit suite (`tests/Unit`)

[](#unit-suite-testsunit)

Runs unconditionally, with no real extensions, network, filesystem daemons, or vendor SDKs required — extension and SDK dependent adapters are exercised against mocks/fakes instead:

- `Logger`, formatters, `CompositeAdapter`, `MinLevelAdapter`, `NullAdapter`, `StreamAdapter`, and `RotatingFileAdapter` are plain PHPUnit tests (no faking needed).
- `PdoAdapter` is tested against `createMock(\PDO::class)`/`createMock(\PDOStatement::class)` — no real database driver is touched.
- `SyslogAdapter` is tested against `openlog()`/`syslog()`/`closelog()` overrides declared in `tests/Fixtures/FunctionOverrides/syslog-functions.php`, recorded by `tests/Fixtures/FakeSyslog.php`.
- `CurlHttpTransport` is tested against `curl_init()`/`curl_exec()`/etc. overrides in `tests/Fixtures/FunctionOverrides/curl-functions.php`, recorded by `tests/Fixtures/FakeCurl.php`.
- `PapertrailAdapter` is tested against `fsockopen()`/`stream_socket_client()` overrides in `tests/Fixtures/FunctionOverrides/socket-functions.php`, recorded by `tests/Fixtures/FakeSockets.php`.
- `AzureMonitorAdapter` and `SlackWebhookAdapter` are tested against an injected `HttpTransportInterface` double (no curl involved at all).
- `CloudWatchLogsAdapter` and `GoogleCloudLoggingAdapter` are tested via duck-typed client doubles, so the unit suite never needs `aws/aws-sdk-php` or `google/cloud-logging` installed.

These PHP-level function overrides work because PHP resolves an unqualified function call (e.g. `curl_exec()`from a file in `Thingston\Log\Adapter\Cloud`) against that same namespace first, falling back to the global function — so the fake is only active while a test arms it, and the real extension is used everywhere else, including by the integration suite below.

The unit suite enforces **100% line/method/class coverage** (checked in CI). The only lines excluded via `@codeCoverageIgnore` are `assert()` type-narrowing calls that PHP itself compiles out when `zend.assertions` is `-1` (the recommended production setting), and are therefore unreachable by design.

### Integration suite (`tests/Integration`)

[](#integration-suite-testsintegration)

Exercises real extensions/resources and is grouped per dependency so each can run in isolation:

```
composer test:integration:curl     # real HTTP request against a local built-in PHP server
composer test:integration:pdo      # real SQLite database via ext-pdo
composer test:integration:syslog   # real openlog()/syslog() calls against the system logger
composer test:integration:sockets  # real TCP socket via ext-sockets
```

`curl`, `pdo`, `syslog`, and `sockets` need no external configuration (they spin up a local built-in server, SQLite file, or TCP listener) and run in CI on every PR.

CI runs the unit suite (with the 100% coverage gate, PHPStan, and coding-standards checks) and the full integration suite across PHP 8.4 and 8.5 (see `.github/workflows/ci.yml`).

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance90

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

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

Unknown

Total

1

Last Release

48d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/04e91a806958f09ca8b3f4c24c6f5c4bcbce3c4e44dd882fa1e4c737ee7774c6?d=identicon)[pedrobrazao](/maintainers/pedrobrazao)

---

Top Contributors

[![pedrobrazao](https://avatars.githubusercontent.com/u/772193?v=4)](https://github.com/pedrobrazao "pedrobrazao (2 commits)")

---

Tags

logpsr-3phploggingawspdoslackazureloggerwebhookPSR3syslogphp8cloudwatchgoogle cloudpsr-logcloud loggingpapertrailmonolog-alternativerotating-filecloudwatch-logsgoogle-cloud-loggingazure-monitor

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/thingston-psr3/health.svg)

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

###  Alternatives

[analog/analog

Fast, flexible, easy PSR-3-compatible PHP logging package with dozens of handlers.

3441.6M24](/packages/analog-analog)[apix/log

Minimalist, thin and fast PSR-3 compliant (multi-bucket) logger.

511.1M26](/packages/apix-log)[markrogoyski/simplelog-php

Powerful PSR-3 logging. So easy, it's simple.

2819.5k4](/packages/markrogoyski-simplelog-php)

PHPackages © 2026

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