Skip to content

Commit 18e4e49

Browse files
committed
ConfigReader-based inits for ProfileRecorderServerConfiguration
1 parent 68286c6 commit 18e4e49

File tree

4 files changed

+491
-16
lines changed

4 files changed

+491
-16
lines changed

[email protected]

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// swift-tools-version:6.1
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "swift-profile-recorder",
8+
platforms: [
9+
// supported
10+
// Linux
11+
.macOS(.v11),
12+
13+
// not supported, listed to make compilation work
14+
.iOS(.v14),
15+
.watchOS(.v7),
16+
.tvOS(.v14),
17+
],
18+
products: [
19+
.library(name: "ProfileRecorder", targets: ["ProfileRecorder"]),
20+
.library(name: "ProfileRecorderServer", targets: ["ProfileRecorderServer"]),
21+
.executable(name: "swipr-sample-conv", targets: ["swipr-sample-conv"]),
22+
// _ProfileRecorderSampleConversion is not part of public API, internal benchmark use
23+
.library(name: "_ProfileRecorderSampleConversion", targets: ["_ProfileRecorderSampleConversion"]),
24+
],
25+
dependencies: [
26+
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
27+
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.0"),
28+
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.1"),
29+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.80.0"),
30+
.package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.24.1"),
31+
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.31.1"),
32+
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.25.2"),
33+
.package(url: "https://github.com/apple/swift-configuration.git", .upToNextMinor(from: "0.2.0")),
34+
],
35+
targets: [
36+
// MARK: - Executables
37+
.executableTarget(
38+
name: "swipr-mini-demo",
39+
dependencies: [
40+
"ProfileRecorder",
41+
"ProfileRecorderServer",
42+
"ProfileRecorderHelpers",
43+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
44+
.product(name: "NIO", package: "swift-nio"),
45+
.product(name: "Logging", package: "swift-log"),
46+
]
47+
),
48+
.target(
49+
name: "_ProfileRecorderSampleConversion",
50+
dependencies: [
51+
"ProfileRecorder",
52+
"CProfileRecorderSwiftELF",
53+
"CProfileRecorderDarwin",
54+
"ProfileRecorderPprofFormat",
55+
"ProfileRecorderHelpers",
56+
.product(name: "NIO", package: "swift-nio"),
57+
.product(name: "NIOFoundationCompat", package: "swift-nio"),
58+
.product(name: "Logging", package: "swift-log"),
59+
.product(name: "NIOExtras", package: "swift-nio-extras"),
60+
],
61+
path: "Sources/ProfileRecorderSampleConversion"
62+
),
63+
.executableTarget(
64+
name: "swipr-sample-conv",
65+
dependencies: [
66+
"CProfileRecorderSwiftELF",
67+
"_ProfileRecorderSampleConversion",
68+
"ProfileRecorderHelpers",
69+
"ProfileRecorder",
70+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
71+
.product(name: "Logging", package: "swift-log"),
72+
]
73+
),
74+
75+
// MARK: - Library targets
76+
.target(
77+
name: "ProfileRecorder",
78+
dependencies: [
79+
"ProfileRecorderHelpers",
80+
.targetItem(
81+
name: "CProfileRecorderSampler",
82+
// We currently only support Linux but we compile just fine on macOS too.
83+
// Let's be a little conservative and allow-list macOS & Linux.
84+
condition: .when(platforms: [.macOS, .linux])
85+
),
86+
.product(name: "NIO", package: "swift-nio"),
87+
.product(name: "_NIOFileSystem", package: "swift-nio"),
88+
]
89+
),
90+
.target(
91+
name: "ProfileRecorderHelpers",
92+
dependencies: [
93+
.product(name: "NIO", package: "swift-nio"),
94+
.product(name: "_NIOFileSystem", package: "swift-nio"),
95+
]
96+
),
97+
.target(
98+
name: "ProfileRecorderPprofFormat",
99+
dependencies: [
100+
"ProfileRecorderHelpers",
101+
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
102+
]
103+
),
104+
.target(
105+
name: "ProfileRecorderServer",
106+
dependencies: [
107+
"ProfileRecorderHelpers",
108+
.product(name: "NIO", package: "swift-nio"),
109+
.product(name: "NIOFoundationCompat", package: "swift-nio"),
110+
.product(name: "NIOHTTP1", package: "swift-nio"),
111+
.product(name: "_NIOFileSystem", package: "swift-nio"),
112+
.product(name: "Logging", package: "swift-log"),
113+
.product(name: "Configuration", package: "swift-configuration"),
114+
"ProfileRecorder",
115+
"_ProfileRecorderSampleConversion",
116+
"ProfileRecorderPprofFormat",
117+
]
118+
),
119+
.target(
120+
name: "CProfileRecorderSwiftELF",
121+
dependencies: []
122+
),
123+
.target(
124+
name: "CProfileRecorderDarwin",
125+
dependencies: []
126+
),
127+
.target(
128+
name: "CProfileRecorderSampler",
129+
dependencies: []
130+
),
131+
132+
// MARK: - Tests
133+
.testTarget(
134+
name: "ProfileRecorderTests",
135+
dependencies: [
136+
"ProfileRecorder",
137+
"_ProfileRecorderSampleConversion",
138+
"ProfileRecorderHelpers",
139+
.product(name: "Atomics", package: "swift-atomics"),
140+
.product(name: "NIO", package: "swift-nio"),
141+
.product(name: "Logging", package: "swift-log"),
142+
.product(name: "_NIOFileSystem", package: "swift-nio"),
143+
]
144+
),
145+
.testTarget(
146+
name: "ProfileRecorderServerTests",
147+
dependencies: [
148+
"ProfileRecorder",
149+
"ProfileRecorderServer",
150+
"_ProfileRecorderSampleConversion",
151+
"ProfileRecorderHelpers",
152+
.product(name: "Atomics", package: "swift-atomics"),
153+
.product(name: "NIO", package: "swift-nio"),
154+
.product(name: "Logging", package: "swift-log"),
155+
.product(name: "_NIOFileSystem", package: "swift-nio"),
156+
.product(name: "AsyncHTTPClient", package: "async-http-client"),
157+
]
158+
),
159+
.testTarget(
160+
name: "ProfileRecorderSampleConversionTests",
161+
dependencies: [
162+
"ProfileRecorder",
163+
"_ProfileRecorderSampleConversion",
164+
"ProfileRecorderHelpers",
165+
.product(name: "Atomics", package: "swift-atomics"),
166+
.product(name: "NIO", package: "swift-nio"),
167+
.product(name: "Logging", package: "swift-log"),
168+
.product(name: "_NIOFileSystem", package: "swift-nio"),
169+
]
170+
),
171+
],
172+
cxxLanguageStandard: .cxx14
173+
)
174+
175+
for target in package.targets {
176+
var settings = target.swiftSettings ?? []
177+
settings.append(.enableExperimentalFeature("StrictConcurrency=complete"))
178+
target.swiftSettings = settings
179+
}

