On this page

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:

  1. The built-in default logger, unless the build selects PUBNUB_PROVIDER_LOGGER=none.
  2. The logger field of pubnub_config_t, if you set it to a non-NULL provider.

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:

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;
LevelValuePurpose
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() or pubnub_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 lines

Returns

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 by pubnub/log.h)
  • Types: the PUBNUB_LOG_LEVEL_*_VALUE integer constants, PUBNUB_LOG_LEVEL_ALL (0x1F)
  • Feature flag: -DPUBNUB_CFG_LOG_LEVEL_COMPILED=<mask> at compile time, default PUBNUB_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:

  1. 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 literal PUBNUB_LOG_LEVEL_* constant, the normal case, the compiler constant-folds this check. If the level's bit is not in PUBNUB_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.
  2. 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 lines

Returns

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_t and 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 self back 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 entry passed to log(), 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 lines

This 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:

  1. The built-in default logger (if the build didn't select PUBNUB_PROVIDER_LOGGER=none)
  2. cfg.logger, if you set it
  3. 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

TargetRecommended 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.