Skip to content

Commit

Permalink
Merge pull request #865 from jenkinsci/grafana-loki-logs-backend-with…
Browse files Browse the repository at this point in the history
…-jenkins-viz

First version of the GrafanaLogsBackendWithJenkinsVisualization
  • Loading branch information
cyrille-leclerc authored Jun 25, 2024
2 parents 3aa8270 + b6060ca commit 46101e2
Show file tree
Hide file tree
Showing 38 changed files with 2,349 additions and 89 deletions.
6 changes: 6 additions & 0 deletions docs/http-requests-traces.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Monitor and troubleshoot performance problems of the Jenkins Controller using tr
```
otel.instrumentation.jenkins.web.enabled=false
```
* HTTP request parameters can be captured using the standard configuration parameter
([docs](https://opentelemetry.io/docs/zero-code/java/agent/configuration/#capturing-servlet-request-parameters)):

```
otel.instrumentation.servlet.experimental.capture-request-parameters=<<coma separated list of parameter names>>
```

* Observability solutions provide aggregated views on the overall activity on the Jenkins Controller UI, often enabling monitoring dashboards, alerting, and automated anomaly detection

Expand Down
31 changes: 29 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
<description>Monitor, troubleshoot and observe Jenkins with OpenTelemetry.
Monitor Jenkins with dashboards and alerts on critical health metrics.
Troubleshoot Jenkins problems using traces of job executions and HTTP requests.
Store pipeline build logs in an Observability backend like Elastic to improve Jenkins reliability and scalability while improving the traceability of builds.</description>
Store pipeline build logs in an Observability backend like Elastic or Grafana/Loki
to improve Jenkins reliability and scalability while improving the traceability of builds.</description>
<url>https://github.com/jenkinsci/${project.artifactId}-plugin</url>

<dependencyManagement>
Expand Down Expand Up @@ -73,6 +74,20 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-bom</artifactId>
<version>${opentelemetry-instrumentation.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-bom-alpha</artifactId>
<version>${opentelemetry-instrumentation.version}-alpha</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
Expand All @@ -83,6 +98,11 @@
<artifactId>parsson</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>io.prometheus</groupId>
<artifactId>simpleclient_bom</artifactId>
Expand Down Expand Up @@ -157,7 +177,6 @@
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-runtime-telemetry-java17</artifactId>
<version>${opentelemetry-instrumentation-alpha.version}</version>
</dependency>
-->
<dependency>
Expand Down Expand Up @@ -264,6 +283,14 @@
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-durable-task-step</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>apache-httpcomponents-client-4-api</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-apache-httpclient-4.3</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>cloudbees-disk-usage-simple</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public String getDisplayName() {

private transient Template buildLogsVisualizationUrlGTemplate;

protected LokiOTelLogFormat lokiOTelLogFormat = LokiOTelLogFormat.LOKI_V2_JSON_OTEL_FORMAT;
protected LokiOTelLogFormat lokiOTelLogFormat;

/**
* Returns {@code null} if the backend is not capable of retrieving logs(ie the {@link NoGrafanaLogsBackend}
Expand Down Expand Up @@ -155,7 +155,7 @@ public DescriptorImpl getDescriptor() {

@NonNull
public String getLokiOTelLogFormat() {
return Optional.ofNullable(lokiOTelLogFormat).map(Enum::name).orElse(LokiOTelLogFormat.LOKI_V2_JSON_OTEL_FORMAT.name());
return Optional.ofNullable(lokiOTelLogFormat).map(Enum::name).orElse(getDescriptor().getDefaultLokiOTelLogFormat());

Check warning on line 158 in src/main/java/io/jenkins/plugins/opentelemetry/backend/grafana/GrafanaLogsBackend.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 158 is not covered by tests
}

@DataBoundSetter
Expand All @@ -178,5 +178,7 @@ public ListBoxModel doFillLokiOTelLogFormatItems() {
}
return items;
}

public abstract String getDefaultLokiOTelLogFormat();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ public int hashCode() {

@Extension(ordinal = 100)
public static class DescriptorImpl extends GrafanaLogsBackend.DescriptorImpl {
@Override
public String getDefaultLokiOTelLogFormat() {
return LokiOTelLogFormat.LOKI_V2_JSON_OTEL_FORMAT.name();

Check warning on line 61 in src/main/java/io/jenkins/plugins/opentelemetry/backend/grafana/GrafanaLogsBackendBackendWithLogMirroringInJenkins.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 61 is not covered by tests
}

@Nonnull
@Override
public String getDisplayName() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/*
* Copyright The Original Author or Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.jenkins.plugins.opentelemetry.backend.grafana;

import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import com.google.errorprone.annotations.MustBeClosed;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
import hudson.model.Item;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import io.jenkins.plugins.opentelemetry.JenkinsControllerOpenTelemetry;
import io.jenkins.plugins.opentelemetry.TemplateBindingsProvider;
import io.jenkins.plugins.opentelemetry.backend.GrafanaBackend;
import io.jenkins.plugins.opentelemetry.backend.ObservabilityBackend;
import io.jenkins.plugins.opentelemetry.jenkins.CredentialsNotFoundException;
import io.jenkins.plugins.opentelemetry.jenkins.JenkinsCredentialsToApacheHttpCredentialsAdapter;
import io.jenkins.plugins.opentelemetry.job.log.LogStorageRetriever;
import io.opentelemetry.api.OpenTelemetry;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.apache.http.auth.Credentials;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.verb.POST;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.logging.Logger;

public class GrafanaLogsBackendWithJenkinsVisualization extends GrafanaLogsBackend implements TemplateBindingsProvider {
private final static String MSG_LOKI_URL_IS_BLANK = "Loki URL is blank, logs will not be stored in Elasticsearch";
private final static Logger logger = Logger.getLogger(GrafanaLogsBackendWithJenkinsVisualization.class.getName());

private String grafanaLokiDatasourceIdentifier = GrafanaBackend.DEFAULT_LOKI_DATA_SOURCE_IDENTIFIER;

private String lokiUrl;
private boolean disableSslVerifications;
private String lokiCredentialsId;
private String lokiTenantId;

@DataBoundConstructor
public GrafanaLogsBackendWithJenkinsVisualization() {

}

public String getGrafanaLokiDatasourceIdentifier() {
return grafanaLokiDatasourceIdentifier;
}

@DataBoundSetter
public void setGrafanaLokiDatasourceIdentifier(String grafanaLokiDatasourceIdentifier) {
this.grafanaLokiDatasourceIdentifier = grafanaLokiDatasourceIdentifier;
}

@Override
@MustBeClosed
public LogStorageRetriever newLogStorageRetriever(TemplateBindingsProvider templateBindingsProvider) {
if (StringUtils.isBlank(lokiUrl)) {
throw new IllegalStateException(MSG_LOKI_URL_IS_BLANK);
}
// TODO shall we inject this through @Inject?

Check warning on line 76 in src/main/java/io/jenkins/plugins/opentelemetry/backend/grafana/GrafanaLogsBackendWithJenkinsVisualization.java

View check run for this annotation

ci.jenkins.io / Open Tasks Scanner

TODO

NORMAL: shall we inject this through @Inject?
OpenTelemetry openTelemetry = JenkinsControllerOpenTelemetry.get();

String serviceName = templateBindingsProvider.getBindings().get(ObservabilityBackend.TemplateBindings.SERVICE_NAME).toString();
Optional<String> serviceNamespace = Optional.ofNullable(templateBindingsProvider.getBindings().get(ObservabilityBackend.TemplateBindings.SERVICE_NAMESPACE)).map(Object::toString);
Optional<String> lokiTenantId = Optional.ofNullable(this.lokiTenantId).filter(StringUtils::isNotBlank);
return new LokiLogStorageRetriever(
lokiUrl,
disableSslVerifications,
getLokiApacheHttpCredentials(lokiCredentialsId), lokiTenantId,
getBuildLogsVisualizationUrlTemplate(),
TemplateBindingsProvider.compose(templateBindingsProvider, this.getBindings()),
serviceName,
serviceNamespace,
openTelemetry);
}

public String getLokiUrl() {
return lokiUrl;
}

@DataBoundSetter
public void setLokiUrl(String lokiUrl) {
this.lokiUrl = lokiUrl;
}

public boolean isDisableSslVerifications() {
return disableSslVerifications;
}


@DataBoundSetter
public void setDisableSslVerifications(boolean disableSslVerifications) {
this.disableSslVerifications = disableSslVerifications;
}

public String getLokiTenantId() {
return lokiTenantId;
}

@DataBoundSetter
public void setLokiTenantId(String lokiTenantId) {
this.lokiTenantId = lokiTenantId;
}

public String getLokiCredentialsId() {
return lokiCredentialsId;
}

/**
*
* @param lokiCredentialsId Jenkins credentials id
*/
@NonNull
protected static Optional<Credentials> getLokiApacheHttpCredentials(@Nullable String lokiCredentialsId) {
return Optional.ofNullable(lokiCredentialsId).filter(StringUtils::isNotBlank).map(JenkinsCredentialsToApacheHttpCredentialsAdapter::new);
}

@DataBoundSetter
public void setLokiCredentialsId(String lokiCredentialsId) {
this.lokiCredentialsId = lokiCredentialsId;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GrafanaLogsBackendWithJenkinsVisualization that = (GrafanaLogsBackendWithJenkinsVisualization) o;
return Objects.equals(grafanaLokiDatasourceIdentifier, that.grafanaLokiDatasourceIdentifier);
}

@Override
public int hashCode() {
return Objects.hash(grafanaLokiDatasourceIdentifier);
}

@Override
public String toString() {
return "GrafanaLogsBackendWithJenkinsVisualization{" +
"grafanaLokiDatasourceIdentifier='" + grafanaLokiDatasourceIdentifier + '\'' +
", lokiUrl='" + lokiUrl + '\'' +
", disableSslVerifications=" + disableSslVerifications +
", lokiCredentialsId='" + lokiCredentialsId + '\'' +
'}';
}

@Override
public Map<String, Object> getBindings() {
return Map.of(
GrafanaBackend.TemplateBindings.GRAFANA_LOKI_DATASOURCE_IDENTIFIER, getGrafanaLokiDatasourceIdentifier());

Check warning on line 165 in src/main/java/io/jenkins/plugins/opentelemetry/backend/grafana/GrafanaLogsBackendWithJenkinsVisualization.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 47-165 are not covered by tests
}

@Extension(ordinal = 50)
public static class DescriptorImpl extends GrafanaLogsBackend.DescriptorImpl {

public FormValidation doCheckLokiUrl(@QueryParameter("lokiUrl") String url) {
if (StringUtils.isEmpty(url)) {
return FormValidation.ok();
}
try {
new URI(url).toURL();
} catch (URISyntaxException | MalformedURLException | IllegalArgumentException e) {
return FormValidation.error("Invalid Loki URL: " + e.getMessage());
}
return FormValidation.ok();
}

public ListBoxModel doFillLokiCredentialsIdItems(Item context, @QueryParameter String lokiCredentialsId) {
if (context == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER)
|| context != null && !context.hasPermission(context.CONFIGURE)) {
return new StandardListBoxModel();
}

return new StandardListBoxModel().includeEmptyValue()
.includeAs(ACL.SYSTEM, context, StandardUsernameCredentials.class)
.includeCurrentValue(lokiCredentialsId);
}

@SuppressFBWarnings(value = "RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT",
justification = "We don't care about the return value, we just want to check that the credentials are valid")
@POST
public FormValidation doCheckLokiCredentialsId(Item context, @QueryParameter String lokiCredentialsId) {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);

if (context == null && !Jenkins.get().hasPermission(Jenkins.ADMINISTER)
|| context != null && !context.hasPermission(context.CONFIGURE)) {
return FormValidation.ok();
}

if (lokiCredentialsId == null || lokiCredentialsId.isEmpty()) {
return FormValidation.ok(); // support anonymous access
}
try {
new JenkinsCredentialsToApacheHttpCredentialsAdapter(lokiCredentialsId)
.getUserPrincipal().getName();
} catch (CredentialsNotFoundException e) {
return FormValidation.error("Loki credentials are not valid: " + e.getMessage());
}
return FormValidation.ok();
}

@POST
public FormValidation doValidate(@QueryParameter String lokiUrl,
@QueryParameter boolean disableSslVerifications, @QueryParameter String lokiCredentialsId,
@QueryParameter String lokiTenantId) {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);

FormValidation lokiUrlValidation = doCheckLokiUrl(lokiUrl);
if (lokiUrlValidation.kind != FormValidation.Kind.OK) {
return lokiUrlValidation;
}
OpenTelemetry openTelemetry = JenkinsControllerOpenTelemetry.get();
try (LokiLogStorageRetriever lokiLogStorageRetriever = new LokiLogStorageRetriever(
lokiUrl,
disableSslVerifications,
getLokiApacheHttpCredentials(lokiCredentialsId),
Optional.ofNullable(lokiTenantId).filter(Predicate.not(String::isBlank)),
ObservabilityBackend.ERROR_TEMPLATE,
TemplateBindingsProvider.empty(),
"##not-needed-to-invoke-check-loki-setup##",
Optional.empty(),
openTelemetry)) {
return FormValidation.aggregate(lokiLogStorageRetriever.checkLokiSetup());
} catch (NoSuchElementException e) {
return FormValidation.error("No credentials found for id '" + lokiCredentialsId + "'");
} catch (Exception e) {
return FormValidation.error(e, e.getMessage());
}
}


@NonNull
public String getDefaultLokiDataSourceIdentifier() {
return GrafanaBackend.DEFAULT_LOKI_DATA_SOURCE_IDENTIFIER;
}

@Override
public String getDefaultLokiOTelLogFormat() {
return LokiOTelLogFormat.LOKI_V2_JSON_OTEL_FORMAT.name();

Check warning on line 254 in src/main/java/io/jenkins/plugins/opentelemetry/backend/grafana/GrafanaLogsBackendWithJenkinsVisualization.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 172-254 are not covered by tests
}

@NonNull
@Override
public String getDisplayName() {
return "Store pipeline logs In Loki and visualize logs both in Grafana and through Jenkins ";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ public String getDefaultLokiDataSourceIdentifier(){
return GrafanaBackend.DEFAULT_LOKI_DATA_SOURCE_IDENTIFIER;
}

@Override
public String getDefaultLokiOTelLogFormat() {
return LokiOTelLogFormat.LOKI_V3_OTEL_FORMAT.name();

Check warning on line 80 in src/main/java/io/jenkins/plugins/opentelemetry/backend/grafana/GrafanaLogsBackendWithoutJenkinsVisualization.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 80 is not covered by tests
}

@NonNull
@Override
public String getDisplayName() {
Expand Down
Loading

0 comments on commit 46101e2

Please sign in to comment.