Skip to content

Commit b621eda

Browse files
committed
feat: Kubernetes ecosystem integration — Helm chart, Docker, K8s labels, OTel
T1: Server bind address changed from 127.0.0.1 to 0.0.0.0 (required for Prometheus scraping in K8s) T2: Docker deployment files - Dockerfile.agent: init container / base layer image - Dockerfile.example: multi-stage for any JVM app - Dockerfile.spring-boot: Spring Boot starter path - docker-compose.yml: full local stack with Prometheus + Grafana T3: Helm chart (charts/argus/) - ServiceMonitor CRD for Prometheus Operator - PrometheusRule with 3 alerts (GC overhead, memory leak, high CPU) - Grafana dashboard ConfigMap with sidecar auto-discovery - NetworkPolicy for metrics port - ConfigMap with agent JVM system properties T4: Kubernetes documentation (docs/kubernetes.md) - 3 deployment methods (agent, init container, Spring Boot) - Annotation-based and ServiceMonitor scraping patterns - Complete Pod spec example with Downward API T5: K8s-aware metric labels (KubernetesLabels.java) - Auto-detects ARGUS_POD_NAME, ARGUS_NAMESPACE, ARGUS_NODE_NAME - Appends pod/namespace/node labels to all Prometheus metrics - Zero impact outside K8s (no env vars = no labels) T6: OpenTelemetry Collector config - OTLP push + Prometheus pull receivers - k8sattributes processor for pod metadata - Ready-to-deploy with any OTel Collector Signed-off-by: rlaope <piyrw9754@gmail.com>
1 parent 7107def commit b621eda

23 files changed

Lines changed: 3613 additions & 15 deletions

