Audit Logging

Thread-safe JSONL audit log for multi-agent tool call tracking

Every tool call can be recorded to an append-only JSONL audit log โ€” thread-safe for concurrent multi-agent writes.

python
from praisonaiagents import Agent

agent = Agent(name="audited", instructions="Log every tool call for compliance.")
agent.start("List recent deployments.")

The user enables audit logging; each tool invocation is appended to JSONL safely even when many agents run in parallel.

Diagram

Audit Logging Flow

Agent 1

threading.Lock

Agent 2

audit.jsonl

How It Works

Each tool call the agent makes is appended to the audit log before the result returns to the user.

Diagram
AuditLogAgentUserAuditLogAgentUserRequestAppend tool call (JSONL)RecordedResponse

Quick Start

  1. Enable audit logging
    python
    from praisonaiagents import Agent
    from praisonai.security import enable_audit_log
    
    enable_audit_log()  # default: ~/.praisonai/audit.jsonl
    
    agent = Agent(
        name="AuditedAgent",
        instructions="Every tool call is logged.",
    )
    agent.start("Summarise today's PRs")
  2. Close on shutdown
    python
    from praisonai.security import get_audit_log
    
    get_audit_log().close()  # flush and release handle

What’s Logged

Each JSONL line records:

  • timestamp, session_id, agent_name
  • tool_name, tool_input, execution_time_ms
  • Optional tool_output (when include_output=True)

The hook registers on after_tool automatically when you call enable_audit_log().


Log Rotation

Audit records keep landing on the live file after an external rotator moves or removes it โ€” no user action, no descriptor leak (PR #4205).

Each write compares the inode behind the open handle against the inode on the log path; when they differ (or the path is gone), the stale handle is closed and the path reopened at 0o600 before the line is appended.

Diagram

Yes

No / missing

๐Ÿ“ Audit write

๐Ÿ” Path inode
== fd inode?

โœ๏ธ Append line

โ™ป๏ธ Close old fd
reopen path 0o600


Secret Redaction

Sensitive values in tool_input are replaced with REDACTED before the JSONL line is written โ€” on by default, no configuration needed.

Diagram

๐Ÿ“ฅ raw tool_input
api_key=sk-123

๐Ÿงผ redactor

โœ… JSONL line
api_key=REDACTED

Key matching is case-insensitive and recurses through nested dicts, lists, and tuples. The built-in denylist (_DEFAULT_SENSITIVE_KEYS in praisonai/security/audit.py):

api_key, apikey, authorization, auth, password, passwd,
secret, token, access_token, refresh_token, bearer,
x-api-key, openai_api_key, anthropic_api_key, cookie

Before / after on disk:

Before redaction
{"tool_name": "call_api", "tool_input": {"url": "https://api.example.com", "api_key": "sk-live-abc123", "password": "hunter2"}}
After redaction (what is written)
{"tool_name": "call_api", "tool_input": {"url": "https://api.example.com", "api_key": "***REDACTED***", "password": "***REDACTED***"}}

Configuration

OptionTypeDefaultDescription
log_pathstr~/.praisonai/audit.jsonlAppend-only JSONL path
include_outputboolFalseInclude truncated tool output
max_output_charsint500Max output chars when include_output=True
redactor`Callable[[Any], Any] \None`built-in denylist redactorApplied to tool_input before writing. Pass None to disable redaction entirely.
sensitive_keys`frozenset[str] \None`_DEFAULT_SENSITIVE_KEYSOverride the denylist. Only takes effect with the default redactor; a custom redactor is used exactly as supplied.
python
enable_audit_log(
    log_path="./my-audit.jsonl",
    include_output=True,
    max_output_chars=1000,
)

Customising Redaction

Instantiate AuditLogHook directly for full control over redaction.

Pick the option that fits your situation:

Diagram

Defaults are enough

Extra key names

Different transform

Turn it off

Redaction need?

Do nothing
built-in denylist

sensitive_keys=frozenset&(…&)

redactor=my_fn

redactor=None

Extend the denylist
from praisonai.security import AuditLogHook
from praisonaiagents.hooks import add_hook

audit = AuditLogHook(
    sensitive_keys=frozenset({
        "api_key", "token", "password",   # keep the important defaults
        "customer_ssn", "internal_secret",  # add your own
    })
)
add_hook("after_tool", audit.create_after_tool_hook())
Custom redactor
import hashlib
from praisonai.security import AuditLogHook
from praisonaiagents.hooks import add_hook

def hash_redactor(obj):
    # A custom redactor replaces the default entirely โ€” recurse yourself.
    if isinstance(obj, dict):
        return {k: (f"sha256:{hashlib.sha256(str(v).encode()).hexdigest()[:12]}"
                    if k.lower() in {"api_key", "token"} else hash_redactor(v))
                for k, v in obj.items()}
    if isinstance(obj, (list, tuple)):
        return type(obj)(hash_redactor(v) for v in obj)
    return obj

add_hook("after_tool",
         AuditLogHook(redactor=hash_redactor).create_after_tool_hook())
Disable redaction
from praisonai.security import AuditLogHook
from praisonaiagents.hooks import add_hook

# NOT recommended in production โ€” secrets are written verbatim.
add_hook("after_tool",
         AuditLogHook(redactor=None).create_after_tool_hook())

sensitive_keys only affects the built-in redactor. A redactor= callable you supply is used exactly as given โ€” the class does not merge it with the default.


Thread Safety (PR #2062)

  • Uses threading.Lock for concurrent multi-agent writes
  • Rotation-safe: each write stats the log path and reopens when the on-disk inode differs from the open handle (recovers from logrotate, mv, or rm mid-run) โ€” added in PR #4205
  • Each write calls fsync for crash durability
  • Call get_audit_log().close() on shutdown to flush and release the handle

Best Practices

Enable early in production

Call enable_audit_log() before creating agents so every tool invocation is captured from the first turn โ€” retrofitting mid-session misses earlier calls.

Close on shutdown

Call get_audit_log().close() in your shutdown handler to flush the file handle. Long-running daemons that skip this may lose the last buffered line on crash.

Keep output logging selective

Leave include_output=False unless you need forensic replay. When enabled, tune max_output_chars to avoid bloating the JSONL with large tool payloads. Redaction covers tool_input only โ€” outputs are written verbatim.

Verify redaction after enabling

Grep your audit file for known-sensitive strings once, especially if your tools use exotic key names not in the default denylist. If you see leaks, extend sensitive_keys or supply a custom redactor on a directly-instantiated AuditLogHook.

Rotate and protect log files

Store logs outside web-served directories. The file is created 0o600 (owner-only) by default, and the audit path is protected โ€” do not disable Protected Paths on production hosts. External rotators (logrotate, container log drivers, cron mv + HUP) are supported directly โ€” the writer reopens the live file on the next write, so you never need to signal the process (PR #4205).

Scope audit logging in multi-tenant hosts

enable_audit_log() registers on the shared praisonaiagents.hooks registry, so every Agent in the same Python process is audited under a single sink and single configuration. This is fine for single-tenant deployments; for multi-tenant servers (e.g. praisonai serve), keep the returned hook ID and call praisonaiagents.hooks.remove_hook(hook_id) at teardown, or instantiate AuditLogHook and add_hook("after_tool", ...) per-request for per-tenant sinks.


Related