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

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:

ValueDefaultDescription
agent.port9202Argus metrics server port
agent.jvmArgs""Additional JVM arguments
prometheus.serviceMonitor.enabledfalseCreate a ServiceMonitor
grafana.dashboards.enabledfalseAuto-provision Grafana dashboard
otelCollector.enabledfalseDeploy 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:

VariablePurposeLocal mode behavior
$datasourceSelect the Prometheus datasourceRequired
$namespaceFilter fleet views by Kubernetes namespaceAll remains valid when labels are absent
$deploymentFilter by deploymentAll remains valid when labels are absent
$podSelect a pod for drilldown linksEmpty/local scrapes still render unfiltered panels
$instanceFilter by Prometheus scrape targetOptional

Drilldown links preserve selected pod context:

LinkTarget
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:

SignalSuggested thresholdDuration
GC overheadargus_gc_overhead_ratio > 0.105m
Heap usageargus_heap_usage_ratio > 0.9010m
Pinning raterate(argus_virtual_threads_pinned_total[5m]) > 05m
Scrape durationargus_scrape_duration_seconds > 15m
Allocation spikeCompare argus_allocation_rate_bytes_per_second to a 30m baseline5m

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:

VariableDownward API fieldLabel in metrics
ARGUS_POD_NAMEmetadata.namepod
ARGUS_NAMESPACEmetadata.namespacenamespace
ARGUS_NODE_NAMEspec.nodeNamenode

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 via k8sattributes processor, and re-exports to Prometheus.
  • metrics/scrape — scrapes the Argus /prometheus endpoint 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_OPTIONS or -javaagent points at argus-agent.jar. Check pod logs for Argus agent started.
  • The port is not declared in the container spec. Add containerPort: 9202 to 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.