watchtower15 / cakephp
The WatchTower15 PHP SDK's CakePHP plugin.
Package info
github.com/Watchtower15/watchtower15-cakephp
Type:cakephp-plugin
pkg:composer/watchtower15/cakephp
Requires
- php: ^8.3
- composer-runtime-api: ^2.2
- cakephp/cakephp: ^5.1.10
- watchtower15/sdk: v1.0.2
Requires (Dev)
None
Suggests
- ext-apcu: An APCu Cache engine for the metrics store, flushed from requests
- ext-memcached: A Memcached Cache engine for the metrics store (an atomic increment)
- ext-redis: A Redis Cache engine for the metrics store (an atomic increment)
Provides
None
Conflicts
None
Replaces
None
README
The WatchTower15 PHP SDK's CakePHP plugin. It turns the core SDK (watchtower15/sdk) into CakePHP services: the application's
WatchTower15\Client, the metrics aggregator and its flush, a CakePHP log engine, a transport on CakePHP's own HTTP client, and the
console commands watchtower15 test and watchtower15 flush. CakePHP ^5.1.10, PHP ^8.3.
Nothing is built, sent or logged when the application boots, and a mistyped credential never breaks the application or your logging.
This repository is a read-only split of the WatchTower15 PHP SDK: pull requests are not accepted. Documentation: https://docs.watchtower15.com. Report a vulnerability to contact@watchtower15.com with the subject "Security".
Support policy. PHP 8.3 and later and CakePHP 5.1 and later, Semantic Versioning, all four packages released together: see the support policy.
Install
composer require watchtower15/cakephp
The plugin installer registers the plugin from its Composer type (cakephp-plugin). Without it, load it in src/Application.php:
public function bootstrap(): void { parent::bootstrap(); $this->addPlugin(\WatchTower15\Cake\CakePlugin::class); }
Then check the setup:
bin/cake watchtower15 test
Configuration
Every key is optional. Credentials come from the environment (WT15_API_KEY, WT15_METRICS_TOKEN, WT15_LOGS_TOKEN, and
WT15_API_URL, WT15_INGEST_URL) unless Configure's WatchTower15 key names them, which wins. A blank value is read as not set.
// config/app_local.php 'WatchTower15' => [ 'metrics' => [ 'cache' => 'redis', // a Cache config on the Redis engine, shared by the fleet 'prefix' => 'shop', // required with 'cache': one per application 'allowed' => ['orders.shipped' => 'count', 'queue.depth' => 'level'], ], ], 'Log' => [ 'watchtower15' => [ 'className' => \WatchTower15\Cake\Log\WatchTower15Log::class, 'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'], ], ],
| Key | Meaning |
|---|---|
apiKey, apiUrl, ingestUrl |
The API key, and the two base URLs (unset is the SDK's default) |
timeout, connectTimeout, maxRetries, appInfo |
Passed to the application's client |
httpClient |
true (default) sends through CakePHP's HTTP client; false uses the SDK's own cURL transport |
metrics.token, metrics.enabled |
The metrics token, and the switch for the pipe (true by default) |
metrics.cache, metrics.prefix |
A Cache config name for the store, and the application's prefix (below) |
metrics.store |
file:///absolute/folder or memory://; unset is a folder under TMP |
metrics.allowed |
A map of metric key to count or level: nothing else is sent |
metrics.allowAny |
true sends any metric key (not with metrics.cache) |
metrics.heartbeat |
A metric key the flusher adds once a minute |
metrics.flushAfterResponse |
auto, always or never (below) |
logs.token |
The logs token |
A value of the wrong type is reported by bin/cake watchtower15 test, naming its key, and the default is used. A key the plugin does
not know is reported the same way (a typo), and ignored.
The clients
| Client | Holds | A bad value |
|---|---|---|
WatchTower15\Client |
The API key, and the other credentials | Throws WatchTower15\ConfigurationException to the code that asks for it, and to nothing else |
| the metrics pipe's client | The metrics token alone | Never throws: the flush stops with a config count and keeps its buckets |
| the log engine's client | The logs token alone | Counted config by the engine, never thrown |
WatchTower15\Client, WatchTower15\Metrics\Aggregator, Flusher and BucketStore are in the application's container, so a controller
or command asks for them in its constructor.
public function __construct(private readonly \WatchTower15\Client $wt15, private readonly \WatchTower15\Metrics\Aggregator $metrics) {} // … $this->metrics->increment('orders.shipped'); $page = $this->wt15->servers->list(['health' => ['critical']]);
The transport
The plugin sends through its own Cake\Http\Client, which never follows redirects and which the HTTP client's HttpClient.beforeSend
and HttpClient.afterSend events and Client::addMockResponse() reach as they do for your own calls. Timeouts are set in milliseconds,
and a response larger than the SDK's cap is stopped during the transfer.
The metrics store
The store is the SDK's, on one of your Cache configs. CakePHP's cache has no way to list a key, so the plugin reads the keys you
allow (metrics.allowed and the heartbeat), and a store that is not a Redis, Memcached or APCu config is refused.
// config/app_local.php 'Cache' => ['redis' => ['className' => 'Redis', 'host' => 'redis', 'prefix' => 'cake_']],
| Store | Shared by |
|---|---|
unset: a file under TMP/watchtower15 |
PHP-FPM and the CLI on one host |
metrics.store file:///absolute/folder |
one host |
metrics.cache on a Redis config |
a fleet |
metrics.cache on a Memcached config |
a fleet (not tested by the SDK's own suite) |
metrics.cache on an APCu config |
one SAPI only |
metrics.store memory:// |
one process |
metrics.prefixis required withmetrics.cache, one per application: two applications on one cache backend would share buckets, and the first flusher would send the other's keys under its own token. Keys live underwt15_<the config's prefix><metrics.prefix>:, so a clear of your application's cache leaves them alone.- The plugin keeps one config per expiry, named
wt15_<your config>_<seconds>, each a copy of yours without its groups and without a fallback. They appear inCache::configured(). - ⚠️ APCu is per SAPI: the CLI cannot see what PHP-FPM wrote to APCu, so an APCu store is flushed from requests only.
memory://is for console commands and tests only.- A store the plugin refuses (an unknown scheme, an engine that cannot increment, a missing prefix) disables the metrics pipe, with its
reason, and throws nothing: nothing is buffered, and
watchtower15 testandwatchtower15 flushprint the reason. - While
Cache::disable()is on, or a Redis server is unreachable, the pipe counts the failure and the request goes on.
Flushing
-
After the response.
flushAfterResponse: autoflushes onServer.terminatewhere the response is already finished (PHP-FPM and FrankenPHP, decided byfastcgi_finish_request()and never by the SAPI's name), for an APCu ormemory://store in every runtime, and onCommand.afterExecuteof every command butwatchtower15 flush.alwaysdoes so in every runtime;neveron neither. One flush a minute runs per shared store, whoever claims it first. -
Every minute. In the web server's user's crontab on each host:
* * * * * cd /app && bin/cake watchtower15 flush --if-dueThe files a file store writes carry the process's umask: a cron job running as another user than PHP-FPM cannot write FPM's files, so run them all as the same user.
Logs
Configure the engine under Log as above. It sends one push per request, after the response where PHP-FPM allows it, and it never
sends the SDK's own warnings back to WatchTower15: the SDK writes those to your other log engines.
Commands
bin/cake watchtower15 testchecks each configured credential on a client of its own: the API key withme(), the metrics token with the source's listing, the logs token by its format (a log push is a write). It then prints the store and its scope, the allow-list, the flush paths and the log engines. Exit 2 when nothing is configured or a credential or URL is malformed (the checks the others allow still run); else 1 when a check failed or the store is refused; else 0.bin/cake watchtower15 flushsends the closed minute buckets now. It prints "disabled" (exit 0) while metrics are off or have no token, refuses a store it cannot read from the CLI or a refused configuration (exit 2, with no store call), and otherwise flushes and prints the report (exit 1 when the flush stopped on a failure).--if-duetakes the same minute claim as the after-response flush and prints "Not due" when another process holds it: this is the cron line's form.
Security
API keys and tokens are personal: keep them in the environment or a secrets store, never in code. The plugin never prints a credential.
MIT licence.