• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / jobs / 24850654336

23 Apr 2026 05:59PM UTC coverage: 52.404% (-0.04%) from 52.447%
24850654336

push

github

daycry
fix

1210 of 2309 relevant lines covered (52.4%)

4.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.16
/src/Loggers/JobLogger.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Queues.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Jobs\Loggers;
15

16
use CodeIgniter\I18n\Time;
17
use CodeIgniter\Log\Handlers\BaseHandler;
18
use Daycry\Jobs\Exceptions\JobException;
19
use Daycry\Jobs\Execution\ExecutionResult;
20
use Daycry\Jobs\Job;
21

22
/**
23
 * Centralized job logger replacing legacy LogTrait.
24
 * Captures start/end timestamps and writes structured execution records through
25
 * the configured handler (file or database). Accepts immutable ExecutionResult.
26
 */
27
class JobLogger
28
{
29
    private ?Time $start          = null;
30
    private ?Time $end            = null;
31
    private ?BaseHandler $handler = null;
32
    private readonly string $executionId;
33

34
    public function __construct()
35
    {
36
        $this->executionId = (string) service('uuid')->uuid7()->toRfc4122();
10✔
37
    }
38

39
    /**
40
     * Mark start timestamp (optionally injecting a specific datetime string).
41
     */
42
    public function start(?string $at = null): void
43
    {
44
        $this->start = ($at) ? new Time($at) : Time::now();
10✔
45
    }
46

47
    /**
48
     * Mark end timestamp (optionally injecting a specific datetime string).
49
     */
50
    public function end(?string $at = null): void
51
    {
52
        $this->end = ($at) ? new Time($at) : Time::now();
10✔
53
    }
54

55
    public function getStart(): ?Time
56
    {
57
        return $this->start;
×
58
    }
59

60
    public function getEnd(): ?Time
61
    {
62
        return $this->end;
×
63
    }
64

65
    /**
66
     * Human readable HH:MM:SS duration or null if incomplete.
67
     */
68
    public function duration(): ?string
69
    {
70
        if (! $this->start || ! $this->end) {
10✔
71
            return null;
×
72
        }
73
        $interval = $this->end->diff($this->start);
10✔
74

75
        return $interval->format('%H:%I:%S');
10✔
76
    }
77

78
    /**
79
     * Persist log for executed job using ExecutionResult (respects logPerformance flag).
80
     */
81
    public function log(Job $job, ExecutionResult $result, ?Time $testTime = null): void
82
    {
83
        $config = config('Jobs');
10✔
84
        if (! $config->logPerformance) {
10✔
85
            return; // logging disabled
×
86
        }
87
        if (! $this->start instanceof Time) {
10✔
88
            $this->start = Time::createFromTimestamp((int) $result->startedAt);
×
89
        }
90
        if (! $this->end instanceof Time) {
10✔
91
            $this->end = Time::createFromTimestamp((int) $result->endedAt);
×
92
        }
93
        $this->ensureHandler();
10✔
94

95
        $output = $result->success ? $result->output : null;
10✔
96
        $error  = $result->success ? null : $result->error;
10✔
97

98
        // Truncation
99
        if ($config->maxOutputLength !== null && $config->maxOutputLength >= 0) {
10✔
100
            $truncate = static function (?string $text) use ($config): ?string {
1✔
101
                if ($text === null) {
1✔
102
                    return null;
1✔
103
                }
104
                $len = strlen($text);
1✔
105
                if ($len > $config->maxOutputLength) {
1✔
106
                    return substr($text, 0, $config->maxOutputLength) . "\n[truncated {$len} -> {$config->maxOutputLength} chars]";
1✔
107
                }
108

109
                return $text;
×
110
            };
1✔
111
            $output = $truncate($output);
1✔
112
            $error  = $truncate($error);
1✔
113
        }
114

115
        $rawPayload = $job->getPayload();
10✔
116
        // Merge default keys with user-configured (prevent accidental override dropping defaults)
117
        $defaultKeys   = ['password', 'token', 'secret', 'authorization', 'api_key'];
10✔
118
        $configured    = is_array($config->sensitiveKeys ?? null) ? $config->sensitiveKeys : [];
10✔
119
        $sensitiveKeys = array_values(array_unique(array_merge($defaultKeys, $configured)));
10✔
120
        $maskedPayload = $this->maskSensitive($rawPayload, $sensitiveKeys);
10✔
121

122
        // Additional pattern-based sanitization for token-like strings
123
        $maskedPayload = $this->sanitizeTokenPatterns($maskedPayload);
10✔
124
        $output        = $this->sanitizeTokenPatterns($output);
10✔
125
        $error         = $this->sanitizeTokenPatterns($error);
10✔
126
        $payloadJson   = $this->normalize($maskedPayload);
10✔
127

128
        $outputLength = $output !== null ? strlen((string) $output) : 0;
10✔
129
        $payloadHash  = $payloadJson ? hash('sha256', $payloadJson) : null;
10✔
130

131
        $data = [
10✔
132
            'executionId'   => $this->executionId,
10✔
133
            'name'          => $job->getName(),
10✔
134
            'job'           => $job->getJob(),
10✔
135
            'attempt'       => $job->getAttempt(),
10✔
136
            'queue'         => $job->getQueue(),
10✔
137
            'source'        => method_exists($job, 'getSource') ? $job->getSource() : null,
10✔
138
            'retryStrategy' => $config->retryBackoffStrategy ?? null,
10✔
139
            'payload'       => $payloadJson,
10✔
140
            'payloadHash'   => $payloadHash,
10✔
141
            'environment'   => null,
10✔
142
            'start_at'      => $this->start?->format('Y-m-d H:i:s'),
10✔
143
            'end_at'        => $this->end?->format('Y-m-d H:i:s'),
10✔
144
            'duration'      => $this->duration(),
10✔
145
            'output'        => $this->normalize($this->maskSensitive($output, $sensitiveKeys)),
10✔
146
            'outputLength'  => $outputLength,
10✔
147
            'error'         => $this->normalize($this->maskSensitive($error, $sensitiveKeys)),
10✔
148
            'test_time'     => $testTime?->format('Y-m-d H:i:s'),
10✔
149
        ];
10✔
150

151
        if (method_exists($this->handler, 'setPath')) {
10✔
152
            $this->handler->setPath($job->getName());
10✔
153
        }
154
        $this->handler->handle('info', json_encode($data));
10✔
155
    }
156

157
    /**
158
     * Normalize scalar/complex data to JSON or return null if empty.
159
     */
160
    private function normalize(mixed $data): ?string
161
    {
162
        if ($data === null || $data === '') {
10✔
163
            return null;
10✔
164
        }
165
        if (is_scalar($data)) {
10✔
166
            return (string) $data;
10✔
167
        }
168

169
        $encoded = json_encode($data);
9✔
170

171
        return $encoded !== false ? $encoded : null;
9✔
172
    }
173

174
    /**
175
     * Resolve and memoize the configured handler, validating configuration.
176
     */
177
    private function ensureHandler(): void
178
    {
179
        $config = config('Jobs');
10✔
180
        if (! $config->log || ! array_key_exists($config->log, $config->loggers)) {
10✔
181
            throw JobException::forInvalidLogType();
×
182
        }
183
        if (! $this->handler instanceof BaseHandler) {
10✔
184
            $class         = $config->loggers[$config->log];
10✔
185
            $this->handler = new $class();
10✔
186
        }
187
        if (method_exists($this->handler, 'setPath')) {
10✔
188
            // Use job name as path/filename context
189
            // setPath is called later with the job name by consumer if needed
190
        }
191
    }
192

193
    /**
194
     * Recursively mask sensitive keys in arrays/objects.
195
     *
196
     * @param list<string> $keys
197
     */
198
    private function maskSensitive(mixed $value, array $keys): mixed
199
    {
200
        if (! $value || $keys === []) {
10✔
201
            return $value;
10✔
202
        }
203
        $lowerKeys = array_map(strtolower(...), $keys);
10✔
204
        $mask      = static function ($v) use ($lowerKeys, &$mask) {
10✔
205
            if (is_array($v)) {
10✔
206
                $out = [];
5✔
207

208
                foreach ($v as $k => $val) {
5✔
209
                    $out[$k] = in_array(strtolower((string) $k), $lowerKeys, true) ? '***' : $mask($val);
5✔
210
                }
211

212
                return $out;
5✔
213
            }
214
            if (is_object($v)) {
10✔
215
                $o = clone $v;
5✔
216

217
                foreach (get_object_vars($o) as $k => $val) {
5✔
218
                    $o->{$k} = in_array(strtolower((string) $k), $lowerKeys, true) ? '***' : $mask($val);
1✔
219
                }
220

221
                return $o;
5✔
222
            }
223

224
            return $v;
10✔
225
        };
10✔
226

227
        return $mask($value);
10✔
228
    }
229

230
    /**
231
     * Sanitize token-like patterns (API keys, JWTs, etc.) from strings.
232
     * Detects common patterns and masks them automatically.
233
     */
234
    private function sanitizeTokenPatterns(mixed $value): mixed
235
    {
236
        if (is_string($value)) {
10✔
237
            // Mask JWT tokens (format: xxx.yyy.zzz where parts are base64)
238
            $value = preg_replace(
10✔
239
                '/\b[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/',
10✔
240
                '***JWT_TOKEN***',
10✔
241
                $value,
10✔
242
            );
10✔
243

244
            // Mask long alphanumeric strings (likely API keys/tokens)
245
            // At least 32 chars, mostly alphanumeric
246
            $value = preg_replace(
10✔
247
                '/\b[A-Za-z0-9_-]{32,}\b/',
10✔
248
                '***API_KEY***',
10✔
249
                (string) $value,
10✔
250
            );
10✔
251

252
            // Mask Bearer tokens
253
            $value = preg_replace(
10✔
254
                '/Bearer\s+[A-Za-z0-9_\-\.]+/i',
10✔
255
                'Bearer ***TOKEN***',
10✔
256
                (string) $value,
10✔
257
            );
10✔
258
        } elseif (is_array($value)) {
10✔
259
            return array_map($this->sanitizeTokenPatterns(...), $value);
5✔
260
        } elseif (is_object($value)) {
10✔
261
            $clone = clone $value;
5✔
262

263
            foreach (get_object_vars($clone) as $k => $v) {
5✔
264
                $clone->{$k} = $this->sanitizeTokenPatterns($v);
1✔
265
            }
266

267
            return $clone;
5✔
268
        }
269

270
        return $value;
10✔
271
    }
272
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc