Compare commits

...

11 Commits

Author SHA1 Message Date
bdd5a20117 Refactor GitHub Actions workflow to install LaTeX dependencies and build document directly, updating artifact upload step
Some checks failed
Build LaTeX Document / build_latex (pull_request) Failing after 55s
2025-02-09 22:55:30 +01:00
2adb70e448 Update actions/checkout and actions/upload-artifact versions in workflow
Some checks failed
Build LaTeX Document / build_latex (pull_request) Failing after 10s
2025-02-09 22:05:35 +01:00
740f8b52a5 Add Docker image specification for LaTeX action in workflow
Some checks failed
Build LaTeX Document / build_latex (pull_request) Failing after 11s
2025-02-09 18:16:45 +01:00
978506f9fd Update LaTeX action version in GitHub Actions workflow
Some checks failed
Build LaTeX Document / build_latex (pull_request) Has been cancelled
2025-02-09 18:03:11 +01:00
50e6ca1b7a Merge branch 'main' into feature/dockerize-collector
Some checks failed
Build LaTeX Document / build_latex (pull_request) Failing after 11s
2025-02-09 17:52:39 +01:00
fe71b6986e Add GitHub Actions workflow for building LaTeX document
Some checks failed
Build LaTeX Document / build_latex (pull_request) Failing after 14m43s
2025-02-09 17:42:18 +01:00
37de0dedce Update submodule commit for Peertube statnerd 2025-02-09 17:18:04 +01:00
12cae3e82e Remove outdated instructions from PeerTube README 2025-02-06 22:45:47 +01:00
6b648f5491 Add submodule for Peertube collector 2025-02-05 02:01:45 +01:00
6ba64413d1 wip: moving to submobule 2025-02-05 01:54:04 +01:00
ad97310d7e Add Docker support and update requirements for data collection and visualization 2025-02-05 01:44:39 +01:00
34 changed files with 37 additions and 1082 deletions

32
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Build LaTeX Document
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build_latex:
runs-on: ubuntu-latest
container:
steps:
- name: Install Node.js
run: |
apt update && apt install -y curl
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
apt install -y nodejs
- name: Set up Git repository
uses: actions/checkout@v4.2.2
- name: Install LaTeX
run: sudo apt update && sudo apt install texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended
- name: Build LaTeX document
run: pdflatex -interaction=nonstopmode -halt-on-error Tesi.tex
- name: Upload PDF artifact
uses: actions/upload-artifact@v4
with:
path: Test.pdf

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "peertube/statnerd"]
path = peertube/statnerd
url = ssh://git@gitea.kobim.cloud:30009/kobim/peertube-collector.git

1
peertube/README.md Normal file
View File

@@ -0,0 +1 @@
# TODO

View File

@@ -1,5 +1,3 @@
selenium
beautifulsoup4
pymongo
pandas
matplotlib

1
peertube/statnerd Submodule

Submodule peertube/statnerd added at 76edbf0624

View File

@@ -1,17 +0,0 @@
## Statistiche per nerd da Peer Tube
spero funzioni
per installarlo baste creare un nuovo ambiente e installare le dipendenze con:
```
python -m venv env
source .\env\bin\activate
pip install -r requirements.txt
```
per eseguire il tutto basta lanciare:
```
jupyter-lab
```

View File

