how can i get metrics that are displayed in cloud ...
# ask-ai
l
how can i get metrics that are displayed in cloud programatically or through SDK ?
t
Thanks for reaching out, Anirban. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — you're asking how to retrieve the metrics shown in LiveKit Cloud programmatically or via an SDK. Here are the ways you can get them and what you’ll need to do. 1) Analytics API (Cloud) • What it gives: session- and participant-level telemetry (sessionId, roomName, createdAt/endedAt, bandwidthIn/bandwidthOut, numParticipants, connectionCounts) and detailed session info (participant device info, connection-quality over time, published tracks). • How to use: a. Create an access token with the
roomList
grant (CLI example shown below). b. Call the REST endpoints to list sessions or fetch a session by id. • Examples: ◦ Create token (CLI):
Copy code
lk token create \
      --api-key $LIVEKIT_API_KEY \
      --api-secret $LIVEKIT_SECRET_KEY \
      --list \
      --valid-for 24h
• List sessions:
Copy code
curl -H "Authorization: Bearer $TOKEN" \
      "<https://cloud-api.livekit.io/api/project/$PROJECT_ID/sessions?start=2024-01-12&end=2024-01-13&limit=100>"
• Get session details:
Copy code
curl -H "Authorization: Bearer $TOKEN" \
      "<https://cloud-api.livekit.io/api/project/$PROJECT_ID/sessions/$SESSION_ID>"
• Notes: Date range is supported (
start
,
end
), pagination via
limit
and
page
. The Analytics API is available to LiveKit Cloud customers on Scale plan or higher. 2) SDK / Agent event metrics (real-time, fine-grained) • What it gives: real-time pipeline metrics emitted by the Agents SDKs (STT, LLM, TTS, VAD, EOUs). Example metric types: ◦ STT:
ttfb
,
duration
,
audio_duration
◦ LLM:
ttft
,
completion_tokens
,
prompt_tokens
◦ TTS:
ttfb
,
duration
,
characters_count
• How to use: register event listeners in your agent/session code to receive
metrics_collected
(or similar) events and log or export the metrics. • Example (pseudocode):
Copy code
def on_metrics_collected(metrics: LLMMetrics):
      print(f"TTFT: {metrics.ttft}s, Tokens: {metrics.total_tokens}")

  session.llm.on("metrics_collected", on_metrics_collected)
Which should I help you with first? • If you want historical/session-level metrics from Cloud, I can show full request/response examples and required token scopes for the Analytics API. • If you need real-time agent metrics, tell me which agent metric type (STT, LLM, TTS, VAD) and which SDK/language you’re using so I can provide a concrete listener example. Sources: Analytics API | LiveKit docs | STT Metrics | LiveKit docs | LLM Metrics | LiveKit docs | VAD Metrics | LiveKit docs | TTS Metrics | LiveKit docs
l
i get this reposen -> go-jose/go-jose: error in cryptographic primitive
t
Thanks — I’m responding about the “go-jose/go-jose: error in cryptographic primitive” error you’re seeing when trying to get metrics (i.e., while generating a LiveKit access token). This error indicates a problem generating or signing the JWT on your backend (not with LiveKit’s API). Common causes and fixes: • Invalid / modified API secret ◦ Use the exact LIVEKIT_API_SECRET value from the dashboard with no base64 decode/encode or truncation. • Wrong signing algorithm ◦ Tokens must be signed with HS256 (HMAC-SHA256). Using RS/ES or another algorithm fails. • Hand-rolled or malformed JWT claims ◦ Required fields:
iss
= API Key,
sub
= identity, and a
video
grant (e.g.,
roomJoin: true
) if joining rooms. Use the server SDK to avoid mistakes. Quick checks 1. Are you using an official LiveKit server SDK for your backend language? (recommended) 2. Is the error thrown during token generation (server) or when the client connects? 3. Do you pass the raw API secret as the HMAC key (no extra encoding)? 4. Are you calling the SDK method that produces a JWT (e.g.,
ToJWT()
/
.toJwt()
), not manually signing? If you’re using Go, example that works:
Copy code
at := auth.NewAccessToken(apiKey, apiSecret)
grant := &auth.VideoGrant{
  RoomJoin:   true,
  Room:       "my-room",
  CanPublish: true,
}
at.SetVideoGrant(grant).
  SetIdentity("admin-user").
  SetValidFor(time.Minute * 10)

