Skip to main content
Version: 0.9.17-patch2

Call Detail Records (CDR)

Use this page to understand what is in a Call Detail Record (CDR), when it is emitted, and how to query it. A CDR is a single structured log record (logType='cdr') that summarises one call/conversation end-to-end. It is intended for reporting, troubleshooting, and analytics — a single JSON object per call that consolidates the data otherwise scattered across in-memory state, OTel spans, structured logs, and the Conversation row.

For the broader log schema, SigNoz queries, and per-service log-level variables, see Monitoring in SigNoz. For call-flow events on TelSys / TelPro / RTPEngine, see Application and call-flow logging.

Scope

v1 emits CDRs only. Sending them to a remote HTTP server is a follow-up task and is not covered here.

The CDR is emitted at StasisEnd for every SIP caller leg whose call actually ends:

BranchCDR emitted?
Non-resumable cleanupYes
Resumable fallback cleanup (pause not supported / no app config found)Yes
ExternalMedia channel_end (UnicastRTP/…, WebSocket/…)No
Resumable pause success (cleanupChannelResourcesPartial)No

CDR emission is wrapped in a defensive try/catch, so a build failure never blocks cleanupChannelResources.

Filterable shape

Each CDR is a single OTel log record with eventName='cdr' (filterable as attributes.logType == 'cdr'). Every field on the entry is also flattened into the OTel attributes map, so a flat attributes.status == "error" query works without nested-path syntax.

The same record is persisted to Postgres AppLog with logType='cdr' and the full JSON in the data JSONB column. Reuse the existing AppLog indexes on (conversationId, timestamp) and (logType, timestamp).

Fields

Correlation IDs

FieldSource
channelIdARI SIP channel UUID
conversationIdConversation.id (DB row)
callIdX-Call-ID from SIP headers (resumable session id)
traceIdActive OTel trace; falls back to the X-TRACE-ID header
parentSpanIdActive OTel span at hangup time
appIdResolved FlowDefinition / app
teamIdCall metadata teamId; falls back to the Conversation row
flowDefinitionId, flowVersionCall metadata, falling back to the Conversation row
sessionIdchannelData.conversationId (cross-service runtime owner id placeholder)

These match the identifiers used across TelWeb, TelPhi, and SigNoz — see Monitoring in SigNoz → Structured logging schema.

Timing

FieldSource
startedAtchannelData.startedAt (epoch ms)
endedAtDate.now() at emission
durationSeconds(endedAt - startedAt) / 1000

Participants

FieldSource
callerNumberchannelData.fullCallerNumber (unredacted; redaction handled downstream)
calledNumberchannel.dialplan.exten

PII redaction (caller / called / transcript text) is handled by the existing redactForLogs pipeline — see PII redaction. No new redaction rules are required for the CDR.

Status / failure

status is an enum derived from the call-end path:

ValueWhen
completedCaller or bot hangup, normal clearing
transferredLast flow-engine node exited with transfer or transfer_completed
abandonedChannel ended during setup (cancelPendingChannelSetup returned true)
errorfinalizeFlowEngineConversationOnHangup reported an error

failure carries the supporting detail:

Sub-fieldSource
hangupCauseAsterisk channel.hangupcause
categoryDerived from hangupCausenormal_clearing, busy, no_answer, transfer, error
errorTypeFlowEngineError when a node recorded a structured error
failureCodeMost recent nodeHistory[].error.code → last exitReasonhangupCausenull

Bot / service context

FieldSource
botNameflowEngineContext.getAllCallMetadata().botName
botDisplayNamebotDisplayName; falls back to botName
flowNameCall metadata flowName
flowVersionLabelCall metadata flowVersionLabel
botOutcome.botOperationResultextractBotOperationParams(callMetadata)success/failure
botOutcome.botOperationDataSame as above (arbitrary payload, e.g. { intent: 'order_status' })
botOutcome.lastNodeExitReasonLast nodeHistory[].exitReason
botOutcome.lastErrorCodeLast nodeHistory[].error.code

Provider / service identifiers

provider.providerMode is either unified (one provider/model) or modular (separate STT / LLM / TTS components).

Unified mode

FieldSource
unifiedProviderproviderInstance.provider
unifiedModelproviderInstance.model

Modular mode