@@ -1,190 +0,0 @@
import signal
import json
import time
import socket
import logging
import os
from functools import partial
from http.server import HTTPServer
from utils.PostHandler import Handler
from utils.ColoredFormatter import ColoredFormatter
from bs4 import BeautifulSoup as bs
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec
logger = logging.getLogger(__name__)
def setupLogger():
logging_format = "[%(asctime)s] (%(levelname)s) %(module)s - %(funcName)s: %(message)s"
logging.basicConfig(level=logging.INFO, format=logging_format)
(logger := logging.getLogger(__name__)).setLevel(logging.INFO)
logger.propagate = False
(logger_handler := logging.StreamHandler()).setFormatter(
ColoredFormatter(fmt=logging_format)
)
logger.addHandler(logger_handler)
def interrupt_handler(signum, driver: webdriver.Chrome):
logger.info(f'Handling signal {signum} ({signal.Signals(signum).name}).')
driver.quit()
raise SystemExit
def setupChromeDriver():
logger.log(logging.INFO, 'Setting up Chrome driver.')
chrome_options = Options()
#chrome_options.add_argument("--headless")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--mute-audio")
chrome_options.add_argument("--window-size=1280,720")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--no-default-browser-check")
chrome_options.add_argument("--disable-features=WebRtcHideLocalIpsWithMdns")
chrome_options.add_argument(f"--load-extension={os.path.abspath(os.path.join(os.path.dirname(__file__), 'webrtc-internals-exporter'))}")
chrome_options.add_experimental_option('prefs', {'intl.accept_languages': 'en,en_US'})
driver = webdriver.Chrome(options=chrome_options)
#driver = webdriver.Remote(command_executor='http://localhost:4444', options=chrome_options)
logger.log(logging.INFO, 'Chrome driver setup complete.')
return driver
def saveStats(stats: list):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
logger.log(logging.DEBUG, f'Saving stats: {json.dumps(stats, indent=4)}')
sock.sendto(json.dumps(stats).encode(), ('localhost', 8094))
sock.close()
logger.log(logging.DEBUG, 'Sent stats to socket.')
except socket.error as e:
logger.error(f'Got socket error: {e}')
def downloadStats(driver: webdriver.Chrome, peersDict: dict):
html = driver.find_element(By.CLASS_NAME ,'vjs-stats-list').get_attribute('innerHTML')
if html is not None:
htmlBS = bs(html, 'html.parser')
else:
raise ValueError("html is None")
stats = htmlBS.find_all('div', attrs={'style': 'display: block;'})
playerStats = {
stat.div.text: stat.span.text.replace('\u21d3', 'down').replace('down/', 'down /').replace('\u21d1 ', 'up').replace('\u21d1', 'up').replace('\u00b7', '-').strip()
for stat in stats
}
keys = list(playerStats.keys())
for stat in keys:
if 'Viewport / Frames' == stat:
viewport, frames = playerStats[stat].split(' / ')
width, height = viewport.split('x')
height, devicePixelRatio = height.split('*')
dropped, total = frames.split(' of ')[0].split()[0], frames.split(' of ')[1].split()[0]
playerStats[stat] = {'Width': int(width), 'Height': int(height), 'Pixel ratio': float(devicePixelRatio), 'Frames': {'Dropped': int(dropped), 'Total': int(total)}}
if 'Codecs' == stat:
video, audio = playerStats[stat].split(' / ')
playerStats[stat] = {'Video': video, 'Audio': audio}
if 'Volume' == stat:
if ' (' in playerStats[stat]:
volume, muted = playerStats[stat].split(' (')
playerStats[stat] = {'Volume': int(volume), 'Muted': 'muted' in muted}
else:
playerStats[stat] = {'Volume': int(playerStats[stat]), 'Muted': False}
if 'Connection Speed' == stat:
speed, unit = playerStats[stat].split()
speedBytes = int(speed) * (1024 ** {'B/s': 0, 'KB/s': 1, 'MB/s': 2, 'GB/s': 3}[unit])
playerStats[stat] = {'Speed': int(speedBytes), 'Granularity': 's'}
if 'Network Activity' == stat:
downString, upString = playerStats[stat].split(' / ')
down, downUnit = downString.replace('down', '').strip().split()
up, upUnit = upString.replace('up', '').strip().split()
downBytes = int(down) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[downUnit])
upBytes = int(up) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[upUnit])
playerStats[stat] = {'Down': downBytes, 'Up': upBytes}
if 'Total Transfered' == stat:
downString, upString = playerStats[stat].split(' / ')
down, downUnit = downString.replace('down', '').strip().split()
up, upUnit = upString.replace('up', '').strip().split()
downBytes = int(down) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[downUnit])
upBytes = int(up) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[upUnit])
playerStats[stat] = {'Down': downBytes, 'Up': upBytes}
if 'Download Breakdown' == stat:
server, peer = playerStats[stat].split(' - ')
server, serverUnit = server.replace('from servers', '').strip().split()
peer, peerUnit = peer.replace('from peers', '').strip().split()
serverBytes = int(server) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[serverUnit])
peerBytes = int(peer) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[peerUnit])
playerStats[stat] = {'Server': serverBytes, 'Peers': peerBytes}
if 'Buffer State' == stat:
del(playerStats[stat])
if 'Live Latency' == stat:
latency, edge = playerStats[stat].split(' (from edge: ')
latency = sum(int(x) * 60 ** i for i, x in enumerate(reversed([part for part in latency.replace('s', '').split('m') if part])))
edge = sum(int(x) * 60 ** i for i, x in enumerate(reversed([part for part in edge.replace('s', '').replace(')', '').split('m') if part])))
playerStats[stat] = {'Latency': latency, 'Edge': edge}
stats = {
'player': playerStats,
'peers': peersDict,
'url': driver.current_url,
'timestamp': int(time.time() * 1000),
'session': driver.session_id
}
saveStats([stats])
def setupStats(driver: webdriver.Chrome, url: str):
logger.log(logging.INFO, 'Setting up stats.')
actions = ActionChains(driver)
wait = WebDriverWait(driver, 30, poll_frequency=0.2)
driver.get(url)
wait.until(ec.presence_of_element_located((By.CLASS_NAME, 'vjs-big-play-button')))
actions.click(driver.find_element(By.CLASS_NAME ,'video-js')).perform()
wait.until(ec.visibility_of_element_located((By.CLASS_NAME, 'vjs-control-bar')))
actions.context_click(driver.find_element(By.CLASS_NAME ,'video-js')).perform()
statsForNerds = driver.find_elements(By.CLASS_NAME ,'vjs-menu-item')
actions.click(statsForNerds[-1]).perform()
wait.until(ec.text_to_be_present_in_element((By.CLASS_NAME, 'vjs-stats-list'), 'Player'))
actions.move_to_element(driver.find_element(By.CLASS_NAME ,'vjs-control-bar')).perform()
logger.log(logging.INFO, 'Stats setup complete.')
return driver
if __name__ == '__main__':
setupLogger()
driver = setupChromeDriver()
signal.signal(signal.SIGINT, lambda signum, frame: interrupt_handler(signum, driver))
setupStats(driver, "https://tube.kobim.cloud/w/iN2T8PmbSb4HJTDA2rV3sg")
logger.log(logging.INFO, 'Starting server collector.')
httpd = HTTPServer(('localhost', 9092), partial(Handler, downloadStats, driver, logger))
logger.info('Server collector started.')
httpd.serve_forever()

