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

daycry / jobs / 21444656761

28 Jan 2026 03:37PM UTC coverage: 55.831% (-0.5%) from 56.301%
21444656761

push

github

daycry
Add background execution to QueueRunCommand

Introduces support for running the queue command in the background using Symfony Process. Refactors parameter handling for 'queue', 'oneTime', and 'background' options, and improves queue prompt with validation.

0 of 18 new or added lines in 1 file covered. (0.0%)

2 existing lines in 2 files now uncovered.

1173 of 2101 relevant lines covered (55.83%)

4.23 hits per line

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

93.33
/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 = (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) {
10✔
88
            $this->start = Time::createFromTimestamp((int) $result->startedAt);
×
89
        }
90
        if (! $this->end) {
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

UNCOV
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($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
        return json_encode($data);
9✔
170
    }
171

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

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

206
                foreach ($v as $k => $val) {
5✔
207
                    if (in_array(strtolower((string) $k), $lowerKeys, true)) {
5✔
208
                        $out[$k] = '***';
3✔
209
                    } else {
210
                        $out[$k] = $mask($val);
5✔
211
                    }
212
                }
213

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

219
                foreach (get_object_vars($o) as $k => $val) {
5✔
220
                    if (in_array(strtolower((string) $k), $lowerKeys, true)) {
1✔
221
                        $o->{$k} = '***';
1✔
222
                    } else {
223
                        $o->{$k} = $mask($val);
1✔
224
                    }
225
                }
226

227
                return $o;
5✔
228
            }
229

230
            return $v;
10✔
231
        };
10✔
232

233
        return $mask($value);
10✔
234
    }
235

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

250
            // Mask long alphanumeric strings (likely API keys/tokens)
251
            // At least 32 chars, mostly alphanumeric
252
            $value = preg_replace(
10✔
253
                '/\b[A-Za-z0-9_-]{32,}\b/',
10✔
254
                '***API_KEY***',
10✔
255
                $value,
10✔
256
            );
10✔
257

258
            // Mask Bearer tokens
259
            $value = preg_replace(
10✔
260
                '/Bearer\s+[A-Za-z0-9_\-\.]+/i',
10✔
261
                'Bearer ***TOKEN***',
10✔
262
                $value,
10✔
263
            );
10✔
264
        } elseif (is_array($value)) {
10✔
265
            return array_map([$this, 'sanitizeTokenPatterns'], $value);
5✔
266
        } elseif (is_object($value)) {
10✔
267
            $clone = clone $value;
5✔
268

269
            foreach (get_object_vars($clone) as $k => $v) {
5✔
270
                $clone->{$k} = $this->sanitizeTokenPatterns($v);
1✔
271
            }
272

273
            return $clone;
5✔
274
        }
275

276
        return $value;
10✔
277
    }
278
}
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