- The Logger
- Writing messages
- Message text composition
- Disabling a Logger
- Severity Levels
- Synchronous and Asynchronous Logging
Logging is done via a Log
instance.
Depending on your application's architecture/complexity, you may have single or multiple logger instances, each with its settings.
The best way to create a log instance is to use the initialization via configuration callback:
let logger = Log {
$0.subsystem = "com.indomio.analytics"
$0.category = "mixpanel"
$0.level = .info
$0.transports = [
ConsoleTransport(...),
SQLiteTransport(...)
]
}
Configuration
object, passed inside the callback param, allows you to configure different aspects of the logger's behavior. For example:
subsystem
andcategory
: used to identify a logger specifying the main package andlevel
sets the minimum severity level accepted by the log (any message with a lower severity received by the logger is automatically discarded).transports
defines one or more destinations for received messages. A transport can save your messages in a local file or a database or send them remotely to a web service like the ELK stacks or Sentry.io.isEnabled
: whenfalse
, any message received by the log is ignored. Especially useful to temporarily disable all the functionalities.isSynchronous
: Identify how the messages must be handled when sent to the logger instance. Typically you want to set it tofalse
in production andtrue
in development.
Sending a message to a logger is pretty simple; append the severity level channel to your logger instance and call write()
function:
logger.error?.write(msg: "Something bad has occurred")
logger.trace?.write(msg: "User tapped buy button for item \(item.id)")
Warning The first message is accepted by the logger, but the second one is ignored because the message's severity level is below the log's set level.
Each event includes message
but also several other properties used to enrich the context of the message (we'll take a look at Scope
later below).
When you write a new message, you can also customize the following fields.
message
: the message of the event (it can be a literal or interpolated message. Take a look here for more info).object
: you can attach an object to the event (it must conform to theSerializableObject
protocol; all simple data types andCodable
conform objects are automatically supported).extra
: you can attach a dictionary of key-value objects to give more context to an event.tags
: tags is another dictionary, but some transport may index these values for search.SentryTransport
, for example, makes tags indexed.
Glider's offer different write()
functions.
For simple messages, you can use the write(msg:object:extra:tags:)
where the only required parameter is the event's message.
Note You should use this method when the creation of the text message is silly and fast. If your message is complex and you think it could take some CPU effort, consider using the
write()
function via closure.
// It generates an info message which includes the details of the operation.
// `extra` fields includes accessory data, while `tags` are indexed values.
logger.info?.write(msg: "User tapped BUY button",
extra: ["qt": quantity, "currency": currency],
tags: ["productId": productId])
Logging a message is easy, but knowing when to add the logic necessary to build a log message and tune it for performance can be a bit tricky. We want to make sure logic is encapsulated and very performant. Glider log level closures allow you to cleanly wrap all the logic to build up the message.
Note Glider works exclusively with logging closures to ensure maximum performance in all situations. Closures defer the execution of all the logic inside the closure until absolutely necessary, including the string evaluation itself.
In cases where the Log
instance is disabled, or channel is nil
(severity of message is below Log
severity), log execution time was reduced by 97% over the traditional log message methods taking a String
parameter.
Additionally, the overhead for creating a closure was measured at 1% over the traditional method making it negligible.
In summary, closures allow Glider to be extremely performant in all situations.
logger.info?.write {
$0.message = "User tapped BUY button"
$0.extra = ["qt": quantity, "currency": currency]
$0.tags = ["productId": productId]
}
This is the best way to write an event, and we suggest using it every time.
Finally there are some situations where you need to create an event in a moment and send it later:
let event = Event(message: "Message #\($0)", extra: ["idx": $0])
// somewhere later
log.info?.write(event: &events)
Messages can be simple literals string or may include data coming from variables read at runtime.
Glider supports privacy and formatting options, allowing to manage of the visibility of values in log messages and how data is presented, like Apple's OSLog.
When you create set a message
for an event, you can specify several attributes for each interpolated value:
privacy
: Because users can have access to log messages that your app generates, use the.private
or.partialHide
privacy options to hide potentially sensitive information. For example, you might use it to hide or mask account information or personal data. By default, all data is visible in debugging, while in production, every variable - when not specified - isprivate
.pad
: value printed consists of an original value that is padded with leading, middle or trailing characters to a specified total length. The padding character can be a space or a specified character. The resulting string appears to be either right-aligned or left-aligned.trunc
: value is truncated to a max length (lead/trail/middle).
Moreover, common data types also support formatting styles.
For example, you can decide how to print Bool
values (true/false
, 1/0
, yes/no
), Double
, Int
, Date
(ISO8601 or custom format) and so on.
Some examples:
// Strings
logger.info?.write(msg: "Hello \(self.user.fullName), user-id:\(self.user.id, privacy: .private), email:\(self.user.email, privacy: .partiallyHide)") // Hello Mark Ross, user-id:<redacted>, email:hello@dan********
// Boolean
log.info?.write(msg: "Value is \(boolValue, format: .numeric)") // Value is 1/0
// Float as currency
let price = 12.555
log.info?.write(msg: "Price is \(price, format: .currency(symbol: "EUR"))") // Price is 12.5€
// Date
let date = Date()
log.info?.write(msg: "Now is \(date, format: .iso8601)") // Now is 2018-09-12T12:11:00Z
let someLongString = "My long string is not enough to represent anything but it will truncate anyway"
log.alert?.write(msg: "Value is \(someLongString, trunc: .middle(length: 20), privacy: .public)")
// Value is …nyway
The Log
class has an isEnabled
property to allow you to completely disable logging. This can be helpful for turning off specific logger objects at the app level or, more commonly, disabling logging in a third-party library.
let logger = Log { ... }
logger.isEnabled = false
// No log messages will get sent to the registered transports
logger.isEnabled = true
// We're back in business...
Any new message received by a logger is encapsulated in a payload called Event
; each event has its own severity, which allows identifying what kind of data is received (is the event an error? or just a notice?).
The severity of all levels is assumed to be numerically ascending from most important (emergency
) to least important (trace
).
Glider uses the RFC-5424 standard with 9 different levels for your message (see this discussion on Swift Forum).
Level | Usage/Description |
---|---|
emergency |
Application/system is unusable. |
alert |
Action must be taken immediately. |
critical |
Logging at this level or higher could have a significant performance cost. The logging system may collect and store enough information such as stack shot etc., that may help in debugging these critical errors. |
error |
Error conditions. |
warning |
Abnormal conditions that do not prevent the program from completing a specific task. These are meant to be persisted (unless the system runs out of storage quota). |
notice |
Conditions that are not error conditions but that may require special handling or that are likely to lead to an error. These messages will be stored by the logging system unless it runs out of the storage quota. |
info |
Informational messages that are not essential for troubleshooting errors. These can be discarded by the logging system, especially if there are resource constraints. |
debug |
Messages are meant to be useful only during development. This is meant to be disabled in the shipping code. |
trace |
Trace messages. |
Logging can greatly affect the runtime performance of your application or library. Glider makes it very easy to log messages synchronously or asynchronously.
You can define this behavior when creating the Configuration
for your Log
instance.
let log = Log {
$0.isSynchronous = false
// ...configure other parameters
}
Synchronous logging is very helpful when you are developing your application or library. The log operation will be completed before executing the next line of code. This can be very useful when stepping through the debugger.
The downside is that this can seriously affect performance if logging on the main thread.
Note Glider automatically set the
isSynchronous
totrue
on#DEBUG
andfalse
in production.
Asynchronous logging should be used for deployment builds of your application or library.
This will offload the logging operations to a separate dispatch queue that will not affect the performance of the main thread. This allows you to still capture logs in the manner that the Logger is configured, yet not affect the performance of the main thread operations.