mirror of
https://github.com/apple/container.git
synced 2026-09-23 08:05:38 +00:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user