argus-server/src/main/java/io/argus/server/ArgusServer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ protected void initChannel(SocketChannel ch) {
246246
.option(ChannelOption.SO_BACKLOG, 128)
247247
.childOption(ChannelOption.SO_KEEPALIVE, true);
248248

249-
String bindAddress = System.getProperty("argus.server.bind", "127.0.0.1");
249+
String bindAddress = System.getProperty("argus.server.bind", "0.0.0.0");
250250
serverChannel = bootstrap.bind(bindAddress, port).sync().channel();
251251

252252
// Start event broadcasting
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package io.argus.server.metrics;
2+
3+
import java.util.LinkedHashMap;
4+
import java.util.Map;
5+
6+
/**
7+
* Detects Kubernetes environment via Downward API environment variables
8+
* and provides labels for Prometheus metrics.
9+
*
10+
* Expected env vars (set via K8s Downward API):
11+
* - ARGUS_POD_NAME or HOSTNAME
12+
* - ARGUS_NAMESPACE or POD_NAMESPACE
13+
* - ARGUS_NODE_NAME
14+
* - ARGUS_DEPLOYMENT (optional)
15+
*/
16+
public final class KubernetesLabels {
17+
18+
private static final Map<String, String> LABELS = detectLabels();
19+
20+
private KubernetesLabels() {}
21+
22+
public static boolean isKubernetes() {
23+
return !LABELS.isEmpty();
24+
}
25+
26+
public static Map<String, String> getLabels() {
27+
return LABELS;
28+
}
29+
30+
/**
31+
* Returns Prometheus label suffix string like {pod="abc",namespace="default"}
32+
* or empty string if not in K8s.
33+
*/
34+
public static String prometheusLabelSuffix() {
35+
if (LABELS.isEmpty()) return "";
36+
StringBuilder sb = new StringBuilder("{");
37+
boolean first = true;
38+
for (var entry : LABELS.entrySet()) {
39+
if (!first) sb.append(',');
40+
sb.append(entry.getKey()).append("=\"").append(entry.getValue()).append('"');
41+
first = false;
42+
}
43+
sb.append('}');
44+
return sb.toString();
45+
}
46+
47+
/**
48+
* Merges K8s labels into an existing Prometheus label string.
49+
* If existingLabels is empty, returns {@link #prometheusLabelSuffix()}.
50+
* Otherwise inserts K8s labels before the closing brace.
51+
*
52+
* @param existingLabels existing label string, e.g. {@code {monitor="java.util.concurrent.locks.ReentrantLock"}}
53+
* @return merged label string, or existingLabels unchanged if not in K8s
54+
*/
55+
public static String mergeLabels(String existingLabels) {
56+
if (LABELS.isEmpty()) return existingLabels;
57+
if (existingLabels == null || existingLabels.isEmpty()) return prometheusLabelSuffix();
58+
59+
// Build K8s label pairs without braces
60+
StringBuilder k8sPairs = new StringBuilder();
61+
for (var entry : LABELS.entrySet()) {
62+
k8sPairs.append(',')
63+
.append(entry.getKey()).append("=\"").append(entry.getValue()).append('"');
64+
}
65+
66+
// Insert before closing brace
67+
int closingBrace = existingLabels.lastIndexOf('}');
68+
if (closingBrace < 0) return existingLabels;
69+
return existingLabels.substring(0, closingBrace) + k8sPairs + "}";
70+
}
71+
72+
private static Map<String, String> detectLabels() {
73+
Map<String, String> labels = new LinkedHashMap<>();
74+
75+
String pod = env("ARGUS_POD_NAME", env("HOSTNAME", null));
76+
String namespace = env("ARGUS_NAMESPACE", env("POD_NAMESPACE", null));
77+
String node = env("ARGUS_NODE_NAME", null);
78+
79+
// Only add labels if we detect at least namespace (strong K8s signal)
80+
if (namespace != null) {
81+
if (pod != null) labels.put("pod", pod);
82+
labels.put("namespace", namespace);
83+
if (node != null) labels.put("node", node);
84+
}
85+
86+
return Map.copyOf(labels);
87+
}
88+
89+
private static String env(String name, String fallback) {
90+
String val = System.getenv(name);
91+
return (val != null && !val.isBlank()) ? val : fallback;
92+
}
93+
}

argus-server/src/main/java/io/argus/server/metrics/PrometheusMetricsCollector.java

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,11 @@ private void appendContentionMetrics(StringBuilder sb) {
241241
sb.append("# HELP argus_contention_hotspot_events Contention events per monitor class\n");
242242
sb.append("# TYPE argus_contention_hotspot_events gauge\n");
243243
for (var hotspot : hotspots.stream().limit(10).toList()) {
244-
sb.append("argus_contention_hotspot_events{monitor=\"")
245-
.append(escapeLabel(hotspot.monitorClass()))
246-
.append("\"} ")
244+
String labels = KubernetesLabels.mergeLabels(
245+
"{monitor=\"" + escapeLabel(hotspot.monitorClass()) + "\"}");
246+
sb.append("argus_contention_hotspot_events")
247+
.append(labels)
248+
.append(' ')
247249
.append(hotspot.eventCount())
248250
.append('\n');
249251
}
@@ -267,9 +269,11 @@ private void appendAllocationMetrics(StringBuilder sb) {
267269
sb.append("# HELP argus_allocation_class_bytes Bytes allocated by class\n");
268270
sb.append("# TYPE argus_allocation_class_bytes gauge\n");
269271
for (var classAlloc : topClasses.stream().limit(10).toList()) {
270-
sb.append("argus_allocation_class_bytes{class=\"")
271-
.append(escapeLabel(classAlloc.className()))
272-
.append("\"} ")
272+
String labels = KubernetesLabels.mergeLabels(
273+
"{class=\"" + escapeLabel(classAlloc.className()) + "\"}");
274+
sb.append("argus_allocation_class_bytes")
275+
.append(labels)
276+
.append(' ')
273277
.append(classAlloc.totalBytes())
274278
.append('\n');
275279
}
@@ -287,11 +291,12 @@ private void appendProfilingMetrics(StringBuilder sb) {
287291
sb.append("# HELP argus_profiling_method_samples Execution samples per method\n");
288292
sb.append("# TYPE argus_profiling_method_samples gauge\n");
289293
for (var method : topMethods.stream().limit(20).toList()) {
290-
sb.append("argus_profiling_method_samples{class=\"")
291-
.append(escapeLabel(method.className()))
292-
.append("\",method=\"")
293-
.append(escapeLabel(method.methodName()))
294-
.append("\"} ")
294+
String labels = KubernetesLabels.mergeLabels(
295+
"{class=\"" + escapeLabel(method.className())
296+
+ "\",method=\"" + escapeLabel(method.methodName()) + "\"}");
297+
sb.append("argus_profiling_method_samples")
298+
.append(labels)
299+
.append(' ')
295300
.append(method.sampleCount())
296301
.append('\n');
297302
}
@@ -315,7 +320,7 @@ private void appendBuildInfo(StringBuilder sb) {
315320
private void appendGauge(StringBuilder sb, String name, String help, double value) {
316321
sb.append("# HELP ").append(name).append(' ').append(help).append('\n');
317322
sb.append("# TYPE ").append(name).append(" gauge\n");
318-
sb.append(name).append(' ');
323+
sb.append(name).append(KubernetesLabels.prometheusLabelSuffix()).append(' ');
319324
if (value == (long) value) {
320325
sb.append((long) value);
321326
} else {
@@ -327,13 +332,13 @@ private void appendGauge(StringBuilder sb, String name, String help, double valu
327332
private void appendCounter(StringBuilder sb, String name, String help, long value) {
328333
sb.append("# HELP ").append(name).append(' ').append(help).append('\n');
329334
sb.append("# TYPE ").append(name).append(" counter\n");
330-
sb.append(name).append(' ').append(value).append('\n');
335+
sb.append(name).append(KubernetesLabels.prometheusLabelSuffix()).append(' ').append(value).append('\n');
331336
}
332337

333338
private void appendCounter(StringBuilder sb, String name, String help, double value) {
334339
sb.append("# HELP ").append(name).append(' ').append(help).append('\n');
335340
sb.append("# TYPE ").append(name).append(" counter\n");
336-
sb.append(name).append(' ').append(value).append('\n');
341+
sb.append(name).append(KubernetesLabels.prometheusLabelSuffix()).append(' ').append(value).append('\n');
337342
}
338343

339344
private String escapeLabel(String value) {

charts/argus/Chart.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: v2
2+
name: argus
3+
description: Argus JVM Observability — Prometheus metrics, Grafana dashboards, and alerting for JVM applications
4+
type: application
5+
version: 1.0.0
6+
appVersion: "1.0.0"
7+
home: https://github.com/rlaope/Argus
8+
sources:
9+
- https://github.com/rlaope/Argus
10+
keywords:
11+
- jvm
12+
- observability
13+
- gc
14+
- virtual-threads
15+
- prometheus
16+
- grafana
17+
maintainers:
18+
- name: rlaope

0 commit comments

Comments
 (0)