Feature/opentelemetry (#89)

* initial adds for otel tracing

* initial basic testing

* basic tracing for incoming calls

* linting

* add traceId to the webhook params

* trace webhook calls

* tracing: add new commands as tags when receiving async commands over websocket

* tracing new commands

* add summary for config verb

* trace async commands

* bugfix: undefined ref

* tracing: give time for final webhooks before closing root span

* tracing bugfix: span for background gather was not ended

* tracing - minor tag changes

* tracing - add span atttribute for reason call ended

* trace call status webhooks, add app version to trace output

* config: add support for automatically re-enabling

* env var to customize service name in tracing UI

* config: change to use 'sticky' attribute to re-enable bargein automatically

* fix warnings

* when adulting create a new root span

* when background gather triggers bargein via vad clear queue of tasks

* additional trace attributes for dial and refer

* fix dial tracing

* add better summary for dial

* fix prev commit

* add exponential backoff to WsRequestor reconnection logic

* add calling number to log metadata, as this will be frequently the key data given for troubleshooting

* add accountSid to log metadata

* make handshake timeout for ws connections configurable with default 1.5 secs

* rename env var

* fix bug prev checkin

* logging fixes

* consistent env naming
This commit is contained in:
Dave Horton
2022-03-28 15:38:28 -04:00
committed by GitHub
parent f1f83598ca
commit 6abfdafe05
24 changed files with 1852 additions and 87 deletions

View File

@@ -24,6 +24,7 @@ class TaskConfig extends Task {
].forEach((k) => {
if (this.bargeIn[k]) this.gatherOpts[k] = this.bargeIn[k];
});
if (this.bargeIn.sticky) this.autoEnable = true;
}
this.preconditions = this.hasBargeIn ? TaskPreconditions.Endpoint : TaskPreconditions.None;
}
@@ -36,6 +37,22 @@ class TaskConfig extends Task {
get hasBargeIn() { return Object.keys(this.bargeIn).length; }
get summary() {
const phrase = [];
if (this.hasBargeIn) phrase.push('enable barge-in');
if (this.hasSynthesizer) {
const {vendor:v, language:l, voice} = this.synthesizer;
const s = `{${v},${l},${voice}}`;
phrase.push(`set synthesizer${s}`);
}
if (this.hasRecognizer) {
const {vendor:v, language:l} = this.recognizer;
const s = `{${v},${l}}`;
phrase.push(`set recognizer${s}`);
}
return `${this.name}{${phrase.join(',')}`;
}
async exec(cs) {
await super.exec(cs);
@@ -63,7 +80,7 @@ class TaskConfig extends Task {
if (this.hasBargeIn) {
if (this.gatherOpts) {
this.logger.debug({opts: this.gatherOpts}, 'Config: enabling bargeIn');
cs.enableBotMode(this.gatherOpts);
cs.enableBotMode(this.gatherOpts, this.autoEnable);
}
else {
this.logger.debug('Config: disabling bargeIn');

View File

@@ -138,7 +138,20 @@ class TaskDial extends Task {
}
get summary() {
if (this.target.length === 1) return `${this.name}{type=${this.target[0].type}}`;
if (this.target.length === 1) {
const target = this.target[0];
switch (target.type) {
case 'phone':
case 'teams':
return `${this.name}{type=${target.type},number=${target.number}}`;
case 'user':
return `${this.name}{type=${target.type},name=${target.name}}`;
case 'sip':
return `${this.name}{type=${target.type},sipUri=${target.sipUri}}`;
default:
return `${this.name}`;
}
}
else return `${this.name}{${this.target.length} targets}`;
}
@@ -384,6 +397,7 @@ class TaskDial extends Task {
this._killOutdials();
}, this.timeout * 1000);
this.span.setAttributes('dial.target', JSON.stringify(this.target));
this.target.forEach(async(t) => {
try {
t.url = t.url || this.confirmUrl;
@@ -420,7 +434,9 @@ class TaskDial extends Task {
target: t,
opts,
callInfo: cs.callInfo,
accountInfo: cs.accountInfo
accountInfo: cs.accountInfo,
rootSpan: cs.rootSpan,
startSpan: this.startSpan.bind(this)
});
this.dials.set(sd.callSid, sd);

View File

@@ -397,6 +397,7 @@ class TaskGather extends Task {
if (this.bargein && this.minBargeinWordCount === 0) {
this.logger.debug('TaskGather:_onVadDetected');
this._killAudio(cs);
this.emit('vad');
}
}
@@ -407,9 +408,10 @@ class TaskGather extends Task {
async _resolve(reason, evt) {
if (this.resolved) return;
this.resolved = true;
this.logger.debug(`TaskGather:resolve with reason ${reason}`);
this.logger.info(`TaskGather:resolve with reason ${reason}`);
clearTimeout(this.interDigitTimer);
this.span.setAttributes({'stt.resolve': reason, 'stt.result': JSON.stringify(evt)});
if (this.ep && this.ep.connected) {
this.ep.stopTranscription({vendor: this.vendor})
.catch((err) => this.logger.error({err}, 'Error stopping transcription'));

View File

@@ -13,6 +13,10 @@ class TaskPlay extends Task {
get name() { return TaskName.Play; }
get summary() {
return `${this.name}:{url=${this.url}}`;
}
async exec(cs, ep) {
await super.exec(cs);
this.ep = ep;

View File

@@ -19,7 +19,7 @@ class TaskSay extends Task {
if (this.text[i].startsWith('silence_stream')) continue;
return `${this.name}{text=${this.text[i].slice(0, 15)}${this.text[i].length > 15 ? '...' : ''}}`;
}
return this.text[0];
return `${this.name}{${this.text[0]}}`;
}
async exec(cs, ep) {
@@ -44,6 +44,7 @@ class TaskSay extends Task {
this.logger.info({vendor, language, voice}, 'TaskSay:exec');
this.ep = ep;
let span;
try {
if (!credentials) {
writeAlerts({
@@ -55,6 +56,14 @@ class TaskSay extends Task {
}
// synthesize all of the text elements
let lastUpdated = false;
/* otel: trace time for tts */
span = this.startSpan('tts-generation', {
'tts.vendor': vendor,
'tts.language': language,
'tts.voice': voice
});
const filepath = (await Promise.all(this.text.map(async(text) => {
if (this.killed) return;
if (text.startsWith('silence_stream://')) return text;
@@ -82,9 +91,10 @@ class TaskSay extends Task {
updateSpeechCredentialLastUsed(credentials.speech_credential_sid)
.catch(() => {/*already logged error */});
}
span.setAttributes({'tts.cached': servedFromCache});
return filePath;
}))).filter((fp) => fp && fp.length);
span?.end();
this.logger.debug({filepath}, 'synthesized files for tts');
while (!this.killed && (this.loop === 'forever' || this.loop--) && this.ep?.connected) {
@@ -102,6 +112,7 @@ class TaskSay extends Task {
} while (!this.killed && ++segment < filepath.length);
}
} catch (err) {
span?.end();
this.logger.info(err, 'TaskSay:exec error');
}
this.emit('playDone');

View File

@@ -26,6 +26,12 @@ class TaskSipRefer extends Task {
try {
this.notifyHandler = this._handleNotify.bind(this, cs, dlg);
dlg.on('notify', this.notifyHandler);
/* otel: trace time for tts */
this.referSpan = this.startSpan('send-refer', {
'refer.refer_to': referTo,
'refer.referred_by': referredBy
});
const response = await dlg.request({
method: 'REFER',
headers: {
@@ -35,16 +41,20 @@ class TaskSipRefer extends Task {
}
});
this.referStatus = response.status;
this.referSpan.setAttributes({'refer.status_code': response.status});
this.logger.info(`TaskSipRefer:exec - received ${this.referStatus} to REFER`);
/* if we fail, fall through to next verb. If success, we should get BYE from far end */
if (this.referStatus === 202) {
await this.awaitTaskDone();
}
else await this.performAction({refer_status: this.referStatus});
else {
await this.performAction({refer_status: this.referStatus});
}
} catch (err) {
this.logger.info({err}, 'TaskSipRefer:exec - error sending REFER');
}
this.referSpan?.end();
}
async kill(cs) {
@@ -69,6 +79,7 @@ class TaskSipRefer extends Task {
await cs.requestor.request('verb:hook', this.eventHook, {event: 'transfer-status', call_status: status});
}
if (status >= 200) {
this.referSpan.setAttributes({'refer.finalNotify': status});
await this.performAction({refer_status: 202, final_referred_call_status: status});
this.notifyTaskDone();
}

View File

@@ -32,6 +32,7 @@
"bargeIn": {
"properties": {
"enable": "boolean",
"sticky": "boolean",
"actionHook": "object|string",
"input": "array",
"finishOnKey": "string",

View File

@@ -4,6 +4,7 @@ const debug = require('debug')('jambonz:feature-server');
const assert = require('assert');
const {TaskPreconditions} = require('../utils/constants');
const normalizeJambones = require('../utils/normalize-jambones');
const {trace} = require('@opentelemetry/api');
const specs = new Map();
const _specData = require('./specs');
for (const key in _specData) {specs.set(key, _specData[key]);}
@@ -71,6 +72,15 @@ class Task extends Emitter {
setImmediate(() => this.parentTask = null);
}
startSpan(name, attributes) {
const {srf} = require('../..');
const {tracer} = srf.locals.otel;
const span = tracer.startSpan(name, undefined, this.ctx);
if (attributes) span.setAttributes(attributes);
trace.setSpan(this.ctx, span);
return span;
}
/**
* when a subclass Task has completed its work, it should call this method
*/
@@ -111,29 +121,49 @@ class Task extends Emitter {
async performAction(results, expectResponse = true) {
if (this.actionHook) {
const params = results ? Object.assign(results, this.cs.callInfo.toJSON()) : this.cs.callInfo.toJSON();
const json = await this.cs.requestor.request('verb:hook', this.actionHook, params);
if (expectResponse && json && Array.isArray(json)) {
const makeTask = require('./make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
this.callSession.replaceApplication(tasks);
const span = this.startSpan('verb:hook', {'hook.url': this.actionHook});
span.setAttributes({'http.body': JSON.stringify(params)});
try {
const json = await this.cs.requestor.request('verb:hook', this.actionHook, params);
span.setAttributes({'http.statusCode': 200});
span.end();
if (expectResponse && json && Array.isArray(json)) {
const makeTask = require('./make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
this.callSession.replaceApplication(tasks);
}
}
} catch (err) {
span.setAttributes({'http.statusCode': err.statusCode});
span.end();
throw err;
}
}
}
async performHook(cs, hook, results) {
const json = await cs.requestor.request('verb:hook', hook, results);
if (json && Array.isArray(json)) {
const makeTask = require('./make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
this.redirect(cs, tasks);
return true;
const span = this.startSpan('verb:hook', {'hook.url': hook});
span.setAttributes({'http.body': JSON.stringify(results)});
try {
const json = await cs.requestor.request('verb:hook', hook, results);
span.setAttributes({'http.statusCode': 200});
span.end();
if (json && Array.isArray(json)) {
const makeTask = require('./make_task');
const tasks = normalizeJambones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
this.redirect(cs, tasks);
return true;
}
}
return false;
} catch (err) {
span.setAttributes({'http.statusCode': err.statusCode});
span.end();
throw err;
}
return false;
}
redirect(cs, tasks) {