Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 8 additions & 37 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,9 @@ jobs:
strategy:
matrix:
os: [ubuntu-22.04]
scala: [3]
java: [temurin@11, temurin@17]
scala: [3.8.3]
java: [temurin@17]
project: [rootJS, rootJVM]
exclude:
- project: rootJS
java: temurin@17
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
Expand All @@ -45,19 +42,6 @@ jobs:
- name: Setup sbt
uses: sbt/setup-sbt@v1

- name: Setup Java (temurin@11)
id: setup-java-temurin-11
if: matrix.java == 'temurin@11'
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 11
cache: sbt

- name: sbt update
if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false'
run: sbt +update

- name: Setup Java (temurin@17)
id: setup-java-temurin-17
if: matrix.java == 'temurin@17'
Expand All @@ -75,11 +59,11 @@ jobs:
run: sbt githubWorkflowCheck

- name: Check headers and formatting
if: matrix.java == 'temurin@11' && matrix.os == 'ubuntu-22.04'
if: matrix.java == 'temurin@17' && matrix.os == 'ubuntu-22.04'
run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck lucumaScalafmtCheck lucumaScalafixCheck

- name: Check scalafix lints
if: matrix.java == 'temurin@11' && matrix.os == 'ubuntu-22.04'
if: matrix.java == 'temurin@17' && matrix.os == 'ubuntu-22.04'
run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' 'scalafixAll --check'

- name: scalaJSLink
Expand All @@ -90,11 +74,11 @@ jobs:
run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' test

- name: Check binary compatibility
if: matrix.java == 'temurin@11' && matrix.os == 'ubuntu-22.04'
if: matrix.java == 'temurin@17' && matrix.os == 'ubuntu-22.04'
run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' mimaReportBinaryIssues

- name: Generate API documentation
if: matrix.java == 'temurin@11' && matrix.os == 'ubuntu-22.04'
if: matrix.java == 'temurin@17' && matrix.os == 'ubuntu-22.04'
run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' doc

- name: Aggregate coverage reports
Expand All @@ -106,11 +90,11 @@ jobs:
publish:
name: Publish Artifacts
needs: [build]
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master')
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
strategy:
matrix:
os: [ubuntu-22.04]
java: [temurin@11]
java: [temurin@17]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout current branch (full)
Expand All @@ -121,19 +105,6 @@ jobs:
- name: Setup sbt
uses: sbt/setup-sbt@v1

- name: Setup Java (temurin@11)
id: setup-java-temurin-11
if: matrix.java == 'temurin@11'
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 11
cache: sbt

- name: sbt update
if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false'
run: sbt +update

- name: Setup Java (temurin@17)
id: setup-java-temurin-17
if: matrix.java == 'temurin@17'
Expand Down
11 changes: 5 additions & 6 deletions .mergify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ pull_request_rules:
- body~=labels:.*early-semver-patch
- body~=labels:.*early-semver-minor
- 'title=flake.lock: Update'
- status-success=Test (ubuntu-22.04, 3, temurin@11, rootJS)
- status-success=Test (ubuntu-22.04, 3, temurin@11, rootJVM)
- status-success=Test (ubuntu-22.04, 3, temurin@17, rootJVM)
- status-success=Test (ubuntu-22.04, 3.8.3, temurin@17, rootJS)
- status-success=Test (ubuntu-22.04, 3.8.3, temurin@17, rootJVM)
actions:
merge: {}
- name: Label core PRs
Expand Down Expand Up @@ -58,13 +57,13 @@ pull_request_rules:
add:
- model
remove: []
- name: Label natchez PRs
- name: Label otel4s PRs
conditions:
- files~=^natchez/
- files~=^otel4s/
actions:
label:
add:
- natchez
- otel4s
remove: []
- name: Label output PRs
conditions:
Expand Down
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,32 @@ Either:
EmberClientBuilder
.default[IO]
.build
.use: client =>
.use: client =>
given Backend[IO] = Http4sHttpBackend[IO](client)
val fetchClient: FetchClient[IO, StarWars] =
val fetchClient: FetchClient[IO, StarWars] =
Http4sHttpClient.of[IO, StarWars]("https://starwars.com/graphql")

// Scala JVM with JDK WS client behind http4s
import import org.http4s.jdkhttpclient.JdkWSClient

JdkWSClient
.simple[IO]
.use: client =>
given StreamingBackend[IO] = Http4sWebSocketBackend[IO](client)
val streamingClient: StreamingClient[IO, StarWars] =
val streamingClient: StreamingClient[IO, StarWars] =
Http4sWebSocketClient.of[IO, StarWars]("wss://starwars.com/graphql")


// Scala.js with default fetch/WS client
import clue.js.*

given Backend[IO] = AjaxJSBackend[IO]
val fetchClient: FetchClient[IO, StarWars] =
val fetchClient: FetchClient[IO, StarWars] =
FetchJsClient.of[IO, StarWars]("https://starwars.com/graphql")

