Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<properties>
<revision>2.19.1</revision>
<changelist>-SNAPSHOT</changelist>
<jenkins.version>2.387.3</jenkins.version>
<jenkins.version>2.401.3</jenkins.version>
<gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo>
<opentelemetry.version>1.33.0</opentelemetry.version>
<!--
Expand All @@ -44,8 +44,8 @@
<dependencies>
<dependency>
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-2.387.x</artifactId>
<version>2102.v854b_fec19c92</version>
<artifactId>bom-2.401.x</artifactId>
<version>2312.v91115fa_5b_2b_6</version>
<scope>import</scope>
<type>pom</type>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.model.BuildListener;
import io.jenkins.plugins.opentelemetry.OpenTelemetrySdkProvider;
import io.jenkins.plugins.opentelemetry.opentelemetry.GlobalOpenTelemetrySdk;
import io.jenkins.plugins.opentelemetry.opentelemetry.common.OffsetClock;
import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes;
import io.opentelemetry.sdk.common.Clock;
import jenkins.util.JenkinsJVM;
import org.jenkinsci.plugins.workflow.log.OutputStreamTaskListener;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
Expand All @@ -33,7 +36,7 @@
* <p>
* See https://github.com/jenkinsci/pipeline-cloudwatch-logs-plugin/blob/pipeline-cloudwatch-logs-0.2/src/main/java/io/jenkins/plugins/pipeline_cloudwatch_logs/CloudWatchSender.java
*/
abstract class OtelLogSenderBuildListener implements BuildListener {
abstract class OtelLogSenderBuildListener implements BuildListener, OutputStreamTaskListener {

protected final static Logger LOGGER = Logger.getLogger(OtelLogSenderBuildListener.class.getName());
final RunTraceContext runTraceContext;
Expand All @@ -44,11 +47,14 @@ abstract class OtelLogSenderBuildListener implements BuildListener {
* Timestamps of the logs emitted by the Jenkins Agents must be chronologically ordered with the timestamps of
* the logs & traces emitted on the Jenkins controller even if the system clock are not perfectly synchronized
*/
@SuppressFBWarnings("IS2_INCONSISTENT_SYNC")
transient Clock clock;

@CheckForNull
transient PrintStream logger;
transient OutputStream outputStream;

@CheckForNull
transient PrintStream logger;

public OtelLogSenderBuildListener(@NonNull RunTraceContext runTraceContext, @NonNull Map<String, String> otelConfigProperties, @NonNull Map<String, String> otelResourceAttributes) {
this.runTraceContext = runTraceContext;
Expand All @@ -60,6 +66,15 @@ public OtelLogSenderBuildListener(@NonNull RunTraceContext runTraceContext, @Non
JenkinsJVM.checkJenkinsJVM();
}

@NonNull
@Override
public synchronized final OutputStream getOutputStream() {
if (outputStream == null) {
outputStream = new OtelLogOutputStream(runTraceContext, getOtelLogger(), clock);
}
return outputStream;
}

@NonNull
@Override
public synchronized final PrintStream getLogger() {
Expand Down Expand Up @@ -150,37 +165,38 @@ private void writeObject(ObjectOutputStream stream) throws IOException {


private Object readResolve() {
adjustClock();
GlobalOpenTelemetrySdk.configure(
otelConfigProperties,
otelResourceAttributes,
/* the JVM shutdown hook is too late to flush the Otel signals as the OTel classes have been unloaded */
false );
// TODO find the right lifecycle event to shutdown the Otel SDK on agent shutdown
// hudson.remoting.EngineListener doesn't seem to be the right event
return this;
}

/**
* Timestamps of the logs emitted by the Jenkins Agents must be chronologically ordered with the timestamps of
* the logs & traces emitted on the Jenkins controller even if the system clock are not perfectly synchronized
*/
private void adjustClock() {
JenkinsJVM.checkNotJenkinsJVM();

/*
* Timestamps of the logs emitted by the Jenkins Agents must be chronologically ordered with the timestamps of
* the logs & traces emitted on the Jenkins controller even if the system clock are not perfectly synchronized
*/
if (instantInNanosOnJenkinsControllerBeforeSerialization == 0) {
logger.log(Level.INFO, () -> "adjustClock(): unexpected timeBeforeSerialization of 0ns, don't adjust the clock");
logger.log(Level.INFO, () -> "adjustClock: unexpected timeBeforeSerialization of 0ns, don't adjust the clock");
this.clock = Clock.getDefault();
} else {
long instantInNanosOnJenkinsAgentAtDeserialization = Clock.getDefault().now();
long offsetInNanosOnJenkinsAgent = instantInNanosOnJenkinsControllerBeforeSerialization - instantInNanosOnJenkinsAgentAtDeserialization;
logger.log(Level.FINE, () ->
"adjustClock(): " +
"adjustClock: " +
"offsetInNanos: " + TimeUnit.MILLISECONDS.convert(offsetInNanosOnJenkinsAgent, TimeUnit.NANOSECONDS) + "ms / " + offsetInNanosOnJenkinsAgent + "ns. "+
"A negative offset of few milliseconds is expected due to the latency of the communication from the Jenkins Controller to the Jenkins Agent. " +
"Higher offsets indicate a synchronization gap of the system clocks between the Jenkins Controller that will be work arounded by the clock adjustment."
);
this.clock = OffsetClock.offsetClock(offsetInNanosOnJenkinsAgent);
}

// Setup OTel
GlobalOpenTelemetrySdk.configure(
otelConfigProperties,
otelResourceAttributes,
/* the JVM shutdown hook is too late to flush the Otel signals as the OTel classes have been unloaded */
false );
// TODO find the right lifecycle event to shutdown the Otel SDK on agent shutdown
// hudson.remoting.EngineListener doesn't seem to be the right event
return this;
}


}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.jenkins.plugins.opentelemetry.job.log;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.console.AnnotatedLargeText;
import hudson.model.BuildListener;
Expand All @@ -11,18 +12,18 @@
import io.jenkins.plugins.opentelemetry.job.MonitoringAction;
import io.jenkins.plugins.opentelemetry.job.OtelTraceService;
import io.jenkins.plugins.opentelemetry.job.log.util.TeeBuildListener;
import io.jenkins.plugins.opentelemetry.job.log.util.TeeTaskListener;
import io.jenkins.plugins.opentelemetry.job.log.util.TeeOutputStreamBuildListener;
import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import jenkins.util.BuildListenerAdapter;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.log.BrokenLogStorage;
import org.jenkinsci.plugins.workflow.log.FileLogStorage;
import org.jenkinsci.plugins.workflow.log.LogStorage;

import edu.umd.cs.findbugs.annotations.NonNull;
import org.jenkinsci.plugins.workflow.log.OutputStreamTaskListener;

import java.io.File;
import java.io.FileOutputStream;
Expand Down Expand Up @@ -76,39 +77,66 @@ public BuildListener overallListener() throws IOException {
Map<String, String> otelResourceAttributes = new HashMap<>();
otelConfiguration.toOpenTelemetryResource().getAttributes().asMap().forEach((k, v) -> otelResourceAttributes.put(k.getKey(), v.toString()));

BuildListener result = new OtelLogSenderBuildListener.OtelLogSenderBuildListenerOnController(runTraceContext, otelConfigurationProperties, otelResourceAttributes);
OtelLogSenderBuildListener otelLogSenderBuildListener = new OtelLogSenderBuildListener.OtelLogSenderBuildListenerOnController(runTraceContext, otelConfigurationProperties, otelResourceAttributes);

BuildListener result;
if (OpenTelemetrySdkProvider.get().isOtelLogsMirrorToDisk()) {
try {
File logFile = new File(runFolderPath, "log");
result = new TeeBuildListener(result, FileLogStorage.forFile(logFile).overallListener());
} catch (IOException|InterruptedException e) {
File logFile = new File(runFolderPath, "log");
BuildListener fileStorageBuildListener = FileLogStorage.forFile(logFile).overallListener();
if (fileStorageBuildListener instanceof OutputStreamTaskListener) {
result = new TeeOutputStreamBuildListener(otelLogSenderBuildListener, fileStorageBuildListener);
} else {
logger.log(Level.INFO, () -> "overallListener(): FileLogStorage's TaskListener is not a OutputStreamTaskListener, use TeeBuildListener for " + fileStorageBuildListener);
result = new TeeBuildListener(otelLogSenderBuildListener, fileStorageBuildListener);
}
} catch (IOException | InterruptedException e) {
throw new IOException("Failure creating the mirror logs.", e);
}
}
} else {
result = otelLogSenderBuildListener;
}

return result;
}

/**
* @param flowNode a running node
* @return a {@link BuildListener} rather than a {@link hudson.model.TaskListener} because the caller of
* {@link LogStorage#nodeListener(FlowNode)} will wrap any {@link hudson.model.TaskListener} into a {@link BuildListener}
* causing problems in {@link OutputStreamTaskListener#getOutputStream(TaskListener)}
* @throws IOException
*/
@NonNull
@Override
public TaskListener nodeListener(@NonNull FlowNode flowNode) throws IOException {
public BuildListener nodeListener(@NonNull FlowNode flowNode) throws IOException {
OpenTelemetryConfiguration otelConfiguration = JenkinsOpenTelemetryPluginConfiguration.get().toOpenTelemetryConfiguration();
Map<String, String> otelConfigurationProperties = otelConfiguration.toOpenTelemetryProperties();
Map<String, String> otelResourceAttributes = new HashMap<>();
otelConfiguration.toOpenTelemetryResource().getAttributes().asMap().forEach((k, v) -> otelResourceAttributes.put(k.getKey(), v.toString()));

Span span = otelTraceService.getSpan(run, flowNode);
FlowNodeTraceContext flowNodeTraceContext = FlowNodeTraceContext.newFlowNodeTraceContext(run, flowNode, span);
TaskListener result = new OtelLogSenderBuildListener.OtelLogSenderBuildListenerOnController(flowNodeTraceContext, otelConfigurationProperties, otelResourceAttributes);
OtelLogSenderBuildListener otelLogSenderBuildListener = new OtelLogSenderBuildListener.OtelLogSenderBuildListenerOnController(flowNodeTraceContext, otelConfigurationProperties, otelResourceAttributes);

BuildListener result;
if (OpenTelemetrySdkProvider.get().isOtelLogsMirrorToDisk()) {
try {
File logFile = new File(runFolderPath, "log");
result = new TeeTaskListener(result, FileLogStorage.forFile(logFile).nodeListener(flowNode));
} catch (IOException|InterruptedException e) {
File logFile = new File(runFolderPath, "log");
BuildListener fileStorageBuildListener = BuildListenerAdapter.wrap(FileLogStorage.forFile(logFile).nodeListener(flowNode));
if (fileStorageBuildListener instanceof OutputStreamTaskListener) {
result = new TeeOutputStreamBuildListener(otelLogSenderBuildListener, fileStorageBuildListener);
} else {
logger.log(Level.INFO, () -> "nodeListener(): FileLogStorage's TaskListener is not a OutputStreamTaskListener, use TeeBuildListener for " + fileStorageBuildListener);
result = new TeeBuildListener(otelLogSenderBuildListener, fileStorageBuildListener);
}
} catch (IOException | InterruptedException e) {
throw new IOException("Failure creating the mirror logs.", e);
}
}
} else {
result = otelLogSenderBuildListener;
}

return result;
}

Expand Down Expand Up @@ -184,6 +212,7 @@ public AnnotatedLargeText<FlowNode> stepLog(@NonNull FlowNode flowNode, boolean
span.end();
}
}

@SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST", justification = "forBuild only accepts Run")
@Deprecated
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@

package io.jenkins.plugins.opentelemetry.job.log.util;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.BuildListener;
import hudson.model.TaskListener;
import edu.umd.cs.findbugs.annotations.NonNull;

import java.io.Closeable;
import java.io.IOException;
import java.io.PrintStream;
import java.util.logging.Level;
import java.util.logging.Logger;

public final class TeeBuildListener implements BuildListener, Closeable {
public final class TeeBuildListener implements BuildListener, AutoCloseable {

private final static Logger logger = Logger.getLogger(TeeBuildListener.class.getName());

Expand All @@ -30,11 +30,7 @@ public TeeBuildListener(TaskListener main, TaskListener secondary) {
@NonNull
@Override
public PrintStream getLogger() {
try {
return new TeePrintStream(main.getLogger(), secondary.getLogger());
} catch (IOException e) {
throw new RuntimeException(e);
}
return new TeePrintStream(main.getLogger(), secondary.getLogger());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright The Original Author or Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.jenkins.plugins.opentelemetry.job.log.util;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.OutputStream;

public class TeeOutputStream extends OutputStream {

final OutputStream primary;
final OutputStream secondary;

public TeeOutputStream(OutputStream primary, OutputStream secondary) {
this.primary = primary;
this.secondary = secondary;
}

@Override
public void write(int b) throws IOException {
primary.write(b);
secondary.write(b);
}

@Override
public void write(@Nonnull byte[] b) throws IOException {
primary.write(b);
secondary.write(b);
}

@Override
public void write(@Nonnull byte[] b, int off, int len) throws IOException {
primary.write(b, off, len);
secondary.write(b, off, len);
}

@Override
public void flush() throws IOException {
primary.flush();
secondary.flush();
}

@Override
public void close() throws IOException {
primary.close();
secondary.close();
}
}
Loading