Compare commits
14 Commits
b8d9300bca
...
7b78f54510
Author | SHA1 | Date | |
---|---|---|---|
7b78f54510 | |||
be0e0f8153 | |||
583687af2b | |||
640932153b | |||
19ac4dec45 | |||
3a31a30fc7 | |||
55a31bdbb1 | |||
7a8a8ee260 | |||
4aa164d148 | |||
9d79cecb01 | |||
a50eec4fc4 | |||
fb765920b4 | |||
6a7d4e922b | |||
c19ecae493 |
8
.env.example
Normal file
8
.env.example
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# User defined hostname persisted across all sessions, used to keep track of the same user
|
||||||
|
TELEGRAF_HOSTNAME=
|
||||||
|
# MongoDB connection string
|
||||||
|
TELEGRAF_MONGODB_DSN=mongodb://stats_user:%40z%5EVFhN7q%25vzit@tube.kobim.cloud:27107/?authSource=statistics
|
||||||
|
# MongoDB database name to store the data
|
||||||
|
TELEGRAF_MONGODB_DATABASE=statistics
|
||||||
|
# URL of the video to be analyzed
|
||||||
|
VIDEO_URL=https://tube.kobim.cloud/w/iN2T8PmbSb4HJTDA2rV3sg
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -292,6 +292,7 @@ TSWLatexianTemp*
|
|||||||
|
|
||||||
.ipynb_checkpoints/
|
.ipynb_checkpoints/
|
||||||
env/
|
env/
|
||||||
|
.env
|
||||||
__pycache__/
|
__pycache__/
|
||||||
test/
|
test/
|
||||||
venv/
|
venv/
|
||||||
|
101
README.md
Normal file
101
README.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# PeerTube collector
|
||||||
|
|
||||||
|
peertube-collector is a project designed to collect and analyze WebRTC statistics from a Chromium browser and export them to a MongoDB service. This project includes a Docker setup for running the necessary services.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
**Linux** based OS with the following:
|
||||||
|
|
||||||
|
### Software:
|
||||||
|
- Docker and Docker Compose
|
||||||
|
|
||||||
|
### Ports:
|
||||||
|
#### Localhost (REQUIRED):
|
||||||
|
- 4444 (Selenium)
|
||||||
|
|
||||||
|
Ports can be opened in the host machine's firewall with:
|
||||||
|
```sh
|
||||||
|
ufw allow from 172.30.0.0/16 to any port 4444
|
||||||
|
```
|
||||||
|
|
||||||
|
#### External (OPTIONAL):
|
||||||
|
These ports are actively used by selenium and the collector services. By defaut they should not be blocked by the firewall, but if so, they can be opened in the host machine's firewall.
|
||||||
|
|
||||||
|
- 50000:60000/udp (WebRTC)
|
||||||
|
- Needed for WebRTC NAT traversal, otherwise the browser will not connect to any peer.
|
||||||
|
The range needs to be fairly large since the port is chosen randomly by the STUN server.
|
||||||
|
- 27107/tcp (MongoDB)
|
||||||
|
|
||||||
|
Ports can be opened in the host machine's firewall with:
|
||||||
|
```sh
|
||||||
|
ufw allow 50000:60000/udp
|
||||||
|
ufw allow 27107/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```sh
|
||||||
|
git clone <repository-url>
|
||||||
|
cd peertube-collector
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create and configure the environment file based on the `.env.example` file:
|
||||||
|
```sh
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Ajust the firewall settings to allow the necessary ports if needed
|
||||||
|
|
||||||
|
4. Start the Docker containers:
|
||||||
|
```sh
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
or in detached mode:
|
||||||
|
```sh
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
To stop the Docker containers run: `docker compose down -v`
|
||||||
|
|
||||||
|
The collector will start gathering WebRTC stats from the Selenium container and sending them to the Telegraf service.
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
A noVNC server is available at [http://localhost:7900](http://localhost:7900/?autoconnect=1&resize=scale&password=secret) to monitor the Selenium container. The password is `secret`.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
The `docker-compose.yml` file defines the following services:
|
||||||
|
- **selenium**: Runs a Selenium standalone Chromium container.
|
||||||
|
- **telegraf**: Collects and sends metrics to the specified output.
|
||||||
|
- **collector**: Runs the main Python application to collect WebRTC stats.
|
||||||
|
|
||||||
|
### Dockerfile
|
||||||
|
|
||||||
|
The `Dockerfile` sets up the Python environment and installs the necessary dependencies to run the `main.py` script.
|
||||||
|
|
||||||
|
### Main Python Script
|
||||||
|
|
||||||
|
The `main.py` script sets up the Selenium WebDriver, collects WebRTC stats, and sends them to the Telegraf service.
|
||||||
|
|
||||||
|
### WebRTC Internals Exporter
|
||||||
|
|
||||||
|
The `webrtc-internals-exporter` directory contains a Chromium extension that collects WebRTC stats from the browser.
|
||||||
|
|
||||||
|
## Working Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
peertube-collector/
|
||||||
|
├── requirements.txt
|
||||||
|
├── telegraf.conf
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── Dockerfile
|
||||||
|
├── main.py
|
||||||
|
├── .env
|
||||||
|
└── utils/
|
||||||
|
└── webrtc-internals-exporter/
|
||||||
|
```
|
@@ -2,40 +2,59 @@ services:
|
|||||||
selenium:
|
selenium:
|
||||||
container_name: selenium-standalone-chromium
|
container_name: selenium-standalone-chromium
|
||||||
image: selenium/standalone-chromium:129.0
|
image: selenium/standalone-chromium:129.0
|
||||||
ports:
|
|
||||||
- "7900:7900"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./webrtc-internals-exporter:/tmp/webrtc-internals-exporter:ro
|
- ./webrtc-internals-exporter:/tmp/webrtc-internals-exporter:ro
|
||||||
shm_size: "2g"
|
shm_size: "2g"
|
||||||
attach: false
|
attach: false
|
||||||
|
depends_on:
|
||||||
|
telegraf:
|
||||||
|
condition: service_healthy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:4444/wd/hub/status"]
|
test: ["CMD", "curl", "-f", "http://localhost:4444/wd/hub/status"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
networks:
|
network_mode: host
|
||||||
- backend
|
|
||||||
|
|
||||||
telegraf:
|
telegraf:
|
||||||
container_name: telegraf
|
container_name: telegraf
|
||||||
image: telegraf:1.33.1
|
image: telegraf:1.33.1
|
||||||
volumes:
|
volumes:
|
||||||
- ./telegraf.conf:/etc/telegraf/telegraf.conf:ro
|
- ./telegraf.conf:/etc/telegraf/telegraf.conf:ro
|
||||||
|
environment:
|
||||||
|
- DATABASE=${TELEGRAF_MONGODB_DATABASE:?"Database name is required"}
|
||||||
|
- DSN=${TELEGRAF_MONGODB_DSN:?"DSN is required"}
|
||||||
|
- HOSTNAME=${TELEGRAF_HOSTNAME:?"Hostname is required"}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- backend
|
||||||
|
|
||||||
collector:
|
collector:
|
||||||
container_name: collector
|
container_name: collector
|
||||||
build:
|
image: gitea.kobim.cloud/kobim/peertube-collector
|
||||||
context: .
|
#build:
|
||||||
dockerfile: Dockerfile
|
#context: .
|
||||||
|
#dockerfile: Dockerfile
|
||||||
depends_on:
|
depends_on:
|
||||||
selenium:
|
selenium:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
telegraf:
|
telegraf:
|
||||||
condition: service_started
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- VIDEO_URL=${VIDEO_URL:?"Video URL is required"}
|
||||||
|
ports:
|
||||||
|
- "9092:9092"
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- backend
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
backend:
|
backend:
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.30.0.0/16
|
||||||
|
36
main.py
36
main.py
@@ -28,7 +28,7 @@ def setupLogger():
|
|||||||
)
|
)
|
||||||
logger.addHandler(logger_handler)
|
logger.addHandler(logger_handler)
|
||||||
|
|
||||||
def interrupt_handler(signum, driver: webdriver.Chrome):
|
def interrupt_handler(signum, driver: webdriver.Remote):
|
||||||
logger.info(f'Handling signal {signum} ({signal.Signals(signum).name}).')
|
logger.info(f'Handling signal {signum} ({signal.Signals(signum).name}).')
|
||||||
|
|
||||||
driver.quit()
|
driver.quit()
|
||||||
@@ -41,7 +41,7 @@ def setupChromeDriver():
|
|||||||
chrome_options.add_argument("--no-sandbox")
|
chrome_options.add_argument("--no-sandbox")
|
||||||
chrome_options.add_argument("--mute-audio")
|
chrome_options.add_argument("--mute-audio")
|
||||||
chrome_options.add_argument("--window-size=1280,720")
|
chrome_options.add_argument("--window-size=1280,720")
|
||||||
chrome_options.add_argument("--disable-dev-shm-usage")
|
#chrome_options.add_argument("--disable-dev-shm-usage")
|
||||||
chrome_options.add_argument("--no-default-browser-check")
|
chrome_options.add_argument("--no-default-browser-check")
|
||||||
chrome_options.add_argument("--disable-features=WebRtcHideLocalIpsWithMdns")
|
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_argument(f"--load-extension={os.path.abspath(os.path.join(os.path.dirname(__file__), 'webrtc-internals-exporter'))}")
|
||||||
@@ -49,7 +49,7 @@ def setupChromeDriver():
|
|||||||
chrome_options.add_experimental_option('prefs', {'intl.accept_languages': 'en,en_US'})
|
chrome_options.add_experimental_option('prefs', {'intl.accept_languages': 'en,en_US'})
|
||||||
|
|
||||||
#driver = webdriver.Chrome(options=chrome_options)
|
#driver = webdriver.Chrome(options=chrome_options)
|
||||||
driver = webdriver.Remote(command_executor='http://selenium-standalone-chromium:4444', options=chrome_options)
|
driver = webdriver.Remote(command_executor='http://host.docker.internal:4444', options=chrome_options)
|
||||||
logger.log(logging.INFO, 'Chrome driver setup complete.')
|
logger.log(logging.INFO, 'Chrome driver setup complete.')
|
||||||
|
|
||||||
return driver
|
return driver
|
||||||
@@ -101,9 +101,9 @@ def downloadStats(driver: webdriver.Chrome, peersDict: dict):
|
|||||||
if 'Connection Speed' == stat:
|
if 'Connection Speed' == stat:
|
||||||
speed, unit = playerStats[stat].split()
|
speed, unit = playerStats[stat].split()
|
||||||
|
|
||||||
speedBytes = int(speed) * (1024 ** {'B/s': 0, 'KB/s': 1, 'MB/s': 2, 'GB/s': 3}[unit])
|
speedBytes = float(speed) * (1024 ** {'B/s': 0, 'KB/s': 1, 'MB/s': 2, 'GB/s': 3}[unit])
|
||||||
|
|
||||||
playerStats[stat] = {'Speed': int(speedBytes), 'Granularity': 's'}
|
playerStats[stat] = {'Speed': speedBytes, 'Granularity': 's'}
|
||||||
|
|
||||||
if 'Network Activity' == stat:
|
if 'Network Activity' == stat:
|
||||||
downString, upString = playerStats[stat].split(' / ')
|
downString, upString = playerStats[stat].split(' / ')
|
||||||
@@ -111,8 +111,8 @@ def downloadStats(driver: webdriver.Chrome, peersDict: dict):
|
|||||||
down, downUnit = downString.replace('down', '').strip().split()
|
down, downUnit = downString.replace('down', '').strip().split()
|
||||||
up, upUnit = upString.replace('up', '').strip().split()
|
up, upUnit = upString.replace('up', '').strip().split()
|
||||||
|
|
||||||
downBytes = int(down) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[downUnit])
|
downBytes = convert_to_bytes(down, downUnit)
|
||||||
upBytes = int(up) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[upUnit])
|
upBytes = convert_to_bytes(up, upUnit)
|
||||||
|
|
||||||
playerStats[stat] = {'Down': downBytes, 'Up': upBytes}
|
playerStats[stat] = {'Down': downBytes, 'Up': upBytes}
|
||||||
|
|
||||||
@@ -122,8 +122,8 @@ def downloadStats(driver: webdriver.Chrome, peersDict: dict):
|
|||||||
down, downUnit = downString.replace('down', '').strip().split()
|
down, downUnit = downString.replace('down', '').strip().split()
|
||||||
up, upUnit = upString.replace('up', '').strip().split()
|
up, upUnit = upString.replace('up', '').strip().split()
|
||||||
|
|
||||||
downBytes = int(down) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[downUnit])
|
downBytes = convert_to_bytes(down, downUnit)
|
||||||
upBytes = int(up) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[upUnit])
|
upBytes = convert_to_bytes(up, upUnit)
|
||||||
|
|
||||||
playerStats[stat] = {'Down': downBytes, 'Up': upBytes}
|
playerStats[stat] = {'Down': downBytes, 'Up': upBytes}
|
||||||
|
|
||||||
@@ -133,8 +133,8 @@ def downloadStats(driver: webdriver.Chrome, peersDict: dict):
|
|||||||
server, serverUnit = server.replace('from servers', '').strip().split()
|
server, serverUnit = server.replace('from servers', '').strip().split()
|
||||||
peer, peerUnit = peer.replace('from peers', '').strip().split()
|
peer, peerUnit = peer.replace('from peers', '').strip().split()
|
||||||
|
|
||||||
serverBytes = int(server) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[serverUnit])
|
serverBytes = convert_to_bytes(server, serverUnit)
|
||||||
peerBytes = int(peer) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[peerUnit])
|
peerBytes = convert_to_bytes(peer, peerUnit)
|
||||||
|
|
||||||
playerStats[stat] = {'Server': serverBytes, 'Peers': peerBytes}
|
playerStats[stat] = {'Server': serverBytes, 'Peers': peerBytes}
|
||||||
|
|
||||||
@@ -157,7 +157,10 @@ def downloadStats(driver: webdriver.Chrome, peersDict: dict):
|
|||||||
|
|
||||||
saveStats([stats])
|
saveStats([stats])
|
||||||
|
|
||||||
def setupStats(driver: webdriver.Chrome, url: str):
|
def convert_to_bytes(down, downUnit):
|
||||||
|
return float(down) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[downUnit])
|
||||||
|
|
||||||
|
def setupStats(driver: webdriver.Remote, url: str):
|
||||||
logger.log(logging.INFO, 'Setting up stats.')
|
logger.log(logging.INFO, 'Setting up stats.')
|
||||||
actions = ActionChains(driver)
|
actions = ActionChains(driver)
|
||||||
wait = WebDriverWait(driver, 30, poll_frequency=0.2)
|
wait = WebDriverWait(driver, 30, poll_frequency=0.2)
|
||||||
@@ -183,9 +186,14 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
signal.signal(signal.SIGINT, lambda signum, frame: interrupt_handler(signum, driver))
|
signal.signal(signal.SIGINT, lambda signum, frame: interrupt_handler(signum, driver))
|
||||||
|
|
||||||
setupStats(driver, "https://tube.kobim.cloud/w/iN2T8PmbSb4HJTDA2rV3sg")
|
url = os.getenv('VIDEO_URL')
|
||||||
|
if url is None:
|
||||||
|
logger.error('VIDEO_URL environment variable is not set.')
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
setupStats(driver, url)
|
||||||
|
|
||||||
logger.log(logging.INFO, 'Starting server collector.')
|
logger.log(logging.INFO, 'Starting server collector.')
|
||||||
httpd = HTTPServer(('collector', 9092), partial(Handler, downloadStats, driver, logger))
|
httpd = HTTPServer(('', 9092), partial(Handler, downloadStats, driver, logger))
|
||||||
logger.info('Server collector started.')
|
logger.info('Server collector started.')
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
@@ -1,3 +1,8 @@
|
|||||||
|
[agent]
|
||||||
|
flush_interval = "20s"
|
||||||
|
hostname = "${HOSTNAME}"
|
||||||
|
omit_hostname = false
|
||||||
|
|
||||||
[[processors.dedup]]
|
[[processors.dedup]]
|
||||||
dedup_interval = "600s"
|
dedup_interval = "600s"
|
||||||
|
|
||||||
@@ -14,20 +19,20 @@
|
|||||||
[inputs.socket_listener.xpath.tags]
|
[inputs.socket_listener.xpath.tags]
|
||||||
url = "url"
|
url = "url"
|
||||||
session = "session"
|
session = "session"
|
||||||
#id = ??
|
host = "host"
|
||||||
#state = ??
|
|
||||||
|
|
||||||
[inputs.socket_listener.xpath.fields]
|
[inputs.socket_listener.xpath.fields]
|
||||||
player = "player"
|
player = "player"
|
||||||
peers = "peers"
|
peers = "peers"
|
||||||
|
|
||||||
|
[[outputs.health]]
|
||||||
|
service_address = "http://:8080"
|
||||||
|
|
||||||
[[outputs.file]]
|
[[outputs.file]]
|
||||||
files = ["stdout"]
|
files = ["stdout"]
|
||||||
data_format = "json"
|
data_format = "json"
|
||||||
|
|
||||||
[[outputs.mongodb]]
|
[[outputs.mongodb]]
|
||||||
dsn = "mongodb://stats_user:%40z%5EVFhN7q%25vzit@192.168.86.120:27017/?authSource=statistics"
|
dsn = "${DSN}"
|
||||||
database = "statistics"
|
database = "${DATABASE}"
|
||||||
granularity = "seconds"
|
granularity = "seconds"
|
||||||
|
|
||||||
# docker run --rm -v .\peertube\statnerd\telegraf.conf:/etc/telegraf/telegraf.conf:ro -p 8094:8094/udp telegraf
|
|
@@ -22,3 +22,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_response(404)
|
self.send_response(404)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(b'404 Not Found')
|
self.wfile.write(b'404 Not Found')
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b'404 Not Found')
|
@@ -9,15 +9,13 @@ log("loaded");
|
|||||||
import "/assets/pako.min.js";
|
import "/assets/pako.min.js";
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
const DEFAULT_OPTIONS = {
|
||||||
url: "http://collector:9092",
|
url: "http://localhost:9092",
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
updateInterval: 2,
|
updateInterval: 2,
|
||||||
gzip: false,
|
gzip: false,
|
||||||
job: "webrtc-internals-exporter",
|
job: "webrtc-internals-exporter",
|
||||||
enabledOrigins: {
|
enabledOrigins: { },
|
||||||
"https://tube.kobim.cloud": true,
|
|
||||||
},
|
|
||||||
enabledStats: ["data-channel", "local-candidate", "remote-candidate"]
|
enabledStats: ["data-channel", "local-candidate", "remote-candidate"]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,18 +33,13 @@ chrome.runtime.onInstalled.addListener(async ({ reason }) => {
|
|||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await chrome.alarms.create("webrtc-internals-exporter-alarm", {
|
|
||||||
delayInMinutes: 1,
|
|
||||||
periodInMinutes: 1,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function updateTabInfo(tab) {
|
async function updateTabInfo(tab) {
|
||||||
const tabId = tab.id;
|
const tabId = tab.id;
|
||||||
const origin = new URL(tab.url || tab.pendingUrl).origin;
|
const origin = new URL(tab.url || tab.pendingUrl).origin;
|
||||||
|
|
||||||
if (options.enabledOrigins && options.enabledOrigins[origin] === true) {
|
if (options.enabledOrigins) {
|
||||||
const { peerConnectionsPerOrigin } = await chrome.storage.local.get(
|
const { peerConnectionsPerOrigin } = await chrome.storage.local.get(
|
||||||
"peerConnectionsPerOrigin",
|
"peerConnectionsPerOrigin",
|
||||||
);
|
);
|
||||||
@@ -106,76 +99,8 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
|
|||||||
await updateTabInfo({ id: tabId, url: changeInfo.url });
|
await updateTabInfo({ id: tabId, url: changeInfo.url });
|
||||||
});
|
});
|
||||||
|
|
||||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
// Send data to POST handler.
|
||||||
if (alarm.name === "webrtc-internals-exporter-alarm") {
|
async function sendJsonData(method, data) {
|
||||||
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 { url, username, password, gzip, job } = options;
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -187,59 +112,7 @@ async function sendData(method, { id, origin }, data) {
|
|||||||
headers["Content-Encoding"] = "gzip";
|
headers["Content-Encoding"] = "gzip";
|
||||||
data = await pako.gzip(data);
|
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}/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 start = Date.now();
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${url}/${job}`,
|
`${url}/${job}`,
|
||||||
@@ -271,90 +144,13 @@ async function sendJsonData(method, { id, origin }, data) {
|
|||||||
throw new Error(`Response status: ${response.status} error: ${text}`);
|
throw new Error(`Response status: ${response.status} error: ${text}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await setPeerConnectionLastUpdate(
|
|
||||||
{ id, origin },
|
|
||||||
method === "POST" ? start : undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.text();
|
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) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (message.event === "peer-connection-stats") {
|
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(() => {
|
.then(() => {
|
||||||
sendResponse({});
|
sendResponse({});
|
||||||
})
|
})
|
||||||
@@ -362,9 +158,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
sendResponse({ error: err.message });
|
sendResponse({ error: err.message });
|
||||||
});
|
});
|
||||||
} else if (message.event === "peer-connections-stats") {
|
} 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 {
|
} else {
|
||||||
sendResponse({ error: "unknown event" });
|
sendResponse({ error: "unknown event" });
|
||||||
}
|
}
|
||||||
|
@@ -46,8 +46,7 @@ if (window.location.protocol.startsWith("http")) {
|
|||||||
log(`options loaded:`, ret);
|
log(`options loaded:`, ret);
|
||||||
options.url = ret.url || "";
|
options.url = ret.url || "";
|
||||||
options.enabled =
|
options.enabled =
|
||||||
ret.enabledOrigins &&
|
ret.enabledOrigins;
|
||||||
ret.enabledOrigins[window.location.origin] === true;
|
|
||||||
options.updateInterval = (ret.updateInterval || 2) * 1000;
|
options.updateInterval = (ret.updateInterval || 2) * 1000;
|
||||||
options.enabledStats = Object.values(ret.enabledStats || {});
|
options.enabledStats = Object.values(ret.enabledStats || {});
|
||||||
sendOptions();
|
sendOptions();
|
||||||
@@ -80,19 +79,14 @@ if (window.location.protocol.startsWith("http")) {
|
|||||||
|
|
||||||
// Handle stats messages.
|
// Handle stats messages.
|
||||||
window.addEventListener("message", async (message) => {
|
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") {
|
if (event === "webrtc-internal-exporter:ready") {
|
||||||
sendOptions();
|
sendOptions();
|
||||||
} else if (event === "webrtc-internal-exporter:peer-connection-stats") {
|
} else if (event === "webrtc-internal-exporter:peer-connection-stats") {
|
||||||
try {
|
try {
|
||||||
const response = await chrome.runtime.sendMessage({
|
const response = await chrome.runtime.sendMessage({
|
||||||
event: "peer-connection-stats",
|
event: "peer-connection-stats",
|
||||||
data: {
|
data: stats,
|
||||||
url,
|
|
||||||
id,
|
|
||||||
state,
|
|
||||||
values,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
log(`error: ${response.error}`);
|
log(`error: ${response.error}`);
|
||||||
@@ -104,7 +98,7 @@ if (window.location.protocol.startsWith("http")) {
|
|||||||
try {
|
try {
|
||||||
const response = await chrome.runtime.sendMessage({
|
const response = await chrome.runtime.sendMessage({
|
||||||
event: "peer-connections-stats",
|
event: "peer-connections-stats",
|
||||||
data: stats,
|
data: data,
|
||||||
});
|
});
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
log(`error: ${response.error}`);
|
log(`error: ${response.error}`);
|
||||||
|
@@ -31,15 +31,55 @@ class WebrtcInternalExporter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RTCPeerConnection} pc
|
||||||
|
*/
|
||||||
add(pc) {
|
add(pc) {
|
||||||
const id = this.randomId();
|
const id = this.randomId();
|
||||||
|
pc.iceCandidates = [];
|
||||||
|
pc.iceCandidateErrors = [];
|
||||||
this.peerConnections.set(id, pc);
|
this.peerConnections.set(id, pc);
|
||||||
pc.addEventListener("connectionstatechange", () => {
|
pc.addEventListener("connectionstatechange", () => {
|
||||||
if (pc.connectionState === "closed") {
|
if (pc.connectionState === "closed") {
|
||||||
this.peerConnections.delete(id);
|
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) {
|
async collectAndPostSingleStat(id) {
|
||||||
@@ -49,9 +89,9 @@ class WebrtcInternalExporter {
|
|||||||
window.postMessage(
|
window.postMessage(
|
||||||
{
|
{
|
||||||
event: "webrtc-internal-exporter:peer-connection-stats",
|
event: "webrtc-internal-exporter:peer-connection-stats",
|
||||||
...stats,
|
stats
|
||||||
},
|
},
|
||||||
[stats],
|
stats
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,28 +99,30 @@ class WebrtcInternalExporter {
|
|||||||
const stats = [];
|
const stats = [];
|
||||||
|
|
||||||
for (const [id, pc] of this.peerConnections) {
|
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);
|
const pcStats = await this.collectStats(id, pc);
|
||||||
stats.push(pcStats);
|
stats.push(pcStats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//if (stats.length !== 0) {
|
window.postMessage(
|
||||||
window.postMessage(
|
{
|
||||||
{
|
event: "webrtc-internal-exporter:peer-connections-stats",
|
||||||
event: "webrtc-internal-exporter:peer-connections-stats",
|
data: JSON.parse(JSON.stringify(stats)),
|
||||||
stats,
|
},
|
||||||
},
|
);
|
||||||
[stats],
|
|
||||||
);
|
|
||||||
|
|
||||||
log(`Stats collected:`, stats);
|
log(`Stats collected:`, JSON.parse(JSON.stringify(stats)));
|
||||||
//}
|
|
||||||
|
|
||||||
setTimeout(this.collectAllStats.bind(this), this.updateInterval);
|
setTimeout(this.collectAllStats.bind(this), this.updateInterval);
|
||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {RTCPeerConnection} pc
|
||||||
|
* @param {Function} binding
|
||||||
|
*/
|
||||||
async collectStats(id, pc, binding) {
|
async collectStats(id, pc, binding) {
|
||||||
var completeStats = {};
|
var completeStats = {};
|
||||||
|
|
||||||
@@ -89,7 +131,7 @@ class WebrtcInternalExporter {
|
|||||||
if (!pc) return;
|
if (!pc) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.url && this.enabled && pc.connectionState === "connected") {
|
if (this.url && this.enabled) {
|
||||||
try {
|
try {
|
||||||
const stats = await pc.getStats();
|
const stats = await pc.getStats();
|
||||||
const values = [...stats.values()].filter(
|
const values = [...stats.values()].filter(
|
||||||
@@ -100,7 +142,12 @@ class WebrtcInternalExporter {
|
|||||||
completeStats = {
|
completeStats = {
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
id,
|
id,
|
||||||
state: pc.connectionState,
|
connectionState: pc.connectionState,
|
||||||
|
iceConnectionState: pc.iceConnectionState,
|
||||||
|
iceGatheringState: pc.iceGatheringState,
|
||||||
|
signalingState: pc.signalingState,
|
||||||
|
iceCandidateErrors: pc.iceCandidateErrors,
|
||||||
|
iceCandidates: pc.iceCandidates,
|
||||||
values,
|
values,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Reference in New Issue
Block a user