Argus on Kubernetes
June 9, 2026 · View on GitHub
This guide covers deploying Argus in a Kubernetes cluster, collecting JVM metrics, and integrating with Prometheus and Grafana.
Quick Start
Add Argus to an existing Kubernetes Deployment in three steps.
Step 1 — Add the agent JAR to your image.
FROM eclipse-temurin:21-jre
COPY argus-agent.jar /argus/argus-agent.jar
COPY app.jar /app/app.jar
ENTRYPOINT ["java", "-javaagent:/argus/argus-agent.jar", "-jar", "/app/app.jar"]
Use the argus-agent.jar release artifact for -javaagent. The CLI fat JAR is for the argus command and is not a Java agent.
Step 2 — Expose the metrics port and add scrape annotations.
metadata:
annotations:
argus.io/scrape: "true"
argus.io/port: "9202"
spec:
containers:
- ports:
- containerPort: 9202
name: argus-metrics
Step 3 — Annotate the namespace so Prometheus finds the pods.
kubectl annotate namespace my-app argus.io/monitored=true
Prometheus will now scrape http://<pod-ip>:9202/prometheus automatically.
Deployment Methods
Method A: -javaagent in Dockerfile (recommended)
Bundle the agent directly in your application image. This is the simplest approach and works with any JVM application.
FROM eclipse-temurin:21-jre
# Copy agent and application
COPY argus-agent.jar /argus/argus-agent.jar
COPY app.jar /app/app.jar
# Agent starts automatically with the JVM
ENTRYPOINT ["java", \
"-javaagent:/argus/argus-agent.jar", \
"-Dargus.server.port=9202", \
"-jar", "/app/app.jar"]
Method B: Init container pattern
Use an init container to inject the agent JAR via a shared volume. This avoids modifying the application image.
initContainers:
- name: argus-init
image: ghcr.io/rlaope/argus-agent:1.5.0
command: ["cp", "/opt/argus/argus-agent.jar", "/argus-volume/argus-agent.jar"]
volumeMounts:
- name: argus-volume
mountPath: /argus-volume
containers:
- name: app
image: my-app:latest
env:
- name: JAVA_TOOL_OPTIONS
value: "-javaagent:/argus/argus-agent.jar"
volumeMounts:
- name: argus-volume
mountPath: /argus
volumes:
- name: argus-volume
emptyDir: {}
Method C: Spring Boot starter (no agent JAR needed)
Add the Argus Spring Boot starter to pom.xml or build.gradle. No -javaagent flag is required — the starter auto-configures the agent at startup.
Maven:
<dependency>
<groupId>io.argus</groupId>
<artifactId>argus-spring-boot-starter</artifactId>
<version>1.5.0</version>
</dependency>
Gradle:
implementation 'io.argus:argus-spring-boot-starter:1.5.0'
The /prometheus and /argus endpoints are auto-registered alongside your application's existing endpoints.
Method C.1: Diagnostics-only mode (production-safe)
For production deployments where you don't want a daemon HTTP server listening on port 9202 or a JFR stream running 24/7, set argus.mode=diagnostics. You still get @Autowired DoctorService, /actuator/argus-doctor, /actuator/argus-gc, and the optional scheduled doctor — without any of the runtime overhead of mode=full.
# application.yml
argus:
mode: diagnostics
doctor:
schedule:
enabled: true
interval-ms: 60000 # 1 minute
management:
endpoints:
web:
exposure:
include: argus-doctor,argus-gc,health
K8s manifest delta — no extra port, no extra service:
spec:
template:
spec:
containers:
- name: my-app
ports:
- containerPort: 8080 # only your app port; argus is in-process
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
# Optional: feed your existing GC log into /actuator/argus-gc
- name: ARGUS_DOCTOR_GC_LOG_PATH
value: /var/log/jvm/gc.log
Findings emitted by the scheduled doctor appear in stdout as standard slf4j log lines (logger name argus.doctor), so Loki / Datadog / Promtail / Fluent Bit pick them up without extra config.
Prometheus Integration
Annotation-based scraping
Add these annotations to your Pod or Deployment template. Prometheus scrapes any pod with argus.io/scrape: "true" automatically when the OTel Collector or a Prometheus Operator is configured with the matching relabel rules (see deploy/otel-collector-config.yaml).
metadata:
annotations:
argus.io/scrape: "true" # Enable scraping
argus.io/port: "9202" # Argus metrics port (default: 9202)
argus.io/path: "/prometheus" # Metrics path (default: /prometheus)
ServiceMonitor (Prometheus Operator)
If you use the Prometheus Operator, create a ServiceMonitor instead of relying on pod annotations.
First expose Argus as a Service:
apiVersion: v1
kind: Service
metadata:
name: my-app-argus
labels:
app: my-app
argus: "true"
spec:
selector:
app: my-app
ports:
- name: argus-metrics
port: 9202
targetPort: 9202
Then create the ServiceMonitor:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: my-app-argus
labels:
release: kube-prometheus-stack # match your Prometheus Operator label selector
spec:
selector:
matchLabels:
argus: "true"
endpoints:
- port: argus-metrics
path: /prometheus
interval: 15s
Helm Chart
A Helm chart is provided under charts/argus for deploying the Argus agent configuration alongside your application.
# Add the Argus Helm repository
helm repo add argus https://rlaope.github.io/argus/charts
helm repo update
# Install with default values
helm install argus argus/argus \
--namespace monitoring \
--create-namespace
# Install with custom values
helm install argus argus/argus \
--namespace monitoring \
--set agent.port=9202 \
--set prometheus.serviceMonitor.enabled=true \
--set grafana.dashboards.enabled=true
Key chart values:
| Value | Default | Description |
|---|---|---|
agent.port | 9202 | Argus metrics server port |
agent.jvmArgs | "" | Additional JVM arguments |
prometheus.serviceMonitor.enabled | false | Create a ServiceMonitor |
grafana.dashboards.enabled | false | Auto-provision Grafana dashboard |
otelCollector.enabled | false | Deploy OTel Collector as a sidecar |
Grafana
When grafana.dashboards.enabled=true in the Helm chart, Argus auto-provisions a dashboard via a ConfigMap labelled grafana_dashboard: "1". Grafana's sidecar picks this up automatically.
The dashboard includes:
- Incident triage row for GC warnings, heap saturation, pinning, allocation, contention, and scrape duration
- Virtual thread active count, start/end rate, and pinning rate
- GC pause time (max, avg, total), p50/p95/p99 pause histograms, collector/cause breakdown, and heap usage
- CPU usage (JVM user, JVM system, machine total)
- Metaspace usage and class count
- Lock contention top-10 hotspots
- Allocation rate and top-10 allocating classes
- Method profiling top-20 hot methods
- Drilldown links to Fleet, selected-pod Dashboard, Profiles, Console,
/prometheus, and this setup guide
To import the dashboard manually, load docs/grafana-dashboard.json from the Argus repository into Grafana via Dashboards → Import.
Dashboard variables:
| Variable | Purpose | Local mode behavior |
|---|---|---|
$datasource | Select the Prometheus datasource | Required |
$namespace | Filter fleet views by Kubernetes namespace | All remains valid when labels are absent |
$deployment | Filter by deployment | All remains valid when labels are absent |
$pod | Select a pod for drilldown links | Empty/local scrapes still render unfiltered panels |
$instance | Filter by Prometheus scrape target | Optional |
Drilldown links preserve selected pod context:
| Link | Target |
|---|---|
| Fleet | /fleet.html |
| Selected pod Dashboard | /?pod=<pod-id> |
| CPU profile | /profiles.html?pod=<pod-id>&event=cpu&range=3600 |
| Allocation profile | /profiles.html?pod=<pod-id>&event=alloc&range=3600 |
| Console | /console.html?pod=<pod-id> |
Recommended alert rules to provision in your Prometheus stack:
| Signal | Suggested threshold | Duration |
|---|---|---|
| GC overhead | argus_gc_overhead_ratio > 0.10 | 5m |
| Heap usage | argus_heap_usage_ratio > 0.90 | 10m |
| Pinning rate | rate(argus_virtual_threads_pinned_total[5m]) > 0 | 5m |
| Scrape duration | argus_scrape_duration_seconds > 1 | 5m |
| Allocation spike | Compare argus_allocation_rate_bytes_per_second to a 30m baseline | 5m |
Example Deployment YAML
A complete Deployment with the Argus agent, Downward API environment variables, and a readiness probe:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
annotations:
argus.io/scrape: "true"
argus.io/port: "9202"
spec:
containers:
- name: my-app
image: my-app:latest
ports:
- containerPort: 8080
name: http
- containerPort: 9202
name: argus-metrics
env:
- name: JAVA_TOOL_OPTIONS
value: "-javaagent:/argus/argus-agent.jar -Dargus.server.port=9202"
# Downward API: exposes K8s identity to Argus for metric labels
- name: ARGUS_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: ARGUS_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: ARGUS_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
readinessProbe:
httpGet:
path: /argus/health
port: 9202
initialDelaySeconds: 10
periodSeconds: 15
volumeMounts:
- name: argus-volume
mountPath: /argus
initContainers:
- name: argus-init
image: ghcr.io/rlaope/argus-agent:1.5.0
command: ["cp", "/opt/argus/argus-agent.jar", "/argus-volume/argus-agent.jar"]
volumeMounts:
- name: argus-volume
mountPath: /argus-volume
volumes:
- name: argus-volume
emptyDir: {}
Downward API environment variables
Argus reads these environment variables to attach Kubernetes identity labels to every Prometheus metric:
| Variable | Downward API field | Label in metrics |
|---|---|---|
ARGUS_POD_NAME | metadata.name | pod |
ARGUS_NAMESPACE | metadata.namespace | namespace |
ARGUS_NODE_NAME | spec.nodeName | node |
When running in Kubernetes, metrics will look like:
argus_virtual_threads_active{pod="my-app-7d9f",namespace="default",node="node-1"} 42
When running outside Kubernetes (local dev, bare metal), the label suffix is omitted:
argus_virtual_threads_active 42
OTel Collector (OTLP Push Mode)
Instead of Prometheus scraping, you can configure Argus to push metrics to an OpenTelemetry Collector via OTLP.
Configure the Argus agent:
-Dargus.metrics.otlp.endpoint=http://otel-collector:4318/v1/metrics
Deploy the collector using the provided configuration:
kubectl create configmap otel-collector-config \
--from-file=config.yaml=deploy/otel-collector-config.yaml \
--namespace monitoring
The collector config at deploy/otel-collector-config.yaml supports two pipelines:
metrics/otlp— receives OTLP push from Argus, enriches with K8s attributes viak8sattributesprocessor, and re-exports to Prometheus.metrics/scrape— scrapes the Argus/prometheusendpoint directly from pods (annotation-based discovery) and remote-writes to Prometheus.
RBAC for the k8sattributes processor:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: otel-collector
rules:
- apiGroups: [""]
resources: ["pods", "namespaces", "nodes"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: otel-collector
subjects:
- kind: ServiceAccount
name: otel-collector
namespace: monitoring
roleRef:
kind: ClusterRole
name: otel-collector
apiGroup: rbac.authorization.k8s.io
Troubleshooting
Argus metrics port not reachable
Symptom: curl http://<pod-ip>:9202/prometheus times out or is refused.
Causes and fixes:
- The agent is not loaded. Verify
JAVA_TOOL_OPTIONSor-javaagentpoints atargus-agent.jar. Check pod logs forArgus agent started. - The port is not declared in the container spec. Add
containerPort: 9202to the container ports list. - A network policy is blocking the port. Add an ingress rule allowing traffic on port 9202 from the Prometheus namespace.
JFR not available — metrics are empty
Symptom: The /prometheus endpoint returns only argus_build_info.
Cause: The JVM does not have JFR access. JFR requires a commercial JDK or OpenJDK 11+.
Fix: Use eclipse-temurin:21-jre or another OpenJDK 11+ image. Add -XX:+FlightRecorder if running on JDK 11 where it is not enabled by default.
Bind address conflict
Symptom: Address already in use: 0.0.0.0:9202 in pod logs.
Fix: Change the port with -Dargus.server.port=<free-port> and update the pod annotations and containerPort accordingly.
JFR permissions denied in container
Symptom: java.lang.RuntimeException: JFR recording failed — permission denied.
Cause: The container runs with a read-only filesystem or a restrictive seccomp profile.
Fix: Either allow write access to /tmp (JFR writes recording files there) or mount a writable emptyDir at /tmp:
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
K8s labels not appearing on metrics
Symptom: Metrics are emitted without pod, namespace, or node labels.
Cause: The Downward API environment variables are not set.
Fix: Add the three env entries shown in the Example Deployment YAML section. Argus detects Kubernetes by the presence of ARGUS_NAMESPACE (or POD_NAMESPACE). Without at least one of these, no K8s labels are added.