Skip to content

Commit 8464ddf

Browse files
committed
Allow for custom Grafana dashboards
1 parent c98c20c commit 8464ddf

File tree

9 files changed

+2155
-1
lines changed

9 files changed

+2155
-1
lines changed

docs/src/main/asciidoc/observability-devservices-lgtm.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@ Each dashboard is tuned for the specific application setup. The available dashbo
185185
Some panels in the dashboards might take a few minutes to show accurate data when their values are calculated over a sliding time window.
186186
====
187187

188+
==== Custom dashboards
189+
190+
Users can add their own Grafana dashboards aka their configuration. All you need to do is to place a `grafana-dashboard-[your name].json` into a `META-INF/grafana` directory in your application, and LGTM DevResource will pick this up automatically, and include this dashboard configuration in the overall Grafana dashboards yaml configuration.
191+
192+
e.g. /META-INF/grafana-dashboard-my-simple-prometheus-dashboard.json configuration file creates `My Simple Promeheus Dashboard` titled dashboard
193+
188194
=== Additional configuration
189195

190196
This extension will configure your `quarkus-opentelemetry` and `quarkus-micrometer-registry-otlp` extensions to send data to the OTel Collector bundled with the Grafana OTel LGTM image.

extensions/observability-devservices/testcontainers/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
</exclusion>
3232
</exclusions>
3333
</dependency>
34+
<dependency>
35+
<groupId>org.yaml</groupId>
36+
<artifactId>snakeyaml</artifactId>
37+
</dependency>
3438
<dependency>
3539
<groupId>io.quarkus</groupId>
3640
<artifactId>quarkus-junit4-mock</artifactId>
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package io.quarkus.observability.testcontainers;
2+
3+
import java.io.StringWriter;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
6+
import java.util.HashSet;
7+
import java.util.LinkedHashMap;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Set;
11+
import java.util.function.Consumer;
12+
13+
import org.yaml.snakeyaml.Yaml;
14+
15+
import io.quarkus.bootstrap.classloading.ClassPathElement;
16+
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
17+
18+
public class GrafanaUtils {
19+
20+
static final String META_INF_GRAFANA = "META-INF/grafana";
21+
22+
static String consumeGrafanaResources(String config, Consumer<String> consumer) {
23+
return consumeGrafanaResources(config, consumer, findGrafanaDashboards());
24+
}
25+
26+
@SuppressWarnings("unchecked")
27+
// Take dashboards as param, so we can test this
28+
static String consumeGrafanaResources(String config, Consumer<String> consumer, Set<String> dashboards) {
29+
Yaml yaml = new Yaml();
30+
Map<String, Object> data = yaml.load(config);
31+
List<Map<String, Object>> providers = (List<Map<String, Object>>) data.get("providers");
32+
33+
dashboards.forEach(s -> {
34+
String sub = s.substring("grafana-dashboard-".length(), s.length() - ".json".length());
35+
Map<String, Object> provider = new LinkedHashMap<>();
36+
String name = toName(sub);
37+
provider.put("name", name);
38+
provider.put("type", "file");
39+
Map<String, Object> options = new LinkedHashMap<>();
40+
options.put("path", "/otel-lgtm/" + s);
41+
options.put("foldersFromFilesStructure", false);
42+
provider.put("options", options);
43+
providers.add(provider);
44+
45+
consumer.accept(s);
46+
});
47+
48+
StringWriter writer = new StringWriter();
49+
yaml.dump(data, writer);
50+
51+
return writer.toString();
52+
}
53+
54+
private static String toName(String path) {
55+
if (path.isEmpty()) {
56+
throw new IllegalArgumentException("Illegal path: " + path);
57+
}
58+
StringBuilder name = new StringBuilder();
59+
boolean dash = true;
60+
for (int i = 0; i < path.length(); i++) {
61+
char ch = path.charAt(i);
62+
if (dash) {
63+
ch = Character.toUpperCase(ch);
64+
dash = false;
65+
} else {
66+
if (ch == '-') {
67+
dash = true;
68+
ch = ' ';
69+
}
70+
}
71+
name.append(ch);
72+
}
73+
return name.toString();
74+
}
75+
76+
/**
77+
* Visits all {@code META-INF/grafana} directories and their content found on the runtime classpath
78+
* and returns all Grafana dashboard json configurations
79+
*/
80+
private static Set<String> findGrafanaDashboards() {
81+
Set<String> dashboards = new HashSet<>();
82+
final List<ClassPathElement> elements = QuarkusClassLoader.getElements(META_INF_GRAFANA, false);
83+
if (!elements.isEmpty()) {
84+
for (var element : elements) {
85+
if (element.isRuntime()) {
86+
element.apply(tree -> {
87+
tree.walkIfContains(META_INF_GRAFANA, visit -> {
88+
Path visitPath = visit.getPath();
89+
if (!Files.isDirectory(visitPath)) {
90+
String rel = visit.getRelativePath();
91+
// Ensure that the relative path starts with the right prefix and suffix before calling substring
92+
if (rel.startsWith(META_INF_GRAFANA + "/grafana-dashboard-") && rel.endsWith(".json")) {
93+
// Strip the "META-INF/grafana/" prefix
94+
String subPath = rel.substring(META_INF_GRAFANA.length() + 1);
95+
dashboards.add(subPath);
96+
}
97+
}
98+
});
99+
return null;
100+
});
101+
}
102+
}
103+
}
104+
return dashboards;
105+
}
106+
107+
}

