Skip to content

v1.0.0

Latest

Choose a tag to compare

@belisarh belisarh released this 15 Sep 03:16
3bea6da

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 extending ControlPoint with 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 same Outcome.
  • Two terminals. run() returns the value or throws what escaped;
    attempt() returns the Outcome and 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, false and 0 included.
    A handler that throws escalates. First declared match wins.
  • Escalation. escalate() with a closure or an Escalation class, called
    for every failure no correction covers; an escalation that throws is recorded
    as EscalationFailed and 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. Retry with backoff, multiplier and jitter; Transaction
    retrying deadlocks as whole transactions; Breaker on 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, messaging and internal bundle
    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 at
    resources/schema/record-1.json): point, run_id, parent_run_id,
    trace_id, domain, origin, status, attempts, duration_ms and 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 observability profile; 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. JsonTap and RecordFormatter write 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, adding
    origin and domain to 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 the CheckBreakers
    route middleware with a real Retry-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:outcomes and monitor:prune.

Added - verification

  • Monitor::fake() records every outcome while still running the point,
    cans values with returning() and failures with failing(), and asserts
    with assertRan, assertSucceeded, assertRecovered, assertEscalated,
    assertRefused, assertRetried, assertLimitBreached,
    assertNothingEscalated and friends.
  • monitor:points inventories every point without running it, class-form
    points in full and inline points by name, with table, JSON and SARIF
    output. --check fails 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:explain describes one point's contract in prose.
  • make:control-point generates 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. MonitorFake implements the Fake
    contract, Monitor::fake() is idempotent, outcomes() is a Collection,
    and every positive assertion has its negative: assertNotRan,
    assertNotRecovered, assertNotEscalated, assertNotRefused,
    assertNotRetried, plus assertRanOnce. assertRecovered and
    assertEscalated take 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,
    outcomes and escalations tools, the record schema and guidelines as
    resources, and a wrap_operation prompt. Read-only, off by default, every
    response redacted JSON-aware. Registered from config, or by hand in
    routes/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,
    Policy and Rule. Commands live in Console\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,
    PointDescription and Finding implement Arrayable and JsonSerializable.
  • Control is Conditionable and Macroable, and StructuredLogger is
    Conditionable, so a declaration can branch with when() and unless() 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.
  • MonitorException is an interface. Each exception keeps its natural SPL
    parent: the four Invalid* exceptions extend InvalidArgumentException,
    the risks extend RuntimeException. Messages quote values in brackets.
  • CircuitBreaker::attempt() is permit(), so it no longer shares a name
    with Control::attempt() on the same facade. The policies drop their
    setTimes() and setRetries() setters in favour of constructor arguments.
  • The provider does nothing at boot beyond registering: commands and
    publishing sit behind runningInConsole(), the migration is published with
    publishesMigrations() and never auto-loaded, middleware aliases are
    registered when the router resolves, the console trace starts on
    CommandStarting, and php artisan about shows 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::looping and
    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 a Duplicate risk. 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 any PendingRequest:
    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 through Queue\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
    implementing Contracts\Correction, resolved from the container and named
    in the inventory.
  • escalateLimits() hands a run that completed but breached a limit to
    the escalation, and throttleEscalation(seconds) lets at most one
    escalation through per point per window, announcing the rest with
    EscalationThrottled.
  • Monitor::log() with no origin binds to the running control point, so
    ad hoc lines inside a point carry its origin and domain.
  • Outcome::$startedAt and $endedAt, written to the store's new
    started_at column and to ended_at in 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. The
    Queue\Middleware\WaitForBreaker job 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_ENABLED and MONITOR_MCP_ENABLED.
  • kirschbaum-development/redactor 1.x is a hard dependency.

Removed

  • Monitor::controlled(), catching(), onUncaughtException(),
    withCircuitBreaker(), withDatabaseTransaction(), overrideContext(),
    addContext(), overrideTraceId() and NestedControlledBlockException.
  • Monitor::time(), LogTimer, Monitor::redactor(), the internal
    LogRedactor, OriginWrapper and the origin prefix, separator and wrapper
    settings.
  • The enabled, exception_trace, console_auto_trace, trace_header,
    circuit_breaker and redactor config keys.