Ratpack services OpenTelemetry (#7477)

While using Ratpack Handlers, the context is added by the
`OpenTelemetryServerHandler`

While using Ratpack Services, we might need to add manually the OT
Context into the Ratpack Execution and try to get it from the Execution
in the Ratpack `HttpClient`
This commit is contained in:
Javier Salinas 2023-01-25 19:28:44 +01:00 committed by GitHub
parent fc81be47e5
commit 99e600fff7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 155 additions and 13 deletions

View File

@ -28,7 +28,8 @@ final class OpenTelemetryHttpClient {
httpClientSpec -> { httpClientSpec -> {
httpClientSpec.requestIntercept( httpClientSpec.requestIntercept(
requestSpec -> { requestSpec -> {
Context parentOtelCtx = Context.current(); Context parentOtelCtx =
Execution.current().maybeGet(Context.class).orElse(Context.current());
if (!instrumenter.shouldStart(parentOtelCtx, requestSpec)) { if (!instrumenter.shouldStart(parentOtelCtx, requestSpec)) {
return; return;
} }

View File

@ -5,9 +5,11 @@
package io.opentelemetry.instrumentation.ratpack.client package io.opentelemetry.instrumentation.ratpack.client
import io.opentelemetry.api.trace.SpanKind import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.api.trace.StatusCode import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator
import io.opentelemetry.context.Context
import io.opentelemetry.context.propagation.ContextPropagators import io.opentelemetry.context.propagation.ContextPropagators
import io.opentelemetry.instrumentation.ratpack.RatpackTelemetry import io.opentelemetry.instrumentation.ratpack.RatpackTelemetry
import io.opentelemetry.sdk.OpenTelemetrySdk import io.opentelemetry.sdk.OpenTelemetrySdk
@ -15,10 +17,13 @@ import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter
import io.opentelemetry.sdk.trace.SdkTracerProvider import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import ratpack.exec.Execution
import ratpack.exec.Promise import ratpack.exec.Promise
import ratpack.func.Action import ratpack.func.Action
import ratpack.guice.Guice import ratpack.guice.Guice
import ratpack.http.client.HttpClient import ratpack.http.client.HttpClient
import ratpack.service.Service
import ratpack.service.StartEvent
import ratpack.test.embed.EmbeddedApp import ratpack.test.embed.EmbeddedApp
import spock.lang.Specification import spock.lang.Specification
import spock.util.concurrent.PollingConditions import spock.util.concurrent.PollingConditions
@ -27,6 +32,8 @@ import java.time.Duration
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import static io.opentelemetry.api.trace.SpanKind.CLIENT
import static io.opentelemetry.api.trace.SpanKind.SERVER
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_METHOD
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_ROUTE
import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_STATUS_CODE
@ -84,14 +91,14 @@ class InstrumentedHttpClientTest extends Specification {
new PollingConditions().eventually { new PollingConditions().eventually {
def spanData = spanExporter.finishedSpanItems.find { it.name == "/foo" } def spanData = spanExporter.finishedSpanItems.find { it.name == "/foo" }
def spanClientData = spanExporter.finishedSpanItems.find { it.name == "HTTP GET" && it.kind == SpanKind.CLIENT } def spanClientData = spanExporter.finishedSpanItems.find { it.name == "HTTP GET" && it.kind == CLIENT }
def spanDataApi = spanExporter.finishedSpanItems.find { it.name == "/bar" && it.kind == SpanKind.SERVER } def spanDataApi = spanExporter.finishedSpanItems.find { it.name == "/bar" && it.kind == SERVER }
spanData.traceId == spanClientData.traceId spanData.traceId == spanClientData.traceId
spanData.traceId == spanDataApi.traceId spanData.traceId == spanDataApi.traceId
spanData.kind == SpanKind.SERVER spanData.kind == SERVER
spanClientData.kind == SpanKind.CLIENT spanClientData.kind == CLIENT
def atts = spanClientData.attributes.asMap() def atts = spanClientData.attributes.asMap()
atts[HTTP_ROUTE] == "/bar" atts[HTTP_ROUTE] == "/bar"
atts[HTTP_METHOD] == "GET" atts[HTTP_METHOD] == "GET"
@ -154,15 +161,15 @@ class InstrumentedHttpClientTest extends Specification {
spanData.traceId == spanClientData1.traceId spanData.traceId == spanClientData1.traceId
spanData.traceId == spanClientData2.traceId spanData.traceId == spanClientData2.traceId
spanData.kind == SpanKind.SERVER spanData.kind == SERVER
spanClientData1.kind == SpanKind.CLIENT spanClientData1.kind == CLIENT
def atts = spanClientData1.attributes.asMap() def atts = spanClientData1.attributes.asMap()
atts[HTTP_ROUTE] == "/foo" atts[HTTP_ROUTE] == "/foo"
atts[HTTP_METHOD] == "GET" atts[HTTP_METHOD] == "GET"
atts[HTTP_STATUS_CODE] == 200L atts[HTTP_STATUS_CODE] == 200L
spanClientData2.kind == SpanKind.CLIENT spanClientData2.kind == CLIENT
def atts2 = spanClientData2.attributes.asMap() def atts2 = spanClientData2.attributes.asMap()
atts2[HTTP_ROUTE] == "/bar" atts2[HTTP_ROUTE] == "/bar"
atts2[HTTP_METHOD] == "GET" atts2[HTTP_METHOD] == "GET"
@ -207,8 +214,7 @@ class InstrumentedHttpClientTest extends Specification {
} }
} }
app.test { httpClient -> "error" == httpClient.get("path-name").body.text app.test { httpClient -> "error" == httpClient.get("path-name").body.text }
}
new PollingConditions().eventually { new PollingConditions().eventually {
def spanData = spanExporter.finishedSpanItems.find { it.name == "/path-name" } def spanData = spanExporter.finishedSpanItems.find { it.name == "/path-name" }
@ -216,8 +222,8 @@ class InstrumentedHttpClientTest extends Specification {
spanData.traceId == spanClientData.traceId spanData.traceId == spanClientData.traceId
spanData.kind == SpanKind.SERVER spanData.kind == SERVER
spanClientData.kind == SpanKind.CLIENT spanClientData.kind == CLIENT
def atts = spanClientData.attributes.asMap() def atts = spanClientData.attributes.asMap()
atts[HTTP_ROUTE] == "/foo" atts[HTTP_ROUTE] == "/foo"
atts[HTTP_METHOD] == "GET" atts[HTTP_METHOD] == "GET"
@ -232,4 +238,139 @@ class InstrumentedHttpClientTest extends Specification {
attributes[HTTP_STATUS_CODE] == 200L attributes[HTTP_STATUS_CODE] == 200L
} }
} }
def "propagate http trace in ratpack services with compute thread"() {
expect:
def latch = new CountDownLatch(1)
def otherApp = EmbeddedApp.of { spec ->
spec.handlers {
it.get("foo") { ctx -> ctx.render("bar") }
}
}
def app = EmbeddedApp.of { spec ->
spec.registry(
Guice.registry { bindings ->
telemetry.configureServerRegistry(bindings)
bindings.bindInstance(HttpClient, telemetry.instrumentHttpClient(HttpClient.of(Action.noop())))
bindings.bindInstance(new BarService(latch, "${otherApp.address}foo", openTelemetry))
},
)
spec.handlers { chain ->
chain.get("foo") { ctx -> ctx.render("bar") }
}
}
app.address
latch.await()
new PollingConditions().eventually {
def spanData = spanExporter.finishedSpanItems.find { it.name == "a-span" }
def trace = spanExporter.finishedSpanItems.findAll { it.traceId == spanData.traceId }
trace.size() == 3
}
}
def "propagate http trace in ratpack services with fork executions"() {
expect:
def latch = new CountDownLatch(1)
def otherApp = EmbeddedApp.of { spec ->
spec.handlers {
it.get("foo") { ctx -> ctx.render("bar") }
}
}
def app = EmbeddedApp.of { spec ->
spec.registry(
Guice.registry { bindings ->
telemetry.configureServerRegistry(bindings)
bindings.bindInstance(HttpClient, telemetry.instrumentHttpClient(HttpClient.of(Action.noop())))
bindings.bindInstance(new BarForkService(latch, "${otherApp.address}foo", openTelemetry))
},
)
spec.handlers { chain ->
chain.get("foo") { ctx -> ctx.render("bar") }
}
}
app.address
latch.await()
new PollingConditions().eventually {
def spanData = spanExporter.finishedSpanItems.find { it.name == "a-span" }
def trace = spanExporter.finishedSpanItems.findAll { it.traceId == spanData.traceId }
trace.size() == 3
}
}
}
class BarService implements Service {
private final String url
private final CountDownLatch latch
private final OpenTelemetry opentelemetry
BarService(CountDownLatch latch, String url, OpenTelemetry opentelemetry) {
this.latch = latch
this.url = url
this.opentelemetry = opentelemetry
}
private Tracer tracer = opentelemetry.tracerProvider.tracerBuilder("testing").build()
void onStart(StartEvent event) {
def parentContext = Context.current()
def span = tracer.spanBuilder("a-span")
.setParent(parentContext)
.startSpan()
Context otelContext = parentContext.with(span)
otelContext.makeCurrent().withCloseable {
Execution.current().add(Context, otelContext)
def httpClient = event.registry.get(HttpClient)
httpClient.get(new URI(url))
.flatMap { httpClient.get(new URI(url)) }
.then {
span.end()
latch.countDown()
}
}
}
}
class BarForkService implements Service {
private final String url
private final CountDownLatch latch
private final OpenTelemetry opentelemetry
BarForkService(CountDownLatch latch, String url, OpenTelemetry opentelemetry) {
this.latch = latch
this.url = url
this.opentelemetry = opentelemetry
}
private Tracer tracer = opentelemetry.tracerProvider.tracerBuilder("testing").build()
void onStart(StartEvent event) {
Execution.fork().start {
def parentContext = Context.current()
def span = tracer.spanBuilder("a-span")
.setParent(parentContext)
.startSpan()
Context otelContext = parentContext.with(span)
otelContext.makeCurrent().withCloseable {
Execution.current().add(Context, otelContext)
def httpClient = event.registry.get(HttpClient)
httpClient.get(new URI(url))
.flatMap { httpClient.get(new URI(url)) }
.then {
span.end()
latch.countDown()
}
}
}
}
} }