In the previous Prompt Cache Monitor chapter, we learned how to optimize our API usage to save money and time.
Now we have a fully functioning application. The Client connects, the File Manager moves data, and the Session Sync saves our history. But we have one final question: How is the patient doing?
Imagine you are flying a plane, but you have no dashboard.
Without Telemetry & Observability, your code is a "Black Box." If a user complains "It's slow," you have to guess why. Is it the network? Is the AI thinking too long? Is the file upload stuck?
Telemetry & Observability is the nervous system of the api project. It connects to every other module we've builtβthe Client, the File Manager, the Session Syncβand sends vital signals back to the brain.
It is responsible for:
A user reports a bug: "I asked Claude to write a poem, but it crashed."
502 Bad Gateway error. You check the "Prompt Dump" and see the user sent a 10MB text file that choked the system. You know exactly what to fix.The telemetry system is designed to run automatically, but as a developer, you need to trigger the logs at the right time.
Before we log anything, we must respect the user's privacy settings. This is done via metricsOptOut.ts.
import { checkMetricsEnabled } from './metricsOptOut.js';
const status = await checkMetricsEnabled();
if (status.enabled) {
console.log("We are allowed to send analytics.");
} else {
console.log("User opted out. Keep secrets locally.");
}
When an API call finishes successfully, we record the stats using logAPISuccess from logging.ts.
import { logAPISuccess } from './logging.js';
// After the request finishes...
logAPISuccess({
model: 'claude-3-opus',
durationMs: 4500, // How long it took
messageTokens: 1024, // Size of response
costUSD: 0.03, // How much it cost
usage: apiResponse.usage // Raw token counts
});
If things go wrong (like in our Resilient Request Executor), we log the failure details.
import { logAPIError } from './logging.js';
try {
// ... api call ...
} catch (error) {
logAPIError({
error: error,
model: 'claude-3-opus',
durationMs: 1200,
attempt: 2 // We tried twice before failing
});
}
This system acts like a filter and a broadcaster. It takes raw events, cleans them up, checks if it's allowed to share them, and then saves them.
Let's look at the three main components: Privacy, Logging, and Debug Dumping.
In metricsOptOut.ts, we handle the opt-out logic. We don't want to ask the server "Is metrics enabled?" for every single request (that would be slow). So, we cache the answer.
// inside metricsOptOut.ts
export async function checkMetricsEnabled() {
// 1. Check local memory first (Fastest)
if (cachedStatus) return cachedStatus;
// 2. Check disk config (Fast)
const config = getGlobalConfig();
if (config.metricsStatus) return config.metricsStatus;
// 3. Ask the server (Slow - only done once per day)
const result = await fetchMetricsStatusAPI();
saveToDisk(result);
return result;
}
In logging.ts, we handle the formatting. Raw errors are messy. This file cleans them up so they look nice in your analytics dashboard.
It also calculates "Total Duration" including retries. If the Resilient Request Executor tried 3 times, we want to know the total time the user waited, not just the time of the last attempt.
// inside logging.ts
export function logAPISuccessAndDuration(params) {
// Calculate total time user waited
const durationMsIncludingRetries = Date.now() - params.startIncludingRetries;
// Send the clean event
logAPISuccess({
...params,
durationMsIncludingRetries,
// Detect if we used a proxy/gateway (like Helicone)
gateway: detectGateway(params.headers)
});
}
Sometimes, metrics aren't enough. You need to see the exact JSON sent to the API to reproduce a bug. dumpPrompts.ts handles this.
It creates a hidden file on your computer containing the full conversation history for debugging.
// inside dumpPrompts.ts
function dumpRequest(body, timestamp, filePath) {
// Parse the raw JSON string
const req = JSON.parse(body);
// We only want to log user messages to keep the file small
const entries = req.messages
.filter(msg => msg.role === 'user')
.map(msg => JSON.stringify({
type: 'message',
timestamp,
data: msg
}));
// Append to a local file (e.g., ~/.claude/dump-prompts/session_1.jsonl)
appendToFile(filePath, entries);
}
Why is this separate? Sending full prompt text to an analytics server is a privacy risk and requires huge bandwidth. We only write this to the user's local disk for them to inspect if they choose to.
Often in code, we need a "placeholder" for usage statistics before a request finishes. Instead of using null or undefined (which cause crashes), emptyUsage.ts provides a safe default.
// inside emptyUsage.ts
export const EMPTY_USAGE = {
input_tokens: 0,
output_tokens: 0,
cost: 0,
// ... safely initialized to zero
};
In this final chapter, we explored Telemetry & Observability.
metricsOptOut before doing anything.logging.ts captures costs, latency, and errors.dumpPrompts.ts saves raw data locally for deep inspection.Congratulations! You have completed the API Project Tutorial.
We have built a complete, production-grade system for interacting with LLMs:
You now understand the "Nervous System" and the "Brain" of a modern AI application. Happy coding!
Generated by Code IQ