diff --git a/flake.lock b/flake.lock index d7614da..dc4d564 100644 --- a/flake.lock +++ b/flake.lock @@ -1127,11 +1127,11 @@ "tailwindcss-animated-src": "tailwindcss-animated-src_2" }, "locked": { - "lastModified": 1789738727, - "narHash": "sha256-s8bdFNdlR5q48WC0cx8sZuHNHnqqVYzrgEpkv5tRvxE=", + "lastModified": 1790081384, + "narHash": "sha256-iT3jHIxVZVTA/BpbxPjqoqZlTutv87c1Bj+31QS3b7Y=", "ref": "sv-fe", - "rev": "164d970eb8aac7a1c19c3b5e8eb058fb3516d720", - "revCount": 4144, + "rev": "5567c7afd7b955be90e0a329aaa1d236b604d3e6", + "revCount": 4197, "type": "git", "url": "https://tangled.org/tangled.org/core" }, diff --git a/hosts/mirror-fsn/configuration.nix b/hosts/mirror-fsn/configuration.nix index b150cd5..4915438 100644 --- a/hosts/mirror-fsn/configuration.nix +++ b/hosts/mirror-fsn/configuration.nix @@ -2,6 +2,6 @@ imports = [./disk-config.nix]; networking.hostName = "mirror-fsn"; networking.enableIPv6 = false; - networking.firewall.interfaces."tailscale0".allowedTCPPorts = [ 7100 6060 6070 ]; + networking.firewall.interfaces."tailscale0".allowedTCPPorts = [ 7100 9091 6060 6070 ]; system.stateVersion = "25.05"; } diff --git a/hosts/mirror-fsn/services/knotmirror.nix b/hosts/mirror-fsn/services/knotmirror.nix index a1eeee8..dbfc370 100644 --- a/hosts/mirror-fsn/services/knotmirror.nix +++ b/hosts/mirror-fsn/services/knotmirror.nix @@ -30,6 +30,7 @@ after = ["postgresql.service"]; environment.MIRROR_METRICS_LISTEN = "0.0.0.0:7100"; }; + systemd.services.gitmirror.environment.GITMIRROR_METRICS_BIND = "0.0.0.0:9091"; services.redis.servers.knotmirror.settings = { maxmemory = "8gb"; maxmemory-policy = "allkeys-lru"; diff --git a/hosts/prometheus-hel/dashboards/gitmirror.json b/hosts/prometheus-hel/dashboards/gitmirror.json new file mode 100644 index 0000000..9af3208 --- /dev/null +++ b/hosts/prometheus-hel/dashboards/gitmirror.json @@ -0,0 +1,975 @@ +{ + "uid": "gitmirror", + "title": "gitmirror", + "description": "gitmirror XRPC traffic, latency, blocking-pool saturation, and per-repository detail.", + "tags": [ + "gitmirror", + "git" + ], + "schemaVersion": 38, + "refresh": "30s", + "time": { + "from": "now-6h", + "to": "now" + }, + "templating": { + "list": [ + { + "name": "slow_ms", + "label": "slow threshold (ms)", + "description": "Requests slower than this appear in the per-repo slow requests panel.", + "type": "textbox", + "query": "1000", + "current": { + "selected": true, + "text": "1000", + "value": "1000" + }, + "options": [ + { + "selected": true, + "text": "1000", + "value": "1000" + } + ], + "skipUrlSync": false + }, + { + "name": "search", + "label": "log search", + "description": "Case-insensitive text filter for the per-repo log panels. Paste a repo DID to follow one repository.", + "type": "textbox", + "query": "", + "current": { + "selected": true, + "text": "", + "value": "" + }, + "options": [ + { + "selected": true, + "text": "", + "value": "" + } + ], + "skipUrlSync": false + } + ] + }, + "panels": [ + { + "type": "row", + "title": "health", + "collapsed": false, + "panels": [], + "id": 2, + "gridPos": { + "x": 0, + "y": 0, + "w": 24, + "h": 1 + } + }, + { + "type": "stat", + "title": "metrics target reachable", + "targets": [ + { + "refId": "A", + "expr": "up{job=\"gitmirror\"}" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 1 + } + ] + }, + "unit": "percentunit", + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "textMode": "value", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + } + }, + "description": "Whether Prometheus can currently reach gitmirror's metrics endpoint. Zero means the process is down, the port moved, or the tailscale route broke \u2014 every other panel is stale while this is zero.", + "id": 3, + "gridPos": { + "x": 0, + "y": 1, + "w": 4, + "h": 4 + } + }, + { + "type": "stat", + "title": "requests / s", + "targets": [ + { + "refId": "A", + "expr": "sum(rate(gitmirror_xrpc_requests_total{job=\"gitmirror\"}[5m]))" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + }, + "unit": "reqps", + "decimals": 2 + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "textMode": "value", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + } + }, + "description": "XRPC requests per second across every method, averaged over the last five minutes.", + "id": 4, + "gridPos": { + "x": 4, + "y": 1, + "w": 4, + "h": 4 + } + }, + { + "type": "stat", + "title": "server error rate", + "targets": [ + { + "refId": "A", + "expr": "(sum(rate(gitmirror_xrpc_requests_total{job=\"gitmirror\",status=~\"5..\"}[5m])) or vector(0)) / clamp_min(sum(rate(gitmirror_xrpc_requests_total{job=\"gitmirror\"}[5m])), 0.0001)" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 0.01 + }, + { + "color": "red", + "value": 0.05 + } + ] + }, + "unit": "percentunit", + "decimals": 1 + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "textMode": "value", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + } + }, + "description": "Share of requests gitmirror itself failed, over the last five minutes. Requests rejected because the caller asked for something missing or was not authorised are 4xx and are deliberately not counted here.", + "id": 5, + "gridPos": { + "x": 8, + "y": 1, + "w": 4, + "h": 4 + } + }, + { + "type": "stat", + "title": "requests in flight", + "targets": [ + { + "refId": "A", + "expr": "sum(gitmirror_xrpc_in_flight{job=\"gitmirror\"})" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + }, + "unit": "short", + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "textMode": "value", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + } + }, + "description": "Requests being handled at this instant. A number that stays high while the request rate is flat means requests are taking longer to finish than they are arriving.", + "id": 6, + "gridPos": { + "x": 12, + "y": 1, + "w": 4, + "h": 4 + } + }, + { + "type": "stat", + "title": "blocking threads busy", + "targets": [ + { + "refId": "A", + "expr": "sum(gitmirror_blocking_running{job=\"gitmirror\"})" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + }, + "unit": "short", + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "textMode": "value", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + } + }, + "description": "Threads in the blocking pool currently occupied by git work or by a push to a knot. The pool holds 512 threads; this is how many of them are in use.", + "id": 7, + "gridPos": { + "x": 16, + "y": 1, + "w": 4, + "h": 4 + } + }, + { + "type": "stat", + "title": "tasks waiting for a thread", + "targets": [ + { + "refId": "A", + "expr": "sum(gitmirror_blocking_queued{job=\"gitmirror\"})" + } + ], + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 1 + }, + { + "color": "red", + "value": 16 + } + ] + }, + "unit": "short", + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "colorMode": "value", + "graphMode": "area", + "textMode": "value", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + } + }, + "description": "Git tasks that have been handed to the blocking pool but have not started yet, because every thread is busy. Anything above zero for a sustained period means the pool is the bottleneck, not git.", + "id": 8, + "gridPos": { + "x": 20, + "y": 1, + "w": 4, + "h": 4 + } + }, + { + "type": "row", + "title": "xrpc traffic", + "collapsed": false, + "panels": [], + "id": 9, + "gridPos": { + "x": 0, + "y": 5, + "w": 24, + "h": 1 + } + }, + { + "type": "timeseries", + "title": "requests / s by method", + "targets": [ + { + "refId": "A", + "expr": "sum by (path) (rate(gitmirror_xrpc_requests_total{job=\"gitmirror\"}[5m]))", + "legendFormat": "{{path}}" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 8, + "lineWidth": 1, + "showPoints": "never" + }, + "min": 0, + "unit": "reqps", + "decimals": 2 + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "calcs": [ + "lastNotNull", + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "id": 10, + "gridPos": { + "x": 0, + "y": 6, + "w": 8, + "h": 7 + }, + "description": "Request rate for each XRPC method. Requests that matched no route are grouped as \"unknown\"." + }, + { + "type": "timeseries", + "title": "requests / s by status", + "targets": [ + { + "refId": "A", + "expr": "sum by (status) (rate(gitmirror_xrpc_requests_total{job=\"gitmirror\"}[5m]))", + "legendFormat": "{{status}}" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 8, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "mode": "normal", + "group": "A" + } + }, + "min": 0, + "unit": "reqps", + "decimals": 2 + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "calcs": [ + "lastNotNull", + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "id": 11, + "gridPos": { + "x": 8, + "y": 6, + "w": 8, + "h": 7 + }, + "description": "Request rate split by HTTP status. 2xx worked, 4xx means the caller asked for something gitmirror could not serve, 5xx means gitmirror failed." + }, + { + "type": "timeseries", + "title": "failures / s by cause", + "targets": [ + { + "refId": "A", + "expr": "sum by (error) (rate(gitmirror_xrpc_requests_total{job=\"gitmirror\",error!=\"none\"}[5m]))", + "legendFormat": "{{error}}" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 8, + "lineWidth": 1, + "showPoints": "never" + }, + "min": 0, + "unit": "reqps", + "decimals": 2 + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "calcs": [ + "lastNotNull", + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "id": 12, + "gridPos": { + "x": 16, + "y": 6, + "w": 8, + "h": 7 + }, + "description": "Failures named by cause rather than by status code, so a missing repository is told apart from a failed revision comparison. Requests rejected before reaching a handler \u2014 a malformed query, a missing token \u2014 have no cause name and are absent here; watch 4xx on the panel to the left for those." + }, + { + "type": "row", + "title": "latency", + "collapsed": false, + "panels": [], + "id": 13, + "gridPos": { + "x": 0, + "y": 13, + "w": 24, + "h": 1 + } + }, + { + "type": "timeseries", + "title": "median request duration by method", + "targets": [ + { + "refId": "A", + "expr": "gitmirror_xrpc_duration_seconds{job=\"gitmirror\",quantile=\"0.5\"}", + "legendFormat": "{{path}}" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 8, + "lineWidth": 1, + "showPoints": "never" + }, + "min": 0, + "unit": "s", + "decimals": 2 + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "calcs": [ + "lastNotNull", + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "id": 14, + "gridPos": { + "x": 0, + "y": 14, + "w": 8, + "h": 7 + }, + "description": "Typical time to serve each method, measured over a rolling recent window. Half of requests finished faster than this." + }, + { + "type": "timeseries", + "title": "p99 request duration by method", + "targets": [ + { + "refId": "A", + "expr": "gitmirror_xrpc_duration_seconds{job=\"gitmirror\",quantile=\"0.99\"}", + "legendFormat": "{{path}}" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 8, + "lineWidth": 1, + "showPoints": "never" + }, + "min": 0, + "unit": "s", + "decimals": 2 + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "calcs": [ + "lastNotNull", + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "id": 15, + "gridPos": { + "x": 8, + "y": 14, + "w": 8, + "h": 7 + }, + "description": "The slow tail: one request in a hundred took longer than this. Compare against the median beside it \u2014 a large gap means a few requests are far slower than the rest, usually large repositories or deep histories." + }, + { + "type": "table", + "title": "slowest methods in range", + "targets": [ + { + "refId": "A", + "expr": "topk(5, 1000 * max_over_time(gitmirror_xrpc_duration_seconds{job=\"gitmirror\",quantile=\"0.99\"}[$__range]))", + "legendFormat": "{{path}}", + "instant": true, + "range": false, + "format": "table" + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms", + "decimals": 1 + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "path" + }, + "properties": [ + { + "id": "custom.width", + "value": 360 + }, + { + "id": "custom.wrapText", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "worst p99" + }, + "properties": [ + { + "id": "custom.width", + "value": 110 + } + ] + } + ] + }, + "options": { + "cellHeight": "auto", + "showHeader": true, + "showTypeIcons": false, + "sortBy": [ + { + "displayName": "worst p99", + "desc": true + } + ], + "maxRowHeight": 64 + }, + "id": 16, + "gridPos": { + "x": 16, + "y": 14, + "w": 8, + "h": 7 + }, + "description": "The five methods whose slow tail was worst at any point in the selected period. Use this to find which method to look at; use the per-repo log panels below to find which repository caused it.", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true, + "job": true, + "instance": true, + "quantile": true, + "method": true + }, + "indexByName": { + "path": 0, + "Value": 1 + }, + "renameByName": { + "Value": "worst p99" + } + } + } + ] + }, + { + "type": "row", + "title": "concurrency and the blocking pool", + "collapsed": false, + "panels": [], + "id": 17, + "gridPos": { + "x": 0, + "y": 21, + "w": 24, + "h": 1 + } + }, + { + "type": "timeseries", + "title": "requests in flight by method", + "targets": [ + { + "refId": "A", + "expr": "gitmirror_xrpc_in_flight{job=\"gitmirror\"}", + "legendFormat": "{{path}}" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 8, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "mode": "normal", + "group": "A" + } + }, + "min": 0, + "unit": "short", + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "calcs": [ + "lastNotNull", + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "id": 18, + "gridPos": { + "x": 0, + "y": 22, + "w": 8, + "h": 7 + }, + "description": "How many requests of each method are being handled right now. This is what one method monopolising the service looks like." + }, + { + "type": "timeseries", + "title": "blocking pool occupancy", + "targets": [ + { + "refId": "A", + "expr": "gitmirror_blocking_running{job=\"gitmirror\"}", + "legendFormat": "{{kind}} running" + }, + { + "refId": "B", + "expr": "gitmirror_blocking_queued{job=\"gitmirror\"}", + "legendFormat": "{{kind}} queued" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 8, + "lineWidth": 1, + "showPoints": "never" + }, + "min": 0, + "unit": "short", + "decimals": 0 + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "calcs": [ + "lastNotNull", + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "id": 19, + "gridPos": { + "x": 8, + "y": 22, + "w": 8, + "h": 7 + }, + "description": "Threads in the blocking pool, split by what is holding them. \"git\" is local work on a repository; \"transport\" is a thread parked on a knot while a merge or branch deletion is pushed, which occupies the pool just as much. \"queued\" rising above zero means tasks are waiting for a free thread." + }, + { + "type": "timeseries", + "title": "wait for a free thread", + "targets": [ + { + "refId": "A", + "expr": "gitmirror_blocking_queue_wait_seconds{job=\"gitmirror\",quantile=\"0.5\"}", + "legendFormat": "{{kind}} median" + }, + { + "refId": "B", + "expr": "gitmirror_blocking_queue_wait_seconds{job=\"gitmirror\",quantile=\"0.99\"}", + "legendFormat": "{{kind}} p99" + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 8, + "lineWidth": 1, + "showPoints": "never" + }, + "min": 0, + "unit": "s", + "decimals": 2 + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "calcs": [ + "lastNotNull", + "max" + ] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "id": 20, + "gridPos": { + "x": 16, + "y": 22, + "w": 8, + "h": 7 + }, + "description": "Time a task sat waiting before a thread picked it up. Normally microseconds. When this climbs at the same time as request duration, the service is not slow at git \u2014 it is out of threads." + }, + { + "type": "row", + "title": "per-repo detail", + "collapsed": false, + "panels": [], + "id": 21, + "gridPos": { + "x": 0, + "y": 29, + "w": 24, + "h": 1 + } + }, + { + "id": 22, + "type": "logs", + "title": "slowest requests by repo", + "description": "Individual requests that took longer than the slow threshold above, newest first. Repositories are identified by DID, which is why this is a log panel and not a graph \u2014 there are too many repositories to chart one line each. Open a row for its full structured fields.", + "datasource": { + "type": "loki", + "uid": "loki" + }, + "gridPos": { + "x": 0, + "y": 30, + "w": 24, + "h": 8 + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "{service_name=\"gitmirror\"} |= \"xrpc served\" | duration_ms > ${slow_ms} | line_format \"{{.duration_ms}}ms {{.path}} repo={{.repo}} {{.base_repo}} status={{.status}}\" |~ \"(?i)${search:regex}\"", + "queryType": "range", + "refId": "A" + } + ], + "options": { + "showLabels": false, + "wrapLogMessage": true, + "enableLogDetails": true, + "showCommonLabels": false, + "showTime": true, + "sortOrder": "Descending", + "showControls": true + } + }, + { + "id": 23, + "type": "logs", + "title": "failed requests by repo", + "description": "Every request that failed with a named cause, and the repository it was for. A single repository filling this panel usually means that repository is broken on disk, not that gitmirror is.", + "datasource": { + "type": "loki", + "uid": "loki" + }, + "gridPos": { + "x": 0, + "y": 38, + "w": 24, + "h": 8 + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "loki" + }, + "editorMode": "code", + "expr": "{service_name=\"gitmirror\"} |= \"xrpc served\" | error != \"none\" | line_format \"{{.error}} {{.path}} repo={{.repo}} {{.base_repo}} status={{.status}} {{.duration_ms}}ms\" |~ \"(?i)${search:regex}\"", + "queryType": "range", + "refId": "A" + } + ], + "options": { + "showLabels": false, + "wrapLogMessage": true, + "enableLogDetails": true, + "showCommonLabels": false, + "showTime": true, + "sortOrder": "Descending", + "showControls": true + } + } + ], + "version": 1 +} diff --git a/hosts/prometheus-hel/services/prometheus.nix b/hosts/prometheus-hel/services/prometheus.nix index 3e87f63..b2cdf44 100644 --- a/hosts/prometheus-hel/services/prometheus.nix +++ b/hosts/prometheus-hel/services/prometheus.nix @@ -126,17 +126,21 @@ in { } ]; } + # { + # job_name = "knotmirror"; + # static_configs = [{targets = ["mirror-fsn:7100"];}]; + # metric_relabel_configs = [ + # { + # source_labels = ["repo"]; + # regex = ".+"; + # action = "drop"; + # } + # ]; + # sample_limit = 100000; + # } { - job_name = "knotmirror"; - static_configs = [{targets = ["mirror-fsn:7100"];}]; - metric_relabel_configs = [ - { - source_labels = ["repo"]; - regex = ".+"; - action = "drop"; - } - ]; - sample_limit = 100000; + job_name = "gitmirror"; + static_configs = [{targets = ["mirror-fsn:9091"];}]; } { job_name = "appview"; @@ -249,6 +253,7 @@ in { environment.etc."grafana/dashboards/node-exporter.json".source = nodeExporterDashboard; environment.etc."grafana/dashboards/knotmirror.json".source = ../dashboards/knotmirror.json; + environment.etc."grafana/dashboards/gitmirror.json".source = ../dashboards/gitmirror.json; environment.etc."grafana/dashboards/appview.json".source = ../dashboards/appview.json; environment.etc."grafana/dashboards/spindle.json".source = ../dashboards/spindle.json; environment.etc."grafana/dashboards/spindle-abuse.json".source = ../dashboards/spindle-abuse.json;