Skip to content

Commit 5d624a7

Browse files
committed
Add server example
1 parent 3f2d8df commit 5d624a7

13 files changed

+395
-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,25 @@
1+
import Hummingbird
2+
import OpenFeature
3+
import ServiceLifecycle
4+
5+
public struct APIService: Service {
6+
private let app: Application<RouterResponder<APIRequestContext>>
7+
private let client: OpenFeatureClient
8+
9+
public init(router: Router<APIRequestContext>) {
10+
client = OpenFeatureSystem.client()
11+
12+
router
13+
.addMiddleware {
14+
AuthMiddleware()
15+
EvaluationContextMiddleware()
16+
}
17+
.addRoutes(FeedController().routes)
18+
19+
app = Application(router: router)
20+
}
21+
22+
public func run() async throws {
23+
try await app.run()
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import HTTPTypes
2+
import Hummingbird
3+
import HummingbirdAuth
4+
5+
struct AuthMiddleware: AuthenticatorMiddleware {
6+
typealias Context = APIRequestContext
7+
8+
func authenticate(request: Request, context: APIRequestContext) async throws -> User? {
9+
guard let userID = request.headers[.userID] else { return nil }
10+
return User(id: userID)
11+
}
12+
}
13+
14+
extension HTTPField.Name {
15+
fileprivate static let userID = Self("X-User-Id")!
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Hummingbird
2+
import OpenFeature
3+
4+
struct EvaluationContextMiddleware: MiddlewareProtocol {
5+
func handle(
6+
_ request: Request,
7+
context: APIRequestContext,
8+
next: (Request, APIRequestContext) async throws -> Response
9+
) async throws -> Response {
10+
var evaluationContext = OpenFeatureEvaluationContext.current ?? OpenFeatureEvaluationContext()
11+
evaluationContext.targetingKey = context.identity?.id
12+
return try await OpenFeatureEvaluationContext.$current.withValue(evaluationContext) {
13+
try await next(request, context)
14+
}
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Hummingbird
2+
import OpenFeature
3+
4+
struct FeedController {
5+
private let featureFlags: OpenFeatureClient
6+
7+
init() {
8+
featureFlags = OpenFeatureSystem.client()
9+
}
10+
11+
var routes: RouteCollection<APIRequestContext> {
12+
let routes = RouteCollection(context: APIRequestContext.self)
13+
routes.get(use: list)
14+
return routes
15+
}
16+
17+
private func list(request: Request, context: APIRequestContext) async throws -> Feed {
18+
let useNewFeedAlgorithm = await featureFlags.value(for: "experimental-feed-algorithm", defaultingTo: false)
19+
20+
if useNewFeedAlgorithm {
21+
// the new algorithm is faster but unfortunately still contains some bugs
22+
if UInt.random(in: 0 ..< 100) == 42 {
23+
throw HTTPError(.internalServerError)
24+
}
25+
try await Task.sleep(for: .seconds(1))
26+
return Feed.stub
27+
} else {
28+
try await Task.sleep(for: .seconds(2))
29+
return Feed.stub
30+
}
31+
}
32+
}
33+
34+
struct Feed: ResponseCodable {
35+
let posts: [Post]
36+
37+
struct Post: ResponseCodable {
38+
let id: String
39+
}
40+
41+
static let stub = Feed(posts: [
42+
Post(id: "1"),
43+
Post(id: "2"),
44+
Post(id: "3"),
45+
])
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Hummingbird
2+
import HummingbirdAuth
3+
4+
public struct APIRequestContext: AuthRequestContext, Sendable {
5+
public var coreContext: CoreRequestContextStorage
6+
public var identity: User?
7+
8+
public init(source: ApplicationRequestContextSource) {
9+
coreContext = .init(source: source)
10+
}
11+
}
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
public struct User: Identifiable, Sendable {
2+
public let id: String
3+
}

0 commit comments

Comments
 (0)