Skip to content

Commit 59c6238

Browse files
committed
Redactor refactor
1 parent fedb2ac commit 59c6238

14 files changed

+951
-507
lines changed

README.md

Lines changed: 90 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,15 @@ class StripePaymentService
110110

111111
**What it does:** Monitors critical operations with automatic start/end logging, exception-specific handling, DB transactions, circuit breakers, and true escalation for uncaught exceptions.
112112

113+
**Note:** The second parameter `$origin` (usually `$this`) is optional and automatically provides origin context to the structured logger used by the controlled block, eliminating the need for a separate `->from()` call.
114+
113115
#### **Factory & Execution**
114116

115117
```php
116118
use Kirschbaum\Monitor\Facades\Monitor;
117119

118120
// Create and execute controlled block
119-
$result = Monitor::controlled('payment_processing')
121+
$result = Monitor::controlled('payment_processing', $this)
120122
->run(function() {
121123
return processPayment($data);
122124
});
@@ -128,50 +130,54 @@ $result = Monitor::controlled('payment_processing')
128130
/*
129131
* Adds additional context to the structured logger.
130132
*/
131-
->addContext([
132-
'transaction_id' => 'txn_456',
133-
'gateway' => 'stripe'
134-
]);
133+
Monitor::controlled('payment_processing', $this)
134+
->addContext([
135+
'transaction_id' => 'txn_456',
136+
'gateway' => 'stripe'
137+
]);
135138

136139
/*
137140
* Will completely replace structured logger context.
138141
* ⚠️ Not recommended unless you have a good reason to do so.
139142
*/
140-
->overrideContext([
141-
'user_id' => 123,
142-
'operation' => 'payment',
143-
'amount' => 99.99
144-
]);
143+
Monitor::controlled('payment_processing', $this)
144+
->overrideContext([
145+
'user_id' => 123,
146+
'operation' => 'payment',
147+
'amount' => 99.99
148+
]);
145149
```
146150

147151
#### **Exception Handling**
148152

149153
**Exception-Specific Handlers (`catching`):**
150154
```php
151-
->catching([
152-
DatabaseException::class => function($exception, $meta) {
153-
$cachedData = ExampleModel::getCachedData();
154-
return $cachedData; // Recovery value
155-
},
156-
NetworkException::class => function($exception, $meta) {
157-
$this->exampleRetryLater($meta);
158-
// No return = just handle, don't recover
159-
},
160-
PaymentException::class => function($exception, $meta) {
161-
$this->exampleNotifyFinanceTeam($exception, $meta);
162-
throw $exception; // Re-throw if needed
163-
},
164-
// Other exception types remain uncaught.
165-
])
155+
Monitor::controlled('payment_processing', $this)
156+
->catching([
157+
DatabaseException::class => function($exception, $meta) {
158+
$cachedData = ExampleModel::getCachedData();
159+
return $cachedData; // Recovery value
160+
},
161+
NetworkException::class => function($exception, $meta) {
162+
$this->exampleRetryLater($meta);
163+
// No return = just handle, don't recover
164+
},
165+
PaymentException::class => function($exception, $meta) {
166+
$this->exampleNotifyFinanceTeam($exception, $meta);
167+
throw $exception; // Re-throw if needed
168+
},
169+
// Other exception types remain uncaught.
170+
])
166171
```
167172

168173
**Uncaught Exception Handling (`onUncaughtException`):**
169174
```php
170-
->onUncaughtException(function($exception, $meta) {
171-
// Example actions, the exception will remain uncaught
172-
$this->alertOpsTeam($exception, $meta);
173-
$this->sendToErrorTracking($exception);
174-
})
175+
Monitor::controlled('payment_processing', $this)
176+
->onUncaughtException(function($exception, $meta) {
177+
// Example actions, the exception will remain uncaught
178+
$this->alertOpsTeam($exception, $meta);
179+
$this->sendToErrorTracking($exception);
180+
})
175181
```
176182

177183
**Key Behavior:**
@@ -182,16 +188,63 @@ $result = Monitor::controlled('payment_processing')
182188

