Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/stats-server.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createServer } from "node:http";
import { logDatabaseSize } from "./telemetry.mjs";
import { computeMemoryStats } from "./memory-stats.mjs";

const LOOPBACK_ADDRS = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
Expand All @@ -19,7 +20,7 @@ export function isLoopbackAddress(addr) {
* @param {{ loadMemories: () => Promise<object[]>, now?: () => number }} options
* @returns {import("node:http").Server}
*/
export function createStatsServer({ loadMemories, now = Date.now } = {}) {
export function createStatsServer({ loadMemories, now = Date.now, logger = console.log } = {}) {
if (typeof loadMemories !== "function") {
throw new TypeError("createStatsServer requires loadMemories to be a function");
}
Expand All @@ -35,6 +36,7 @@ export function createStatsServer({ loadMemories, now = Date.now } = {}) {

try {
const memories = await loadMemories();
logDatabaseSize({ memories }, logger);
const stats = computeMemoryStats(memories, { now: now() });
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(stats));
Expand Down
53 changes: 53 additions & 0 deletions src/telemetry.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Instrumentation for database size and query performance metrics.
// Pure/side-effect-light: logging is injectable so tests don't need to
// capture real console output.

/**
* Computes the on-disk size of a memory store, in bytes.
* @param {Object} memoryStore
* @returns {number}
*/
export function computeDatabaseSizeBytes(memoryStore = {}) {
return Buffer.byteLength(JSON.stringify(memoryStore), "utf8");
}

/**
* Logs the current database size and memory count.
* @param {Object} memoryStore
* @param {(entry: object) => void} [logger] - Defaults to console.log
* @returns {{ event: string, bytes: number, memoryCount: number, timestamp: string }}
*/
export function logDatabaseSize(memoryStore = {}, logger = console.log) {
const entry = {
event: "database_size",
bytes: computeDatabaseSizeBytes(memoryStore),
memoryCount: Array.isArray(memoryStore.memories) ? memoryStore.memories.length : 0,
timestamp: new Date().toISOString(),
};
logger(JSON.stringify(entry));
return entry;
}

/**
* Wraps a query function so every call logs its duration and result count.
* @param {Function} queryFn - e.g. retrieveMemories
* @param {(entry: object) => void} [logger] - Defaults to console.log
* @returns {Function} A wrapped version of queryFn with identical behavior
*/
export function instrumentQuery(queryFn, logger = console.log) {
return function instrumented(...args) {
const startedAt = Date.now();
const result = queryFn(...args);
const durationMs = Date.now() - startedAt;
const resultCount = Array.isArray(result) ? result.length : (Array.isArray(result?.memories) ? result.memories.length : undefined);

logger(JSON.stringify({
event: "query_metrics",
durationMs,
resultCount,
timestamp: new Date().toISOString(),
}));

return result;
};
}
29 changes: 28 additions & 1 deletion test/memory-stats.test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
import { computeMemoryStats } from "../src/memory-stats.mjs";
import { isLoopbackAddress } from "../src/stats-server.mjs";
import { isLoopbackAddress, createStatsServer } from "../src/stats-server.mjs";

// ---------------------------------------------------------------------------
// activeCount
Expand Down Expand Up @@ -178,3 +178,30 @@ test("isLoopbackAddress returns false for 0.0.0.0", () => {
test("isLoopbackAddress returns false for empty string", () => {
assert.ok(!isLoopbackAddress(""));
});

// ---------------------------------------------------------------------------
// createStatsServer telemetry logging
// ---------------------------------------------------------------------------

test("createStatsServer logs database size on each stats request", async () => {
const logs = [];
const fakeMemories = [{ id: "m_01", status: "active" }, { id: "m_02", status: "active" }];

const server = createStatsServer({
loadMemories: async () => fakeMemories,
logger: (line) => logs.push(line),
});

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address();

const response = await fetch(`http://127.0.0.1:${port}/`);
await response.json();

await new Promise((resolve) => server.close(resolve));

assert.equal(logs.length, 1);
const parsed = JSON.parse(logs[0]);
assert.equal(parsed.event, "database_size");
assert.equal(parsed.memoryCount, 2);
});
59 changes: 59 additions & 0 deletions test/telemetry.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import test from "node:test";
import assert from "node:assert";
import { computeDatabaseSizeBytes, logDatabaseSize, instrumentQuery } from "../src/telemetry.mjs";

test("Memory Consumption Telemetry Logs", async (t) => {

await t.test("should compute the byte size of a memory store", () => {
const store = { memories: [{ id: "m_01" }] };
const size = computeDatabaseSizeBytes(store);
assert.strictEqual(size, Buffer.byteLength(JSON.stringify(store), "utf8"));
assert.ok(size > 0);
});

await t.test("should log database size with memory count and return the logged entry", () => {
const logs = [];
const store = { memories: [{ id: "m_01" }, { id: "m_02" }] };

const entry = logDatabaseSize(store, (line) => logs.push(line));

assert.strictEqual(logs.length, 1);
const parsed = JSON.parse(logs[0]);
assert.strictEqual(parsed.event, "database_size");
assert.strictEqual(parsed.memoryCount, 2);
assert.strictEqual(entry.memoryCount, 2);
assert.ok(parsed.bytes > 0);
});

await t.test("should handle a store with no memories array", () => {
const entry = logDatabaseSize({}, () => {});
assert.strictEqual(entry.memoryCount, 0);
});

await t.test("should log query duration and result count without changing the result", () => {
const logs = [];
const fakeQuery = (query) => [{ id: "m_01" }, { id: "m_02" }].filter((m) => query !== "none");

const wrapped = instrumentQuery(fakeQuery, (line) => logs.push(line));
const result = wrapped("health");

assert.deepStrictEqual(result, [{ id: "m_01" }, { id: "m_02" }]);
assert.strictEqual(logs.length, 1);

const parsed = JSON.parse(logs[0]);
assert.strictEqual(parsed.event, "query_metrics");
assert.strictEqual(parsed.resultCount, 2);
assert.ok(typeof parsed.durationMs === "number");
});

await t.test("should still log correctly when the wrapped query returns zero results", () => {
const logs = [];
const fakeQuery = () => [];

const wrapped = instrumentQuery(fakeQuery, (line) => logs.push(line));
wrapped("nothing matches");

const parsed = JSON.parse(logs[0]);
assert.strictEqual(parsed.resultCount, 0);
});
});