dean151/swift-server-gcp
A lightweight Swift toolkit for running services on Google Cloud Platform.
What's in the box
| Module | Purpose | | --- | --- | | GcpJsonLogHandler | A swift-log LogHandler that emits one JSON object per line, shaped to Cloud Logging's structured-logging contract. Severity, source location, and trace correlation fields are mapped automatically. | | TraceContext | W3C Trace Context (traceparent / tracestate) + Google's legacy X-Cloud-Trace-Context. Parses inbound request headers, builds outbound forwarding headers, and bridges between the two formats. |
The two modules are designed to interoperate but don't depend on each other — you can adopt one without the other.
Requirements
- Swift 6.0 or later (the package opts in to Swift 6 language mode)
- macOS 13+ or Linux
- Dependencies:
swift-log≥ 1.6,
swift-http-types ≥ 1.3
Installation
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/Dean151/swift-server-gcp.git", from: "0.1.0"),
],
targets: [
.target(
name: "MyServer",
dependencies: [
.product(name: "GoogleCloudPlatform", package: "swift-server-gcp"),
]
),
]Usage
Structured logging for Cloud Logging
Bootstrap swift-log once at startup. When running on GCP (Cloud Run, GKE, Compute Engine, App Engine flex), Cloud Logging picks up JSON written to stdout and parses the special fields.
import Logging
import GoogleCloudPlatform
LoggingSystem.bootstrap { label in
GcpJsonLogHandler(
label: label,
googleCloudProject: ProcessInfo.processInfo.environment["GOOGLE_CLOUD_PROJECT"]
)
}
let logger = Logger(label: "my-service")
logger.info("server started", metadata: ["port": "8080"])Each log line looks like:
{
"severity": "INFO",
"message": "server started",
"time": "2026-05-25T10:40:00.123Z",
"logger": "my-service",
"logging.googleapis.com/sourceLocation": {
"file": "main.swift", "line": "12", "function": "run()"
},
"port": "8080"
}Severity mapping
| swift-log level | Cloud Logging severity | | --- | --- | | .trace, .debug | DEBUG | | .info | INFO | | .notice | NOTICE | | .warning | WARNING | | .error | ERROR | | .critical | CRITICAL |
Linking logs to traces
If your logger metadata contains the standard trace keys (trace.id, trace.span_id, trace.sampled), the handler rewrites them to the Cloud-Logging-recognised fields so the entry deep-links to its Cloud Trace span:
logger.info("handling request", metadata: [
"trace.id": "0af7651916cd43dd8448eb211c80319c",
"trace.span_id": "b7ad6b7169203331",
"trace.sampled": "true",
])becomes
{
"logging.googleapis.com/trace": "projects/my-project/traces/0af7651916cd43dd8448eb211c80319c",
"logging.googleapis.com/spanId": "b7ad6b7169203331",
"logging.googleapis.com/trace_sampled": true,
...
}The key constants are published on TraceContext.MetadataKey so middleware and log handlers share a single source of truth.
Trace propagation
TraceContext reads either format from an incoming request's HTTPFields and produces a matched pair of headers to forward downstream.
import HTTPTypes
import GoogleCloudPlatform
func handle(headers: HTTPFields) {
guard let trace = TraceContext.extract(from: headers) else {
// No usable trace header — fall back to your own request id.
return
}
// Attach to logger metadata for the duration of the request.
var logger = Logger(label: "handler")
logger[metadataKey: TraceContext.MetadataKey.traceID] = "\(trace.traceID)"
if let span = trace.spanID {
logger[metadataKey: TraceContext.MetadataKey.spanID] = "\(span)"
}
if let sampled = trace.sampled {
logger[metadataKey: TraceContext.MetadataKey.sampled] = "\(sampled)"
}
// When you call another service, forward the trace.
let outbound = trace.forwardingHeaders
// outbound contains both `traceparent`/`tracestate` and `X-Cloud-Trace-Context`.
}What it handles for you:
- Format precedence —
traceparentwins overX-Cloud-Trace-Contextwhen
both are present.
- Validation — all-zero trace/span ids are rejected per spec; malformed
headers fall through to the next format or to nil.
- Span-id normalisation — W3C uses 16-hex, X-Cloud uses decimal uint64.
forwardingHeaders converts between them so a context received in one format propagates cleanly in both.
tracestatepass-through — verbatim, but only emitted alongside a
valid traceparent (W3C forbids it standalone).
- Conservative sampling — unknown sampling decisions become
00in
W3C output; the ;o= segment is simply omitted from the X-Cloud header.
Design notes
- No GCP SDK
- No SwiftNIO dependency
- Strict concurrency
- Decoupled trace ↔ log handover
Contributions
Issues and PRs welcome.
License
MIT © Thomas Durand.
Package Metadata
Repository: dean151/swift-server-gcp
Default branch: main
README: README.md