Skip to content

🎃 Export the audit log as CSV or JSON from /logs #998

Open

Description

@helpcodeai

Part of the October Challenge #997.

Why

Admins regularly need the tool-call history outside the product: for a security review, to attach to a support ticket, to load into a spreadsheet or a SIEM, or to keep it beyond the retention window. Today the Audit Log page (/logs) shows 50 rows per page and offers no download. The only way out is paging through GET /api/audit/invocations by hand. GitHub's audit log, Auth0 and Grafana all offer CSV and JSON export of the filtered view.

What already exists: the page filters by status, tool/connector search, connector and MCP server (logs/page.tsx:116-134), and the API supports the same filters plus limit/offset (audit.controller.ts:13-37). There is no date filter and no export. The header button passed to AppShell as exportButton (logs/page.tsx:183-207) is only a Refresh button with a misleading variable name. docs/api-reference.md:226-253 documents toolName, from and to filters that the API does not have, and a stats response (last24h, last7d, total) that does not match getStats() (invocations24h, errors24h, invocations7d, totalInvocations).

Self-hosted vs Cloud

  • Self-hosted: enabled in Community and Business. No ee/ code. There is no retention job on self-hosted, so an export can cover the whole history of the organization.
  • Cloud (DEPLOYMENT_MODE=cloud): enabled. Rows older than INVOCATION_RETENTION_DAYS (default 90) are deleted and payloads older than INVOCATION_PAYLOAD_RETENTION_DAYS (default 14) are cut to an excerpt (ee/cloud/kg-cron.service.ts:372, :407), so an export only contains what is still stored. Say so in a tooltip next to the button. Do not change anything under ee/. The export must be streamed in batches: an organization can have hundreds of thousands of rows, and loading them in one query would put the shared backend's heap at risk.
  • Who can use it: org ADMIN only (@Roles('ADMIN') with RolesGuard, the pattern on site-settings.controller.ts:111-112). Other roles keep reading /logs as today but get no bulk download. Rows are always scoped to the caller's organization. If the JWT carries no organizationId, return 403: orgScope() returns {} (no filter) for a missing org (audit.service.ts:297-300), and an export must never fall back to all rows.

What to build

  1. Shared filter builder: move the where construction out of getRecentInvocations (audit.service.ts:302-345) into one helper used by the list and the export, and add from / to on createdAt (ISO date or datetime; from inclusive, to exclusive). The list endpoint gets from/to too.
  2. Endpoint: GET /api/audit/invocations/export?format=csv|json with the list filters (status, search, connectorId, mcpServerId, toolId, from, to) plus payloads=true|false (default false). Headers: Content-Type: text/csv; charset=utf-8 or application/json, and Content-Disposition: attachment; filename="anythingmcp-audit-<from>-<to>.<ext>".
  3. Streaming: read in batches of 1,000 with a keyset cursor on (createdAt desc, id desc) and write each batch to the response (@Res()), never collecting all rows in memory. Cap one export at AUDIT_EXPORT_MAX_ROWS (default 100,000). When the cap is reached, end the file cleanly and add a final marker (a last CSV comment line or a truncated: true field) so the UI can say "narrow the date range". For JSON, write an array incrementally or NDJSON; pick one and document it.
  4. Columns (same names in CSV and JSON): createdAt (ISO 8601 UTC), status, tool, connector, connectorType, mcpServer, userEmail, authMethod and apiKeyName (both parsed from clientInfo), durationMs, repeatCount, error; with payloads=true also input and output (JSON strings in CSV). Keep repeatCount: one stored row can stand for many identical failures (audit.service.ts:12-18).
  5. CSV safety: RFC 4180 quoting (quotes, commas, newlines), UTF-8 with BOM so Excel shows umlauts correctly, and formula-injection protection: prefix a cell that starts with =, +, -, @, tab or carriage return with '. Tool arguments and upstream errors are user- and vendor-controlled text.
  6. UI: on /logs, rename exportButton to what it is and add an "Export" menu next to Refresh (CSV, JSON, and an "Include input and output" option), shown to admins only (useAuth().user.role). Add a From / To date range to the toolbar (logs/page.tsx:210-267) that drives both the list and the export. request() in lib/api.ts:27-57 always parses JSON, so add a separate client helper that uses fetch + blob() and an object URL, like handleExportAll on the Connectors page (connectors/page.tsx:139-160).

Where to look

  • packages/backend/src/audit/audit.controller.ts:13-37: list endpoint and its filters.
  • packages/backend/src/audit/audit.service.ts:297-345: orgScope() and getRecentInvocations() (the includes give tool, connector, user and server names).
  • packages/backend/prisma/schema.prisma:678-718: ToolInvocation (clientInfo is a JSON string; repeatCount).
  • packages/backend/src/auth/roles.guard.ts:11 and packages/backend/src/settings/site-settings.controller.ts:111-112: admin-only guard pattern.
  • packages/backend/src/ee/cloud/kg-cron.service.ts:372, :407: Cloud payload trim and retention (read only).
  • packages/frontend/src/app/logs/page.tsx:83-207: state, filters, fetch, header actions.
  • packages/frontend/src/lib/api.ts:753-765: audit.invocations() client.
  • packages/frontend/src/components/ui/action-menu.tsx: dropdown for the menu.
  • docs/api-reference.md:219-253: Audit section to correct and extend.

Acceptance criteria

  • Unit specs next to the service (cd packages/backend && npm test): filters and from/to build the expected where; another organization's rows never appear; CSV escaping (quotes, newlines, a cell starting with =); the row cap ends the file with the truncation marker; payloads=false omits input/output.
  • EDITOR and VIEWER get 403 from the export endpoint; a JWT without organizationId gets 403.
  • Exporting 50,000 seeded rows keeps backend memory flat (describe how you checked in the PR).
  • The CSV opens in LibreOffice or Excel with correct columns and umlauts.
  • Playwright test in packages/frontend/tests/e2e/ (stub /api/audit/invocations* like mobile.spec.ts): an admin sees the Export menu and the request carries the active filters and dates; a viewer does not see it. Run with cd packages/frontend && npm run test:e2e.
  • docs/api-reference.md: export endpoint documented, toolName example removed, from/to documented, stats response corrected.

Out of scope

  • Scheduled or emailed exports, pushing to S3 or a SIEM.
  • Exporting security events or product events.
  • Changing retention on Cloud or adding retention to self-hosted.
  • Any change under ee/.

Size

M (1–2 days)

How to claim

Comment "I'd like to work on this" and we'll assign you. Rules in #997.

Activity

  1. jasperdingg commented on Oct 20, 2026

    @jasperdingg

    Hi, I'd also like to take this issue, will open a pr soon!

  2. helpcodeai commented on Oct 20, 2026

    @helpcodeai
    ContributorAuthor

    @jasperdingg it's yours too, thanks! Same as on #1014: this comment is the assignment, since GitHub only lets us assign repository members.

    The parts that matter most in review:

    • Streaming: the export must stream in batches (cursor or keyset pagination, not one big findMany). Cloud organizations can have hundreds of thousands of rows.
    • Organization scope: every query stays filtered by organizationId, and the export applies exactly the filters the page shows.
    • No ee/ changes: don't touch anything under ee/. A tooltip next to the button should say that on Cloud only what is still stored can be exported (retention).
    • Docs: please fix the mismatches the issue lists in docs/api-reference.md while you are there.
    • Tests: a backend spec for the export endpoint (filters, org isolation, CSV escaping of commas, quotes and newlines) and a Playwright check for the button.

    As said on #1014, one PR at a time please.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthacktoberfestGood for Hacktoberfest contributorshelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions