Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add java.util.logging javaagent instrumentation #4941

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ tasks.withType<ShadowJar>().configureEach {

// this is for instrumentation on opentelemetry-api itself
relocate("application.io.opentelemetry", "io.opentelemetry")

// this is for instrumentation on java.util.logging (since java.util.logging itself is shaded above)
relocate("application.java.util.logging", "java.util.logging")
}
12 changes: 12 additions & 0 deletions instrumentation/java-util-logging/javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
id("otel.javaagent-instrumentation")
}

dependencies {
compileOnly(project(":instrumentation:java-util-logging:shaded-stub-for-instrumenting"))

compileOnly(project(":instrumentation-api-appender"))

testLibrary("org.jboss.logmanager:jboss-logmanager:1.0.0.GA")
testImplementation("org.awaitility:awaitility")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.jul;

import application.java.util.logging.Logger;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.appender.GlobalLogEmitterProvider;
import io.opentelemetry.instrumentation.api.appender.LogBuilder;
import io.opentelemetry.instrumentation.api.appender.Severity;
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.concurrent.TimeUnit;
import java.util.logging.Formatter;
import java.util.logging.Level;
import java.util.logging.LogRecord;

public class JavaUtilLoggingHelper {
trask marked this conversation as resolved.
Show resolved Hide resolved

private static final Formatter FORMATTER = new AccessibleFormatter();

public static void capture(Logger logger, LogRecord logRecord) {

if (!logger.isLoggable(logRecord.getLevel())) {
// this is already checked in most cases, except if Logger.log(LogRecord) was called directly
return;
}

LogBuilder builder =
GlobalLogEmitterProvider.get().logEmitterBuilder(logger.getName()).build().logBuilder();
mapLogRecord(builder, logRecord);
builder.emit();
}

/**
* Map the {@link LogRecord} data model onto the {@link LogBuilder}. Unmapped fields include:
*
* <ul>
* <li>Fully qualified class name - {@link LogRecord#getSourceClassName()}
* <li>Fully qualified method name - {@link LogRecord#getSourceMethodName()}
* <li>Thread id - {@link LogRecord#getThreadID()}
* </ul>
*/
private static void mapLogRecord(LogBuilder builder, LogRecord logRecord) {
// message
String message = FORMATTER.formatMessage(logRecord);
if (message != null) {
builder.setBody(message);
}

// time
// TODO (trask) use getInstant() for more precision on Java 9
long timestamp = logRecord.getMillis();
builder.setEpoch(timestamp, TimeUnit.MILLISECONDS);

// level
Level level = logRecord.getLevel();
if (level != null) {
builder.setSeverity(levelToSeverity(level));
builder.setSeverityText(logRecord.getLevel().getName());
}

// throwable
Throwable throwable = logRecord.getThrown();
if (throwable != null) {
AttributesBuilder attributes = Attributes.builder();

// TODO (trask) extract method for recording exception into instrumentation-api-appender
attributes.put(SemanticAttributes.EXCEPTION_TYPE, throwable.getClass().getName());
attributes.put(SemanticAttributes.EXCEPTION_MESSAGE, throwable.getMessage());
StringWriter writer = new StringWriter();
throwable.printStackTrace(new PrintWriter(writer));
attributes.put(SemanticAttributes.EXCEPTION_STACKTRACE, writer.toString());

builder.setAttributes(attributes.build());
}

// span context
builder.setContext(Context.current());
}

private static Severity levelToSeverity(Level level) {
int lev = level.intValue();
if (lev <= Level.FINEST.intValue()) {
return Severity.TRACE;
}
if (lev <= Level.FINER.intValue()) {
return Severity.DEBUG;
}
if (lev <= Level.FINE.intValue()) {
return Severity.DEBUG2;
}
if (lev <= Level.CONFIG.intValue()) {
return Severity.DEBUG3;
}
if (lev <= Level.INFO.intValue()) {
return Severity.INFO;
}
if (lev <= Level.WARNING.intValue()) {
return Severity.WARN;
}
if (lev <= Level.SEVERE.intValue()) {
return Severity.ERROR;
}
return Severity.FATAL;
}

// this is just needed for calling formatMessage in abstract super class
private static class AccessibleFormatter extends Formatter {

@Override
public String format(final LogRecord record) {
throw new UnsupportedOperationException();
}
}

private JavaUtilLoggingHelper() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.jul;

import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import application.java.util.logging.Logger;
import io.opentelemetry.instrumentation.api.appender.LogEmitterProvider;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import io.opentelemetry.javaagent.instrumentation.api.CallDepth;
import java.util.logging.LogRecord;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

class JavaUtilLoggingInstrumentation implements TypeInstrumentation {

@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return extendsClass(named("application.java.util.logging.Logger"));
}

@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
isMethod()
.and(isPublic())
.and(named("log"))
.and(takesArguments(1))
.and(takesArgument(0, named("java.util.logging.LogRecord"))),
JavaUtilLoggingInstrumentation.class.getName() + "$LogAdvice");
transformer.applyAdviceToMethod(
trask marked this conversation as resolved.
Show resolved Hide resolved
isMethod()
.and(isPublic())
.and(named("logRaw"))
.and(takesArguments(1))
.and(takesArgument(0, named("org.jboss.logmanager.ExtLogRecord"))),
JavaUtilLoggingInstrumentation.class.getName() + "$LogAdvice");
}

