-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[IOSP-296] [IOSP-297] Logz.io-compatible JSON Logs (#53)
* Migrate to LogzIOLogger * Switch to new LogzIOLogger * Added AppTests/LogsTests * swift test --generate-linuxmain * Cleanup * Fix Linux compilation issue (hopefully) * Fix wrong Logger resolved at runtime * Fix Logger runtime resolution bug for Middleware + Add comment on necessary `print` call * Update Sources/App/Logger/LogzIOLogger.swift Co-Authored-By: David Rodrigues <[email protected]> * Rename LogzIOLogger -> JSONLogger * Rename, pass 2 * Provide minimumLevel comparisons * Provide minimum log level via ENV var * Cleanup * swift test --generate-linuxmain * 🧹 * test update * Defaults minimumLogLevel to info * allLevels -> allPredefinedLevels * Cut dependency to Vapor Logging framework * swift test --generate-linuxmain * Use rawValue for LogLevel <-> String * import * Fix rawValue (uppercase) * uppercase env var to be case insensitive in config * Relative paths in logs * Comment fix Co-authored-by: David Rodrigues <[email protected]>
- Loading branch information
1 parent
036eb7f
commit 12deb87
Showing
16 changed files
with
289 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import Vapor | ||
import Bot | ||
|
||
private let basePath = #file.components(separatedBy: "/").dropLast(4).joined(separator: "/") + "/" | ||
|
||
final class JSONLogger: LoggerProtocol, Service { | ||
private let serializer = JSONEncoder() | ||
let minimumLogLevel: Bot.LogLevel | ||
|
||
init(minimumLogLevel: Bot.LogLevel = .info) { | ||
self.minimumLogLevel = minimumLogLevel | ||
} | ||
|
||
public func log(_ string: String, at level: Bot.LogLevel, | ||
file: String = #file, function: String = #function, line: UInt = #line, column: UInt = #column | ||
) { | ||
guard level >= minimumLogLevel else { return } | ||
let formatted: String | ||
do { | ||
let message = LogMessage( | ||
timestamp: Date(), | ||
message: string, | ||
level: level, | ||
file: file, | ||
function: function, | ||
line: line, | ||
column: column | ||
) | ||
let data = try serializer.encode(message) | ||
formatted = String(data: data, encoding: .utf8)! | ||
} catch { | ||
formatted = "[\(Date())] [\(level.rawValue)] \(string)" | ||
} | ||
|
||
print(formatted) | ||
} | ||
} | ||
|
||
// MARK: Private structure of LogMessage | ||
|
||
extension JSONLogger { | ||
struct LogMessage: Encodable { | ||
let timestamp: Date | ||
let message: String | ||
let level: Bot.LogLevel | ||
let file: String | ||
let function: String | ||
let line: UInt | ||
let column: UInt | ||
|
||
static var dateFormatter: DateFormatter = { | ||
let df = DateFormatter() | ||
df.dateFormat = "yyyy-MM-dd'T'hh:mm:ss.SSSZ" | ||
df.locale = Locale(identifier: "en_US_POSIX") | ||
return df | ||
}() | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case timestamp = "@timestamp" | ||
case message = "message" | ||
case level = "level" | ||
case context = "context" | ||
} | ||
|
||
public func encode(to encoder: Encoder) throws { | ||
var container = encoder.container(keyedBy: CodingKeys.self) | ||
try container.encode(LogMessage.dateFormatter.string(from: self.timestamp), forKey: .timestamp) | ||
try container.encode(self.message, forKey: .message) | ||
try container.encode(self.level.rawValue, forKey: .level) | ||
let relativePathIndex = file.index(file.startIndex, offsetBy: file.hasPrefix(basePath) ? basePath.count : 0) | ||
let context = "\(file[relativePathIndex...]):\(line):\(column) - \(function)" | ||
try container.encode(context, forKey: .context) | ||
} | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,48 @@ | ||
import Foundation | ||
|
||
// MARK: LogLevel | ||
|
||
/// Different log levels | ||
public enum LogLevel: String, Equatable, CaseIterable { | ||
/// For more verbose logs used when debugging in depth. Usually provides a lot of details | ||
case debug = "DEBUG" | ||
/// For informative logs, like state changes | ||
case info = "INFO" | ||
/// For reporting errors and failures | ||
case error = "ERROR" | ||
} | ||
|
||
extension LogLevel: Comparable { | ||
public static func < (lhs: LogLevel, rhs: LogLevel) -> Bool { | ||
let index = LogLevel.allCases.firstIndex(of:) | ||
return index(lhs)! < index(rhs)! | ||
} | ||
} | ||
|
||
|
||
// MARK: LoggerProtocol | ||
|
||
public protocol LoggerProtocol { | ||
func log(_ message: String) | ||
/// Logs an encodable at the provided log level The encodable can be encoded to the required format. | ||
/// The log level indicates the type of log and/or severity | ||
/// | ||
/// Normally, you will use one of the convenience methods (i.e., `verbose(...)`, `info(...)`). | ||
func log(_ string: String, at level: LogLevel, file: String, function: String, line: UInt, column: UInt) | ||
} | ||
|
||
extension LoggerProtocol { | ||
/// Debug logs are used to debug problems | ||
public func debug(_ string: String, file: String = #file, function: String = #function, line: UInt = #line, column: UInt = #column) { | ||
self.log(string, at: .debug, file: file, function: function, line: line, column: column) | ||
} | ||
|
||
/// Info logs are used to indicate a specific infrequent event occurring. | ||
public func info(_ string: String, file: String = #file, function: String = #function, line: UInt = #line, column: UInt = #column) { | ||
self.log(string, at: .info, file: file, function: function, line: line, column: column) | ||
} | ||
|
||
/// Error, indicates something went wrong and a part of the execution was failed. | ||
public func error(_ string: String, file: String = #file, function: String = #function, line: UInt = #line, column: UInt = #column) { | ||
self.log(string, at: .error, file: file, function: function, line: line, column: column) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import XCTest | ||
@testable import App | ||
import Bot | ||
|
||
class JSONLoggerTests: XCTestCase { | ||
func test_json_logs_formatting() throws { | ||
let message = JSONLogger.LogMessage( | ||
timestamp: JSONLoggerTests.fixedDate, | ||
message: JSONLoggerTests.message, | ||
level: .debug, | ||
file: #file, | ||
function: "somefunction", | ||
line: 1337, | ||
column: 42 | ||
) | ||
|
||
let serializer = JSONEncoder() | ||
let data = try serializer.encode(message) | ||
|
||
XCTAssertEqualJSON(data, JSONLoggerTests.cannedLog) | ||
} | ||
|
||
// Check that we print everything if minimum log level is .debug | ||
func test_loglevel_compare_debug_level() { | ||
XCTAssertEqual(LogLevel.debug >= .debug, true) | ||
XCTAssertEqual(LogLevel.info >= .debug, true) | ||
XCTAssertEqual(LogLevel.error >= .debug, true) | ||
} | ||
|
||
// Check that we print anything above but nothing below the .info level | ||
func test_loglevel_compare_info_level() { | ||
XCTAssertEqual(LogLevel.debug >= .info, false) | ||
XCTAssertEqual(LogLevel.info >= .info, true) | ||
XCTAssertEqual(LogLevel.error >= .info, true) | ||
} | ||
|
||
// Check that we print only error logs when minimum level is .error | ||
func test_loglevel_compare_error_level() { | ||
XCTAssertEqual(LogLevel.debug >= .error, false) | ||
XCTAssertEqual(LogLevel.info >= .error, false) | ||
XCTAssertEqual(LogLevel.error >= .error, true) | ||
} | ||
|
||
// MARK: Canned data | ||
|
||
private static let fixedDate: Date = { | ||
return DateComponents( | ||
calendar: Calendar(identifier: .gregorian), | ||
timeZone: TimeZone(secondsFromGMT: 0), | ||
year: 2020, month: 2, day: 1, | ||
hour: 10, minute: 20, second: 30, | ||
nanosecond: 456_000_000 | ||
).date! | ||
}() | ||
|
||
private static let message = """ | ||
Some long log message | ||
For example, one spanning multiple lines | ||
like an HTTP request body for example | ||
""" | ||
|
||
private static let escapedMessage = message | ||
.split(separator: "\n", omittingEmptySubsequences: false) | ||
.joined(separator: "\\n") | ||
|
||
private static let cannedLog = """ | ||
{ | ||
"@timestamp": "2020-02-01T10:20:30.456+0000", | ||
"message": "\(escapedMessage)", | ||
"level": "DEBUG", | ||
"context": "Tests/AppTests/Logger/JSONLoggerTests.swift:1337:42 - somefunction" | ||
} | ||
""".data(using: .utf8)! | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../TestUtils.swift |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
#if !canImport(ObjectiveC) | ||
import XCTest | ||
|
||
extension JSONLoggerTests { | ||
// DO NOT MODIFY: This is autogenerated, use: | ||
// `swift test --generate-linuxmain` | ||
// to regenerate. | ||
static let __allTests__JSONLoggerTests = [ | ||
("test_json_logs_formatting", test_json_logs_formatting), | ||
("test_loglevel_compare_debug_level", test_loglevel_compare_debug_level), | ||
("test_loglevel_compare_error_level", test_loglevel_compare_error_level), | ||
("test_loglevel_compare_info_level", test_loglevel_compare_info_level), | ||
] | ||
} | ||
|
||
public func __allTests() -> [XCTestCaseEntry] { | ||
return [ | ||
testCase(JSONLoggerTests.__allTests__JSONLoggerTests), | ||
] | ||
} | ||
#endif |
Oops, something went wrong.