packages feed

shibuya-metrics-0.11.0.0: src/Shibuya/Metrics.hs

-- | Metrics web server for Shibuya queue processing framework.
--
-- This module provides HTTP/JSON, Prometheus, and WebSocket endpoints
-- for exposing Shibuya processor metrics.
--
-- == Quick Start
--
-- @
-- import Shibuya.App (runApp, AppHandle, getAppMaster)
-- import Shibuya.Metrics
--
-- main :: IO ()
-- main = do
--   -- Start your Shibuya app...
--   Right appHandle <- runEff $ runApp ...
--
--   -- Start metrics server on port 9090
--   withMetricsServer defaultConfig (getAppMaster appHandle) $ \\server -> do
--     putStrLn $ "Metrics server running on port " ++ show server.serverPort
--     -- Your app logic here...
-- @
--
-- == Endpoints
--
-- * @GET /metrics@ - JSON metrics for all processors
-- * @GET /metrics/:id@ - JSON metrics for a specific processor
-- * @GET /health@ - Detailed health status (for debugging)
-- * @GET /health/live@ - Liveness probe (Kubernetes)
-- * @GET /health/ready@ - Readiness probe (Kubernetes)
-- * @GET /metrics/prometheus@ - Prometheus-format metrics
-- * @WS /ws@ - WebSocket for real-time updates
--
-- == Latency and in-flight detail
--
-- Opt in through @AppConfig.metricsDetail = MetricsDetailed defaultDetailOptions@.
-- JSON and WebSocket frames gain optional @latency@ (count, sumSeconds, finite
-- cumulative buckets) and @oldestInFlight@ (messageId, startedAt, ageSeconds).
-- Timing includes finalization retries once per attempt or emitted batch.
-- Prometheus exposes @shibuya_message_processing_seconds@ and the optional
-- @shibuya_processor_oldest_in_flight_seconds@ gauge. Age-only changes do not
-- emit WebSocket deltas: clients extrapolate from their last received frame.
-- HTTP reads and snapshots return freshly sampled age. Basic accounting remains
-- the default and omits these members.
--
-- == Cross-origin access
--
-- Set 'corsAllowedOrigins' to explicit serialized browser origins, for example
-- @defaultConfig {corsAllowedOrigins = ["https://ops.example.com"]}@. The empty
-- default preserves read-route headers and WebSocket upgrade behavior. Wildcards,
-- whitespace, paths, queries and fragments are refused by the built-in server.
-- Matching is exact, without case folding or default-port normalization.
-- Allowed HTTP responses (including errors) gain @Access-Control-Allow-Origin@;
-- enabled responses merge @Vary: Origin@ even for absent or refused origins.
-- Preflight @OPTIONS@ requests allow @GET, POST, OPTIONS@ and valid requested
-- header tokens (or @Content-Type@), returning empty 204 responses with a 600-second
-- max age and variation by requested method and headers. Credentials are never granted.
-- Disallowed WebSocket Origins are refused before acquiring a connection slot;
-- headerless clients remain supported. This policy is not authentication. Use a
-- trusted network or an authenticating reverse proxy. Hosts mounting 'combinedApp'
-- directly must enforce the documented configuration constraints themselves.
-- Same-origin reads need no origin list; browser control POSTs must explicitly
-- allow their page origin, even in same-origin deployments.
--
-- == HTTP errors
--
-- Error bodies contain a human-readable @error@, a stable @code@, and optional
-- context. Unknown paths return 404 @not_found@; unknown @/metrics/:id@ processors
-- return 404 @processor_not_found@ with @processor@; plain HTTP @/ws@ returns 404
-- @websocket_upgrade_required@ when WebSockets are enabled. Preflight and upgrade
-- origin refusals return 403 @origin_not_allowed@. Unsupported preflight methods
-- return 403 @cors_method_not_allowed@; invalid requested header lists return 403
-- @cors_headers_not_allowed@. Existing error members, statuses and content types
-- are preserved, and @code@ is an additive member. 'errorResponse' protects its
-- reserved @error@ and @code@ members from overrides by context.
--
-- == WebSocket Protocol
--
-- Clients can send:
--
-- * @{"type": "subscribe_all"}@ - Subscribe to all processor updates
-- * @{"type": "subscribe", "processors": ["id1", "id2"]}@ - Subscribe to specific processors
-- * @{"type": "unsubscribe", "processors": ["id1"]}@ - Unsubscribe from processors
-- * @{"type": "ping"}@ - Keepalive ping
--
-- Server sends:
--
-- * @{"type": "snapshot", "metrics": {...}}@ - Full metrics snapshot
-- * @{"type": "update", "processor": "id", "metrics": {...}}@ - Single processor update
-- * @{"type": "terminal", "processor": "id", "status": "stopped"}@ - Processor left the live registry
-- * @{"type": "pong"}@ - Response to ping
-- * @{"type": "goodbye"}@ - Server shutting down
-- * @{"type": "error", "code": "invalid_message", "message": "..."}@ - Invalid client JSON/tag
--
-- Error codes are @invalid_message@ (receive continues), @subscription_limit_exceeded@
-- (error before close 1008), and @queue_overflow@ (immediately followed by a current
-- snapshot). Subscribe and unsubscribe changes establish ordered snapshot boundaries.
-- One writer drains a queue bounded by 'wsMaxQueuedFrames' (64, minimum 2) and
-- 'wsMaxQueuedBytes' (1 MiB). All queued frames count, including snapshots, errors,
-- terminal events and goodbye. Oldest expendable updates/pongs may be dropped;
-- required-frame saturation or oversized snapshots close with 1013. Reconnect and
-- read a fresh snapshot on close; metrics have no replay cursor.
-- 'wsSendTimeoutUs' bounds sends and the whole close flush (5 seconds). Responsive
-- shutdown peers receive goodbye; stalled peers may time out without it. At most
-- one popped frame is outside the queue; transient encoding and the entire process
-- heap are not covered by the encoded-payload budget. Closed decoders must add the
-- new @error@ tag; raw JSON consumers may ignore unknown frame types.
-- See @docs/architecture/METRICS.md@ for the complete shared-convention mapping.
-- Detailed processor metrics also carry optional acknowledged cursor progress: latest,
-- bounded partitions and sticky truncation, with lossless tagged cursor values.
-- Paused intake is reported with live in-flight counts and the existing last-progress
-- clock; drained pauses remain ready and stuck in-flight pauses do not.
-- Control routes (enableJSON required): GET /control discovers enabled/operations;
-- POST /control/processors/<id>/pause or /resume returns atomic intake intent.
-- enableControl defaults False: actions return 403 control_disabled without mutation.
-- Success is {processor,state:"running"} or {processor,state:"paused",pausedAt}.
-- Actual POST Origin must be explicitly allowed, even when CORS is disabled.
-- Refusals: 403 origin_not_allowed; 404 processor_not_found/not_found;
-- 409 processor_terminal/processor_not_controllable/processor_draining;
-- 405 method_not_allowed with Allow GET or POST. Lifecycle refusals include processor.
-- Control adds no authentication or WebSocket mutation frames.
module Shibuya.Metrics
  ( -- * Server Lifecycle
    startMetricsServer,
    startMetricsServerWithDeps,
    stopMetricsServer,
    withMetricsServer,
    combinedApp,

    -- * Configuration
    MetricsServerConfig (..),
    defaultConfig,

    -- * Host Application Helpers
    corsMiddleware,
    errorResponse,

    -- * Server Handle
    MetricsServer (..),

    -- * Health Check Types
    DependencyCheck,
    DependencyStatus (..),
    LivenessStatus (..),
    ReadinessStatus (..),
    ApplicationStatus (..),
    ProcessorHealth (..),
    HealthConfig (..),
    defaultHealthConfig,

    -- * WebSocket Protocol Types
    ClientMessage (..),
    ProcessorTerminalStatus (..),
    ServerMessage (..),
  )
where

import Shibuya.Metrics.Config (MetricsServerConfig (..), defaultConfig)
import Shibuya.Metrics.Cors (corsMiddleware)
import Shibuya.Metrics.Error (errorResponse)
import Shibuya.Metrics.Health
  ( ApplicationStatus (..),
    DependencyCheck,
    DependencyStatus (..),
    HealthConfig (..),
    LivenessStatus (..),
    ProcessorHealth (..),
    ReadinessStatus (..),
    defaultHealthConfig,
  )
import Shibuya.Metrics.Server (combinedApp, startMetricsServer, startMetricsServerWithDeps, stopMetricsServer, withMetricsServer)
import Shibuya.Metrics.Types (ClientMessage (..), MetricsServer (..), ProcessorTerminalStatus (..), ServerMessage (..))