Skip to content

Commit

Permalink
Merge pull request #590 from jenkinsci/add-grafana
Browse files Browse the repository at this point in the history
Add Grafana integration
  • Loading branch information
cyrille-leclerc authored Feb 7, 2023
2 parents f2c83a0 + 2071c74 commit c7b89a5
Show file tree
Hide file tree
Showing 6 changed files with 345 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* Copyright The Original Author or Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.jenkins.plugins.opentelemetry.backend;

import hudson.Extension;
import hudson.util.FormValidation;
import io.jenkins.plugins.opentelemetry.TemplateBindingsProvider;
import org.apache.commons.lang.StringUtils;
import org.jenkins.ui.icon.Icon;
import org.jenkins.ui.icon.IconSet;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;

import javax.annotation.Nullable;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

public class GrafanaBackend extends ObservabilityBackend implements TemplateBindingsProvider {

public static final String DEFAULT_BACKEND_NAME = "Grafana";

public static final String OTEL_GRAFANA_URL = "OTEL_GRAFANA_URL";

private static final String DEFAULT_TEMPO_DATA_SOURCE_IDENTIFIER = "grafanacloud-traces";
private static final String DEFAULT_GRAFANA_ORG_ID = "1";

static {
IconSet.icons.addIcon(
new Icon(
"icon-otel-grafana icon-sm",
ICONS_PREFIX + "grafana.svg",
Icon.ICON_SMALL_STYLE));
IconSet.icons.addIcon(
new Icon(
"icon-otel-grafana icon-md",
ICONS_PREFIX + "grafana.svg",
Icon.ICON_MEDIUM_STYLE));
IconSet.icons.addIcon(
new Icon(
"icon-otel-grafana icon-lg",
ICONS_PREFIX + "grafana.svg",
Icon.ICON_LARGE_STYLE));
IconSet.icons.addIcon(
new Icon(
"icon-otel-grafana icon-xlg",
ICONS_PREFIX + "grafana.svg",
Icon.ICON_XLARGE_STYLE));
}

private String grafanaBaseUrl;

private String grafanaMetricsDashboard;
private String tempoDataSourceIdentifier = DEFAULT_TEMPO_DATA_SOURCE_IDENTIFIER;

private String grafanaOrgId = DEFAULT_GRAFANA_ORG_ID;

@DataBoundConstructor
public GrafanaBackend() {

}

@Nullable
@Override
public String getTraceVisualisationUrlTemplate() {
return
"${" + TemplateBindings.GRAFANA_BASE_URL + "}" +
"/explore?orgId=" +
"${" + TemplateBindings.GRAFANA_ORG_ID + "}" +
"&left=%7B%22datasource%22:%22" +
"${" + TemplateBindings.GRAFANA_TEMPO_DATASOURCE_IDENTIFIER + "}" +
"%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22datasource%22:%7B%22type%22:%22tempo%22,%22uid%22:%22" +
"${" + TemplateBindings.GRAFANA_TEMPO_DATASOURCE_IDENTIFIER + "}" +
"%22%7D,%22queryType%22:%22traceId%22,%22query%22:%22" +
"${traceId}" +
"%22%7D%5D,%22range%22:%7B%22from%22:%22" +
"${startTime.minusSeconds(600).atZone(java.util.TimeZone.getDefault().toZoneId()).toInstant().toEpochMilli()}" +
"%22,%22to%22:%22" +
"${startTime.plusSeconds(600).atZone(java.util.TimeZone.getDefault().toZoneId()).toInstant().toEpochMilli()}" +
"%22%7D%7D";
}

/**
* Not yet instrumented
*/
@Nullable
@Override
public String getMetricsVisualizationUrlTemplate() {
return grafanaMetricsDashboard;
}

@Nullable
@Override
public String getIconPath() {
return "icon-otel-grafana";
}

@Nullable
@Override
public String getEnvVariableName() {
return OTEL_GRAFANA_URL;
}

@Nullable
@Override
public String getDefaultName() {
return DEFAULT_BACKEND_NAME;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GrafanaBackend that = (GrafanaBackend) o;
return grafanaOrgId == that.grafanaOrgId && Objects.equals(grafanaBaseUrl, that.grafanaBaseUrl) && Objects.equals(tempoDataSourceIdentifier, that.tempoDataSourceIdentifier);
}

@Override
public int hashCode() {
return Objects.hash(grafanaBaseUrl, tempoDataSourceIdentifier, grafanaOrgId);
}

@Override
public Map<String, Object> mergeBindings(Map<String, Object> bindings) {
Map<String, Object> mergedBindings = new HashMap<>(bindings);
mergedBindings.putAll(getBindings());
return mergedBindings;
}

@Override
public Map<String, String> getBindings() {
Map<String, String> bindings = new LinkedHashMap<>();
bindings.put(TemplateBindings.BACKEND_NAME, getName());
bindings.put(ElasticBackend.TemplateBindings.BACKEND_24_24_ICON_URL, "/plugin/opentelemetry/images/24x24/grafana.png");

bindings.put(TemplateBindings.GRAFANA_BASE_URL, this.getGrafanaBaseUrl());
bindings.put(TemplateBindings.GRAFANA_ORG_ID, String.valueOf(this.getGrafanaOrgId()));
bindings.put(TemplateBindings.GRAFANA_TEMPO_DATASOURCE_IDENTIFIER, this.getTempoDataSourceIdentifier());

return bindings;
}

public String getGrafanaBaseUrl() {
return grafanaBaseUrl;
}

@DataBoundSetter
public void setGrafanaBaseUrl(String grafanaBaseUrl) {
this.grafanaBaseUrl = grafanaBaseUrl;
}

@DataBoundSetter
public String getTempoDataSourceIdentifier() {
return tempoDataSourceIdentifier;
}

@DataBoundSetter
public void setTempoDataSourceIdentifier(String tempoDataSourceIdentifier) {
this.tempoDataSourceIdentifier = tempoDataSourceIdentifier;
}

@DataBoundSetter
public void setGrafanaMetricsDashboard(String grafanaMetricsDashboard) {
this.grafanaMetricsDashboard = grafanaMetricsDashboard;
}

public String getGrafanaOrgId() {
return grafanaOrgId;
}

public void setGrafanaOrgId(String grafanaOrgId) {
this.grafanaOrgId = grafanaOrgId;
}

@Extension
@Symbol("grafana")
public static class DescriptorImpl extends ObservabilityBackendDescriptor {

@Override
public String getDisplayName() {
return DEFAULT_BACKEND_NAME;
}

public String getDefaultGrafanaOrgId() {
return DEFAULT_GRAFANA_ORG_ID;
}

public String getDefaultTempoDataSourceIdentifier() {
return DEFAULT_TEMPO_DATA_SOURCE_IDENTIFIER;
}

public FormValidation doCheckGrafanaBaseUrl(@QueryParameter("grafanaBaseUrl") String grafanaBaseUrl) {
if (StringUtils.isEmpty(grafanaBaseUrl)) {
return FormValidation.ok();
}
try {
new URL(grafanaBaseUrl);
} catch (MalformedURLException e) {
return FormValidation.error("Invalid URL: " + e.getMessage());
}
return FormValidation.ok();
}
}

/**
* List the attribute keys of the template bindings exposed by {@link ObservabilityBackend#getBindings()}
*/
public interface TemplateBindings extends ObservabilityBackend.TemplateBindings {
String GRAFANA_BASE_URL = "grafanaBaseUrl";
String GRAFANA_TEMPO_DATASOURCE_IDENTIFIER = "grafanaTempoDatasourceIdentifier";
String GRAFANA_ORG_ID = "grafanaOrgId";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core"
xmlns:f="/lib/form"
>
<f:entry title="Grafana base URL" field="grafanaBaseUrl" description="e.g. 'https://example.grafana.net/'">
<f:textbox/>
</f:entry>
<f:entry title="Grafana metrics dashboard URL" field="grafanaMetricsDashboard" description="e.g. 'https://example.grafana.net/...'">
<f:textbox/>
</f:entry>
<f:advanced>
<p>
<strong>Grafana</strong>
</p>
<f:entry title="Tempo data source identifier" field="tempoDataSourceIdentifier"
description="Identifier of the Tempo datasource in which the Jenkins pipeline build traces are stored.">
<f:textbox default="${descriptor.defaultTempoDataSourceIdentifier}"/>
</f:entry>
<f:entry title="Grafana Org Id" field="grafanaOrgId">
<f:textbox default="${descriptor.defaultGrafanaOrgId}"/>
</f:entry>
<f:entry title="Display Name" field="name" description="Name used in Jenkins GUI">
<f:textbox default="${descriptor.displayName}"/>
</f:entry>
</f:advanced>
</j:jelly>
Binary file added src/main/webapp/images/24x24/grafana.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/main/webapp/images/48x48/grafana.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 62 additions & 0 deletions src/main/webapp/images/svgs/grafana.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright The Original Author or Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.jenkins.plugins.opentelemetry.backend;

import org.junit.Test;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

public class GrafanaBackendTest {

@Test
public void testTraceUrl() {
GrafanaBackend grafanaBackend = new GrafanaBackend();
grafanaBackend.setGrafanaBaseUrl("https://cleclerc.grafana.net");
grafanaBackend.setGrafanaOrgId("1");
grafanaBackend.setTempoDataSourceIdentifier("grafanacloud-traces");

LocalDateTime buildTime = LocalDateTime.parse("2023-02-05 23:31:52.610", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));

Map<String, Object> bindings = new HashMap<>();
bindings.put("serviceName", "jenkins");
bindings.put("rootSpanName", "BUILD my-app");
bindings.put("traceId", "f464e1f32444443d3fc00fdb19e5c124");
bindings.put("spanId", "00799ea60984f33f");
bindings.put("startTime", buildTime);

String actualTraceVisualisationUrl = grafanaBackend.getTraceVisualisationUrl(bindings);
System.out.println(actualTraceVisualisationUrl);
}
}

0 comments on commit c7b89a5

Please sign in to comment.