Skip to content

Commit 65e7833

Browse files
committed
Add cats-effect instrumentation
1 parent e366fcc commit 65e7833

File tree

13 files changed

+808
-0
lines changed

13 files changed

+808
-0
lines changed

.fossa.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,15 @@ targets:
382382
- type: gradle
383383
path: ./
384384
target: ':instrumentation:cassandra:cassandra-4.4:library'
385+
- type: gradle
386+
path: ./
387+
target: ':instrumentation:cats-effect:cats-effect-3.6:bootstrap'
388+
- type: gradle
389+
path: ./
390+
target: ':instrumentation:cats-effect:cats-effect-3.6:javaagent'
391+
- type: gradle
392+
path: ./
393+
target: ':instrumentation:cats-effect:cats-effect-common-3.6:javaagent'
385394
- type: gradle
386395
path: ./
387396
target: ':instrumentation:couchbase:couchbase-2-common:javaagent'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
plugins {
2+
id("otel.javaagent-bootstrap")
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.bootstrap.catseffect.v3_6;
7+
8+
import io.opentelemetry.context.Context;
9+
import io.opentelemetry.context.Scope;
10+
import java.util.concurrent.atomic.AtomicReference;
11+
import java.util.function.Supplier;
12+
import java.util.logging.Level;
13+
import java.util.logging.Logger;
14+
import javax.annotation.Nullable;
15+
16+
/** The helper stores a reference to the IOLocal#unsafeThreadLocal. */
17+
public final class FiberLocalContextHelper {
18+
19+
private static final Logger logger = Logger.getLogger(FiberLocalContextHelper.class.getName());
20+
21+
private static final AtomicReference<ThreadLocal<Context>> fiberContextThreadLocal =
22+
new AtomicReference<>();
23+
24+
private static final AtomicReference<Supplier<Boolean>> isUnderFiberContextSupplier =
25+
new AtomicReference<>(() -> false);
26+
27+
public static void initialize(
28+
ThreadLocal<Context> fiberThreadLocal, Supplier<Boolean> isUnderFiberContext) {
29+
if (fiberContextThreadLocal.get() == null) {
30+
fiberContextThreadLocal.set(fiberThreadLocal);
31+
isUnderFiberContextSupplier.set(isUnderFiberContext);
32+
logger.fine("The fiberThreadLocalContext is configured");
33+
} else {
34+
if (!fiberContextThreadLocal.get().equals(fiberThreadLocal)) {
35+
logger.warning(
36+
"The fiberThreadLocalContext is already configured. Ignoring subsequent calls.");
37+
}
38+
}
39+
}
40+
41+
public static Boolean isUnderFiberContext() {
42+
return isUnderFiberContextSupplier.get().get();
43+
}
44+
45+
@Nullable
46+
public static Context current() {
47+
ThreadLocal<Context> local = getFiberThreadLocal();
48+
return local != null ? local.get() : null;
49+
}
50+
51+
public static Scope attach(Context toAttach) {
52+
ThreadLocal<Context> local = fiberContextThreadLocal.get();
53+
if (toAttach == null || local == null) {
54+
return Scope.noop();
55+
} else {
56+
Context beforeAttach = current();
57+
if (toAttach == beforeAttach) {
58+
return Scope.noop();
59+
} else {
60+
local.set(toAttach);
61+
return new ScopeImpl(beforeAttach, toAttach);
62+
}
63+
}
64+
}
65+
66+
@Nullable
67+
private static ThreadLocal<Context> getFiberThreadLocal() {
68+
return fiberContextThreadLocal.get();
69+
}
70+
71+
private static class ScopeImpl implements Scope {
72+
@Nullable private final Context beforeAttach;
73+
private final Context toAttach;
74+
private boolean closed;
75+
76+
private ScopeImpl(@Nullable Context beforeAttach, Context toAttach) {
77+
this.beforeAttach = beforeAttach;
78+
this.toAttach = toAttach;
79+
}
80+
81+
@Override
82+
public void close() {
83+
if (!this.closed && FiberLocalContextHelper.current() == this.toAttach) {
84+
this.closed = true;
85+
FiberLocalContextHelper.fiberContextThreadLocal.get().set(this.beforeAttach);
86+
} else {
87+
FiberLocalContextHelper.logger.log(
88+
Level.FINE,
89+
"Trying to close scope which does not represent current context. Ignoring the call.");
90+
}
91+
}
92+
}
93+
94+
private FiberLocalContextHelper() {}
95+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
plugins {
2+
id("otel.javaagent-instrumentation")
3+
id("otel.nullaway-conventions")
4+
id("otel.scala-conventions")
5+
}
6+
7+
val scalaVersion = "2.13"
8+
val catsEffectVersion = "3.6.0"
9+
10+
muzzle {
11+
pass {
12+
group.set("org.typelevel")
13+
module.set("cats-effect_2.12")
14+
versions.set("[$catsEffectVersion,)")
15+
assertInverse.set(true)
16+
}
17+
pass {
18+
group.set("org.typelevel")
19+
module.set("cats-effect_2.13")
20+
versions.set("[$catsEffectVersion,)")
21+
assertInverse.set(true)
22+
}
23+
pass {
24+
group.set("org.typelevel")
25+
module.set("cats-effect_3")
26+
versions.set("[$catsEffectVersion,)")
27+
assertInverse.set(true)
28+
}
29+
}
30+
31+
dependencies {
32+
bootstrap(project(":instrumentation:cats-effect:cats-effect-3.6:bootstrap"))
33+
34+
// we need access to the "application.io.opentelemetry.context.Context"
35+
// to properly bridge fiber and agent context storages
36+
compileOnly(project(":opentelemetry-api-shaded-for-instrumenting", configuration = "shadow"))
37+
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
38+
implementation(project(":instrumentation:opentelemetry-api:opentelemetry-api-1.0:javaagent"))
39+
40+
implementation(project(":instrumentation:cats-effect:cats-effect-common-3.6:javaagent"))
41+
42+
compileOnly("org.typelevel:cats-effect_$scalaVersion:$catsEffectVersion")
43+
44+
testImplementation("org.typelevel:cats-effect_$scalaVersion:$catsEffectVersion")
45+
46+
latestDepTestLibrary("org.typelevel:cats-effect_$scalaVersion:latest.release")
47+
}
48+
49+
tasks {
50+
withType<Test>().configureEach {
51+
jvmArgs("-Dio.opentelemetry.javaagent.shaded.io.opentelemetry.context.enableStrictContext=false")
52+
jvmArgs("-Dcats.effect.trackFiberContext=true")
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.catseffect.v3_6;
7+
8+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
9+
10+
import com.google.auto.service.AutoService;
11+
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
12+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
13+
import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
14+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
15+
import java.util.Arrays;
16+
import java.util.List;
17+
import net.bytebuddy.matcher.ElementMatcher;
18+
19+
@AutoService(InstrumentationModule.class)
20+
public class CatsEffectInstrumentationModule extends InstrumentationModule
21+
implements ExperimentalInstrumentationModule {
22+
23+
public CatsEffectInstrumentationModule() {
24+
super("cats-effect", "cats-effect-3.6");
25+
}
26+
27+
@Override
28+
public List<TypeInstrumentation> typeInstrumentations() {
29+
return Arrays.asList(new IoRuntimeInstrumentation(), new IoInstrumentation());
30+
}
31+
32+
@Override
33+
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
34+
return hasClassesNamed("cats.effect.IO")
35+
// missing before 3.6.0
36+
.and(hasClassesNamed("cats.effect.unsafe.metrics.IORuntimeMetrics"));
37+
}
38+
39+
@Override
40+
public boolean defaultEnabled(ConfigProperties config) {
41+
return super.defaultEnabled(config)
42+
&& config.getBoolean("cats.effect.trackFiberContext", false);
43+
}
44+
45+
// ensure it's the last one
46+
@Override
47+
public int order() {
48+
return Integer.MAX_VALUE;
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.catseffect.v3_6;
7+
8+
import io.opentelemetry.context.Context;
9+
import io.opentelemetry.context.ContextStorage;
10+
import io.opentelemetry.context.Scope;
11+
import io.opentelemetry.javaagent.bootstrap.catseffect.v3_6.FiberLocalContextHelper;
12+
import javax.annotation.Nullable;
13+
14+
public class FiberContextBridge implements ContextStorage {
15+
16+
private final ContextStorage agentContextStorage;
17+
18+
public FiberContextBridge(ContextStorage delegate) {
19+
this.agentContextStorage = delegate;
20+
}
21+
22+
@Override
23+
public Scope attach(Context toAttach) {
24+
if (FiberLocalContextHelper.isUnderFiberContext()) {
25+
return FiberLocalContextHelper.attach(toAttach);
26+
} else {
27+
return agentContextStorage.attach(toAttach);
28+
}
29+
}
30+
31+
@Nullable
32+
@Override
33+
public Context current() {
34+
if (FiberLocalContextHelper.isUnderFiberContext()) {
35+
return FiberLocalContextHelper.current();
36+
} else {
37+
return agentContextStorage.current();
38+
}
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.catseffect.v3_6;
7+
8+
import com.google.auto.service.AutoService;
9+
import io.opentelemetry.context.ContextStorage;
10+
import io.opentelemetry.javaagent.tooling.BeforeAgentListener;
11+
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
12+
import java.util.logging.Logger;
13+
14+
/**
15+
* A {@link BeforeAgentListener} that installs {@link FiberContextBridge} if `cats.effect.IO` is
16+
* present in the classpath.
17+
*/
18+
@AutoService(BeforeAgentListener.class)
19+
public class FiberContextBridgeInstaller implements BeforeAgentListener {
20+
21+
private static final Logger logger =
22+
Logger.getLogger(FiberContextBridgeInstaller.class.getName());
23+
24+
@Override
25+
public void beforeAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) {
26+
ContextStorage.addWrapper(FiberContextBridge::new);
27+
logger.fine("Installed Cats Effect FiberContextBridge");
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.catseffect.v3_6;
7+
8+
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
9+
import static net.bytebuddy.matcher.ElementMatchers.named;
10+
11+
import application.io.opentelemetry.context.Context;
12+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
13+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
14+
import io.opentelemetry.javaagent.instrumentation.catseffect.common.v3_6.IoLocalContextSingleton;
15+
import net.bytebuddy.asm.Advice;
16+
import net.bytebuddy.description.type.TypeDescription;
17+
import net.bytebuddy.matcher.ElementMatcher;
18+
19+
public class IoInstrumentation implements TypeInstrumentation {
20+
21+
@Override
22+
public ElementMatcher<TypeDescription> typeMatcher() {
23+
return named("cats.effect.IO");
24+
}
25+
26+
@Override
27+
public void transform(TypeTransformer transformer) {
28+
transformer.applyAdviceToMethod(
29+
isMethod().and(named("unsafeRunFiber")),
30+
this.getClass().getName() + "$UnsafeRunFiberAdvice");
31+
}
32+
33+
@SuppressWarnings("unused")
34+
public static final class UnsafeRunFiberAdvice {
35+
private UnsafeRunFiberAdvice() {}
36+
37+
@Advice.OnMethodEnter(suppress = Throwable.class)
38+
public static void onEnter(@Advice.This(readOnly = false) cats.effect.IO<?> io) {
39+
io = IoLocalContextSingleton.ioLocal.asLocal().scope(io, Context.current());
40+
}
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.catseffect.v3_6;
7+
8+
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
9+
import static net.bytebuddy.matcher.ElementMatchers.named;
10+
11+
import cats.effect.unsafe.IORuntime;
12+
import io.opentelemetry.javaagent.bootstrap.catseffect.v3_6.FiberLocalContextHelper;
13+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
14+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
15+
import io.opentelemetry.javaagent.instrumentation.catseffect.common.v3_6.IoLocalContextSingleton;
16+
import net.bytebuddy.asm.Advice;
17+
import net.bytebuddy.description.type.TypeDescription;
18+
import net.bytebuddy.matcher.ElementMatcher;
19+
20+
public class IoRuntimeInstrumentation implements TypeInstrumentation {
21+
22+
@Override
23+
public ElementMatcher<TypeDescription> typeMatcher() {
24+
return named("cats.effect.unsafe.IORuntime");
25+
}
26+
27+
@Override
28+
public void transform(TypeTransformer transformer) {
29+
transformer.applyAdviceToMethod(
30+
isConstructor(), this.getClass().getName() + "$ConstructorAdvice");
31+
}
32+
33+
@SuppressWarnings("unused")
34+
public static final class ConstructorAdvice {
35+
private ConstructorAdvice() {}
36+
37+
@Advice.OnMethodEnter(suppress = Throwable.class)
38+
public static void onEnter() {
39+
FiberLocalContextHelper.initialize(
40+
IoLocalContextSingleton.contextThreadLocal, IORuntime::isUnderFiberContext);
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)