I am using CocoaLumberjack for a Swift project. I would like to implement custom log levels/flags, as I would like to use 6 rather than the default 5, and would prefer different names.
The documentation for doing this is not helpful. It is only a solution for Objective-C.
The fact that DDLogFlag is defined as NS_OPTIONS means I actually could simply ignore the pre-defined values here, create my own constants, and just write some wrapping code to convert from one to the other.
However, DDLogLevel is defined as NS_ENUM, which means Swift won't be very happy with me trying to instantiate something to say 0b111111, which isn't an existing value in the enum. If it were an NS_OPTIONS, like DDLogFlag, I could just ignore the pre-existing definitions from the library and use whatever valid UInt values I wanted to.
As far as I can tell, I just have to write some Objective-C code to define my own replacements for DDLogLevel, DDLogFlag, and write a custom function to pass this in to and access these properties on DDLogMessage. But this feels bad.
How can I use my own custom logging levels in Swift with CocoaLumberjack?
This is indeed only possible in Objective-C right now - and there also only for the #define Log Macros. Even then I could imagine that the "modern" ObjC compiler will warn about the types that are passed to DDLogMessage.
The docs are indeed a bit outdated here and stem from a time where Objective-C was closer to C that it is to Swift nowadays... :-)
Nevertheless, in the end DDLogLevel and DDLogFlag are both stored as NSUInteger. Which means it can theoretically take any NSUInteger value (aka UInt in Swift).
To define your own levels, you would simply create an enum MyLogLevel: UInt { /*...*/ } and then write your own logging functions.
Those functions can actually forward to the existing functions:
extension DDLogFlag {
public static let fatal = DDLogFlag(rawValue: 0x0001)
public static let failure = DDLogFlag(rawValue: 0x0010)
}
public enum MyLogLevel: UInt {
case fatal = 0x0001
case failure = 0x0011
}
extension MyLogLevel {
public static var defaultLevel: MyLogLevel = .fatal
}
#inlinable
public func LogFatal(_ message: #autoclosure () -> Any,
level: MyLogLevel = .defaultLevel,
context: Int = 0,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line,
tag: Any? = nil,
asynchronous async: Bool = asyncLoggingEnabled,
ddlog: DDLog = .sharedInstance) {
_DDLogMessage(message(), level: unsafeBitCast(level, to: DDLogLevel.self), flag: .fatal, context: context, file: file, function: function, line: line, tag: tag, asynchronous: async, ddlog: ddlog)
}
The unsafeBitCast here works, because in the end it's just an UInt and _DDLogMessage does not switch over the level, but instead does a bit mask check against the flag.
Disclaimer: I'm a CocoaLumberjack maintainer myself.
We don't recommend using a custom log level in Swift. There's not much benefit from it and logging frameworks like swift-log also use predefined log levels.
However, I personally could also imagine declaring DDLogLevel with NS_OPTIONS instead of NS_ENUM. The OSLog Swift overlay also uses an extensible OSLogType.
If this is something you'd like to see, please open a PR so we can discuss it with the team. We need to be a bit careful with API compatibility, but like I said it's totally doable.
On a side-note: May I ask what you need custom levels for?
I'm trying to log function arguments into os_log like this:
func foo(x: String, y: [String:String]) {
//...
os_log("foo: \(x) \(y.description)", log: OSLog.default, type: .debug)
}
But getting error:
Cannot convert value of type 'String' to expected argument type 'StaticString'
So how can I log function arguments, or any other dynamic data?
See Logging:
Formatting Log Messages
To format a log message, use a standard NSString or printf format string, ...
and String Format Specifiers for the standard format string specifiers, such as %# and %d.
In your case:
os_log("foo: %# %#", log: .default, type: .debug, x, y.description)
The format string is restricted to static strings to prevent
(unintentional) expansion of format string specifiers. Here is an example demonstrating the
problem, using NSLog() because that does not restrict the format
to constant strings:
let s = "50%"
NSLog("\(s)percent")
// Output: 500x0ercent
The %p expects a pointer on the variable argument list, which is
not provided. This is undefined behavior, it can lead to crashes
or unexpected output.
In Xcode 12 / Swift 5.3 / iOS 14, you don't have to call os_log directly at all. Instead, replace your use of the OSLog class with the new Logger class (available when you import os). Here's an example:
let myLog = Logger(subsystem: "testing", category: "exploring")
You can then call a method directly on your Logger object to log with that subsystem and category:
myLog.log("logging at \(#function)")
To log at a level other than the default, use that level as the name of the method:
myLog.debug("logging at \(#function)")
In the message string, as you can see, Swift string interpolation is legal. It is allowed for Int, Double, Objective-C objects with a description, and Swift objects that conform to CustomStringConvertible.
The legality of Swift string interpolation here is surprising, because the point of os_log format specifiers is to postpone evaluation of the arguments, pushing it out of your app (so that your app is not slowed down by logging) and into the logging mechanism itself. Well, surprise! Thanks to the custom Swift string interpolation hooks that were introduced in Swift 5, the interpolation is postponed.
And the use of custom string interpolation has two further benefits here. First, the custom string interpolation mechanism allows an interpolation to be accompanied by additional parameters specifying its behavior. That's how you prevent a value from being redacted:
myLog.log("logging at \(#function, privacy: .public)")
You can also use additional parameters to perform various sorts of string formatting that you would otherwise have had to perform using NSLog format specifiers, such as dictating the number of digits after the decimal point and other sorts of padding and alignment:
myLog.log("the number is \(i, format: .decimal(minDigits: 5))") // e.g. 00001
So you'll never need to call os_log directly again, and you won't have to use the NSLog-type format specifiers any more.
OLD ANSWER FOR iOS 13 AND BEFORE:
Two points expanding on Martin R's answer:
os_log("foo: %# %#", log: .default, type: .debug, x, y.description)
You can omit the type: parameter, but you cannot omit the log: parameter; you must have it, including the log: label, or os_log will misinterpret your intentions.
Also, the log: value does not have to be .default. It is usual to create one or more OSLog objects up front, to use as the argument to the log: parameter. The advantage here is that you get to specify the Subsystem and Category for the OSLog object, and these in turn permit you to filter on the results, in the Xcode console or the Console application.
Also, with regard to pkamb's answer, if we know our message is always going to be a string, we can write the OSLog extension like this (taking advantage of the new Swift 5.2 callAsFunction method):
extension OSLog {
func callAsFunction(_ s: String) {
os_log("%{public}s", log: self, s)
}
}
The result is that we can now treat our OSLog object myLog itself as a function:
myLog("The main view's bounds are \(self.view.bounds)")
That's nice because it's as simple as a basic print statement. I appreciate that the WWDC 2016 warns against that sort of preformatting, but if it's what you were already doing in a print statement I can't imagine it's all that harmful.
This is my approach:
import Foundation
import os.log
struct Log {
enum LogLevel: String {
case error = "⛔️"
case warning = "⚠️"
case debug = "💬"
}
static func debug(_ info: String, level: LogLevel = .debug, file: String = #file, function: String = #function, line: Int = #line) {
os_log("%# %#:%d %#: %#", type: .default, level.rawValue, (file as NSString).lastPathComponent, line, function, info)
}
static func warning(_ info: String, level: LogLevel = .warning, file: String = #file, function: String = #function, line: Int = #line) {
os_log("%# %#:%d %#: %#", type: .default, level.rawValue, (file as NSString).lastPathComponent, line, function, info)
}
static func error(_ error: NSError, level: LogLevel = .error, file: String = #file, function: String = #function, line: Int = #line) {
os_log("%# %#:%d %#: %#", type: .default, level.rawValue, (file as NSString).lastPathComponent, line, function, "\(error)")
}
}
Usage:
Log.debug("MyLog")
Output example:
💬 AppDelegate.swift:26 application(_:didFinishLaunchingWithOptions:): MyLog
I was getting annoyed at not being able to use "\(variable)" Swift string interpolation in os_log.
I wrote a small extension to get around the issue:
import os.log
extension OSLog {
static func log(_ message: String, log: OSLog = .default, type: OSLogType = .default) {
os_log("%#", log: log, type: type, message)
}
}
This does cause "private" logging, which is expected.
App Name <private>
In Console.app, how can I reveal to what <private> tags are actually referring?
In Apple's WWDC 2016 presentation "Unified Logging and Activity Tracing" they say:
Avoid wrapping os log APIs in other functions.
If you wrap it in another function you then lose our ability to collect the file and line number for you.
If you absolutely have to wrap our APIs, then wrap them in macros and not in functions.
So this is perhaps not the best solution if you are concerned about additional collected information. Although that information still may not be available even using stock os_log: How to find source file and line number from os_log()
A "macro" alternative that allows "\(variable)" substitution would be welcome if anyone want to write it.
The macOS 11 Big Sur Release Notes state that os_log can now be passed Swift string interpolation:
https://developer.apple.com/documentation/macos-release-notes/macos-big-sur-11-beta-release-notes
Logging
New Features
New APIs are available for using os_log from Swift as part of the os framework:
A new type Logger can be instantiated using a subsystem and category and provides methods for logging at different levels ( debug(_:) , error(_:) , fault(_:) ).
The Logger APIs support specifying most formatting and privacy options supported by legacy os_log APIs.
The new APIs provide significant performance improvements over the legacy APIs.
You can now pass Swift string interpolation to the os_log function.
Note: The new APIs can't be back deployed; however, the existing os_log API remains available for back deployment. (22539144)
In order to do logging nicely, I want to know how to convert argument to argument name string In Swift.
I already know Logging Method signature using swift.
I want to know how to log function argument as it is , following its value?
PS: I want to create a Util method helping logging.
func printMore(objName: Any){
print("objName: \(objName)")
}
the print("objName:..., I want the 'objName' injected automatically.
Suck as conditions:
let foo = "Sark"
print("foo: \(foo)")
PPS: It is about Swift runtime. After I alloc some memory, I create some objects, how to get them through runtime instead of hard coding.
Hm, if you called next method inside your function, it shows method declaration with all parameters:
func testFunction(param1: String, param2: String) {
print(#function)
}
testFunction(param1:param2:)
Write C .
U have to release it youself
object.description
Is there a way to get the compile time name of a variable in Swift 2?
I mean the first variable name, which references to a new class instance, if any.
Here is a simple example:
public class Parameter : FloatLiteralConvertible {
var name:String?
var value:Double
// init from float literal
public required init (floatLiteral value: FloatLiteralType) {
self.value = Double(value)
self.name = getLiteralName()
}
func getLiteralName () -> String {
var literalName:String = ""
// do some magic to return the name
return literalName
}
}
let x:Parameter = 2.0
print(x.value) // this returns "2.0"
print(x.name!) // I want this to return "x"
I've already checked similar questions on that topic handling mirroring or objective-c reflections. But in all those cases, one can get only the property names in a class - in the example above name and value.
The same question has been asked in 2014 - Swift: Get Variable Actual Name as String
- and I hope, that since then there is a solution in swift 2.
No, there is no way to do that.
You have to understand that in the compiled state that variable usually does not exist. It can be optimized out or it is represented only as an item on the execution stack.
Even in languages with much better reflection that Swift has, usually you cannot inspect local variables.
To be honest, getting the name of a local variable dynamically has no practical use case.
In Objective-C, we could use the __LINE__ and __PRETTY_FUNCTION__ macros. These are not exposed in the Swift language. Is there another way to get similar information in Swift?
Literal
Type
Value
#file
String
The path to the file in which it appears.
#fileID
String
The name of the file and module in which it appears.
#filePath
String
The path to the file in which it appears.
#line
Int
The line number on which it appears.
#column
Int
The column number in which it begins.
#function
String
The name of the declaration in which it appears.
#dsohandle
UnsafeRawPointer
The dynamic shared object (DSO) handle in use where it appears.
See documentation for more information
Example:
print("Function: \(#function), line: \(#line)")
With default values in parameters you can also create a function:
public func track(_ message: String, file: String = #file, function: String = #function, line: Int = #line ) {
print("\(message) called from \(function) \(file):\(line)")
}
which can be used like this
track("enters app")
The Swift Language Reference defines a few "special literals" that offer this behavior:
Literal
Type
Value
#file
String
The name of the file in which it appears.
#line
Int
The line number on which it appears.
#column
Int
The column number in which it begins.
#function
String
The name of the declaration in which it appears.
You can get just the file name this way
Swift 5
let errorLocation = (#file as NSString).lastPathComponent
print(errorLocation)
or get with separator from last component
let errorLocation = filePath.components(separatedBy: "/").last!
print(errorLocation)
Output
ViewController.swift
If you want to get call-site position of a function, you need to pass filename and line as a default parameters.
Trace.swift
func trace(_ msg: String, file: String = #fileID, line: Int = #line) {
let pos = "\(file.split(on: "/").last!):\(line)"
print("\(pos): \(msg))
}
Foo.swift
func bar() {
trace("baz!")
}
Output
Foo.swift:8: baz!