From d9bb0726951d475a4433cb6c46bc0c674ca31daa Mon Sep 17 00:00:00 2001 From: etserend Date: Mon, 24 Aug 2026 20:28:41 -0500 Subject: [PATCH 1/7] fix(docs): correct stale flush() docstrings and restore before_galileo examples (SAO-16166) Co-Authored-By: Claude Opus 4.7 --- splunk-ao-migration-tool/README.md | 1 + splunk-ao-migration-tool/examples/before_galileo.py | 1 + src/splunk_ao/decorator.py | 8 +++----- src/splunk_ao/utils/singleton.py | 9 ++++----- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/splunk-ao-migration-tool/README.md b/splunk-ao-migration-tool/README.md index 672d00d5..d0ee644f 100644 --- a/splunk-ao-migration-tool/README.md +++ b/splunk-ao-migration-tool/README.md @@ -391,6 +391,7 @@ logger = GalileoLogger(project="my-project", log_stream="production") logger.start_session(name="my-session") logger.add_llm_span(input="Hello", output="Hi", model="gpt-4") logger.conclude() # closes current span; no flush kwarg +logger.flush() # uploads traces ``` ### After (splunk-ao) diff --git a/splunk-ao-migration-tool/examples/before_galileo.py b/splunk-ao-migration-tool/examples/before_galileo.py index 3c70b763..7f7c1433 100644 --- a/splunk-ao-migration-tool/examples/before_galileo.py +++ b/splunk-ao-migration-tool/examples/before_galileo.py @@ -21,3 +21,4 @@ def call_llm(prompt: str) -> str: logger.start_session(name="my-session") logger.add_llm_span(input="Hello", output="Hi", model="gpt-4") logger.conclude() # closes current span; no flush kwarg +logger.flush() # uploads traces diff --git a/src/splunk_ao/decorator.py b/src/splunk_ao/decorator.py index fb02472b..44e9da69 100644 --- a/src/splunk_ao/decorator.py +++ b/src/splunk_ao/decorator.py @@ -1309,9 +1309,9 @@ def flush( on_error: Callable[[Exception], None] | None = None, ) -> None: """ - Upload all captured traces under a project and agent stream context to Splunk AO. + Drain completed spans for the given project and agent stream context; does not conclude open spans. - If no project or agent stream is provided, then the currently initialized context is used. + Falls back to the currently initialized context when no project or agent stream is provided. Parameters ---------- @@ -1349,9 +1349,7 @@ def _on_flush_error(exc: Exception) -> None: def flush_all(self) -> None: """ - Upload all captured traces under all contexts to Splunk AO. - - This method flushes all traces regardless of project or log stream. + Drain completed spans across all contexts; does not conclude open spans. """ SplunkAOLoggerSingleton().flush_all() diff --git a/src/splunk_ao/utils/singleton.py b/src/splunk_ao/utils/singleton.py index 99c00017..71dbc62d 100644 --- a/src/splunk_ao/utils/singleton.py +++ b/src/splunk_ao/utils/singleton.py @@ -295,11 +295,10 @@ def flush( agent_stream_id: str | None = None, ) -> None: """ - Flush (upload and clear) a SplunkAOLogger instance. + Drain completed spans for a SplunkAOLogger instance; does not conclude open spans. - If both project and agent_stream are None, then all cached loggers are flushed - and cleared. Otherwise, only the specific logger corresponding to the provided - key (project, agent_stream) is flushed and removed. + If both project and agent_stream are None all cached loggers are drained, + otherwise only the logger for the given (project, agent_stream) key is drained. Parameters ---------- @@ -331,7 +330,7 @@ def flush( self._splunk_ao_loggers[key].flush() def flush_all(self) -> None: - """Flush (upload and clear) all SplunkAOLogger instances.""" + """Drain completed spans for all SplunkAOLogger instances; does not conclude open spans.""" with self._lock: # Terminate and clear all logger instances. for logger in self._splunk_ao_loggers.values(): From 5f9b0161ee6d094d73d5fbfdbf5d444585e5c1fb Mon Sep 17 00:00:00 2001 From: Fernando Correia Date: Wed, 26 Aug 2026 17:25:18 -0700 Subject: [PATCH 2/7] fix(docs): correct flush() teardown and hook-path docstrings (SAO-16166) The drain-only rewording in the previous commit left three claims that do not match the code. Teardown: SAO-16166 asked each docstring to name terminate() as the teardown call, but terminate() is defined only on SplunkAOLogger. Neither SplunkAOLoggerSingleton nor SplunkAODecorator exposes it, so that advice would raise AttributeError if followed. Point at each layer's real teardown instead -- reset()/reset_all() on the singleton, reset() on the decorator -- and note that atexit already terminates loggers at interpreter exit, so an explicit call is for deterministic shutdown rather than an obligation. SplunkAODecorator.flush_all() names none, since the decorator has no reset_all() and reset() only covers the current context. Hook path: flush() is not drain-only when an ingestion hook is configured. It concludes open spans, computes local metrics, delivers the accumulated traces to the hook, and clears them. Document flush() and async_flush() as two egress paths and carry the caveat into the wrappers that can reach a hook-backed logger. SplunkAODecorator.flush() is excluded on purpose: it resolves a logger without forwarding an ingestion hook, so drain-only holds there unconditionally. Scoping: SplunkAOLoggerSingleton.flush() never drained every cached logger. The cache key includes the thread name and mode, and routing falls back to deployment defaults, so a bare call is far narrower than documented. Also drop a stale "terminate and clear" comment in flush_all() that contradicted the docstring above it. The three-line reformat in logger.py's constructor is pre-existing and was applied by the ruff-format hook. Co-Authored-By: Claude --- src/splunk_ao/decorator.py | 13 +++++++++++-- src/splunk_ao/logger/logger.py | 27 +++++++++++++++++++++------ src/splunk_ao/utils/singleton.py | 26 +++++++++++++++++++++----- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/splunk_ao/decorator.py b/src/splunk_ao/decorator.py index 44e9da69..8f7d74eb 100644 --- a/src/splunk_ao/decorator.py +++ b/src/splunk_ao/decorator.py @@ -1309,10 +1309,13 @@ def flush( on_error: Callable[[Exception], None] | None = None, ) -> None: """ - Drain completed spans for the given project and agent stream context; does not conclude open spans. + Drain completed spans for the resolved project and agent stream context; does not conclude open spans. Falls back to the currently initialized context when no project or agent stream is provided. + Draining is not a shutdown: exporters stay open and the logger stays cached. Use + ``reset()`` to terminate the current context's loggers and clear the context. + Parameters ---------- project @@ -1349,7 +1352,13 @@ def _on_flush_error(exc: Exception) -> None: def flush_all(self) -> None: """ - Drain completed spans across all contexts; does not conclude open spans. + Drain completed spans for every logger across all contexts. + + Open spans are left unconcluded, except for hook-backed loggers, which conclude any + open spans on the active trace before handing it off. + + Draining is not a shutdown: exporters stay open and every cached logger is retained. + Loggers terminate via their ``atexit`` hooks at interpreter exit. """ SplunkAOLoggerSingleton().flush_all() diff --git a/src/splunk_ao/logger/logger.py b/src/splunk_ao/logger/logger.py index f7213c3b..5ee40aab 100644 --- a/src/splunk_ao/logger/logger.py +++ b/src/splunk_ao/logger/logger.py @@ -367,7 +367,9 @@ def __init__( "User must provide project_name or project_id to SplunkAOLogger, or set it as an environment variable." ) if self.experiment_id is None and self.agent_stream_name is None and self.agent_stream_id is None: - raise SplunkAOLoggerException("agent_stream or agent_stream_id is required to initialize SplunkAOLogger.") + raise SplunkAOLoggerException( + "agent_stream or agent_stream_id is required to initialize SplunkAOLogger." + ) if local_metrics: self.local_metrics = local_metrics @@ -2216,7 +2218,18 @@ def conclude( @nop_sync def flush(self, on_error: Callable[[Exception], None] | None = None) -> None: """ - Drain completed spans waiting in the batch processor. + Drain telemetry that is ready to export. + + Behavior depends on the egress path: + + - OTLP export (default): drains completed spans waiting in the batch processor. + Open spans are left alone; unconcluded steps are not converted or emitted. + - Ingestion hook: concludes any open spans on the active trace, computes local + metrics if configured, hands the accumulated traces to the hook, and clears + them. Distributed stub traces are left unconcluded. + + Neither path shuts down owned resources; call ``terminate()`` during application + teardown. Parameters ---------- @@ -2225,9 +2238,6 @@ def flush(self, on_error: Callable[[Exception], None] | None = None) -> None: is passed to the callback instead of being logged as a warning. The callback itself is protected: if it raises, the exception is logged as a warning. Defaults to None (swallow and log warning). - - Unconcluded steps are not converted or emitted. This method does not - shut down the processor; call ``terminate()`` during application teardown. """ try: if self._ingestion_hook: @@ -2254,7 +2264,12 @@ def flush(self, on_error: Callable[[Exception], None] | None = None) -> None: @nop_async @async_warn_catch_exception(exceptions=(Exception,)) async def async_flush(self) -> None: - """Drain completed spans without blocking the caller's event loop.""" + """ + Drain telemetry that is ready to export without blocking the caller's event loop. + + Path-dependent behavior matches ``flush()``, including concluding open spans and + clearing accumulated traces when an ingestion hook is configured. + """ if self._ingestion_hook: await self._flush_batch() return diff --git a/src/splunk_ao/utils/singleton.py b/src/splunk_ao/utils/singleton.py index 71dbc62d..9735cb8c 100644 --- a/src/splunk_ao/utils/singleton.py +++ b/src/splunk_ao/utils/singleton.py @@ -295,10 +295,18 @@ def flush( agent_stream_id: str | None = None, ) -> None: """ - Drain completed spans for a SplunkAOLogger instance; does not conclude open spans. + Drain completed spans for the matching cached SplunkAOLogger instances. - If both project and agent_stream are None all cached loggers are drained, - otherwise only the logger for the given (project, agent_stream) key is drained. + With no arguments, drains the loggers registered for the current thread whose mode and + resolved project/agent stream match the active defaults — not every cached logger. + Passing a project or agent stream narrows this to the loggers matching that key. + + Open spans are left unconcluded, except for hook-backed loggers, which conclude any + open spans on the active trace before handing it off. + + Draining is not a shutdown: exporters stay open and the loggers remain cached. Use + ``reset()`` or ``reset_all()`` to terminate and evict them; otherwise each logger + terminates via its ``atexit`` hook at interpreter exit. Parameters ---------- @@ -330,9 +338,17 @@ def flush( self._splunk_ao_loggers[key].flush() def flush_all(self) -> None: - """Drain completed spans for all SplunkAOLogger instances; does not conclude open spans.""" + """ + Drain completed spans for every cached SplunkAOLogger instance. + + Open spans are left unconcluded, except for hook-backed loggers, which conclude any + open spans on the active trace before handing it off. + + Draining is not a shutdown: exporters stay open and the loggers remain cached. Use + ``reset_all()`` to terminate and evict them; otherwise each logger terminates via its + ``atexit`` hook at interpreter exit. + """ with self._lock: - # Terminate and clear all logger instances. for logger in self._splunk_ao_loggers.values(): logger.flush() From fc6b9bec430cd10a70b3c74e3039cf3a25caaf1a Mon Sep 17 00:00:00 2001 From: Fernando Correia Date: Wed, 26 Aug 2026 17:31:09 -0700 Subject: [PATCH 3/7] style(examples): apply ruff import sorting and UP015 to chatbot samples These files predate the current ruff config and trip the pre-commit hooks as soon as they are touched. Landing the automated fixes on their own keeps the churn out of the docs change that follows. All hook-applied: isort ordering, the redundant "r" mode in open(), and one collapsed call. No behavior or wording changes. Co-Authored-By: Claude --- .../chatbot/sample-project-chatbot/anthropic/app.py | 10 +++------- .../sample-project-chatbot/azure-inference/app.py | 7 +++---- .../sample-project-chatbot/openai-ollama/app.py | 4 ++-- .../openai-ollama/create_sample_logs.py | 10 +++++----- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/examples/chatbot/sample-project-chatbot/anthropic/app.py b/examples/chatbot/sample-project-chatbot/anthropic/app.py index 0b5b7128..b3b4983a 100644 --- a/examples/chatbot/sample-project-chatbot/anthropic/app.py +++ b/examples/chatbot/sample-project-chatbot/anthropic/app.py @@ -25,14 +25,13 @@ """ -from datetime import datetime import os +from datetime import datetime from anthropic import Anthropic - from dotenv import load_dotenv -from splunk_ao import splunk_ao_context, log +from splunk_ao import log, splunk_ao_context # Load the environment variables from the .env file # This will override any existing environment variables with the same name @@ -102,10 +101,7 @@ def send_chat_to_anthropic() -> str: # Send the chat history to the Anthropic API and get the response response = client.messages.create( - max_tokens=1024, - messages=chat_history_anthropic, - system=system_prompt, - model=MODEL_NAME, + max_tokens=1024, messages=chat_history_anthropic, system=system_prompt, model=MODEL_NAME ) # Print the response to the console diff --git a/examples/chatbot/sample-project-chatbot/azure-inference/app.py b/examples/chatbot/sample-project-chatbot/azure-inference/app.py index dcf9f903..3f6d428f 100644 --- a/examples/chatbot/sample-project-chatbot/azure-inference/app.py +++ b/examples/chatbot/sample-project-chatbot/azure-inference/app.py @@ -26,16 +26,15 @@ """ -from datetime import datetime import os +from datetime import datetime from azure.ai.inference import ChatCompletionsClient -from azure.ai.inference.models import SystemMessage, UserMessage, AssistantMessage +from azure.ai.inference.models import AssistantMessage, SystemMessage, UserMessage from azure.core.credentials import AzureKeyCredential - from dotenv import load_dotenv -from splunk_ao import splunk_ao_context, log +from splunk_ao import log, splunk_ao_context # Load the environment variables from the .env file # This will override any existing environment variables with the same name diff --git a/examples/chatbot/sample-project-chatbot/openai-ollama/app.py b/examples/chatbot/sample-project-chatbot/openai-ollama/app.py index 9b6fa658..a463efe1 100644 --- a/examples/chatbot/sample-project-chatbot/openai-ollama/app.py +++ b/examples/chatbot/sample-project-chatbot/openai-ollama/app.py @@ -28,12 +28,12 @@ """ -from datetime import datetime import os +from datetime import datetime from dotenv import load_dotenv -from splunk_ao import splunk_ao_context, log +from splunk_ao import log, splunk_ao_context from splunk_ao.openai import OpenAI # Load the environment variables from the .env file diff --git a/examples/chatbot/sample-project-chatbot/openai-ollama/create_sample_logs.py b/examples/chatbot/sample-project-chatbot/openai-ollama/create_sample_logs.py index 384afe8c..3696f187 100644 --- a/examples/chatbot/sample-project-chatbot/openai-ollama/create_sample_logs.py +++ b/examples/chatbot/sample-project-chatbot/openai-ollama/create_sample_logs.py @@ -1,20 +1,20 @@ # A script to generate log streams # Load the dataset.json -from datetime import datetime import json import uuid +from datetime import datetime -from splunk_ao import splunk_ao_context - -from app import chat_with_llm, chat_history +from app import chat_history, chat_with_llm # Load environment variables from .env file from dotenv import load_dotenv +from splunk_ao import splunk_ao_context + load_dotenv(override=True) -with open("../dataset.json", "r", encoding="utf-8") as f: +with open("../dataset.json", encoding="utf-8") as f: dataset_content = json.load(f) print(f"Starting to log {len(dataset_content)} interactions...") From d3b26fa88b7bb03dbf35af43506607e3d28e0c34 Mon Sep 17 00:00:00 2001 From: Fernando Correia Date: Wed, 26 Aug 2026 17:31:40 -0700 Subject: [PATCH 4/7] fix(examples): credit conclude() rather than flush() for trace rotation (SAO-16166) The chatbot samples told readers that flushing is what starts the next trace. It is not: flush() leaves trace state alone on the OTLP path, and only clears the current parent when an ingestion hook is configured. What actually frees the next start_trace() at the top of the loop is conclude(). Reword the comment to attribute each call correctly and to say what flush() is really for here -- exporting immediately rather than waiting out the batch processor's schedule delay. Comments only; the surrounding conclude()/flush() calls are unchanged. Co-Authored-By: Claude --- examples/chatbot/sample-project-chatbot/anthropic/app.py | 4 ++-- .../chatbot/sample-project-chatbot/azure-inference/app.py | 4 ++-- examples/chatbot/sample-project-chatbot/openai-ollama/app.py | 4 ++-- .../openai-ollama/create_sample_logs.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/chatbot/sample-project-chatbot/anthropic/app.py b/examples/chatbot/sample-project-chatbot/anthropic/app.py index b3b4983a..b215de78 100644 --- a/examples/chatbot/sample-project-chatbot/anthropic/app.py +++ b/examples/chatbot/sample-project-chatbot/anthropic/app.py @@ -178,8 +178,8 @@ def main() -> None: # Call the chat_with_llm function to get a response from the LLM response = chat_with_llm(user_input) - # Conclude and flush the logger after each interaction - # so that a new trace is started each time + # conclude() ends the trace so the next interaction starts a new one; + # flush() exports it immediately instead of waiting for the batch timer logger.conclude(output=response) logger.flush() diff --git a/examples/chatbot/sample-project-chatbot/azure-inference/app.py b/examples/chatbot/sample-project-chatbot/azure-inference/app.py index 3f6d428f..ab3f3fe1 100644 --- a/examples/chatbot/sample-project-chatbot/azure-inference/app.py +++ b/examples/chatbot/sample-project-chatbot/azure-inference/app.py @@ -182,8 +182,8 @@ def main() -> None: # Call the chat_with_llm function to get a response from the LLM response = chat_with_llm(user_input) - # Conclude and flush the logger after each interaction - # so that a new trace is started each time + # conclude() ends the trace so the next interaction starts a new one; + # flush() exports it immediately instead of waiting for the batch timer logger.conclude(output=response) logger.flush() diff --git a/examples/chatbot/sample-project-chatbot/openai-ollama/app.py b/examples/chatbot/sample-project-chatbot/openai-ollama/app.py index a463efe1..3b6267ba 100644 --- a/examples/chatbot/sample-project-chatbot/openai-ollama/app.py +++ b/examples/chatbot/sample-project-chatbot/openai-ollama/app.py @@ -165,8 +165,8 @@ def main() -> None: # Call the chat_with_llm function to get a response from the LLM response = chat_with_llm(user_input) - # Conclude and flush the logger after each interaction - # so that a new trace is started each time + # conclude() ends the trace so the next interaction starts a new one; + # flush() exports it immediately instead of waiting for the batch timer logger.conclude(output=response) logger.flush() diff --git a/examples/chatbot/sample-project-chatbot/openai-ollama/create_sample_logs.py b/examples/chatbot/sample-project-chatbot/openai-ollama/create_sample_logs.py index 3696f187..29ad774c 100644 --- a/examples/chatbot/sample-project-chatbot/openai-ollama/create_sample_logs.py +++ b/examples/chatbot/sample-project-chatbot/openai-ollama/create_sample_logs.py @@ -44,8 +44,8 @@ # Print the response from the LLM print(f"LLM Response: {response}") - # Conclude and flush the logger after each interaction - # so that a new trace is started each time + # conclude() ends the trace so the next interaction starts a new one; + # flush() exports it immediately instead of waiting for the batch timer logger.conclude(output=response) logger.flush() From 29169dc75b5683a36865b1d507c65486863add7d Mon Sep 17 00:00:00 2001 From: Fernando Correia Date: Wed, 26 Aug 2026 17:35:05 -0700 Subject: [PATCH 5/7] style(migration): strip trailing whitespace flagged by pre-commit The trailing-whitespace hook rewrites this file as soon as it is touched. Landing it alone keeps the churn out of the docs change that follows. Note these two were Markdown hard line breaks, so the affected blockquote and paragraph lines now flow together rather than breaking. They were the only two such breaks in the file, so this normalizes them. Co-Authored-By: Claude --- splunk-ao-migration-tool/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/splunk-ao-migration-tool/README.md b/splunk-ao-migration-tool/README.md index d0ee644f..a75364d1 100644 --- a/splunk-ao-migration-tool/README.md +++ b/splunk-ao-migration-tool/README.md @@ -25,7 +25,7 @@ Additionally there are a handful of **removed features** (Protect, `GalileoScore ### 1.1 Package Availability -> **`splunk-ao` is not yet published to PyPI.** +> **`splunk-ao` is not yet published to PyPI.** > Use one of the two installation methods below until a public release is available. **Option A — Install directly from GitHub (recommended for most users)** @@ -81,7 +81,7 @@ splunk-ao = { path = "../splunk-ao-python", develop = true } ### 1.2 Optional Extra Groups -The extras keys are unchanged (`langchain`, `openai`, `crewai`, `middleware`, `otel`, `all`). +The extras keys are unchanged (`langchain`, `openai`, `crewai`, `middleware`, `otel`, `all`). One new dependency was added to the `otel` and `all` extras: | Extra | Change | From a08a70be97724db31510d286143367dc6b278820 Mon Sep 17 00:00:00 2001 From: Fernando Correia Date: Wed, 26 Aug 2026 17:35:27 -0700 Subject: [PATCH 6/7] docs(migration): document that flush() is no longer required (SAO-16166) Restoring logger.flush() to the galileo "Before" snippet made the delta visible, but the guide never said what changed: flush() appeared nowhere outside the two code blocks in section 7, and section 5 "Removed / Changed Features" did not cover it. A reader's only signal was a line going missing between two 25-line examples. Add 5.4 stating that conclude() enqueues and the atexit hook exports at interpreter exit, so no explicit call is needed -- while being clear that flush() still exists and still exports, for callers who want spans out ahead of the batch timer, and that terminate() is the deterministic shutdown. Also point the "After" block at 5.4, and fix a stale log_stream reference in its comment that should have read agent_stream. Co-Authored-By: Claude --- splunk-ao-migration-tool/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/splunk-ao-migration-tool/README.md b/splunk-ao-migration-tool/README.md index a75364d1..92814966 100644 --- a/splunk-ao-migration-tool/README.md +++ b/splunk-ao-migration-tool/README.md @@ -350,6 +350,17 @@ This file is never read back and has no effect on authentication or config resolution. If you have an existing `~/.galileo/galileo-python-config.json` from `galileo-python`, it can be deleted at leisure or simply ignored. +### 5.4 `flush()` No Longer Required + +In `galileo`, `logger.flush()` uploaded the accumulated traces and was required +before the process exited. In `splunk-ao`, `conclude()` enqueues the span with the +batch processor and an `atexit` hook exports at interpreter exit, so an explicit +call is no longer needed. + +`flush()` still exists and still exports — call it to push spans out immediately +rather than waiting for the batch timer. For deterministic shutdown, call +`terminate()`, which drains and then shuts down the exporter. + --- ## 6. HTTP Tracing Headers @@ -413,11 +424,12 @@ with splunk_ao_context(project="my-project", agent_stream="production"): result = call_llm("Hello") # Direct logger approach -# project/log_stream are constructor args, not start_session args +# project/agent_stream are constructor args, not start_session args logger = SplunkAOLogger(project="my-project", agent_stream="production") logger.start_session(name="my-session") logger.add_llm_span(input="Hello", output="Hi", model="gpt-4") logger.conclude() # closes current span; no flush kwarg +# no explicit flush() required — see 5.4 ``` --- From 4eee22bca94bd783815b129186f5311afb7a97df Mon Sep 17 00:00:00 2001 From: Fernando Correia Date: Wed, 26 Aug 2026 17:45:47 -0700 Subject: [PATCH 7/7] fix(docs): correct reset() lifecycle docs and the cache key description Found while correcting the flush() docstrings: the same wrong mental model had produced two more inaccurate claims about reset(), and the class docstring that seeded them. SplunkAODecorator.reset() claimed it "deletes all traces that haven't been flushed". It does not. reset() terminates the context's loggers, and terminate() calls force_flush() before shutdown(), so anything already concluded is exported. Only spans still open are discarded. SplunkAOLoggerSingleton.reset() claimed it resets "one or all" instances. Like flush(), it prefix-matches a full key, so with no arguments it covers the current thread at the default mode and resolved routing. reset_all() is the "all" API. The prefix match does pull in the per-trace and hook-backed variants, which is worth stating. The class docstring described the cache key as a (project, agent_stream) tuple. It is really (thread name, mode, deployment, project, agent stream or experiment) plus trace and span IDs when present -- which is why "all cached loggers" kept getting written. Documenting the thread component also explains why instances are never shared across threads. Also drop an overstatement in three chatbot examples: flush() was described as ensuring delivery, but it runs with a timeout, degrades failures to a logged warning, and is a no-op under SPLUNK_AO_LOGGING_DISABLED. It exports immediately; it does not guarantee. Co-Authored-By: Claude --- .../sample-project-chatbot/anthropic/app.py | 2 +- .../azure-inference/app.py | 2 +- .../sample-project-chatbot/openai-ollama/app.py | 2 +- src/splunk_ao/decorator.py | 9 +++++++-- src/splunk_ao/utils/singleton.py | 17 ++++++++++++++--- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/examples/chatbot/sample-project-chatbot/anthropic/app.py b/examples/chatbot/sample-project-chatbot/anthropic/app.py index b215de78..5488e595 100644 --- a/examples/chatbot/sample-project-chatbot/anthropic/app.py +++ b/examples/chatbot/sample-project-chatbot/anthropic/app.py @@ -11,7 +11,7 @@ as a workflow span - The call to the LLM is logged manually as an LLM span. - After the response is received, the trace is concluded with the response - and flushed to ensure it is sent to Splunk AO. + and flushed so it is exported immediately. To run this, you will need to have the following environment variables set: - `SPLUNK_AO_API_KEY`: Your Splunk AO API key. diff --git a/examples/chatbot/sample-project-chatbot/azure-inference/app.py b/examples/chatbot/sample-project-chatbot/azure-inference/app.py index ab3f3fe1..e33ad8ef 100644 --- a/examples/chatbot/sample-project-chatbot/azure-inference/app.py +++ b/examples/chatbot/sample-project-chatbot/azure-inference/app.py @@ -11,7 +11,7 @@ as a workflow span - The call to the LLM is logged manually as an LLM span. - After the response is received, the trace is concluded with the response - and flushed to ensure it is sent to Splunk AO. + and flushed so it is exported immediately. To run this, you will need to have the following environment variables set: - `SPLUNK_AO_API_KEY`: Your Splunk AO API key. diff --git a/examples/chatbot/sample-project-chatbot/openai-ollama/app.py b/examples/chatbot/sample-project-chatbot/openai-ollama/app.py index 3b6267ba..9429abc8 100644 --- a/examples/chatbot/sample-project-chatbot/openai-ollama/app.py +++ b/examples/chatbot/sample-project-chatbot/openai-ollama/app.py @@ -12,7 +12,7 @@ - The call to the LLM is logged as an LLM span using the Splunk AO OpenAI integration which logs the span automatically. - After the response is received, the trace is concluded with the response - and flushed to ensure it is sent to Splunk AO. + and flushed so it is exported immediately. To run this, you will need to have the following environment variables set: - `SPLUNK_AO_API_KEY`: Your Splunk AO API key. diff --git a/src/splunk_ao/decorator.py b/src/splunk_ao/decorator.py index 8f7d74eb..911297bf 100644 --- a/src/splunk_ao/decorator.py +++ b/src/splunk_ao/decorator.py @@ -1364,9 +1364,14 @@ def flush_all(self) -> None: def reset(self) -> None: """ - Reset the entire context, which also deletes all traces that haven't been flushed. + Reset the entire context and terminate the loggers for the current context. - This method clears all context variables and resets the logger singleton. + Terminating drains completed spans before shutting the exporter down, so work that + has already concluded is still exported. Spans left open at that point are discarded + rather than exported. + + This method clears all context variables and stacks, and evicts the terminated + loggers from the singleton cache. """ SplunkAOLoggerSingleton().reset( project=_project_context.get(), diff --git a/src/splunk_ao/utils/singleton.py b/src/splunk_ao/utils/singleton.py index 9735cb8c..86311a29 100644 --- a/src/splunk_ao/utils/singleton.py +++ b/src/splunk_ao/utils/singleton.py @@ -20,8 +20,12 @@ class SplunkAOLoggerSingleton: provides a thread-safe way to retrieve or create SplunkAOLogger clients based on the given 'project' and 'agent_stream' parameters. If the parameters are not provided, the class attempts to read the values from the environment variables - SPLUNK_AO_PROJECT and SPLUNK_AO_AGENT_STREAM. The loggers are stored in a dictionary - using a tuple (project, agent_stream) as the key. + SPLUNK_AO_PROJECT and SPLUNK_AO_AGENT_STREAM, falling back to the standalone defaults. + + Loggers are cached under a tuple key built from the calling thread's name, the logger + mode, the deployment, and the resolved project and agent stream (or experiment) + identity, plus the distributed trace and span IDs when present. Because the thread name + is part of the key, instances are never shared across threads. """ _instance = None # Class-level attribute to hold the singleton instance. @@ -244,7 +248,14 @@ def reset( agent_stream_id: str | None = None, ) -> None: """ - Reset (terminate and remove) one or all SplunkAOLogger instances. + Reset (terminate and remove) the SplunkAOLogger instances matching the given key. + + Matching is by key prefix, so a logger's per-trace and hook-backed variants are + included. With no arguments this covers the current thread's loggers at the default + mode and resolved routing, not every cached instance; use ``reset_all()`` for that. + + Terminating drains completed spans before shutting the exporter down. Spans still + open at that point are discarded rather than exported. Parameters ----------