Skip to content

Commit 450505e

Browse files
committed
Add server example
1 parent 4327a37 commit 450505e

13 files changed

+486
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"name": "Server",
3+
"workspaceFolder": "/workspace",
4+
"dockerComposeFile": [
5+
"docker-compose.yaml"
6+
],
7+
"service": "server",
8+
"runServices": [
9+
"flipt",
10+
"jaeger",
11+
"otel-collector",
12+
"server"
13+
],
14+
"shutdownAction": "stopCompose",
15+
"features": {
16+
"ghcr.io/devcontainers/features/common-utils:2": {
17+
"installZsh": "false",
18+
"username": "server",
19+
"upgradePackages": "false"
20+
},
21+
"ghcr.io/devcontainers/features/git:1": {
22+
"version": "os-provided",
23+
"ppa": "false"
24+
}
25+
},
26+
"customizations": {
27+
"vscode": {
28+
"settings": {
29+
"lldb.library": "/usr/lib/liblldb.so",
30+
"swift.autoGenerateLaunchConfigurations": false
31+
},
32+
"extensions": [
33+
"swiftlang.swift-vscode"
34+
]
35+
}
36+
},
37+
"forwardPorts": [8080, 8081, 16686],
38+
"portsAttributes": {
39+
"8080": {
40+
"label": "API",
41+
"onAutoForward": "notify"
42+
},
43+
"8081": {
44+
"label": "Flipt"
45+
},
46+
"16686": {
47+
"label": "Jaeger"
48+
}
49+
},
50+
"remoteUser": "server"
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: swift-ofrep-example-server
2+
services:
3+
server:
4+
image: swift:6.0
5+
command: sleep infinity
6+
volumes:
7+
- ..:/workspace:cached
8+
9+
# ==/ Feature Flagging Systems \==
10+
11+
flipt:
12+
image: flipt/flipt:v1.54.2
13+
command: ["./flipt", "--force-migrate"]
14+
depends_on:
15+
flipt-seed:
16+
condition: service_completed_successfully
17+
volumes:
18+
- flipt_data:/var/opt/flipt
19+
ports:
20+
- 8081:8080 # OFREP & Flipt UI
21+
environment:
22+
FLIPT_LOG_LEVEL: debug
23+
FLIPT_TRACING_ENABLED: true
24+
FLIPT_TRACING_EXPORTER: otlp
25+
FLIPT_TRACING_OTLP_ENDPOINT: otel-collector:4317
26+
FLIPT_METRICS_ENABLED: false
27+
FLIPT_META_TELEMETRY_ENABLED: false
28+
flipt-seed:
29+
image: flipt/flipt:v1.54.2
30+
command: ["./flipt", "import", "--skip-existing" , "flipt.yaml"]
31+
volumes:
32+
- ./flipt-seed.yaml:/flipt.yaml
33+
- flipt_data:/var/opt/flipt
34+
environment:
35+
FLIPT_LOG_LEVEL: debug
36+
FLIPT_META_TELEMETRY_ENABLED: false
37+
38+
# ==/ Observability \==
39+
40+
otel-collector:
41+
image: otel/opentelemetry-collector-contrib:latest
42+
command: ["--config=/etc/otel-collector-config.yaml"]
43+
volumes:
44+
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
45+
ports:
46+
- "4317:4317" # OTLP/gRPC
47+
48+
jaeger:
49+
image: jaegertracing/jaeger:2.3.0
50+
ports:
51+
- "16686:16686" # Jaeger UI
52+
53+
volumes:
54+
flipt_data:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
version: "1.4"
2+
namespace:
3+
key: default
4+
name: Default
5+
description: Default namespace
6+
flags:
7+
- key: experimental-feed-algorithm
8+
name: experimental-feed-algorithm
9+
type: BOOLEAN_FLAG_TYPE
10+
enabled: false
11+
rollouts:
12+
- segment:
13+
key: internal-testers
14+
value: true
15+
segments:
16+
- key: internal-testers
17+
name: internal-testers
18+
constraints:
19+
- type: STRING_COMPARISON_TYPE
20+
property: targetingKey
21+
operator: isoneof
22+
value: '["42"]'
23+
match_type: ALL_MATCH_TYPE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
receivers:
2+
otlp:
3+
protocols:
4+
grpc:
5+
endpoint: "otel-collector:4317"
6+
7+
exporters:
8+
otlp/jaeger:
9+
endpoint: "jaeger:4317"
10+
tls:
11+
insecure: true
12+
13+
service:
14+
pipelines:
15+
traces:
16+
receivers: [otlp]
17+
exporters: [otlp/jaeger]
18+
19+
# yaml-language-server: $schema=https://raw.githubusercontent.com/srikanthccv/otelcol-jsonschema/main/schema.json

