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

daycry / jobs / 18716788956

07 Oct 2025 07:58AM UTC coverage: 61.756% (-0.06%) from 61.815%
18716788956

push

github

daycry
Update README.md

1048 of 1697 relevant lines covered (61.76%)

4.78 hits per line

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

91.75
/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 string $executionId;
33

34
    public function __construct()
35
    {
36
        $this->executionId = $this->generateUuidV4();
11✔
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();
11✔
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();
11✔
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) {
11✔
71
            return null;
×
72
        }
73
        $interval = $this->end->diff($this->start);
11✔
74

75
        return $interval->format('%H:%I:%S');
11✔
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');
11✔
84
        if (! $config->logPerformance) {
11✔
85
            return; // logging disabled
×
86
        }
87
        if (! $this->start) {
11✔
88
            $this->start = Time::createFromTimestamp((int) $result->startedAt);
×
89
        }
90
        if (! $this->end) {
11✔
91
            $this->end = Time::createFromTimestamp((int) $result->endedAt);
×
92
        }
93
        $this->ensureHandler();
11✔
94

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

98
        // Truncation
99
        if ($config->maxOutputLength !== null && $config->maxOutputLength >= 0) {
11✔
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();
11✔
116
        // Merge default keys with user-configured (prevent accidental override dropping defaults)
117
        $defaultKeys   = ['password', 'token', 'secret', 'authorization', 'api_key'];
11✔
118
        $configured    = is_array($config->sensitiveKeys ?? null) ? $config->sensitiveKeys : [];
11✔
119
        $sensitiveKeys = array_values(array_unique(array_merge($defaultKeys, $configured)));
11✔
120
        $maskedPayload = $this->maskSensitive($rawPayload, $sensitiveKeys);
11✔
121
        $payloadJson   = $this->normalize($maskedPayload);
11✔
122

123
        $outputLength = $output !== null ? strlen($output) : 0;
11✔
124
        $payloadHash  = $payloadJson ? hash('sha256', $payloadJson) : null;
11✔
125

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

146
        if (method_exists($this->handler, 'setPath')) {
11✔
147
            $this->handler->setPath($job->getName());
11✔
148
        }
149
        $this->handler->handle('info', json_encode($data));
11✔
150
    }
151

152
    /**
153
     * Normalize scalar/complex data to JSON or return null if empty.
154
     */
155
    private function normalize(mixed $data): ?string
156
    {
157
        if ($data === null || $data === '') {
11✔
158
            return null;
11✔
159
        }
160
        if (is_scalar($data)) {
11✔
161
            return (string) $data;
11✔
162
        }
163

164
        return json_encode($data);
10✔
165
    }
166

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

186
    /**
187
     * Generate a RFC 4122 compliant UUID v4 (random).
188
     */
189
    private function generateUuidV4(): string
190
    {
191
        $data    = random_bytes(16);
11✔
192
        $data[6] = chr((ord($data[6]) & 0x0F) | 0x40); // version 4
11✔
193
        $data[8] = chr((ord($data[8]) & 0x3F) | 0x80); // variant
11✔
194

195
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
11✔
196
    }
197

198
    /**
199
     * Recursively mask sensitive keys in arrays/objects.
200
     *
201
     * @param list<string> $keys
202
     */
203
    private function maskSensitive(mixed $value, array $keys): mixed
204
    {
205
        if (! $value || empty($keys)) {
11✔
206
            return $value;
11✔
207
        }
208
        $lowerKeys = array_map(static fn ($k) => strtolower($k), $keys);
11✔
209
        $mask      = static function ($v) use ($lowerKeys, &$mask) {
11✔
210
            if (is_array($v)) {
11✔
211
                $out = [];
6✔
212

213
                foreach ($v as $k => $val) {
6✔
214
                    if (in_array(strtolower((string) $k), $lowerKeys, true)) {
6✔
215
                        $out[$k] = '***';
3✔
216
                    } else {
217
                        $out[$k] = $mask($val);
6✔
218
                    }
219
                }
220

221
                return $out;
6✔
222
            }
223
            if (is_object($v)) {
11✔
224
                $o = clone $v;
5✔
225

226
                foreach (get_object_vars($o) as $k => $val) {
5✔
227
                    if (in_array(strtolower((string) $k), $lowerKeys, true)) {
1✔
228
                        $o->{$k} = '***';
1✔
229
                    } else {
230
                        $o->{$k} = $mask($val);
1✔
231
                    }
232
                }
233

234
                return $o;
5✔
235
            }
236

237
            return $v;
11✔
238
        };
11✔
239

240
        return $mask($value);
11✔
241
    }
242
}
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