-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathFileLogger.swift
More file actions
177 lines (147 loc) · 6.11 KB
/
FileLogger.swift
File metadata and controls
177 lines (147 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import Foundation
import System
public final class FileLoggingLocation {
public static let path = {
FilePath(stringLiteral: NSHomeDirectory())
.appending("Library")
.appending("Logs")
.appending("GitHubCopilot")
}()
}
final class FileLogger {
private let timestampFormat = Date.ISO8601FormatStyle.iso8601
.year()
.month()
.day()
.timeZone(separator: .omitted).time(includingFractionalSeconds: true)
private let pid = "\(ProcessInfo.processInfo.processIdentifier)"
private static let implementation = FileLoggerImplementation()
private func timestamp() -> String {
return Date().formatted(timestampFormat)
}
public func log(level: LogLevel, category: String, message: String) {
let log = "[\(timestamp())] [\(level)] [\(category)] [\(pid)] \(message)\(message.hasSuffix("\n") ? "" : "\n")"
Task {
await FileLogger.implementation.logToFile(log)
}
}
}
actor FileLoggerImplementation {
#if DEBUG
private let logBaseName = "github-copilot-for-xcode-dev"
#else
private let logBaseName = "github-copilot-for-xcode"
#endif
private let logExtension = "log"
private let maxLogSize = 5_000_000
private let logOverflowLimit = 5_000_000 * 2
private let maxLogs = 10
private let maxLockTime = 3_600 // 1 hour
private let logDir: FilePath
private let logName: String
private let lockFilePath: FilePath
private var logStream: OutputStream?
private var logHandle: FileHandle?
public init() {
logDir = FileLoggingLocation.path
logName = "\(logBaseName).\(logExtension)"
lockFilePath = logDir.appending(logName + ".lock")
}
public func logToFile(_ log: String) {
if let stream = logAppender() {
let data = [UInt8](log.utf8)
stream.write(data, maxLength: data.count)
}
}
private func logAppender() -> OutputStream? {
if logStream == nil {
reopenLogFile()
}
if rotateIfNeeded() > logOverflowLimit {
return nil // do not exceed the overflow limit
}
return logStream
}
private func reopenLogFile() {
if !FileManager.default.fileExists(atPath: logDir.string) {
let success: ()? = try? FileManager.default.createDirectory(atPath: logDir.string, withIntermediateDirectories: true)
guard success != nil else { return }
}
let fileName = logDir.appending(logName).string
logStream = OutputStream(toFileAtPath: fileName, append: true)
logStream?.open()
logHandle = FileHandle(forReadingAtPath: fileName)
}
private func logSize() -> UInt64{
return logHandle?.seekToEndOfFile() ?? 0
}
/// @returns The resulting size of the log file
private func rotateIfNeeded() -> UInt64 {
let size = logSize()
if size > maxLogSize {
rotateLogs()
return logSize() // return the new size of the log file
}
return size
}
private func rotateLogs() {
// attempt to acquire a lock for rotating logs
let fd = try? FileDescriptor.open(
lockFilePath,
.readWrite,
options: .init([.create, .exclusiveCreate]),
permissions: .init(rawValue: 0o666)
)
guard fd != nil else {
// if we can't get the lock, another process is already rotating
checkLockValidity() // prevents stale locks
return // write to the existing log while rotation is happening
}
defer {
try? fd?.close()
try? FileManager.default.removeItem(atPath: lockFilePath.string)
}
// check the log size again. if it's under the limit, another process already rotated the logs
let fileName = logDir.appending(logName).string
let attributes = try? FileManager.default.attributesOfItem(atPath: fileName)
let size = (attributes?[FileAttributeKey.size] ?? 0) as! Int
if (size > maxLogSize) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMddHHmmss"
let archiveName = "\(logBaseName)-\(formatter.string(from: Date())).\(logExtension)"
let newName = logDir.appending(archiveName).string
// moving the log file does not affect any open file handles. they continue writing to the new location.
try? FileManager.default.moveItem(atPath: fileName, toPath: newName)
cleanupOldLogs()
}
reopenLogFile()
}
/// Note: This is only safe to call if the caller has already obtained a lock on the log directory
private func cleanupOldLogs() {
let logFiles = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: logDir.string), includingPropertiesForKeys: nil)
.filter { $0.pathExtension == logExtension && $0.lastPathComponent != logName }
.sorted { $0.lastPathComponent > $1.lastPathComponent }
if let oldLogFiles = logFiles, oldLogFiles.count > maxLogs {
for fileURL in oldLogFiles[maxLogs...] {
try? FileManager.default.removeItem(at: fileURL)
}
}
}
/// Checks the lock file's creation time and removes it if it is stale.
///
/// If a process hangs or crashes while rotating logs, the lock file will
/// be left behind, preventing other processes from rotating logs. To
/// prevent this, an lock file older than the lock limit (1 hour) is
/// considered stale and removed.
///
/// The pending log entry will still be written to the existing log, but
/// by removing the lock file, rotation will resume the next time an entry
/// is logged.
private func checkLockValidity() {
let attributes = try? FileManager.default.attributesOfItem(atPath: lockFilePath.string)
let ctime = (attributes?[FileAttributeKey.creationDate] ?? NSDate()) as! NSDate
if ctime.timeIntervalSinceNow < -TimeInterval(maxLockTime) {
try? FileManager.default.removeItem(atPath: lockFilePath.string)
}
}
}