token, err := at.ToJWT()
if err != nil { log.Fatal(err) }
If you’re using Node:
Copy code
const at = new AccessToken(apiKey, apiSecret, { identity: 'admin-user', ttl: '10m' });
at.addGrant({ roomJoin: true, room: 'my-room' });
const token = await at.toJwt();
Please share (one or two items): • The code snippet you use to generate the token (server-side). • Your backend language/runtime. • Whether you copied the API secret exactly and whether you altered it (base64 decode, etc.). • Full server error log if available. With that, I’ll pinpoint the exact cryptographic misstep and provide the minimal fix. Sources: Authentication | LiveKit docs | server-sdk-go | livekit/livekit_composite | DeepWiki | Server-side | livekit/livekit_composite | DeepWiki | server-sdk-kotlin | livekit/livekit_composite | DeepWiki | server-sdk-ruby | livekit/livekit_composite | DeepWiki
l
async function saveMetricsToDatabase(agentId: string, metricsData: any) { try { await prisma.agentMetrics.create({ data: { agent_id: agentId, metricsType: metricsData.type, metricsData: metricsData, timestamp: new Date(), }, }); } catch (error) { console.error('Failed to save metrics:', error); } } export default defineAgent({ prewarm: async (proc: JobProcess) => { console.log('Prewarming agent...'); // Load Silero VAD model for voice activity detection proc.userData.vad = await silero.VAD.load(); console.log('VAD model loaded successfully'); }, entry: async (ctx: JobContext) => { const agentId = process.env.AGENT_ID || 'unknown'; console.log(
Agent ${agentId} entry point triggered
); await ctx.connect(); console.log('Connected to LiveKit room'); // Wait for a participant to join const participant = await ctx.waitForParticipant(); console.log(
Participant joined: ${participant.identity}
); const usageCollector = new metrics.UsageCollector(); const agent = new voice.Agent({ instructions: '{{INSTRUCTIONS}}', tools: { lookupWeather, getCurrentTime, }, }); // Preloaded VAD const vad = ctx.proc.userData.vad as silero.VAD; const infomaniakSTT = new InfomaniakSTT({ apiToken: process.env.INFOMANIAK_API_TOKEN!, productId: process.env.INFOMANIAK_PRODUCT_ID!, pollingInterval: 500, maxPollingAttempts: 60, }); // This is required for non streaming STT like Infomaniak or OpenAI Whisper const streamAdapter = new stt.StreamAdapter(infomaniakSTT, vad); const session = new voice.AgentSession({ stt: streamAdapter, llm: new inference.LLM({ model: '{{LLM_MODEL}}', }), tts: new inference.TTS({ model: '{{TTS_MODEL}}', voice: '{{TTS_VOICE}}', }), vad: vad, }); session.on(voice.AgentSessionEventTypes.MetricsCollected, async (ev) => { // Log metrics to console metrics.logMetrics(ev.metrics); // Collect usage for summary usageCollector.collect(ev.metrics); // Save to database for analytics const metricsData = { type: ev.metrics.type, timestamp: new Date().toISOString(), data: ev.metrics, }; await saveMetricsToDatabase(agentId, metricsData); // Log specific metric types if (ev.metrics.type === 'llm_metrics') { console.log(
[LLM Metrics] Tokens: ${ev.metrics.completionTokens}, TTFT: ${ev.metrics.ttftMs}ms, Duration: ${ev.metrics.durationMs}ms
); } else if (ev.metrics.type === 'tts_metrics') { console.log(
[TTS Metrics] Audio Duration: ${ev.metrics.audioDurationMs}ms, TTFB: ${ev.metrics.ttfbMs}ms
); } else if (ev.metrics.type === 'stt_metrics') { console.log(
[STT Metrics] Audio Duration: ${ev.metrics.audioDurationMs}ms, Duration: ${ev.metrics.durationMs}ms
); } else if (ev.metrics.type === 'eou_metrics') { console.log(
[EOU Metrics] EOU Delay: ${ev.metrics.endOfUtteranceDelayMs}ms
); } }); console.log('AgentSession created with Infomaniak STT wrapped in StreamAdapter'); console.log('VAD will buffer audio and trigger transcription on speech end'); await session.start({ agent, room: ctx.room, }); console.log('AgentSession started'); await session.generateReply({ instructions: '{{GREETING_INSTRUCTIONS}}', }); console.log('Initial greeting sent'); }, }); cli.runApp( new WorkerOptions({ agent: fileURLToPath(import.meta.url), agentName: '{{AGENT_NAME}}', // NOTE --> Make sure agent has a name to avoid problems during dispatch }), ); is this code correct ?
t
Thanks — I reviewed your code. Overall it’s close and follows the right patterns; a few fixes and best-practices will make it robust in production. Key issues to fix • proc.userdata vs proc.userData: use
proc.userdata
(lowercase
userdata
) when prewarming and reading VAD. • Async event handler safety: don’t leave an
async
callback unhandled on
.on(...)
. Wrap async work in a fire-and-forget async function with try/catch to avoid unhandled rejections. • Prisma JSON field: ensure your Prisma schema defines
metricsData
as
Json
(or serialize before insert). • Error handling: wrap
session.start()
(and other awaited calls) in try/catch. • Optional: add timeout or handling for
ctx.waitForParticipant()
so the agent doesn’t hang indefinitely if no participant joins. Minimal concrete fixes (high level) • Change: ◦
proc.userData.vad = await silero.VAD.load();
◦ to: ▪︎
proc.userdata.vad = await silero.VAD.load();
◦ And read it with
const vad = ctx.proc.userdata.vad as silero.VAD;
• Change event listener from
async (ev) => { ... }
to:
Copy code
session.on(voice.AgentSessionEventTypes.MetricsCollected, (ev) => {
  void (async () => {
    try {
      // log, collect, and save metrics
    } catch (err) {
      console.error('Error processing metrics event:', err);
    }
  })();
});
• Ensure Prisma model:
Copy code
model agentMetrics {
  id          String   @id @default(cuid())
  agent_id    String
  metricsType String
  metricsData Json
  timestamp   DateTime @default(now())
}
• Wrap session start in try/catch:
Copy code
try {
  await session.start({ agent, room: ctx.room, record: true });
} catch (err) {
  console.error('Failed to start session:', err);
}
Other recommendations • Serialize/flatten metrics if your DB or downstream consumers expect specific fields (e.g., include requestId/speechId if present). • Batch DB inserts at high volume to reduce load. • Consider enabling
record: true
for observability and using tracing IDs for correlation. If you want, I can: • Provide a corrected full file with the fixes applied. • Convert the metrics save to a batched writer for higher throughput. Which would you like next? Sources: Agents (Node.js) | livekit/livekit_composite | DeepWiki | TTS Metrics | LiveKit docs | Streamline Troubleshooting with Agent Observability | https://blog.livekit.io/streamline-troubleshooting-with-agent-observability/#/portal/signup| | Agents (Python) | livekit/livekit_composite | DeepWiki
l
i do not see any agent metric being saved to database after having tested with agent playground
t
Thanks — I’m responding about why no agent metrics are appearing in your database after testing with the agent playground. Let’s narrow this down quickly. Please check and share results for these items (start from the top): 1) Confirm the MetricsCollected handler runs • Add an explicit console.log at the top of the handler to ensure it fires:
Copy code
session.on(voice.AgentSessionEventTypes.MetricsCollected, (ev) => {
    console.log('MetricsCollected event fired:', ev.metrics.type);
    void (async () => { ... })();
  });
• Do you see that log when exercising the playground? 2) Verify saveMetricsToDatabase is invoked and not erroring • Add logging before/after the DB call and inside catch:
Copy code
console.log('Saving metrics to DB', metricsData);
  await prisma.agentMetrics.create(...);
  console.log('Saved metrics to DB');
• Check your application logs for those messages or any "Failed to save metrics:" errors. 3) Check Prisma / DB connectivity and schema • Confirm
DATABASE_URL
is set in the environment the worker uses. • Ensure Prisma client is initialized in the same process and not closed prematurely. • Confirm your Prisma schema has
metricsData
typed as
Json
and table/columns exist: ◦ model includes
agent_id String
,
metricsType String
,
metricsData Json
,
timestamp DateTime
. 4) Ensure event handler async work isn’t being dropped • Use the fire-and-forget pattern with try/catch (we recommended this). If you used
async
directly on the handler, exceptions could be lost. Confirm the handler wraps async logic in a try/catch. 5) Confirm metrics are actually emitted by the playground run • Some playground scenarios may not trigger metrics (e.g., no LLM/TT S/STT activity). Log the full
ev.metrics
to confirm contents:
Copy code
console.log('Metrics payload:', JSON.stringify(ev.metrics));
6) Check for batching/transaction issues or silent DB rollbacks • If you use transactions or a job worker that exits quickly, ensure the process stays alive until the DB write completes. • If using connection pooling, verify pool limits and errors in logs. 7) Review worker environment differences • Does the playground run the same code and environment variables as your local/production worker? The worker running in the playground may not have
DATABASE_URL
or Prisma migrations applied. If you can, please paste: • The console output showing whether the MetricsCollected log appears. • The small logs you added around the DB call (saving / saved / errors). • Your Prisma model for agentMetrics (the schema snippet). • Confirmation of where the agent process runs (local vs Playground hosted) and the value/source of DATABASE_URL (no secrets — just whether it’s set). Give me those details and I’ll point to the exact fix. If you prefer, I can provide a minimal patched handler you can drop in to verify end-to-end saving. Which do you want next?
l
It runs as a PM2 process which is why i am not able to see any logs
t
Thanks — since your agent runs under PM2 (and you can’t see logs), let’s get visibility and verify the metrics path end‑to‑end. Do these steps and share the log lines you get for the MetricsCollected handler and any DB errors. 1) Enable PM2 logs (stdout + stderr files) • Run: ◦ pm2 start app.js --name my-agent (if not already) ◦ pm2 logs my-agent --lines 200 • Or view files: ◦ pm2 show my-agent (look for "error log" and "out log" paths) ◦ tail -f /path/to/out.log /path/to/error.log 2) Add explicit debug logs in your handler (drop in exactly)
Copy code
session.on(voice.AgentSessionEventTypes.MetricsCollected, (ev) => {
  console.log('METRICS_EVENT fired:', ev.metrics.type);
  void (async () => {
    try {
      console.log('METRICS_PAYLOAD:', JSON.stringify(ev.metrics));
      const metricsData = { type: ev.metrics.type, timestamp: new Date().toISOString(), data: ev.metrics };
      console.log('SAVING_METRICS:', metricsData);
      await saveMetricsToDatabase(agentId, metricsData);
      console.log('SAVED_METRICS');
    } catch (err) {
      console.error('METRICS_SAVE_ERROR:', err);
    }
  })();
});
3) Ensure PM2 preserves logs across restarts • Start with ecosystem file or flags: ◦ pm2 start ecosystem.config.js ◦ or pm2 start app.js --name my-agent --output ./logs/out.log --error ./logs/err.log 4) Confirm Prisma writes and DB connectivity • Add these checks at startup and before write:
Copy code
console.log('DATABASE_URL present?', Boolean(process.env.DATABASE_URL));
try {
  await prisma.$connect();
  console.log('Prisma connected');
} catch (e) {
  console.error('Prisma connect error:', e);
}
• Ensure your process has correct DATABASE_URL and migrations applied. 5) Keep process alive until write completes • If your worker may exit, ensure PM2 doesn’t restart mid-write. Logs above will show if writes complete. 6) If still nothing appears, capture these artifacts and paste them: • Output of pm2 logs around a test run (lines that include METRICS_EVENT, SAVING_METRICS, SAVED_METRICS, METRICS_SAVE_ERROR). • The prisma model snippet for agentMetrics from schema.prisma. • Confirmation that DATABASE_URL used by PM2 matches the DB you inspect. If you want, I can provide a one-file small repro agent that logs and writes a test metric so you can verify PM2 + Prisma end-to-end. Would you like that?