View File

@@ -1,33 +0,0 @@
[[processors.dedup]]
dedup_interval = "600s"
[[inputs.socket_listener]]
service_address = "udp://:8094"
data_format = "xpath_json"
[[inputs.socket_listener.xpath]]
metric_name = "'peertube'"
metric_selection = "/*"
timestamp = "timestamp"
timestamp_format = "unix_ms"
[inputs.socket_listener.xpath.tags]
url = "url"
session = "session"
#id = ??
#state = ??
[inputs.socket_listener.xpath.fields]
player = "player"
peers = "peers"
[[outputs.file]]
files = ["stdout"]
data_format = "json"
[[outputs.mongodb]]
dsn = "mongodb://stats_user:%40z%5EVFhN7q%25vzit@192.168.86.120:27017/?authSource=statistics"
database = "statistics"
granularity = "seconds"
# docker run --rm -v .\peertube\statnerd\telegraf.conf:/etc/telegraf/telegraf.conf:ro -p 8094:8094/udp telegraf

View File

@@ -1,43 +0,0 @@
import logging
class ColoredFormatter(logging.Formatter):
"""Colored formatter for the logging package."""
def __init__(
self, fmt=None, datefmt=None, style="%", validate=True, *, defaults=None
):
"""Colored formatter for the logging package."""
fmt = fmt or "%(levelname)s: %(message)s"
super().__init__(fmt, datefmt, style, validate, defaults=defaults)
colors = {
"red": "\x1b[31;20m",
"bold_red": "\x1b[31;1m",
"green": "\x1b[32;20m",
"bold_green": "\x1b[32;1m",
"yellow": "\x1b[33;20m",
"bold_yellow": "\x1b[33;1m",
"blue": "\x1b[34;20m",
"bold_blue": "\x1b[34;1m",
"grey": "\x1b[37;20m",
"bold_grey": "\x1b[37;1m",
"reset": "\x1b[0m",
}
self._default_formatter = logging.Formatter(fmt)
self._formatters = {
100: logging.Formatter(colors["bold_blue"] + fmt + colors["reset"]),
logging.DEBUG: logging.Formatter(colors["grey"] + fmt + colors["reset"]),
logging.INFO: logging.Formatter(colors["green"] + fmt + colors["reset"]),
logging.WARNING: logging.Formatter(
colors["yellow"] + fmt + colors["reset"]
),
logging.ERROR: logging.Formatter(colors["red"] + fmt + colors["reset"]),
logging.CRITICAL: logging.Formatter(
colors["bold_red"] + fmt + colors["reset"]
),
}
def format(self, record):
"""Override of logging.Formatter.format"""
return self._formatters.get(record.levelno, self._default_formatter).format(
record
)

