Skip to content

Commit 2e8fcec

Browse files
Merge pull request #3 from Brainfinance/2
2 parents 3f48cf9 + 4a69a10 commit 2e8fcec

File tree

2 files changed

+151
-97
lines changed

2 files changed

+151
-97
lines changed

README.md

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A [SwiftLog](https://github.com/apple/swift-log) `LogHandler` that logs GCP Sta
44
For more information on Stackdriver structured logging, see: https://cloud.google.com/logging/docs/structured-logging and [LogEntry](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry)
55

66
## Dependencies
7-
This Stackdriver `LogHandler` has a dependency on [SwiftNIO](https://github.com/apple/swift-nio) which is used to create and save your new log entries in a non-blocking fashion.
7+
This Stackdriver `LogHandler` depends on [SwiftNIO](https://github.com/apple/swift-nio) which is used to create and save your new log entries in a non-blocking fashion.
88

99
## How to install
1010

@@ -19,29 +19,45 @@ In your target's dependencies add `"StackdriverLogging"` e.g. like this:
1919
```
2020

2121
## Bootstrapping
22-
**Check out [bootstrapping Stackdriver logging for a Vapor 4 application](https://gist.github.com/jordanebelanger/4307bf34b4ff256c9c8ec52d94db905b) for a practical example using Vapor 4.**
23-
24-
A factory is used to instantiate `StackdriverLogHandler` instances. Before bootstrapping your `LoggingSystem`, you must first call the `StackdriverLogHandlerFactory.prepare(:)` function with a `StackdriverLoggingConfiguration`, an NIO `NonBlockingFileIO` to write the logs asynchronously and an `EventLoopGroup` used to process new log entries in the background.
25-
26-
You are responsible for gracefully shutting down the NIO dependencies used to prepare the `StackdriverLogHandlerFactory`.
27-
28-
Here's an example of how this works:
29-
```Swift
30-
let config = StackdriverLoggingConfiguration(logFilePath: "var/log/myapp.log", defaultLogLevel: "debug")
31-
32-
let threadPool = NIOThreadPool(numberOfThreads: NonBlockingFileIO.defaultThreadPoolSize)
33-
threadPool.start()
34-
let fileIO = NonBlockingFileIO(threadPool: threadPool)
35-
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: NonBlockingFileIO.defaultThreadPoolSize)
36-
37-
StackdriverLogHandlerFactory.prepare(with: configuration,
38-
fileIO: fileIO,
39-
eventLoopGroup: eventLoopGroup)
40-
22+
A factory is used to instantiate `StackdriverLogHandler` instances. Before bootstrapping your swift-log `LoggingSystem`, you must first call the `StackdriverLogHandler.Factory.prepare(_:_:)` with your logging destination.
23+
The Logging destination can be either the standard output which would be whats expected under a gcp Cloud Run environment or a file of your choice.
24+
You are also responsible for gracefully shutting down the NIO dependencies used internally by the `StackdriverLogHandler.Factory` by calling its shutdown function, preferably in a defer statement right after preparing the factory.
25+
```swift
26+
try StackdriverLogHandler.Factory.prepare(for: .stdout)
27+
defer {
28+
try! StackdriverLogHandler.Factory.syncShutdownGracefully()
29+
}
30+
let logLevel = Logger.Level.info
4131
LoggingSystem.bootstrap { label -> LogHandler in
42-
return StackdriverLogHandlerFactory.make()
32+
var logger = StackdriverLogHandler.Factory.make()
33+
logger.logLevel = logLevel
34+
return logger
4335
}
4436
```
37+
### Vapor 4
38+
Here's a bootstrapping example for a standard Vapor 4 application.
39+
```swift
40+
import App
41+
import Vapor
42+
43+
var env = try Environment.detect()
44+
try StackdriverLogHandler.Factory.prepare(for: .stdout)
45+
defer {
46+
try! StackdriverLogHandler.Factory.syncShutdownGracefully()
47+
}
48+
try LoggingSystem.bootstrap(from: &env) { (logLevel) -> (String) -> LogHandler in
49+
return { label -> LogHandler in
50+
var logger = StackdriverLogHandler.Factory.make()
51+
logger.logLevel = logLevel
52+
return logger
53+
}
54+
}
55+
let app = Application(env)
56+
defer { app.shutdown() }
57+
try configure(app)
58+
try app.run()
59+
```
60+
4561
## Logging JSON values using `Logger.MetadataValue`
4662
To log metadata values as JSON, simply log all JSON values other than `String` as a `Logger.MetadataValue.stringConvertible` and, instead of the usual conversion of your value to a `String` in the log entry, it will keep the original JSON type of your values whenever possible.
4763

@@ -91,8 +107,10 @@ Will log the non pretty-printed representation of:
91107
```
92108

93109
## Stackdriver logging agent + fluentd config
94-
You must use this LogHandler in combination with the Stackdriver logging agent https://cloud.google.com/logging/docs/agent/installation and a matching json format
95-
google-fluentd config (/etc/google-fluentd/config.d/example.conf) to automatically send your JSON logs to Stackdriver.
110+
You should preferably run the agent using the standard output destination `StackdriverLogHandler.Destination.stdout` which will get you up and running automatically under certain gcp environments such as Cloud Run.
111+
112+
If you prefer logging to a file, you can use a file destination `StackdriverLogHandler.Destination.stdout` in combination with the Stackdriver logging agent https://cloud.google.com/logging/docs/agent/installation and a matching json format
113+
google-fluentd config (/etc/google-fluentd/config.d/example.conf) to automatically send your JSON logs to Stackdriver for you.
96114

97115
Here's an example google-fluentd conf file that monitors a json based logfile and send new log entries to Stackdriver:
98116
```
@@ -111,6 +129,3 @@ Here's an example google-fluentd conf file that monitors a json based logfile an
111129
tag exampletag
112130
</source>
113131
```
114-
115-
## Future
116-
A Stackdriver gRPC API based implementation is being considered.

Sources/StackdriverLogging/StackdriverLogHandler.swift

Lines changed: 110 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -3,94 +3,44 @@ import Logging
33
import NIO
44
import NIOConcurrencyHelpers
55

6-
/// A global configuration for the `StackdriverLogHandler`s created by the `StackdriverLogHandlerFactory`.
7-
public struct StackdriverLoggingConfiguration: Codable {
8-
9-
/// The filePath of your Stackdriver logging agent structured JSON logfile.
10-
public var logFilePath: String
11-
12-
/// The default Logger.Level of your factory's loggers.
13-
public var logLevel: Logger.Level
14-
15-
public init(logFilePath: String, defaultLogLevel logLevel: Logger.Level) {
16-
self.logFilePath = logFilePath
17-
self.logLevel = logLevel
18-
}
19-
20-
}
21-
22-
/// A factory enum used to create new instances of `StackdriverLogHandler`.
23-
/// You must first prepare it by calling the `prepare` with the required dependencies.
24-
public enum StackdriverLogHandlerFactory {
25-
public typealias Configuration = StackdriverLoggingConfiguration
26-
27-
private static var initialized = false
28-
private static let lock = Lock()
29-
30-
private static var logger: StackdriverLogHandler!
31-
32-
/// Prepares the factory's internal so that new `LogHandler`s can be made using its `make` function. You are responsible
33-
/// for cleanly shutting down the NIO dependencies passed here, i.e the `NIOThreadPool` used to create the `NonBlockingFileIO`
34-
/// and the `eventLoopGroup`.
35-
/// - Parameters:
36-
/// - configuration: The `LogHandler`s global configuration which include the logfile's filepath and the default `Logger.Level`.
37-
/// - fileIO: An NIO `NonBlockingFileIO`, recommend instantiating it with an `NIOThreadPool` of size `NonBlockingFileIO.defaultThreadPoolSize`
38-
/// - eventLoopGroup: An `EventLoopGroup` used to process new log entries asynchronously. Its Recommended number of threads is
39-
/// is the same as `NonBlockingFileIO.defaultThreadPoolSize`.
40-
public static func prepare(with configuration: Configuration, fileIO: NonBlockingFileIO, eventLoopGroup: EventLoopGroup) throws {
41-
self.logger = try lock.withLock {
42-
assert(initialized == false, "`StackdriverLogHandlerFactory` `prepare` should only be called once.")
43-
defer {
44-
initialized = true
45-
}
46-
47-
let logFileURL = URL(fileURLWithPath: configuration.logFilePath)
48-
let fileHandle = try NIOFileHandle(path: configuration.logFilePath,
49-
mode: .write,
50-
flags: .posix(flags: O_APPEND | O_CREAT, mode: S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH))
51-
52-
var logger = StackdriverLogHandler(logFileURL: logFileURL,
53-
fileHandle: fileHandle,
54-
fileIO: fileIO,
55-
processingEventLoopGroup: eventLoopGroup)
56-
logger.logLevel = configuration.logLevel
57-
return logger
58-
}
59-
}
60-
61-
/// Creates a new `StackdriverLogHandler` instance.
62-
public static func make() -> StackdriverLogHandler {
63-
assert(initialized == true, "You must prepare the `StackdriverLogHandlerFactory` with the `prepare` method before creating new loggers.")
64-
return logger
65-
}
66-
67-
}
68-
696
/// `LogHandler` to log JSON to GCP Stackdriver using a fluentd config and the GCP logging-assistant.
70-
/// Use the `MetadataValue.stringConvertible` case to log non-string JSON values supported by JSONSerializer like NSNull, Bool, Int, Float/Double, NSNumber, etc.
7+
/// Use the `MetadataValue.stringConvertible` case to log non-string JSON values supported by JSONSerializer such as NSNull, Bool, Int, Float/Double, NSNumber, etc.
718
/// The `MetadataValue.stringConvertible` type will also take care of automatically logging `Date` as an iso8601 timestamp and `Data` as a base64
729
/// encoded `String`.
7310
///
7411
/// The log entry format matches https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
7512
///
76-
/// ** Use the `StackdriverLogHandlerFactory` to instantiate new `StackdriverLogHandler` instances.
13+
/// ** Use the `StackdriverLogHandler.Factory` to instantiate new `StackdriverLogHandler` instances.
7714
public struct StackdriverLogHandler: LogHandler {
78-
public typealias Factory = StackdriverLogHandlerFactory
15+
/// A `StackdriverLogHandler` output destination, can be either the standard output or a file.
16+
public enum Destination: CustomStringConvertible {
17+
case file(_ filepath: String)
18+
case stdout
19+
20+
public var description: String {
21+
switch self {
22+
case .stdout:
23+
return "standard output"
24+
case .file(let filePath):
25+
return URL(fileURLWithPath: filePath).description
26+
}
27+
}
28+
}
7929

8030
public var metadata: Logger.Metadata = .init()
8131

8232
public var logLevel: Logger.Level = .info
8333

84-
private let logFileURL: URL
34+
private let destination: Destination
8535

8636
private let fileHandle: NIOFileHandle
8737

8838
private let fileIO: NonBlockingFileIO
8939

9040
private let processingEventLoopGroup: EventLoopGroup
9141

92-
fileprivate init(logFileURL: URL, fileHandle: NIOFileHandle, fileIO: NonBlockingFileIO, processingEventLoopGroup: EventLoopGroup) {
93-
self.logFileURL = logFileURL
42+
fileprivate init(destination: Destination, fileHandle: NIOFileHandle, fileIO: NonBlockingFileIO, processingEventLoopGroup: EventLoopGroup) {
43+
self.destination = destination
9444
self.fileHandle = fileHandle
9545
self.fileIO = fileIO
9646
self.processingEventLoopGroup = processingEventLoopGroup
@@ -105,7 +55,7 @@ public struct StackdriverLogHandler: LogHandler {
10555
}
10656
}
10757

108-
public func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, file: String, function: String, line: UInt) {
58+
public func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, source: String, file: String, function: String, line: UInt) {
10959
let eventLoop = processingEventLoopGroup.next()
11060
eventLoop.execute {
11161
// JSONSerialization and its internal JSONWriter calls seem to leak significant memory, especially when
@@ -139,7 +89,7 @@ public struct StackdriverLogHandler: LogHandler {
13989

14090
self.fileIO.write(fileHandle: self.fileHandle, buffer: buffer, eventLoop: eventLoop)
14191
.whenFailure { error in
142-
print("Failed to write logfile entry at '\(self.logFileURL.path)' with error: '\(error.localizedDescription)'")
92+
print("Failed to write logfile entry to '\(self.destination)' with error: '\(error.localizedDescription)'")
14393
}
14494
} catch {
14595
print("Failed to serialize your log entry metadata to JSON with error: '\(error.localizedDescription)'")
@@ -261,6 +211,95 @@ extension StackdriverLogHandler {
261211

262212
}
263213

214+
extension StackdriverLogHandler {
215+
/// A factory enum used to create new instances of `StackdriverLogHandler`.
216+
/// You must first prepare it by calling the `prepare(_:_:)` function. You must also shutdown the internal dependencies
217+
/// created by this factory and used internally by the `StackdriverLogHandler`s by calling the `syncShutdownGracefully`.
218+
/// This is commonly done in a defer statement after preparing the facotry using the `prepare(_:_:)
219+
public enum Factory {
220+
221+
public enum State {
222+
case initial
223+
case running
224+
case shutdown
225+
}
226+
227+
public private(set) static var state = State.initial
228+
private static let lock = Lock()
229+
private static var eventLoopGroup: MultiThreadedEventLoopGroup?
230+
private static var threadPool: NIOThreadPool?
231+
232+
private static var logger: StackdriverLogHandler!
233+
234+
/// Shuts the `StackdriverLogHandler.Factory` down which will close and shutdown the `NIOThreadPool` and the
235+
/// `MultiThreadedEventLoopGroup` used internally by the `StackdriverLogHandler`s to write log entries.
236+
///
237+
/// A good practice is to call this in a defer statement after preparing the factory using the `prepare(_:_:)` function.
238+
public static func syncShutdownGracefully() throws {
239+
defer {
240+
self.lock.withLockVoid {
241+
self.state = .shutdown
242+
}
243+
}
244+
try self.threadPool?.syncShutdownGracefully()
245+
try self.eventLoopGroup?.syncShutdownGracefully()
246+
}
247+
248+
/// Prepares the factory's internal so that new `LogHandler`s can be made using its `make` function. This will create
249+
/// certain internal dependencies that must be shutdown before your application exits using the `syncShutdownGracefully` function.
250+
///
251+
/// - Parameters:
252+
/// - destination: The destination at which to send the logs to such as the standard output or a file.
253+
/// - numberOfThreads: The number of threads that will be used to process and write new log entries.
254+
public static func prepare(
255+
for destination: StackdriverLogHandler.Destination,
256+
numberOfThreads: Int = NonBlockingFileIO.defaultThreadPoolSize
257+
) throws {
258+
self.logger = try lock.withLock {
259+
assert(state == .initial, "`StackdriverLogHandler.Factory.prepare` should only be called once.")
260+
defer {
261+
self.state = .running
262+
}
263+
264+
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: numberOfThreads)
265+
self.eventLoopGroup = eventLoopGroup
266+
267+
let threadPool = NIOThreadPool(numberOfThreads: numberOfThreads)
268+
threadPool.start()
269+
self.threadPool = threadPool
270+
271+
let fileIO = NonBlockingFileIO(threadPool: threadPool)
272+
273+
let fileHandle: NIOFileHandle
274+
switch destination {
275+
case .stdout:
276+
fileHandle = NIOFileHandle(descriptor: FileHandle.standardOutput.fileDescriptor)
277+
case .file(let filepath):
278+
fileHandle = try NIOFileHandle(
279+
path: filepath,
280+
mode: .write,
281+
flags: .posix(flags: O_APPEND | O_CREAT, mode: S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH)
282+
)
283+
}
284+
285+
return StackdriverLogHandler(
286+
destination: destination,
287+
fileHandle: fileHandle,
288+
fileIO: fileIO,
289+
processingEventLoopGroup: eventLoopGroup
290+
)
291+
}
292+
}
293+
294+
/// Creates a new `StackdriverLogHandler` instance.
295+
public static func make() -> StackdriverLogHandler {
296+
assert(state == .running, "You must prepare the `StackdriverLogHandler.Factory` with the `prepare` method before creating new loggers.")
297+
return logger
298+
}
299+
300+
}
301+
}
302+
264303
// Stackdriver related metadata helpers
265304
extension Logger {
266305
/// Set the metadata for a Stackdriver formatted "LogEntryOperation", i.e used to give a unique tag to all the log entries related to some, potentially long running, operation

0 commit comments

Comments
 (0)