Logging
The package provides structured execution logs capturing detailed metadata for each job run.
Drivers
File (v1.1+): each job name gets its own NDJSON file (
<filePath>/<safe_name>.json) — one JSON object per line, oldest at the top, newest at the bottom. Writes are atomic viaflock(LOCK_EX).history()andlastRun()transparently read both NDJSON and the legacy JSON-array format produced by pre-v1.1 versions; the first append after upgrade rewrites a legacy file as NDJSON in-place. Pruning is deterministic (maxLogsPerJobis a hard cap, last-N entries are kept).Database: each execution is stored as a row in the jobs log table via
JobsLogModel.
Configured via Jobs::$log (file or database).
Data Fields
Field |
Description |
|---|---|
executionId |
UUID v4 unique per run scope (start -> end). |
name |
Logical job name (from |
job |
Handler key (e.g. |
attempt |
Attempts count at completion of the run. |
queue |
Queue name (if queued). |
source |
Origin (e.g. |
retryStrategy |
Current configured strategy string. |
payload |
Masked JSON payload snapshot. |
payloadHash |
SHA-256 of payload JSON (null if empty). |
start_at / end_at |
Timestamps of run boundaries. |
duration |
HH:MM:SS formatted diff. |
output |
Masked/truncated output (success only). |
error |
Masked/truncated error (failure only). |
outputLength |
Raw character length of (truncated) output. |
test_time |
Optional injected test timestamp. |
environment |
Reserved for future environment tagging. |
status (DB only) |
OK or ERROR derived from presence of |
data (DB only) |
Full JSON dump for forward compatibility. |
Sensitive Data Masking
Sensitive keys are the union of defaults (password, token, secret, authorization, api_key) plus user configured keys. Recursively replaced with ***. Recursion is bounded by MAX_MASK_DEPTH = 10 (v1.0.3+); deeper structures are replaced by [truncated:max-depth] so adversarial deep payloads cannot trigger a stack overflow.
Pattern-based detection (independent of key names) covers JWT tokens, Bearer tokens, and known API-key prefixes (Stripe, AWS, GitHub, Slack) plus opaque alphanumeric strings of 40+ characters. The previous “32+ chars” rule was tightened in v1.0.3 so 32-character UUIDs and SHA-1 hex digests are no longer false positives.
Truncation
If maxOutputLength is set, output & error strings longer than the limit are truncated with a suffix marker.
Pruning
maxLogsPerJob enforces a rolling window. The database handler deletes oldest rows beyond the limit. The v1.1+ file handler also enforces it deterministically: after every append the file is rewritten with the last maxLogsPerJob entries when the cap is exceeded.
Example (File JSON Entry)
{
"executionId": "b7c4...",
"name": "nightly_runner",
"job": "command",
"attempt": 1,
"queue": "default",
"source": "queue",
"retryStrategy": "exponential",
"payload": "{\"task\":\"rotate\"}",
"payloadHash": "a9d...",
"start_at": "2025-10-06 12:30:00",
"end_at": "2025-10-06 12:30:02",
"duration": "00:00:02",
"output": null,
"error": null,
"outputLength": 0
}
Usage
$logger = new JobLogger();
$logger->start();
// ... run job ...
$result = new ExecutionResult(true, 'All good', null, $startedAt, microtime(true));
$logger->end();
$logger->log($job, $result);
Accessing History
Always prefer the handler API ($handler->history($name, $limit)) over reading the file directly — it returns entries newest-first regardless of the on-disk format (legacy JSON-array or NDJSON), so test code and operators do not need to care which version produced the file.
File:
(new FileHandler())->history($name, $limit)(or read the file directly: NDJSON one object per line, oldest-first).Database:
DatabaseHandler::history($name, $limit)or query via the model.
Tests in this repo use the readJobLogFile() helper on Tests\Support\TestCase which handles both formats transparently (added in v1.1).
Extended Fields
Additional fields (payloadHash, outputLength, retryStrategy) support observability and integrity checks.
Error Masking Example
Error messages themselves pass through masking filter—embedded sensitive key-value substrings are redacted when keys are explicit.
Architecture Overview
The logging pipeline is intentionally thin:
JobLoggerorchestrates timing (start()/end()), shapes the structured array and applies masking & truncation.The selected handler (file or database) receives a single JSON string (level + message) via
handle().Handlers persist the record (append to file, insert row) and may enforce pruning.
Handlers are stateless aside from optional per-run context (e.g. setPath($name)). Retry / attempt logic is entirely external and only the final attempt value is logged.
Sequence
JobLogger::start()
-> execute job logic
JobLogger::end()
JobLogger::log($job, $executionResult)
-> ensure handler
-> build normalized data structure
-> mask sensitive fields
-> JSON encode and delegate to handler->handle('info', $json)
Implementing a Custom Logger Handler
You can add new persistence or forwarding targets (e.g. Elasticsearch, stdout, HTTP webhook, syslog) by creating a handler compatible with CodeIgniter log handler conventions.
Minimal Custom Handler Example
namespace App\Logging;
use CodeIgniter\Log\Handlers\BaseHandler;
class StdoutHandler extends BaseHandler
{
private ?string $name = null;
public function handle($level, $message): bool
{
// $message is a JSON string produced by JobLogger
fwrite(STDOUT, '[' . strtoupper($level) . '] ' . $message . PHP_EOL);
return true;
}
// Optional: allow JobLogger to set a logical name
public function setPath(string $name): self
{
$this->name = $name;
return $this;
}
}
Registering the Handler
Add it to the config map (Jobs::$loggers) and select it:
$cfg = config('Jobs');
$cfg->loggers['stdout'] = \App\Logging\StdoutHandler::class;
$cfg->log = 'stdout';
Handler Responsibilities
Accept the JSON message from
JobLoggerunchanged (do not mutate structure).Perform fast, non-blocking write if used in high-throughput queues (consider async/buffer for remote sinks).
Return
trueeven on benign failures to avoid throwing inside critical path (or throw intentionally for strict mode).
Pruning Strategy
If your backend accumulates data (e.g. a custom database / index), implement a similar pruning routine:
Count existing records for a job name.
If >=
maxLogsPerJobdelete oldest(count - max + 1).
Masking & Security
Do NOT re-mask inside handler. Payload is already sanitized. If you need additional filtering (e.g. GDPR redaction), clone and edit the decoded object, not the original string (keep original for audit if allowed).
Adding Derived Fields
If you want custom derived metadata (e.g. latency buckets) prefer doing it upstream (extend JobLogger) to keep handlers focused. Alternatively, decode $message, append keys and re-encode—understanding this may break forward compatibility if schema evolves.
Extending JobLogger Itself
To inject extra global tags (environment, build info):
class TaggedJobLogger extends \Daycry\Jobs\Loggers\JobLogger {
protected function additionalContext(): array { return ['build' => 'abc123']; }
}
You would then override the log() method to merge additionalContext() into the $data array before encoding.
Testing Custom Handlers
Create a job that emits known output.
Use the handler in a test environment (
$cfg->log = 'stdout').Capture output or mock resources (e.g. HTTP client) asserting JSON schema (presence of required fields) and masking (
***for sensitive keys).
Field Stability & Versioning
While core fields are stable, you should design handlers to ignore unknown future keys (treat as forward-compatible). When storing JSON in a schemaless sink, keep original raw message for audit.
Troubleshooting
Symptom |
Cause |
Fix |
|---|---|---|
Empty file |
|
Enable in config. |
Unmasked secrets |
Custom handler modified JSON or masking keys absent |
Ensure handler is pass-through; extend |
Duplicate entries |
Multiple logger instances per run |
Reuse single |
Pruning not working |
Custom handler missing deletion logic |
Implement count + delete oldest records. |
For questions or enhancements, open an issue and describe your handler use-case.