Current section
Files
Jump to
Current section
Files
test/py_logging_SUITE.erl
%% 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.
%%% @doc Test suite for Python logging and tracing integration.
-module(py_logging_SUITE).
-include_lib("common_test/include/ct.hrl").
-export([
all/0,
init_per_suite/1,
end_per_suite/1,
init_per_testcase/2,
end_per_testcase/2
]).
-export([
test_basic_logging/1,
test_log_levels/1,
test_log_from_thread/1,
test_trace_basic/1,
test_trace_nested/1,
test_trace_decorator/1,
test_trace_events/1,
test_trace_error/1,
test_configure_logging/1
]).
all() ->
[
test_basic_logging,
test_log_levels,
test_log_from_thread,
test_trace_basic,
test_trace_nested,
test_trace_decorator,
test_trace_events,
test_trace_error,
test_configure_logging
].
init_per_suite(Config) ->
{ok, _} = application:ensure_all_started(erlang_python),
Config.
end_per_suite(_Config) ->
application:stop(erlang_python),
ok.
init_per_testcase(_TestCase, Config) ->
%% Clear traces before each test
py:clear_traces(),
Config.
end_per_testcase(_TestCase, _Config) ->
%% Disable tracing after each test
py:disable_tracing(),
ok.
%% @doc Test basic Python logging integration.
test_basic_logging(_Config) ->
%% Configure logging
ok = py:configure_logging(),
%% Log a message from Python (use __import__ for single expression)
{ok, _} = py:eval(<<"__import__('logging').info('test message from Python')">>),
%% Give a moment for async delivery
timer:sleep(50),
%% Success if no error
ok.
%% @doc Test log level filtering.
test_log_levels(_Config) ->
%% Configure logging with warning level
ok = py:configure_logging(#{level => warning}),
%% Log at different levels using exec for multi-statement code
ok = py:exec(<<"
import logging
logging.debug('debug msg')
logging.info('info msg')
logging.warning('warning msg')
logging.error('error msg')
">>),
%% Give a moment for async delivery
timer:sleep(50),
ok.
%% @doc Test logging from Python threads.
test_log_from_thread(_Config) ->
ok = py:configure_logging(),
ok = py:exec(<<"
import logging
import threading
def log_in_thread():
logging.info('message from thread')
t = threading.Thread(target=log_in_thread)
t.start()
t.join()
">>),
timer:sleep(100),
ok.
%% @doc Test basic trace span functionality.
test_trace_basic(_Config) ->
ok = py:enable_tracing(),
ok = py:exec(<<"
import erlang
with erlang.Span('test-span', key='value'):
pass
">>),
{ok, Spans} = py:get_traces(),
1 = length(Spans),
[Span] = Spans,
<<"test-span">> = maps:get(name, Span),
ok = maps:get(status, Span),
true = maps:get(duration_us, Span) >= 0,
#{<<"key">> := <<"value">>} = maps:get(attributes, Span),
ok.
%% @doc Test nested trace spans.
test_trace_nested(_Config) ->
ok = py:enable_tracing(),
ok = py:exec(<<"
import erlang
with erlang.Span('parent'):
with erlang.Span('child'):
pass
">>),
{ok, Spans} = py:get_traces(),
2 = length(Spans),
%% Spans are in completion order (child completes first)
[Child, Parent] = Spans,
<<"child">> = maps:get(name, Child),
<<"parent">> = maps:get(name, Parent),
%% Child should have parent's span_id as parent_id
ParentSpanId = maps:get(span_id, Parent),
ParentSpanId = maps:get(parent_id, Child),
ok.
%% @doc Test trace decorator.
test_trace_decorator(_Config) ->
ok = py:enable_tracing(),
ok = py:exec(<<"
import erlang
@erlang.trace()
def my_traced_function():
return 42
result = my_traced_function()
">>),
{ok, Spans} = py:get_traces(),
1 = length(Spans),
[Span] = Spans,
%% Function name includes module
Name = maps:get(name, Span),
true = is_binary(Name),
ok.
%% @doc Test span events.
test_trace_events(_Config) ->
ok = py:enable_tracing(),
ok = py:exec(<<"
import erlang
with erlang.Span('work') as span:
span.event('checkpoint', items=10)
span.event('progress', percent=50)
">>),
{ok, [Span]} = py:get_traces(),
Events = maps:get(events, Span),
2 = length(Events),
%% Events are in reverse order (most recent first)
[Event2, Event1] = Events,
<<"checkpoint">> = maps:get(name, Event1),
<<"progress">> = maps:get(name, Event2),
ok.
%% @doc Test span error handling.
test_trace_error(_Config) ->
ok = py:enable_tracing(),
ok = py:exec(<<"
import erlang
try:
with erlang.Span('failing'):
raise ValueError('intentional error')
except ValueError:
pass
">>),
{ok, [Span]} = py:get_traces(),
error = maps:get(status, Span),
EndAttrs = maps:get(end_attrs, Span),
true = maps:is_key(<<"exception">>, EndAttrs),
ok.
%% @doc Test configure_logging with format.
test_configure_logging(_Config) ->
%% Test with custom format
ok = py:configure_logging(#{
level => debug,
format => <<"%(name)s - %(levelname)s - %(message)s">>
}),
{ok, _} = py:eval(<<"__import__('logging').info('formatted message')">>),
timer:sleep(50),
ok.