CI observability enhancements. (#1193)

- Adds a a `--log-root` option to `swift system start`, propagating the
value as `CONTAINER_LOG_ROOT` to services for logging to files instead
of the OS log facility. This is not a "production" capability as it
neither merges nor rotates logs.
- Currently we don't collect logs on CI builds, and we don't have
permission to run the `log` command there. The PR adds `--log-root` to
the CI test phase, archives the results, and uploads the archive as an
artifact.
- Use FilePath from swift-system for the log root. Foundation URL is a
bit of a footgun for filesystem paths, so unless we identify a
showstopper, we should incrementally transition to this type everywhere
except where we really need network URLs.
- Output the hostname of the CI runner at the start of the test phase so
we can identify runner-specific issues where they exist.
- Fix formatting for log messages with multiple metadata items, and fix
unstructured messages on instances that weren't found using `grep -r
'log\.' Sources`.
- Adds command reference documentation for `--log-root`.
This commit is contained in:
J Logan
2026-02-19 17:47:41 -08:00
committed by GitHub
parent 3425f04206
commit d29947121f
31 changed files with 628 additions and 117 deletions
@@ -172,6 +172,7 @@ public struct Application: AsyncLoggableCommand {
return try PluginLoader(
appRoot: systemHealth.appRoot,
installRoot: systemHealth.installRoot,
logRoot: systemHealth.logRoot,
pluginDirectories: pluginDirectories,
pluginFactories: pluginFactories,
log: bootstrapLogger
@@ -63,9 +63,13 @@ extension Application {
}
if !allErrors.isEmpty {
let logger = Logger(label: "ImageInspect", factory: { _ in StderrLogHandler() })
for (name, error) in allErrors {
logger.error("\(name): \(error.localizedDescription)")
log.error(
"image inspect failed",
metadata: [
"name": "\(name)",
"error": "\(error.localizedDescription)",
])
}
throw InspectError(succeeded: succeededImages, failed: allErrors)
@@ -89,7 +89,7 @@ extension Application {
}
var failed = [String]()
let logger = log
let _log = log
try await withThrowingTaskGroup(of: NetworkState?.self) { group in
for network in networks {
group.addTask {
@@ -100,7 +100,12 @@ extension Application {
print(network.id)
return nil
} catch {
logger.error("failed to delete network \(network.id): \(error)")
_log.error(
"failed to delete network",
metadata: [
"id": "\(network.id)",
"error": "\(error)",
])
return network
}
}
@@ -55,7 +55,12 @@ extension Application.NetworkCommand {
// Note: This failure may occur due to a race condition between the network/
// container collection above and a container run command that attaches to a
// network listed in the networksToPrune collection.
log.error("failed to prune network", metadata: ["id": "\(network.id)", "error": "\(error)"])
log.error(
"failed to prune network",
metadata: [
"id": "\(network.id)",
"error": "\(error)",
])
}
}
@@ -21,6 +21,7 @@ import ContainerPlugin
import ContainerXPC
import ContainerizationError
import Foundation
import SystemPackage
import TerminalProgress
extension Application {
@@ -42,6 +43,12 @@ extension Application {
transform: { URL(filePath: $0) })
var installRoot = InstallRoot.defaultURL
@Option(
name: .long,
help: "Path to the root directory for log data, using macOS log facility if not set",
transform: { FilePath($0) })
var logRoot: FilePath? = nil
@Flag(
name: .long,
inversion: .prefixedEnableDisable,
@@ -85,7 +92,12 @@ extension Application {
var env = PluginLoader.filterEnvironment()
env[ApplicationRoot.environmentName] = appRoot.path(percentEncoded: false)
env[InstallRoot.environmentName] = installRoot.path(percentEncoded: false)
if let logRoot {
env[LogRoot.environmentName] =
logRoot.isAbsolute
? logRoot.string
: FilePath(FileManager.default.currentDirectoryPath).appending(logRoot.components).string
}
let plist = LaunchPlist(
label: "com.apple.container.apiserver",
arguments: args,
@@ -68,7 +68,7 @@ extension Application.VolumeCommand {
}
var failed = [String]()
let logger = log
let _log = log
try await withThrowingTaskGroup(of: Volume?.self) { group in
for volume in volumes {
group.addTask {
@@ -77,7 +77,12 @@ extension Application.VolumeCommand {
print(volume.id)
return nil
} catch {
logger.error("failed to delete volume \(volume.id): \(error)")
_log.error(
"failed to delete volume",
metadata: [
"id": "\(volume.id)",
"error": "\(error)",
])
return volume
}
}
+101
View File
@@ -0,0 +1,101 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import Logging
import SystemPackage
/// Log handler that appends messages to a file, without any
/// rotation or truncation strategy. Use for development purposes only.
public struct FileLogHandler: LogHandler {
public var logLevel: Logger.Level = .info
public var metadata: Logger.Metadata = [:]
private let label: String
private let category: String
private let fileHandle: FileHandle
public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
get {
self.metadata[metadataKey]
}
set {
self.metadata[metadataKey] = newValue
}
}
/// Create a log handler that appends to the specified file.
///
/// - Parameters:
/// - label: A unique identifier for the application.
/// - category: An identifier for the application subsystem.
/// - path: The log file location. The log handler creates the
/// file and parent directory if needed.
/// - Returns: The log handler.
public init(label: String, category: String, path: FilePath) throws {
self.label = label
self.category = category
let parentPath = path.removingLastComponent()
try FileManager.default.createDirectory(atPath: parentPath.string, withIntermediateDirectories: true)
if !FileManager.default.fileExists(atPath: path.string) {
FileManager.default.createFile(atPath: path.string, contents: nil)
}
guard let handle = FileHandle(forWritingAtPath: path.string) else {
throw FileLogFailure.openFailed
}
self.fileHandle = handle
self.fileHandle.seekToEndOfFile()
}
public func log(
level: Logger.Level,
message: Logger.Message,
metadata: Logger.Metadata?,
source: String,
file: String,
function: String,
line: UInt
) {
let timestampFormatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions.insert(.withFractionalSeconds)
return formatter
}()
let timestamp = timestampFormatter.string(from: Date())
// Merge logger-level metadata with per-message metadata
var effectiveMetadata = self.metadata
if let metadata {
effectiveMetadata.merge(metadata) { _, new in new }
}
let text: String
if !effectiveMetadata.isEmpty {
text = "\(timestamp) [\(level)] \(label) \(category) \(effectiveMetadata.description): \(message)\n"
} else {
text = "\(timestamp) [\(level)] \(label): \(category) \(message)\n"
}
if let data = text.data(using: .utf8) {
fileHandle.write(data)
}
}
/// Failures relating to the log handler.
public enum FileLogFailure: Error {
/// The log handler could not open the log file.
case openFailed
}
}
+74
View File
@@ -0,0 +1,74 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Logging
import SystemPackage
/// Common logging setup for application services.
public struct ServiceLogger {
/// Set up the logging system and create a root logger.
///
/// - Parameters:
/// - label: A unique identifier for the application.
/// - category: An identifier for the application subsystem.
/// - metadata: Metadata to include for all messsages. A message
/// specific value for a duplicate key overrides these values.
/// - debug: Enable debug logging.
/// - logPath: If supplied, create log files under the named
/// directory. Otherwise, log to the OS log facility.
/// - Returns: The root logger.
public static func bootstrap(
label: String = "com.apple.container",
category: String,
metadata: [String: String] = [:],
debug: Bool,
logPath: FilePath?
) -> Logger {
// Select the log handler and bootstrap logging.
LoggingSystem.bootstrap { label in
if let logPath {
if let handler = try? FileLogHandler(
label: label,
category: category,
path: logPath
) {
return handler
}
}
return OSLogHandler(label: label, category: category)
}
// Configure log level and metadata.
var log = Logger(label: label)
if debug {
log.logLevel = .debug
}
for (key, value) in metadata {
log[metadataKey: key] = "\(value)"
}
// Log an error if for some reason FileLogHandler init failed.
if let logPath, log.handler as? OSLogHandler != nil {
log.error(
"unable to initialize FileLogHandler, using OSLogHandler",
metadata: [
"logPath": "\(logPath)"
])
}
return log
}
}
+39
View File
@@ -0,0 +1,39 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import SystemPackage
/// Provides the application data root path.
public struct LogRoot {
private static let envPath = ProcessInfo.processInfo.environment[Self.environmentName].flatMap {
$0.isEmpty ? nil : FilePath($0)
}
/// The environment variable that if set, determines the root directory for log files.
/// Otherwise, the application uses the macOS log facility.
public static let environmentName = "CONTAINER_LOG_ROOT"
/// The path object for the log file root directory
public static let path = envPath.map {
guard !$0.isAbsolute else { return $0 }
return FilePath(FileManager.default.currentDirectoryPath).appending($0.components)
}
/// The pathname to the log file root directory
public static let pathname = path?.string
}
@@ -17,12 +17,15 @@
import ContainerizationOS
import Foundation
import Logging
import SystemPackage
public struct PluginLoader: Sendable {
private let appRoot: URL
private let installRoot: URL
private let logRoot: FilePath?
private let pluginDirectories: [URL]
private let pluginFactories: [PluginFactory]
@@ -39,6 +42,7 @@ public struct PluginLoader: Sendable {
public init(
appRoot: URL,
installRoot: URL,
logRoot: FilePath?,
pluginDirectories: [URL],
pluginFactories: [PluginFactory],
log: Logger? = nil
@@ -48,6 +52,7 @@ public struct PluginLoader: Sendable {
self.pluginResourceRoot = pluginResourceRoot
self.appRoot = appRoot
self.installRoot = installRoot
self.logRoot = logRoot
self.pluginDirectories = pluginDirectories
self.pluginFactories = pluginFactories
self.log = log
@@ -223,6 +228,12 @@ extension PluginLoader {
var env = Self.filterEnvironment()
env[ApplicationRoot.environmentName] = appRoot.path(percentEncoded: false)
env[InstallRoot.environmentName] = installRoot.path(percentEncoded: false)
if let logRoot {
env[LogRoot.environmentName] =
logRoot.isAbsolute
? logRoot.string
: FilePath(FileManager.default.currentDirectoryPath).appending(logRoot.components).string
}
let processedArgs = (args ?? ["start"]) + (debug ? ["--debug"] : [])
let plist = LaunchPlist(
+12 -2
View File
@@ -199,14 +199,24 @@ public struct XPCServer: Sendable {
let response = try await handler(message)
xpc_connection_send_message(connection, response.underlying)
} catch let error as ContainerizationError {
log.error("route handler threw an error", metadata: ["route": "\(route)", "error": "\(error)"])
log.error(
"route handler threw an error",
metadata: [
"route": "\(route)",
"error": "\(error)",
])
Self.replyWithError(
connection: connection,
object: object,
err: error
)
} catch {
log.error("route handler threw an error", metadata: ["route": "\(route)", "error": "\(error)"])
log.error(
"route handler threw an error",
metadata: [
"route": "\(route)",
"error": "\(error)",
])
let message = XPCMessage(object: object)
let reply = message.reply()
@@ -17,6 +17,7 @@
import ArgumentParser
import ContainerAPIClient
import ContainerAPIService
import ContainerLog
import ContainerNetworkService
import ContainerPlugin
import ContainerResource
@@ -24,6 +25,7 @@ import ContainerXPC
import DNSServer
import Foundation
import Logging
import SystemPackage
extension APIServer {
struct Start: AsyncParsableCommand {
@@ -43,9 +45,12 @@ extension APIServer {
var installRoot = InstallRoot.url
var logRoot = LogRoot.path
func run() async throws {
let commandName = Self.configuration.commandName ?? "container-apiserver"
let log = APIServer.setupLogger(debug: debug)
let commandName = APIServer._commandName
let logPath = logRoot.map { $0.appending("\(commandName).log") }
let log = ServiceLogger.bootstrap(category: "APIServer", debug: debug, logPath: logPath)
log.info("starting helper", metadata: ["name": "\(commandName)"])
defer {
log.info("stopping helper", metadata: ["name": "\(commandName)"])
@@ -91,6 +96,7 @@ extension APIServer {
log.info("starting XPC server")
try await server.listen()
}
// start up host table DNS
group.addTask {
let hostsResolver = ContainerDNSHandler(networkService: networkService)
@@ -113,7 +119,12 @@ extension APIServer {
/*
group.addTask {
let localhostResolver = LocalhostDNSHandler(log: log)
try localhostResolver.monitorResolvers()
do {
try localhostResolver.monitorResolvers()
} catch {
log.error("could not initialize resolver monitor", metadata: ["error": "\(error)"])
throw error
}
let nxDomainResolver = NxDomainResolver()
let compositeResolver = CompositeResolver(handlers: [localhostResolver, nxDomainResolver])
@@ -131,7 +142,12 @@ extension APIServer {
*/
}
} catch {
log.error("helper failed", metadata: ["name": "\(commandName)", "error": "\(error)"])
log.error(
"helper failed",
metadata: [
"name": "\(commandName)",
"error": "\(error)",
])
APIServer.exit(withError: error)
}
}
@@ -178,6 +194,7 @@ extension APIServer {
return try PluginLoader(
appRoot: appRoot,
installRoot: installRoot,
logRoot: logRoot,
pluginDirectories: pluginDirectories,
pluginFactories: pluginFactories,
log: log
@@ -209,7 +226,12 @@ extension APIServer {
private func initializeHealthCheckService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) {
log.info("initializing health check service")
let svc = HealthCheckHarness(appRoot: appRoot, installRoot: installRoot, log: log)
let svc = HealthCheckHarness(
appRoot: appRoot,
installRoot: installRoot,
logRoot: logRoot,
log: log
)
routes[XPCRoute.ping] = svc.ping
}
-16
View File
@@ -15,9 +15,7 @@
//===----------------------------------------------------------------------===//
import ArgumentParser
import ContainerLog
import ContainerVersion
import Logging
@main
struct APIServer: AsyncParsableCommand {
@@ -27,18 +25,4 @@ struct APIServer: AsyncParsableCommand {
version: ReleaseVersion.singleLine(appName: "container-apiserver"),
subcommands: [Start.self],
)
static func setupLogger(debug: Bool) -> Logger {
LoggingSystem.bootstrap { label in
OSLogHandler(
label: label,
category: "APIServer"
)
}
var log = Logger(label: "com.apple.container")
if debug {
log.logLevel = .debug
}
return log
}
}
@@ -66,7 +66,12 @@ public class DirectoryWatcher {
let files = try FileManager.default.contentsOfDirectory(atPath: directoryURL.path)
try handler(files.map { directoryURL.appending(path: $0) })
} catch {
self.log.error("failed to run DirectoryWatcher handler", metadata: ["error": "\(error)", "path": "\(directoryURL.path)"])
self.log.error(
"failed to run DirectoryWatcher handler",
metadata: [
"error": "\(error)",
"path": "\(directoryURL.path)",
])
}
}
+10 -16
View File
@@ -54,11 +54,14 @@ extension ImagesHelper {
var installRoot = InstallRoot.url
var logRoot = LogRoot.path
private static let unpackStrategy = SnapshotStore.defaultUnpackStrategy
func run() async throws {
let commandName = ImagesHelper._commandName
let log = setupLogger()
let logPath = logRoot.map { $0.appending("\(commandName).log") }
let log = ServiceLogger.bootstrap(category: "ImagesHelper", debug: debug, logPath: logPath)
log.info("starting helper", metadata: ["name": "\(commandName)"])
defer {
log.info("stopping helper", metadata: ["name": "\(commandName)"])
@@ -77,7 +80,12 @@ extension ImagesHelper {
log.info("starting XPC server")
try await xpc.listen()
} catch {
log.error("helper failed", metadata: ["name": "\(commandName)", "error": "\(error)"])
log.error(
"helper failed",
metadata: [
"name": "\(commandName)",
"error": "\(error)",
])
ImagesHelper.exit(withError: error)
}
}
@@ -114,19 +122,5 @@ extension ImagesHelper {
routes[ImagesServiceXPCRoute.contentIngestCancel.rawValue] = harness.cancelIngestSession
routes[ImagesServiceXPCRoute.contentIngestComplete.rawValue] = harness.completeIngestSession
}
private func setupLogger() -> Logger {
LoggingSystem.bootstrap { label in
OSLogHandler(
label: label,
category: "ImagesHelper"
)
}
var log = Logger(label: "com.apple.container")
if debug {
log.logLevel = .debug
}
return log
}
}
}
@@ -15,8 +15,10 @@
//===----------------------------------------------------------------------===//
import ArgumentParser
import ContainerLog
import ContainerNetworkService
import ContainerNetworkServiceClient
import ContainerPlugin
import ContainerResource
import ContainerXPC
import ContainerizationError
@@ -64,9 +66,12 @@ extension NetworkVmnetHelper {
return .reserved
}()
var logRoot = LogRoot.path
func run() async throws {
let commandName = NetworkVmnetHelper._commandName
let log = setupLogger(id: id, debug: debug)
let logPath = logRoot.map { $0.appending("\(commandName)-\(id).log") }
let log = ServiceLogger.bootstrap(category: "NetworkVmnetHelper", metadata: ["id": "\(id)"], debug: debug, logPath: logPath)
log.info("starting helper", metadata: ["name": "\(commandName)"])
defer {
log.info("stopping helper", metadata: ["name": "\(commandName)"])
@@ -110,7 +115,12 @@ extension NetworkVmnetHelper {
log.info("starting XPC server")
try await xpc.listen()
} catch {
log.error("helper failed", metadata: ["name": "\(commandName)", "error": "\(error)"])
log.error(
"helper failed",
metadata: [
"name": "\(commandName)",
"error": "\(error)",
])
NetworkVmnetHelper.exit(withError: error)
}
}
@@ -15,9 +15,7 @@
//===----------------------------------------------------------------------===//
import ArgumentParser
import ContainerLog
import ContainerVersion
import Logging
@main
struct NetworkVmnetHelper: AsyncParsableCommand {
@@ -29,19 +27,4 @@ struct NetworkVmnetHelper: AsyncParsableCommand {
Start.self
]
)
static func setupLogger(id: String, debug: Bool) -> Logger {
LoggingSystem.bootstrap { label in
OSLogHandler(
label: label,
category: "NetworkVmnetHelper"
)
}
var log = Logger(label: "com.apple.container")
if debug {
log.logLevel = .debug
}
log[metadataKey: "id"] = "\(id)"
return log
}
}
@@ -16,6 +16,7 @@
import ArgumentParser
import ContainerLog
import ContainerPlugin
import ContainerResource
import ContainerSandboxService
import ContainerSandboxServiceClient
@@ -42,13 +43,16 @@ extension RuntimeLinuxHelper {
@Option(name: .shortAndLong, help: "Root directory for the sandbox")
var root: String
var logRoot = LogRoot.path
var machServiceLabel: String {
"\(Self.label).\(uuid)"
}
func run() async throws {
let commandName = RuntimeLinuxHelper._commandName
let log = RuntimeLinuxHelper.setupLogger(debug: debug, metadata: ["uuid": "\(uuid)"])
let logPath = logRoot.map { $0.appending("\(commandName)-\(uuid).log") }
let log = ServiceLogger.bootstrap(category: "RuntimeLinuxHelper", metadata: ["uuid": "\(uuid)"], debug: debug, logPath: logPath)
log.info("starting helper", metadata: ["name": "\(commandName)"])
defer {
log.info("stopping helper", metadata: ["name": "\(commandName)"])
@@ -117,7 +121,12 @@ extension RuntimeLinuxHelper {
_ = try await group.next()
}
} catch {
log.error("helper failed", metadata: ["name": "\(commandName)", "error": "\(error)"])
log.error(
"helper failed",
metadata: [
"name": "\(commandName)",
"error": "\(error)",
])
try? await eventLoopGroup.shutdownGracefully()
RuntimeLinuxHelper.Start.exit(withError: error)
}
@@ -15,10 +15,7 @@
//===----------------------------------------------------------------------===//
import ArgumentParser
import ContainerLog
import ContainerVersion
import Logging
import OSLog
@main
struct RuntimeLinuxHelper: AsyncParsableCommand {
@@ -30,24 +27,4 @@ struct RuntimeLinuxHelper: AsyncParsableCommand {
Start.self
]
)
package static func setupLogger(debug: Bool, metadata: [String: Logging.Logger.Metadata.Value] = [:]) -> Logging.Logger {
LoggingSystem.bootstrap { label in
OSLogHandler(
label: label,
category: "RuntimeLinuxHelper"
)
}
var log = Logger(label: "com.apple.container")
if debug {
log.logLevel = .debug
}
for (key, val) in metadata {
log[metadataKey: key] = val
}
return log
}
}
@@ -17,6 +17,7 @@
import ContainerXPC
import ContainerizationError
import Foundation
import SystemPackage
public enum ClientHealthCheck {
static let serviceIdentifier = "com.apple.container.apiserver"
@@ -37,6 +38,7 @@ extension ClientHealthCheck {
guard let installRootValue = reply.string(key: .installRoot), let installRoot = URL(string: installRootValue) else {
throw ContainerizationError(.internalError, message: "failed to decode installRoot in health check")
}
let logRoot = reply.string(key: .logRoot).map { FilePath($0) }
guard let apiServerVersion = reply.string(key: .apiServerVersion) else {
throw ContainerizationError(.internalError, message: "failed to decode apiServerVersion in health check")
}
@@ -52,6 +54,7 @@ extension ClientHealthCheck {
return .init(
appRoot: appRoot,
installRoot: installRoot,
logRoot: logRoot,
apiServerVersion: apiServerVersion,
apiServerCommit: apiServerCommit,
apiServerBuild: apiServerBuild,
@@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//
import Foundation
import SystemPackage
/// Snapshot of the health of container services and resources
public struct SystemHealth: Sendable, Codable {
@@ -24,6 +25,9 @@ public struct SystemHealth: Sendable, Codable {
/// The full pathname of the application install root.
public let installRoot: URL
/// The full pathname of the application install root.
public let logRoot: FilePath?
/// The release version of the container services.
public let apiServerVersion: String
@@ -61,6 +61,7 @@ public enum XPCKeys: String {
case ping
case appRoot
case installRoot
case logRoot
case apiServerVersion
case apiServerCommit
case apiServerBuild
@@ -119,7 +119,12 @@ public actor ContainersService {
}
} catch {
try? FileManager.default.removeItem(at: dir)
log.warning("failed to load container", metadata: ["path": "\(dir.path)", "error": "\(error)"])
log.warning(
"failed to load container",
metadata: [
"path": "\(dir.path)",
"error": "\(error)",
])
}
}
return results
@@ -546,7 +551,12 @@ public actor ContainersService {
let waitFunc: ExitMonitor.WaitHandler = {
log.info("registering container with exit monitor")
let code = try await client.wait(id)
log.info("container finished in exit monitor", metadata: ["id": "\(id)", "rc": "\(code)"])
log.info(
"container finished in exit monitor",
metadata: [
"id": "\(id)",
"rc": "\(code)",
])
return code
}
@@ -882,7 +892,12 @@ public actor ContainersService {
private func handleContainerExit(id: String, code: ExitStatus?, context: AsyncLock.Context) async throws {
if let code {
self.log.info("handling container exit", metadata: ["id": "\(id)", "rc": "\(code)"])
self.log.info(
"handling container exit",
metadata: [
"id": "\(id)",
"rc": "\(code)",
])
}
var state: ContainerState
@@ -916,7 +931,12 @@ public actor ContainersService {
do {
try await client.shutdown()
} catch {
self.log.error("failed to shutdown sandbox service", metadata: ["id": "\(id)", "error": "\(error)"])
self.log.error(
"failed to shutdown sandbox service",
metadata: [
"id": "\(id)",
"error": "\(error)",
])
}
}
@@ -927,7 +947,12 @@ public actor ContainersService {
try ServiceManager.deregister(fullServiceLabel: label)
self.log.info("deregistered sandbox service", metadata: ["id": "\(id)"])
} catch {
self.log.error("failed to deregister sandbox service", metadata: ["id": "\(id)", "error": "\(error)"])
self.log.error(
"failed to deregister sandbox service",
metadata: [
"id": "\(id)",
"error": "\(error)",
])
}
// Best effort deallocate network attachments for the container. Don't throw on
@@ -999,7 +1024,12 @@ public actor ContainersService {
do {
config = try bundle.configuration
} catch {
self.log.warning("failed to read bundle configuration during cleanup for container", metadata: ["id": "\(id)", "error": "\(error)"])
self.log.warning(
"failed to read bundle configuration during cleanup for container",
metadata: [
"id": "\(id)",
"error": "\(error)",
])
}
// Only try to deregister service if we have a valid config
@@ -1017,7 +1047,12 @@ public actor ContainersService {
do {
try bundle.delete()
} catch {
self.log.warning("failed to delete bundle for container", metadata: ["id": "\(id)", "error": "\(error)"])
self.log.warning(
"failed to delete bundle for container",
metadata: [
"id": "\(id)",
"error": "\(error)",
])
}
self.containers.removeValue(forKey: id)
@@ -21,15 +21,18 @@ import ContainerXPC
import Containerization
import Foundation
import Logging
import SystemPackage
public actor HealthCheckHarness {
private let appRoot: URL
private let installRoot: URL
private let logRoot: FilePath?
private let log: Logger
public init(appRoot: URL, installRoot: URL, log: Logger) {
public init(appRoot: URL, installRoot: URL, logRoot: FilePath?, log: Logger) {
self.appRoot = appRoot
self.installRoot = installRoot
self.logRoot = logRoot
self.log = log
}
@@ -38,6 +41,9 @@ public actor HealthCheckHarness {
let reply = message.reply()
reply.set(key: .appRoot, value: appRoot.absoluteString)
reply.set(key: .installRoot, value: installRoot.absoluteString)
if let logRoot {
reply.set(key: .logRoot, value: logRoot.string)
}
reply.set(key: .apiServerVersion, value: ReleaseVersion.singleLine(appName: "container-apiserver"))
reply.set(key: .apiServerCommit, value: get_git_commit().map { String(cString: $0) } ?? "unspecified")
// Extra optional fields for richer client display
@@ -337,7 +337,13 @@ public actor VolumesService {
try await store.create(volume)
log.info("created volume", metadata: ["name": "\(name)", "driver": "\(driver)", "isAnonymous": "\(volume.isAnonymous)"])
log.info(
"created volume",
metadata: [
"name": "\(name)",
"driver": "\(driver)",
"isAnonymous": "\(volume.isAnonymous)",
])
return volume
}