defmodule SyntropyWeb.Telemetry do @moduledoc """ Telemetry supervisor: periodic VM measurements plus the in-process Prometheus reporter backing `GET /metrics`. Metrics cover the BEAM (memory, run queues), Phoenix endpoint latency, Ecto query timing (only emitted when persistence is enabled), and the custom runtime events defined in `Syntropy.Telemetry` — task runs, LLM requests and token usage, webhook deliveries, and event recorder batch flushes. """ use Supervisor import Telemetry.Metrics @reporter :syntropy_prometheus_metrics @duration_buckets_ms [ 10, 50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000, 30_000, 60_000, 120_000 ] @spec start_link(keyword()) :: Supervisor.on_start() def start_link(opts) do Supervisor.start_link(__MODULE__, opts, name: __MODULE__) end @impl true def init(_opts) do children = [ # telemetry_poller also emits its default VM measurements # ([:vm, :memory], [:vm, :total_run_queue_lengths], ...) on this period. {:telemetry_poller, measurements: [], period: 10_000, name: :syntropy_telemetry_poller}, {TelemetryMetricsPrometheus.Core, metrics: metrics(), name: @reporter} ] Supervisor.init(children, strategy: :one_for_one) end @doc """ Renders the current metric values in Prometheus text exposition format. """ @spec scrape() :: String.t() def scrape do TelemetryMetricsPrometheus.Core.scrape(@reporter) end @spec metrics() :: [Telemetry.Metrics.t()] def metrics do [ # BEAM last_value("vm.memory.total", unit: :byte), last_value("vm.total_run_queue_lengths.total"), last_value("vm.total_run_queue_lengths.cpu"), last_value("vm.total_run_queue_lengths.io"), # Phoenix distribution("phoenix.endpoint.stop.duration", unit: {:native, :millisecond}, reporter_options: [buckets: @duration_buckets_ms] ), # Ecto (no-op unless persistence is enabled) distribution("syntropy.repo.query.total_time", unit: {:native, :millisecond}, reporter_options: [buckets: @duration_buckets_ms] ), # Task runs. The histogram carries run count (by status/strategy), # total duration, and latency buckets. distribution("syntropy.task_run.stop.duration_ms", tags: [:status, :strategy], reporter_options: [buckets: @duration_buckets_ms], description: "Terminal run duration by status and strategy" ), sum("syntropy.task_run.stop.agent_count", tags: [:status], description: "Total agents dispatched across terminal runs" ), # LLM requests distribution("syntropy.llm_request.stop.duration_ms", tags: [:provider, :status], reporter_options: [buckets: @duration_buckets_ms], description: "LLM completion call latency by provider and outcome" ), sum("syntropy.llm_request.tokens.input_tokens", tags: [:provider]), sum("syntropy.llm_request.tokens.output_tokens", tags: [:provider]), sum("syntropy.llm_request.tokens.total_tokens", tags: [:provider]), # Webhook deliveries counter("syntropy.webhook_delivery.stop.count", tags: [:status], description: "Webhook delivery count by final status" ), sum("syntropy.webhook_delivery.stop.attempts", tags: [:status], description: "Total delivery attempts (including retries)" ), # Event recorder. The histogram carries flush count and total flush time. sum("syntropy.event_recorder.flush.batch_size", description: "Total events flushed to Postgres" ), distribution("syntropy.event_recorder.flush.duration_ms", reporter_options: [buckets: [1, 5, 10, 25, 50, 100, 250, 500, 1_000]] ) ] end end