FieldSource
sttProvidersttComponent.provider
llmProvider, llmModelllmComponent.{provider,model}
ttsProviderttsComponent.provider
currentSttIndex, currentTtsIndexActive fallback-chain index at hangup time
sttChain, ttsChainConfigured provider fallback chain
ttsMediaCacheBackendnone | media | local

tobiConversationId is populated when the call used the Vodafone TOBi managed LLM. hasRecording reflects whether channelData.callRecorder was set.

Latency / KPIs

FieldSource
durationSecondsSame as in Timing
rtpMediaSecondsBest-effort: rtpSentStats.bytes * 8 / 64000 (G.711 μ-law approximation)
llmInputTokens, llmOutputTokens, llmTotalTokensusage.totals.*
totalLatencyMsAggregate latency across realtime turns
ttsProviders[]Per-provider rollup from usage.runtime.tts.providers[] — provider, model, requests, chars
llmModels[]Per-model rollup from usage.runtime.llm.models[] — provider, model, requests, in/out tokens

The per-usage-rollup detail is what makes CDRs usable for cost dashboards and per-provider SLI/SLO reporting.

Schema

schemaVersion: "1.0" — bump this when adding, removing, or renaming fields. Consumers can branch on it.

Querying

SigNoz (live OTel)

Filter by record type:

service.name = "telphi" AND attributes.logType = "cdr"

Add a status filter to find errored calls:

service.name = "telphi" AND attributes.logType = "cdr" AND attributes.status = "error"

Or trace a single call:

attributes.channelId = "<channel-uuid>"
attributes.callId = "<x-call-id>"
attributes.conversationId = "<conversation-uuid>"

Postgres (AppLog)

CDR rows land in AppLog with the full payload in data JSONB:

SELECT
data->>'callId' AS call_id,
data->>'status' AS status,
data->>'durationSeconds' AS duration_s,
data->>'callerNumber' AS caller,
data->>'appId' AS app_id,
data->'latency'->>'llmTotalTokens' AS llm_tokens,
"createdAt"
FROM "AppLog"
WHERE "logType" = 'cdr'
ORDER BY "createdAt" DESC
LIMIT 50;

Filter on a specific bot or team:

SELECT data
FROM "AppLog"
WHERE "logType" = 'cdr'
AND "appId" = 'app-uuid'
AND data->>'status' = 'error'
AND "createdAt" > NOW() - INTERVAL '24 hours';

The AppLog schema already indexes (logType, timestamp), (conversationId, timestamp), and (appId, timestamp) — see Schema overview.

PII and retention

CDRs flow through the same redactForLogs pipeline as every other structured log, so:

  • callerNumber and calledNumber are redacted when ENABLE_PII_LOGGING is off (and again at the OTel collector's transform/pii).
  • Transcript-derived fields (none on the CDR itself) inherit the existing redaction rules.

CDR retention is the same as AppLog retention, which is governed by the platform's standard log retention policy. If you need a longer horizon for analytics, export to your warehouse from AppLog or wait for the follow-up "send CDR to remote server" feature.

Limitations / not yet

  • No remote HTTP sender. CDRs are emitted and persisted only. Sending to an external collector is a separate task.
  • No new Prisma model. The CDR lives in AppLog.data JSONB. If you need strongly-typed columns or a dedicated retention horizon, that is a follow-up schema change.
  • No new dashboard / alert. Filter in SigNoz using the queries above; build dashboards against AppLog once you have a representative data window.
  • Synchronous emission. CDR build runs inline at StasisEnd; it is small (single JSON, no network) and bounded by try/catch, but if the build ever grows heavy, move it behind a Tasker job (the PERSIST_CONVERSATION_SPAN_TREE pattern is the template).
  • Bot / fallback tracking beyond usage.runtime.*. If a flow engine emits new structured events about provider fallbacks (distinct from usage rollups), extend the CDR builder to surface them — don't add ad-hoc fields without bumping schemaVersion.

Where to look

SurfaceBest for
SigNoz Logs filtered by attributes.logType = 'cdr'Live, cross-call search by status, trace id, app id.
Postgres AppLog filtered by logType='cdr'Historical reporting, joins with Conversation, retention control.
Monitoring in SigNozSchema keys, log-level env vars, dashboards, PII redaction.
Application and call-flow loggingPer-event TelSys / TelPro / RTPEngine logs.
SIP trunk log referenceInbound SIP trunk health messages when calls fail before reaching TelSys.