Sources/ProfileRecorderServer/Server.swift

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Foundation
2222
import NIOFoundationCompat
2323
import NIOConcurrencyHelpers
2424
import Logging
25+
import Configuration
2526

2627
typealias ProfileRecorderServerRouteHandler = _ProfileRecorderServerRouteHandler
2728

@@ -111,26 +112,93 @@ public struct ProfileRecorderServerConfiguration: Sendable {
111112
)
112113
}
113114

114-
/// Returns the configuration parsed from environment variables.
115+
/// Parses `ProfileRecorderServerConfiguration` from environment variables.
115116
///
116-
/// Checks for the environment variables `PROFILE_RECORDER_SERVER_URL` for a URL with a socket and port,
117-
/// or `PROFILE_RECORDER_SERVER_URL_PATTERN` to provide a UNIX domain socket over which to read the samples.
118-
public static func parseFromEnvironment() async throws -> Self {
119-
let serverURLString: String
120-
121-
if let string = ProcessInfo.processInfo.environment["PROFILE_RECORDER_SERVER_URL"], !string.isEmpty {
122-
serverURLString = string
123-
} else if let string = ProcessInfo.processInfo.environment["PROFILE_RECORDER_SERVER_URL_PATTERN"],
124-
!string.isEmpty
125-
{
126-
serverURLString =
127-
string
117+
/// ## Environment variables
118+
/// - `PROFILE_RECORDER_SERVER_URL`
119+
/// A direct URL selecting the bind target. Supported schemes:
120+
/// - `unix:///path.sock`
121+
/// - `http+unix://%2Fpath.sock` (percent-encoded path)
122+
/// - `http://host:port`
123+
///
124+
/// - `PROFILE_RECORDER_SERVER_URL_PATTERN`
125+
/// Same as above, but may contain `{PID}` or `{UUID}` which are expanded at runtime.
126+
///
127+
/// The direct URL key takes precedence over the pattern key.
128+
/// If neither key is provided, the default configuration (no bind target) is returned.
129+
/// The event loop group is always set to the shared singleton group.
130+
///
131+
/// - Throws: Errors from `URL` parsing or socket address creation.
132+
/// - Returns: The profile recorder server configuration.
133+
public static func parseFromEnvironment() throws -> Self {
134+
try Self._parseFromEnvironment(ProcessInfo.processInfo.environment)
135+
}
136+
137+
package static func _parseFromEnvironment(_ env: [String: String]) throws -> Self {
138+
if let direct = env["PROFILE_RECORDER_SERVER_URL"] {
139+
return try Self.parseBindTarget(from: direct, pattern: false)
140+
}
141+
if let pattern = env["PROFILE_RECORDER_SERVER_URL_PATTERN"] {
142+
return try Self.parseBindTarget(from: pattern, pattern: true)
143+
}
144+
return .default
145+
}
146+
147+
#if swift(>=6.1)
148+
149+
/// Parses `ProfileRecorderServerConfiguration` from external configuration data via `ConfigReader`s.
150+
///
151+
/// ## Optional configuration keys
152+
/// - `profile.recorder.server.url`
153+
/// A direct URL selecting the bind target. Supports:
154+
/// - `unix:///path.sock`
155+
/// - `http+unix://%2Fpath.sock` (percent-encoded path)
156+
/// - `http://host:port`
157+
///
158+
/// - `profile.recorder.server.url.pattern`
159+
/// Same as above, but the value may contain `{PID}` or `{UUID}`,
160+
/// which are expanded at runtime.
161+
///
162+
/// The direct URL key takes precedence over the pattern key.
163+
/// If neither key is provided, the default configuration (no bind target) is returned.
164+
/// The event loop group is always set to the shared singleton group.
165+
///
166+
/// - Parameters:
167+
/// - configReader: The configuration reader.
168+
/// - Throws: Errors from `URL` parsing or socket address creation.
169+
/// - Returns: The profile recorder server configuration.
170+
@available(macOS 15, *)
171+
public static func parseFromConfig(_ configReader: ConfigReader) throws -> Self {
172+
if let directURL = configReader.string(forKey: "profile.recorder.server.url") {
173+
return try Self.parseBindTarget(from: directURL, pattern: false)
174+
}
175+
176+
if let patternURL = configReader.string(forKey: "profile.recorder.server.url.pattern"){
177+
return try Self.parseBindTarget(from: patternURL, pattern: true)
178+
}
179+
180+
return .default
181+
}
182+
183+
#endif
184+
185+
/// Internal helper to parse and construct the bind target.
186+
///
187+
/// Expands `{PID}` and `{UUID}` placeholders when `pattern == true`.
188+
package static func parseBindTarget(from serverURLInput: String?, pattern: Bool) throws -> Self {
189+
guard let serverURLInput, !serverURLInput.isEmpty else {
190+
return .default
191+
}
192+
193+
let serverURLString =
194+
pattern
195+
? serverURLInput
128196
.replacingOccurrences(of: "{PID}", with: "\(getpid())")
129197
.replacingOccurrences(of: "{UUID}", with: "\(UUID().uuidString)")
130-
} else {
131-
return Self(group: .singleton, bindTarget: nil, unixDomainSocketPath: nil)
132-
}
198+
: serverURLInput
199+
133200
let serverURL = URL(string: serverURLString)
201+
134202
let bindTarget: SocketAddress
135203
switch serverURL?.scheme {
136204
case "http":

0 commit comments

Comments
 (0)