Current section

Files

Jump to
erlang_python c_src py_logging.c
Raw

c_src/py_logging.c

/*
* Copyright 2026 Benoit Chesneau
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @file py_logging.c
* @brief NIF-based logging and distributed tracing for Python-Erlang integration
* @author Benoit Chesneau
*
* @ingroup logging
*
* This module provides fire-and-forget logging and tracing from Python to Erlang:
*
* @par Logging
* - Python's `logging` module integrated with Erlang's `logger`
* - Fire-and-forget: `erlang._log()` never blocks Python execution
* - Level filtering at NIF level to skip unnecessary message creation
*
* @par Tracing
* - Span-based distributed tracing from Python
* - Context propagation via thread-local storage
* - Events within spans for milestones
*
* @par Architecture
* ```
* Python logging.info()
* |
* v
* ErlangHandler.emit()
* |
* v
* erlang._log() ----NIF----> enif_send() ----> py_logger (gen_server)
* | |
* | (returns immediately) logger:log()
* v
* continue execution
* ```
*
* @note This file is included from py_nif.c (single compilation unit)
*/
/* ============================================================================
* Global state for logging and tracing
* ============================================================================ */
/** @brief PID of the Erlang process receiving log messages */
ErlNifPid g_log_receiver_pid;
/** @brief Flag: log receiver is registered */
volatile bool g_has_log_receiver = false;
/** @brief Minimum log level threshold (Python levelno) */
volatile int g_log_level_threshold = 0;
/** @brief PID of the Erlang process receiving trace spans */
ErlNifPid g_trace_receiver_pid;
/** @brief Flag: trace receiver is registered */
volatile bool g_has_trace_receiver = false;
/* ============================================================================
* Level mapping
* ============================================================================ */
/**
* @brief Convert Python log level (levelno) to Erlang logger level atom
*
* Python levels:
* DEBUG = 10
* INFO = 20
* WARNING = 30
* ERROR = 40
* CRITICAL = 50
*
* @param env NIF environment for atom creation
* @param level Python levelno
* @return Erlang atom: debug | info | warning | error | critical
*/
static ERL_NIF_TERM level_to_atom(ErlNifEnv *env, int level) {
if (level <= 10) return enif_make_atom(env, "debug");
if (level <= 20) return enif_make_atom(env, "info");
if (level <= 30) return enif_make_atom(env, "warning");
if (level <= 40) return enif_make_atom(env, "error");
return enif_make_atom(env, "critical");
}
/* ============================================================================
* Python-callable functions
* ============================================================================ */
/**
* @brief Python: erlang._log(level, logger_name, message, metadata)
*
* Fire-and-forget log message to Erlang. Never blocks Python.
* If no receiver is registered or level is below threshold, returns immediately.
*
* @param self Module reference (unused)
* @param args Tuple: (level:int, logger_name:str, message:str, metadata:dict)
* @return None
*/
static PyObject *erlang_log_impl(PyObject *self, PyObject *args) {
(void)self;
/* Quick exit if no receiver */
if (!g_has_log_receiver) {
Py_RETURN_NONE;
}
int level;
const char *logger_name;
Py_ssize_t logger_name_len;
const char *message;
Py_ssize_t message_len;
PyObject *metadata;
if (!PyArg_ParseTuple(args, "is#s#O",
&level, &logger_name, &logger_name_len, &message, &message_len, &metadata)) {
return NULL;
}
/* Level filtering */
if (level < g_log_level_threshold) {
Py_RETURN_NONE;
}
/* Create message environment (will be freed after send) */
ErlNifEnv *msg_env = enif_alloc_env();
if (msg_env == NULL) {
/* Non-fatal: just skip this log message */
Py_RETURN_NONE;
}
/* Build the message: {py_log, Level, LoggerName, Message, Metadata, Timestamp} */
ERL_NIF_TERM level_term = level_to_atom(msg_env, level);
ERL_NIF_TERM logger_term;
unsigned char *logger_buf = enif_make_new_binary(msg_env, logger_name_len, &logger_term);
if (logger_buf != NULL) {
memcpy(logger_buf, logger_name, logger_name_len);
}
ERL_NIF_TERM message_term;
unsigned char *msg_buf = enif_make_new_binary(msg_env, message_len, &message_term);
if (msg_buf != NULL) {
memcpy(msg_buf, message, message_len);
}
/* Convert metadata dict to Erlang map */
ERL_NIF_TERM meta_term = py_to_term(msg_env, metadata);
/* Timestamp in microseconds */
uint64_t ts = get_monotonic_ns() / 1000;
ERL_NIF_TERM ts_term = enif_make_uint64(msg_env, ts);
ERL_NIF_TERM msg = enif_make_tuple6(msg_env,
ATOM_PY_LOG,
level_term,
logger_term,
message_term,
meta_term,
ts_term);
/* Fire-and-forget send */
enif_send(NULL, &g_log_receiver_pid, msg_env, msg);
enif_free_env(msg_env);
Py_RETURN_NONE;
}
/**
* @brief Python: erlang._trace_start(name, span_id, parent_id, attrs)
*
* Signal start of a trace span to Erlang.
*
* @param self Module reference (unused)
* @param args Tuple: (name:str, span_id:int, parent_id:int|None, attrs:dict)
* @return None
*/
static PyObject *erlang_trace_start_impl(PyObject *self, PyObject *args) {
(void)self;
if (!g_has_trace_receiver) {
Py_RETURN_NONE;
}
const char *name;
Py_ssize_t name_len;
unsigned long long span_id;
PyObject *parent_id_obj;
PyObject *attrs;
if (!PyArg_ParseTuple(args, "s#KOO",
&name, &name_len, &span_id, &parent_id_obj, &attrs)) {
return NULL;
}
ErlNifEnv *msg_env = enif_alloc_env();
if (msg_env == NULL) {
Py_RETURN_NONE;
}
ERL_NIF_TERM span_id_term = enif_make_uint64(msg_env, span_id);
ERL_NIF_TERM parent_id_term;
if (parent_id_obj == Py_None) {
parent_id_term = enif_make_atom(msg_env, "undefined");
} else {
unsigned long long parent_id = PyLong_AsUnsignedLongLong(parent_id_obj);
parent_id_term = enif_make_uint64(msg_env, parent_id);
}
ERL_NIF_TERM name_term;
unsigned char *name_buf = enif_make_new_binary(msg_env, name_len, &name_term);
if (name_buf != NULL) {
memcpy(name_buf, name, name_len);
}
ERL_NIF_TERM attrs_term = py_to_term(msg_env, attrs);
uint64_t ts = get_monotonic_ns() / 1000;
ERL_NIF_TERM ts_term = enif_make_uint64(msg_env, ts);
/* {span_start, SpanId, ParentId, Name, Attrs, StartTime} */
ERL_NIF_TERM msg = enif_make_tuple6(msg_env,
ATOM_SPAN_START,
span_id_term,
parent_id_term,
name_term,
attrs_term,
ts_term);
enif_send(NULL, &g_trace_receiver_pid, msg_env, msg);
enif_free_env(msg_env);
Py_RETURN_NONE;
}
/**
* @brief Python: erlang._trace_end(span_id, status, attrs)
*
* Signal end of a trace span to Erlang.
*
* @param self Module reference (unused)
* @param args Tuple: (span_id:int, status:str, attrs:dict)
* @return None
*/
static PyObject *erlang_trace_end_impl(PyObject *self, PyObject *args) {
(void)self;
if (!g_has_trace_receiver) {
Py_RETURN_NONE;
}
unsigned long long span_id;
const char *status;
PyObject *attrs;
if (!PyArg_ParseTuple(args, "KsO", &span_id, &status, &attrs)) {
return NULL;
}
ErlNifEnv *msg_env = enif_alloc_env();
if (msg_env == NULL) {
Py_RETURN_NONE;
}
ERL_NIF_TERM span_id_term = enif_make_uint64(msg_env, span_id);
ERL_NIF_TERM status_term = enif_make_atom(msg_env, status);
ERL_NIF_TERM attrs_term = py_to_term(msg_env, attrs);
uint64_t ts = get_monotonic_ns() / 1000;
ERL_NIF_TERM ts_term = enif_make_uint64(msg_env, ts);
/* {span_end, SpanId, Status, Attrs, EndTime} */
ERL_NIF_TERM msg = enif_make_tuple5(msg_env,
ATOM_SPAN_END,
span_id_term,
status_term,
attrs_term,
ts_term);
enif_send(NULL, &g_trace_receiver_pid, msg_env, msg);
enif_free_env(msg_env);
Py_RETURN_NONE;
}
/**
* @brief Python: erlang._trace_event(span_id, name, attrs)
*
* Add an event to a trace span.
*
* @param self Module reference (unused)
* @param args Tuple: (span_id:int, name:str, attrs:dict)
* @return None
*/
static PyObject *erlang_trace_event_impl(PyObject *self, PyObject *args) {
(void)self;
if (!g_has_trace_receiver) {
Py_RETURN_NONE;
}
unsigned long long span_id;
const char *name;
Py_ssize_t name_len;
PyObject *attrs;
if (!PyArg_ParseTuple(args, "Ks#O", &span_id, &name, &name_len, &attrs)) {
return NULL;
}
ErlNifEnv *msg_env = enif_alloc_env();
if (msg_env == NULL) {
Py_RETURN_NONE;
}
ERL_NIF_TERM span_id_term = enif_make_uint64(msg_env, span_id);
ERL_NIF_TERM name_term;
unsigned char *name_buf = enif_make_new_binary(msg_env, name_len, &name_term);
if (name_buf != NULL) {
memcpy(name_buf, name, name_len);
}
ERL_NIF_TERM attrs_term = py_to_term(msg_env, attrs);
uint64_t ts = get_monotonic_ns() / 1000;
ERL_NIF_TERM ts_term = enif_make_uint64(msg_env, ts);
/* {span_event, SpanId, Name, Attrs, Time} */
ERL_NIF_TERM msg = enif_make_tuple5(msg_env,
ATOM_SPAN_EVENT,
span_id_term,
name_term,
attrs_term,
ts_term);
enif_send(NULL, &g_trace_receiver_pid, msg_env, msg);
enif_free_env(msg_env);
Py_RETURN_NONE;
}
/* ============================================================================
* NIF functions for receiver management
* ============================================================================ */
/**
* @brief NIF: set_log_receiver(Pid, Level) -> ok
*
* Register the Erlang process that will receive log messages.
*
* @param env NIF environment
* @param argc Argument count (2)
* @param argv [Pid, Level]
* @return ok atom
*/
static ERL_NIF_TERM nif_set_log_receiver(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
ErlNifPid pid;
int level;
if (!enif_get_local_pid(env, argv[0], &pid)) {
return make_error(env, "invalid_pid");
}
if (!enif_get_int(env, argv[1], &level)) {
return make_error(env, "invalid_level");
}
g_log_receiver_pid = pid;
g_log_level_threshold = level;
g_has_log_receiver = true;
return ATOM_OK;
}
/**
* @brief NIF: clear_log_receiver() -> ok
*
* Unregister the log receiver. Log messages will be dropped.
*
* @param env NIF environment
* @param argc Argument count (0)
* @param argv (unused)
* @return ok atom
*/
static ERL_NIF_TERM nif_clear_log_receiver(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)env;
(void)argc;
(void)argv;
g_has_log_receiver = false;
return ATOM_OK;
}
/**
* @brief NIF: set_trace_receiver(Pid) -> ok
*
* Register the Erlang process that will receive trace spans.
*
* @param env NIF environment
* @param argc Argument count (1)
* @param argv [Pid]
* @return ok atom
*/
static ERL_NIF_TERM nif_set_trace_receiver(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
ErlNifPid pid;
if (!enif_get_local_pid(env, argv[0], &pid)) {
return make_error(env, "invalid_pid");
}
g_trace_receiver_pid = pid;
g_has_trace_receiver = true;
return ATOM_OK;
}
/**
* @brief NIF: clear_trace_receiver() -> ok
*
* Unregister the trace receiver. Trace events will be dropped.
*
* @param env NIF environment
* @param argc Argument count (0)
* @param argv (unused)
* @return ok atom
*/
static ERL_NIF_TERM nif_clear_trace_receiver(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)env;
(void)argc;
(void)argv;
g_has_trace_receiver = false;
return ATOM_OK;
}