// Streaming doesn't require Apollo, it just follows the Apollo protocol for GraphQL over WS
given StreamingBackend[IO] = WebSocketJsBackend[IO]
val streamingClient: StreamingClient[IO, StarWars] =
val streamingClient: StreamingClient[IO, StarWars] =
ApolloStreamingClient.of[IO, StarWars]("wss://starwars.com/graphql")
```

Expand Down Expand Up @@ -108,4 +108,29 @@ fetchClient.request(CharacterQuery)(CharacterQuery.Variables("0001"))
.forEach(println).unsafeRunSync()

# Data(Some(Character("0001", Some("Luke"))))
```
```

### Tracing with otel4s

The `clue-otel4s` module wraps any clue with OpenTelemetry tracing via [otel4s](https://typelevel.org/otel4s/).

A `SpanKind.Client` span is emitted per HTTP request or subscription.
W3C trace context is automatically propagated to the server:

- **HTTP requests**: the current span's `traceparent` (and `tracestate`) is injected into outgoing
request headers, thus client spans can propagate to the server
- **WebSocket requests**: `traceparent` is included in the `extensions` field, so the server can
create child reading `extensions.traceparent`.

Some span attributes recorded automatically:
* `clue.version`
* `http.request.method`
* `graphql.operation.type`
* `graphql.operation.name`
* `graphql.document`
* `clue.response.hasData`

And on errors `clue.response.hasErrors`, `clue.response.errorCount`, `clue.response.errors`.

W3C propagation requires the SDK to be configured with `W3CTraceContextPropagator`.
See https://ochenashko.com/practical-observability-distributed-tracing/#6-cross-service-propagation
69 changes: 32 additions & 37 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
lazy val V = _root_.scalafix.sbt.BuildInfo

ThisBuild / tlBaseVersion := "0.51"
ThisBuild / tlCiReleaseBranches := Seq("master")
ThisBuild / tlJdkRelease := Some(8)
ThisBuild / githubWorkflowJavaVersions := Seq("11", "17").map(JavaSpec.temurin(_))
ThisBuild / scalaVersion := "3.7.4"
Global / onChangedBuildSource := ReloadOnSourceChanges
ThisBuild / tlBaseVersion := "0.52"
ThisBuild / tlJdkRelease := Some(17)
ThisBuild / githubWorkflowJavaVersions := Seq("17").map(JavaSpec.temurin(_))
ThisBuild / scalaVersion := "3.8.3"
ThisBuild / crossScalaVersions := Seq("3.8.3")
ThisBuild / githubWorkflowScalaVersions := Seq("3.8.3")
Global / onChangedBuildSource := ReloadOnSourceChanges

lazy val root = tlCrossRootProject
.aggregate(
Expand All @@ -14,7 +15,7 @@ lazy val root = tlCrossRootProject
scalaJS,
http4s,
http4sJDKDemo,
natchez,
otel4s,
genRules,
genInput,
genOutput,
Expand All @@ -30,9 +31,7 @@ lazy val model =
.crossType(CrossType.Pure)
.in(file("model"))
.settings(
moduleName := "clue-model",
// temporary? fix for upgrading to Scala 3.7: https://github.com/scala/scala3/issues/22890
dependencyOverrides += "org.scala-lang" %% "scala3-library" % scalaVersion.value,
moduleName := "clue-model",
libraryDependencies ++=
Settings.Libraries.Cats.value ++
Settings.Libraries.CatsTestkit.value ++
Expand All @@ -44,17 +43,18 @@ lazy val model =
Settings.Libraries.Monocle.value ++
Settings.Libraries.MonocleLaw.value ++
Settings.Libraries.MUnit.value,
Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat // Needed for circe's codec tests
Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat // Needed for circe's codec tests
)

lazy val core =
crossProject(JVMPlatform, JSPlatform)
.crossType(CrossType.Pure)
.in(file("core"))
.enablePlugins(BuildInfoPlugin)
.settings(
moduleName := "clue-core",
// temporary? fix for upgrading to Scala 3.7: https://github.com/scala/scala3/issues/22890
dependencyOverrides += "org.scala-lang" %% "scala3-library" % scalaVersion.value,
moduleName := "clue-core",
buildInfoPackage := "clue",
buildInfoKeys := Seq[BuildInfoKey](name, version),
libraryDependencies ++=
Settings.Libraries.Cats.value ++
Settings.Libraries.CatsEffect.value ++
Expand All @@ -71,10 +71,8 @@ lazy val scalaJS =
.in(file("scalajs"))
.enablePlugins(ScalaJSPlugin)
.settings(
moduleName := "clue-scalajs",
coverageEnabled := false,
// temporary? fix for upgrading to Scala 3.7: https://github.com/scala/scala3/issues/22890
dependencyOverrides += "org.scala-lang" %% "scala3-library" % scalaVersion.value,
moduleName := "clue-scalajs",
coverageEnabled := false,
libraryDependencies ++=
Settings.Libraries.ScalaJsDom.value ++
Settings.Libraries.ScalaJsMacrotaskExecutor.value
Expand All @@ -86,12 +84,11 @@ lazy val http4s =
.crossType(CrossType.Pure)
.in(file("http4s"))
.settings(
moduleName := "clue-http4s",
// temporary? fix for upgrading to Scala 3.7: https://github.com/scala/scala3/issues/22890
dependencyOverrides += "org.scala-lang" %% "scala3-library" % scalaVersion.value,
moduleName := "clue-http4s",
libraryDependencies ++=
Settings.Libraries.Http4sCirce.value ++
Settings.Libraries.Http4sClient.value
Settings.Libraries.Http4sClient.value ++
Settings.Libraries.Http4sOtel4sMiddleware.value
)
.dependsOn(core)

Expand All @@ -101,7 +98,7 @@ lazy val http4sJDKDemo =
.enablePlugins(NoPublishPlugin)
.settings(
moduleName := "clue-http4s-jdk-client-demo",
tlJdkRelease := Some(11),
tlJdkRelease := Some(17),
Compile / run / fork := true,
libraryDependencies ++= Seq(
"org.typelevel" %% "log4cats-slf4j" % Settings.LibraryVersions.log4Cats,
Expand All @@ -111,13 +108,13 @@ lazy val http4sJDKDemo =
)
.dependsOn(http4s.jvm)

lazy val natchez =
lazy val otel4s =
crossProject(JVMPlatform, JSPlatform)
.crossType(CrossType.Pure)
.in(file("natchez"))
.in(file("otel4s"))
.settings(
moduleName := "clue-natchez",
libraryDependencies ++= Settings.Libraries.Natchez.value
moduleName := "clue-otel4s",
libraryDependencies ++= Settings.Libraries.Otel4s.value
)
.dependsOn(core)

Expand Down Expand Up @@ -186,23 +183,23 @@ lazy val sbtPlugin =
.in(file("sbt-plugin"))
.enablePlugins(SbtPlugin, BuildInfoPlugin)
.settings(
moduleName := "sbt-clue",
crossScalaVersions := List("2.12.20"),
scalacOptions := Nil,
moduleName := "sbt-clue",
crossScalaVersions := List("2.12.20"),
scalacOptions := Nil,
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % V.scalafixVersion),
addSbtPlugin("org.portable-scala" % "sbt-platform-deps" % "1.0.2"),
addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "1.3.2"),
buildInfoPackage := "clue.sbt",
buildInfoKeys := Seq[BuildInfoKey](
buildInfoPackage := "clue.sbt",
buildInfoKeys := Seq[BuildInfoKey](
version,
organization,
"rulesModule" -> (genRules / moduleName).value,
"coreModule" -> (core.jvm / moduleName).value
),
buildInfoOptions += BuildInfoOption.PackagePrivate,
Test / test :=
Test / test :=
scripted.toTask("").value,
scripted := scripted
scripted := scripted
.dependsOn(
genRules / publishLocal,
model.jvm / publishLocal,
Expand All @@ -214,7 +211,5 @@ lazy val sbtPlugin =
"-Dplugin.version=" + version.value,
"-Dscala.version=" + (core.jvm / scalaVersion).value
),
scriptedBufferLog := false,
// temporary? fix for upgrading to Scala 3.7: https://github.com/scala/scala3/issues/22890
dependencyOverrides += "org.scala-lang" %% "scala3-library" % scalaVersion.value
scriptedBufferLog := false
)
6 changes: 5 additions & 1 deletion core/src/main/scala/clue/FetchClientImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ class FetchClientImpl[F[_]: MonadThrow: Logger, P, S](requestParams: P)(using
document: GraphQLQuery,
operationName: Option[String],
variables: Option[JsonObject],
extensions: Option[JsonObject],
modParams: P => P = identity
): F[GraphQLResponse[D]] =
backend
.request(GraphQLRequest(document, operationName, variables), modParams(requestParams))
.request(
GraphQLRequest(document, operationName, variables, extensions),
modParams(requestParams)
)
.map(decode[GraphQLResponse[D]])
.rethrow
.onError(_.warnF("Error executing query:"))
Expand Down
14 changes: 14 additions & 0 deletions core/src/main/scala/clue/TraceHeaderInjector.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) 2016-2025 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package clue

// Typeclass to abstract over injecting header for different transports, http, ws ...
trait TraceHeaderInjector[A]:
def addHeaders(params: A, headers: Map[String, String]): A

object TraceHeaderInjector:
def apply[A](using ev: TraceHeaderInjector[A]): TraceHeaderInjector[A] = ev

given TraceHeaderInjector[Unit] with
def addHeaders(params: Unit, headers: Map[String, String]): Unit = ()
Loading
Loading