Logging for C SDK
This page explains how to configure logging in the PubNub C Software Development Kit (SDK): the log-level enum, runtime and compile-time filtering, the structured log-entry format, and how to author a custom logger provider. For the general logging philosophy shared across all PubNub SDKs, refer to Logging practices.
Logging architecture
Every context dispatches log entries through a small fan-out component that holds up to PUBNUB_CFG_MAX_LOGGERS logger providers (4 by default). At context creation, the SDK populates this list with:
- The built-in default logger, unless the build selects
PUBNUB_PROVIDER_LOGGER=none. - The
loggerfield ofpubnub_config_t, if you set it to a non-NULLprovider.
In the common case, both slots are occupied: a compiled-in default logger and a configured cfg.logger. You can then register up to PUBNUB_CFG_MAX_LOGGERS - 2 additional providers at runtime with pubnub_logger_add(). If the build sets PUBNUB_PROVIDER_LOGGER=none or you leave cfg.logger NULL, that slot is free and more providers fit before you hit the limit. Every registered provider receives every log entry that passes the runtime level check, in the order the providers were added. See Multiple loggers and fan-out order.
Each log entry is a small stack-allocated struct. It carries a severity level, a type discriminator (text, structured object, error, network request, or network response), the source file and line that emitted it, and an identifier for the emitting context. There is no heap allocation anywhere in the logging path.
Two independent mechanisms control which entries actually produce output:
- Compile-time stripping removes call sites entirely from the binary for levels you don't compile in. See Compile-time log level stripping.
- Runtime filtering drops entries below a per-context threshold that you can change while the program runs. See Change the log level at runtime.
Log levels
pubnub_log_level_t, declared in pubnub/providers/logger_types.h, defines five active severities plus a sentinel that disables logging entirely:
typedef enum pubnub_log_level {
PUBNUB_LOG_LEVEL_TRACE = 0x01, /* Verbose trace events. */
PUBNUB_LOG_LEVEL_DEBUG = 0x02, /* Debugging messages. */
PUBNUB_LOG_LEVEL_INFO = 0x04, /* Informational messages. */
PUBNUB_LOG_LEVEL_WARNING = 0x08, /* Recoverable warnings. */
PUBNUB_LOG_LEVEL_ERROR = 0x10, /* Error conditions. */
PUBNUB_LOG_LEVEL_NONE = 0x00 /* Disable all logging. */
} pubnub_log_level_t;
| Level | Value | Purpose |
|---|---|---|
PUBNUB_LOG_LEVEL_TRACE | 0x01 | Verbose trace events, including full network request and response bodies |
PUBNUB_LOG_LEVEL_DEBUG | 0x02 | Debugging messages |
PUBNUB_LOG_LEVEL_INFO | 0x04 | Informational messages, such as successful initialization |
PUBNUB_LOG_LEVEL_WARNING | 0x08 | Recoverable warnings |
PUBNUB_LOG_LEVEL_ERROR | 0x10 | Error conditions |
PUBNUB_LOG_LEVEL_NONE | 0x00 | Disables all logging. Not a severity, only usable as a threshold or compiled mask |
The values are powers of two so they combine into a bitmask for compile-time stripping (PUBNUB_CFG_LOG_LEVEL_COMPILED), but they are also monotonically increasing (TRACE < DEBUG < INFO < WARNING < ERROR), which a compile-time static assertion in the header enforces. This dual role matters: the bitmask form lets a build strip an arbitrary combination of levels (for example, INFO, WARNING, and ERROR but not TRACE or DEBUG), while the ordering lets pubnub_set_log_level() and a provider's set_level() callback use simple threshold comparisons at runtime.
Network logs expose full request and response bodies at TRACE
At PUBNUB_LOG_LEVEL_TRACE, the SDK's own network-request and network-response log entries carry complete HTTP headers and bodies, including authentication tokens, signed-request query parameters, and message content. Never leave TRACE enabled in a production build, and treat any log output captured at TRACE as sensitive.
Change the log level at runtime
pubnub_set_log_level(), declared in pubnub/client.h, sets a per-context minimum severity threshold. Entries below this threshold are dropped by the fan-out component before any provider's log() callback runs.
- Header:
#include <pubnub/client.h> - Types:
pubnub_context_t,pubnub_log_level_t,pubnub_res_t - Prerequisite: an initialized context (
pubnub_create()orpubnub_init()) - Blocking: no, updates in-memory state only
pubnub_res_t pubnub_set_log_level(pubnub_context_t* ctx, unsigned int level);
pubnub_log_level_t pubnub_logger_log_level(pubnub_context_t* ctx);
pubnub_config_t has a log_level field of type pubnub_log_level_t. When you zero-initialize the config struct (= {0}) it defaults to PUBNUB_LOG_LEVEL_NONE (silent). pubnub_config_defaults() sets it to PUBNUB_LOG_LEVEL_INFO. Set cfg.log_level before calling pubnub_create() or pubnub_init() to control the threshold before any SDK-internal log messages are emitted. After init, change the threshold with pubnub_set_log_level(). Query the current threshold with pubnub_logger_log_level(), which returns PUBNUB_LOG_LEVEL_NONE for a NULL or uninitialized context.
Sample code
#include <pubnub/pubnub.h>
int main(void)
{
pubnub_config_t cfg = pubnub_config_defaults();
cfg.subscribe_key = "demo";
cfg.publish_key = "demo";
cfg.user_id = "my-user-id";
pubnub_context_t* ctx = pubnub_create(&cfg);
if (NULL == ctx) {
return -1;
}
/* Raise verbosity for troubleshooting. */
show all 24 linesReturns
pubnub_set_log_level() returns PUBNUB_OK on success. pubnub_logger_log_level() returns the context's current pubnub_log_level_t threshold.
Compile-time log level stripping
PUBNUB_CFG_LOG_LEVEL_COMPILED, declared in pubnub/providers/logger_types.h, is a compile-time bitmask of the levels present in the binary at all. If a level's bit is absent from this mask, its call sites are eliminated entirely: no entry struct is constructed, no function pointer is invoked, and the associated strings and value trees are never referenced in the compiled output.
- Header:
#include <pubnub/providers/logger_types.h>(pulled in automatically bypubnub/log.h) - Types: the
PUBNUB_LOG_LEVEL_*_VALUEinteger constants,PUBNUB_LOG_LEVEL_ALL(0x1F) - Feature flag:
-DPUBNUB_CFG_LOG_LEVEL_COMPILED=<mask>at compile time, defaultPUBNUB_LOG_LEVEL_ALL
Two independent gates apply to every call to pubnub_log_text(), pubnub_log_object(), pubnub_log_error(), or the PUBNUB_LOG_TEXT/PUBNUB_LOG_OBJECT/PUBNUB_LOG_ERR macros, evaluated in this order:
- Compile-time gate. The macros guard entry construction and dispatch behind
PUBNUB_LOG_LEVEL_ENABLED(lvl), a preprocessor-visible boolean expression. When you pass a literalPUBNUB_LOG_LEVEL_*constant, the normal case, the compiler constant-folds this check. If the level's bit is not inPUBNUB_CFG_LOG_LEVEL_COMPILED, the entire block is dead code and the compiler removes it: no entry struct, no call. This is what makes a stripped call free at runtime, not just cheap. - Runtime gate. Only reached once a call survives gate 1. The fan-out component compares the entry's level against the context's current threshold, set by
pubnub_set_log_level(), and drops the entry if it falls below that threshold, or if no providers are registered. This gate is separate from compile-time stripping: a level that is compiled in but below the runtime threshold still pays for a runtime-conditional entry-struct construction and a comparison before being dropped.
Use the CMake convenience variable PUBNUB_LOG_MIN_LEVEL (TRACE, DEBUG, INFO, WARNING, ERROR, or NONE) to set PUBNUB_CFG_LOG_LEVEL_COMPILED without computing the mask yourself. It translates to the mask covering that level and every more severe level. Passing NONE compiles out all logging call sites. See Environment setup for how build profiles set these variables.
Two similarly named macros, two different jobs
PUBNUB_LOG_LEVEL_ENABLED(lvl) (declared in pubnub/providers/logger.h) takes an enum value like PUBNUB_LOG_LEVEL_DEBUG. The PUBNUB_LOG_TEXT/_OBJECT/_ERR macros use it internally to guard entry construction. PUBNUB_LOG_ENABLED(LEVEL) (declared in pubnub/providers/logger_types.h) takes the bare level name, DEBUG rather than PUBNUB_LOG_LEVEL_DEBUG, and is the one usable directly in a preprocessor #if directive. Use PUBNUB_LOG_LEVEL_ENABLED in ordinary C code and PUBNUB_LOG_ENABLED when you need an #if.
PUBNUB_LOG_LEVEL_ENABLED() guards the macros' own entry construction, but if you build a pubnub_log_value_t tree yourself before calling pubnub_log_object(), that construction code is not automatically removed. On stack-constrained targets, wrap the construction in #if PUBNUB_LOG_ENABLED(DEBUG) (or the level you're using) so the tree-building code is also stripped.
Emit log entries
pubnub/log.h declares the functions you call to write a log entry through a context's configured loggers. Each is a no-op when ctx is NULL or when the level is compiled out.
- Header:
#include <pubnub/log.h> - Types:
pubnub_context_t,pubnub_log_level_t,pubnub_log_value_t - Prerequisite: an initialized context
- Ownership / lifetime: every string and value-tree argument is borrowed. The SDK does not retain any pointer after the call returns
- Blocking: no, each call dispatches synchronously to every registered provider on the calling thread
void pubnub_log_text(pubnub_context_t* ctx,
pubnub_log_level_t level,
const char* message);
void pubnub_log_object(pubnub_context_t* ctx,
pubnub_log_level_t level,
const char* label,
const pubnub_log_value_t* value);
void pubnub_log_error(pubnub_context_t* ctx,
int error_code,
const char* message,
const pubnub_log_value_t* details);
pubnub_log_error() always stamps the entry at PUBNUB_LOG_LEVEL_ERROR. There is no way to emit an error-type entry at any other severity. When PUBNUB_CFG_MAX_LOG_MESSAGE_SIZE is nonzero (default 512), pubnub/log.h also declares pubnub_log_text_formatted(), a printf-style variant that formats into a stack buffer of that size and truncates silently if the formatted message doesn't fit.
Sample code
#include <pubnub/pubnub.h>
int main(void)
{
pubnub_config_t cfg = pubnub_config_defaults();
cfg.subscribe_key = "demo";
cfg.publish_key = "demo";
cfg.user_id = "my-user-id";
pubnub_context_t* ctx = pubnub_create(&cfg);
if (NULL == ctx) {
return -1;
}
pubnub_log_text(ctx, PUBNUB_LOG_LEVEL_INFO, "Client initialized");
show all 22 linesReturns
Both functions are void. There is no result to check. A NULL context or a compiled-out level silently produces no output.
Structured log values
pubnub_log_value_t, declared in pubnub/providers/logger_types.h, is a stack-allocated tagged union for attaching structured data to a log entry via pubnub_log_object(). Arrays and maps are singly-linked lists of pubnub_log_value_t nodes rather than native containers, which keeps the whole tree heap-free:
typedef struct pubnub_log_value {
pubnub_log_value_type_t type;
union {
int bool_val;
int64_t number_val;
struct { const char* ptr; size_t len; } string_val;
struct { struct pubnub_log_value* head; } array_val;
struct { const char* key; struct pubnub_log_value* value; } map_val;
} data;
struct pubnub_log_value* next;
} pubnub_log_value_t;
Build a tree with the factory functions (pubnub_log_value_string(), pubnub_log_value_number(), pubnub_log_value_bool(), pubnub_log_value_null(), pubnub_log_value_array_init(), pubnub_log_value_map_init()) and the append/prepend helpers (pubnub_log_value_array_append_node(), pubnub_log_value_map_set_entry()), or use the convenience macros PUBNUB_LOG_MAP_SET_STRING/_NUMBER/_BOOL and PUBNUB_LOG_ARRAY_APPEND_STRING/_NUMBER/_BOOL to build one entry at a time. Walk a tree from a custom logger with the read-only accessors pubnub_log_value_type(), pubnub_log_value_get_bool(), pubnub_log_value_get_number(), pubnub_log_value_get_string(), pubnub_log_value_first(), pubnub_log_value_key(), and pubnub_log_value_next(). Every accessor is NULL-safe.
#include <pubnub/providers/logger.h>
static void log_publish_params(pubnub_logger_provider_t* logger, const char* channel)
{
#if PUBNUB_LOG_ENABLED(DEBUG)
pubnub_log_value_t map_ = PUBNUB_LOG_VALUE_NULL_INIT();
map_.type = PUBNUB_LOG_VALUE_MAP;
PUBNUB_LOG_MAP_SET_STRING(map_, channel, channel);
PUBNUB_LOG_OBJECT(logger, PUBNUB_LOG_LEVEL_DEBUG, "params", &map_);
#endif
}
The tree must remain valid only for the duration of the pubnub_log_object() call. A provider's log() callback must not retain any pointer into it after returning. Because the values are borrowed, this pattern only works when the pointers you store in the tree (such as channel above) stay alive for at least that long, which stack-allocated locals and function parameters naturally satisfy.
Default logger
Only a stdout backend ships in this SDK. Building with PUBNUB_PROVIDER_LOGGER=stdout (the default in the full and minimal build profiles) registers it automatically at context creation, alongside anything you set in cfg.logger. Building with PUBNUB_PROVIDER_LOGGER=none (the default in the embedded profile) registers no default logger at all. You must then supply cfg.logger or call pubnub_logger_add() to get any output. See Environment setup for how build profiles set this variable.
When active, the stdout backend writes one line per entry in the form:
2026-08-18T10:15:03.421Z PubNub-1a2b3c4d INFO src/core/client.c:512 Client initialized
ERROR-level entries go to stderr. Every other level goes to stdout. Object entries print the label on its own line followed by an indented walk of the value tree (array elements as - bullets, map entries as key: value). Network request and response entries always print the method and URL. Headers and the full body print only when the active threshold is PUBNUB_LOG_LEVEL_TRACE and the response's content type looks text-like. Otherwise the body is truncated to 256 bytes.
Custom loggers
Implement pubnub_logger_provider_t, declared in pubnub/providers/logger.h, to route log entries anywhere the built-in stdout backend can't reach, such as a file, a network sink, or an embedded target's own console driver:
typedef struct pubnub_logger_provider {
void (*log)(struct pubnub_logger_provider* self,
const pubnub_log_entry_t* entry);
void (*set_level)(struct pubnub_logger_provider* self,
pubnub_log_level_t min_level);
} pubnub_logger_provider_t;
- Header:
#include <pubnub/providers/logger.h> - Types:
pubnub_logger_provider_t,pubnub_log_entry_tand its five subtypes (pubnub_log_entry_text_t,_object_t,_error_t,_net_request_t,_net_response_t) - Prerequisite: none to implement the vtable, an initialized context to register it
- Ownership / lifetime: the provider is a shared object: the SDK never allocates, frees, or otherwise manages its lifecycle. You create it, keep it alive for as long as any context holds a reference to it, and release it yourself. Embed the vtable as the first member of your own struct so you can cast
selfback to your type inside the callbacks. - Thread / callback context: callbacks run synchronously, inline, on whichever thread called the emitting function (or, for the SDK's own internal logging, whichever thread is driving request processing). They are never called from ISR context.
- Buffers: the
entrypassed tolog(), and every string or value tree it points to, is valid only for the duration of the call. Copy anything you need before returning.
log is invoked for every entry that passes the runtime threshold. set_level is optional: leave it NULL if your provider doesn't need to react to threshold changes, since every call site null-checks it before invoking it. Neither the vtable nor its doc comments say whether log() may safely call back into the SDK (for example, publishing on the same context). Treat that as unsupported and avoid it.
Sample code
The following implements a minimal custom logger that copies each entry's level and a short summary into an application-owned ring buffer, dispatching on entry->type the way the shipped stdout backend does:
#include <pubnub/pubnub.h>
#include <pubnub/providers/logger.h>
#include <string.h>
#define RING_CAPACITY 16
typedef struct {
pubnub_logger_provider_t base; /* Must be first member. */
char summaries[RING_CAPACITY][64];
pubnub_log_level_t levels[RING_CAPACITY];
size_t next;
} ring_logger_t;
static void ring_log(pubnub_logger_provider_t* self, const pubnub_log_entry_t* entry)
{
show all 80 linesThis is an illustrative implementation written for this page, not code copied from an SDK example. Build your own provider from the vtable contract above and adapt the dispatch pattern to your sink.
Register and remove a logger
- Header:
#include <pubnub/log.h>
pubnub_res_t pubnub_logger_add(pubnub_context_t* ctx, pubnub_logger_provider_t* logger);
pubnub_res_t pubnub_logger_remove(pubnub_context_t* ctx, pubnub_logger_provider_t* logger);
void pubnub_logger_remove_all(pubnub_context_t* ctx);
pubnub_logger_add() returns PUBNUB_OK on success, PUBNUB_ERR_INVALID_ARGUMENT for a NULL or uninitialized ctx/logger, and PUBNUB_ERR_QUEUE_FULL once the context already holds PUBNUB_CFG_MAX_LOGGERS providers. The built-in default logger and cfg.logger each occupy a slot from the start, so with the default limit of 4, only two further pubnub_logger_add() calls succeed. Raise PUBNUB_CFG_MAX_LOGGERS at compile time if you need more. pubnub_logger_remove() returns PUBNUB_ERR_INVALID_ARGUMENT if the provider isn't currently registered. pubnub_logger_remove_all() is void and safe to call on a NULL context.
The provider pointer you pass is borrowed: it must outlive the context, or you must remove it before destroying the context.
Multiple loggers and fan-out order
When more than one logger is registered, the SDK dispatches every log entry that passes the runtime threshold to each provider's log() callback in turn, synchronously, in the order the providers were registered:
- The built-in default logger (if the build didn't select
PUBNUB_PROVIDER_LOGGER=none) cfg.logger, if you set it- Any providers added later with
pubnub_logger_add(), in call order
Removing a provider shifts the remaining providers to keep the list compact, but does not change their relative order. Every provider that implements set_level has its threshold synchronized to the context's current threshold when it is added, and receives any subsequent pubnub_set_log_level() change automatically. You don't need to call each provider's set_level yourself.
Logging best practices
Choose the compiled level range for your target
| Target | Recommended PUBNUB_LOG_MIN_LEVEL |
|---|---|
| Production embedded firmware | NONE, or WARNING if you need field diagnostics |
| Production server/desktop build | WARNING |
| Staging | INFO |
| Development | DEBUG |
| Deep troubleshooting with PubNub support | TRACE |
On stack- and flash-constrained embedded targets, prefer compile-time stripping over a high runtime threshold: a stripped level costs nothing at all, while a compiled-in level that's merely filtered at runtime still pays for entry construction on every call site.
Protect sensitive data
Never compile in or enable TRACE in a build that handles production traffic. Network log entries at TRACE carry complete request and response headers and bodies, including auth tokens and signed-request query parameters, and this SDK has no equivalent of a content-length cap or key allowlist to redact them.
Keep custom logger callbacks fast and non-reentrant
log() runs synchronously and inline on the caller's thread, so slow work inside it (disk I/O, network calls, blocking locks) delays whatever call emitted the entry. Copy what you need and return quickly. Do not call back into the SDK from inside log().
Size PUBNUB_CFG_MAX_LOGGERS for your use case
The default of 4 reserves two slots for the built-in default logger and cfg.logger, leaving room for two pubnub_logger_add() calls. Raise the value at compile time if you register more than two additional providers. pubnub_logger_add() fails with PUBNUB_ERR_QUEUE_FULL once the limit is reached.
For build-profile and CMake variable details, see Environment setup. For the logger field's place among the rest of pubnub_config_t, see Configuration. For how the SDK reports operation outcomes independent of logging, see Status Events.