fix: complete caller analytics reliability remediation
This commit is contained in:
@@ -76,6 +76,8 @@ const copyEntries = [
|
||||
['infra/server-b/s58', 'infra/server-b/s58', true],
|
||||
['infra/server-b/caller-analytics', 'infra/server-b/caller-analytics', true],
|
||||
['scripts/instrument-caller-analytics.mjs', 'scripts/instrument-caller-analytics.mjs', true],
|
||||
['scripts/maintain-caller-analytics.mjs', 'scripts/maintain-caller-analytics.mjs', true],
|
||||
['scripts/rebuild-caller-analytics.mjs', 'scripts/rebuild-caller-analytics.mjs', true],
|
||||
['infra/ops/startup/lisglosips-start-b.sh', 'infra/ops/startup/lisglosips-start-b.sh', true],
|
||||
['infra/server-a/s28/lisglosips_hotpath.lua', 'infra/server-a/s28/lisglosips_hotpath.lua', true],
|
||||
['scripts/phase2-gateway-migration.mjs', 'scripts/phase2-gateway-migration.mjs', true],
|
||||
|
||||
@@ -3,11 +3,29 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
||||
const [source, output] = process.argv.slice(2);
|
||||
if (!source || !output) throw new Error('Usage: node instrument-caller-analytics.mjs source.cfg output.cfg');
|
||||
let cfg = readFileSync(source, 'utf8').replaceAll('\r\n', '\n').replace(/^\uFEFF/, '');
|
||||
if (cfg.includes('CRA1|')) throw new Error('Already instrumented; use the recorded pre-analytics backup');
|
||||
if (cfg.includes('CRA_INSTRUMENTATION_VERSION=2')) throw new Error('Already instrumented at version 2');
|
||||
function insert(marker, extra, after = false) {
|
||||
if (cfg.split(marker).length !== 2) throw new Error(`Unsupported routing layout: ${marker}`);
|
||||
cfg = cfg.replace(marker, after ? marker + extra : extra + marker);
|
||||
}
|
||||
const versionMarker = '# CRA_INSTRUMENTATION_VERSION=2\n';
|
||||
const registerStats = () => insert('modparam("statistics", "variable", "s28_cdr_xadd_error_total/no_reset")', '\nmodparam("statistics", "variable", "cra_event_total/no_reset")', true);
|
||||
const ackHook = ` if (is_method("ACK")) {\n $var(cra_kind)="TALK_START"; $var(cra_code)=200; $var(cra_source)="dialog-acked"; route(CRA_EMIT_DLG);\n }\n`;
|
||||
if (cfg.includes('CRA1|')) {
|
||||
registerStats();
|
||||
insert(' if (has_totag()) {\n', ackHook, true);
|
||||
cfg = cfg
|
||||
.replace('$var(cra_kind)="END"; $var(cra_code)=$var(s28_reply_code); $var(cra_source)="platform-final"; route(CRA_EMIT_AVP);', '$var(cra_kind)="END"; $var(cra_code)=$var(s28_reply_code); $var(cra_source)="platform-final"; route(CRA_EMIT_AVP);\n $var(cra_kind)="CLOSE"; $var(cra_source)="platform-closed"; route(CRA_EMIT_AVP);')
|
||||
.replace('$var(cra_kind)="END"; $var(cra_code)=200; $var(cra_source)="verified-dialog-final"; route(CRA_EMIT_DLG);', '$var(cra_kind)="END"; $var(cra_code)=200; $var(cra_source)="verified-dialog-final"; route(CRA_EMIT_DLG);\n $var(cra_kind)="CLOSE"; $var(cra_source)="dialog-closed"; route(CRA_EMIT_DLG);')
|
||||
.replace('$var(cra_kind)="END"; $var(cra_code)=408; $var(cra_source)="verified-dialog-final"; route(CRA_EMIT_DLG);', '$var(cra_kind)="END"; $var(cra_code)=408; $var(cra_source)="verified-dialog-final"; route(CRA_EMIT_DLG);\n $var(cra_kind)="CLOSE"; $var(cra_source)="dialog-timeout"; route(CRA_EMIT_DLG);')
|
||||
.replace('$var(cra_kind)="END"; $var(cra_code)=$T_reply_code; $var(cra_source)="verified-dialog-final"; route(CRA_EMIT_DLG);', '$var(cra_kind)="END"; $var(cra_code)=$T_reply_code; $var(cra_source)="verified-dialog-final"; route(CRA_EMIT_DLG);\n $var(cra_kind)="CLOSE"; $var(cra_source)="attempts-exhausted"; route(CRA_EMIT_DLG);')
|
||||
.replaceAll('xlog("L_NOTICE", "CRA1|', 'update_stat("cra_event_total", 1);\n xlog("L_NOTICE", "CRA1|');
|
||||
writeFileSync(output, versionMarker + cfg, 'utf8');
|
||||
console.log('Analytics observation hooks upgraded to version 2; OpenSIPS config validation is required before activation.');
|
||||
process.exit(0);
|
||||
}
|
||||
registerStats();
|
||||
insert(' if (has_totag()) {\n', ackHook, true);
|
||||
const metadata = {
|
||||
uid: '$ci + "~" + $ft + "~" + $TS', callid: '$ci', customer: '$var(s28_customer_id)',
|
||||
gateway: '$var(s28_gateway_id)', vendor: '$var(s28_vendor_id)', vendorGateway: '$var(s28_vendor_gateway_id)',
|
||||
@@ -20,23 +38,26 @@ const bind = Object.keys(metadata).map(k => ` $dlg_val(cra_${k}) = $avp(cra_$
|
||||
insert(' $dlg_val(s28_customer_id) = $var(s28_customer_id);', `${bind}\n dlg_on_hangup("CRA_HANGUP");\n dlg_on_timeout("CRA_TIMEOUT");\n`);
|
||||
insert(' t_on_reply("S28_REPLY");', ' $dlg_val(cra_attempt)="1"; $avp(cra_attempt)="1";\n $var(cra_kind)="ATTEMPT"; $var(cra_code)=0; $var(cra_source)="opensips"; route(CRA_EMIT_DLG);\n t_on_failure("CRA_FAILURE");\n');
|
||||
insert('onreply_route[S28_REPLY] {', '\n if ($rs==180 || $rs==183 || $rs==100 || $rs==181 || $rs==182) {\n $var(cra_kind)="PROGRESS"; $var(cra_code)=$rs; $var(cra_source)="opensips"; route(CRA_EMIT_DLG);\n }\n if ($rs>=200 && $rs<300) {\n $var(cra_kind)="ACCEPTED"; $var(cra_code)=$rs; $var(cra_source)="opensips"; route(CRA_EMIT_DLG);\n }\n', true);
|
||||
insert('route[S28_CDR_FAILURE] {', '\n $var(cra_kind)="END"; $var(cra_code)=$var(s28_reply_code); $var(cra_source)="platform-final"; route(CRA_EMIT_AVP);\n', true);
|
||||
insert('route[S28_CDR_FAILURE] {', '\n $var(cra_kind)="END"; $var(cra_code)=$var(s28_reply_code); $var(cra_source)="platform-final"; route(CRA_EMIT_AVP);\n $var(cra_kind)="CLOSE"; $var(cra_source)="platform-closed"; route(CRA_EMIT_AVP);\n', true);
|
||||
const fields = ['uid', 'callid', 'customer', 'gateway', 'vendor', 'vendorGateway', 'caller', 'landing', 'callee', 'city', 'carrier', 'attempt'];
|
||||
function emitter(type, prefix) {
|
||||
const ref = k => `$${prefix}(cra_${k})`;
|
||||
const encoded = fields.map(k => `$(${prefix}(cra_${k}){s.b64encode})`).join('|');
|
||||
return `\nroute[CRA_EMIT_${type}] {\n if (${ref('customer')}==NULL || ${ref('customer')}=="none") return;\n xlog("L_NOTICE", "CRA1|$var(cra_kind)|$Ts|$Tsm|${ref('startsec')}|${ref('startmicro')}|$var(cra_code)|$var(cra_source)|${encoded}\\n");\n}\n`;
|
||||
return `\nroute[CRA_EMIT_${type}] {\n if (${ref('customer')}==NULL || ${ref('customer')}=="none") return;\n update_stat("cra_event_total", 1);\n xlog("L_NOTICE", "CRA1|$var(cra_kind)|$Ts|$Tsm|${ref('startsec')}|${ref('startmicro')}|$var(cra_code)|$var(cra_source)|${encoded}\\n");\n}\n`;
|
||||
}
|
||||
cfg += emitter('AVP', 'avp') + emitter('DLG', 'dlg_val') + `
|
||||
route[CRA_HANGUP] {
|
||||
$var(cra_kind)="END"; $var(cra_code)=200; $var(cra_source)="verified-dialog-final"; route(CRA_EMIT_DLG);
|
||||
$var(cra_kind)="CLOSE"; $var(cra_source)="dialog-closed"; route(CRA_EMIT_DLG);
|
||||
}
|
||||
route[CRA_TIMEOUT] {
|
||||
$var(cra_kind)="END"; $var(cra_code)=408; $var(cra_source)="verified-dialog-final"; route(CRA_EMIT_DLG);
|
||||
$var(cra_kind)="CLOSE"; $var(cra_source)="dialog-timeout"; route(CRA_EMIT_DLG);
|
||||
}
|
||||
failure_route[CRA_FAILURE] {
|
||||
$var(cra_kind)="END"; $var(cra_code)=$T_reply_code; $var(cra_source)="verified-dialog-final"; route(CRA_EMIT_DLG);
|
||||
$var(cra_kind)="CLOSE"; $var(cra_source)="attempts-exhausted"; route(CRA_EMIT_DLG);
|
||||
}
|
||||
`;
|
||||
writeFileSync(output, cfg, 'utf8');
|
||||
writeFileSync(output, versionMarker + cfg, 'utf8');
|
||||
console.log('Analytics observation hooks generated; OpenSIPS config validation is required before activation.');
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Retention is intentionally shorter for raw evidence than aggregate trends.
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import assert from 'node:assert/strict';
|
||||
const apply=process.argv.includes('--apply'); const db=new PrismaClient();
|
||||
const days=(name,fallback)=>{const value=Number(process.env[name]??fallback);assert(Number.isInteger(value)&&value>=7&&value<=730,`Invalid ${name}`);return value;};
|
||||
const eventBefore=new Date(Date.now()-days('ANALYTICS_EVENT_RETENTION_DAYS',30)*86400000);
|
||||
const stateBefore=new Date(Date.now()-days('ANALYTICS_STATE_RETENTION_DAYS',90)*86400000);
|
||||
const bucketBefore=new Date(Date.now()-days('ANALYTICS_BUCKET_RETENTION_DAYS',400)*86400000);
|
||||
try{
|
||||
const counts={events:await db.callerAnalysisEvent.count({where:{createdAt:{lt:eventBefore}}}),states:await db.callerAnalysisState.count({where:{startedAt:{lt:stateBefore}}}),buckets:await db.callerAnalysisMinuteBucket.count({where:{startedAt:{lt:bucketBefore}}}),deadLetters:await db.callerAnalysisDeadLetter.count({where:{status:{not:'OPEN'},createdAt:{lt:stateBefore}}}),gaps:await db.callerAnalysisGap.count({where:{status:{not:'OPEN'},startedAt:{lt:stateBefore}}})};
|
||||
if(apply)await db.$transaction(async tx=>{await tx.callerAnalysisEvent.deleteMany({where:{createdAt:{lt:eventBefore}}});await tx.callerAnalysisMinuteBucket.deleteMany({where:{startedAt:{lt:bucketBefore}}});await tx.callerAnalysisState.deleteMany({where:{startedAt:{lt:stateBefore}}});await tx.$executeRaw`DELETE c FROM caller_analysis_calls c LEFT JOIN caller_analysis_states s ON s.call_key=c.id WHERE s.id IS NULL AND c.updated_at<${stateBefore}`;await tx.callerAnalysisDeadLetter.deleteMany({where:{status:{not:'OPEN'},createdAt:{lt:stateBefore}}});await tx.callerAnalysisGap.deleteMany({where:{status:{not:'OPEN'},startedAt:{lt:stateBefore}}});},{timeout:60000});
|
||||
console.log(JSON.stringify({apply,cutoffs:{eventBefore,stateBefore,bucketBefore},counts}));
|
||||
}finally{await db.$disconnect();}
|
||||
Reference in New Issue
Block a user