183189
#### **Circuit Breaker & Database Protection**
184190

191+
**What are Circuit Breakers?**
192+
Circuit breakers prevent cascading failures by temporarily stopping requests to a failing service, allowing it time to recover. They automatically "open" after a threshold of failures and "close" once the service is healthy again, protecting your application from wasting resources on operations likely to fail.
193+
185194
```php
186-
->withCircuitBreaker('payment_gateway', 3, 60) // 3 failures, 60s timeout
187-
->withDatabaseTransaction(2, [DeadlockException::class], [ValidationException::class])
195+
Monitor::controlled('payment_processing', $this)
196+
->withCircuitBreaker('payment_gateway', 3, 60) // 3 failures, 60s timeout
197+
->withDatabaseTransaction(2, [DeadlockException::class], [ValidationException::class])
188198
```
189199

200+
**Circuit Breaker HTTP Middleware**
201+
202+
You can also protect entire routes or route groups using the `CheckCircuitBreakers` middleware:
203+
204+
```php
205+
// bootstrap/app.php or register as route middleware
206+
->withMiddleware(function (Middleware $middleware) {
207+
$middleware->alias([
208+
'circuit' => \Kirschbaum\Monitor\Http\Middleware\CheckCircuitBreakers::class,
209+
]);
210+
})
211+
212+
// In your routes
213+
Route::middleware(['circuit:payment_gateway,external_api'])
214+
->group(function () {
215+
Route::post('/payments', [PaymentController::class, 'store']);
216+
Route::get('/external-data', [DataController::class, 'fetch']);
217+
});
218+
219+
// Or on individual routes
220+
Route::get('/api/data')
221+
->middleware('circuit:slow_service')
222+
->name('data.fetch');
223+
```
224+
225+
**Circuit Breaker Middleware Features:**
226+
- **Multiple Breakers**: Check multiple circuit breakers with `circuit:breaker1,breaker2,breaker3`
227+
- **Graceful Degradation**: Returns HTTP 503 (Service Unavailable) when circuit is open
228+
- **Standard Headers**: Includes `Retry-After`, `X-Circuit-Breaker`, and `X-Circuit-Breaker-Status` headers
229+
- **Jitter Protection**: Built-in randomized retry delays prevent thundering herd effects
230+
- **Auto-Recovery**: Circuits automatically close when services recover
231+
232+
**Response Headers When Circuit is Open:**
233+
```
234+
HTTP/1.1 503 Service Unavailable
235+
Retry-After: 45
236+
X-Circuit-Breaker: payment_gateway
237+
X-Circuit-Breaker-Status: open
238+
```
239+
240+
The `Retry-After` header includes intelligent jitter - instead of all clients retrying at the exact same time, it provides a random delay between 0 and the remaining decay time, preventing overwhelming the recovering service.
241+
190242
#### **Tracing & Logging**
191243

192244
```php
193-
->overrideTraceId('custom-trace-12345')
194-
->from('PaymentService') // Custom logger origin
245+
Monitor::controlled('payment_processing', $this)
246+
->overrideTraceId('custom-trace-12345')
247+
// Origin is automatically set from the second parameter ($this)
195248
```
196249

