Contents

dean151/swift-server-gcp-hummingbird

Glue between swift-server-gcp

Requirements

Installation

dependencies: [
    .package(
        url: "https://github.com/Dean151/swift-server-gcp-hummingbird.git",
        from: "0.1.0"
    ),
],
targets: [
    .executableTarget(
        name: "MyServer",
        dependencies: [
            .product(name: "Hummingbird", package: "hummingbird"),
            .product(
                name: "GoogleCloudPlatformHummingbird",
                package: "swift-server-gcp-hummingbird"
            ),
            // GcpJsonLogHandler lives in the upstream package; pull it in
            // alongside if you want trace-correlated structured logs.
            .product(name: "GoogleCloudPlatform", package: "swift-server-gcp"),
        ]
    ),
]

Wiring it up

Two things have to line up for trace correlation to work end-to-end:

  1. The log handler has to be the GCP JSON handler — it's the piece that

rewrites the generic trace.* metadata keys into the logging.googleapis.com/{trace,spanId,trace_sampled} fields Cloud Logging promotes.

  1. TraceContextMiddleware has to run before anything that emits a

log line for the request (most importantly LogRequestsMiddleware), so the trace metadata is already on the logger by the time those entries are written.

import Foundation
import GoogleCloudPlatform
import GoogleCloudPlatformHummingbird
import Hummingbird
import Logging

// 1. Bootstrap swift-log with the GCP handler. Do this once, before any
//    logger is constructed — the upstream README has the details and the
//    full severity-mapping table.
LoggingSystem.bootstrap { label in
    GcpJsonLogHandler(
        label: label,
        googleCloudProject: ProcessInfo.processInfo.environment["GOOGLE_CLOUD_PROJECT"]
    )
}

// 2. Build the router. `TraceContextMiddleware` is registered first so
//    every later middleware (and every route handler) sees a logger that
//    already carries trace.id / trace.span_id / trace.sampled.
let router = Router()
router.add(middleware: TraceContextMiddleware())
router.add(middleware: LogRequestsMiddleware(.info))

router.get("/") { _, context -> String in
    // No manual metadata plumbing — context.logger already has the trace
    // ids if the incoming request carried them.
    context.logger.info("hello")
    return "ok"
}

let app = Application(
    router: router,
    configuration: .init(address: .hostname("0.0.0.0", port: 8080))
)
try await app.runService()

When a request arrives with either header set, every log line the request produces — the per-request access log from LogRequestsMiddleware, anything your handlers log, anything middlewares further down the chain log — comes out shaped like this:

{
  "severity": "INFO",
  "message": "Request",
  "time": "2026-05-25T10:40:00.123Z",
  "logger": "hb",
  "logging.googleapis.com/trace": "projects/my-project/traces/0af7651916cd43dd8448eb211c80319c",
  "logging.googleapis.com/spanId": "b7ad6b7169203331",
  "logging.googleapis.com/trace_sampled": true,
  "hb.request.id": "…",
  "hb.request.method": "GET",
  "hb.request.path": "/"
}

When neither trace header is present, the middleware is a no-op and Hummingbird's framework-assigned hb.request.id remains the only correlation key.

Notes

  • The middleware is generic over Context: RequestContext, so it works

with the default BasicRequestContext and any custom context type.

  • The metadata keys it writes (trace.id, trace.span_id,

trace.sampled) are deployment-agnostic — defined in TraceContext.MetadataKey upstream. You can pair the middleware with a non-GCP log handler and emit the generic keys as-is.

  • Propagating the trace to downstream HTTP calls is handled by

TraceContext.forwardingHeaders from the upstream package — extract a TraceContext from the inbound request in your handler and copy those headers onto your outbound request.

License

MIT — see LICENSE.

Package Metadata

Repository: dean151/swift-server-gcp-hummingbird

Default branch: main

README: README.md