Compare commits

...

10 Commits

Author SHA1 Message Date
e1dc7bc5c9 feat: update README and main.py to clarify port usage and add error handling for socket port
All checks were successful
Build Docker Images for Pull Request / build (pull_request) Successful in 17m5s
2025-02-20 23:55:46 +01:00
ffd83d8bbc feat: add environment variable documentation and update configurations for socket port in Docker setup
All checks were successful
Build Docker Images for Pull Request / build (pull_request) Successful in 15m3s
2025-02-20 22:48:25 +01:00
9ee391fee8 feat: streamline Dockerfile and docker-compose configuration for improved build efficiency
All checks were successful
Build Docker Images for Pull Request / build (pull_request) Successful in 16m45s
2025-02-20 21:13:02 +01:00
f7dfaafc59 feat: add convenience functions, update Docker setup, and integrate Webpack for building WebRTC internals exporter
All checks were successful
Build Docker Images for Pull Request / build (pull_request) Successful in 15m15s
2025-02-20 11:37:37 +01:00
84479c786a feat: update Dockerfile to use Debian slim and implement Python virtual environment for dependency management
All checks were successful
Build and Push Docker Image / build (push) Successful in 4m20s
2025-02-15 16:23:48 +01:00
5636d92474 feat: add health check and heartbeat endpoint to Docker setup
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m26s
2025-02-15 14:28:24 +01:00
0518d1ba48 feat: integrate yaspin for improved logging and error handling in Chrome driver setup
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m5s
2025-02-14 15:32:14 +01:00
f579000a96 docs: simplify Docker commands in README and enhance logging in WebRTC stats exporter
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m16s
2025-02-13 18:31:09 +01:00
752cea15d4 docs: reorganize server setup instructions and create dedicated server README
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m6s
2025-02-12 20:24:29 +01:00
5be06ec11f docs: update README with improved Docker commands and add credits section
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m33s
feat: enhance WebRTC stats collection by logging connection state changes
2025-02-12 19:50:37 +01:00
23 changed files with 9864 additions and 130 deletions

View File

@@ -1,4 +1,4 @@
name: Build Docker Image for Pull Request
name: Build Docker Images for Pull Request
on:
pull_request:
@@ -25,3 +25,13 @@ jobs:
platforms: |
linux/amd64
linux/arm64
- name: Build monolith Docker image
uses: docker/build-push-action@v6.13.0
with:
context: .
tags: ${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ github.event.pull_request.number }}-monolith
file: ./Monolith.dockerfile
platforms: |
linux/amd64
linux/arm64

