Skip to content

Commit

Permalink
Improve the debugging experience by persisting logs in the file syste…
Browse files Browse the repository at this point in the history
…m for every run (tuist#7261)

* Show log file at the end

* Add a clarifying note

* Remove the dependency between Environment and FileHandle

* Address some comments

* Document how to diagnose with logs

* Address comments

* Clean old logs

* Have the default in a single place

* Address some comments

* Address more comments

* Update docs/docs/en/cli/logging.md

Co-authored-by: Marek Fořt <[email protected]>

* Add support for selective testing for Xcode non-generated projects (tuist#7287)

* Add support for selective testing for Xcode projects not generated with Tuist

* Update XcodeGraph

* Update reference from xctest-dynamic-overlay to swift-issue-reporting in Package.resolved

* Add xcode_project_with_tests to a list of fixtures

* Add log for which test modules are skipped

* Add Tuist.swift for the xcode_project_with_tests fixture

* Override acceptance test case arguments for XcodeBuildCommand

* Add logs for which targets were skippped only when some were

* Show log file at the end

* Document how to diagnose issues on CI

* Add some guidelines around how to do logging

* Address some issues after rebasing

* Rename LogsCleaner to LogsController and extract some logic from TuistCLI into the controller

* Add missing dependency

* Fix issues after rebasing

* Fix linting issue

* Update Sources/TuistKit/Utils/LogsController.swift

Co-authored-by: Marek Fořt <[email protected]>

---------

Co-authored-by: Marek Fořt <[email protected]>
  • Loading branch information
pepicrft and fortmarek authored Feb 11, 2025
1 parent 536dc45 commit 7dbd061
Show file tree
Hide file tree
Showing 20 changed files with 436 additions and 73 deletions.
20 changes: 19 additions & 1 deletion Package.resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "1e4d907dfd7f7c760087efd4410e7861f779556f8a1990ea6836b33ba13a4f54",
"originHash" : "6325b6a4a73e22fb29731011bc3458c5507f0ac03322e2261472256f61c2f448",
"pins" : [
{
"identity" : "aexml",
Expand Down Expand Up @@ -235,6 +235,15 @@
"version" : "1.6.2"
}
},
{
"identity" : "swift-log-file",
"kind" : "remoteSourceControl",
"location" : "https://github.com/crspybits/swift-log-file",
"state" : {
"revision" : "aa94b38bf88c7d9cbc87ceafcdffadaffbc2bffa",
"version" : "0.1.0"
}
},
{
"identity" : "swift-log-oslog",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -325,6 +334,15 @@
"version" : "2.20.0"
}
},
{
"identity" : "xcglogger",
"kind" : "remoteSourceControl",
"location" : "https://github.com/DaveWoodCom/XCGLogger.git",
"state" : {
"revision" : "4def3c1c772ca90ad5e7bfc8ac437c3b0b4276cf",
"version" : "7.1.5"
}
},
{
"identity" : "xcodegraph",
"kind" : "remoteSourceControl",
Expand Down
12 changes: 10 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ let targets: [Target] = [
"Mockable",
"FileSystem",
"Command",
.product(name: "LoggingOSLog", package: "swift-log-oslog"),
.product(name: "FileLogging", package: "swift-log-file"),
.product(name: "ServiceContextModule", package: "swift-service-context"),
],
swiftSettings: [
Expand Down Expand Up @@ -517,8 +519,14 @@ let package = Package(
.package(url: "https://github.com/tuist/Command.git", .upToNextMajor(from: "0.8.0")),
.package(url: "https://github.com/sparkle-project/Sparkle.git", from: "2.6.4"),
.package(url: "https://github.com/apple/swift-collections", .upToNextMajor(from: "1.1.4")),
.package(url: "https://github.com/apple/swift-service-context", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/chrisaljoudi/swift-log-oslog.git", .upToNextMajor(from: "0.2.2")),
.package(
url: "https://github.com/apple/swift-service-context", .upToNextMajor(from: "1.0.0")
),
.package(
url: "https://github.com/chrisaljoudi/swift-log-oslog.git",
.upToNextMajor(from: "0.2.2")
),
.package(url: "https://github.com/crspybits/swift-log-file", .upToNextMajor(from: "0.1.0")),
],
targets: targets
)
4 changes: 3 additions & 1 deletion Sources/TuistKit/Commands/BuildCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public struct BuildOptions: ParsableArguments {
}

/// Command that builds a target from the project in the current directory.
public struct BuildCommand: AsyncParsableCommand {
public struct BuildCommand: AsyncParsableCommand, LogConfigurableCommand {
public init() {}
public static var generatorFactory: GeneratorFactorying = GeneratorFactory()
public static var cacheStorageFactory: CacheStorageFactorying = EmptyCacheStorageFactory()
Expand All @@ -132,6 +132,8 @@ public struct BuildCommand: AsyncParsableCommand {
)
}

var logFilePathDisplayStrategy: LogFilePathDisplayStrategy = .always

@OptionGroup()
var buildOptions: BuildOptions

Expand Down
4 changes: 3 additions & 1 deletion Sources/TuistKit/Commands/TestCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import TuistServer
import TuistSupport

/// Command that tests a target from the project in the current directory.
public struct TestCommand: AsyncParsableCommand {
public struct TestCommand: AsyncParsableCommand, LogConfigurableCommand {
public init() {}

public static var generatorFactory: GeneratorFactorying = GeneratorFactory()
Expand All @@ -20,6 +20,8 @@ public struct TestCommand: AsyncParsableCommand {
)
}

var logFilePathDisplayStrategy: LogFilePathDisplayStrategy = .always

@Argument(
help: "The scheme to be tested. By default it tests all the testable targets of the project in the current directory.",
envKey: .testScheme
Expand Down
26 changes: 22 additions & 4 deletions Sources/TuistKit/Commands/TuistCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public struct TuistCommand: AsyncParsableCommand {
}

public static func main(
logFilePath: AbsolutePath,
_ arguments: [String]? = nil,
parseAsRoot: ((_ arguments: [String]?) throws -> ParsableCommand) = Self.parseAsRoot
) async throws {
Expand Down Expand Up @@ -96,6 +97,8 @@ public struct TuistCommand: AsyncParsableCommand {
let executeCommand: () async throws -> Void
let processedArguments = Array(processArguments(arguments)?.dropFirst() ?? [])
var parsedError: Error?
var logFilePathDisplayStrategy: LogFilePathDisplayStrategy = .onError

do {
if processedArguments.first == ScaffoldCommand.configuration.commandName {
try await ScaffoldCommand.preprocess(processedArguments)
Expand All @@ -105,6 +108,9 @@ public struct TuistCommand: AsyncParsableCommand {
}
let command = try parseAsRoot(processedArguments)
executeCommand = {
logFilePathDisplayStrategy = (command as? LogConfigurableCommand)?
.logFilePathDisplayStrategy ?? logFilePathDisplayStrategy

let trackableCommand = TrackableCommand(
command: command,
commandArguments: processedArguments
Expand All @@ -121,32 +127,44 @@ public struct TuistCommand: AsyncParsableCommand {
}

do {
defer { WarningController.shared.flush() }
try await executeCommand()
outputCompletion(logFilePath: logFilePath, shouldOutputLogFilePath: logFilePathDisplayStrategy == .always)
} catch let error as FatalError {
WarningController.shared.flush()
errorHandler.fatal(error: error)
self.outputCompletion(logFilePath: logFilePath, shouldOutputLogFilePath: true)
_exit(exitCode(for: error).rawValue)
} catch let error as ClientError where error.underlyingError is ServerClientAuthenticationError {
WarningController.shared.flush()
// swiftlint:disable:next force_cast
ServiceContext.current?.logger?.error("\((error.underlyingError as! ServerClientAuthenticationError).description)")
outputCompletion(logFilePath: logFilePath, shouldOutputLogFilePath: true)
_exit(exitCode(for: error).rawValue)
} catch {
WarningController.shared.flush()
if let parsedError {
handleParseError(parsedError)
}

// Exit cleanly
if exitCode(for: error).rawValue == 0 {
exit(withError: error)
} else {
errorHandler.fatal(error: UnhandledError(error: error))
outputCompletion(logFilePath: logFilePath, shouldOutputLogFilePath: true)
_exit(exitCode(for: error).rawValue)
}
}
}

private static func outputCompletion(logFilePath: AbsolutePath, shouldOutputLogFilePath: Bool) {
WarningController.shared.flush()
if shouldOutputLogFilePath {
outputLogFilePath(logFilePath)
}
}

private static func outputLogFilePath(_ logFilePath: AbsolutePath) {
ServiceContext.current?.logger?.info("\nLogs are available at \(logFilePath.pathString)")
}

private static func executeTask(with processedArguments: [String]) async throws {
try await TuistService().run(
arguments: processedArguments,
Expand Down
18 changes: 18 additions & 0 deletions Sources/TuistKit/Utils/LogConfigurableCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
enum LogFilePathDisplayStrategy: Decodable {
/// Only shows the path to the log file on error
case onError

/// Always shows the path to the log file.
case always

/// The log file path is never shown.
case never
}

/// This is a protocol that commands can conform to provide their preferences
/// regarding how the log file path should be shown.
///
/// If a command doesn't conform to this protocol, the default strategy used is "onError"
protocol LogConfigurableCommand {
var logFilePathDisplayStrategy: LogFilePathDisplayStrategy { get }
}
66 changes: 66 additions & 0 deletions Sources/TuistKit/Utils/LogsController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import FileSystem
import Foundation
import Path
import TuistSupport

public struct LogsController {
private let fileSystem: FileSystem

public init(fileSystem: FileSystem = FileSystem()) {
self.fileSystem = fileSystem
}

public func setup(
stateDirectory: AbsolutePath,
action: (@escaping @Sendable (String) -> any LogHandler, AbsolutePath) async throws -> Void
) async throws {
let logFilePath = try await touchLogFile(stateDirectory: stateDirectory)
let machineReadableCommands = [DumpCommand.self]
// swiftformat:disable all
let isCommandMachineReadable =
CommandLine.arguments.count > 1
&& machineReadableCommands.map { $0._commandName }.contains(CommandLine.arguments[1])
// swiftformat:enable all
let loggingConfig =
if isCommandMachineReadable || CommandLine.arguments.contains("--json") {
LoggingConfig(
loggerType: .json,
verbose: ProcessInfo.processInfo.environment[Constants.EnvironmentVariables.verbose] != nil
)
} else {
LoggingConfig.default
}

try await clean(logsDirectory: logFilePath.parentDirectory)

let loggerHandler = try Logger.defaultLoggerHandler(
config: loggingConfig, logFilePath: logFilePath
)

try await action(loggerHandler, logFilePath)
}

private func clean(logsDirectory: AbsolutePath) async throws {
let fiveDaysAgo = Calendar.current.date(byAdding: .day, value: -5, to: Date())!

for logPath in try await fileSystem.glob(directory: logsDirectory, include: ["*"]).collect() {
if let creationDate = try FileManager.default.attributesOfItem(atPath: logPath.pathString)[.creationDate] as? Date,
creationDate < fiveDaysAgo
{
try await fileSystem.remove(logPath)
}
}
}

private func touchLogFile(stateDirectory: AbsolutePath) async throws -> Path.AbsolutePath {
let fileSystem = FileSystem()
let logFilePath = stateDirectory.appending(components: [
"logs", "\(UUID().uuidString).log",
])
if !(try await fileSystem.exists(logFilePath.parentDirectory)) {
try await fileSystem.makeDirectory(at: logFilePath.parentDirectory)
}
try await fileSystem.touch(logFilePath)
return logFilePath
}
}
49 changes: 41 additions & 8 deletions Sources/TuistSupport/Logging/Logger.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import class Foundation.ProcessInfo
import FileLogging
@_exported import Logging
import LoggingOSLog
import Path
import ServiceContextModule

import class Foundation.ProcessInfo

private enum LoggerServiceContextKey: ServiceContextKey {
typealias Value = Logger
}
Expand All @@ -10,7 +14,8 @@ extension ServiceContext {
public var logger: Logger? {
get {
self[LoggerServiceContextKey.self]
} set {
}
set {
self[LoggerServiceContextKey.self] = newValue
}
}
Expand All @@ -35,7 +40,10 @@ public struct LoggingConfig {
}

extension Logger {
public static func defaultLoggerHandler(config: LoggingConfig = .default) -> (String) -> any LogHandler {
public static func defaultLoggerHandler(
config: LoggingConfig = .default,
logFilePath: AbsolutePath
) throws -> @Sendable (String) -> any LogHandler {
let handler: VerboseLogHandler.Type

switch config.loggerType {
Expand All @@ -51,10 +59,35 @@ extension Logger {
return quietLogHandler
}

let fileLogger = try FileLogging(to: logFilePath.url)

let baseLoggers = { (label: String) -> [any LogHandler] in
var loggers: [any LogHandler] = [
FileLogHandler(label: label, fileLogger: fileLogger),
]

// OSLog is not needed in development.
// If we include it, the Xcode console will show duplicated logs, making it harder for contributors to debug the
// execution
// within Xcode.
// When run directly from a terminal, logs are not duplicated.
#if RELEASE
loggers.append(LoggingOSLog(label: label))
#endif
return loggers
}
if config.verbose {
return handler.verbose
return { label in
var loggers = baseLoggers(label)
loggers.append(handler.verbose(label: label))
return MultiplexLogHandler(loggers)
}
} else {
return handler.init
return { label in
var loggers = baseLoggers(label)
loggers.append(handler.init(label: label))
return MultiplexLogHandler(loggers)
}
}
}
}
Expand Down Expand Up @@ -82,8 +115,8 @@ extension LoggingConfig {

// A `VerboseLogHandler` allows for a LogHandler to be initialised with the `debug` logLevel.
protocol VerboseLogHandler: LogHandler {
static func verbose(label: String) -> LogHandler
init(label: String)
@Sendable static func verbose(label: String) -> LogHandler
@Sendable init(label: String)
}

extension DetailedLogHandler: VerboseLogHandler {
Expand All @@ -110,6 +143,6 @@ extension JSONLogHandler: VerboseLogHandler {
}
}

private func quietLogHandler(label: String) -> LogHandler {
@Sendable private func quietLogHandler(label: String) -> LogHandler {
return StandardLogHandler(label: label, logLevel: .notice)
}
Loading

0 comments on commit 7dbd061

Please sign in to comment.