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.
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.
How It Works
Each tool call the agent makes is appended to the audit log before the result returns to the user.
Quick Start
- 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") - 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_nametool_name,tool_input,execution_time_ms- Optional
tool_output(wheninclude_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.
Secret Redaction
Sensitive values in tool_input are replaced with REDACTED before the JSONL line is written โ on by default, no configuration needed.
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, cookieBefore / after on disk:
{"tool_name": "call_api", "tool_input": {"url": "https://api.example.com", "api_key": "sk-live-abc123", "password": "hunter2"}}{"tool_name": "call_api", "tool_input": {"url": "https://api.example.com", "api_key": "***REDACTED***", "password": "***REDACTED***"}}Configuration
| Option | Type | Default | Description | |
|---|---|---|---|---|
log_path | str | ~/.praisonai/audit.jsonl | Append-only JSONL path | |
include_output | bool | False | Include truncated tool output | |
max_output_chars | int | 500 | Max output chars when include_output=True | |
redactor | `Callable[[Any], Any] \ | None` | built-in denylist redactor | Applied to tool_input before writing. Pass None to disable redaction entirely. |
sensitive_keys | `frozenset[str] \ | None` | _DEFAULT_SENSITIVE_KEYS | Override the denylist. Only takes effect with the default redactor; a custom redactor is used exactly as supplied. |
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:
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())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())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.Lockfor 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 fromlogrotate,mv, orrmmid-run) โ added in PR #4205 - Each write calls
fsyncfor 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.