Skip to content

feat(plugins): port DebugLoggingPlugin from adk-python - #1432

Open
svetanis wants to merge 1 commit into
google:mainfrom
svetanis:feature/debug-logging-plugin
Open

feat(plugins): port DebugLoggingPlugin from adk-python#1432
svetanis wants to merge 1 commit into
google:mainfrom
svetanis:feature/debug-logging-plugin

Conversation

@svetanis

Copy link
Copy Markdown
Contributor

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

2. Or, if no issue exists, describe the change:

Problem:

When an agent misbehaves, adk-java has no way to capture what happened in a form you can read
afterwards or attach to a bug report. The contents sent to the model, what came back, each tool call
and its result, the events the runner yielded — all of it is observable only while the run is
happening, and nothing produces a structured record. adk-python ships DebugLoggingPlugin for
exactly this; adk-java has no equivalent.

Solution:

A port of that plugin. New files in a new com.google.adk.plugins.debuglogging subpackage, no
existing file modified
, no existing API changed, and no new dependency
jackson-dataformat-yaml is already a compile-scope dependency of core. Registration uses the
surface that is already there:

Runner runner =
    new InMemoryRunner(
        agent, "my-app", ImmutableList.of(new DebugLoggingPlugin(Path.of("adk_debug.yaml"))));

Twelve new files, the same count plugins/agentanalytics already ships:

New file(s) Concern
DebugLoggingPlugin.java the twelve hooks; each body records one entry and returns
DebugYamlWriter.java append one YAML document per invocation, serialized by a lock, and how a mapper is configured for traces
DebugEntry.java, InvocationDebugState.java, DebugTraceRecorder.java one entry, one invocation's accumulator, and the live-invocation map that timestamped entries are filed into
TracePayload.java (sealed, + 8 nested) the closed set of shapes an entry's data can take
ContentTrace.java (nesting PartTrace and its 6 part kinds), EventTrace.java, LlmRequestTrace.java, LlmResponseTrace.java, UsageTrace.java the curated shape of each ADK/genai type as it appears in a trace
SafeSerializer.java arbitrary tool values made YAML-safe without ever throwing

The PR adds exactly one public type, DebugLoggingPlugin. Everything else is package-private.

All twelve hooks are observe-only — eleven return Maybe.empty(), and afterRunCallback returns
a Completable that only writes the file. Failures inside are logged, not thrown.

The trace curates rather than dumps, which is why this is not a generic ObjectMapper over ADK's
types. Tool declarations are recorded as names only; inline data as mime_type with
_data_omitted: true and no component for the bytes at all; grounding metadata as
has_grounding_metadata: true; requested auth configs as a count. Each matches upstream.

One difference from adk-python. onUserMessageCallback fires before beforeRunCallback, and
upstream opens its per-invocation state only in the later hook, so its user_message entry is
dropped. Here either hook opens the invocation, so the user's message is recorded. A test and a demo
row pin it.

On the write path:

  • The write is off the calling thread but still ordered. afterRunCallback returns
    Completable, so the write runs on Schedulers.io(); Runner.runAgentWithUpdatedSession joins it
    onto the event stream with concatWith, so a caller that drains runAsync has the file on disk
    when it returns.
  • The writer is synchronized. Two invocations finishing together would otherwise interleave
    their documents into a file that no longer parses — a wide window on the JVM, with the write on
    Schedulers.io().

Ported from adk-python/src/google/adk/plugins/debug_logging_plugin.py (572 lines).

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.
mvn -o -pl core test -Dtest='com.google.adk.plugins.debuglogging.*Test'
Tests run: 97, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS

Ninety-seven tests, all new — these are new classes, so there is no before/after state to contrast.
They assert on serialized output, not on intermediate maps, and the writer and plugin tests
assert on the re-parsed YAML, because "the trace can be read back" is the actual promise:

Test class Pins
EventTraceTest (17) all thirteen envelope components on their wire keys; an absent streaming flag omitted rather than written false; the actions block only when non-empty; auth configs as a count; the live action maps copied
LlmExchangeTraceTest (15) request and response shape; tool names only; includeSystemInstruction=false records presence, not text; response schema and grounding metadata each reduced to a boolean
SafeSerializerTest (12) an arbitrary tool value never throws; nulls never reach an immutable collection; cycles are cut instead of overflowing the stack
ContentTraceTest (12) all six part kinds on their wire keys; blank text and empty parts dropped; inline data keeps its mime type, never its bytes
TracePayloadTest (12) the eight payload shapes on their wire keys; data: {} for markers; args: {} kept when a tool takes none
DebugLoggingPluginTest (9) all twelve hooks record, in the order a real run fires them; the Maybe hooks return empty; the user message survives arriving before the run opens; both config switches; the state is released after the write
DebugTraceRecorderTest (9) entry field order; an entry for an unknown invocation is dropped, not thrown; entries() is a snapshot, not the live queue; a session without app name or user id omits those keys
DebugYamlWriterTest (8) two invocations, two documents, both re-parse; one --- each; all four scalar-lookalikes survive as strings; multi-line text as a literal block; an unwritable path logs instead of throwing; eight threads appending at once still yield eight intact documents
UsageTraceTest (3) three token counts for an event, four for a response — the cached-content count present in one and absent in the other

Manual End-to-End (E2E) Tests:

One real invocation against gemini-2.5-flash through an InMemoryRunner, with one tool. Two model
calls, 15 entries, one document — excerpt:

---
invocation_id: e-3b988d1a-5abc-4345-b1c1-acd6c5c84516
session_id: 339cf111-94ac-4014-b737-e3c4250f81d1
app_name: f15-debug-logging
user_id: demo-user
start_time: 2026-08-14T13:55:53.934
entries:
- timestamp: 2026-08-14T13:55:53.941
  entry_type: user_message
  invocation_id: e-3b988d1a-5abc-4345-b1c1-acd6c5c84516
  data:
    content:
      role: user
      parts:
      - text: "Where is order 42? Use the lookupOrder tool, then tell me in one sentence."
- timestamp: 2026-08-14T13:55:54.000
  entry_type: llm_request
  invocation_id: e-3b988d1a-5abc-4345-b1c1-acd6c5c84516
  agent_name: debug-logging-demo-agent
  data:
    model: gemini-2.5-flash
    content_count: 1
    contents:
    - role: user
      parts:
      - text: "Where is order 42? Use the lookupOrder tool, then tell me in one sentence."
    tools:
    - lookupOrder
- timestamp: 2026-08-14T13:55:55.547
  entry_type: tool_call
  invocation_id: e-3b988d1a-5abc-4345-b1c1-acd6c5c84516
  agent_name: debug-logging-demo-agent
  data:
    tool_name: lookupOrder
    function_call_id: adk-5f689fbe-8bbb-465d-b516-6303c70c2adf
    args:
      orderId: "42"
- timestamp: 2026-08-14T13:55:55.554
  entry_type: tool_response
  invocation_id: e-3b988d1a-5abc-4345-b1c1-acd6c5c84516
  agent_name: debug-logging-demo-agent
  data:
    tool_name: lookupOrder
    function_call_id: adk-5f689fbe-8bbb-465d-b516-6303c70c2adf
    result:
      orderId: "42"
      status: shipped
      carrier: DHL

Eight further scenarios were run offline against scripted models, covering the entry sequence, both
tool-failure shapes, a model error, append across two invocations, two invocations racing on one
file, a blob in the conversation, and the configuration switches.

Checklist

  • I have read the CONTRIBUTING.md document.
  • My pull request contains a single commit.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Port DebugLoggingPlugin from adk-python

1 participant