@@ -15,8 +15,11 @@ import clue.model.GraphQLResponse
1515import io .circe .Decoder
1616import io .circe .JsonObject
1717import org .typelevel .otel4s .Attribute
18- import org .typelevel .otel4s .trace .Tracer
18+ import org .typelevel .otel4s .trace .Span
19+ import org .typelevel .otel4s .trace .SpanBuilder
20+ import org .typelevel .otel4s .trace .SpanKind
1921import org .typelevel .otel4s .trace .StatusCode
22+ import org .typelevel .otel4s .trace .Tracer
2023
2124object Otel4sMiddleware :
2225 private [otel4s] def extractOperationType (query : String ): Option [String ] =
@@ -25,90 +28,152 @@ object Otel4sMiddleware:
2528 else if trimmed.startsWith(" mutation" ) then Some (" mutation" )
2629 else if trimmed.startsWith(" subscription" ) then Some (" subscription" )
2730 else None
31+
32+ type SpanBuilderMod [F [_]] = SpanBuilder [F ] => SpanBuilder [F ]
33+
34+ private def identityMod [F [_]]: SpanBuilderMod [F ] = identity
35+
36+ private def emptyAttrs [F [_]: Applicative ]
37+ : (GraphQLQuery , Option [JsonObject ]) => F [List [Attribute [? ]]] =
38+ (_, _) => List .empty[Attribute [? ]].pure[F ]
39+
40+ // Fetch client overloads
41+ def apply [F [_]: Tracer : MonadCancelThrow , P , S ](
42+ client : FetchClientWithPars [F , P , S ],
43+ spanBuilderMod : SpanBuilderMod [F ],
44+ additionalAttributesF : (GraphQLQuery , Option [JsonObject ]) => F [List [Attribute [? ]]]
45+ ): FetchClientWithPars [F , P , S ] =
46+ Otel4sFetchClient [F , P , S ](client, spanBuilderMod, additionalAttributesF)
47+
2848 def apply [F [_]: Tracer : MonadCancelThrow , P , S ](
2949 client : FetchClientWithPars [F , P , S ],
3050 additionalAttributesF : (GraphQLQuery , Option [JsonObject ]) => F [List [Attribute [? ]]]
3151 ): FetchClientWithPars [F , P , S ] =
32- Otel4sFetchClient [ F , P , S ](client , additionalAttributesF)
52+ apply(client, identityMod[ F ] , additionalAttributesF)
3353
3454 def apply [F [_]: Tracer : MonadCancelThrow , P , S ](
3555 client : FetchClientWithPars [F , P , S ]
3656 ): FetchClientWithPars [F , P , S ] =
37- apply(client, (_ : GraphQLQuery , _ : Option [JsonObject ]) => List .empty[Attribute [? ]].pure[F ])
57+ apply(client, identityMod[F ], emptyAttrs[F ])
58+
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 ])
3864
3965 def withAttributes [F [_]: Tracer : MonadCancelThrow , P , S ](
4066 client : FetchClientWithPars [F , P , S ]
41- )(
42- additionalAttributes : Attribute [? ]*
43- ): FetchClientWithPars [F , P , S ] =
44- apply(
45- client,
46- (_ : GraphQLQuery , _ : Option [JsonObject ]) => additionalAttributes.toList.pure[F ]
47- )
67+ )(additionalAttributes : Attribute [? ]* ): FetchClientWithPars [F , P , S ] =
68+ apply(client, identityMod[F ], (_, _) => additionalAttributes.toList.pure[F ])
69+
70+ // Streaming client overloads
71+
72+ def apply [F [_]: Tracer : MonadCancelThrow , S ](
73+ client : StreamingClient [F , S ],
74+ spanBuilderMod : SpanBuilderMod [F ],
75+ additionalAttributesF : (GraphQLQuery , Option [JsonObject ]) => F [List [Attribute [? ]]]
76+ ): StreamingClient [F , S ] =
77+ Otel4sStreamingClient (client, spanBuilderMod, additionalAttributesF)
4878
4979 def apply [F [_]: Tracer : MonadCancelThrow , S ](
5080 client : StreamingClient [F , S ],
5181 additionalAttributesF : (GraphQLQuery , Option [JsonObject ]) => F [List [Attribute [? ]]]
5282 ): StreamingClient [F , S ] =
53- Otel4sStreamingClient (client, additionalAttributesF)
83+ apply (client, identityMod[ F ] , additionalAttributesF)
5484
5585 def apply [F [_]: Tracer : MonadCancelThrow , S ](
5686 client : StreamingClient [F , S ]
5787 ): StreamingClient [F , S ] =
58- apply(client, ( _ : GraphQLQuery , _ : Option [ JsonObject ]) => List .empty[ Attribute [ ? ]].pure [F ])
88+ apply(client, identityMod[ F ], emptyAttrs [F ])
5989
60- def withAttributes [F [_]: Tracer : MonadCancelThrow , P , S ](
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 ])
95+
96+ def withAttributes [F [_]: Tracer : MonadCancelThrow , S ](
6197 client : StreamingClient [F , S ]
6298 )(additionalAttributes : Attribute [? ]* ): StreamingClient [F , S ] =
63- apply(
64- client,
65- (_ : GraphQLQuery , _ : Option [JsonObject ]) => additionalAttributes.toList.pure[F ]
99+ apply(client, identityMod[F ], (_, _) => additionalAttributes.toList.pure[F ])
100+
101+ private [otel4s] def commonAttributes (
102+ document : GraphQLQuery ,
103+ operationName : Option [String ]
104+ ): List [Attribute [? ]] =
105+ val base = List [Attribute [? ]](
106+ Attribute (" clue.version" , BuildInfo .version),
107+ Attribute (" http.request.method" , " POST" ),
108+ Attribute (" graphql.document" , document.value)
66109 )
110+ val opType = extractOperationType(document.value)
111+ .map(t => Attribute (" graphql.operation.type" , t))
112+ .toList
113+ val opName = operationName.map(n => Attribute (" graphql.operation.name" , n)).toList
114+ base ++ opType ++ opName
115+
116+ private [otel4s] def recordResponseAttributes [F [_]: Applicative , D ](
117+ span : Span [F ],
118+ result : GraphQLResponse [D ]
119+ ): F [Unit ] =
120+ span.addAttribute(Attribute (" clue.response.hasData" , result.data.isDefined)) *>
121+ result.errors.fold(Applicative [F ].unit): errs =>
122+ val errorsStr = errs.toList.mkString(" [" , " , " , " ]" )
123+ span.addAttributes(
124+ Attribute (" clue.response.hasErrors" , true ),
125+ Attribute (" clue.response.errorCount" , errs.length.toLong),
126+ Attribute (" clue.response.errors" , errorsStr)
127+ ) *> span.setStatus(StatusCode .Error , " GraphQL request returned errors" )
67128
68- // Extension methods for convenient tracing
69129object http4s :
130+ import Otel4sMiddleware .SpanBuilderMod
131+
70132 extension [F [_], P , S ](client : F [FetchClientWithPars [F , P , S ]]) {
71133 @ scala.annotation.targetName(" tracedFetchClient" )
72134 def traced (using
73- org.typelevel.otel4s.trace. Tracer [F ],
74- cats.effect. MonadCancelThrow [F ],
135+ Tracer [F ],
136+ MonadCancelThrow [F ],
75137 cats.Functor [F ]
76138 ): F [FetchClientWithPars [F , P , S ]] =
77139 client.map(Otel4sMiddleware (_))
78140
79141 @ scala.annotation.targetName(" tracedWithFetchClient" )
80142 def tracedWith (
81- additionalAttributesF : (clue.model.GraphQLQuery , Option [io.circe.JsonObject ]) => F [List [org.typelevel.otel4s.Attribute [? ]]]
143+ spanBuilderMod : SpanBuilderMod [F ],
144+ additionalAttributesF : (GraphQLQuery , Option [JsonObject ]) => F [List [Attribute [? ]]]
82145 )(using
83- org.typelevel.otel4s.trace. Tracer [F ],
84- cats.effect. MonadCancelThrow [F ],
146+ Tracer [F ],
147+ MonadCancelThrow [F ],
85148 cats.Functor [F ]
86149 ): F [FetchClientWithPars [F , P , S ]] =
87- client.map(Otel4sMiddleware (_, additionalAttributesF))
150+ client.map(Otel4sMiddleware (_, spanBuilderMod, additionalAttributesF))
88151 }
89152
90153 extension [F [_], S ](client : F [StreamingClient [F , S ]]) {
91154 @ scala.annotation.targetName(" tracedStreamingClient" )
92155 def traced (using
93- org.typelevel.otel4s.trace. Tracer [F ],
94- cats.effect. MonadCancelThrow [F ],
156+ Tracer [F ],
157+ MonadCancelThrow [F ],
95158 cats.Functor [F ]
96159 ): F [StreamingClient [F , S ]] =
97160 client.map(Otel4sMiddleware (_))
98161
99162 @ scala.annotation.targetName(" tracedWithStreamingClient" )
100163 def tracedWith (
101- additionalAttributesF : (clue.model.GraphQLQuery , Option [io.circe.JsonObject ]) => F [List [org.typelevel.otel4s.Attribute [? ]]]
164+ spanBuilderMod : SpanBuilderMod [F ],
165+ additionalAttributesF : (GraphQLQuery , Option [JsonObject ]) => F [List [Attribute [? ]]]
102166 )(using
103- org.typelevel.otel4s.trace. Tracer [F ],
104- cats.effect. MonadCancelThrow [F ],
167+ Tracer [F ],
168+ MonadCancelThrow [F ],
105169 cats.Functor [F ]
106170 ): F [StreamingClient [F , S ]] =
107- client.map(Otel4sMiddleware (_, additionalAttributesF))
171+ client.map(Otel4sMiddleware (_, spanBuilderMod, additionalAttributesF))
108172 }
109173
110174class Otel4sFetchClient [F [_]: Tracer : MonadCancelThrow , P , S ](
111175 wrapped : FetchClientWithPars [F , P , S ],
176+ spanBuilderMod : Otel4sMiddleware .SpanBuilderMod [F ],
112177 additionalAttributesF : (GraphQLQuery , Option [JsonObject ]) => F [List [Attribute [? ]]]
113178) extends FetchClientWithPars [F , P , S ]:
114179 override protected [clue] def requestInternal [D : Decoder ](
@@ -118,63 +183,46 @@ class Otel4sFetchClient[F[_]: Tracer: MonadCancelThrow, P, S](
118183 modParams : P => P
119184 ): F [GraphQLResponse [D ]] =
120185 MonadCancelThrow [F ].uncancelable: poll =>
121- Tracer [F ].span(s " clue-client-request- ${document.querySummary}" ).use: span =>
186+ spanBuilderMod(
187+ Tracer [F ]
188+ .spanBuilder(s " clue-client-request- ${document.querySummary}" )
189+ .withSpanKind(SpanKind .Client )
190+ .addAttributes(Otel4sMiddleware .commonAttributes(document, operationName)* )
191+ ).build.use: span =>
122192 for
123- // Add clue library version
124- _ <- span.addAttribute(Attribute (" clue.version" , BuildInfo .version))
125- // Add standard OpenTelemetry semantic convention attributes
126- _ <- span.addAttribute(Attribute (" http.request.method" , " POST" ))
127- _ <- Otel4sMiddleware .extractOperationType(document.value).traverse(opType =>
128- span.addAttribute(Attribute (" graphql.operation.type" , opType)))
129- _ <- operationName.traverse(name => span.addAttribute(Attribute (" graphql.operation.name" , name)))
130- _ <- span.addAttribute(Attribute (" graphql.document" , document.value))
131- // Add user-provided additional attributes
132- additionalAttributes <- additionalAttributesF(document, variables)
133- _ <- additionalAttributes.traverse(span.addAttribute)
134- result <- poll :
135- wrapped.requestInternal[D ](document, operationName, variables, modParams)
136- // Add response attributes and handle errors
137- _ <- span.addAttribute:
138- Attribute (" clue.response.hasData" , result.data.isDefined)
139- _ <- result.errors.fold(Applicative [F ].unit): errs =>
140- for
141- _ <- span.addAttribute(Attribute (" clue.response.hasErrors" , true ))
142- _ <- span.addAttribute(Attribute (" clue.response.errorCount" , errs.length.toLong))
143- _ <- span.addAttribute(Attribute (" clue.response.errors" , errs.toList.mkString(" [" , " , " , " ]" )))
144- _ <- span.setStatus(StatusCode .Error , " GraphQL request returned errors" )
145- yield ()
193+ additional <- additionalAttributesF(document, variables)
194+ _ <- span.addAttributes(additional* )
195+ result <- poll(wrapped.requestInternal[D ](document, operationName, variables, modParams))
196+ _ <- Otel4sMiddleware .recordResponseAttributes(span, result)
146197 yield result
147198
148199class Otel4sStreamingClient [F [_]: Tracer : MonadCancelThrow , S ](
149200 wrapped : StreamingClient [F , S ],
201+ spanBuilderMod : Otel4sMiddleware .SpanBuilderMod [F ],
150202 additionalAttributesF : (GraphQLQuery , Option [JsonObject ]) => F [List [Attribute [? ]]]
151- ) extends Otel4sFetchClient [F , Unit , S ](wrapped, additionalAttributesF)
203+ ) extends Otel4sFetchClient [F , Unit , S ](wrapped, spanBuilderMod, additionalAttributesF)
152204 with StreamingClient [F , S ]:
153205 protected [clue] def subscribeInternal [D : Decoder ](
154206 document : GraphQLQuery ,
155207 operationName : Option [String ] = none,
156208 variables : Option [JsonObject ] = none
157209 ): Resource [F , fs2.Stream [F , GraphQLResponse [D ]]] =
158- val resource : Resource [F , fs2.Stream [F , GraphQLResponse [D ]]] =
159- Resource .applyFull: poll =>
160- Tracer [F ].span(s " clue-client-start- ${document.querySummary}" ).use: span =>
161- for
162- // Add clue library version
163- _ <- span.addAttribute(Attribute (" clue.version" , BuildInfo .version))
164- // Add standard OpenTelemetry semantic convention attributes
165- _ <- span.addAttribute(Attribute (" http.request.method" , " POST" ))
166- _ <- Otel4sMiddleware .extractOperationType(document.value).traverse(opType =>
167- span.addAttribute(Attribute (" graphql.operation.type" , opType)))
168- _ <- operationName.traverse(name => span.addAttribute(Attribute (" graphql.operation.name" , name)))
169- _ <- span.addAttribute(Attribute (" graphql.document" , document.value))
170- // Add user-provided additional attributes
171- additionalAttributes <- additionalAttributesF(document, variables)
172- _ <- additionalAttributes.traverse(span.addAttribute)
173- result <- poll :
174- wrapped
175- .subscribeInternal[D ](document, operationName, variables)
176- .allocatedCase
177- yield result
178- resource.onFinalizeCase: exitCase =>
179- Tracer [F ].span(s " clue-client-end- ${document.querySummary}" ).use: span =>
180- span.addAttribute(Attribute (" clue.exitCase" , exitCase.toOutcome.toString))
210+ for
211+ res <- spanBuilderMod(
212+ Tracer [F ]
213+ .spanBuilder(s " clue-client-subscribe- ${document.querySummary}" )
214+ .withSpanKind(SpanKind .Client )
215+ .addAttributes(Otel4sMiddleware .commonAttributes(document, operationName)* )
216+ ).build.resource
217+ span = res.span
218+ _ <- Resource .eval:
219+ additionalAttributesF(document, variables).flatMap: attrs =>
220+ span.addAttributes(attrs* )
221+ stream <- wrapped.subscribeInternal[D ](document, operationName, variables)
222+ yield stream.onFinalizeCase: exitCase =>
223+ span.addAttribute(Attribute (" clue.exitCase" , exitCase.toOutcome.toString)) *>
224+ (exitCase match
225+ case Resource .ExitCase .Errored (e) =>
226+ span.setStatus(StatusCode .Error , Option (e.getMessage).getOrElse(e.toString))
227+ case _ => Applicative [F ].unit
228+ )
0 commit comments