Skip to content

Commit c7b89a5

Browse files
Merge pull request #590 from jenkinsci/add-grafana
Add Grafana integration
2 parents f2c83a0 + 2071c74 commit c7b89a5

File tree

6 files changed

+345
-0
lines changed

6 files changed

+345
-0
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/*
2+
* Copyright The Original Author or Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.jenkins.plugins.opentelemetry.backend;
7+
8+
import hudson.Extension;
9+
import hudson.util.FormValidation;
10+
import io.jenkins.plugins.opentelemetry.TemplateBindingsProvider;
11+
import org.apache.commons.lang.StringUtils;
12+
import org.jenkins.ui.icon.Icon;
13+
import org.jenkins.ui.icon.IconSet;
14+
import org.jenkinsci.Symbol;
15+
import org.kohsuke.stapler.DataBoundConstructor;
16+
import org.kohsuke.stapler.DataBoundSetter;
17+
import org.kohsuke.stapler.QueryParameter;
18+
19+
import javax.annotation.Nullable;
20+
import java.net.MalformedURLException;
21+
import java.net.URL;
22+
import java.util.HashMap;
23+
import java.util.LinkedHashMap;
24+
import java.util.Map;
25+
import java.util.Objects;
26+
27+
public class GrafanaBackend extends ObservabilityBackend implements TemplateBindingsProvider {
28+
29+
public static final String DEFAULT_BACKEND_NAME = "Grafana";
30+
31+
public static final String OTEL_GRAFANA_URL = "OTEL_GRAFANA_URL";
32+
33+
private static final String DEFAULT_TEMPO_DATA_SOURCE_IDENTIFIER = "grafanacloud-traces";
34+
private static final String DEFAULT_GRAFANA_ORG_ID = "1";
35+
36+
static {
37+
IconSet.icons.addIcon(
38+
new Icon(
39+
"icon-otel-grafana icon-sm",
40+
ICONS_PREFIX + "grafana.svg",
41+
Icon.ICON_SMALL_STYLE));
42+
IconSet.icons.addIcon(
43+
new Icon(
44+
"icon-otel-grafana icon-md",
45+
ICONS_PREFIX + "grafana.svg",
46+
Icon.ICON_MEDIUM_STYLE));
47+
IconSet.icons.addIcon(
48+
new Icon(
49+
"icon-otel-grafana icon-lg",
50+
ICONS_PREFIX + "grafana.svg",
51+
Icon.ICON_LARGE_STYLE));
52+
IconSet.icons.addIcon(
53+
new Icon(
54+
"icon-otel-grafana icon-xlg",
55+
ICONS_PREFIX + "grafana.svg",
56+
Icon.ICON_XLARGE_STYLE));
57+
}
58+
59+
private String grafanaBaseUrl;
60+
61+
private String grafanaMetricsDashboard;
62+
private String tempoDataSourceIdentifier = DEFAULT_TEMPO_DATA_SOURCE_IDENTIFIER;
63+
64+
private String grafanaOrgId = DEFAULT_GRAFANA_ORG_ID;
65+
66+
@DataBoundConstructor
67+
public GrafanaBackend() {
68+
69+
}
70+
71+
@Nullable
72+
@Override
73+
public String getTraceVisualisationUrlTemplate() {
74+
return
75+
"${" + TemplateBindings.GRAFANA_BASE_URL + "}" +
76+
"/explore?orgId=" +
77+
"${" + TemplateBindings.GRAFANA_ORG_ID + "}" +
78+
"&left=%7B%22datasource%22:%22" +
79+
"${" + TemplateBindings.GRAFANA_TEMPO_DATASOURCE_IDENTIFIER + "}" +
80+
"%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22" +
81+
"${" + TemplateBindings.GRAFANA_TEMPO_DATASOURCE_IDENTIFIER + "}" +
82+
"%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22" +
83+
"${traceId}" +
84+
"%22%7D%5D,%22range%22:%7B%22from%22:%22" +
85+
"${startTime.minusSeconds(600).atZone(java.util.TimeZone.getDefault().toZoneId()).toInstant().toEpochMilli()}" +
86+
"%22,%22to%22:%22" +
87+
"${startTime.plusSeconds(600).atZone(java.util.TimeZone.getDefault().toZoneId()).toInstant().toEpochMilli()}" +
88+
"%22%7D%7D";
89+
}
90+
91+
/**
92+
* Not yet instrumented
93+
*/
94+
@Nullable
95+
@Override
96+
public String getMetricsVisualizationUrlTemplate() {
97+
return grafanaMetricsDashboard;
98+
}
99+
100+
@Nullable
101+
@Override
102+
public String getIconPath() {
103+
return "icon-otel-grafana";
104+
}
105+
106+
@Nullable
107+
@Override
108+
public String getEnvVariableName() {
109+
return OTEL_GRAFANA_URL;
110+
}
111+
112+
@Nullable
113+
@Override
114+
public String getDefaultName() {
115+
return DEFAULT_BACKEND_NAME;
116+
}
117+
118+
@Override
119+
public boolean equals(Object o) {
120+
if (this == o) return true;
121+
if (o == null || getClass() != o.getClass()) return false;
122+
GrafanaBackend that = (GrafanaBackend) o;
123+
return grafanaOrgId == that.grafanaOrgId && Objects.equals(grafanaBaseUrl, that.grafanaBaseUrl) && Objects.equals(tempoDataSourceIdentifier, that.tempoDataSourceIdentifier);
124+
}
125+
126+
@Override
127+
public int hashCode() {
128+
return Objects.hash(grafanaBaseUrl, tempoDataSourceIdentifier, grafanaOrgId);
129+
}
130+
131+
@Override
132+
public Map<String, Object> mergeBindings(Map<String, Object> bindings) {
133+
Map<String, Object> mergedBindings = new HashMap<>(bindings);
134+
mergedBindings.putAll(getBindings());
135+
return mergedBindings;
136+
}
137+
138+
@Override
139+
public Map<String, String> getBindings() {
140+
Map<String, String> bindings = new LinkedHashMap<>();
141+
bindings.put(TemplateBindings.BACKEND_NAME, getName());
142+
bindings.put(ElasticBackend.TemplateBindings.BACKEND_24_24_ICON_URL, "/plugin/opentelemetry/images/24x24/grafana.png");
143+
144+
bindings.put(TemplateBindings.GRAFANA_BASE_URL, this.getGrafanaBaseUrl());
145+
bindings.put(TemplateBindings.GRAFANA_ORG_ID, String.valueOf(this.getGrafanaOrgId()));
146+
bindings.put(TemplateBindings.GRAFANA_TEMPO_DATASOURCE_IDENTIFIER, this.getTempoDataSourceIdentifier());
147+
148+
return bindings;
149+
}
150+
151+
public String getGrafanaBaseUrl() {
152+
return grafanaBaseUrl;
153+
}
154+
155+
@DataBoundSetter
156+
public void setGrafanaBaseUrl(String grafanaBaseUrl) {
157+
this.grafanaBaseUrl = grafanaBaseUrl;
158+
}
159+
160+
@DataBoundSetter
161+
public String getTempoDataSourceIdentifier() {
162+
return tempoDataSourceIdentifier;
163+
}
164+
165+
@DataBoundSetter
166+
public void setTempoDataSourceIdentifier(String tempoDataSourceIdentifier) {
167+
this.tempoDataSourceIdentifier = tempoDataSourceIdentifier;
168+
}
169+
170+
@DataBoundSetter
171+
public void setGrafanaMetricsDashboard(String grafanaMetricsDashboard) {
172+
this.grafanaMetricsDashboard = grafanaMetricsDashboard;
173+
}
174+
175+
public String getGrafanaOrgId() {
176+
return grafanaOrgId;
177+
}
178+
179+
public void setGrafanaOrgId(String grafanaOrgId) {
180+
this.grafanaOrgId = grafanaOrgId;
181+
}
182+
183+
@Extension
184+
@Symbol("grafana")
185+
public static class DescriptorImpl extends ObservabilityBackendDescriptor {
186+
187+
@Override
188+
public String getDisplayName() {
189+
return DEFAULT_BACKEND_NAME;
190+
}
191+
192+
public String getDefaultGrafanaOrgId() {
193+
return DEFAULT_GRAFANA_ORG_ID;
194+
}
195+
196+
public String getDefaultTempoDataSourceIdentifier() {
197+
return DEFAULT_TEMPO_DATA_SOURCE_IDENTIFIER;
198+
}
199+
200+
public FormValidation doCheckGrafanaBaseUrl(@QueryParameter("grafanaBaseUrl") String grafanaBaseUrl) {
201+
if (StringUtils.isEmpty(grafanaBaseUrl)) {
202+
return FormValidation.ok();
203+
}
204+
try {
205+
new URL(grafanaBaseUrl);
206+
} catch (MalformedURLException e) {
207+
return FormValidation.error("Invalid URL: " + e.getMessage());
208+
}
209+
return FormValidation.ok();
210+
}
211+
}
212+
213+
/**
214+
* List the attribute keys of the template bindings exposed by {@link ObservabilityBackend#getBindings()}
215+
*/
216+
public interface TemplateBindings extends ObservabilityBackend.TemplateBindings {
217+
String GRAFANA_BASE_URL = "grafanaBaseUrl";
218+
String GRAFANA_TEMPO_DATASOURCE_IDENTIFIER = "grafanaTempoDatasourceIdentifier";
219+
String GRAFANA_ORG_ID = "grafanaOrgId";
220+
}
221+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?jelly escape-by-default='true'?>
2+
<j:jelly xmlns:j="jelly:core"
3+
xmlns:f="/lib/form"
4+
>
5+
<f:entry title="Grafana base URL" field="grafanaBaseUrl" description="e.g. 'https://example.grafana.net/'">
6+
<f:textbox/>
7+
</f:entry>
8+
<f:entry title="Grafana metrics dashboard URL" field="grafanaMetricsDashboard" description="e.g. 'https://example.grafana.net/...'">
9+
<f:textbox/>
10+
</f:entry>
11+
<f:advanced>
12+
<p>
13+
<strong>Grafana</strong>
14+
</p>
15+
<f:entry title="Tempo data source identifier" field="tempoDataSourceIdentifier"
16+
description="Identifier of the Tempo datasource in which the Jenkins pipeline build traces are stored.">
17+
<f:textbox default="${descriptor.defaultTempoDataSourceIdentifier}"/>
18+
</f:entry>
19+
<f:entry title="Grafana Org Id" field="grafanaOrgId">
20+
<f:textbox default="${descriptor.defaultGrafanaOrgId}"/>
21+
</f:entry>
22+
<f:entry title="Display Name" field="name" description="Name used in Jenkins GUI">
23+
<f:textbox default="${descriptor.displayName}"/>
24+
</f:entry>
25+
</f:advanced>
26+
</j:jelly>
3.11 KB
Loading
4.96 KB
Loading
Lines changed: 62 additions & 0 deletions
Loading
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright The Original Author or Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.jenkins.plugins.opentelemetry.backend;
7+
8+
import org.junit.Test;
9+
10+
import java.time.LocalDateTime;
11+
import java.time.format.DateTimeFormatter;
12+
import java.util.HashMap;
13+
import java.util.Map;
14+
15+
public class GrafanaBackendTest {
16+
17+
@Test
18+
public void testTraceUrl() {
19+
GrafanaBackend grafanaBackend = new GrafanaBackend();
20+
grafanaBackend.setGrafanaBaseUrl("https://cleclerc.grafana.net");
21+
grafanaBackend.setGrafanaOrgId("1");
22+
grafanaBackend.setTempoDataSourceIdentifier("grafanacloud-traces");
23+
24+
LocalDateTime buildTime = LocalDateTime.parse("2023-02-05 23:31:52.610", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
25+
26+
Map<String, Object> bindings = new HashMap<>();
27+
bindings.put("serviceName", "jenkins");
28+
bindings.put("rootSpanName", "BUILD my-app");
29+
bindings.put("traceId", "f464e1f32444443d3fc00fdb19e5c124");
30+
bindings.put("spanId", "00799ea60984f33f");
31+
bindings.put("startTime", buildTime);
32+
33+
String actualTraceVisualisationUrl = grafanaBackend.getTraceVisualisationUrl(bindings);
34+
System.out.println(actualTraceVisualisationUrl);
35+
}
36+
}

0 commit comments

Comments
 (0)