diff --git a/.dockerignore b/.dockerignore
index 94fdc73a30..5a21f3878f 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -22,3 +22,7 @@ src/*/*/Dockerfile
./src/shipping/target
###################################
+###################################
+# cart
+./src/cart/.build/
+###################################
diff --git a/.env b/.env
index c631f81a54..3ee0ca6f7a 100644
--- a/.env
+++ b/.env
@@ -56,7 +56,7 @@ AD_DOCKERFILE=./src/ad/Dockerfile
# Cart Service
CART_PORT=7070
CART_ADDR=cart:${CART_PORT}
-CART_DOCKERFILE=./src/cart/src/Dockerfile
+CART_DOCKERFILE=./src/cart/Dockerfile
# Checkout Service
CHECKOUT_PORT=5050
@@ -147,8 +147,9 @@ KAFKA_ADDR=${KAFKA_HOST}:${KAFKA_PORT}
KAFKA_DOCKERFILE=./src/kafka/Dockerfile
# Valkey
+VALKEY_HOST=valkey-cart
VALKEY_PORT=6379
-VALKEY_ADDR=valkey-cart:${VALKEY_PORT}
+VALKEY_ADDR=${VALKEY_HOST}:${VALKEY_PORT}
# ********************
# Telemetry Components
diff --git a/.gitignore b/.gitignore
index 4dacf35195..ad967aec30 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@ obj/
.gradle/
.idea/
build/
+.build/
node_modules/
coverage
.next/
@@ -42,7 +43,6 @@ test/tracetesting/tracetesting-vars.yaml
# Ignore copied/generated protobuf files
/src/accounting/src/protos/
-/src/cart/src/protos/
/src/fraud-detection/src/main/proto
/src/payment/demo.proto
/src/shipping/proto/
diff --git a/docker-compose.minimal.yml b/docker-compose.minimal.yml
index 9921830b9b..6f95da5e8a 100644
--- a/docker-compose.minimal.yml
+++ b/docker-compose.minimal.yml
@@ -74,7 +74,6 @@ services:
- OTEL_EXPORTER_OTLP_ENDPOINT
- OTEL_RESOURCE_ATTRIBUTES
- OTEL_SERVICE_NAME=cart
- - ASPNETCORE_URLS=http://*:${CART_PORT}
depends_on:
valkey-cart:
condition: service_started
diff --git a/docker-compose.yml b/docker-compose.yml
index d8549520bb..a59109a1a7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -101,12 +101,13 @@ services:
environment:
- CART_PORT
- FLAGD_HOST
- - FLAGD_PORT
- - VALKEY_ADDR
- - OTEL_EXPORTER_OTLP_ENDPOINT
+ - FLAGD_OFREP_PORT
+ - VALKEY_HOST
+ - VALKEY_PORT
+ - OTEL_EXPORTER_OTLP_ENDPOINT=http://${OTEL_COLLECTOR_HOST}:${OTEL_COLLECTOR_PORT_HTTP}
- OTEL_RESOURCE_ATTRIBUTES
- OTEL_SERVICE_NAME=cart
- - ASPNETCORE_URLS=http://*:${CART_PORT}
+ - OTEL_LOG_LEVEL=debug
depends_on:
valkey-cart:
condition: service_started
diff --git a/docker-gen-proto.sh b/docker-gen-proto.sh
index ed2e3032c5..6dc64db660 100755
--- a/docker-gen-proto.sh
+++ b/docker-gen-proto.sh
@@ -43,10 +43,24 @@ gen_proto_ts() {
/build/pb/demo.proto'
}
+gen_proto_swift() {
+ echo "Generating Swift protobuf files for $1"
+ docker build -f "src/$1/genproto/Dockerfile" -t "$1-genproto" .
+ docker run --rm -e SERVICE=$1 -v $(pwd):/build "$1-genproto" /bin/sh -c '
+ mkdir -p /build/src/$SERVICE/Sources/CTL/Generated && \
+ protoc -I /build/pb \
+ --plugin="$(swift build --show-bin-path)/protoc-gen-swift" \
+ --plugin="$(swift build --show-bin-path)/protoc-gen-grpc-swift-2" \
+ --swift_out="/build/src/$SERVICE/Sources/CTL/Generated" \
+ --grpc-swift-2_opt=Client=false \
+ --grpc-swift-2_out="/build/src/$SERVICE/Sources/CTL/Generated" \
+ /build/pb/demo.proto'
+}
+
if [ -z "$1" ]; then
#gen_proto_dotnet accounting
#gen_proto_java ad
- #gen_proto_dotnet cart
+ gen_proto_swift cart
gen_proto_go checkout
gen_proto_cpp currency
#gen_proto_ruby email
diff --git a/src/cart/.dockerignore b/src/cart/.dockerignore
new file mode 100644
index 0000000000..3de0d9ffb4
--- /dev/null
+++ b/src/cart/.dockerignore
@@ -0,0 +1,3 @@
+**/.build/
+**/.swiftpm/
+Dockerfile*
diff --git a/src/cart/.gitignore b/src/cart/.gitignore
new file mode 100644
index 0000000000..ea0f7b605e
--- /dev/null
+++ b/src/cart/.gitignore
@@ -0,0 +1 @@
+.swiftpm/
diff --git a/src/cart/Directory.Build.props b/src/cart/Directory.Build.props
deleted file mode 100644
index bfd1cab4dd..0000000000
--- a/src/cart/Directory.Build.props
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
- true
- all
- low
-
-
-
- true
-
-
diff --git a/src/cart/Dockerfile b/src/cart/Dockerfile
new file mode 100644
index 0000000000..a447dae164
--- /dev/null
+++ b/src/cart/Dockerfile
@@ -0,0 +1,75 @@
+# ================================
+# Build image
+# ================================
+FROM swift:6.1-noble AS build
+
+# Install OS updates
+RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
+ && apt-get -q update \
+ && apt-get -q dist-upgrade -y \
+ && apt-get install -y libjemalloc-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+# Set up a build area
+WORKDIR /build
+
+# First just resolve dependencies.
+# This creates a cached layer that can be reused
+# as long as your Package.swift/Package.resolved
+# files do not change.
+COPY ./src/cart/Package.* ./
+RUN swift package resolve
+
+# Copy entire repo into container
+COPY ./src/cart .
+
+# Build the application, with optimizations, with static linking, and using jemalloc
+RUN swift build -c release \
+ --product "cart" \
+ --static-swift-stdlib \
+ -Xlinker -ljemalloc
+
+# Switch to the staging area
+WORKDIR /staging
+
+# Copy main executable to staging area
+RUN cp "$(swift build --package-path /build -c release --show-bin-path)/cart" ./
+
+# Copy static swift backtracer binary to staging area
+RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./
+
+# ================================
+# Run image
+# ================================
+FROM ubuntu:noble
+
+# Make sure all system packages are up to date, and install only essential packages.
+RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
+ && apt-get -q update \
+ && apt-get -q dist-upgrade -y \
+ && apt-get -q install -y \
+ libjemalloc2 \
+ ca-certificates \
+ tzdata \
+ && rm -r /var/lib/apt/lists/*
+
+# Create a cart user and group with /app as its home directory
+RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app cart
+
+# Switch to the new home directory
+WORKDIR /app
+
+# Copy built executable and any staged resources from builder
+COPY --from=build --chown=cart:cart /staging /app
+
+# Provide configuration needed by the built-in crash reporter and some sensible default behaviors.
+ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static
+
+# Ensure all further commands run as the cart user
+USER cart:cart
+
+# Let Docker bind to the cart port
+EXPOSE ${CART_PORT}
+
+# Start the cart service when the image is run
+ENTRYPOINT ["./cart"]
diff --git a/src/cart/NuGet.config b/src/cart/NuGet.config
deleted file mode 100644
index adcb2a925f..0000000000
--- a/src/cart/NuGet.config
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/src/cart/Package.resolved b/src/cart/Package.resolved
new file mode 100644
index 0000000000..e5c485a3a3
--- /dev/null
+++ b/src/cart/Package.resolved
@@ -0,0 +1,321 @@
+{
+ "originHash" : "4291195db0b1cbe39e2dcf442d07ad9295f392ada40950197bfff511b85879d0",
+ "pins" : [
+ {
+ "identity" : "async-http-client",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/slashmo/async-http-client.git",
+ "state" : {
+ "branch" : "feature/context-propagation",
+ "revision" : "17883809e29b5d6a79fcda5045069cf288490d63"
+ }
+ },
+ {
+ "identity" : "grpc-swift-2",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/grpc/grpc-swift-2.git",
+ "state" : {
+ "revision" : "0e52abf7ea0fca7ffe876953aa41feff84cc6e29",
+ "version" : "2.1.0"
+ }
+ },
+ {
+ "identity" : "grpc-swift-extras",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/grpc/grpc-swift-extras.git",
+ "state" : {
+ "revision" : "dd34dae247c0b49db0e81dca5da64033b02cb49c",
+ "version" : "2.0.0"
+ }
+ },
+ {
+ "identity" : "grpc-swift-nio-transport",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/grpc/grpc-swift-nio-transport.git",
+ "state" : {
+ "revision" : "108495b705a68f6931281a96156532029ff8b9ed",
+ "version" : "2.1.1"
+ }
+ },
+ {
+ "identity" : "grpc-swift-protobuf",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/grpc/grpc-swift-protobuf.git",
+ "state" : {
+ "revision" : "c008d356d9e9c2255602711888bf25b542a30919",
+ "version" : "2.1.1"
+ }
+ },
+ {
+ "identity" : "swift-algorithms",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-algorithms.git",
+ "state" : {
+ "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023",
+ "version" : "1.2.1"
+ }
+ },
+ {
+ "identity" : "swift-argument-parser",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-argument-parser.git",
+ "state" : {
+ "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3",
+ "version" : "1.6.1"
+ }
+ },
+ {
+ "identity" : "swift-asn1",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-asn1.git",
+ "state" : {
+ "revision" : "f70225981241859eb4aa1a18a75531d26637c8cc",
+ "version" : "1.4.0"
+ }
+ },
+ {
+ "identity" : "swift-async-algorithms",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-async-algorithms.git",
+ "state" : {
+ "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b",
+ "version" : "1.0.4"
+ }
+ },
+ {
+ "identity" : "swift-atomics",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-atomics.git",
+ "state" : {
+ "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7",
+ "version" : "1.3.0"
+ }
+ },
+ {
+ "identity" : "swift-certificates",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-certificates.git",
+ "state" : {
+ "revision" : "20c451f1ad8e344e61ddbb34ef196653d4b73ea6",
+ "version" : "1.13.0"
+ }
+ },
+ {
+ "identity" : "swift-collections",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-collections.git",
+ "state" : {
+ "revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341",
+ "version" : "1.2.1"
+ }
+ },
+ {
+ "identity" : "swift-crypto",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-crypto.git",
+ "state" : {
+ "revision" : "d1c6b70f7c5f19fb0b8750cb8dcdf2ea6e2d8c34",
+ "version" : "3.15.0"
+ }
+ },
+ {
+ "identity" : "swift-distributed-tracing",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-distributed-tracing.git",
+ "state" : {
+ "branch" : "feature/tracing-test-kit",
+ "revision" : "b80efc1c718e51620c394fedbf4fe644cf71b81f"
+ }
+ },
+ {
+ "identity" : "swift-http-structured-headers",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-http-structured-headers.git",
+ "state" : {
+ "revision" : "1625f271afb04375bf48737a5572613248d0e7a0",
+ "version" : "1.4.0"
+ }
+ },
+ {
+ "identity" : "swift-http-types",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-http-types.git",
+ "state" : {
+ "revision" : "a0a57e949a8903563aba4615869310c0ebf14c03",
+ "version" : "1.4.0"
+ }
+ },
+ {
+ "identity" : "swift-log",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-log.git",
+ "state" : {
+ "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2",
+ "version" : "1.6.4"
+ }
+ },
+ {
+ "identity" : "swift-metrics",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-metrics.git",
+ "state" : {
+ "revision" : "4c83e1cdf4ba538ef6e43a9bbd0bcc33a0ca46e3",
+ "version" : "2.7.0"
+ }
+ },
+ {
+ "identity" : "swift-nio",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio.git",
+ "state" : {
+ "revision" : "1c30f0f2053b654e3d1302492124aa6d242cdba7",
+ "version" : "2.86.0"
+ }
+ },
+ {
+ "identity" : "swift-nio-extras",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio-extras.git",
+ "state" : {
+ "revision" : "a55c3dd3a81d035af8a20ce5718889c0dcab073d",
+ "version" : "1.29.0"
+ }
+ },
+ {
+ "identity" : "swift-nio-http2",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio-http2.git",
+ "state" : {
+ "revision" : "5e9e99ec96c53bc2c18ddd10c1e25a3cd97c55e5",
+ "version" : "1.38.0"
+ }
+ },
+ {
+ "identity" : "swift-nio-ssl",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio-ssl.git",
+ "state" : {
+ "revision" : "737e550e607d82bf15bdfddf158ec61652ce836f",
+ "version" : "2.34.0"
+ }
+ },
+ {
+ "identity" : "swift-nio-transport-services",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-nio-transport-services.git",
+ "state" : {
+ "revision" : "e645014baea2ec1c2db564410c51a656cf47c923",
+ "version" : "1.25.1"
+ }
+ },
+ {
+ "identity" : "swift-numerics",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-numerics.git",
+ "state" : {
+ "revision" : "bbadd4b853a33fd78c4ae977d17bb2af15eb3f2a",
+ "version" : "1.1.0"
+ }
+ },
+ {
+ "identity" : "swift-ofrep",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swift-open-feature/swift-ofrep.git",
+ "state" : {
+ "branch" : "main",
+ "revision" : "cafc6f407e89bec0c8d474ac369024235ff0b241"
+ }
+ },
+ {
+ "identity" : "swift-open-feature",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swift-open-feature/swift-open-feature.git",
+ "state" : {
+ "branch" : "main",
+ "revision" : "7fbef4bd2560f1161078312db70237f5c83d4ce8"
+ }
+ },
+ {
+ "identity" : "swift-openapi-async-http-client",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swift-server/swift-openapi-async-http-client.git",
+ "state" : {
+ "revision" : "915aeff168625b0f88a5810ee7ab8e9c00af671b",
+ "version" : "1.1.0"
+ }
+ },
+ {
+ "identity" : "swift-openapi-runtime",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-openapi-runtime.git",
+ "state" : {
+ "revision" : "8f33cc5dfe81169fb167da73584b9c72c3e8bc23",
+ "version" : "1.8.2"
+ }
+ },
+ {
+ "identity" : "swift-otel",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swift-otel/swift-otel.git",
+ "state" : {
+ "revision" : "6e908cb34cab2a0b739c4d6db4c1a4faad0a6ec4",
+ "version" : "1.0.0-alpha.2"
+ }
+ },
+ {
+ "identity" : "swift-protobuf",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-protobuf.git",
+ "state" : {
+ "revision" : "e3f69fd321d0c9fcdc16fb576a0cdd956675face",
+ "version" : "1.31.0"
+ }
+ },
+ {
+ "identity" : "swift-service-context",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-service-context.git",
+ "state" : {
+ "revision" : "1983448fefc717a2bc2ebde5490fe99873c5b8a6",
+ "version" : "1.2.1"
+ }
+ },
+ {
+ "identity" : "swift-service-lifecycle",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swift-server/swift-service-lifecycle.git",
+ "state" : {
+ "revision" : "e7187309187695115033536e8fc9b2eb87fd956d",
+ "version" : "2.8.0"
+ }
+ },
+ {
+ "identity" : "swift-system",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-system.git",
+ "state" : {
+ "revision" : "890830fff1a577dc83134890c7984020c5f6b43b",
+ "version" : "1.6.2"
+ }
+ },
+ {
+ "identity" : "swift-w3c-trace-context",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swift-otel/swift-w3c-trace-context.git",
+ "state" : {
+ "revision" : "3da4b79545b38cf5551f1c525d800756f38cb697",
+ "version" : "1.0.0-beta.3"
+ }
+ },
+ {
+ "identity" : "valkey-swift",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/slashmo/valkey-swift.git",
+ "state" : {
+ "branch" : "feature/distributed-tracing",
+ "revision" : "6ca1b36c5e4df39b40d2a732bd599df03c5780ab"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/src/cart/Package.swift b/src/cart/Package.swift
new file mode 100644
index 0000000000..64ebcfb733
--- /dev/null
+++ b/src/cart/Package.swift
@@ -0,0 +1,43 @@
+// swift-tools-version:6.1
+import PackageDescription
+
+let package = Package(
+ name: "cart",
+ platforms: [.macOS(.v15)],
+ products: [
+ .executable(name: "cart", targets: ["CTL"])
+ ],
+ dependencies: [
+ .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
+ .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.0.0"),
+ .package(url: "https://github.com/grpc/grpc-swift-2.git", from: "2.0.0"),
+ .package(url: "https://github.com/grpc/grpc-swift-protobuf.git", from: "2.0.0"),
+ .package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", from: "2.0.0"),
+ .package(url: "https://github.com/grpc/grpc-swift-extras.git", from: "2.0.0"),
+ .package(url: "https://github.com/swift-otel/swift-otel.git", exact: "1.0.0-alpha.2", traits: ["OTLPHTTP"]),
+ .package(url: "https://github.com/swift-open-feature/swift-open-feature.git", branch: "main"),
+ .package(url: "https://github.com/swift-open-feature/swift-ofrep.git", branch: "main"),
+ .package(url: "https://github.com/slashmo/async-http-client.git", branch: "feature/context-propagation"),
+ .package(url: "https://github.com/slashmo/valkey-swift.git", branch: "feature/distributed-tracing"),
+ ],
+ targets: [
+ .executableTarget(
+ name: "CTL",
+ dependencies: [
+ .product(name: "ArgumentParser", package: "swift-argument-parser"),
+ .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"),
+ .product(name: "GRPCCore", package: "grpc-swift-2"),
+ .product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"),
+ .product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"),
+ .product(name: "GRPCServiceLifecycle", package: "grpc-swift-extras"),
+ .product(name: "GRPCOTelTracingInterceptors", package: "grpc-swift-extras"),
+ .product(name: "OTel", package: "swift-otel"),
+ .product(name: "OpenFeature", package: "swift-open-feature"),
+ .product(name: "OpenFeatureTracing", package: "swift-open-feature"),
+ .product(name: "OFREP", package: "swift-ofrep"),
+ .product(name: "Valkey", package: "valkey-swift"),
+ ]
+ )
+ ],
+ swiftLanguageModes: [.v6]
+)
diff --git a/src/cart/README.md b/src/cart/README.md
index f2b2db6484..1f9738da40 100644
--- a/src/cart/README.md
+++ b/src/cart/README.md
@@ -4,7 +4,7 @@ This service stores user shopping carts in Valkey.
## Local Build
-Run `dotnet restore` and `dotnet build`.
+Run `swift build`.
## Docker Build
diff --git a/src/cart/Sources/CTL/CartCTL.swift b/src/cart/Sources/CTL/CartCTL.swift
new file mode 100644
index 0000000000..b97451b163
--- /dev/null
+++ b/src/cart/Sources/CTL/CartCTL.swift
@@ -0,0 +1,131 @@
+import ArgumentParser
+import Foundation
+import Logging
+import GRPCCore
+import GRPCNIOTransportHTTP2
+import ServiceLifecycle
+import GRPCServiceLifecycle
+import GRPCOTelTracingInterceptors
+import OTel
+import OpenFeature
+import OpenFeatureTracing
+import OFREP
+import Tracing
+import Valkey
+
+@main
+struct CartCTL: AsyncParsableCommand {
+ func run() async throws {
+ let observability = try OTel.bootstrap()
+ let logger = Logger(label: "cart")
+
+ guard let port = ProcessInfo.processInfo.environment["CART_PORT"].flatMap(Int.init),
+ let ofrepHost = ProcessInfo.processInfo.environment["FLAGD_HOST"],
+ let ofrepPort = ProcessInfo.processInfo.environment["FLAGD_OFREP_PORT"],
+ let valkeyHost = ProcessInfo.processInfo.environment["VALKEY_HOST"],
+ let valkeyPort = ProcessInfo.processInfo.environment["VALKEY_PORT"].flatMap(Int.init) else {
+ Self.exit()
+ }
+
+ let valkeyClient = ValkeyClient(.hostname(valkeyHost, port: valkeyPort), logger: logger)
+
+ let ofrepProvider = OFREPProvider(serverURL: URL(string: "http://\(ofrepHost):\(ofrepPort)")!)
+ OpenFeatureSystem.setProvider(ofrepProvider)
+ OpenFeatureSystem.addHooks([OpenFeatureTracingHook()])
+
+ let service = CartService(
+ openFeatureClient: OpenFeatureSystem.client(),
+ valkeyClient: valkeyClient
+ )
+ let server = GRPCServer(
+ transport: .http2NIOPosix(
+ address: .ipv4(host: "0.0.0.0", port: port),
+ transportSecurity: .plaintext
+ ),
+ services: [service],
+ interceptors: [
+ GRPCMetricsInterceptor(serverHostname: "0.0.0.0", networkTransportMethod: "tcp"),
+ ServerOTelTracingInterceptor(serverHostname: "0.0.0.0", networkTransportMethod: "tcp")
+ ]
+ )
+
+ let serviceGroup = ServiceGroup(
+ services: [observability, valkeyClient, ofrepProvider, server],
+ gracefulShutdownSignals: [.sigint, .sigterm],
+ logger: Logger(label: "cart")
+ )
+
+ try await serviceGroup.run()
+ }
+}
+
+struct CartService: Oteldemo_CartService.SimpleServiceProtocol {
+ let openFeatureClient: OpenFeatureClient
+ let valkeyClient: ValkeyClient
+ private let logger = Logger(label: "CartService")
+
+ func addItem(
+ request: Oteldemo_AddItemRequest,
+ context: ServerContext
+ ) async throws -> Oteldemo_Empty {
+ var cart = try await cart(userID: request.userID) ?? Oteldemo_Cart.with { $0.userID = request.userID }
+
+ if let existingIndex = cart.items.firstIndex(where: { $0.productID == request.item.productID }) {
+ cart.items[existingIndex].quantity += request.item.quantity
+ } else {
+ cart.items.append(request.item)
+ }
+
+ let serializedCart: [UInt8] = try cart.serializedBytes()
+ try await valkeyClient.hset(ValkeyKey(request.userID), data: [.init(field: "cart", value: serializedCart)])
+ try await valkeyClient.expire(ValkeyKey(request.userID), seconds: 60 * 60)
+
+ return Oteldemo_Empty()
+ }
+
+ func getCart(
+ request: Oteldemo_GetCartRequest,
+ context: ServerContext
+ ) async throws -> Oteldemo_Cart {
+ logger.info("Fetch cart.", metadata: ["user.id": "\(request.userID)"])
+
+ if let cart = try await cart(userID: request.userID) {
+ return cart
+ } else {
+ // We decided to return empty cart in cases when user wasn't in the cache before
+ return Oteldemo_Cart()
+ }
+ }
+
+ func emptyCart(
+ request: Oteldemo_EmptyCartRequest,
+ context: ServerContext
+ ) async throws -> Oteldemo_Empty {
+ let useExperimentalAlgorithm = await openFeatureClient.value(
+ for: "cartExperimentalClearing",
+ defaultingTo: false
+ )
+
+ if useExperimentalAlgorithm {
+ logger.info("Using experimental algorithm to clear cart.")
+ throw RPCError(code: .unimplemented, message: "Experimental cart clearing algorithm not yet implemented.")
+ }
+
+ let emptyCartBytes: [UInt8] = try Oteldemo_Cart().serializedBytes()
+ try await valkeyClient.hset(ValkeyKey(request.userID), data: [.init(field: "cart", value: emptyCartBytes)])
+ try await valkeyClient.expire(ValkeyKey(request.userID), seconds: 60 * 60)
+
+ return Oteldemo_Empty()
+ }
+
+ private func cart(userID: String) async throws -> Oteldemo_Cart? {
+ try await withSpan("Cart") { span in
+ span.attributes["user.id"] = userID
+
+ guard let buffer = try await valkeyClient.hget(ValkeyKey(userID), field: "cart") else {
+ return nil
+ }
+ return try Oteldemo_Cart(serializedBytes: Array(buffer.readableBytesView))
+ }
+ }
+}
diff --git a/src/cart/Sources/CTL/GRPCMetricsInterceptor.swift b/src/cart/Sources/CTL/GRPCMetricsInterceptor.swift
new file mode 100644
index 0000000000..76d23b9f3b
--- /dev/null
+++ b/src/cart/Sources/CTL/GRPCMetricsInterceptor.swift
@@ -0,0 +1,66 @@
+import Dispatch
+import GRPCCore
+import Metrics
+
+struct GRPCMetricsInterceptor: ServerInterceptor {
+ let serverHostname: String
+ let networkTransportMethod: String
+
+ func intercept(
+ request: GRPCCore.StreamingServerRequest,
+ context: GRPCCore.ServerContext,
+ next: @Sendable (
+ GRPCCore.StreamingServerRequest,
+ GRPCCore.ServerContext
+ ) async throws -> GRPCCore.StreamingServerResponse