Skip to content

Commit 30632e3

Browse files
committed
Use semconv attributes
1 parent a4c2d41 commit 30632e3

4 files changed

Lines changed: 49 additions & 148 deletions

File tree

build.sbt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,7 @@ lazy val otel4s =
125125
.in(file("otel4s"))
126126
.settings(
127127
moduleName := "clue-otel4s",
128-
libraryDependencies ++=
129-
Settings.Libraries.Otel4s.value ++
130-
Settings.Libraries.Otel4sTestkit.value ++
131-
Settings.Libraries.MUnitCatsEffect.value ++
132-
Settings.Libraries.MUnit.value
128+
libraryDependencies ++= Settings.Libraries.Otel4s.value
133129
)
134130
.dependsOn(core)
135131

natchez/src/main/scala/clue/natchez/NatchezMiddleware.scala

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import cats.Applicative
77
import cats.effect.MonadCancelThrow
88
import cats.effect.Resource
99
import cats.syntax.all.*
10-
import clue.BuildInfo
1110
import clue.FetchClientWithPars
1211
import clue.StreamingClient
1312
import clue.model.GraphQLQuery
@@ -117,8 +116,6 @@ class NatchezFetchClient[F[_]: Trace: MonadCancelThrow, P, S](
117116
MonadCancelThrow[F].uncancelable: poll =>
118117
Trace[F].span(s"clue-client-request-${document.querySummary}", spanOptions):
119118
for
120-
// Add clue library version
121-
_ <- Trace[F].put("clue.version" -> BuildInfo.version)
122119
additionalAttributes <- additionalAttributesF(document, variables)
123120
_ <- Trace[F].put(additionalAttributes*)
124121
result <- poll:
@@ -130,54 +127,6 @@ class NatchezFetchClient[F[_]: Trace: MonadCancelThrow, P, S](
130127
"clue.response.errors" -> errs.toList.mkString("[", ", ", "]")
131128
yield result
132129

133-
// Extension methods for convenient tracing
134-
object http4s:
135-
extension [F[_], P, S](client: F[FetchClientWithPars[F, P, S]]) {
136-
@scala.annotation.targetName("tracedFetchClient")
137-
def traced(using
138-
cats.effect.MonadCancelThrow[F],
139-
natchez.Trace[F],
140-
cats.Functor[F]
141-
): F[FetchClientWithPars[F, P, S]] =
142-
client.map(NatchezMiddleware(_))
143-
144-
@scala.annotation.targetName("tracedWithFetchClient")
145-
def tracedWith(
146-
spanOptions: natchez.Span.Options,
147-
additionalAttributesF: (clue.model.GraphQLQuery, Option[io.circe.JsonObject]) => F[
148-
Seq[(String, natchez.TraceValue)]
149-
]
150-
)(using
151-
cats.effect.MonadCancelThrow[F],
152-
natchez.Trace[F],
153-
cats.Functor[F]
154-
): F[FetchClientWithPars[F, P, S]] =
155-
client.map(NatchezMiddleware(_, spanOptions, additionalAttributesF))
156-
}
157-
158-
extension [F[_], S](client: F[StreamingClient[F, S]]) {
159-
@scala.annotation.targetName("tracedStreamingClient")
160-
def traced(using
161-
cats.effect.MonadCancelThrow[F],
162-
natchez.Trace[F],
163-
cats.Functor[F]
164-
): F[StreamingClient[F, S]] =
165-
client.map(NatchezMiddleware(_))
166-
167-
@scala.annotation.targetName("tracedWithStreamingClient")
168-
def tracedWith(
169-
spanOptions: natchez.Span.Options,
170-
additionalAttributesF: (clue.model.GraphQLQuery, Option[io.circe.JsonObject]) => F[
171-
Seq[(String, natchez.TraceValue)]
172-
]
173-
)(using
174-
cats.effect.MonadCancelThrow[F],
175-
natchez.Trace[F],
176-
cats.Functor[F]
177-
): F[StreamingClient[F, S]] =
178-
client.map(NatchezMiddleware(_, spanOptions, additionalAttributesF))
179-
}
180-
181130
class NatchezStreamingClient[F[_]: Trace: MonadCancelThrow, S](
182131
wrapped: StreamingClient[F, S],
183132
spanOptions: Span.Options,
@@ -193,8 +142,6 @@ class NatchezStreamingClient[F[_]: Trace: MonadCancelThrow, S](
193142
Resource.applyFull: poll =>
194143
Trace[F].span(s"clue-client-start-${document.querySummary}", spanOptions):
195144
for
196-
// Add clue library version
197-
_ <- Trace[F].put("clue.version" -> BuildInfo.version)
198145
additionalAttributes <- additionalAttributesF(document, variables)
199146
_ <- Trace[F].put(additionalAttributes*)
200147
result <- poll:

otel4s/src/main/scala/clue/otel4s/Otel4sMiddleware.scala

Lines changed: 45 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,31 @@ import cats.effect.Resource
99
import cats.syntax.all.*
1010
import clue.BuildInfo
1111
import clue.FetchClientWithPars
12+
import clue.PersistentClientStatus
13+
import clue.PersistentStreamingClient
1214
import clue.StreamingClient
1315
import clue.model.GraphQLQuery
1416
import clue.model.GraphQLResponse
1517
import io.circe.Decoder
18+
import io.circe.Encoder
1619
import io.circe.JsonObject
1720
import org.typelevel.otel4s.Attribute
21+
import org.typelevel.otel4s.semconv.attributes.HttpAttributes
22+
import org.typelevel.otel4s.semconv.experimental.attributes.GraphqlExperimentalAttributes
1823
import org.typelevel.otel4s.trace.Span
1924
import org.typelevel.otel4s.trace.SpanBuilder
2025
import org.typelevel.otel4s.trace.SpanKind
2126
import org.typelevel.otel4s.trace.StatusCode
2227
import org.typelevel.otel4s.trace.Tracer
2328

2429
object Otel4sMiddleware:
25-
private[otel4s] def extractOperationType(query: String): Option[String] =
30+
import GraphqlExperimentalAttributes.GraphqlOperationTypeValue
31+
32+
private[otel4s] def extractOperationType(query: String): Option[GraphqlOperationTypeValue] =
2633
val trimmed = query.trim.toLowerCase
27-
if trimmed.startsWith("query") then Some("query")
28-
else if trimmed.startsWith("mutation") then Some("mutation")
29-
else if trimmed.startsWith("subscription") then Some("subscription")
34+
if trimmed.startsWith("query") then Some(GraphqlOperationTypeValue.Query)
35+
else if trimmed.startsWith("mutation") then Some(GraphqlOperationTypeValue.Mutation)
36+
else if trimmed.startsWith("subscription") then Some(GraphqlOperationTypeValue.Subscription)
3037
else None
3138

3239
type SpanBuilderMod[F[_]] = SpanBuilder[F] => SpanBuilder[F]
@@ -37,80 +44,60 @@ object Otel4sMiddleware:
3744
: (GraphQLQuery, Option[JsonObject]) => F[List[Attribute[?]]] =
3845
(_, _) => List.empty[Attribute[?]].pure[F]
3946

40-
// Fetch client overloads
47+
// Fetch client
4148
def apply[F[_]: Tracer: MonadCancelThrow, P, S](
4249
client: FetchClientWithPars[F, P, S],
4350
spanBuilderMod: SpanBuilderMod[F],
4451
additionalAttributesF: (GraphQLQuery, Option[JsonObject]) => F[List[Attribute[?]]]
4552
): FetchClientWithPars[F, P, S] =
4653
Otel4sFetchClient[F, P, S](client, spanBuilderMod, additionalAttributesF)
4754

48-
def apply[F[_]: Tracer: MonadCancelThrow, P, S](
49-
client: FetchClientWithPars[F, P, S],
50-
additionalAttributesF: (GraphQLQuery, Option[JsonObject]) => F[List[Attribute[?]]]
51-
): FetchClientWithPars[F, P, S] =
52-
apply(client, identityMod[F], additionalAttributesF)
53-
5455
def apply[F[_]: Tracer: MonadCancelThrow, P, S](
5556
client: FetchClientWithPars[F, P, S]
5657
): FetchClientWithPars[F, P, S] =
5758
apply(client, identityMod[F], emptyAttrs[F])
5859

59-
def withAttributes[F[_]: Tracer: MonadCancelThrow, P, S](
60-
client: FetchClientWithPars[F, P, S],
61-
spanBuilderMod: SpanBuilderMod[F]
62-
)(additionalAttributes: Attribute[?]*): FetchClientWithPars[F, P, S] =
63-
apply(client, spanBuilderMod, (_, _) => additionalAttributes.toList.pure[F])
64-
65-
def withAttributes[F[_]: Tracer: MonadCancelThrow, P, S](
66-
client: FetchClientWithPars[F, P, S]
67-
)(additionalAttributes: Attribute[?]*): FetchClientWithPars[F, P, S] =
68-
apply(client, identityMod[F], (_, _) => additionalAttributes.toList.pure[F])
69-
70-
// Streaming client overloads
71-
60+
// Streaming client
7261
def apply[F[_]: Tracer: MonadCancelThrow, S](
7362
client: StreamingClient[F, S],
7463
spanBuilderMod: SpanBuilderMod[F],
7564
additionalAttributesF: (GraphQLQuery, Option[JsonObject]) => F[List[Attribute[?]]]
7665
): StreamingClient[F, S] =
7766
Otel4sStreamingClient(client, spanBuilderMod, additionalAttributesF)
7867

79-
def apply[F[_]: Tracer: MonadCancelThrow, S](
80-
client: StreamingClient[F, S],
81-
additionalAttributesF: (GraphQLQuery, Option[JsonObject]) => F[List[Attribute[?]]]
82-
): StreamingClient[F, S] =
83-
apply(client, identityMod[F], additionalAttributesF)
84-
8568
def apply[F[_]: Tracer: MonadCancelThrow, S](
8669
client: StreamingClient[F, S]
8770
): StreamingClient[F, S] =
8871
apply(client, identityMod[F], emptyAttrs[F])
8972

90-
def withAttributes[F[_]: Tracer: MonadCancelThrow, S](
91-
client: StreamingClient[F, S],
92-
spanBuilderMod: SpanBuilderMod[F]
93-
)(additionalAttributes: Attribute[?]*): StreamingClient[F, S] =
94-
apply(client, spanBuilderMod, (_, _) => additionalAttributes.toList.pure[F])
73+
// Persistent streaming client
74+
def apply[F[_]: Tracer: MonadCancelThrow, S, CP, CE](
75+
client: PersistentStreamingClient[F, S, CP, CE],
76+
spanBuilderMod: SpanBuilderMod[F],
77+
additionalAttributesF: (GraphQLQuery, Option[JsonObject]) => F[List[Attribute[?]]]
78+
): PersistentStreamingClient[F, S, CP, CE] =
79+
Otel4sPersistentStreamingClient(client, spanBuilderMod, additionalAttributesF)
9580

96-
def withAttributes[F[_]: Tracer: MonadCancelThrow, S](
97-
client: StreamingClient[F, S]
98-
)(additionalAttributes: Attribute[?]*): StreamingClient[F, S] =
99-
apply(client, identityMod[F], (_, _) => additionalAttributes.toList.pure[F])
81+
def apply[F[_]: Tracer: MonadCancelThrow, S, CP, CE](
82+
client: PersistentStreamingClient[F, S, CP, CE]
83+
): PersistentStreamingClient[F, S, CP, CE] =
84+
apply(client, identityMod[F], emptyAttrs[F])
10085

10186
private[otel4s] def commonAttributes(
10287
document: GraphQLQuery,
10388
operationName: Option[String]
10489
): List[Attribute[?]] =
10590
val base = List[Attribute[?]](
10691
Attribute("clue.version", BuildInfo.version),
107-
Attribute("http.request.method", "POST"),
108-
Attribute("graphql.document", document.value)
92+
HttpAttributes.HttpRequestMethod("POST"),
93+
GraphqlExperimentalAttributes.GraphqlDocument(document.value)
10994
)
11095
val opType = extractOperationType(document.value)
111-
.map(t => Attribute("graphql.operation.type", t))
96+
.map(t => GraphqlExperimentalAttributes.GraphqlOperationType(t.value))
97+
.toList
98+
val opName = operationName
99+
.map(n => GraphqlExperimentalAttributes.GraphqlOperationName(n))
112100
.toList
113-
val opName = operationName.map(n => Attribute("graphql.operation.name", n)).toList
114101
base ++ opType ++ opName
115102

116103
private[otel4s] def recordResponseAttributes[F[_]: Applicative, D](
@@ -126,51 +113,6 @@ object Otel4sMiddleware:
126113
Attribute("clue.response.errors", errorsStr)
127114
) *> span.setStatus(StatusCode.Error, "GraphQL request returned errors")
128115

129-
object http4s:
130-
import Otel4sMiddleware.SpanBuilderMod
131-
132-
extension [F[_], P, S](client: F[FetchClientWithPars[F, P, S]]) {
133-
@scala.annotation.targetName("tracedFetchClient")
134-
def traced(using
135-
Tracer[F],
136-
MonadCancelThrow[F],
137-
cats.Functor[F]
138-
): F[FetchClientWithPars[F, P, S]] =
139-
client.map(Otel4sMiddleware(_))
140-
141-
@scala.annotation.targetName("tracedWithFetchClient")
142-
def tracedWith(
143-
spanBuilderMod: SpanBuilderMod[F],
144-
additionalAttributesF: (GraphQLQuery, Option[JsonObject]) => F[List[Attribute[?]]]
145-
)(using
146-
Tracer[F],
147-
MonadCancelThrow[F],
148-
cats.Functor[F]
149-
): F[FetchClientWithPars[F, P, S]] =
150-
client.map(Otel4sMiddleware(_, spanBuilderMod, additionalAttributesF))
151-
}
152-
153-
extension [F[_], S](client: F[StreamingClient[F, S]]) {
154-
@scala.annotation.targetName("tracedStreamingClient")
155-
def traced(using
156-
Tracer[F],
157-
MonadCancelThrow[F],
158-
cats.Functor[F]
159-
): F[StreamingClient[F, S]] =
160-
client.map(Otel4sMiddleware(_))
161-
162-
@scala.annotation.targetName("tracedWithStreamingClient")
163-
def tracedWith(
164-
spanBuilderMod: SpanBuilderMod[F],
165-
additionalAttributesF: (GraphQLQuery, Option[JsonObject]) => F[List[Attribute[?]]]
166-
)(using
167-
Tracer[F],
168-
MonadCancelThrow[F],
169-
cats.Functor[F]
170-
): F[StreamingClient[F, S]] =
171-
client.map(Otel4sMiddleware(_, spanBuilderMod, additionalAttributesF))
172-
}
173-
174116
class Otel4sFetchClient[F[_]: Tracer: MonadCancelThrow, P, S](
175117
wrapped: FetchClientWithPars[F, P, S],
176118
spanBuilderMod: Otel4sMiddleware.SpanBuilderMod[F],
@@ -224,4 +166,18 @@ class Otel4sStreamingClient[F[_]: Tracer: MonadCancelThrow, S](
224166
(exitCase match
225167
case Resource.ExitCase.Errored(e) =>
226168
span.setStatus(StatusCode.Error, Option(e.getMessage).getOrElse(e.toString))
227-
case _ => Applicative[F].unit)
169+
case _ => Applicative[F].unit
170+
)
171+
172+
class Otel4sPersistentStreamingClient[F[_]: Tracer: MonadCancelThrow, S, CP, CE](
173+
wrapped: PersistentStreamingClient[F, S, CP, CE],
174+
spanBuilderMod: Otel4sMiddleware.SpanBuilderMod[F],
175+
additionalAttributesF: (GraphQLQuery, Option[JsonObject]) => F[List[Attribute[?]]]
176+
) extends Otel4sStreamingClient[F, S](wrapped, spanBuilderMod, additionalAttributesF)
177+
with PersistentStreamingClient[F, S, CP, CE]:
178+
def status: F[PersistentClientStatus] = wrapped.status
179+
def statusStream: fs2.Stream[F, PersistentClientStatus] = wrapped.statusStream
180+
def connect[A: Encoder.AsObject](payload: F[A]): F[JsonObject] = wrapped.connect(payload)
181+
def connect(): F[JsonObject] = wrapped.connect()
182+
def disconnect(closeParameters: CP): F[Unit] = wrapped.disconnect(closeParameters)
183+
def disconnect(): F[Unit] = wrapped.disconnect()

project/Settings.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ object Settings {
158158

159159
val Otel4s = Def.setting(
160160
Seq(
161-
"org.typelevel" %%% "otel4s-core" % otel4s
161+
"org.typelevel" %%% "otel4s-core" % otel4s,
162+
"org.typelevel" %%% "otel4s-semconv" % otel4s,
163+
"org.typelevel" %%% "otel4s-semconv-experimental" % otel4s
162164
)
163165
)
164166

0 commit comments

Comments
 (0)