Simple LLM Observability Dashboard Clone

A minimal Node.js + SQLite dashboard inspired by telemetry.dev

Welcome to another official UVF IT guide. In the next 45 minutes you’ll spin up a tiny observability dashboard that collects LLM request metadata and displays it in a web UI – a stripped‑down version of telemetry.dev.

We’ll use plain Node.js, Express, and SQLite (all free‑tier) so you can run everything locally without any paid APIs.

Before you start…

Component Overview
LOCAL STACK reads/writes HTTP requests Express API + UI SQLite DB Browser UI
1

Phase 1: Scaffold the project

Create a fresh folder for the dashboard, initialize it as an npm project, and pull in the tiny set of libraries we need. Express will serve the API and UI, better‑sqlite3 gives us a fast, zero‑config SQLite wrapper, and cors lets the browser talk to the backend without headaches.

All commands are run from your terminal; after they finish you’ll have a ready‑to‑code directory with a package.json that tracks the dependencies.

mkdir llm-dashboard && cd llm-dashboard
npm init -y
npm install express better-sqlite3 cors
2

Phase 2: Define the data model

We store each telemetry event as a single JSON column together with an auto‑generated timestamp. This keeps the schema ultra‑simple while still letting you query by time or inspect the raw payload.

The helper module below opens (or creates) a SQLite file, ensures the table exists, and exports two tiny functions: one to insert a new event and another to fetch all rows for the UI.

db.js

const Database = require('better-sqlite3');
const path = require('path');

// Create or open a local SQLite file in the project root
const dbPath = path.resolve(__dirname, 'events.db');
const db = new Database(dbPath);

// Ensure the events table exists – id is optional, we just need timestamp and payload
db.exec(`
  CREATE TABLE IF NOT EXISTS events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    ts INTEGER NOT NULL,
    payload TEXT NOT NULL
  );
`);

/** Insert a telemetry event (plain JS object) */
function insertEvent(eventObj) {
  const stmt = db.prepare('INSERT INTO events (ts, payload) VALUES (?, ?)');
  const ts = Date.now();
  stmt.run(ts, JSON.stringify(eventObj));
}

/** Retrieve all stored events, ordered newest first */
function getAllEvents() {
  const rows = db.prepare('SELECT ts, payload FROM events ORDER BY ts DESC').all();
  // Parse the JSON payload back into objects for easy consumption
  return rows.map(r => ({ timestamp: r.ts, data: JSON.parse(r.payload) }));
}

module.exports = { insertEvent, getAllEvents };
3

Phase 3: Build API & UI

In this step we wire up an Express server that talks to the SQLite database we created earlier. A POST /telemetry endpoint will accept JSON payloads from your LLM agents and store them, while a GET / route renders a simple HTML table showing every stored event. Keeping the UI minimal helps you focus on the data flow before adding fancy charts later.

index.js

const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const app = express();
const db = new sqlite3.Database('telemetry.db');
app.use(express.json());
// Ensure table exists
db.run(`CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY AUTOINCREMENT, payload TEXT, ts DATETIME DEFAULT CURRENT_TIMESTAMP)`);
// POST endpoint for agents
app.post('/telemetry', (req, res) => {
  const payload = JSON.stringify(req.body);
  db.run('INSERT INTO events (payload) VALUES (?)', payload, err => {
    if (err) return res.status(500).end();
    res.status(204).end();
  });
});
// GET UI – render simple table
app.get('/', (req, res) => {
  db.all('SELECT id, payload, ts FROM events ORDER BY ts DESC', [], (err, rows) => {
    if (err) return res.status(500).send('DB error');
    const rowsHtml = rows.map(r => `<tr><td>${r.id}</td><td>${r.payload}</td><td>${r.ts}</td></tr>`).join('');
    res.send(`<!doctype html><html><head><title>LLM Telemetry</title></head><body><h1>Events</h1><table border='1'><tr><th>ID</th><th>Payload</th><th>Timestamp</th></tr>${rowsHtml}</table></body></html>`);
  });
});
const PORT = 3000;
app.listen(PORT, () => console.log(`Dashboard running at http://localhost:${PORT}`));
4

Phase 4: Run locally

Now that the server code is ready, start it with Node. The script will create the SQLite file on first run and listen on port 3000. Keep the terminal open so you can see the startup log and any potential errors.

node index.js
5

Phase 5: Verify and extend

With the server up, send a sample telemetry event using curl. The endpoint should respond with HTTP 204 and no body. Refresh http://localhost:3000 in your browser; the new row should appear in the table. From here you can experiment with filters, pagination, or plug in a chart library to visualise metrics.

curl -X POST http://localhost:3000/telemetry \
  -H 'Content-Type: application/json' \
  -d '{"model":"gpt-4","tokens":123,"prompt":"Hello"}'

Verifying the Stack

CheckCommand / Why it matters
Server starts without errorsnode index.js → console shows ‘Dashboard running at http://localhost:3000’
POST request returns 204curl -X POST http://localhost:3000/telemetry -H 'Content-Type: application/json' -d '{"test":1}' → no output and exit code 0
Browser shows a table with the sent payloadOpen http://localhost:3000 and see a row containing the JSON you posted

What’s next?

You now have a functional clone. Consider adding a lightweight chart library (e.g., Chart.js) to plot token usage over time, or expose a WebSocket for real‑time updates.