197250
#### **Complete Example**
@@ -225,7 +278,7 @@ class PaymentService
225278
}
226279
```
227280

228-
#### **📊 What it logs:**
281+
#### **What it logs:**
229282

230283
**Success:**
231284
```json
@@ -246,19 +299,18 @@ class PaymentService
246299
{"message": "[PaymentProcessor] UNCAUGHT", "exception": "RuntimeException", "uncaught": true, "duration_ms": 300}
247300
```
248301

249-
#### **🎯 API Reference**
302+
#### **API Reference**
250303

251304
| Method | Purpose | Returns |
252305
|--------|---------|---------|
253-
| `Controlled::for(string $name)` | Create controlled block | `self` |
306+
| `Monitor::controlled(string $name, string\|object $origin = null)` | Create controlled block with optional origin | `self` |
254307
| `->overrideContext(array $context)` | Replace entire context | `self` |
255308
| `->addContext(array $context)` | Merge additional context | `self` |
256309
| `->catching(array $handlers)` | Define exception-specific handlers | `self` |
257310
| `->onUncaughtException(Closure $callback)` | Handle uncaught exceptions only | `self` |
258311
| `->withCircuitBreaker(string $name, int $threshold, int $decay)` | Configure circuit breaker | `self` |
259312
| `->withDatabaseTransaction(int $retries, array $only, array $exclude)` | Wrap in DB transaction with retry | `self` |
260313
| `->overrideTraceId(string $traceId)` | Set custom trace ID | `self` |
261-
| `->from(string\|object $origin)` | Set logger origin | `self` |
262314
| `->run(Closure $callback)` | Execute the controlled block | `mixed` |
263315

264316
### Distributed Tracing

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"laravel/pint": "^1.22",
3030
"larastan/larastan": "^3.4",
3131
"orchestra/testbench": "^10.3",
32-
"pestphp/pest-plugin-laravel": "^3.1"
32+
"pestphp/pest-plugin-laravel": "^3.1",
33+
"timacdonald/log-fake": "^2.4"
3334
},
3435
"config": {
3536
"allow-plugins": {

config/monitor.php

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,37 @@
417417

418418
'mark_redacted' => env('MONITOR_LOG_REDACTOR_MARK_REDACTED', true),
419419

420+
/*
421+
|----------------------------------------------------------------------
422+
| Track Redacted Keys
423+
|----------------------------------------------------------------------
424+
|
425+
| When enabled, adds a '_redacted_keys' array to the context listing
426+
| which keys were redacted. Useful for debugging and auditing.
427+
| Only applies when mark_redacted is also enabled.
428+
|
429+
*/
430+
431+
'track_redacted_keys' => env('MONITOR_LOG_REDACTOR_TRACK_KEYS', false),
432+
433+
/*
434+
|----------------------------------------------------------------------
435+
| Non-Redactable Object Behavior
436+
|----------------------------------------------------------------------
437+
|
438+
| Defines how to handle objects that cannot be safely redacted due to
439+
| circular references, resources, or other serialization issues.
440+
|
441+
| Available options:
442+
| - 'preserve': Return the original object unchanged (default, safest)
443+
| - 'remove': Remove the object entirely from context
444+
| - 'empty_array': Replace with an empty array []
445+
| - 'redact': Replace with redaction placeholder text
446+
|
447+
*/
448+
449+
'non_redactable_object_behavior' => env('MONITOR_LOG_REDACTOR_OBJECT_BEHAVIOR', 'preserve'),
450+
420451
/*
421452
|----------------------------------------------------------------------
422453
| Maximum Value Length
@@ -431,7 +462,7 @@
431462
|
432463
*/
433464

434-
'max_value_length' => env('MONITOR_LOG_REDACTOR_MAX_VALUE_LENGTH', 20000),
465+
'max_value_length' => env('MONITOR_LOG_REDACTOR_MAX_VALUE_LENGTH', 5000),
435466

436467
/*
437468
|----------------------------------------------------------------------
@@ -494,6 +525,29 @@
494525
| 30: Higher performance, may miss some short tokens
495526
*/
496527
'min_length' => env('MONITOR_LOG_REDACTOR_SHANNON_MIN_LENGTH', 25),
528+
529+
/*
530+
|----------------------------------------------------------------------
531+
| Exclusion Patterns
532+
|----------------------------------------------------------------------
533+
|
534+
| Regex patterns for strings that should NOT be redacted despite having
535+
| high entropy. These patterns identify common safe formats like URLs,
536+
| file paths, UUIDs, etc. that naturally have high entropy but are not
537+
| sensitive data.
538+
|
539+
*/
540+
'exclusion_patterns' => [
541+
'/^https?:\/\//', // URLs
542+
'/^[\/\\\\].+[\/\\\\]/', // File paths
543+
'/^\d{4}-\d{2}-\d{2}/', // Date formats
544+
'/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', // UUIDs
545+
'/^[0-9a-f]+$/i', // Hex strings (short ones < 32 chars)
546+
'/^\s*$/', // Whitespace strings
547+
'/^Mozilla\/\d\.\d|^[A-Za-z]+\/\d+\.\d+|AppleWebKit|Chrome|Safari|Firefox|Opera|Edge/', // User agents
548+
'/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', // IPv4 addresses
549+
'/^[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}$/i', // MAC addresses
550+
],
497551
],
498552
],
499553
];

src/Controlled.php

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
final class Controlled
1616
{
17-
protected ?string $name = null;
17+
protected string $name;
1818

1919
/** @var array<class-string<\Throwable>, \Closure> */
2020
protected array $exceptionHandlers = [];
@@ -46,12 +46,18 @@ final class Controlled
4646

4747
protected ?StructuredLogger $logger = null;
4848

49-
public static function for(string $name): self
49+
public function __construct(string $name, string|object|null $origin = null)
5050
{
51-
$instance = new self;
52-
$instance->name = $name;
51+
$this->name = $name;
5352

54-
return $instance;
53+
if ($origin !== null) {
54+
$this->withStructuredLogger(StructuredLogger::from($origin));
55+
}
56+
}
57+
58+
public static function for(string $name, string|object|null $origin = null): self
59+
{
60+
return new self($name, $origin);
5561
}
5662

5763
/**
@@ -127,9 +133,9 @@ public function overrideTraceId(string $traceId): self
127133
return $this;
128134
}
129135

130-
public function from(string|object $origin): self
136+
public function withStructuredLogger(StructuredLogger $logger): self
131137
{
132-
$this->logger = StructuredLogger::from($origin);
138+
$this->logger = $logger;
133139

134140
return $this;
135141
}
@@ -141,10 +147,6 @@ public function run(Closure $callback): mixed
141147

142148
protected function execute(Closure $callback): mixed
143149
{
144-
if (! $this->name) {
145-
throw new \InvalidArgumentException('Controlled block name is required');
146-
}
147-
148150
if ($this->traceIdOverride) {
149151
Monitor::trace()->override($this->traceIdOverride);
150152
}
@@ -266,7 +268,7 @@ protected function handleCaughtException(Throwable $e, string $blockId, string $
266268
foreach ($this->exceptionHandlers as $exceptionClass => $handler) {
267269
if ($e instanceof $exceptionClass) {
268270
$meta = new ControlledFailureMeta(
269-
name: $this->name ?? 'unknown',
271+
name: $this->name,
270272
id: $blockId,
271273
traceId: $traceId,
272274
attempt: $this->attempt,
@@ -316,7 +318,7 @@ protected function handleCaughtException(Throwable $e, string $blockId, string $
316318
protected function handleUncaughtException(Throwable $e, string $blockId, string $traceId, float $durationMs): void
317319
{
318320
$meta = new ControlledFailureMeta(
319-
name: $this->name ?? 'unknown',
321+
name: $this->name,
320322
id: $blockId,
321323
traceId: $traceId,
322324
attempt: $this->attempt,

src/Facades/Monitor.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@
1111
* @method static \Kirschbaum\Monitor\StructuredLogger log(string|object $origin = 'Monitor')
1212
* @method static \Kirschbaum\Monitor\LogTimer time()
1313
* @method static \Kirschbaum\Monitor\CircuitBreaker breaker()
14-
* @method static \Kirschbaum\Monitor\MonitorWithOrigin from(string|object $origin)
15-
* @method static mixed controlled(string $name, \Closure|null $callback = null, array<string, mixed> $context = [], \Closure|null $onFail = null)
16-
* @method static \Kirschbaum\Monitor\Controlled controlled()
14+
* @method static \Kirschbaum\Monitor\Controlled controlled(string $name, string|object|null $origin = null)
15+
* @method static \Kirschbaum\Monitor\Support\LogRedactor redactor()
1716
*/
1817
class Monitor extends Facade
1918
{

0 commit comments

Comments
 (0)