View File

@@ -1,24 +0,0 @@
import json
import logging
from http.server import BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def __init__(self, custom_func, driver, logger, *args, **kwargs):
self._custom_func = custom_func
self.logger = logger
self.driver = driver
super().__init__(*args, **kwargs)
def do_POST(self):
if self.path == '/webrtc-internals-exporter':
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
self.logger.log(logging.DEBUG, f"POST request,\nPath: {self.path}\nHeaders:\n{self.headers}\n\nBody:\n{post_data.decode('utf-8')}")
self._custom_func(self.driver, json.loads(post_data.decode('utf-8')))
self.send_response(200)
self.end_headers()
self.wfile.write(b'POST request received')
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b'404 Not Found')

View File

@@ -1,37 +0,0 @@
# WebRTC Internals Exporter
A Chromium browser extension that allows to collect WebRTC stats and export them to a Prometheus PushGateway service.
## Install
### Using the Chrome Web Store
[Link](https://chromewebstore.google.com/detail/webrtc-internals-exporter/jbgkajlogkmfemdjhiiicelanbipacpa)
### Using the packed extension
Download the `.crx` file from the [releases page](https://github.com/vpalmisano/webrtc-internals-exporter/releases) and drop it
into the [chrome://extensions/](chrome://extensions/) page.
Alternatively, you can download a `.zip` or `tar.gz` file from the releases page
and load the decompressed folder as an unpacked extension.
Ref. https://developer.chrome.com/docs/extensions/mv3/hosting/
### From sources
Run the `./build.sh` script and load the `build` folder as an unpacked extension
in your Chromium browser after enabling the developer mode.
## Usage
1. Visit the extension options page, set the PushGateway URL and, optionally, the username and password.
2. Load the page where you want to collect the stats and click on the extension icon to enable the stats collection on that URL (disabled by default).
3. The stats will be collected and sent to the PushGateway service. You can use the provided [Grafana dashboard](https://github.com/vpalmisano/webrtc-internals-exporter/tree/main/grafana) to visualize them.
## Debugging
The extension logs are available in the browser console after setting:
```js
localStorage.setItem("webrtc-internal-exporter:debug", "true")
```
The running PeerConnections objects can be manually inspected using the following
command in the browser console:
```js
> webrtcInternalExporter.peerConnections
Map(1) {'b03c3616-3f91-42b5-85df-7dbebefae8bd' => RTCPeerConnection}
```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.version[data-v-4cce5501]{text-decoration:none}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{_ as y,a as O,h as o,j as c,k as _,l as a,m as t,x,q as w,t as k,z as V}from"./_plugin-vue_export-helper-deb87276.js";const C={class:"version",href:"https://github.com/vpalmisano/webrtc-internals-exporter",target:"_blank",title:"Homepage"},B={__name:"Popup",setup(N){var i;const e=O({version:"0.1.9",error:"",info:"",enabled:!1,origin:"",enabledOrigins:{}});async function d(){const{enabledOrigins:n}=await chrome.storage.sync.get("enabledOrigins");e.enabledOrigins=n,e.enabled=!!e.enabledOrigins[e.origin]}async function p(n){if(console.log("saveOptions",e.origin,n),n)e.enabledOrigins={...e.enabledOrigins,[e.origin]:n};else{const r={...e.enabledOrigins};delete r[e.origin],e.enabledOrigins=r}chrome.storage&&await chrome.storage.sync.set({enabledOrigins:e.enabledOrigins})}return(i=chrome.tabs)==null||i.query({active:!0,currentWindow:!0}).then(n=>{const r=n[0];return chrome.scripting.executeScript({target:{tabId:r.id},function:()=>window.location.origin})}).then(n=>{e.origin=n[0].result}).then(()=>d()).catch(n=>{console.error("Load options error:",n),e.error=`Load options error: ${n.message}`}),(n,r)=>{const u=o("v-app-bar"),g=o("v-alert"),m=o("v-checkbox"),s=o("v-col"),l=o("v-row"),b=o("v-container"),v=o("v-main"),f=o("v-layout");return c(),_(f,null,{default:a(()=>[t(u,{title:"WebRTC Internals Exporter",color:"primary",density:"compact"}),t(v,{class:"d-flex align-center justify-left",style:{"min-width":"20rem"}},{default:a(()=>[t(b,null,{default:a(()=>[t(l,null,{default:a(()=>[t(s,{cols:"12",md:"12"},{default:a(()=>[e.error.length>0?(c(),_(g,{key:0,text:e.error,type:"error"},null,8,["text"])):x("",!0),t(m,{color:"indigo",modelValue:e.enabled,"onUpdate:modelValue":[r[0]||(r[0]=h=>e.enabled=h),p],label:"Enable for "+e.origin,"hide-details":""},null,8,["modelValue","label"])]),_:1})]),_:1}),t(l,null,{default:a(()=>[t(s,{cols:"12",md:"12"},{default:a(()=>[w("a",C,"v"+k(e.version),1)]),_:1})]),_:1})]),_:1})]),_:1})]),_:1})}}},I=y(B,[["__scopeId","data-v-16634bc6"]]);V(I);

View File

@@ -1 +0,0 @@
.version[data-v-16634bc6]{font-size:smaller;text-decoration:none}

View File

@@ -1,373 +0,0 @@
/* global chrome, pako */
function log(...args) {
console.log.apply(null, ["[webrtc-internal-exporter:background]", ...args]);
}
log("loaded");
import "/assets/pako.min.js";
const DEFAULT_OPTIONS = {
url: "http://localhost:9092",
username: "",
password: "",
updateInterval: 2,
gzip: false,
job: "webrtc-internals-exporter",
enabledOrigins: {
"https://tube.kobim.cloud": true,
},
enabledStats: ["data-channel", "local-candidate", "remote-candidate"]
};
const options = {};
// Handle install/update.
chrome.runtime.onInstalled.addListener(async ({ reason }) => {
log("onInstalled", reason);
if (reason === "install") {
await chrome.storage.sync.set(DEFAULT_OPTIONS);
} else if (reason === "update") {
const options = await chrome.storage.sync.get();
await chrome.storage.sync.set({
...DEFAULT_OPTIONS,
...options,
});
}
await chrome.alarms.create("webrtc-internals-exporter-alarm", {
delayInMinutes: 1,
periodInMinutes: 1,
});
});
async function updateTabInfo(tab) {
const tabId = tab.id;
const origin = new URL(tab.url || tab.pendingUrl).origin;
if (options.enabledOrigins && options.enabledOrigins[origin] === true) {
const { peerConnectionsPerOrigin } = await chrome.storage.local.get(
"peerConnectionsPerOrigin",
);
const peerConnections =
(peerConnectionsPerOrigin && peerConnectionsPerOrigin[origin]) || 0;
chrome.action.setTitle({
title: `WebRTC Internals Exporter\nActive Peer Connections: ${peerConnections}`,
tabId,
});
chrome.action.setBadgeText({ text: `${peerConnections}`, tabId });
chrome.action.setBadgeBackgroundColor({ color: "rgb(63, 81, 181)", tabId });
} else {
chrome.action.setTitle({
title: `WebRTC Internals Exporter (disabled)`,
tabId,
});
chrome.action.setBadgeText({ text: "", tabId });
}
}
async function optionsUpdated() {
const [tab] = await chrome.tabs.query({
active: true,
lastFocusedWindow: true,
});
await updateTabInfo(tab);
}
chrome.storage.sync.get().then((ret) => {
Object.assign(options, ret);
log("options loaded");
optionsUpdated();
});
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== "sync") return;
for (let [key, { newValue }] of Object.entries(changes)) {
options[key] = newValue;
}
log("options changed");
optionsUpdated();
});
chrome.tabs.onActivated.addListener(async ({ tabId }) => {
try {
const tab = await chrome.tabs.get(tabId);
await updateTabInfo(tab);
} catch (err) {
log(`get tab error: ${err.message}`);
}
});
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
if (!changeInfo.url) return;
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) {
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}`);
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}`);
const start = Date.now();
const response = await fetch(
`${url}/${job}`,
{
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();
}
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))
.then(() => {
sendResponse({});
})
.catch((err) => {
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))
} else {
sendResponse({ error: "unknown event" });
}
return true;
});

