361 lines
13 KiB
Python
361 lines
13 KiB
Python
|
#!/usr/bin/env nix-shell
|
||
|
#!nix-shell -i python3 -p "python3.withPackages(ps: [ps.aioprometheus ps.click ps.httpx ps.starlette ps.uvicorn])"
|
||
|
import asyncio
|
||
|
from contextlib import asynccontextmanager
|
||
|
import logging
|
||
|
from aioprometheus import Counter, Gauge
|
||
|
from aioprometheus.asgi.starlette import metrics
|
||
|
import click
|
||
|
import httpx
|
||
|
from starlette.applications import Starlette
|
||
|
from starlette.routing import Route
|
||
|
import uvicorn
|
||
|
|
||
|
|
||
|
up = Gauge("hydra_up", "Is Hydra running")
|
||
|
time = Gauge("hydra_time", "Hydra's current time")
|
||
|
uptime = Gauge("hydra_uptime", "Hydra's uptime")
|
||
|
|
||
|
builds_queued = Gauge("hydra_builds_queued", "Number of jobs in build queue")
|
||
|
steps_active = Gauge("hydra_steps_active", "Number of active steps in build queue")
|
||
|
steps_building = Gauge("hydra_steps_building", "Number of steps currently building")
|
||
|
steps_copying_to = Gauge(
|
||
|
"hydra_steps_copying_to", "Number of steps copying inputs to a worker"
|
||
|
)
|
||
|
steps_copying_from = Gauge(
|
||
|
"hydra_steps_copying_from", "Number of steps copying outputs from a worker"
|
||
|
)
|
||
|
steps_waiting = Gauge(
|
||
|
"hydra_steps_waiting", "Number of steps currently waiting for a worker slot"
|
||
|
)
|
||
|
steps_unsupported = Gauge(
|
||
|
"hydra_steps_unsupported", "Number of unsupported steps in build queue"
|
||
|
)
|
||
|
|
||
|
bytes_sent = Counter(
|
||
|
"hydra_build_inputs_sent_bytes_total",
|
||
|
"Total number of bytes copied to workers as build inputs",
|
||
|
)
|
||
|
bytes_received = Counter(
|
||
|
"hydra_build_outputs_received_bytes_total",
|
||
|
"Total number of bytes copied from workers as build outputs",
|
||
|
)
|
||
|
|
||
|
builds_read = Counter(
|
||
|
"hydra_builds_read_total",
|
||
|
"Total number of builds whose outputs have been copied from workers",
|
||
|
)
|
||
|
builds_read_seconds = Counter(
|
||
|
"hydra_builds_read_seconds_total",
|
||
|
"Total time spent copying build outputs, in seconds",
|
||
|
)
|
||
|
|
||
|
builds_done = Counter("hydra_builds_done_total", "Total number of builds completed")
|
||
|
steps_started = Counter("hydra_steps_started_total", "Total number of steps started")
|
||
|
steps_done = Counter("hydra_steps_done_total", "Total number of steps completed")
|
||
|
|
||
|
retries = Counter("hydra_retries_total", "Total number of retries")
|
||
|
max_retries = Gauge(
|
||
|
"hydra_max_retries", "Maximum observed number of retries for a single step"
|
||
|
)
|
||
|
|
||
|
queue_wakeups = Counter(
|
||
|
"hydra_queue_wakeup_total",
|
||
|
"Count of the times the queue runner has been notified of queue changes",
|
||
|
)
|
||
|
dispatcher_wakeups = Counter(
|
||
|
"hydra_dispatcher_wakeup_total",
|
||
|
"Count of the times the queue runner work dispatcher woke up due to new runnable builds and completed builds.",
|
||
|
)
|
||
|
|
||
|
dispatch_time = Counter(
|
||
|
"hydra_dispatch_execution_seconds_total",
|
||
|
"Total time the dispatcher has spent working, in seconds",
|
||
|
)
|
||
|
|
||
|
db_connections = Gauge("hydra_db_connections", "Number of connections to the database")
|
||
|
active_db_updates = Gauge("hydra_db_updates", "Number of in-progress database updates")
|
||
|
|
||
|
steps_queued = Gauge("hydra_steps_queued", "Number of steps in build queue")
|
||
|
steps_runnable = Gauge(
|
||
|
"hydra_steps_runnable", "Number of runnable steps in build queue"
|
||
|
)
|
||
|
|
||
|
step_time = Counter(
|
||
|
"hydra_step_time_total", "Total time spent executing steps, in seconds"
|
||
|
)
|
||
|
step_build_time = Counter(
|
||
|
"hydra_step_build_time_total", "Total time spent executing build steps, in seconds"
|
||
|
)
|
||
|
|
||
|
machine_enabled = Gauge("hydra_machine_enabled", "Whether machine is enabled")
|
||
|
machine_steps_done = Counter(
|
||
|
"hydra_machine_steps_done_total", "Total number of steps completed by this worker"
|
||
|
)
|
||
|
machine_current_jobs = Gauge(
|
||
|
"hydra_machine_current_jobs", "Number of jobs currently running on this worker"
|
||
|
)
|
||
|
machine_disabled_until = Gauge(
|
||
|
"hydra_machine_disabled_until",
|
||
|
"Timestamp of when this worker will next become active",
|
||
|
)
|
||
|
machine_last_failure = Gauge(
|
||
|
"hydra_machine_last_failure", "Timestamp of when a build last failed on this worker"
|
||
|
)
|
||
|
machine_consecutive_failures = Gauge(
|
||
|
"hydra_machine_consecutive_failures",
|
||
|
"Number of consecutive failed builds on this worker",
|
||
|
)
|
||
|
|
||
|
machine_idle_since = Gauge(
|
||
|
"hydra_machine_idle_since", "Timestamp of when this worker last had jobs running"
|
||
|
)
|
||
|
machine_step_time = Counter(
|
||
|
"hydra_machine_step_time_total",
|
||
|
"Total time this worker spent executing steps, in seconds",
|
||
|
)
|
||
|
machine_step_build_time = Counter(
|
||
|
"hydra_machine_step_build_time_total",
|
||
|
"Total time this worker spent executing build steps, in seconds",
|
||
|
)
|
||
|
|
||
|
jobset_time = Counter(
|
||
|
"hydra_jobset_seconds_total",
|
||
|
"Total time this jobset has been building for, in seconds",
|
||
|
)
|
||
|
jobset_shares_used = Gauge(
|
||
|
"hydra_jobset_shares_used", "Number of shares currently consumed by this jobset"
|
||
|
)
|
||
|
|
||
|
machine_type_runnable = Gauge(
|
||
|
"hydra_machine_type_runnable",
|
||
|
"Number of steps currently runnable on this machine type",
|
||
|
)
|
||
|
machine_type_running = Gauge(
|
||
|
"hydra_machine_type_running",
|
||
|
"Number of steps currently running on this machine type",
|
||
|
)
|
||
|
machine_type_wait_time = Counter(
|
||
|
"hydra_machine_type_wait_time_total",
|
||
|
"Total time spent waiting for a build slot of this machine type",
|
||
|
)
|
||
|
machine_type_last_active = Gauge(
|
||
|
"hydra_machine_type_last_active",
|
||
|
"Timestamp of when a machine of this type was last active",
|
||
|
)
|
||
|
|
||
|
store_nar_info_read = Counter(
|
||
|
"hydra_store_nar_info_read_total",
|
||
|
"Total number of narinfo files read from the remote store",
|
||
|
)
|
||
|
store_nar_info_read_averted = Counter(
|
||
|
"hydra_store_nar_info_read_averted_total",
|
||
|
"Total number of narinfo file reads averted (already loaded)",
|
||
|
)
|
||
|
store_nar_info_missing = Counter(
|
||
|
"hydra_store_nar_info_missing_total",
|
||
|
"Total number of narinfo files found to be missing",
|
||
|
)
|
||
|
store_nar_info_write = Counter(
|
||
|
"hydra_store_nar_info_write_total",
|
||
|
"Total number of narinfo files written to the remote store",
|
||
|
)
|
||
|
store_nar_info_cache_size = Gauge(
|
||
|
"hydra_store_nar_info_cache_size",
|
||
|
"Size of the in-memory store path information cache",
|
||
|
)
|
||
|
store_nar_read = Counter(
|
||
|
"hydra_store_nar_read_total", "Total number of NAR files read from the remote store"
|
||
|
)
|
||
|
store_nar_read_bytes = Counter(
|
||
|
"hydra_store_nar_read_bytes_total",
|
||
|
"Total number of NAR file bytes read from the remote store (uncompressed)",
|
||
|
)
|
||
|
store_nar_read_compressed_bytes = Counter(
|
||
|
"hydra_store_nar_read_compressed_bytes_total",
|
||
|
"Total number of NAR file bytes read from the remote store (compressed)",
|
||
|
)
|
||
|
store_nar_write = Counter(
|
||
|
"hydra_store_nar_write_total",
|
||
|
"Total number of NAR files written to the remote store",
|
||
|
)
|
||
|
store_nar_write_averted = Counter(
|
||
|
"hydra_store_nar_write_averted_total",
|
||
|
"Total number of NAR file writes averted (already exists on remote)",
|
||
|
)
|
||
|
store_nar_write_bytes = Counter(
|
||
|
"hydra_store_nar_write_bytes_total",
|
||
|
"Total number of NAR file bytes written to the remote store (uncompressed)",
|
||
|
)
|
||
|
store_nar_write_compressed_bytes = Counter(
|
||
|
"hydra_store_nar_write_compressed_bytes_total",
|
||
|
"Total number of NAR file bytes written to the remote store (compressed)",
|
||
|
)
|
||
|
store_nar_write_compression_seconds = Counter(
|
||
|
"hydra_store_nar_write_compression_seconds_total",
|
||
|
"Total time spent compressing NAR files for writing to the remote store",
|
||
|
)
|
||
|
|
||
|
store_s3_put = Counter(
|
||
|
"hydra_store_s3_put_total", "Total number of PUT requests to S3 store"
|
||
|
)
|
||
|
store_s3_put_bytes = Counter(
|
||
|
"hydra_store_s3_put_bytes_total", "Total number of bytes written to S3 store"
|
||
|
)
|
||
|
store_s3_put_seconds = Counter(
|
||
|
"hydra_store_s3_put_seconds_total",
|
||
|
"Total time spent writing to S3 store, in seconds",
|
||
|
)
|
||
|
store_s3_get = Counter(
|
||
|
"hydra_store_s3_get_total", "Total number of GET requests to S3 store"
|
||
|
)
|
||
|
store_s3_get_bytes = Counter(
|
||
|
"hydra_store_s3_get_bytes_total", "Total number of bytes read from S3 store"
|
||
|
)
|
||
|
store_s3_get_seconds = Counter(
|
||
|
"hydra_store_s3_get_seconds_total",
|
||
|
"Total time spent reading from S3 store, in seconds",
|
||
|
)
|
||
|
store_s3_head = Counter(
|
||
|
"hydra_store_s3_head_total", "Total number of HEAD requests to S3 store"
|
||
|
)
|
||
|
|
||
|
|
||
|
def update_metrics(status):
|
||
|
up.set({}, int(status["status"] == "up"))
|
||
|
time.set({}, status["time"])
|
||
|
uptime.set({}, status["uptime"])
|
||
|
|
||
|
builds_queued.set({}, status["nrQueuedBuilds"])
|
||
|
steps_active.set({}, status["nrActiveSteps"])
|
||
|
steps_building.set({}, status["nrStepsBuilding"])
|
||
|
steps_copying_to.set({}, status["nrStepsCopyingTo"])
|
||
|
steps_copying_from.set({}, status["nrStepsCopyingFrom"])
|
||
|
steps_waiting.set({}, status["nrStepsWaiting"])
|
||
|
steps_unsupported.set({}, status["nrUnsupportedSteps"])
|
||
|
|
||
|
bytes_sent.set({}, status["bytesSent"])
|
||
|
bytes_received.set({}, status["bytesReceived"])
|
||
|
|
||
|
builds_read.set({}, status["nrBuildsRead"])
|
||
|
builds_read_seconds.set({}, status["buildReadTimeMs"] / 1000)
|
||
|
|
||
|
builds_done.set({}, status["nrBuildsDone"])
|
||
|
steps_started.set({}, status["nrStepsStarted"])
|
||
|
steps_done.set({}, status["nrStepsDone"])
|
||
|
|
||
|
retries.set({}, status["nrRetries"])
|
||
|
max_retries.set({}, status["maxNrRetries"])
|
||
|
|
||
|
queue_wakeups.set({}, status["nrQueueWakeups"])
|
||
|
dispatcher_wakeups.set({}, status["nrDispatcherWakeups"])
|
||
|
dispatch_time.set({}, status["dispatchTimeMs"] / 1000)
|
||
|
|
||
|
db_connections.set({}, status["nrDbConnections"])
|
||
|
active_db_updates.set({}, status["nrActiveDbUpdates"])
|
||
|
|
||
|
steps_queued.set({}, status["nrUnfinishedSteps"])
|
||
|
steps_runnable.set({}, status["nrRunnableSteps"])
|
||
|
|
||
|
if st := status.get("totalStepTime"):
|
||
|
step_time.set({}, st)
|
||
|
|
||
|
if sbt := status.get("totalStepBuildTime"):
|
||
|
step_build_time.set({}, sbt)
|
||
|
|
||
|
for machine_name, machine_status in status["machines"].items():
|
||
|
labels = {"host": machine_name}
|
||
|
machine_enabled.set(labels, int(machine_status["enabled"]))
|
||
|
machine_steps_done.set(labels, machine_status["nrStepsDone"])
|
||
|
machine_current_jobs.set(labels, machine_status["currentJobs"])
|
||
|
machine_disabled_until.set(labels, machine_status["disabledUntil"])
|
||
|
machine_last_failure.set(labels, machine_status["lastFailure"])
|
||
|
machine_consecutive_failures.set(labels, machine_status["consecutiveFailures"])
|
||
|
|
||
|
if isn := machine_status.get("idleSince"):
|
||
|
machine_idle_since.set(labels, isn)
|
||
|
|
||
|
if st := machine_status.get("totalStepTime"):
|
||
|
machine_step_time.set(labels, st)
|
||
|
|
||
|
if sbt := machine_status.get("totalStepBuildTime"):
|
||
|
machine_step_build_time.set(labels, sbt)
|
||
|
|
||
|
for jobset_name, jobset_status in status["jobsets"].items():
|
||
|
labels = {"name": jobset_name}
|
||
|
jobset_time.set(labels, jobset_status["seconds"])
|
||
|
jobset_shares_used.set(labels, jobset_status["shareUsed"])
|
||
|
|
||
|
for type_name, type_status in status["machineTypes"].items():
|
||
|
labels = {"machineType": type_name}
|
||
|
machine_type_runnable.set(labels, type_status["runnable"])
|
||
|
machine_type_running.set(labels, type_status["running"])
|
||
|
|
||
|
if wt := type_status.get("waitTime"):
|
||
|
machine_type_wait_time.set(labels, wt)
|
||
|
|
||
|
if la := type_status.get("lastActive"):
|
||
|
machine_type_last_active.set(labels, la)
|
||
|
|
||
|
store = status["store"]
|
||
|
store_nar_info_read.set({}, store["narInfoRead"])
|
||
|
store_nar_info_read_averted.set({}, store["narInfoReadAverted"])
|
||
|
store_nar_info_missing.set({}, store["narInfoMissing"])
|
||
|
store_nar_info_write.set({}, store["narInfoWrite"])
|
||
|
store_nar_info_cache_size.set({}, store["narInfoCacheSize"])
|
||
|
store_nar_read.set({}, store["narRead"])
|
||
|
store_nar_read_bytes.set({}, store["narReadBytes"])
|
||
|
store_nar_read_compressed_bytes.set({}, store["narReadCompressedBytes"])
|
||
|
store_nar_write.set({}, store["narWrite"])
|
||
|
store_nar_write_averted.set({}, store["narWriteAverted"])
|
||
|
store_nar_write_bytes.set({}, store["narWriteBytes"])
|
||
|
store_nar_write_compressed_bytes.set({}, store["narWriteCompressedBytes"])
|
||
|
store_nar_write_compression_seconds.set(
|
||
|
{}, store["narWriteCompressionTimeMs"] / 1000
|
||
|
)
|
||
|
|
||
|
if s3 := status.get("s3"):
|
||
|
store_s3_put.set({}, s3["put"])
|
||
|
store_s3_put_bytes.set({}, s3["putBytes"])
|
||
|
store_s3_put_seconds.set({}, s3["putTimeMs"] / 1000)
|
||
|
store_s3_get.set({}, s3["get"])
|
||
|
store_s3_get_bytes.set({}, s3["getBytes"])
|
||
|
store_s3_get_seconds.set({}, s3["getTimeMs"] / 1000)
|
||
|
store_s3_head.set({}, s3["head"])
|
||
|
|
||
|
|
||
|
async def update_metrics_loop(hydra_url, scrape_interval):
|
||
|
async with httpx.AsyncClient(base_url=hydra_url) as client:
|
||
|
while True:
|
||
|
try:
|
||
|
response = await client.get(
|
||
|
"/queue-runner-status",
|
||
|
headers={"Content-Type": "application/json"},
|
||
|
)
|
||
|
|
||
|
update_metrics(response.json())
|
||
|
|
||
|
await asyncio.sleep(scrape_interval)
|
||
|
except Exception as ex:
|
||
|
logging.exception("Failed to update metrics", exc_info=ex)
|
||
|
|
||
|
|
||
|
@click.command()
|
||
|
@click.option("--hydra-url", default="https://hydra.forkos.org/")
|
||
|
@click.option("--port", default=9200)
|
||
|
@click.option("--scrape-interval", default=15)
|
||
|
def main(hydra_url, port, scrape_interval):
|
||
|
@asynccontextmanager
|
||
|
async def lifespan(_):
|
||
|
loop = asyncio.get_event_loop()
|
||
|
loop.create_task(update_metrics_loop(hydra_url, scrape_interval))
|
||
|
yield
|
||
|
|
||
|
app = Starlette(routes=[Route("/metrics", metrics)], lifespan=lifespan)
|
||
|
|
||
|
uvicorn.run(app, port=port, log_level="info")
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main()
|