thingston/psr3
Composer 安装命令:
composer require thingston/psr3
包简介
A lightweight, dependency-free PSR-3 logger with pluggable local and cloud adapters.
关键字:
README 文档
README
A lightweight, dependency-free 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 withMinLevelAdapter, and swapLineFormatter/JsonFormatterper adapter. - 100% test coverage, PHPStan level 8, PSR-12 coding style.
Table of contents
- Installation
- Quick start
- Core concepts
- Formatters
- Composing adapters
- Local adapters
- Cloud adapters
- Writing your own adapter
- PSR-3 message interpolation
- Development
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
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
| Class/interface | Role |
|---|---|
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. This keeps Logger itself free of routing
concerns.
$logger = new Logger($adapter, channel: 'worker'); // channel defaults to "app"
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
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
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
All local adapters only need core PHP extensions (ext-pdo, syslog functions), no vendor SDKs.
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
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
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
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
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 live under Thingston\Log\Adapter\Cloud. None of them are hard dependencies of the package — install
what you use.
CloudWatchLogsAdapter
Requires 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
Requires google/cloud-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
Talks directly to the Azure Monitor HTTP 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
Sends RFC 3164 syslog messages over TCP/TLS. Works with Papertrail 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
Posts to a Slack Incoming Webhook 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
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
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 implementingStringableare replaced. - Arrays and non-
Stringableobjects 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
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)
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, andRotatingFileAdapterare plain PHPUnit tests (no faking needed).PdoAdapteris tested againstcreateMock(\PDO::class)/createMock(\PDOStatement::class)— no real database driver is touched.SyslogAdapteris tested againstopenlog()/syslog()/closelog()overrides declared intests/Fixtures/FunctionOverrides/syslog-functions.php, recorded bytests/Fixtures/FakeSyslog.php.CurlHttpTransportis tested againstcurl_init()/curl_exec()/etc. overrides intests/Fixtures/FunctionOverrides/curl-functions.php, recorded bytests/Fixtures/FakeCurl.php.PapertrailAdapteris tested againstfsockopen()/stream_socket_client()overrides intests/Fixtures/FunctionOverrides/socket-functions.php, recorded bytests/Fixtures/FakeSockets.php.AzureMonitorAdapterandSlackWebhookAdapterare tested against an injectedHttpTransportInterfacedouble (no curl involved at all).CloudWatchLogsAdapterandGoogleCloudLoggingAdapterare tested via duck-typed client doubles, so the unit suite never needsaws/aws-sdk-phporgoogle/cloud-logginginstalled.
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)
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).
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-07-08