View File

@@ -1,123 +0,0 @@
/* global chrome */
if (window.location.protocol.startsWith("http")) {
const log = (...args) => {
try {
if (localStorage.getItem("webrtc-internal-exporter:debug") === "true") {
console.log.apply(null, [
"[webrtc-internal-exporter:content-script]",
...args,
]);
}
} catch (error) {
// Ignore localStorage errors.
}
};
const injectScript = (file_path) => {
const head = document.querySelector("head");
const script = document.createElement("script");
script.setAttribute("type", "text/javascript");
script.setAttribute("src", file_path);
head.appendChild(script);
};
setTimeout(() => injectScript(chrome.runtime.getURL("override.js")));
// Handle options.
const options = {
url: "",
enabled: false,
updateInterval: 2000,
enabledStats: [],
};
const sendOptions = () => {
window.postMessage({
event: "webrtc-internal-exporter:options",
options,
});
};
try {
chrome.storage.sync
.get(["url", "enabledOrigins", "updateInterval", "enabledStats"])
.then((ret) => {
log(`options loaded:`, ret);
options.url = ret.url || "";
options.enabled =
ret.enabledOrigins &&
ret.enabledOrigins[window.location.origin] === true;
options.updateInterval = (ret.updateInterval || 2) * 1000;
options.enabledStats = Object.values(ret.enabledStats || {});
sendOptions();
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== "sync") return;
let changed = false;
for (let [key, { newValue }] of Object.entries(changes)) {
if (key === "url") {
options.url = newValue;
changed = true;
} else if (key === "enabledOrigins") {
options.enabled = newValue[window.location.origin] === true;
changed = true;
} else if (key === "updateInterval") {
options.updateInterval = newValue * 1000;
changed = true;
} else if (key === "enabledStats") {
options.enabledStats = Object.values(newValue);
changed = true;
}
}
if (changed) {
log(`options changed:`, options);
sendOptions();
}
});
// Handle stats messages.
window.addEventListener("message", async (message) => {
const { event, url, id, state, values, 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,
},
});
if (response.error) {
log(`error: ${response.error}`);
}
} catch (error) {
log(`error: ${error.message}`);
}
} else if (event === "webrtc-internal-exporter:peer-connections-stats") {
try {
const response = await chrome.runtime.sendMessage({
event: "peer-connections-stats",
data: stats,
});
if (response.error) {
log(`error: ${response.error}`);
}
} catch (error) {
log(`error: ${error.message}`);
}
}
});
} catch (error) {
console.error(
`[webrtc-internal-exporter:content-script] error: ${error.message}`,
error,
);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,57 +0,0 @@
{
"name": "WebRTC Internals Exporter",
"description": "WebRTC Internals Exporter",
"author": "Vittorio Palmisano",
"version": "0.1.9",
"manifest_version": 3,
"icons": {
"16": "images/icon16.png",
"48": "images/icon48.png",
"128": "images/icon128.png"
},
"permissions": [
"storage",
"activeTab",
"tabs",
"scripting",
"alarms"
],
"host_permissions": [],
"action": {
"default_title": "WebRTC Internals Exporter",
"default_popup": "popup.html"
},
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"content_scripts": [
{
"matches": [
"https://*/*",
"http://*/*"
],
"js": [
"content-script.js"
],
"run_at": "document_start",
"all_frames": true,
"match_about_blank": true
}
],
"background": {
"service_worker": "background.js",
"type": "module"
},
"web_accessible_resources": [
{
"resources": [
"override.js"
],
"matches": [
"http://*/*",
"https://*/*"
]
}
]
}

View File

@@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebRTC Internals Exporter</title>
<script type="module" crossorigin src="/assets/options-8c2aaa1b.js"></script>
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-deb87276.js">
<link rel="stylesheet" href="/assets/_plugin-vue_export-helper.css">
<link rel="stylesheet" href="/assets/options.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -1,135 +0,0 @@
function log(...args) {
console.log.apply(null, ["[webrtc-internal-exporter:override]", ...args]);
}
log("Override RTCPeerConnection.");
class WebrtcInternalExporter {
peerConnections = new Map();
url = "";
enabled = false;
updateInterval = 2000;
enabledStats = [];
constructor() {
window.addEventListener("message", async (message) => {
const { event, options } = message.data;
if (event === "webrtc-internal-exporter:options") {
log("options updated:", options);
Object.assign(this, options);
}
});
window.postMessage({ event: "webrtc-internal-exporter:ready" });
this.collectAllStats();
}
randomId() {
return (
window.crypto?.randomUUID() || (2 ** 64 * Math.random()).toString(16)
);
}
add(pc) {
const id = this.randomId();
this.peerConnections.set(id, pc);
pc.addEventListener("connectionstatechange", () => {
if (pc.connectionState === "closed") {
this.peerConnections.delete(id);
}
});
//this.collectAndPostStats(id);
}
async collectAndPostSingleStat(id) {
const stats = await this.collectStats(id, this.collectAndPostSingleStat);
if (Object.keys(stats).length === 0 || !stats) return;
window.postMessage(
{
event: "webrtc-internal-exporter:peer-connection-stats",
...stats,
},
[stats],
);
}
async collectAllStats() {
const stats = [];
for (const [id, pc] of this.peerConnections) {
if (this.url && this.enabled && pc.connectionState === "connected") {
const pcStats = await this.collectStats(id, pc);
stats.push(pcStats);
}
}
//if (stats.length !== 0) {
window.postMessage(
{
event: "webrtc-internal-exporter:peer-connections-stats",
stats,
},
[stats],
);
log(`Stats collected:`, stats);
//}
setTimeout(this.collectAllStats.bind(this), this.updateInterval);
return stats;
}
async collectStats(id, pc, binding) {
var completeStats = {};
if (!pc) {
pc = this.peerConnections.get(id);
if (!pc) return;
}
if (this.url && this.enabled && pc.connectionState === "connected") {
try {
const stats = await pc.getStats();
const values = [...stats.values()].filter(
(v) =>
["peer-connection", ...this.enabledStats].indexOf(v.type) !== -1
);
completeStats = {
url: window.location.href,
id,
state: pc.connectionState,
values,
};
} catch (error) {
log(`collectStats error: ${error.message}`);
}
}
if (pc.connectionState === "closed") {
this.peerConnections.delete(id);
} else {
if (binding) {
setTimeout(binding.bind(this), this.updateInterval, id);
}
}
return completeStats;
}
}
const webrtcInternalExporter = new WebrtcInternalExporter();
window.RTCPeerConnection = new Proxy(window.RTCPeerConnection, {
construct(target, argumentsList) {
log(`RTCPeerConnection`, argumentsList);
const pc = new target(...argumentsList);
webrtcInternalExporter.add(pc);
return pc;
},
});

View File

@@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebRTC Internals Exporter</title>
<script type="module" crossorigin src="/assets/popup-7cc154e5.js"></script>
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-deb87276.js">
<link rel="stylesheet" href="/assets/_plugin-vue_export-helper.css">
<link rel="stylesheet" href="/assets/popup.css">
</head>
<body>
<div id="app"></div>
</body>
</html>