Examples/server/Package.swift

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// swift-tools-version:6.0
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "server",
6+
platforms: [.macOS(.v15)],
7+
products: [
8+
.executable(name: "server", targets: ["CTL"])
9+
],
10+
dependencies: [
11+
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"),
12+
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
13+
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0"),
14+
.package(url: "https://github.com/swift-otel/swift-otel.git", .upToNextMinor(from: "0.11.0")),
15+
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0"),
16+
.package(url: "https://github.com/hummingbird-project/hummingbird-auth.git", from: "2.0.0"),
17+
.package(url: "https://github.com/swift-open-feature/swift-open-feature.git", branch: "main"),
18+
.package(url: "https://github.com/swift-open-feature/swift-ofrep.git", branch: "main"),
19+
20+
// override HTTP Client until Tracing PR is merged
21+
.package(url: "https://github.com/slashmo/async-http-client.git", branch: "feature/tracing"),
22+
],
23+
targets: [
24+
.executableTarget(
25+
name: "CTL",
26+
dependencies: [
27+
.target(name: "API"),
28+
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
29+
.product(name: "Logging", package: "swift-log"),
30+
.product(name: "Tracing", package: "swift-distributed-tracing"),
31+
.product(name: "OTel", package: "swift-otel"),
32+
.product(name: "OTLPGRPC", package: "swift-otel"),
33+
.product(name: "Hummingbird", package: "hummingbird"),
34+
.product(name: "OpenFeature", package: "swift-open-feature"),
35+
.product(name: "OpenFeatureTracing", package: "swift-open-feature"),
36+
.product(name: "OFREP", package: "swift-ofrep"),
37+
]
38+
),
39+
.target(
40+
name: "API",
41+
dependencies: [
42+
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
43+
.product(name: "Logging", package: "swift-log"),
44+
.product(name: "Hummingbird", package: "hummingbird"),
45+
.product(name: "HummingbirdAuth", package: "hummingbird-auth"),
46+
.product(name: "OpenFeature", package: "swift-open-feature"),
47+
]
48+
),
49+
]
50+
)

Examples/server/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Swift OFREP server example
2+
3+
An HTTP server that can be altered at runtime using feature flags powered by Swift OFREP.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift OpenFeature open source project
4+
//
5+
// Copyright (c) 2025 the Swift OpenFeature project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import Hummingbird
15+
import OpenFeature
16+
import ServiceLifecycle
17+
18+
public struct APIService: Service {
19+
private let app: Application<RouterResponder<APIRequestContext>>
20+
private let client: OpenFeatureClient
21+
22+
public init(router: Router<APIRequestContext>) {
23+
client = OpenFeatureSystem.client()
24+
25+
router
26+
.addMiddleware {
27+
AuthMiddleware()
28+
EvaluationContextMiddleware()
29+
}
30+
.addRoutes(FeedController().routes)
31+
32+
app = Application(router: router)
33+
}
34+
35+
public func run() async throws {
36+
try await app.run()
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift OpenFeature open source project
4+
//
5+
// Copyright (c) 2025 the Swift OpenFeature project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import HTTPTypes
15+
import Hummingbird
16+
import HummingbirdAuth
17+
18+
struct AuthMiddleware: AuthenticatorMiddleware {
19+
typealias Context = APIRequestContext
20+
21+
func authenticate(request: Request, context: APIRequestContext) async throws -> User? {
22+
guard let userID = request.headers[.userID] else { return nil }
23+
return User(id: userID)
24+
}
25+
}
26+
27+
extension HTTPField.Name {
28+
fileprivate static let userID = Self("X-User-Id")!
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift OpenFeature open source project
4+
//
5+
// Copyright (c) 2025 the Swift OpenFeature project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import Hummingbird
15+
import OpenFeature
16+
17+
struct EvaluationContextMiddleware: MiddlewareProtocol {
18+
func handle(
19+
_ request: Request,
20+
context: APIRequestContext,
21+
next: (Request, APIRequestContext) async throws -> Response
22+
) async throws -> Response {
23+
var evaluationContext = OpenFeatureEvaluationContext.current ?? OpenFeatureEvaluationContext()
24+
evaluationContext.targetingKey = context.identity?.id
25+
return try await OpenFeatureEvaluationContext.$current.withValue(evaluationContext) {
26+
try await next(request, context)
27+
}
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift OpenFeature open source project
4+
//
5+
// Copyright (c) 2025 the Swift OpenFeature project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import Hummingbird
15+
import OpenFeature
16+
17+
struct FeedController {
18+
private let featureFlags: OpenFeatureClient
19+
20+
init() {
21+
featureFlags = OpenFeatureSystem.client()
22+
}
23+
24+
var routes: RouteCollection<APIRequestContext> {
25+
let routes = RouteCollection(context: APIRequestContext.self)
26+
routes.get(use: list)
27+
return routes
28+
}
29+
30+
private func list(request: Request, context: APIRequestContext) async throws -> Feed {
31+
let useNewFeedAlgorithm = await featureFlags.value(for: "experimental-feed-algorithm", defaultingTo: false)
32+
33+
if useNewFeedAlgorithm {
34+
// the new algorithm is faster but unfortunately still contains some bugs
35+
if UInt.random(in: 0..<100) == 42 {
36+
throw HTTPError(.internalServerError)
37+
}
38+
try await Task.sleep(for: .seconds(1))
39+
return Feed.stub
40+
} else {
41+
try await Task.sleep(for: .seconds(2))
42+
return Feed.stub
43+
}
44+
}
45+
}
46+
47+
struct Feed: ResponseCodable {
48+
let posts: [Post]
49+
50+
struct Post: ResponseCodable {
51+
let id: String
52+
}
53+
54+
static let stub = Feed(posts: [
55+
Post(id: "1"),
56+
Post(id: "2"),
57+
Post(id: "3"),
58+
])
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift OpenFeature open source project
4+
//
5+
// Copyright (c) 2025 the Swift OpenFeature project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
import Hummingbird
15+
import HummingbirdAuth
16+
17+
public struct APIRequestContext: AuthRequestContext, Sendable {
18+
public var coreContext: CoreRequestContextStorage
19+
public var identity: User?
20+
21+
public init(source: ApplicationRequestContextSource) {
22+
coreContext = .init(source: source)
23+
}
24+
}

0 commit comments

Comments
 (0)