extensions/observability-devservices/testcontainers/src/main/java/io/quarkus/observability/testcontainers/LgtmContainer.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.observability.testcontainers;
22

3+
import java.nio.charset.StandardCharsets;
34
import java.util.Optional;
45
import java.util.Set;
56
import java.util.function.Predicate;
@@ -106,9 +107,19 @@ public LgtmContainer(LgtmConfig config, boolean scrapingRequired) {
106107
Optional<Set<LgtmComponent>> logging = config.logging();
107108
logging.ifPresent(set -> set.forEach(l -> withEnv("ENABLE_LOGS_" + l.name(), "true")));
108109

110+
String dashboards = GrafanaUtils.consumeGrafanaResources(
111+
DASHBOARDS_CONFIG,
112+
s -> {
113+
withCopyFileToContainer(
114+
MountableFile.forClasspathResource(GrafanaUtils.META_INF_GRAFANA + "/" + s),
115+
"/otel-lgtm/" + s);
116+
logger().info("Adding custom Grafana dashboard config: {}", s);
117+
});
118+
109119
// Replacing bundled dashboards with our own
110-
addFileToContainer(DASHBOARDS_CONFIG.getBytes(),
120+
addFileToContainer(dashboards.getBytes(StandardCharsets.UTF_8),
111121
"/otel-lgtm/grafana/conf/provisioning/dashboards/grafana-dashboards.yaml");
122+
112123
withCopyFileToContainer(
113124
MountableFile.forClasspathResource("/grafana-dashboard-quarkus-micrometer-prometheus.json"),
114125
"/otel-lgtm/grafana-dashboard-quarkus-micrometer-prometheus.json");
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package io.quarkus.observability.testcontainers;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.io.StringWriter;
6+
import java.util.Map;
7+
import java.util.Set;
8+
9+
import org.junit.jupiter.api.Test;
10+
import org.yaml.snakeyaml.Yaml;
11+
12+
public class GrafanaUtilsTest {
13+
14+
protected static final String DASHBOARDS_CONFIG = """
15+
apiVersion: 1
16+
17+
providers:
18+
- name: "Quarkus Micrometer Prometheus registry"
19+
type: file
20+
options:
21+
path: /otel-lgtm/grafana-dashboard-quarkus-micrometer-prometheus.json
22+
foldersFromFilesStructure: false
23+
""";
24+
25+
@Test
26+
public void testGrafanaUtils() {
27+
String config = GrafanaUtils.consumeGrafanaResources(
28+
DASHBOARDS_CONFIG,
29+
s -> {
30+
try (InputStream stream = Thread.currentThread().getContextClassLoader()
31+
.getResourceAsStream(GrafanaUtils.META_INF_GRAFANA + "/" + s)) {
32+
System.out.println(new String(stream.readAllBytes()));
33+
System.out.println("------");
34+
} catch (IOException ignored) {
35+
}
36+
},
37+
Set.of("grafana-dashboard-my-test.json"));
38+
39+
Yaml yaml = new Yaml();
40+
Map<String, Object> data = yaml.load(config);
41+
StringWriter writer = new StringWriter();
42+
yaml.dump(data, writer);
43+
System.out.println(writer);
44+
}
45+
46+
}

0 commit comments

Comments
 (0)