Monitor history: per-check logging, daily aggregation, and detail UI (issue #19)#68
Merged
Merged
Conversation
…te mismatch
Blockers:
- Migrations now create monitor_id as unsigned INT to match monitors.id
(increments). foreignId() produced BIGINT and aborted `migrate` on MySQL
with error 3780. Verified up/down on MySQL.
- Daily metrics are now read back under the same server-side timezone they
are aggregated with. The detail page previously queried metrics using the
browser timezone while the scheduler only ever wrote UTC rows, so heatmaps
rendered empty for every non-UTC user. Added config('monitor-history.timezone')
as the single source of truth for both the aggregate command and the controller.
Correctness:
- Idempotency key reduced to monitor_id + check_type + status + checked_at(second),
per the plan, so a quick command retry of the same check de-dupes instead of the
previous message/metadata-sensitive key that never collapsed retries.
- Recent checks now respect the selected date range (was always the global newest 50).
- show() payload now advertises each check type with an `enabled` flag; the UI
renders an explicit "not enabled" panel instead of omitting domain or showing a
permanently empty certificate heatmap.
- Domain "expires today" message is reachable again (diffInDays float cast to int).
- Heatmap legend now reflects the real cell color scale (adds warning/unknown).
- Custom-range inputs stay in sync with the range the server actually applied.
Tests:
- Configure the suite against a dedicated monitor_test MySQL database (with a guard
test) so RefreshDatabase exercises the real engine and the FK regression is caught.
- Add feature/unit coverage for log persistence + idempotency, aggregation math,
the show payload (auth, timezone, check_types, recent-range), and domain outcome
mapping. Full suite: 32 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seeds 4 demo monitors with distinct health profiles (healthy, flaky,
expiring-domain, recovered-outage) and fabricates multi-day uptime/domain/
certificate check logs, then aggregates them into daily metrics so the
monitor detail page heatmaps, tooltips, and totals have realistic data to
render locally.
- Run: php artisan db:seed --class=MonitorHistorySeeder
- Window length via config('monitor-history.seed_days') / MONITOR_HISTORY_SEED_DAYS (default 90)
- Re-runnable (updateOrCreate by URL, clears prior seeded history)
- Guarded against running in production
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…itch
MONITOR_HISTORY_ENABLED was read-path only, so "default off" still ran
per-check ingestion and the hourly aggregate / daily prune jobs. Plus
follow-up correctness and clarity fixes from the code review.
Feature flag (blocker):
- Add MonitorCheckLogService::logCheckIfEnabled(); all automatic per-check
ingestion (uptime hooks, certificate listeners, domain verification) now
no-ops when the flag is off. logCheck() stays unconditional for operator
paths (backfill, seeder) so history can still be pre-staged.
- Gate monitor:aggregate-check-metrics and monitor:prune-check-history behind
->when(config('monitor-history.enabled')) and add withoutOverlapping().
Core uptime/certificate/domain checks remain ungated.
Correctness / clarity:
- Extract DomainService::resolveExpirationNotifications() and cover the
expiry-warning thresholds with tests (locks the whole-day int behaviour).
- Reword monitor:backfill-check-history so it no longer implies multi-day
history: it writes one current-state snapshot per enabled check type and
--days only sizes the aggregation window. Locked with a test.
- Heatmap legend now reflects every colour getCellClasses() emits (green/red
gradients + yellow), not a single swatch per status.
Docs / hygiene:
- Document the flag semantics, a revised rollout checklist, and the dedicated
monitor_test MySQL database the suite requires.
- Untrack the accidentally committed .phpunit.cache artifact and gitignore it.
Tests: 48 passed (was 34). Pint clean. Frontend build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements monitor history (issue #19): every uptime/domain/certificate check is logged, rolled up into daily metrics, and surfaced on a new monitor detail page with GitHub-style calendar heatmaps, per-day tooltips, range filters, and totals.
The whole feature is gated behind a
MONITOR_HISTORY_ENABLEDflag (default off), so it can be merged and rolled out independently.What's included
Data layer (Phase 1)
monitor_check_logs(raw per-check rows) +monitor_daily_check_metrics(pre-aggregated) migrations, models, relations, and query scopes.Ingestion (Phase 2)
Monitor::uptimeRequestSucceeded/Failedto log one row per attempt.CertificateCheckSucceeded/Failedlisteners.DomainService::verifyDomainExpiration()returns a normalized DTO and logs every run.MonitorCheckLogServicewrites viafirstOrCreateon an idempotency key.Aggregation + API (Phase 3)
MonitorDailyCheckMetricsAggregator+monitor:aggregate-check-metricscommand (incremental, scheduled hourly).MonitorsController@showexposes daily heatmap points, all-time + selected-range totals, and recent checks.Detail UI (Phase 4)
Monitors/Show.jsx, reusableMonitorHistoryHeatmap, summary cards, range filters (7d/30d/all/custom), recent-checks table.Ops (Phase 5)
monitor:backfill-check-historyandmonitor:prune-check-historycommands, scheduler entries, anddocs/monitor-history.md.Related
plans/issue-19-monitor-history-implementation-plan.md✅ Fixed (commit
8b17248)Reviewed the implementation against the plan's acceptance criteria / Definition of Done and fixed:
Blockers
monitor_idnow created as unsignedINTto matchmonitors.id(increments());foreignId()producedBIGINTand abortedmigratewith SQLSTATE 3780. Verified up and down on MySQL.config('monitor-history.timezone'), single source of truth for both the command and the controller). The detail page no longer trusts the browser timezone.Correctness
monitor_id + check_type + status + checked_at(second)(per the plan), so a quick command retry of the same check de-dupes instead of the old message/metadata-sensitive key that never collapsed retries.show()advertises each check type with anenabledflag; the UI renders an explicit "not enabled" panel instead of omitting domain or showing a permanently empty certificate heatmap.diffInDaysfloat cast to int).Tests (Definition of Done #4)
monitor_testMySQL database (with a guard test) soRefreshDatabaseexercises the real engine and the FK regression is caught.showpayload (auth, timezone,check_types, recent-range), and domain outcome mapping. 32 passed.🔭 Deferred (follow-ups, not blocking)
response_time_msfor uptime stays null — Spatie's Guzzle flow doesn't expose transfer timing to the override; populating it needs Guzzleon_statsmiddleware. avg/p95 remain null-safe meanwhile.monthrange preset; aggregator chunking for very large windows; "history starts on " messaging.🤖 Generated with Claude Code