LlamaIndex Tutorial (TypeScript): adding audit logs for beginners

By Cyprian AaronsUpdated 2026-04-21
llamaindexadding-audit-logs-for-beginnerstypescript

This tutorial shows you how to add audit logs to a basic LlamaIndex TypeScript app so every user query, tool call, and response can be traced later. You need this when you’re building anything regulated or support-heavy, where you must answer: who asked what, what data was used, and what the model returned.

What You'll Need

  • Node.js 18+ installed
  • A TypeScript project with npm or pnpm
  • llamaindex installed
  • dotenv installed for API keys
  • An OpenAI API key in .env
  • Basic familiarity with index.asQueryEngine() in LlamaIndex TypeScript

Install the packages:

npm install llamaindex dotenv
npm install -D typescript tsx @types/node

Create a .env file:

OPENAI_API_KEY=your_openai_api_key_here

Step-by-Step

  1. Start with a minimal LlamaIndex setup that can answer questions from documents. We’ll wrap this later with an audit logger, so keep the core query path simple and explicit.
import "dotenv/config";
import { Document, VectorStoreIndex } from "llamaindex";

async function main() {
  const docs = [
    new Document({
      text: "Claims must be reviewed within 48 hours.",
      metadata: { source: "policy-handbook", docId: "policy-001" },
    }),
    new Document({
      text: "Escalate fraud cases to the investigations team.",
      metadata: { source: "claims-playbook", docId: "playbook-002" },
    }),
  ];

  const index = await VectorStoreIndex.fromDocuments(docs);
  const queryEngine = index.asQueryEngine();

  const response = await queryEngine.query({ query: "What is the claims review SLA?" });
  console.log(response.toString());
}

main();
  1. Add a small audit logger that writes structured JSON lines to disk. In production, this is the part that gives you immutable-ish event history you can ship to SIEM, S3, or a log pipeline later.
import fs from "node:fs/promises";

type AuditEvent = {
  timestamp: string;
  userId: string;
  action: string;
  query?: string;
  response?: string;
  metadata?: Record<string, unknown>;
};

class AuditLogger {
  constructor(private readonly filePath: string) {}

  async log(event: AuditEvent) {
    const line = JSON.stringify(event) + "\n";
    await fs.appendFile(this.filePath, line, "utf8");
  }
}
  1. Wrap your query path so every request gets logged before and after execution. The important bit is to log enough context to reconstruct the request without dumping secrets or raw internal prompts.
import "dotenv/config";
import { Document, VectorStoreIndex } from "llamaindex";

async function main() {
  const audit = new AuditLogger("./audit.log");

  const docs = [
    new Document({
      text: "Claims must be reviewed within 48 hours.",
      metadata: { source: "policy-handbook", docId: "policy-001" },
    }),
    new Document({
      text: "Escalate fraud cases to the investigations team.",
      metadata: { source: "claims-playbook", docId: "playbook-002" },
    }),
  ];

  const index = await VectorStoreIndex.fromDocuments(docs);
  const queryEngine = index.asQueryEngine();

  const userId = "agent-123";
  const query = "What is the claims review SLA?";

  await audit.log({
    timestamp: new Date().toISOString(),
    userId,
    action: "query_started",
    query,
    metadata: { engine: "VectorStoreIndex" },
  });

  const response = await queryEngine.query({ query });

  await audit.log({
    timestamp: new Date().toISOString(),
    userId,
    action: "query_completed",
    query,
    response: response.toString(),
  });

  console.log(response.toString());
}

main();
  1. If you want better traceability, include document-level metadata in your logs when you know which sources were retrieved. This makes audits much easier when someone asks why the model answered a certain way.
import fs from "node:fs/promises";

type RetrievalAuditEvent = {
  timestamp: string;
  userId: string;
  action: string;
  query?: string;
  sources?: Array<{ docId?: string; source?: string }>;
};

class AuditLogger {
  constructor(private readonly filePath: string) {}

  async log(event: RetrievalAuditEvent) {
    await fs.appendFile(this.filePath, JSON.stringify(event) + "\n", "utf8");
  }
}

function extractSources() {
  return [
    { docId: "policy-001", source: "policy-handbook" },
    { docId: "playbook-002", source: "claims-playbook" },
  ];
}
  1. Put it together in one executable script and run it as your baseline pattern. Once this works, you can swap file logging for a database table or centralized logging service without changing the rest of your LlamaIndex code.
import "dotenv/config";
import fs from "node:fs/promises";
import { Document, VectorStoreIndex } from "llamaindex";

type AuditEvent = {
  timestamp: string;
  userId: string;
  action: string;
  query?: string;
  response?: string;
};

class AuditLogger {
  constructor(private readonly filePath: string) {}
  
async log(event:
AuditEvent) {
    await fs.appendFile(this.filePath, JSON.stringify(event) + "\n", "utf8");
}
}

async function main() {
const audit =
new AuditLogger("./audit.log");
const docs =
[
new Document({ text:
"Claims must be reviewed within
48 hours.", metadata:
{ source:
"policy-handbook",
docId:
"policy-001"
} }),
];
const index =
await VectorStoreIndex.fromDocuments(docs);
const qe =
index.asQueryEngine();
const userId =
"agent-123";
const query =
"What is the claims review SLA?";
await audit.log({ timestamp:
new Date().toISOString(), userId,
action:
"query_started",
query });
const response =
await qe.query({ query });
await audit.log({ timestamp:
new Date().toISOString(), userId,
action:
"query_completed",
query,
response:
response.toString() });
console.log(response.toString());
}
main();

Testing It

Run the script with npx tsx index.ts and confirm you get both an answer in the terminal and two entries in audit.log. Each line should be valid JSON with a timestamp, userId, action name, and the request or response payload.

Open audit.log and check that query_started appears before query_completed. If you’re using this in a real app, also verify that sensitive fields like tokens, passwords, or full internal prompts are not being written to disk.

A good next test is to trigger an error and make sure failures are logged too. In production systems, error audits matter just as much as successful requests because they show what happened before the failure.

Next Steps

  • Add a query_failed audit event inside a try/catch block.
  • Replace flat-file logging with PostgreSQL or DynamoDB for retention and search.
  • Hook audit events into OpenTelemetry or your existing SIEM pipeline.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides