From 6bc62d37bf07accdd9873da5ff8b2f0161f001a4 Mon Sep 17 00:00:00 2001 From: David Traina <44659830+DavidTraina@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:03:05 -0500 Subject: [PATCH] refactor(logging): use lazy %-style formatting in log calls --- AGENTS.md | 1 + code_review.md | 1 + langfuse/_client/client.py | 93 ++++++++++------ langfuse/_client/get_client.py | 4 +- langfuse/_client/observe.py | 4 +- langfuse/_client/propagation.py | 21 ++-- langfuse/_client/resource_manager.py | 63 +++++++---- langfuse/_client/span.py | 6 +- langfuse/_client/span_exporter.py | 100 +++++++++++------- langfuse/_client/span_processor.py | 57 ++++++---- langfuse/_task_manager/media_manager.py | 48 +++++++-- .../_task_manager/media_upload_consumer.py | 6 +- .../_task_manager/score_ingestion_consumer.py | 25 +++-- langfuse/_utils/error_logging.py | 2 +- langfuse/_utils/prompt_cache.py | 23 ++-- langfuse/_utils/serializer.py | 3 +- langfuse/batch_evaluation.py | 51 +++++---- langfuse/experiment.py | 2 +- langfuse/langchain/CallbackHandler.py | 7 +- langfuse/media.py | 6 +- langfuse/openai.py | 2 +- pyproject.toml | 3 + 22 files changed, 351 insertions(+), 177 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a1a6f5846..5b463527d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -147,6 +147,7 @@ If you change CI bootstrap: ## Python-Specific Notes - Exception messages should not inline f-string literals in the `raise` statement. Build the message in a variable first. +- Logging calls must not use f-strings; pass lazy `%`-style args (`langfuse_logger.debug("span=%s", name)`), enforced by ruff `G004`. Arguments are still evaluated eagerly, so guard an expensive one with `langfuse_logger.isEnabledFor(logging.DEBUG)`. - Prefer ASCII-only edits unless the file already uses Unicode or Unicode is clearly required. ## Release And Docs diff --git a/code_review.md b/code_review.md index a3c55051c..a82c78595 100644 --- a/code_review.md +++ b/code_review.md @@ -33,6 +33,7 @@ Use this checklist for `/review`, PR review, or self-review before handoff. ## Python Style - Exception messages should not inline f-string literals in `raise` statements; build the message in a variable first. +- Logging calls should use lazy `%`-style args, not f-strings (ruff `G004`). Flag any logging argument that is expensive to compute and is not behind an `isEnabledFor` guard. - Keep edits ASCII-only unless the file already uses Unicode or Unicode is clearly required. - Keep changes scoped; avoid opportunistic refactors. - Never commit secrets or credentials. diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index 2b2889545..f8267ea45 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -1200,7 +1200,7 @@ def start_as_current_observation( # This should never be reached since all valid types are handled above langfuse_logger.warning( - f"Unknown observation type: {as_type}, falling back to span" + "Unknown observation type: %s, falling back to span", as_type ) return self._start_as_current_otel_span_with_processed_media( as_type="span", @@ -1734,12 +1734,16 @@ def _create_remote_parent_span( ) -> Any: if not self._is_valid_trace_id(trace_id): langfuse_logger.warning( - f"Passed trace ID '{trace_id}' is not a valid 32 lowercase hex char Langfuse trace id. Ignoring trace ID." + "Passed trace ID '%s' is not a valid 32 lowercase hex char Langfuse trace " + "id. Ignoring trace ID.", + trace_id, ) if parent_span_id and not self._is_valid_span_id(parent_span_id): langfuse_logger.warning( - f"Passed span ID '{parent_span_id}' is not a valid 16 lowercase hex char Langfuse span id. Ignoring parent span ID." + "Passed span ID '%s' is not a valid 16 lowercase hex char Langfuse span " + "id. Ignoring parent span ID.", + parent_span_id, ) int_trace_id = int(trace_id, 16) @@ -2051,7 +2055,11 @@ def create_score( except Exception as e: langfuse_logger.exception( - f"Error creating score: Failed to process score event for trace_id={trace_id}, name={name}. Error: {e}" + "Error creating score: Failed to process score event for trace_id=%s, " + "name=%s. Error: %s", + trace_id, + name, + e, ) def _create_trace_tags_via_ingestion( @@ -2084,7 +2092,10 @@ def _create_trace_tags_via_ingestion( self._resources.add_trace_task(event) except Exception as e: langfuse_logger.exception( - f"Error updating trace tags: Failed to process trace update event for trace_id={trace_id}. Error: {e}" + "Error updating trace tags: Failed to process trace update event for " + "trace_id=%s. Error: %s", + trace_id, + e, ) @overload @@ -2167,7 +2178,12 @@ def score_current_span( observation_id = self._get_otel_span_id(current_span) langfuse_logger.info( - f"Score: Creating score name='{name}' value={value} for current span ({observation_id}) in trace {trace_id}" + "Score: Creating score name='%s' value=%s for current span (%s) in trace " + "%s", + name, + value, + observation_id, + trace_id, ) self.create_score( @@ -2265,7 +2281,10 @@ def score_current_trace( trace_id = self._get_otel_trace_id(current_span) langfuse_logger.info( - f"Score: Creating score name='{name}' value={value} for entire trace {trace_id}" + "Score: Creating score name='%s' value=%s for entire trace %s", + name, + value, + trace_id, ) self.create_score( @@ -2476,7 +2495,7 @@ def get_dataset( DatasetClient: The dataset with the given name. """ try: - langfuse_logger.debug(f"Getting datasets {name}") + langfuse_logger.debug("Getting datasets %s", name) dataset = self.api.datasets.get(dataset_name=self._url_encode(name)) dataset_items: List[DatasetItem] = [] @@ -2804,7 +2823,7 @@ async def _run_experiment_async( dataset_version: Optional[datetime] = None, ) -> ExperimentResult: langfuse_logger.debug( - f"Starting experiment '{name}' run '{run_name}' with {len(data)} items" + "Starting experiment '%s' run '%s' with %s items", name, run_name, len(data) ) shared_fallback_experiment_id = self._create_observation_id() @@ -2836,7 +2855,7 @@ async def process_item(item: ExperimentItem) -> ExperimentItemResult: valid_results: List[ExperimentItemResult] = [] for i, result in enumerate(item_results): if isinstance(result, Exception): - langfuse_logger.error(f"Item {i} failed: {result}") + langfuse_logger.error("Item %s failed: %s", i, result) elif isinstance(result, ExperimentItemResult): valid_results.append(result) # type: ignore @@ -2849,7 +2868,7 @@ async def process_item(item: ExperimentItem) -> ExperimentItemResult: ) run_evaluations.extend(evaluations) except Exception as e: - langfuse_logger.error(f"Run evaluator failed: {e}") + langfuse_logger.error("Run evaluator failed: %s", e) # Generate dataset run URL if applicable dataset_run_id = next( @@ -2894,7 +2913,7 @@ async def process_item(item: ExperimentItem) -> ExperimentItemResult: ) except Exception as e: - langfuse_logger.error(f"Failed to store run evaluation: {e}") + langfuse_logger.error("Failed to store run evaluation: %s", e) # Flush scores and traces self.flush() @@ -3016,7 +3035,7 @@ async def _process_experiment_item( except Exception as e: langfuse_logger.error( - f"Failed to create dataset run item: {e}" + "Failed to create dataset run item: %s", e ) experiment_id = dataset_run_id or fallback_experiment_id @@ -3115,7 +3134,7 @@ async def _process_experiment_item( level="ERROR", status_message=str(e), ) - langfuse_logger.error(f"Evaluator failed: {e}") + langfuse_logger.error("Evaluator failed: %s", e) continue evaluations.extend(eval_results) @@ -3134,7 +3153,7 @@ async def _process_experiment_item( ) except Exception as e: langfuse_logger.error( - f"Failed to store evaluation: {e}" + "Failed to store evaluation: %s", e ) if composite_evaluator and evaluations: @@ -3182,7 +3201,7 @@ async def _process_experiment_item( status_message=str(e), ) langfuse_logger.error( - f"Composite evaluator failed: {e}" + "Composite evaluator failed: %s", e ) composite_evals = [] @@ -3201,7 +3220,7 @@ async def _process_experiment_item( ) except Exception as e: langfuse_logger.error( - f"Failed to store composite evaluation: {e}" + "Failed to store composite evaluation: %s", e ) evaluation_span.update( @@ -3481,7 +3500,7 @@ def auth_check(self) -> bool: try: projects = self.api.projects.get() langfuse_logger.debug( - f"Auth check successful, found {len(projects.data)} projects" + "Auth check successful, found %s projects", len(projects.data) ) if len(projects.data) == 0: raise Exception( @@ -3491,7 +3510,7 @@ def auth_check(self) -> bool: except AttributeError as e: langfuse_logger.warning( - f"Auth check failed: Client not properly initialized. Error: {e}" + "Auth check failed: Client not properly initialized. Error: %s", e ) return False @@ -3521,7 +3540,7 @@ def create_dataset( Dataset: The created dataset as returned by the Langfuse API. """ try: - langfuse_logger.debug(f"Creating datasets {name}") + langfuse_logger.debug("Creating datasets %s", name) result = self.api.datasets.create( name=name, @@ -3582,7 +3601,7 @@ def create_dataset_item( ``` """ try: - langfuse_logger.debug(f"Creating dataset item for dataset {dataset_name}") + langfuse_logger.debug("Creating dataset item for dataset %s", dataset_name) # Media uploads must reference the (dataset, item) they belong to, and # the item need not exist yet — so settle on the item id up front and @@ -3757,7 +3776,8 @@ def _replace_json_path_value( return json_path.set_value_at_path(value, path, replacement) except Exception as e: langfuse_logger.warning( - f"Failed to hydrate dataset media reference at JSONPath {path}", + "Failed to hydrate dataset media reference at JSONPath %s", + path, exc_info=e, ) @@ -3902,12 +3922,12 @@ def get_prompt( max_retries, default_max_retries=2, max_retries_upper_bound=4 ) - langfuse_logger.debug(f"Getting prompt '{cache_key}'") + langfuse_logger.debug("Getting prompt '%s'", cache_key) cached_prompt = self._resources.prompt_cache.get(cache_key) if cached_prompt is None or cache_ttl_seconds == 0: langfuse_logger.debug( - f"Prompt '{cache_key}' not found in cache or caching disabled." + "Prompt '%s' not found in cache or caching disabled.", cache_key ) try: return self._fetch_prompt_and_update_cache( @@ -3921,7 +3941,9 @@ def get_prompt( except Exception as e: if fallback: langfuse_logger.warning( - f"Returning fallback prompt for '{cache_key}' due to fetch error: {e}" + "Returning fallback prompt for '%s' due to fetch error: %s", + cache_key, + e, ) fallback_client_args: Dict[str, Any] = { @@ -3949,10 +3971,12 @@ def get_prompt( raise e if cached_prompt.is_expired(): - langfuse_logger.debug(f"Stale prompt '{cache_key}' found in cache.") + langfuse_logger.debug("Stale prompt '%s' found in cache.", cache_key) try: # refresh prompt in background thread, refresh_prompt deduplicates tasks - langfuse_logger.debug(f"Refreshing prompt '{cache_key}' in background.") + langfuse_logger.debug( + "Refreshing prompt '%s' in background.", cache_key + ) def refresh_task() -> None: self._fetch_prompt_and_update_cache( @@ -3970,14 +3994,17 @@ def refresh_task() -> None: refresh_task, ) langfuse_logger.debug( - f"Returning stale prompt '{cache_key}' from cache." + "Returning stale prompt '%s' from cache.", cache_key ) # return stale prompt return cached_prompt.value except Exception as e: langfuse_logger.warning( - f"Error when refreshing cached prompt '{cache_key}', returning cached version. Error: {e}" + "Error when refreshing cached prompt '%s', returning cached version. " + "Error: %s", + cache_key, + e, ) # creation of refresh prompt task failed, return stale prompt return cached_prompt.value @@ -3995,7 +4022,7 @@ def _fetch_prompt_and_update_cache( fetch_timeout_seconds: Optional[int], ) -> PromptClient: cache_key = PromptCache.generate_cache_key(name, version=version, label=label) - langfuse_logger.debug(f"Fetching prompt '{cache_key}' from server...") + langfuse_logger.debug("Fetching prompt '%s' from server...", cache_key) try: @@ -4029,7 +4056,7 @@ def fetch_prompts() -> Any: except NotFoundError as not_found_error: langfuse_logger.warning( - f"Prompt '{cache_key}' not found during refresh, evicting from cache." + "Prompt '%s' not found during refresh, evicting from cache.", cache_key ) if self._resources is not None: self._resources.prompt_cache.delete(cache_key) @@ -4037,7 +4064,7 @@ def fetch_prompts() -> Any: except Exception as e: langfuse_logger.error( - f"Error while fetching prompt '{cache_key}': {str(e)}" + "Error while fetching prompt '%s': %s", cache_key, str(e) ) raise e @@ -4114,7 +4141,7 @@ def create_prompt( ChatPromptClient: The prompt if type argument is 'chat'. """ try: - langfuse_logger.debug(f"Creating prompt {name=}, {labels=}") + langfuse_logger.debug("Creating prompt name=%r, labels=%r", name, labels) if type == "chat": if not isinstance(prompt, list): diff --git a/langfuse/_client/get_client.py b/langfuse/_client/get_client.py index a360430ac..0c4ccd321 100644 --- a/langfuse/_client/get_client.py +++ b/langfuse/_client/get_client.py @@ -142,7 +142,9 @@ def get_client(*, public_key: Optional[str] = None) -> Langfuse: if target_instance is None: # No instance found with this key - client not initialized properly langfuse_logger.warning( - f"No Langfuse client with public key {public_key} has been initialized. Skipping tracing for decorated function." + "No Langfuse client with public key %s has been initialized. Skipping " + "tracing for decorated function.", + public_key, ) return Langfuse( tracing_enabled=False, public_key="fake", secret_key="fake" diff --git a/langfuse/_client/observe.py b/langfuse/_client/observe.py index 53aa79a58..848506a46 100644 --- a/langfuse/_client/observe.py +++ b/langfuse/_client/observe.py @@ -187,7 +187,9 @@ def sub_process(): valid_types = set(get_observation_types_list(ObservationTypeLiteralNoEvent)) if as_type is not None and as_type not in valid_types: logger.warning( - f"Invalid as_type '{as_type}'. Valid types are: {', '.join(sorted(valid_types))}. Defaulting to 'span'." + "Invalid as_type '%s'. Valid types are: %s. Defaulting to 'span'.", + as_type, + ", ".join(sorted(valid_types)), ) as_type = "span" diff --git a/langfuse/_client/propagation.py b/langfuse/_client/propagation.py index d6e26163a..ecf961e4f 100644 --- a/langfuse/_client/propagation.py +++ b/langfuse/_client/propagation.py @@ -625,13 +625,16 @@ def _validate_propagated_value( if not isinstance(value, str): langfuse_logger.warning( # type: ignore - f"Propagated attribute '{key}' value is not a string. Dropping value." + "Propagated attribute '%s' value is not a string. Dropping value.", key ) return None if len(value) > 200: langfuse_logger.warning( - f"Propagated attribute '{key}' value is over 200 characters ({len(value)} chars). Dropping value." + "Propagated attribute '%s' value is over 200 characters (%s chars). " + "Dropping value.", + key, + len(value), ) return None @@ -641,13 +644,16 @@ def _validate_propagated_value( def _validate_string_value(*, value: str, key: str) -> bool: if not isinstance(value, str): langfuse_logger.warning( # type: ignore - f"Propagated attribute '{key}' value is not a string. Dropping value." + "Propagated attribute '%s' value is not a string. Dropping value.", key ) return False if len(value) > 200: langfuse_logger.warning( - f"Propagated attribute '{key}' value is over 200 characters ({len(value)} chars). Dropping value." + "Propagated attribute '%s' value is over 200 characters (%s chars). " + "Dropping value.", + key, + len(value), ) return False @@ -662,13 +668,16 @@ def _validate_environment_value(*, value: Any) -> Optional[str]: if not isinstance(value, str): langfuse_logger.warning( # type: ignore - f"Propagated attribute '{key}' value is not a string. Dropping value." + "Propagated attribute '%s' value is not a string. Dropping value.", key ) return None if len(value) > 40: langfuse_logger.warning( - f"Propagated attribute '{key}' value is over 40 characters ({len(value)} chars). Dropping value." + "Propagated attribute '%s' value is over 40 characters (%s chars). " + "Dropping value.", + key, + len(value), ) return None diff --git a/langfuse/_client/resource_manager.py b/langfuse/_client/resource_manager.py index 67c44920a..a61101fe3 100644 --- a/langfuse/_client/resource_manager.py +++ b/langfuse/_client/resource_manager.py @@ -302,12 +302,13 @@ def _initialize_instance( ) langfuse_logger.info( - f"Startup: Langfuse tracer successfully initialized | " - f"public_key={self.public_key} | " - f"base_url={base_url} | " - f"environment={environment or 'default'} | " - f"sample_rate={sample_rate if sample_rate is not None else 1.0} | " - f"media_threads={self._media_upload_thread_count}" + "Startup: Langfuse tracer successfully initialized | public_key=%s | " + "base_url=%s | environment=%s | sample_rate=%s | media_threads=%s", + self.public_key, + base_url, + environment or "default", + sample_rate if sample_rate is not None else 1.0, + self._media_upload_thread_count, ) def _init_media_manager(self) -> None: @@ -435,7 +436,8 @@ def _at_fork_reinit(self) -> None: os.environ["NO_PROXY"] = "*" langfuse_logger.debug( - f"[PID {os.getpid()}] Fork detected: reinitializing Langfuse consumer threads." + "[PID %s] Fork detected: reinitializing Langfuse consumer threads.", + os.getpid(), ) # Queues are intentionally recreated after fork. Items enqueued before fork @@ -455,8 +457,10 @@ def _at_fork_reinit(self) -> None: self._init_api_clients() except Exception as e: langfuse_logger.error( - f"[PID {os.getpid()}] Failed to recreate HTTP clients after fork: {e}. " - f"Network requests may fail in this worker." + "[PID %s] Failed to recreate HTTP clients after fork: %s. Network " + "requests may fail in this worker.", + os.getpid(), + e, ) try: @@ -465,12 +469,17 @@ def _at_fork_reinit(self) -> None: self.prompt_cache = PromptCache() except Exception as e: langfuse_logger.error( - f"[PID {os.getpid()}] Failed to reinitialize consumer threads after fork: {e}. " - f"Media upload, score ingestion, and prompt cache refresh will be unavailable in this worker." + "[PID %s] Failed to reinitialize consumer threads after fork: %s. Media " + "upload, score ingestion, and prompt cache refresh will be unavailable in " + "this worker.", + os.getpid(), + e, ) langfuse_logger.debug( - f"[PID {os.getpid()}] Langfuse consumer threads and prompt cache reinitialized after fork" + "[PID %s] Langfuse consumer threads and prompt cache reinitialized after " + "fork", + os.getpid(), ) @classmethod @@ -509,7 +518,11 @@ def add_score_task(self, event: dict, *, force_sample: bool = False) -> None: if should_sample: langfuse_logger.debug( - f"Score: Enqueuing event type={event['type']} for trace_id={event['body'].trace_id} name={event['body'].name} value={event['body'].value}" + "Score: Enqueuing event type=%s for trace_id=%s name=%s value=%s", + event["type"], + event["body"].trace_id, + event["body"].name, + event["body"].value, ) self._score_ingestion_queue.put(event, block=False) @@ -521,7 +534,9 @@ def add_score_task(self, event: dict, *, force_sample: bool = False) -> None: return except Exception as e: langfuse_logger.error( - f"Unexpected error: Failed to process score event. The score will be dropped. Error details: {e}" + "Unexpected error: Failed to process score event. The score will be " + "dropped. Error details: %s", + e, ) return @@ -532,7 +547,9 @@ def add_trace_task( ) -> None: try: langfuse_logger.debug( - f"Trace: Enqueuing event type={event['type']} for trace_id={event['body'].id}" + "Trace: Enqueuing event type=%s for trace_id=%s", + event["type"], + event["body"].id, ) self._score_ingestion_queue.put(event, block=False) @@ -544,7 +561,9 @@ def add_trace_task( return except Exception as e: langfuse_logger.error( - f"Unexpected error: Failed to process trace event. The trace update will be dropped. Error details: {e}" + "Unexpected error: Failed to process trace event. The trace update will " + "be dropped. Error details: %s", + e, ) return @@ -563,7 +582,8 @@ def _stop_and_join_consumer_threads(self) -> None: Blocks execution until finished """ langfuse_logger.debug( - f"Shutdown: Waiting for {len(self._media_upload_consumers)} media upload thread(s) to complete processing" + "Shutdown: Waiting for %s media upload thread(s) to complete processing", + len(self._media_upload_consumers), ) for media_upload_consumer in self._media_upload_consumers: media_upload_consumer.pause() @@ -578,11 +598,13 @@ def _stop_and_join_consumer_threads(self) -> None: pass langfuse_logger.debug( - f"Shutdown: Media upload thread #{media_upload_consumer._identifier} successfully terminated" + "Shutdown: Media upload thread #%s successfully terminated", + media_upload_consumer._identifier, ) langfuse_logger.debug( - f"Shutdown: Waiting for {len(self._ingestion_consumers)} score ingestion thread(s) to complete processing" + "Shutdown: Waiting for %s score ingestion thread(s) to complete processing", + len(self._ingestion_consumers), ) for score_ingestion_consumer in self._ingestion_consumers: score_ingestion_consumer.pause() @@ -595,7 +617,8 @@ def _stop_and_join_consumer_threads(self) -> None: pass langfuse_logger.debug( - f"Shutdown: Score ingestion thread #{score_ingestion_consumer._identifier} successfully terminated" + "Shutdown: Score ingestion thread #%s successfully terminated", + score_ingestion_consumer._identifier, ) def flush(self) -> None: diff --git a/langfuse/_client/span.py b/langfuse/_client/span.py index 71f8d68c7..96879499c 100644 --- a/langfuse/_client/span.py +++ b/langfuse/_client/span.py @@ -574,7 +574,9 @@ def _mask_attribute(self, *, data: Any) -> Any: return self._langfuse_client._mask(data=data) except Exception as e: langfuse_logger.error( - f"Masking error: Custom mask function threw exception when processing data. Using fallback masking. Error: {e}" + "Masking error: Custom mask function threw exception when processing " + "data. Using fallback masking. Error: %s", + e, ) return "" @@ -963,7 +965,7 @@ def start_observation( observation_class = _OBSERVATION_CLASS_MAP.get(as_type) if not observation_class: langfuse_logger.warning( - f"Unknown observation type: {as_type}, falling back to LangfuseSpan" + "Unknown observation type: %s, falling back to LangfuseSpan", as_type ) observation_class = LangfuseSpan diff --git a/langfuse/_client/span_exporter.py b/langfuse/_client/span_exporter.py index bbdc569cf..7ea56f81c 100644 --- a/langfuse/_client/span_exporter.py +++ b/langfuse/_client/span_exporter.py @@ -161,9 +161,13 @@ def _process_media_attributes( except Exception as error: langfuse_logger.warning( "Media processing error: Failed to process span attribute before export. " - f"Leaving attribute unchanged. span_name='{span.name}' " - f"trace_id='{_get_trace_id(span)}' span_id='{_get_span_id(span)}' " - f"attribute_key='{key}' error='{error}'" + "Leaving attribute unchanged. span_name='%s' trace_id='%s' span_id='%s' " + "attribute_key='%s' error='%s'", + span.name, + _get_trace_id(span), + _get_span_id(span), + key, + error, ) processed_attributes[key] = value @@ -271,8 +275,9 @@ def _apply_mask_otel_spans( for span, attributes in span_attributes: if not _has_valid_span_context(span): langfuse_logger.warning( - "Masking error: Dropping span from export because span context is missing or invalid. " - f"span_name='{span.name}'" + "Masking error: Dropping span from export because span context is missing " + "or invalid. span_name='%s'", + span.name, ) continue @@ -303,9 +308,10 @@ def _apply_mask_otel_spans( ) except Exception as error: langfuse_logger.error( - "Masking error: mask_otel_spans raised an exception. " - f"Dropping export batch. span_count={len(span_attributes)} " - f"error='{error}'" + "Masking error: mask_otel_spans raised an exception. Dropping export " + "batch. span_count=%s error='%s'", + len(span_attributes), + error, ) return None @@ -314,8 +320,9 @@ def _apply_mask_otel_spans( if not isinstance(result, MaskOtelSpansResult): langfuse_logger.error( - "Masking error: mask_otel_spans returned an invalid result. " - f"Dropping export batch. result_type='{type(result).__name__}'" + "Masking error: mask_otel_spans returned an invalid result. Dropping " + "export batch. result_type='%s'", + type(result).__name__, ) return None @@ -323,9 +330,9 @@ def _apply_mask_otel_spans( if not isinstance(span_patches, MappingCollection): langfuse_logger.error( - "Masking error: mask_otel_spans returned invalid span_patches. " - f"Dropping export batch. " - f"span_patches_type='{type(span_patches).__name__}'" + "Masking error: mask_otel_spans returned invalid span_patches. Dropping " + "export batch. span_patches_type='%s'", + type(span_patches).__name__, ) return None @@ -334,9 +341,9 @@ def _apply_mask_otel_spans( for identifier in span_patches: if identifier not in span_identifiers: langfuse_logger.error( - "Masking error: mask_otel_spans returned a patch for an unknown " - "span identifier. Dropping export batch. " - f"identifier_type='{type(identifier).__name__}'" + "Masking error: mask_otel_spans returned a patch for an unknown span " + "identifier. Dropping export batch. identifier_type='%s'", + type(identifier).__name__, ) return None @@ -372,10 +379,12 @@ def _apply_otel_span_patch( ) -> Optional[Dict[str, AttributeValue]]: if not isinstance(patch, OtelSpanPatch): langfuse_logger.error( - "Masking error: mask_otel_spans returned an invalid span patch. " - "Dropping span. " - f"span_name='{span.name}' trace_id='{_get_trace_id(span)}' " - f"span_id='{_get_span_id(span)}' patch_type='{type(patch).__name__}'" + "Masking error: mask_otel_spans returned an invalid span patch. Dropping " + "span. span_name='%s' trace_id='%s' span_id='%s' patch_type='%s'", + span.name, + _get_trace_id(span), + _get_span_id(span), + type(patch).__name__, ) return None @@ -384,11 +393,12 @@ def _apply_otel_span_patch( if not isinstance(set_attributes, MappingCollection): langfuse_logger.error( - "Masking error: mask_otel_spans returned invalid set_attributes. " - "Dropping span. " - f"span_name='{span.name}' trace_id='{_get_trace_id(span)}' " - f"span_id='{_get_span_id(span)}' " - f"set_attributes_type='{type(set_attributes).__name__}'" + "Masking error: mask_otel_spans returned invalid set_attributes. Dropping " + "span. span_name='%s' trace_id='%s' span_id='%s' set_attributes_type='%s'", + span.name, + _get_trace_id(span), + _get_span_id(span), + type(set_attributes).__name__, ) return None @@ -397,10 +407,12 @@ def _apply_otel_span_patch( ): langfuse_logger.error( "Masking error: mask_otel_spans returned invalid delete_attributes. " - "Dropping span. " - f"span_name='{span.name}' trace_id='{_get_trace_id(span)}' " - f"span_id='{_get_span_id(span)}' " - f"delete_attributes_type='{type(delete_attributes).__name__}'" + "Dropping span. span_name='%s' trace_id='%s' span_id='%s' " + "delete_attributes_type='%s'", + span.name, + _get_trace_id(span), + _get_span_id(span), + type(delete_attributes).__name__, ) return None @@ -409,10 +421,13 @@ def _apply_otel_span_patch( for key in delete_attributes: if not _is_valid_attribute_key(key): langfuse_logger.warning( - "Masking error: mask_otel_spans requested deletion with an invalid attribute key. " - f"Ignoring delete entry. span_name='{span.name}' " - f"trace_id='{_get_trace_id(span)}' span_id='{_get_span_id(span)}' " - f"delete_key_type='{type(key).__name__}'" + "Masking error: mask_otel_spans requested deletion with an invalid " + "attribute key. Ignoring delete entry. span_name='%s' trace_id='%s' " + "span_id='%s' delete_key_type='%s'", + span.name, + _get_trace_id(span), + _get_span_id(span), + type(key).__name__, ) continue @@ -422,9 +437,12 @@ def _apply_otel_span_patch( if not _is_valid_attribute_key(key): langfuse_logger.warning( "Masking error: mask_otel_spans returned an invalid set_attributes key. " - f"Ignoring set entry. span_name='{span.name}' " - f"trace_id='{_get_trace_id(span)}' span_id='{_get_span_id(span)}' " - f"attribute_key_type='{type(key).__name__}'" + "Ignoring set entry. span_name='%s' trace_id='%s' span_id='%s' " + "attribute_key_type='%s'", + span.name, + _get_trace_id(span), + _get_span_id(span), + type(key).__name__, ) continue @@ -434,9 +452,13 @@ def _apply_otel_span_patch( masked_attributes.pop(key, None) langfuse_logger.warning( "Masking error: mask_otel_spans returned an invalid attribute value. " - f"Deleting attribute from export. span_name='{span.name}' " - f"trace_id='{_get_trace_id(span)}' span_id='{_get_span_id(span)}' " - f"attribute_key='{key}' value_type='{type(value).__name__}'" + "Deleting attribute from export. span_name='%s' trace_id='%s' " + "span_id='%s' attribute_key='%s' value_type='%s'", + span.name, + _get_trace_id(span), + _get_span_id(span), + key, + type(value).__name__, ) continue diff --git a/langfuse/_client/span_processor.py b/langfuse/_client/span_processor.py index f01b081c2..be657885b 100644 --- a/langfuse/_client/span_processor.py +++ b/langfuse/_client/span_processor.py @@ -165,16 +165,20 @@ def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None span.set_attributes(propagated_attributes) langfuse_logger.debug( - f"Propagated {len(propagated_attributes)} attributes to span '{format_span_id(span.context.span_id)}': {propagated_attributes}" + "Propagated %s attributes to span '%s': %s", + len(propagated_attributes), + format_span_id(span.context.span_id), + propagated_attributes, ) try: self._mark_app_root_candidate(span=span, parent_context=context) except Exception as error: langfuse_logger.debug( - "Trace: app-root start-time check failed. Span will not be marked as app root | " - f"span_name='{getattr(span, 'name', '')}' | " - f"Error: {error}" + "Trace: app-root start-time check failed. Span will not be marked as app " + "root | span_name='%s' | Error: %s", + getattr(span, "name", ""), + error, ) return super().on_start(span, parent_context) @@ -185,8 +189,14 @@ def on_end(self, span: ReadableSpan) -> None: # This is important to not send spans to wrong project in multi-project setups if is_langfuse_span(span) and not self._is_langfuse_project_span(span): langfuse_logger.debug( - f"Security: Span rejected - belongs to project '{span.instrumentation_scope.attributes.get('public_key') if span.instrumentation_scope and span.instrumentation_scope.attributes else None}' but processor is for '{self.public_key}'. " - f"This prevents cross-project data leakage in multi-project environments." + "Security: Span rejected - belongs to project '%s' but processor is for " + "'%s'. This prevents cross-project data leakage in multi-project " + "environments.", + span.instrumentation_scope.attributes.get("public_key") + if span.instrumentation_scope + and span.instrumentation_scope.attributes + else None, + self.public_key, ) return @@ -194,8 +204,9 @@ def on_end(self, span: ReadableSpan) -> None: if self._is_blocked_instrumentation_scope(span): langfuse_logger.debug( "Trace: Dropping span due to blocked instrumentation scope | " - f"span_name='{span.name}' | " - f"instrumentation_scope='{self._get_scope_name(span)}'" + "span_name='%s' | instrumentation_scope='%s'", + span.name, + self._get_scope_name(span), ) return @@ -204,22 +215,27 @@ def on_end(self, span: ReadableSpan) -> None: should_export = self._should_export_span(span) except Exception as error: langfuse_logger.error( - "Trace: should_export_span callback raised an error. " - f"Dropping span name='{span.name}' scope='{self._get_scope_name(span)}'. " - f"Error: {error}" + "Trace: should_export_span callback raised an error. Dropping span " + "name='%s' scope='%s'. Error: %s", + span.name, + self._get_scope_name(span), + error, ) return if not should_export: langfuse_logger.debug( - "Trace: Dropping span due to should_export_span filter | " - f"span_name='{span.name}' | " - f"instrumentation_scope='{self._get_scope_name(span)}'" + "Trace: Dropping span due to should_export_span filter | span_name='%s' | " + "instrumentation_scope='%s'", + span.name, + self._get_scope_name(span), ) return langfuse_logger.debug( - f"Trace: Processing span name='{span._name}' | Full details:\n{span_formatter(span)}" + "Trace: Processing span name='%s' | Full details:\n%s", + span._name, + span_formatter(span), ) super().on_end(span) @@ -273,11 +289,12 @@ def _is_expected_exported_at_start(self, span: Span) -> bool: return bool(self._should_export_span(readable_span)) except Exception as error: langfuse_logger.debug( - "Trace: should_export_span callback raised during app-root " - f"start-time check. Span will not be marked as app root | " - f"span_name='{readable_span.name}' | " - f"instrumentation_scope='{self._get_scope_name(readable_span)}' | " - f"Error: {error}" + "Trace: should_export_span callback raised during app-root start-time " + "check. Span will not be marked as app root | span_name='%s' | " + "instrumentation_scope='%s' | Error: %s", + readable_span.name, + self._get_scope_name(readable_span), + error, ) return False diff --git a/langfuse/_task_manager/media_manager.py b/langfuse/_task_manager/media_manager.py index 4685099d6..9a66ecd63 100644 --- a/langfuse/_task_manager/media_manager.py +++ b/langfuse/_task_manager/media_manager.py @@ -66,7 +66,9 @@ def process_next_media_upload(self) -> None: return logger.debug( - f"Media: Processing upload for media_id={upload_job['media_id']} in trace_id={upload_job['trace_id']}" + "Media: Processing upload for media_id=%s in trace_id=%s", + upload_job["media_id"], + upload_job["trace_id"], ) self._process_upload_media_job(data=upload_job) @@ -75,7 +77,9 @@ def process_next_media_upload(self) -> None: pass except Exception as e: logger.error( - f"Media upload error: Failed to upload media due to unexpected error. Queue item marked as done. Error: {e}" + "Media upload error: Failed to upload media due to unexpected error. " + "Queue item marked as done. Error: %s", + e, ) self._queue.task_done() @@ -276,7 +280,11 @@ def _process_data_recursively(data: Any, level: int) -> Any: finally: seen.discard(id(data)) - if hasattr(data, "dict") and callable(data.dict) and hasattr(data, "__fields__"): + if ( + hasattr(data, "dict") + and callable(data.dict) + and hasattr(data, "__fields__") + ): # Pydantic v1 BaseModel if id(data) in seen: return data @@ -336,17 +344,28 @@ def _process_media( block=False, ) logger.debug( - f"Queue: Enqueued media ID {media._media_id} for upload processing | trace_id={trace_id} | field={field}" + "Queue: Enqueued media ID %s for upload processing | trace_id=%s | " + "field=%s", + media._media_id, + trace_id, + field, ) except Full: logger.warning( - f"Queue capacity: Media queue is full. Failed to process media_id={media._media_id} for trace_id={trace_id}. Consider increasing queue capacity." + "Queue capacity: Media queue is full. Failed to process media_id=%s for " + "trace_id=%s. Consider increasing queue capacity.", + media._media_id, + trace_id, ) except Exception as e: logger.error( - f"Media processing error: Failed to process media_id={media._media_id} for trace_id={trace_id}. Error: {str(e)}" + "Media processing error: Failed to process media_id=%s for trace_id=%s. " + "Error: %s", + media._media_id, + trace_id, + str(e), ) def _upload_media_sync( @@ -409,14 +428,19 @@ def _process_upload_media_job( if not upload_url: logger.debug( - f"Media status: Media with ID {data['media_id']} already uploaded. Skipping duplicate upload." + "Media status: Media with ID %s already uploaded. Skipping duplicate " + "upload.", + data["media_id"], ) return if upload_url_response.media_id != data["media_id"]: logger.error( - f"Media integrity error: Media ID mismatch between SDK ({data['media_id']}) and Server ({upload_url_response.media_id}). Upload cancelled. Please check media ID generation logic." + "Media integrity error: Media ID mismatch between SDK (%s) and Server " + "(%s). Upload cancelled. Please check media ID generation logic.", + data["media_id"], + upload_url_response.media_id, ) return @@ -472,7 +496,13 @@ def _upload_with_status_check() -> httpx.Response: ) logger.debug( - f"Media upload: Successfully uploaded media_id={data['media_id']} for trace_id={data['trace_id']} | status_code={upload_response.status_code} | duration={upload_time_ms}ms | size={data['content_length']} bytes" + "Media upload: Successfully uploaded media_id=%s for trace_id=%s | " + "status_code=%s | duration=%sms | size=%s bytes", + data["media_id"], + data["trace_id"], + upload_response.status_code, + upload_time_ms, + data["content_length"], ) def _request_with_backoff( diff --git a/langfuse/_task_manager/media_upload_consumer.py b/langfuse/_task_manager/media_upload_consumer.py index b9058066b..b81b3aaad 100644 --- a/langfuse/_task_manager/media_upload_consumer.py +++ b/langfuse/_task_manager/media_upload_consumer.py @@ -31,7 +31,9 @@ def __init__( def run(self) -> None: """Run the media upload consumer.""" logger.debug( - f"Thread: Media upload consumer thread #{self._identifier} started and actively processing queue items" + "Thread: Media upload consumer thread #%s started and actively processing " + "queue items", + self._identifier, ) while self.running: self._media_manager.process_next_media_upload() @@ -39,6 +41,6 @@ def run(self) -> None: def pause(self) -> None: """Pause the media upload consumer.""" logger.debug( - f"Thread: Pausing media upload consumer thread #{self._identifier}" + "Thread: Pausing media upload consumer thread #%s", self._identifier ) self.running = False diff --git a/langfuse/_task_manager/score_ingestion_consumer.py b/langfuse/_task_manager/score_ingestion_consumer.py index ea5c2b34e..1dce00f80 100644 --- a/langfuse/_task_manager/score_ingestion_consumer.py +++ b/langfuse/_task_manager/score_ingestion_consumer.py @@ -87,7 +87,9 @@ def _next(self) -> list: json.dumps(event, cls=EventSerializer) except Exception as e: logger.error( - f"Data error: Failed to serialize score object for ingestion. Score will be dropped. Error: {e}" + "Data error: Failed to serialize score object for ingestion. Score will " + "be dropped. Error: %s", + e, ) self._ingestion_queue.task_done() @@ -98,7 +100,10 @@ def _next(self) -> list: total_size += item_size if total_size >= MAX_BATCH_SIZE_BYTES: logger.debug( - f"Batch management: Reached maximum batch size limit ({total_size} bytes). Processing {len(events)} events now." + "Batch management: Reached maximum batch size limit (%s bytes). " + "Processing %s events now.", + total_size, + len(events), ) break @@ -107,7 +112,10 @@ def _next(self) -> list: except Exception as e: logger.warning( - f"Data processing error: Failed to process score event in consumer thread #{self._identifier}. Event will be dropped. Error: {str(e)}", + "Data processing error: Failed to process score event in consumer thread " + "#%s. Event will be dropped. Error: %s", + self._identifier, + str(e), exc_info=True, ) self._ingestion_queue.task_done() @@ -121,7 +129,11 @@ def _get_item_size(self, item: Any) -> int: def run(self) -> None: """Run the consumer.""" logger.debug( - f"Startup: Score ingestion consumer thread #{self._identifier} started with batch size {self._flush_at} and interval {self._flush_interval}s" + "Startup: Score ingestion consumer thread #%s started with batch size %s " + "and interval %ss", + self._identifier, + self._flush_at, + self._flush_interval, ) while self.running: self.upload() @@ -153,7 +165,7 @@ def pause(self) -> None: def _upload_batch(self, batch: List[Any]) -> None: logger.debug( - f"API: Uploading batch of {len(batch)} score events to Langfuse API" + "API: Uploading batch of %s score events to Langfuse API", len(batch) ) metadata = ScoreIngestionMetadata( @@ -181,5 +193,6 @@ def execute_task_with_backoff(batch: List[Any]) -> None: execute_task_with_backoff(batch) logger.debug( - f"API: Successfully sent {len(batch)} score events to Langfuse API in batch mode" + "API: Successfully sent %s score events to Langfuse API in batch mode", + len(batch), ) diff --git a/langfuse/_utils/error_logging.py b/langfuse/_utils/error_logging.py index 4e3abac92..5d4fd5888 100644 --- a/langfuse/_utils/error_logging.py +++ b/langfuse/_utils/error_logging.py @@ -12,7 +12,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: try: return func(*args, **kwargs) except Exception as e: - logger.error(f"An error occurred in {func.__name__}: {e}", exc_info=True) + logger.error("An error occurred in %s: %s", func.__name__, e, exc_info=True) return wrapper diff --git a/langfuse/_utils/prompt_cache.py b/langfuse/_utils/prompt_cache.py index 7d9c2298b..e286dca49 100644 --- a/langfuse/_utils/prompt_cache.py +++ b/langfuse/_utils/prompt_cache.py @@ -54,14 +54,17 @@ def run(self) -> None: break logger.debug( - f"PromptCacheRefreshConsumer processing task, {self._identifier}" + "PromptCacheRefreshConsumer processing task, %s", self._identifier ) try: task() # Task failed, but we still consider it processed except Exception as e: logger.warning( - f"PromptCacheRefreshConsumer encountered an error, cache was not refreshed: {self._identifier}, {e}" + "PromptCacheRefreshConsumer encountered an error, cache was not " + "refreshed: %s, %s", + self._identifier, + e, ) self._queue.task_done() @@ -95,13 +98,13 @@ def __init__(self, threads: int = 1): def add_task(self, key: str, task: Callable[[], None]) -> None: with self._lock: if key not in self._processing_keys: - logger.debug(f"Adding prompt cache refresh task for key: {key}") + logger.debug("Adding prompt cache refresh task for key: %s", key) self._processing_keys.add(key) wrapped_task = self._wrap_task(key, task) self._queue.put((wrapped_task)) else: logger.debug( - f"Prompt cache refresh task already submitted for key: {key}" + "Prompt cache refresh task already submitted for key: %s", key ) def active_tasks(self) -> int: @@ -113,19 +116,20 @@ def wait_for_idle(self) -> None: def _wrap_task(self, key: str, task: Callable[[], None]) -> Callable[[], None]: def wrapped() -> None: - logger.debug(f"Refreshing prompt cache for key: {key}") + logger.debug("Refreshing prompt cache for key: %s", key) try: task() finally: with self._lock: self._processing_keys.remove(key) - logger.debug(f"Refreshed prompt cache for key: {key}") + logger.debug("Refreshed prompt cache for key: %s", key) return wrapped def shutdown(self) -> None: logger.debug( - f"Shutting down prompt refresh task manager, {len(self._consumers)} consumers,..." + "Shutting down prompt refresh task manager, %s consumers,...", + len(self._consumers), ) atexit.unregister(self.shutdown) @@ -184,7 +188,7 @@ def invalidate(self, prompt_name: str) -> None: del self._cache[key] def add_refresh_prompt_task(self, key: str, fetch_func: Callable[[], None]) -> None: - logger.debug(f"Submitting refresh task for key: {key}") + logger.debug("Submitting refresh task for key: %s", key) self._task_manager.add_task(key, fetch_func) def add_refresh_prompt_task_if_current( @@ -201,7 +205,8 @@ def add_refresh_prompt_task_if_current( and not current_item.is_expired() ): logger.debug( - f"Skipping refresh task for key: {key} because cache is already fresh." + "Skipping refresh task for key: %s because cache is already fresh.", + key, ) return diff --git a/langfuse/_utils/serializer.py b/langfuse/_utils/serializer.py index 53889f29a..46188fce5 100644 --- a/langfuse/_utils/serializer.py +++ b/langfuse/_utils/serializer.py @@ -184,7 +184,8 @@ def _default_inner(self, obj: Any) -> Any: except Exception as e: logger.debug( - f"Serialization failed for object of type {type(obj).__name__}", + "Serialization failed for object of type %s", + type(obj).__name__, exc_info=e, ) return f'""' diff --git a/langfuse/batch_evaluation.py b/langfuse/batch_evaluation.py index d28fd085d..b75f0b3d2 100644 --- a/langfuse/batch_evaluation.py +++ b/langfuse/batch_evaluation.py @@ -923,13 +923,14 @@ async def run_async( last_item_id: Optional[str] = None if verbose: - logger.info(f"Starting batch evaluation on {scope}") + logger.info("Starting batch evaluation on %s", scope) if scope == "traces" and fetch_trace_fields: - logger.info(f"Fetching trace fields: {fetch_trace_fields}") + logger.info("Fetching trace fields: %s", fetch_trace_fields) if resume_from: logger.info( - f"Resuming from {resume_from.last_processed_timestamp} " - f"({resume_from.items_processed} items already processed)" + "Resuming from %s (%s items already processed)", + resume_from.last_processed_timestamp, + resume_from.items_processed, ) # Main pagination loop @@ -937,7 +938,7 @@ async def run_async( # Check if we've reached max_items if max_items is not None and total_items_fetched >= max_items: if verbose: - logger.info(f"Reached max_items limit ({max_items})") + logger.info("Reached max_items limit (%s)", max_items) has_more = True # More items may exist break @@ -954,7 +955,7 @@ async def run_async( except Exception as e: # Failed after max_retries - create resume token and return error_msg = f"Failed to fetch batch after {max_retries} retries" - logger.error(f"{error_msg}: {e}") + logger.error("%s: %s", error_msg, e) resume_token = BatchEvaluationResumeToken( scope=scope, @@ -991,7 +992,7 @@ async def run_async( total_items_fetched += len(items) if verbose: - logger.info(f"Fetched batch {page} ({len(items)} items)") + logger.info("Fetched batch %s (%s items)", page, len(items)) # Limit items if max_items would be exceeded items_to_process = items @@ -1001,8 +1002,9 @@ async def run_async( items_to_process = items[:remaining_capacity] if verbose: logger.info( - f"Limiting batch to {len(items_to_process)} items " - f"to respect max_items={max_items}" + "Limiting batch to %s items to respect max_items=%s", + len(items_to_process), + max_items, ) # Process items concurrently @@ -1039,7 +1041,7 @@ async def process_item( failed_item_ids.append(item_id) error_type = type(result).__name__ error_summary[error_type] = error_summary.get(error_type, 0) + 1 - logger.warning(f"Item {item_id} failed: {result}") + logger.warning("Item %s failed: %s", item_id, result) else: # Item processed successfully total_items_processed += 1 @@ -1075,13 +1077,17 @@ async def process_item( if max_items is not None and max_items > 0: progress_pct = total_items_processed / max_items * 100 logger.info( - f"Progress: {total_items_processed}/{max_items} items " - f"({progress_pct:.1f}%), {total_scores_created} scores created" + "Progress: %s/%s items (%.1f%%), %s scores created", + total_items_processed, + max_items, + progress_pct, + total_scores_created, ) else: logger.info( - f"Progress: {total_items_processed} items processed, " - f"{total_scores_created} scores created" + "Progress: %s items processed, %s scores created", + total_items_processed, + total_scores_created, ) # Check if we should continue to next page @@ -1106,8 +1112,9 @@ async def process_item( if verbose: logger.info( - f"Batch evaluation complete: {total_items_processed} items processed " - f"in {duration:.2f}s" + "Batch evaluation complete: %s items processed in %.2fs", + total_items_processed, + duration, ) # Completed successfully if we either: @@ -1246,8 +1253,10 @@ async def _process_batch_evaluation_item( stats.failed_runs += 1 evaluations_failed += 1 logger.warning( - f"Evaluator {evaluator_name} failed on item " - f"{self._get_item_id(item, scope)}: {e}" + "Evaluator %s failed on item %s: %s", + evaluator_name, + self._get_item_id(item, scope), + e, ) # Create scores for item-level evaluations @@ -1293,7 +1302,7 @@ async def _process_batch_evaluation_item( evaluations.extend(composite_evals) except Exception as e: - logger.warning(f"Composite evaluator failed on item {item_id}: {e}") + logger.warning("Composite evaluator failed on item %s: %s", item_id, e) return ( scores_created, @@ -1493,12 +1502,12 @@ def _build_timestamp_filter( filter_list = json.loads(original_filter) if original_filter else [] if not isinstance(filter_list, list): logger.warning( - f"Filter should be a JSON array, got: {type(filter_list).__name__}" + "Filter should be a JSON array, got: %s", type(filter_list).__name__ ) filter_list = [] except json.JSONDecodeError: logger.warning( - f"Invalid JSON in original filter, ignoring: {original_filter}" + "Invalid JSON in original filter, ignoring: %s", original_filter ) filter_list = [] diff --git a/langfuse/experiment.py b/langfuse/experiment.py index 840027cb7..aa5481829 100644 --- a/langfuse/experiment.py +++ b/langfuse/experiment.py @@ -1017,7 +1017,7 @@ async def _run_evaluator( raise evaluator_name = getattr(evaluator, "__name__", "unknown_evaluator") - logger.error(f"Evaluator {evaluator_name} failed: {e}") + logger.error("Evaluator %s failed: %s", evaluator_name, e) return [] diff --git a/langfuse/langchain/CallbackHandler.py b/langfuse/langchain/CallbackHandler.py index 43e93b385..f6126a066 100644 --- a/langfuse/langchain/CallbackHandler.py +++ b/langfuse/langchain/CallbackHandler.py @@ -201,7 +201,7 @@ def on_llm_new_token( ) -> Any: """Run on new LLM token. Only available when streaming is enabled.""" langfuse_logger.debug( - f"on llm new token: run_id: {run_id} parent_run_id: {parent_run_id}" + "on llm new token: run_id: %s parent_run_id: %s", run_id, parent_run_id ) if ( run_id in self._runs @@ -1498,7 +1498,10 @@ def _log_debug_event( **kwargs: Any, ) -> None: langfuse_logger.debug( - f"Event: {event_name}, run_id: {run_id}, parent_run_id: {parent_run_id}" + "Event: %s, run_id: %s, parent_run_id: %s", + event_name, + run_id, + parent_run_id, ) diff --git a/langfuse/media.py b/langfuse/media.py index d77de188d..6410079ab 100644 --- a/langfuse/media.py +++ b/langfuse/media.py @@ -178,7 +178,7 @@ def _read_file(self, file_path: str) -> Optional[bytes]: with open(file_path, "rb") as file: return file.read() except Exception as e: - logger.error(f"Error reading file at path {file_path}", exc_info=e) + logger.error("Error reading file at path %s", file_path, exc_info=e) return None @@ -399,7 +399,9 @@ def traverse(obj: Any, depth: int) -> Any: ) except Exception as e: logger.warning( - f"Error fetching media content for reference string {reference_string}: {e}" + "Error fetching media content for reference string %s: %s", + reference_string, + e, ) # Do not replace the reference string if there's an error continue diff --git a/langfuse/openai.py b/langfuse/openai.py index 94d7d1393..3e480f541 100644 --- a/langfuse/openai.py +++ b/langfuse/openai.py @@ -1242,7 +1242,7 @@ def _unwrap_raw_response(openai_response: Any) -> Any: if isinstance(openai_response, (LegacyAPIResponse, APIResponse)): return openai_response.parse() except Exception as e: - logger.debug(f"Failed to parse raw OpenAI response for tracing: {e}") + logger.debug("Failed to parse raw OpenAI response for tracing: %s", e) return openai_response diff --git a/pyproject.toml b/pyproject.toml index 14162009d..733be2ce3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,7 @@ target-version = "py310" [tool.ruff.lint] extend-select = [ "I", # Import formatting + "G004", # No f-strings in logging calls; pass lazy %-style args instead # These used to be enabled in the "local ruff.toml", # which was never enforced; hence, enabling these will # cause a large number of linting errors. @@ -127,6 +128,8 @@ extend-select = [ # "D401", # Enforce imperative mood in docstrings ] exclude = ["langfuse/api/**/*.py"] +# Let G004 see the shared logger, which is re-exported rather than built per module +logger-objects = ["langfuse.logger.langfuse_logger"] [tool.ruff.lint.pydocstyle] convention = "google"