From 8d402a8a2dd5214accbcd83d5372271ced88e0e4 Mon Sep 17 00:00:00 2001 From: David Traina <44659830+DavidTraina@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:12:51 -0500 Subject: [PATCH] perf(span-processor): skip span_formatter when debug logging is off --- langfuse/_client/span_processor.py | 11 ++++++-- tests/unit/test_span_processor.py | 44 +++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/langfuse/_client/span_processor.py b/langfuse/_client/span_processor.py index f01b081c2..1302f4502 100644 --- a/langfuse/_client/span_processor.py +++ b/langfuse/_client/span_processor.py @@ -12,6 +12,7 @@ """ import base64 +import logging import os import threading from typing import Callable, Dict, List, Optional, cast @@ -218,9 +219,13 @@ def on_end(self, span: ReadableSpan) -> None: ) return - langfuse_logger.debug( - f"Trace: Processing span name='{span._name}' | Full details:\n{span_formatter(span)}" - ) + # span_formatter serializes the full span; skip it unless DEBUG is on + if langfuse_logger.isEnabledFor(logging.DEBUG): + langfuse_logger.debug( + "Trace: Processing span name='%s' | Full details:\n%s", + span.name, + span_formatter(span), + ) super().on_end(span) finally: diff --git a/tests/unit/test_span_processor.py b/tests/unit/test_span_processor.py index 5d12813a0..f16a59926 100644 --- a/tests/unit/test_span_processor.py +++ b/tests/unit/test_span_processor.py @@ -1,8 +1,12 @@ +import logging from typing import Sequence +from unittest.mock import patch -from opentelemetry.sdk.trace import ReadableSpan +import pytest +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +import langfuse._client.span_processor as span_processor_module from langfuse._client.environment_variables import ( LANGFUSE_FLUSH_AT, LANGFUSE_FLUSH_INTERVAL, @@ -54,3 +58,41 @@ def test_span_processor_uses_env_flush_settings_when_constructor_omits_them( assert processor._batch_processor._schedule_delay_millis == 3250 finally: processor.shutdown() + + +@pytest.fixture +def tracer_with_processor(): + processor = LangfuseSpanProcessor( + public_key="pk-test", + secret_key="sk-test", + base_url="http://localhost:3000", + span_exporter=NoOpSpanExporter(), + ) + provider = TracerProvider() + provider.add_span_processor(processor) + yield provider.get_tracer("test-instrumentor") + processor.shutdown() + + +@pytest.mark.parametrize( + ("level", "expected_formatter_calls"), + [(logging.WARNING, 0), (logging.DEBUG, 1)], +) +def test_on_end_formats_span_only_when_debug_enabled( + caplog, tracer_with_processor, level, expected_formatter_calls +): + caplog.set_level(level, logger="langfuse") + + with patch.object( + span_processor_module, "span_formatter", return_value="{}" + ) as span_formatter: + # gen_ai.* attribute makes the span pass the default export filter + with tracer_with_processor.start_as_current_span( + "llm-call", attributes={"gen_ai.system": "test"} + ): + pass + + assert span_formatter.call_count == expected_formatter_calls + assert ("Processing span name='llm-call'" in caplog.text) == bool( + expected_formatter_calls + )