Skip to content

Observability

Husnain Ali edited this page Sep 27, 2026 · 2 revisions

Observability

Covers environment configuration, logging, metrics, and tracing.

Environment configuration

let dev = NetworkEnvironment.development(
    baseURL: URL(string: "https://dev.api.example.com")!,
    headers: ["X-Env": "dev"]
)
let prod = NetworkEnvironment.production(
    baseURL: URL(string: "https://api.example.com")!
)

#if DEBUG
let environment = dev
#else
let environment = prod
#endif

var config = NetworkConfiguration(environment: environment)
config.environment.logLevel = environment.kind == .production ? .error : .debug
config.sslPinning = environment.kind == .production ? .certificateResources(["api"]) : .disabled

EnvironmentKind is .development / .qa / .staging / .production. Each environment sets its own base URL, default headers, timeout, and log level.

Logging

config.environment.logLevel = .basic    // .none / .error / .basic / .verbose / .debug
config.logger = ConsoleNetworkLogger()  // default; writes to os.Logger
config.redactedHeaders = ["x-internal-signature"]
config.redactedBodyKeys = ["ssn", "card_number"]
config.redactedQueryItems = ["session"]     // on top of access_token, api_key, signature, code, ...
Level Emits
.none nothing
.error failures only
.basic method, URL, status, duration
.verbose + redacted headers
.debug + redacted bodies

Authorization, Cookie, Set-Cookie, X-API-Key, and common token body keys are always redacted, at every level, and sensitive query items (including the key name APIKeyAuth sends in the query, automatically) are masked out of logged URLs too. A custom sink is any NetworkLogger conformer (ship logs to your aggregator, and so on).

Best practice: keep sensitive fields out of logs by adding them to redactedBodyKeys and redactedQueryItems.

Metrics and observability

let metrics = InMemoryMetrics()
config.metrics = metrics

let snapshot = await metrics.snapshot()
snapshot.requestCount
snapshot.successCount
snapshot.failureCount
snapshot.statusCodeHistogram       // [200: 431, 404: 3, 500: 1]
snapshot.averageDuration
snapshot.p95Duration
snapshot.retryCount
snapshot.tokenRefreshCount

Failure events sent to a NetworkMetrics sink carry no request or response body, and credential headers and URL query secrets are masked, so a sink that forwards to an APM (Sentry, Datadog, and so on) does not leak tokens or payloads.

InMemoryMetrics is an actor you inspect in tests; NoopMetrics is the default. A custom NetworkMetrics conformer forwards events to your APM.

Tracing

  • Every request carries a unique X-Request-ID.
  • Wrap a logical operation so its requests share one X-Correlation-ID:
try await client.withCorrelation("checkout-\(orderID)") {
    _ = try await client.request(CreateOrder(draft))
    _ = try await client.request(ChargeCard(orderID))
}

Clone this wiki locally