@SuppressWarnings("unused")
public static class LogAdvice {

@Advice.OnMethodEnter(suppress = Throwable.class)
public static void methodEnter(
@Advice.This final Logger logger,
@Advice.Argument(0) final LogRecord logRecord,
@Advice.Local("otelCallDepth") CallDepth callDepth) {
// need to track call depth across all loggers in order to avoid double capture when one
// logging framework delegates to another
callDepth = CallDepth.forClass(LogEmitterProvider.class);
if (callDepth.getAndIncrement() == 0) {
JavaUtilLoggingHelper.capture(logger, logRecord);
}
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void methodExit(@Advice.Local("otelCallDepth") CallDepth callDepth) {
callDepth.decrementAndGet();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.instrumentation.jul;

import static java.util.Collections.singletonList;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;

@AutoService(InstrumentationModule.class)
public class JavaUtilLoggingInstrumentationModule extends InstrumentationModule {

public JavaUtilLoggingInstrumentationModule() {
super("java-util-logging");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new JavaUtilLoggingInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

import org.jboss.logmanager.LogContext

class JBossJavaUtilLoggingTest extends JavaUtilLoggingTest {

@Override
Object createLogger(String name) {
LogContext.create().getLogger(name)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
import io.opentelemetry.sdk.logs.data.Severity
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import spock.lang.Shared

import java.util.logging.Level
import java.util.logging.Logger

import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace
import static org.assertj.core.api.Assertions.assertThat
import static org.awaitility.Awaitility.await

class JavaUtilLoggingTest extends AgentInstrumentationSpecification {

@Shared
private final Object logger = createLogger("abc")

Object createLogger(String name) {
Logger.getLogger(name)
}

def "test method=#testMethod with exception=#exception and parent=#parent"() {
when:
if (parent) {
runUnderTrace("parent") {
if (exception) {
logger.log(Level."${testMethod.toUpperCase()}", "xyz", new IllegalStateException("hello"))
} else {
logger."$testMethod"("xyz")
}
}
} else {
if (exception) {
logger.log(Level."${testMethod.toUpperCase()}", "xyz", new IllegalStateException("hello"))
} else {
logger."$testMethod"("xyz")
}
}

then:
if (parent) {
waitForTraces(1)
}

if (severity != null) {
await()
.untilAsserted(
() -> {
assertThat(logs).hasSize(1)
})
def log = logs.get(0)
assertThat(log.getBody().asString()).isEqualTo("xyz")
assertThat(log.getInstrumentationLibraryInfo().getName()).isEqualTo("abc")
assertThat(log.getSeverity()).isEqualTo(severity)
assertThat(log.getSeverityText()).isEqualTo(severityText)
if (exception) {
assertThat(log.getAttributes().get(SemanticAttributes.EXCEPTION_TYPE)).isEqualTo(IllegalStateException.class.getName())
assertThat(log.getAttributes().get(SemanticAttributes.EXCEPTION_MESSAGE)).isEqualTo("hello")
assertThat(log.getAttributes().get(SemanticAttributes.EXCEPTION_STACKTRACE)).contains(JavaUtilLoggingTest.name)
} else {
assertThat(log.getAttributes().get(SemanticAttributes.EXCEPTION_TYPE)).isNull()
assertThat(log.getAttributes().get(SemanticAttributes.EXCEPTION_MESSAGE)).isNull()
assertThat(log.getAttributes().get(SemanticAttributes.EXCEPTION_STACKTRACE)).isNull()
}
if (parent) {
assertThat(log.getSpanContext()).isEqualTo(traces.get(0).get(0).getSpanContext())
} else {
assertThat(log.getSpanContext().isValid()).isFalse()
}
} else {
Thread.sleep(500) // sleep a bit just to make sure no span is captured
trask marked this conversation as resolved.
Show resolved Hide resolved
logs.size() == 0
}

where:
[args, exception, parent] << [
[
["fine", null, null],
["info", Severity.INFO, "INFO"],
["warning", Severity.WARN, "WARNING"],
["severe", Severity.ERROR, "SEVERE"]
],
[true, false],
[true, false]
].combinations()

testMethod = args[0]
severity = args[1]
severityText = args[2]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
plugins {
id("otel.java-conventions")
}
Loading