feat: enhance health checks in docker-compose and improve WebRTC stats collection

This commit is contained in:
2025-02-09 02:10:53 +01:00
parent be0e0f8153
commit 7b78f54510
6 changed files with 138 additions and 268 deletions

View File

@@ -33,11 +33,6 @@ chrome.runtime.onInstalled.addListener(async ({ reason }) => {
...options,
});
}
await chrome.alarms.create("webrtc-internals-exporter-alarm", {
delayInMinutes: 1,
periodInMinutes: 1,
});
});
async function updateTabInfo(tab) {
@@ -104,76 +99,8 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
await updateTabInfo({ id: tabId, url: changeInfo.url });
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "webrtc-internals-exporter-alarm") {
cleanupPeerConnections().catch((err) => {
log(`cleanup peer connections error: ${err.message}`);
});
}
});
async function setPeerConnectionLastUpdate({ id, origin }, lastUpdate = 0) {
let { peerConnectionsLastUpdate } = await chrome.storage.local.get(
"peerConnectionsLastUpdate",
);
if (!peerConnectionsLastUpdate) {
peerConnectionsLastUpdate = {};
}
if (lastUpdate) {
peerConnectionsLastUpdate[id] = { origin, lastUpdate };
} else {
delete peerConnectionsLastUpdate[id];
}
await chrome.storage.local.set({ peerConnectionsLastUpdate });
const peerConnectionsPerOrigin = {};
Object.values(peerConnectionsLastUpdate).forEach(({ origin: o }) => {
if (!peerConnectionsPerOrigin[o]) {
peerConnectionsPerOrigin[o] = 0;
}
peerConnectionsPerOrigin[o]++;
});
await chrome.storage.local.set({ peerConnectionsPerOrigin });
await optionsUpdated();
}
async function cleanupPeerConnections() {
let { peerConnectionsLastUpdate } = await chrome.storage.local.get(
"peerConnectionsLastUpdate",
);
if (
!peerConnectionsLastUpdate ||
!Object.keys(peerConnectionsLastUpdate).length
) {
return;
}
log(
`checking stale peer connections (${
Object.keys(peerConnectionsLastUpdate).length
} total)`,
);
const now = Date.now();
await Promise.allSettled(
Object.entries(peerConnectionsLastUpdate)
.map(([id, { origin, lastUpdate }]) => {
if (
now - lastUpdate >
Math.max(2 * options.updateInterval, 30) * 1000
) {
return { id, origin };
}
})
.filter((ret) => !!ret?.id)
.map(({ id, origin }) => {
log(`removing stale peer connection metrics: ${id} ${origin}`);
return sendData("DELETE", { id, origin });
}),
);
}
// Send data to pushgateway.
async function sendData(method, { id, origin }, data) {
// Send data to POST handler.
async function sendJsonData(method, data) {
const { url, username, password, gzip, job } = options;
const headers = {
"Content-Type": "application/json",
@@ -185,59 +112,7 @@ async function sendData(method, { id, origin }, data) {
headers["Content-Encoding"] = "gzip";
data = await pako.gzip(data);
}
log(`sendData: ${data} \n ${data.length} bytes (gzip: ${gzip}) url: ${url} job: ${job}`);
const start = Date.now();
const response = await fetch(
`${url}/metrics/job/${job}/peerConnectionId/${id}`,
{
method,
headers,
body: method === "POST" ? data : undefined,
},
);
const stats = await chrome.storage.local.get([
"messagesSent",
"bytesSent",
"totalTime",
"errors",
]);
if (data) {
stats.messagesSent = (stats.messagesSent || 0) + 1;
stats.bytesSent = (stats.bytesSent || 0) + data.length;
stats.totalTime = (stats.totalTime || 0) + Date.now() - start;
}
if (!response.ok) {
stats.errors = (stats.errors || 0) + 1;
}
await chrome.storage.local.set(stats);
if (!response.ok) {
const text = await response.text();
throw new Error(`Response status: ${response.status} error: ${text}`);
}
await setPeerConnectionLastUpdate(
{ id, origin },
method === "POST" ? start : undefined,
);
return response.text();
}
async function sendJsonData(method, { id, origin }, data) {
const { url, username, password, gzip, job } = options;
const headers = {
"Content-Type": "application/json",
};
if (username && password) {
headers.Authorization = "Basic " + btoa(`${username}:${password}`);
}
if (data && gzip) {
headers["Content-Encoding"] = "gzip";
data = await pako.gzip(data);
}
log(`sendData: ${data} \n ${data.length} bytes (gzip: ${gzip}) url: ${url} job: ${job}`);
log(`sendJsonData: ${data} \n ${data.length} bytes (gzip: ${gzip}) url: ${url} job: ${job}`);
const start = Date.now();
const response = await fetch(
`${url}/${job}`,
@@ -269,90 +144,13 @@ async function sendJsonData(method, { id, origin }, data) {
throw new Error(`Response status: ${response.status} error: ${text}`);
}
await setPeerConnectionLastUpdate(
{ id, origin },
method === "POST" ? start : undefined,
);
return response.text();
}
const QualityLimitationReasons = {
none: 0,
bandwidth: 1,
cpu: 2,
other: 3,
};
/**
* sendPeerConnectionStats
* @param {string} url
* @param {string} id
* @param {RTCPeerConnectionState} state
* @param {any} values
*/
async function sendPeerConnectionStats(url, id, state, values) {
const origin = new URL(url).origin;
if (state === "closed") {
return sendData("DELETE", { id, origin });
}
let data = "";
const sentTypes = new Set();
values.forEach((value) => {
const type = value.type.replace(/-/g, "_");
const labels = [`pageUrl="${url}"`];
const metrics = [];
if (value.type === "peer-connection") {
labels.push(`state="${state}"`);
}
Object.entries(value).forEach(([key, v]) => {
if (typeof v === "number") {
metrics.push([key, v]);
} else if (typeof v === "object") {
Object.entries(v).forEach(([subkey, subv]) => {
if (typeof subv === "number") {
metrics.push([`${key}_${subkey}`, subv]);
}
});
} else if (
key === "qualityLimitationReason" &&
QualityLimitationReasons[v] !== undefined
) {
metrics.push([key, QualityLimitationReasons[v]]);
} else if (key === "googTimingFrameInfo") {
// TODO
} else {
labels.push(`${key}="${v}"`);
}
});
metrics.forEach(([key, v]) => {
const name = `${type}_${key.replace(/-/g, "_")}`;
let typeDesc = "";
if (!sentTypes.has(name)) {
typeDesc = `# TYPE ${name} gauge\n`;
sentTypes.add(name);
}
data += `${typeDesc}${name}{${labels.join(",")}} ${v}\n`;
});
});
if (data.length > 0) {
return sendData("POST", { id, origin }, data + "\n");
}
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.event === "peer-connection-stats") {
const { url, id, state, values } = message.data;
sendData("POST", { id, origin: new URL(url).origin }, JSON.stringify(message.data))
sendJsonData("POST", JSON.stringify(message.data))
.then(() => {
sendResponse({});
})
@@ -360,9 +158,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse({ error: err.message });
});
} else if (message.event === "peer-connections-stats") {
const { stats } = message.data;
sendJsonData("POST", { id: "all", origin: "all" }, JSON.stringify(message.data))
sendJsonData("POST", JSON.stringify(message.data))
.then(() => {
sendResponse({});
})
.catch((err) => {
sendResponse({ error: err.message });
});
} else {
sendResponse({ error: "unknown event" });
}

View File

@@ -79,19 +79,14 @@ if (window.location.protocol.startsWith("http")) {
// Handle stats messages.
window.addEventListener("message", async (message) => {
const { event, url, id, state, values, stats } = message.data;
const { event, data, stats } = message.data;
if (event === "webrtc-internal-exporter:ready") {
sendOptions();
} else if (event === "webrtc-internal-exporter:peer-connection-stats") {
try {
const response = await chrome.runtime.sendMessage({
event: "peer-connection-stats",
data: {
url,
id,
state,
values,
},
data: stats,
});
if (response.error) {
log(`error: ${response.error}`);
@@ -103,7 +98,7 @@ if (window.location.protocol.startsWith("http")) {
try {
const response = await chrome.runtime.sendMessage({
event: "peer-connections-stats",
data: stats,
data: data,
});
if (response.error) {
log(`error: ${response.error}`);

View File

@@ -31,15 +31,55 @@ class WebrtcInternalExporter {
);
}
/**
* @param {RTCPeerConnection} pc
*/
add(pc) {
const id = this.randomId();
pc.iceCandidates = [];
pc.iceCandidateErrors = [];
this.peerConnections.set(id, pc);
pc.addEventListener("connectionstatechange", () => {
if (pc.connectionState === "closed") {
this.peerConnections.delete(id);
}
});
//this.collectAndPostStats(id);
/**
* @param {RTCPeerConnectionIceErrorEvent} event
*/
pc.addEventListener("icecandidateerror", (event) => {
this.peerConnections.get(id).iceCandidateErrors.push({
timestamp: Date.now(),
address: event.errorAddress,
errorCode: event.errorCode,
errorText: event.errorText,
port: event.errorPort,
url: event.url,
});
});
/**
* @param {RTCPeerConnectionIceEvent} event
*/
pc.addEventListener("icecandidate", (event) => {
this.peerConnections.get(id).iceCandidates.push({
timestamp: Date.now(),
candidate: event.candidate?.candidate,
component: event.candidate?.component,
foundation: event.candidate?.foundation,
port: event.candidate?.port,
priority: event.candidate?.priority,
protocol: event.candidate?.protocol,
relatedAddress: event.candidate?.relatedAddress,
relatedPort: event.candidate?.relatedPort,
sdpMLineIndex: event.candidate?.sdpMLineIndex,
sdpMid: event.candidate?.sdpMid,
tcpType: event.candidate?.tcpType,
type: event.candidate?.type,
usernameFragment: event.candidate?.usernameFragment,
});
});
}
async collectAndPostSingleStat(id) {
@@ -49,9 +89,9 @@ class WebrtcInternalExporter {
window.postMessage(
{
event: "webrtc-internal-exporter:peer-connection-stats",
...stats,
stats
},
[stats],
stats
);
}
@@ -59,26 +99,30 @@ class WebrtcInternalExporter {
const stats = [];
for (const [id, pc] of this.peerConnections) {
if (this.url && this.enabled && pc.connectionState === "connected") {
if (this.url && this.enabled) {
const pcStats = await this.collectStats(id, pc);
stats.push(pcStats);
}
}
window.postMessage(
{
event: "webrtc-internal-exporter:peer-connections-stats",
stats,
},
[stats],
);
window.postMessage(
{
event: "webrtc-internal-exporter:peer-connections-stats",
data: JSON.parse(JSON.stringify(stats)),
},
);
log(`Stats collected:`, stats);
log(`Stats collected:`, JSON.parse(JSON.stringify(stats)));
setTimeout(this.collectAllStats.bind(this), this.updateInterval);
return stats;
setTimeout(this.collectAllStats.bind(this), this.updateInterval);
return stats;
}
/**
* @param {string} id
* @param {RTCPeerConnection} pc
* @param {Function} binding
*/
async collectStats(id, pc, binding) {
var completeStats = {};
@@ -87,7 +131,7 @@ class WebrtcInternalExporter {
if (!pc) return;
}
if (this.url && this.enabled && pc.connectionState === "connected") {
if (this.url && this.enabled) {
try {
const stats = await pc.getStats();
const values = [...stats.values()].filter(
@@ -98,7 +142,12 @@ class WebrtcInternalExporter {
completeStats = {
url: window.location.href,
id,
state: pc.connectionState,
connectionState: pc.connectionState,
iceConnectionState: pc.iceConnectionState,
iceGatheringState: pc.iceGatheringState,
signalingState: pc.signalingState,
iceCandidateErrors: pc.iceCandidateErrors,
iceCandidates: pc.iceCandidates,
values,
};
} catch (error) {