This Capacitor 6 plugin is an almost complete implementation of the Console
interface for web, iOS and Android.
A demo is available that illustrates the usage of the API.
Installation | Configuration | Usage | API
- Does not attempt to monkey patch the
Console
interface, uses a separateLogger
class. - Set a log level — including
silent
— for the logger. - Completely filter out logs based on the log level.
- Logs are not part of Capacitor’s output, you get your own distinct tag.
- Customize the log labels, including using emoji. 😁
- Send logs to the system log on iOS, so you can view a production app’s logs on any device using
Console.app
. 🎉 - Instantiate multiple loggers.
Logger
class is available for use by native plugins and apps! 🚀
Logger
uses the same log levels as Console
, from lowest to highest: error
, warn
, info
, and debug
. In addition, silent
is used to disable all logging. These are all defined in the LogLevel
enum.
Unlike Console
, each instance of Logger
has its own log level which determines what is logged. You can change the level at any time by setting the .level
property to a LogLevel
or a level name. If the level name is invalid, it is ignored.
For any given log message, there is a log level associated. If the logger’s current log level is less than the log message’s level, the message is ignored. For example, if the current log level is info
and a debug
message is logged, it will be ignored, because info
is a lower level than debug
.
Note: It is a known bug that iOS simulators will not log debug messages to the system log.
If you use console
methods, your logs go through Capacitor and don’t exactly stand out. Hey Capacitor, my app can stand on its own! 😁 With Logger
, you can configure a custom label for each level as well as a tag that identifies the source of the log message.
There are two styles of level labels: emoji and text. The style determines how the label is displayed. By default, the label is displayed as an emoji. The default (boring) labels are:
error: '🔴',
warn: '🟠',
info: '🟢',
debug: '🔎',
You can set the labels to whatever you want using the .labels
property. For example, you could use something with a little more character:
error: '☠️',
warn: '👀',
info: '💬',
debug: '🪰',
Here is a sample of what emoji and text labels look like in a browser console:
And in the Xcode console:
And in macOS Console.app
🎉:
And in Android logcat:
Note that on Android, if the first character of a label is ASCII, the label is automatically surrounded by square brackets.
The Logger
class provided by this plugin is implemented in TypeScript (web), Swift (iOS) and Java (Android), and is exported for use within your own native code.
In your plugin or app project:
pnpm add @aparajita/capacitor-logger
If you only plan to use the logger from TypeScript code (even on native platforms), this is all you need to do.
One of the original motivations for this plugin was to have better logging from my other plugins. If you are a plugin author, or writing native code at the app layer, you can use a native Logger
class in your native code. To use the logger in your plugins, you must add it both to the plugin and to any app that uses your plugin, because the dependency is not transitive.
If you are want to use Logger
in your plugins, there are a few more installation steps to take. In the following examples we will assume your plugin is called MyPlugin
.
- Add a reference to this plugin in
MyPlugin.podspec
.
Pod::Spec.new do |s|
# ...standard stuff
s.dependency 'Capacitor'
# 👇 Add this
s.dependency 'AparajitaCapacitorLogger'
s.swift_version = '5.1'
end
- Add a reference to this plugin in
ios/Podfile
.
# 👇 Add this
def my_pods
pod 'AparajitaCapacitorLogger', :path => '../node_modules/@aparajita/capacitor-logger'
end
target 'Plugin' do
capacitor_pods
# 👇 Add this
my_pods
end
target 'PluginTests' do
capacitor_pods
# 👇 Add this
my_pods
end
Add this plugin as a peer dependency in your plugin’s package.json
.
{
"peerDependencies": {
"@aparajita/capacitor-logger": "^4.0.0"
}
}
- Add a reference to this plugin in your plugin’s
settings.gradle (android)
.
include ':aparajita-capacitor-logger'
project(':aparajita-capacitor-logger').projectDir = new File('../node_modules/@aparajita/capacitor-logger')
- Add a reference to this plugin in your plugin’s
build.gradle (android)
.
dependencies {
implementation ':aparajita-capacitor-logger'
}
The Logger
API is similar across web, Swift and Java. In Swift I have used as many idiomatic features of the language as possible to make coding easier (yeah Java, I’m looking at you).
To use the logger in TypeScript , import it into your project:
import { Logger } from '@aparajita/capacitor-logger'
Then create a logger instance:
// You can also pass the level and labels here if you want.
const logger = new Logger('MyApp')
If you are only using a single logger for the entire app, you will probably want to put this code in a separate file:
// logger.ts
import { Logger } from '@aparajita/capacitor-logger'
const logger = new Logger('MyApp')
export default logger
Then import the logger into your code:
import logger from '@/logger'
logger.info('Hello world!')
In your plugin or app’s Swift code where you want to instantiate the logger, import the logger module:
import AparajitaCapacitorLogger
If you want to use a hard-coded log tag, create a logger instance anywhere you want:
let logger = Logger(withTag: "MyPlugin")
If you are writing a plugin and want to use your plugin’s name as the log tag, you have to split up the declaration and instantiation into two lines, because the plugin is not fully constructed until load()
:
@objc(SplashScreen)
public class SplashScreen: CAPPlugin {
var logger: Logger?
override public func load() {
logger = Logger(withPlugin: self)
}
Then use the logger anywhere you want:
// If you intantiated directly
logger.info("Hello world!")
// If you used an optional
logger?.info("Hello world!")
If you want to use a logger in the application layer and use the application’s display name as its tag, there is a convenience initializer you can use. First declare the logger in your AppDelegate
.
import UIKit
import AparajitaCapacitorLogger
import Capacitor
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var logger: Logger?
If you don’t care about using the Capacitor config file, you can initialize the logger in application(_:didFinishLaunchingWithOptions)
.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
logger = Logger(withAppDelegate: self)
logger?.info("ready to roll!")
return true
}
If you want the Capacitor config file to be used, you have to initialize the logger the first time applicationDidBecomeActive
is called, because config is not available before then.
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
if logger == nil {
logger = Logger(withAppDelegate: self)
logger?.info("ready to roll!")
}
}
Import the logger
module in your plugin’s code:
import com.aparajita.capacitor.logger.*;
If you want to use a hard-coded log tag, create a logger instance anywhere you want:
Logger logger = new Logger("MyPlugin");
If you are writing a plugin and want to use your plugin’s name as the log tag, you have to split up the declaration and instantiation into two lines, because the plugin is not fully constructed until load()
:
@CapacitorPlugin(name = "MyPlugin")
public class MyPluin extends Plugin {
private Logger logger;
@Override
public void load() {
logger = new Logger(this);
}
}
Then use the logger anywhere you want:
logger.info("Hello world!")
If you want to use a logger in the application layer and use the application’s display name as its tag, there is a convenience constructor you can use. First declare the logger in your MainActivity
.
import com.aparajita.capacitor.logger.*;
public class MainActivity extends BridgeActivity {
private Logger logger;
Then construct the logger in load()
. This constructor will fetch the tag from the app’s display name and also read the Capacitor config.
@Override
protected void load() {
super.load();
logger = new Logger(this);
}
Loggers can be configured at runtime through various class methods. For loggers created within native code, you can also configure the initial state by setting values in the Capacitor config file of any app that installs this plugin. The configuration schema is as follows:
const config: CapacitorConfig = {
plugins: {
Logger: {
// Everything below is optional
level: LogLevelName,
labels: {
// You can define only the labels you want to change from the default
error: string,
warn: string,
info: string,
debug: string,
},
useSyslog: boolean, // Only used on iOS
},
},
}
Config values are read in Logger
constructors that are passed a plugin, and are overridden by any config options you pass to the constructor.
The functionality of Logger
is the same on each of the three platforms, but the API is slightly different.
enum LogLevel {
silent,
error,
warn,
info,
debug
}
// This resolves to the type union 'error' | 'warn' | 'info' | 'debug'
type LogLevelName = Exclude<keyof typeof LogLevel, 'silent'>
// Returned by the .labels getter and passed to the setter.
// It's a map of level names to corresponding labels.
type LogLevelMap = { [key in LogLevelName]?: string }
// Options passed to the constructor
interface Options {
level?: LogLevel
labels?: LogLevelMap
useSyslog?: boolean // only used on iOS
}
class Logger {
constructor(tag: string, options?: Options) {}
// Get the current log level for this logger
get level(): LogLevel {}
// Set the current log level for this logger
set level(level: LogLevel | string) {}
// Get the current log level as a string
get levelName(): string {}
// Set the current log level as a string.
// If the name is invalid, it is ignored.
set levelName(level: string) {}
// Returns the LogLevel enum for the given name,
// or undefined if the name is invalid.
getLevelWithName(name: string): LogLevel | undefined {}
// Returns a copy of a map between log level names and labels
get labels(): LogLevelMap {}
// Sets the mapping between log level names and labels.
// Invalid level names or empty labels are ignored.
set labels(labels: LogLevelMap) {}
// Get the current log tag
get tag(): string {}
// Set the current log tag. If `tag` is empty, it is ignored.
set tag(tag: string) {}
// The following methods log a message at the given level.
// This is the main interface you will use to the `Logger` class.
// Unlike `console.<level>`, if the current log level of the logger
// is less than the level of the method, the message is ignored.
// Also unlike `console.<level>`, you can only pass a single parameter.
// The multi-parameter console methods were created in the days
// before we had template strings, so a single string is enough.
// silent() is there to facilitate dynamically calling
// a logging method based on the level name without needing
// to special-case 'silent'.
silent(message: string) {}
error(message: string) {}
warn(message: string) {}
info(message: string) {}
log(message: string) {} // alias for info()
debug(message: string) {}
// Logs a message at the given level. If `level` is
// an invalid level name, the level defaults to info.
logAtLevel(level: LogLevel | string, message: string) {}
// Same as `logAt`, but you can also specify the tag.
logWithTagAtLevel(level: LogLevel | string, tag: string, message: string) {}
// Logs a debug representation of a value at the info level.
dir(item: unknown) {}
// Clears the console on the web, does nothing on native platforms.
clear() {}
// These are the exact equivalents of the console timer methods,
// but are implemented on all platforms.
time(label?: string | undefined) {}
timeLog(label?: string) {}
timeEnd(label?: string) {}
// These methods are passed through to `console` on the web
// and do nothing on native platforms.
count(label?: string | undefined) {}
countReset(label?: string | undefined) {}
group(...label: any[]) {}
groupCollapsed(...label: any[]) {}
groupEnd() {}
table(tabularData: any, properties?: readonly string[] | undefined) {}
// Logs a stack trace at the current point of execution
trace() {}
// Returns whether logging should be routed to the system log on iOS.
get useSyslog(): boolean {}
// Sets whether logging should be routed to the system log on iOS.
set useSyslog(use: boolean) {}
public class Logger {
public enum LogLevel: Int {
case silent = 0
case error
case warn
case info
case debug
// The equivalent of `getLevelWithName()` in TypeScript.
// Returns the LogLevel with the given name, or nil if invalid.
public static subscript(_ str: String) -> LogLevel? {}
}
// Use this to pass options to the Logger initializer
public struct Options {
public var level: LogLevel?
public var labels: [String: String]?
public var useSyslog: Bool?
// Initializer for Options
public init(
level: LogLevel = LogLevel.info,
labels: [String: String] = [:],
useSyslog: Bool = false
) {}
}
// The exact equivalent of `labels` property in TypeScript. Lets you
// get and set level labels as a map of level names to labels.
public var labels: [String: String]
// The current log level
public var level: LogLevel
// The current log tag. When setting, an empty tag is ignored.
public var tag: String
// Whether to log to the system log, defaults to false
public var useSyslog: Bool
// Initialize a logger with the given app delegate using the
// app's bundle display name as the tag and reading config from disk
// (see Configuration above).
public convenience init(
withAppDelegate delegate: UIApplicationDelegate,
options: Options? = nil
) {}
// Initialize a logger with the given plugin, using the plugin's name as the tag,
// and reading config from disk (see Configuration above).
public convenience init(withPlugin plugin: CAPPlugin, options: Options?) {}
// Initialize a logger with the given tag and options.
public convenience init(
withTag tag: String,
options: Options? = nil
) {}
// Level logging methods
public func error(_ message: String) {}
public func warn(_ message: String) {}
public func info(_ message: String) {}
public func log(_ message: String) {} // alias for info()
public func debug(_ message: String) {}
// Generic logging methods
public func log(atLevel level: LogLevel, message: String) {}
public func log(
atLevel: level: LogLevel,
label: String?,
tag: String,
message: String) {}
// Log a debug representation of a value at info level
public func dir(_ value: Any?) {}
// Implementation of the console timer methods
public func time(_ label: String?) {}
public func timeLog(_ label: String?) {}
public func timeEnd(_ label: String?) {}
// Logs a stack trace at the current point of execution on all platforms
public func trace() {}
}
public class Logger {
public enum LogLevel {
silent,
error,
warn,
info,
debug,
}
// Options passed to Logger constructor
public static class Options {
LogLevel level;
Map<String, String> labels;
// Make default options: level = info, labels = null
Options() {}
// Copy constructor
Options(Options other) {}
Options(LogLevel level) {}
Options(Map<String, String> labels) {}
Options(LogLevel level, Map<String, String> labels) {}
}
// The current log level
public LogLevel level;
// Construct a logger with the given activity, using the apps's display
// name as the tag, log level of info, default labels, and reading config
// from disk (see Configuration above).
public Logger(@NonNull BridgeActivity activity) {}
// Same as above, but passing options that will override level and labels
public Logger(@NonNull BridgeActivity activity, Options options) {}
// Construct a logger with the given plugin, using the plugin's name as the tag,
// log level of info, default labels, and reading config from disk
// (see Configuration above).
public Logger(Plugin plugin) {}
// Same as above, but passing options that will override level and labels
public Logger(Plugin plugin, Options options) {}
// Construct a logger with the given tag and default level and labels
public Logger(String tag) {}
// Same as above, but passing options that will override level and labels
public Logger(String tag, Options options) {}
// Return the LogLevel with the given name, or null if invalid
@Nullable
public Object getLevelWithName(String name) {}
// Return the name of the current log level
public String getLevelName() {}
// Set the current level by name. Invalid names are ignored.
public void setLevelName(String name) {}
// Return a map of level names to level labels
public Map<String, String> getLabels() {}
// Set level labels given a map of level names to labels.
// Invalid level names are ignored.
public void setLabels(Map<String, String> labels) {}
// Return the current tag
public String getTag() {}
// Set the tag. An empty string is ignored.
public void setTag(String tag) {}
// Level logging methods
public void error(String message) {}
public void warn(String message) {}
public void info(String message) {}
public void log(String message) {} // alias for info()
public void debug(String message) {}
// Generic logging
public void logAtLevel(LogLevel level, String message) {}
public void logWithTagAtLevel(LogLevel level, String tag, String message) {}
public void logWithTagAtLevel(
LogLevel level,
String label,
String tag,
String message
) {}
// Generic logging. If level is an invalid level name, defaults to info.
public void logAtLevel(String level, String message) {}
public void logWithTagAtLevel(String level, String tag, String message) {}
// Log a debug representation of a value at info level
public void dir(Object value) {}
// Implementation of the console timer methods
public void time() {}
public void time(String label) {}
public void timeLog() {}
public void timeLog(String label) {}
public void timeEnd() {}
public void timeEnd(String label) {}
// Logs a stack trace at the current point of execution on all platforms
public void trace() {}
}