Changelog
All notable changes to this project will be documented in this file.
v1.0.0 - 2026-09-15
A rewrite around the control point, designed from the problem rather than from 0.1. There is no compatibility layer; docs/upgrading.md lists every 0.1 surface and its replacement.
Added - the control point
- Two forms.
Monitor::control('payment.charge', $this)declares a point inline; a class extendingControlPointwith a#[Point]attribute declares one that the inventory can read in full, the stub can generate, and an agent can be sent to. Both produce the sameOutcome. - Two terminals.
run()returns the value or throws what escaped;attempt()returns theOutcomeand never throws for what the operation did. - Risks and corrections.
recover(Class, fn)declares an expected failure; the handler's return value is the result,null,falseand0included. A handler that throws escalates. First declared match wins. - Escalation.
escalate()with a closure or anEscalationclass, called for every failure no correction covers; an escalation that throws is recorded asEscalationFailedand the original exception still propagates. - Limits.
within()records a duration breach and never fails a completed run;attempts()caps retries whatever the policies ask;ensure()fails a run whose result is wrong, after every policy, so it is never retried. - Policies.
Retrywith backoff, multiplier and jitter;Transactionretrying deadlocks as whole transactions;Breakeron a named circuit. A fixed pipeline order (breaker, retry, transaction) so a breaker sees one failure per run. Custom policies implement one interface. - Profiles.
external,database,messagingandinternalbundle policies and limits from config; a point starts from one and overrides what it declares. - Nesting. Points nest to any depth with a parent run id and the stack on
every record. A child's escalation reaches the parent's corrections, a
child's refusal surfaces as
BreakerOpen, and retries never compose across the stack. - Outcome. One readonly object per run: status, value, exception, the risk recovered from, attempts, duration, limits breached, policies, context, timeline and stack.
Added - records
- One schema for every transition (
monitor/1, published atresources/schema/record-1.json):point,run_id,parent_run_id,trace_id,domain,origin,status,attempts,duration_msand the rest as log context fields, with a readable message. Levels per event are configurable. - Domain as a field. Derived from the origin's namespace through a configurable map, so "escalations per domain" is a query rather than a regex.
- Redaction through Redactor. Context and exception messages pass through
the
observabilityprofile; identifiers are left alone so records stay joinable. - Events first.
PointStarted,PointRetried,PointLimitBreached,PointRecovered,PointEscalated,PointRefused,PointEnded,EscalationFailed,BreakerOpened,BreakerHalfOpen,BreakerClosed. Log lines and the store are listeners. - NDJSON.
JsonTapandRecordFormatterwrite one JSON object per line with the record fields at the top level, and never lose a line to bad bytes. Monitor::log($origin)is a PSR-3 logger bound to an origin, addingoriginanddomainto every record it writes.
Added - tracing
- Laravel Context. The trace id and the current point live in Context, so they reach queued jobs and every log line the application writes. Monitor keeps no request state of its own.
- W3C traceparent read and written by
StartTrace, alongside a legacy header; incoming values are validated and an invalid one is replaced. Http::traced()adds both headers to an outgoing request.- Jobs and console pick up or start a trace on their own.
Added - breakers
- A real state machine: closed, open, half-open with a single probe, kept in the cache so every process sees the same circuit. "after" failures "within" seconds open it "for" seconds.
- Standalone API through
Monitor::breaker()and theCheckBreakersroute middleware with a realRetry-After.
Added - store
monitor_outcomes, off by default. Rows are written after the response or the job, never on the request path; a failing write is logged once and never reaches a control point.monitor:outcomesandmonitor:prune.
Added - verification
Monitor::fake()records every outcome while still running the point, cans values withreturning()and failures withfailing(), and asserts withassertRan,assertSucceeded,assertRecovered,assertEscalated,assertRefused,assertRetried,assertLimitBreached,assertNothingEscalatedand friends.monitor:pointsinventories every point without running it, class-form points in full and inline points by name, with table, JSON and SARIF output.--checkfails the build on duplicate names, names off the pattern, a point with no escalation and no catch-all,control()that cannot be read statically, a class in a critical namespace that is not a control point, and warns on catch-alls without escalation and computed names.monitor:explaindescribes one point's contract in prose.make:control-pointgenerates the class form and a test that already uses the fake.- A Pest expectation (
toBeControlled(),toHaveCompleteControlPoints()) and a PHPStan rule (monitor.uncontrolled) with the same semantics. - The fake follows the framework's.
MonitorFakeimplements theFakecontract,Monitor::fake()is idempotent,outcomes()is aCollection, and every positive assertion has its negative:assertNotRan,assertNotRecovered,assertNotEscalated,assertNotRefused,assertNotRetried, plusassertRanOnce.assertRecoveredandassertEscalatedtake a class or a closure.
Added - agents
- Guidelines shipped at
.ai/guidelines/core.blade.php, which Laravel Boost composes into every consuming application's agent guidelines. - An MCP server on
laravel/mcp:list_points,explain_point,outcomesandescalationstools, the record schema and guidelines as resources, and awrap_operationprompt. Read-only, off by default, every response redacted JSON-aware. Registered from config, or by hand inroutes/ai.php. - A Boost skill at
resources/boost/skills/monitor-development/SKILL.md, the path Laravel's own packages use, alongside the guideline.
Added - conventions, following Laravel's first-party packages
- Contracts live in
Contracts\:Runner,Escalation,Limit,PolicyandRule. Commands live inConsole\Commands\. - Services are open, value objects are
final readonly.Outcome,RunInfo,BreakerState,BreakerConfig,Decision,PointDescription,Finding,ScannedFile, the limits and#[Point]are immutable data; everything else can be extended.Outcome,BreakerState,PointDescriptionandFindingimplementArrayableandJsonSerializable. ControlisConditionableandMacroable, andStructuredLoggerisConditionable, so a declaration can branch withwhen()andunless()and an application can add methods of its own.- Point names accept a backed enum wherever a name is passed, so an application can centralise them.
MonitorExceptionis an interface. Each exception keeps its natural SPL parent: the fourInvalid*exceptions extendInvalidArgumentException, the risks extendRuntimeException. Messages quote values in brackets.CircuitBreaker::attempt()ispermit(), so it no longer shares a name withControl::attempt()on the same facade. The policies drop theirsetTimes()andsetRetries()setters in favour of constructor arguments.- The provider does nothing at boot beyond registering: commands and
publishing sit behind
runningInConsole(), the migration is published withpublishesMigrations()and never auto-loaded, middleware aliases are registered when the router resolves, the console trace starts onCommandStarting, andphp artisan aboutshows the package's settings. - The store flushes where first-party packages flush: after each request
and command through the kernels, after each job, on
Queue::loopingand worker stop, and when its buffer reaches 100, so it is safe under Octane and in long-running commands. - Redaction degrades: a Redactor failure inside the recorder or the MCP server never reaches a control point.
Added - after the first review
- Idempotency.
once('invoice:'.$id, ttl: 3600)runs a point at most once per key inside a window; a second run is refused before anything executes with aDuplicaterisk. The key is released when the run fails inside the policies and kept when it completed, because the side effect has happened. Outermost in the pipeline, so a refused run never claims a key. - A typed
run().Control::run()carries a template, so PHPStan knows the value a callback returns. Http::breaker('stripe')on the HTTP client, and on anyPendingRequest: the same circuit a control point uses, refusing without sending while open and counting connection failures and 5xx responses.- Dispatchable points.
ChargeCard::dispatch($invoice)runs a control point class as a queued job throughQueue\RunControlPoint, named and tagged after the point for Horizon; a run refused by an open breaker releases the job for the breaker's retry-after instead of failing it. - Corrections as classes.
recover(Class, Handler::class)with a class implementingContracts\Correction, resolved from the container and named in the inventory. escalateLimits()hands a run that completed but breached a limit to the escalation, andthrottleEscalation(seconds)lets at most one escalation through per point per window, announcing the rest withEscalationThrottled.Monitor::log()with no origin binds to the running control point, so ad hoc lines inside a point carry its origin and domain.Outcome::$startedAtand$endedAt, written to the store's newstarted_atcolumn and toended_atin place of the flush time.- Jobs start clean. A job hands off the control stack it inherited through
Context and keeps the dispatching run as
dispatched_from_run, so a point inside a job has no parent from another process. TheQueue\Middleware\WaitForBreakerjob middleware releases any job for a breaker's retry-after while the circuit is open.
Changed
- Requires PHP 8.3 to 8.5 and Laravel 12 or 13. Laravel 11 is no longer supported.
- Environment variables are
MONITOR_STORE_ENABLEDandMONITOR_MCP_ENABLED. kirschbaum-development/redactor1.x is a hard dependency.
Removed
Monitor::controlled(),catching(),onUncaughtException(),withCircuitBreaker(),withDatabaseTransaction(),overrideContext(),addContext(),overrideTraceId()andNestedControlledBlockException.Monitor::time(),LogTimer,Monitor::redactor(), the internalLogRedactor,OriginWrapperand the origin prefix, separator and wrapper settings.- The
enabled,exception_trace,console_auto_trace,trace_header,circuit_breakerandredactorconfig keys.
v0.1.3 - 2025-07-08
- Switch to scoped container registration.
v0.1.2 - 2025-07-01
- Fix doubled context on custom taps.
v0.1.1 - 2025-06-19
- Use the published Redactor package.
v0.1.0 - 2025-06-17
- First release: controlled blocks, structured logging, trace ids, circuit breaker.