Logging and Console Messages
Capture library log output and listen for console messages from your pages.
On this page
You can capture the library's internal log messages and listen for console output from your pages while you develop.
Neither stream goes anywhere by itself— your application decides where each one lands. Capturing both streams lets you inspect renderer warnings, track down page script errors, and catch mistakes in your native bindings.
📘 App::Create() installs a logger for you
Apps created with
App::Create()get a file logger unless you install one first. It writesultralight.logto the per-app folder derived fromSettings::developer_nameandapp_name(on Windows the roaming app-data folder, covered in App Lifecycle and Settings)— though page console output still needs a listener.
The Library Log
Implementing and Installing a Logger
To handle log output yourself, subclass Logger and implement LogMessage(). The method receives a LogLevel and the message string. In C, populate a ULLogger struct with a log_message callback that receives a ULLogLevel and a ULString valid only for the duration of the callback.
Pass your instance to Platform::instance().set_logger() (or pass the struct by value to ulPlatformSetLogger() in C) before creating the Renderer. Your application keeps ownership of the C++ logger, and the instance must outlive the Renderer (see Setting Up the Platform).
#include <Ultralight/Ultralight.h>
#include <iostream>
using namespace ultralight;
class MyLogger : public Logger {
public:
///
/// Called whenever the library has something to say (from any thread).
///
void LogMessage(LogLevel log_level, const String& message) override {
const char* prefix = "[info] ";
if (log_level == LogLevel::Fatal)
prefix = "[fatal] ";
else if (log_level == LogLevel::Error)
prefix = "[error] ";
else if (log_level == LogLevel::Warning)
prefix = "[warning] ";
std::cout << prefix << message.utf8().data() << std::endl;
}
};
MyLogger logger;
void InitLogger() {
///
/// Install our logger before creating the Renderer.
/// (Ownership stays with us, so 'logger' must outlive the Renderer.)
///
Platform::instance().set_logger(&logger);
}
#include <Ultralight/CAPI.h>
#include <stdio.h>
///
/// Called whenever the library has something to say (from any thread).
///
static void LogMessage(ULLogLevel log_level, ULString message) {
const char* prefix = "[info] ";
if (log_level == kLogLevel_Fatal)
prefix = "[fatal] ";
else if (log_level == kLogLevel_Error)
prefix = "[error] ";
else if (log_level == kLogLevel_Warning)
prefix = "[warning] ";
printf("%s%s\n", prefix, ulStringGetData(message));
}
void InitLogger(void) {
///
/// Install our logger before creating the Renderer. (In C the logger is
/// a struct of callbacks, so there is no instance to keep alive.)
///
ULLogger logger = { LogMessage };
ulPlatformSetLogger(logger);
}
Log Levels and Tags
The library logs at five levels, from most to least severe:
| Level | Meaning |
|---|---|
Fatal |
The application can't continue (eg, missing ICU data or a required platform handler that wasn't set). |
Error |
A failure the library recovers from (eg, a resource that failed to load). |
Warning |
A problem that isn't a failure (eg, a misused API call). |
Info |
Status information. |
Debug |
Verbose detail. |
The library drops messages below Config::diagnostics.min_log_level before they reach your logger. It defaults to LogLevel::Info, so you'll need to lower it to LogLevel::Debug to see verbose detail (in C, call ulConfigSetMinLogLevel()).
Each message starts with a tag in brackets for the part of the library it came from, such as [net] Failed to load cacert.pem ... or [js] for the JavaScript bridge.
Thread Safety
The library calls LogMessage() (or log_message in C) from any of its threads, and more than one thread can be inside the callback at the same time, so your implementation must serialize its writes.
Stock File Logger
If you want to write log output to a file without implementing your own logger, GetDefaultLogger(path) in <AppCore/Platform.h> returns the library's built-in file logger pointed at the path you provide. In C, ulEnableDefaultLogger(log_path) in <AppCore/CAPI.h> installs the same logger.
This logger displays a native message box for every Fatal message, the level the library uses when it can't continue (every other level goes to the file only). The library owns this instance, so you must never destroy it.
Page Console Output
Console output arrives through the ViewListener interface's OnAddConsoleMessage() callback. Implement the callback and pass your listener instance to View::set_view_listener() (in C, call ulViewSetAddConsoleMessageCallback()). Ownership remains with your application (see Handling View Events for the listener's other callbacks).
Each ConsoleMessage provides details about where the message originated and its severity. In C, ulViewSetAddConsoleMessageCallback() passes the source, level, message, line, column, and source id directly to your callback, but provides no message type or argument values.
| Method | Description |
|---|---|
source() |
Subsystem that produced the message, such as JavaScript, the network, or native API bindings. |
level() |
Severity level (kMessageLevel_Log, Warning, Error, Debug, or Info). |
type() |
Console method behind the call (eg, kMessageType_Table for console.table()). |
message() |
Message string. |
line_number(), column_number() |
Location in the script that produced the message. |
source_id() |
Source URL or identifier of the script. |
Here is a listener implementation that prints incoming messages and gives native API diagnostics a channel of their own:
#include <Ultralight/Ultralight.h>
#include <iostream>
using namespace ultralight;
RefPtr<View> view;
class MyApp : public ViewListener {
public:
///
/// Called for every message the page adds to its console.
///
void OnAddConsoleMessage(View* caller,
const ConsoleMessage& message) override {
///
/// Route diagnostics from our own native APIs to a channel of their own.
///
if (message.source() == kMessageSource_NativeAPI) {
std::cout << "[native api] " << message.message().utf8().data()
<< std::endl;
return;
}
///
/// Print the level, where it came from, and the text for everything else.
///
std::cout << "[" << LevelName(message.level()) << "] "
<< message.source_id().utf8().data() << ":"
<< message.line_number() << " "
<< message.message().utf8().data() << std::endl;
}
private:
static const char* LevelName(MessageLevel level) {
switch (level) {
case kMessageLevel_Error: return "error";
case kMessageLevel_Warning: return "warning";
case kMessageLevel_Debug: return "debug";
case kMessageLevel_Info: return "info";
default: return "log";
}
}
};
MyApp app;
void AttachListener() {
///
/// Hand the View a pointer to our listener.
/// (Ownership stays with us, so 'app' must outlive its use by the View.)
///
view->set_view_listener(&app);
}
#include <Ultralight/CAPI.h>
#include <stdio.h>
static ULView view = NULL;
static const char* LevelName(ULMessageLevel level) {
switch (level) {
case kMessageLevel_Error: return "error";
case kMessageLevel_Warning: return "warning";
case kMessageLevel_Debug: return "debug";
case kMessageLevel_Info: return "info";
default: return "log";
}
}
///
/// Called for every message the page adds to its console.
///
static void OnAddConsoleMessage(void* user_data, ULView caller,
ULMessageSource source, ULMessageLevel level,
ULString message, unsigned int line_number,
unsigned int column_number,
ULString source_id) {
///
/// Route diagnostics from our own native APIs to a channel of their own.
///
if (source == kMessageSource_NativeAPI) {
printf("[native api] %s\n", ulStringGetData(message));
return;
}
///
/// Print the level, where it came from, and the text for everything else.
///
printf("[%s] %s:%u %s\n", LevelName(level), ulStringGetData(source_id),
line_number, ulStringGetData(message));
}
void AttachListener(void) {
///
/// Hand the View our callback (no user data to pass along or destroy).
///
ulViewSetAddConsoleMessageCallback(view, OnAddConsoleMessage, NULL, NULL);
}
Reading console.log() Arguments
When JavaScript calls console.log() with multiple parameters, message() converts only the first argument to a string.
To inspect the remaining arguments in C++, use num_arguments() and argument_at(). Each argument arrives as a raw JSValueRef (indexed from 0), valid within the context returned by argument_context(). You can convert these values using the JavaScriptCore C API (see Using JavaScriptCore Directly). The C console callback does not provide access to these raw arguments.
#include <Ultralight/Ultralight.h>
#include <iostream>
#include <string>
using namespace ultralight;
void PrintArguments(const ConsoleMessage& message) {
JSContextRef ctx = message.argument_context();
for (uint32_t i = 0; i < message.num_arguments(); ++i) {
///
/// Convert each argument to a string with the JavaScriptCore C API.
/// (Anything you Copy you must Release.)
///
JSStringRef text = JSValueToStringCopy(ctx, message.argument_at(i),
nullptr);
if (!text)
continue; // A value that can't convert (eg, a Symbol) yields NULL.
std::string buffer(JSStringGetMaximumUTF8CStringSize(text), '\0');
JSStringGetUTF8CString(text, buffer.data(), buffer.size());
JSStringRelease(text);
std::cout << " arg " << i << ": " << buffer.c_str() << std::endl;
}
}
Capturing Bridge Diagnostics
The JavaScript API, the DOM API, and data bindings report mistakes in how native code and markup use them.
These warnings go to the Logger (tagged [js], [dom], or [data]). When a page is involved, the message also appears in that page's console with source kMessageSource_NativeAPI— this is why the listener example above gives them their own channel.
See Developer Mode and Diagnostics to turn on developer mode, configure diagnostics levels, and review what each API checks.