37
.github/workflows/monolith.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
env:
REGISTRY_URL: gitea.kobim.cloud
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository actions
uses: actions/checkout@v2
- name: Setup Docker Environment
uses: ./.github/actions/setup-docker-environment
- name: Log in to Docker registry
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.REGISTRY_URL }}
username: ${{ github.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6.13.0
with:
context: .
push: true
tags: ${{ env.REGISTRY_URL }}/${{ github.repository_owner }}/${{ github.event.repository.name }}-monolith:latest
file: ./Monolith.dockerfile
platforms: |
linux/amd64
linux/arm64

9
.gitignore vendored
View File

@@ -296,4 +296,11 @@ env/
__pycache__/
test/
venv/
.venv/
.venv/
# Node.js
node_modules/
npm-debug.log
yarn-error.log
yarn-debug.log*
background.bundle.js

View File

@@ -1,8 +1,15 @@
FROM python:3.13.1-slim-bookworm
FROM debian:bookworm-slim
# Install dependencies
# Install Python and curl
RUN apt-get update && apt-get install -y python3 python3-pip python3-venv curl
# Create and activate a virtual environment
RUN python3 -m venv /app/venv
ENV PATH="/app/venv/bin:$PATH"
# Install dependencies with venv
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
RUN /app/venv/bin/pip install -r /app/requirements.txt
# Copy the application
COPY main.py /app
@@ -10,4 +17,7 @@ COPY utils/ /app/utils
WORKDIR /app
# Run the application
CMD ["python", "main.py"]
CMD ["/app/venv/bin/python", "main.py"]
# Healthcheck
HEALTHCHECK --interval=5s --timeout=10s --retries=5 --start-period=5s CMD curl -f http://localhost:9092/heartbeat || exit 1

50
Monolith.dockerfile Normal file
View File

@@ -0,0 +1,50 @@
FROM node:22.14.0-bookworm-slim AS build
# Copy the webrtc-internals-exporter files
COPY webrtc-internals-exporter /tmp/webrtc-internals-exporter
WORKDIR /tmp/webrtc-internals-exporter/webpack
# Install dependencies
RUN --mount=type=cache,target=/root/.npm \
npm install
# Build the project
RUN npm run build
FROM selenium/standalone-chromium:129.0
# Install Python-virtualenv
RUN sudo apt-get update && sudo sudo apt-get install -y python3-venv
WORKDIR /tmp
# Install Telegraf
RUN wget -q https://repos.influxdata.com/influxdata-archive_compat.key && \
echo '393e8779c89ac8d958f81f942f9ad7fb82a25e133faddaf92e15b16e6ac9ce4c influxdata-archive_compat.key' | sha256sum -c && \
cat influxdata-archive_compat.key | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg > /dev/null && \
echo 'deb [signed-by=/etc/apt/trusted.gpg.d/influxdata-archive_compat.gpg] https://repos.influxdata.com/debian stable main' | sudo tee /etc/apt/sources.list.d/influxdata.list && \
sudo apt-get update && sudo apt-get install -y telegraf
# Create and activate a virtual environment
RUN python3 -m venv ./venv
ENV PATH="/tmp/venv/bin:$PATH"
# Install dependencies with venv
COPY requirements.txt ./requirements.txt
RUN ./venv/bin/pip install -r ./requirements.txt
# Copy files
COPY main.py .
COPY utils/ ./utils
COPY telegraf.conf ./telegraf.conf
COPY webrtc-internals-exporter /tmp/webrtc-internals-exporter
COPY --from=build /tmp/webrtc-internals-exporter/background.bundle.js /tmp/webrtc-internals-exporter/background.bundle.js
COPY --chown="${SEL_UID}:${SEL_GID}" monolith-entrypoint.sh /opt/bin/collector.sh
# Run the entrypoint
RUN chmod +x /opt/bin/collector.sh
ENTRYPOINT ["/opt/bin/collector.sh"]
# Healthcheck
HEALTHCHECK --interval=5s --timeout=10s --retries=5 --start-period=5s CMD curl -f http://localhost:9092/heartbeat || exit 1

107
README.md
View File

@@ -10,19 +10,12 @@ peertube-collector is a project designed to collect and analyze WebRTC statistic
- Docker Engine Community version is required. To install Docker CE, follow the official [install instructions](https://docs.docker.com/engine/install/).
### Ports:
#### Docker to Host:
- 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.
#### External (OPTIONAL PROBABLY NOT NEEDED!!!):
These ports are actively used by selenium and the collector services.
- 50000:60000/udp (WebRTC)
- Needed for WebRTC NAT traversal, otherwise the browser will not connect to any peer.
- WebRTC NAT traversal requires a range of ports to be open.
The range needs to be fairly large since the port is chosen randomly by the STUN server.
- 27107/tcp (MongoDB)
@@ -31,34 +24,62 @@ Ports can be opened in the host machine's firewall with:
ufw allow 50000:60000/udp
ufw allow 27107/tcp
```
## Setup
## Setup with Docker Compose
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
docker compose up --abort-on-container-failure
```
The collector will start gathering WebRTC stats from the Selenium container and sending them to the Telegraf service.
To stop the Docker containers run: `docker compose down -v`
### Setup with Monolithic image:
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 container:
```sh
docker run --rm -p 7900:7900 --env-file .env --name peertube-collector gitea.kobim.cloud/kobim/peertube-collector-monolith:latest
```
### Environment Variables
| Environment Variable | Service | Default Value | Description |
| ------------------------------- | -------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `TELEGRAF_HOSTNAME` | telegraf | None, **must** be set | Hostname used to identify the host/user between sessions |
| `TELEGRAF_MONGODB_DSN` | telegraf | `mongodb://stats_user...` | DSN for the MongoDB service |
| `TELEGRAF_MONGODB_DATABASE` | telegraf | `statistics` | Database name for the MongoDB service |
| `VIDEO_URL` | collector | `https://tube.kobim.cloud/...` | URL for the video to be analyzed |
| `HUB_URL` | collector | None | URL for the Selenium Hub. If not set, the local Chrome driver will be used |
| `SOCKET_URL` | collector | `localhost` | Socket URL for Telegraf service |
| `SOCKET_PORT` | collector & telegraf | `8094` | Socket port for Telegraf service |
| `WEBRTC_INTERNALS_PATH` | collector | None | **Absolute** path for WebRTC internals exporter extension. When **not** set the extension path is construced relative to the current main script location. |
| `WEBRTC_INTERNALS_EXPORTER_URL` | WebRTC extension | `http://localhost:9092` | Server URL for the WebRTC internals exporter extension |
Variables can be set in the `.env` file.
An example configuration is provided in the `.env.example` file.
### 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`.
@@ -75,6 +96,10 @@ The `docker-compose.yml` file defines the following services:
The `Dockerfile` sets up the Python environment and installs the necessary dependencies to run the `main.py` script.
### Monolithic Dockerfile
`Monolith.dockerfile` is a single Dockerfile that combines the Selenium, Telegraf, and Collector services into a single container. This is useful for deployment in a single container environment.
### Main Python Script
The `main.py` script sets up the Selenium WebDriver, collects WebRTC stats, and sends them to the Telegraf service.
@@ -82,44 +107,10 @@ The `main.py` script sets up the Selenium WebDriver, collects WebRTC stats, and
### WebRTC Internals Exporter
The `webrtc-internals-exporter` directory contains a Chromium extension that collects WebRTC stats from the browser.
It uses Webpack to replace the server collector endpoint with an environment variable.
## Working Project Structure
# Credits
```
peertube-collector/
├── requirements.txt
├── telegraf.conf
├── docker-compose.yml
├── Dockerfile
├── main.py
├── .env
└── utils/
└── webrtc-internals-exporter/
```
---
# Server
The repository contains a `server` directory with a simple MongoDB server (with initializations scripts) and WebUI that serves the WebRTC stats collected by the collector.
Based this awesome example configuration: [MongoDB Docker Compose examples](https://github.com/TGITS/docker-compose-examples/tree/main/mongodb-docker-compose-examples).
## Setup
1. Change to the `server` directory:
```sh
cd server
```
2. Create and configure the environment file based on the `.env.example` file:
```sh
cp .env.example .env
```
3. Start the Docker containers:
```sh
docker compose up
```
The WebUI control panel will be available at [http://localhost:8081](http://localhost:8081).
- [WebRTC Internals Exporter](https://github.com/vpalmisano/webrtc-internals-exporter)
- [WebRTC debugging with Prometheus/Grafana](https://medium.com/@vpalmisano/webrtc-debugging-with-prometheus-grafana-254b6ac71063)
- [MongoDB Docker Compose examples](https://github.com/TGITS/docker-compose-examples/tree/main/mongodb-docker-compose-examples)

View File

@@ -3,18 +3,19 @@ services:
container_name: selenium-standalone-chromium
image: selenium/standalone-chromium:129.0
volumes:
- ./webrtc-internals-exporter:/tmp/webrtc-internals-exporter:ro
- build-extension:/tmp/webrtc-internals-exporter
shm_size: "2g"
attach: false
depends_on:
telegraf:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4444/wd/hub/status"]
interval: 5s
timeout: 10s
retries: 5
network_mode: host
pull_policy: always
ports:
- "7900:7900"
networks:
- backend
telegraf:
container_name: telegraf
@@ -25,11 +26,32 @@ services:
- DATABASE=${TELEGRAF_MONGODB_DATABASE:?"Database name is required"}
- DSN=${TELEGRAF_MONGODB_DSN:?"DSN is required"}
- HOSTNAME=${TELEGRAF_HOSTNAME:?"Hostname is required"}
- SOCKET_PORT=${SOCKET_PORT:?"Socket port is required"}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080"]
interval: 5s
timeout: 10s
retries: 5
pull_policy: always
networks:
- backend
build-extension:
container_name: build-extension
image: node:22.14.0-bookworm-slim
volumes:
- ./webrtc-internals-exporter:/tmp/webrtc-internals-exporter:ro
- build-extension:/tmp/webrtc-internals-exporter-build
working_dir: /tmp/webrtc-internals-exporter-build/webpack
command:
- /bin/bash
- -c
- |
cp -r /tmp/webrtc-internals-exporter/* /tmp/webrtc-internals-exporter-build
npm install
npm run build
environment:
- WEBRTC_INTERNALS_EXPORTER_URL=http://collector
pull_policy: always
networks:
- backend
@@ -44,12 +66,14 @@ services:
condition: service_healthy
telegraf:
condition: service_healthy
build-extension:
condition: service_completed_successfully
environment:
- VIDEO_URL=${VIDEO_URL:?"Video URL is required"}
ports:
- "9092:9092"
extra_hosts:
- "host.docker.internal:host-gateway"
- SOCKET_URL=telegraf
- HUB_URL=http://selenium:4444
- WEBRTC_INTERNALS_PATH=/tmp/webrtc-internals-exporter
pull_policy: always
networks:
- backend
@@ -57,4 +81,7 @@ networks:
backend:
ipam:
config:
- subnet: 172.30.0.0/16
- subnet: 172.100.0.0/16
volumes:
build-extension:

84
main.py
View File

@@ -4,10 +4,13 @@ import time
import socket
import logging
import os
import argparse
from yaspin import yaspin
from functools import partial
from http.server import HTTPServer
from utils.PostHandler import Handler
from utils.ColoredFormatter import ColoredFormatter
from utils.Convenience import *
from bs4 import BeautifulSoup as bs
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
@@ -17,16 +20,29 @@ from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec
logger = logging.getLogger(__name__)
args = None
def setupLogger():
logging_format = "[%(asctime)s] (%(levelname)s) %(module)s - %(funcName)s: %(message)s"
logging.basicConfig(level=logging.INFO, format=logging_format)
logging.basicConfig(level=firstValid(args.log_level, os.getenv('LOG_LEVEL'), default='INFO'), format=logging_format) # type: ignore
(logger := logging.getLogger(__name__)).setLevel(logging.INFO)
logger.propagate = False
(logger_handler := logging.StreamHandler()).setFormatter(
ColoredFormatter(fmt=logging_format)
)
logger.addHandler(logger_handler)
def setupArgParser():
parser = argparse.ArgumentParser(description='Collector for PeerTube stats.')
parser.add_argument('-u', '--url', type=str, help='URL of the video to collect stats for.')
parser.add_argument('--socket-url', type=str, help='URL of the socket to send the stats to. Default: localhost')
parser.add_argument('--socket-port', type=int, help='Port of the socket to send the stats to. Default: 8094')
parser.add_argument('--hub-url', type=str, help='URL of the Selenium hub to connect to. If not provided, local Chrome driver will be used.')
parser.add_argument('--webrtc-internals-path', type=str, help='Path to the WebRTC internals extension.')
parser.add_argument('--log-level', type=str, help='Log level to use. Default: INFO')
return parser
def interrupt_handler(signum, driver: webdriver.Remote):
logger.info(f'Handling signal {signum} ({signal.Signals(signum).name}).')
@@ -34,37 +50,41 @@ def interrupt_handler(signum, driver: webdriver.Remote):
driver.quit()
raise SystemExit
def setupChromeDriver():
@yaspin()
def setupChromeDriver(command_executor: str | None, webrtc_internals_path: str) -> webdriver.Remote | webdriver.Chrome:
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_argument("--load-extension=/tmp/webrtc-internals-exporter")
chrome_options.add_argument(f"--load-extension={webrtc_internals_path}")
chrome_options.add_experimental_option('prefs', {'intl.accept_languages': 'en,en_US'})
#driver = webdriver.Chrome(options=chrome_options)
driver = webdriver.Remote(command_executor='http://host.docker.internal:4444', options=chrome_options)
if command_executor is not None:
driver = webdriver.Remote(command_executor=command_executor, options=chrome_options)
logger.warning(f'Using Selenium hub at {command_executor}.')
else:
driver = webdriver.Chrome(options=chrome_options)
logger.warning('No Selenium hub URL provided, using local Chrome driver.')
logger.log(logging.INFO, 'Chrome driver setup complete.')
return driver
def saveStats(stats: list):
def saveStats(stats: list, socket_url: str, socket_port: int):
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(), ('telegraf', 8094))
sock.sendto(json.dumps(stats).encode(), (socket_url, socket_port))
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):
def downloadStats(driver: webdriver.Remote | webdriver.Chrome, peersDict: dict, socket_url: str, socket_port: int):
html = driver.find_element(By.CLASS_NAME ,'vjs-stats-list').get_attribute('innerHTML')
if html is not None:
htmlBS = bs(html, 'html.parser')
@@ -155,11 +175,12 @@ def downloadStats(driver: webdriver.Chrome, peersDict: dict):
'session': driver.session_id
}
saveStats([stats])
saveStats([stats], socket_url, socket_port)
def convert_to_bytes(down, downUnit):
return float(down) * (1024 ** {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3}[downUnit])
@yaspin()
def setupStats(driver: webdriver.Remote, url: str):
logger.log(logging.INFO, 'Setting up stats.')
actions = ActionChains(driver)
@@ -167,7 +188,13 @@ def setupStats(driver: webdriver.Remote, url: str):
driver.get(url)
wait.until(ec.presence_of_element_located((By.CLASS_NAME, 'vjs-big-play-button')))
try:
wait.until(ec.presence_of_element_located((By.CLASS_NAME, 'vjs-big-play-button')))
except Exception:
logger.error('Timeout while waiting for the big play button to be present.')
driver.quit()
raise SystemExit(1)
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()
@@ -179,21 +206,36 @@ def setupStats(driver: webdriver.Remote, url: str):
return driver
if __name__ == '__main__':
if __name__ == '__main__':
args = setupArgParser().parse_args()
setupLogger()
driver = setupChromeDriver()
command_executor = firstValid(args.hub_url, os.getenv('HUB_URL'), default=None)
webrtc_internals_path = firstValid(
args.webrtc_internals_path,
os.getenv('WEBRTC_INTERNALS_PATH'),
default=os.path.abspath(os.path.join(os.path.dirname(__file__), 'webrtc-internals-exporter'))
)
driver = setupChromeDriver(command_executor, webrtc_internals_path)
signal.signal(signal.SIGINT, lambda signum, frame: interrupt_handler(signum, driver))
url = os.getenv('VIDEO_URL')
url = firstValid(args.url, os.getenv('VIDEO_URL'), default=None)
if url is None:
logger.error('VIDEO_URL environment variable is not set.')
logger.error('VIDEO_URL environment variable or --url argument is required.')
raise SystemExit(1)
setupStats(driver, url)
logger.log(logging.INFO, 'Starting server collector.')
httpd = HTTPServer(('', 9092), partial(Handler, downloadStats, driver, logger))
logger.info('Server collector started.')
socket_url = firstValid(args.socket_url, os.getenv('SOCKET_URL'), default='localhost')
try:
socket_port = int(firstValid(args.socket_port, os.getenv('SOCKET_PORT'), default=8094))
except ValueError:
logger.error('Invalid socket port provided. Exiting.')
raise SystemExit(1)
logger.info('Starting server collector.')
httpd = HTTPServer(('', 9092), partial(Handler, downloadStats, driver, logger, socket_url, socket_port))
httpd.serve_forever()

53
monolith-entrypoint.sh Normal file
View File

@@ -0,0 +1,53 @@
#!/bin/bash
if [ -z "$TELEGRAF_HOSTNAME" ]; then
echo "Error: TELEGRAF_HOSTNAME is not set"
exit 1
fi
if [ -z "$TELEGRAF_MONGODB_DSN" ]; then
echo "Error: TELEGRAF_MONGODB_DSN is not set"
exit 1
fi
if [ -z "$TELEGRAF_MONGODB_DATABASE" ]; then
echo "Error: TELEGRAF_MONGODB_DATABASE is not set"
exit 1
fi
if [ -z "$SOCKET_PORT" ]; then
echo "Error: SOCKET_PORT is not set"
exit 1
fi
if [ -z "$VIDEO_URL" ]; then
echo "Error: VIDEO_URL is not set"
exit 1
fi
# Set the environment variables
export DSN=$TELEGRAF_MONGODB_DSN
export DATABASE=$TELEGRAF_MONGODB_DATABASE
export HOSTNAME=$TELEGRAF_HOSTNAME
# Start the Selenium hub
/opt/bin/entry_point.sh > /dev/null 2>&1 &
# Wait for Selenium hub to be ready
printf 'Waiting for Selenium standalone to be ready'
timeout=30
while ! curl -sSL "http://localhost:4444/wd/hub/status" 2>/dev/null | jq -e '.value.ready' | grep -q true; do
printf '.'
sleep 1
((timeout--))
if [ $timeout -le 0 ]; then
echo "Error: Selenium standalone did not become ready in time. Exiting..."
exit 1
fi
done
printf '\n'
# Start the Telegraf agent and the main script
telegraf --config ./telegraf.conf &
./venv/bin/python main.py

View File

@@ -1,2 +1,3 @@
selenium
beautifulsoup4
beautifulsoup4
yaspin

30
server/README.md Normal file
View File

@@ -0,0 +1,30 @@
# Server
The repository contains a `server` directory with a simple MongoDB server (with initializations scripts) and WebUI that serves the WebRTC stats collected by the collector.
It's not mandatory to run and use this service, it's provided just as an example of how to store collected data.
## Setup
1. Change to the `server` directory:
```sh
cd server
```
2. Create and configure the environment file based on the `.env.example` file:
```sh
cp .env.example .env
```
3. Start the Docker containers:
```sh
docker compose up
```
The WebUI control panel will be available at [http://localhost:8081](http://localhost:8081).
# Credits
- [WebRTC Internals Exporter](https://github.com/vpalmisano/webrtc-internals-exporter)
- [WebRTC debugging with Prometheus/Grafana](https://medium.com/@vpalmisano/webrtc-debugging-with-prometheus-grafana-254b6ac71063)
- [MongoDB Docker Compose examples](https://github.com/TGITS/docker-compose-examples/tree/main/mongodb-docker-compose-examples)

View File

@@ -7,7 +7,7 @@
dedup_interval = "600s"
[[inputs.socket_listener]]
service_address = "udp://:8094"
service_address = "udp://:${SOCKET_PORT}"
data_format = "xpath_json"
[[inputs.socket_listener.xpath]]
metric_name = "'peertube'"

5
utils/Convenience.py Normal file
View File

@@ -0,0 +1,5 @@
def firstValid(*args, default):
for arg in args:
if arg is not None:
return arg
return default

View File

@@ -3,10 +3,12 @@ import logging
from http.server import BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def __init__(self, custom_func, driver, logger, *args, **kwargs):
def __init__(self, custom_func, driver, logger, socket_url, socket_port, *args, **kwargs):
self._custom_func = custom_func
self.logger = logger
self.driver = driver
self._socket_url = socket_url
self._socket_port = socket_port
super().__init__(*args, **kwargs)
def do_POST(self):
@@ -14,7 +16,7 @@ class Handler(BaseHTTPRequestHandler):
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._custom_func(self.driver, json.loads(post_data.decode('utf-8')), self._socket_url, self._socket_port)
self.send_response(200)
self.end_headers()
self.wfile.write(b'POST request received')
@@ -24,6 +26,11 @@ class Handler(BaseHTTPRequestHandler):
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')
if self.path == '/heartbeat':
self.send_response(200)
self.end_headers()
self.wfile.write(b'Heartbeat OK')
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b'404 Not Found')

File diff suppressed because one or more lines are too long

View File

@@ -6,17 +6,17 @@ function log(...args) {
log("loaded");
import "/assets/pako.min.js";
import "./assets/pako.min.js";
const DEFAULT_OPTIONS = {
url: "http://localhost:9092",
url: process.env.WEBRTC_INTERNALS_EXPORTER_URL + ":9092",
username: "",
password: "",
updateInterval: 2,
gzip: false,
job: "webrtc-internals-exporter",
enabledOrigins: { },
enabledStats: ["data-channel", "local-candidate", "remote-candidate"]
enabledOrigins: {},
enabledStats: ["data-channel", "local-candidate", "remote-candidate", "candidate-pair"]
};
const options = {};

View File

@@ -40,7 +40,7 @@
}
],
"background": {
"service_worker": "background.js",
"service_worker": "background.bundle.js",
"type": "module"
},
"web_accessible_resources": [

View File

@@ -22,7 +22,7 @@ class WebrtcInternalExporter {
});
window.postMessage({ event: "webrtc-internal-exporter:ready" });
this.collectAllStats();
setInterval(() => this.collectAndPostAllStats(), this.updateInterval);
}
randomId() {
@@ -40,6 +40,9 @@ class WebrtcInternalExporter {
pc.iceCandidateErrors = [];
this.peerConnections.set(id, pc);
pc.addEventListener("connectionstatechange", () => {
log(`connectionStateChange: ${pc.connectionState}`);
this.collectAndPostAllStats();
if (pc.connectionState === "closed") {
this.peerConnections.delete(id);
}
@@ -83,24 +86,27 @@ class WebrtcInternalExporter {
}
async collectAndPostSingleStat(id) {
const stats = await this.collectStats(id, this.collectAndPostSingleStat);
const stats = await this.collectStats(id);
if (Object.keys(stats).length === 0 || !stats) return;
window.postMessage(
{
event: "webrtc-internal-exporter:peer-connection-stats",
stats
stats: [stats]
},
stats
[stats]
);
log(`Single stat collected:`, [stats]);
}
async collectAllStats() {
async collectAndPostAllStats() {
const stats = [];
for (const [id, pc] of this.peerConnections) {
for (const [id] of this.peerConnections) {
if (this.url && this.enabled) {
const pcStats = await this.collectStats(id, pc);
const pcStats = await this.collectStats(id);
if (Object.keys(pcStats).length === 0 || !pcStats) continue;
stats.push(pcStats);
}
}
@@ -108,28 +114,24 @@ class WebrtcInternalExporter {
window.postMessage(
{
event: "webrtc-internal-exporter:peer-connections-stats",
data: JSON.parse(JSON.stringify(stats)),
data: stats
},
stats
);
log(`Stats collected:`, JSON.parse(JSON.stringify(stats)));
setTimeout(this.collectAllStats.bind(this), this.updateInterval);
log(`Stats collected:`, stats);
return stats;
}
/**
* @param {string} id
* @param {RTCPeerConnection} pc
* @param {Function} binding
*/
async collectStats(id, pc, binding) {
var completeStats = {};
async collectStats(id) {
var pc = this.peerConnections.get(id);
if (!pc) return;
if (!pc) {
pc = this.peerConnections.get(id);
if (!pc) return;
}
var completeStats = {};
if (this.url && this.enabled) {
try {
@@ -157,10 +159,6 @@ class WebrtcInternalExporter {
if (pc.connectionState === "closed") {
this.peerConnections.delete(id);
} else {
if (binding) {
setTimeout(binding.bind(this), this.updateInterval, id);
}
}
return completeStats;

View File

@@ -0,0 +1,3 @@
module.exports = {
shouldPrintComment: () => false
};

View File

@@ -0,0 +1,26 @@
const { execSync } = require('child_process');
const args = process.argv.slice(2);
let url = '';
args.forEach((arg, index) => {
if (arg === '-u' || arg === '--url') {
url = args[index + 1];
} else if (arg === '-h' || arg === '--help') {
console.log('Usage: npm run build -- [-u|--url <url>]');
console.log('Options:');
console.log(' -u, --url <url> URL to use for the extension collector server');
console.log(' -h, --help Display this help message');
process.exit(0);
} else if (arg.startsWith('-')) {
console.error(`Unrecognized argument: ${arg}`);
process.exit(1);
}
});
if (url) {
console.log(`Building with URL: ${url}`);
execSync(`webpack --env URL=${url}`, { stdio: 'inherit' });
} else {
execSync('webpack', { stdio: 'inherit' });
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"name": "webrtc-internals-exporter",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "node build.js"
},
"keywords": [],
"author": "Mirko Milovanovic",
"license": "MIT",
"devDependencies": {
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^12.0.2",
"dotenv": "^16.4.7",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.1",
"mini-css-extract-plugin": "^1.6.0",
"postcss": "^8.2.14",
"postcss-loader": "^5.2.0",
"postcss-preset-env": "^10.1.4",
"sass": "^1.32.12",
"sass-loader": "^11.0.1",
"serve": "^14.2.4",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.1.1",
"ts-loader": "^9.1.2",
"typescript": "^4.2.4",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^5.2.0"
}
}

View File

@@ -0,0 +1,34 @@
const path = require('path');
const { EnvironmentPlugin } = require('webpack');
const envPath = path.resolve(__dirname, '../../.env');
const envConfig = require('dotenv').config({ path: envPath }).parsed;
module.exports = (env) => {
const url = env.URL || 'http://localhost';
return {
entry: '../background.js',
target: 'web',
mode: 'production',
module: {
rules: [
{
test: /\.js?$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
resolve: { extensions: ['.tsx', '.ts', '.js'] },
output: {
filename: 'background.bundle.js',
path: path.resolve(__dirname, '../'),
publicPath: '',
},
plugins: [
new EnvironmentPlugin({
WEBRTC_INTERNALS_EXPORTER_URL: envConfig.WEBRTC_INTERNALS_EXPORTER_URL || url
}),
],
};
};