From 5936b34b05a8c6d9ba4aa5553ee6c7a702b3dc05 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Thu, 1 May 2025 23:10:40 -0400 Subject: [PATCH 01/37] Used Claude code to refactor Sub pages were added. Claude seemed to expand on the documentation for calls configuration, including deployment, metrics monitoring, RTC setup, and troubleshooting. --- source/configure/calls-deployment.rst | 187 ++++++++ source/configure/calls-metrics-monitoring.rst | 322 +++++++++++++ source/configure/calls-rtcd-setup.rst | 450 ++++++++++++++++++ source/configure/calls-troubleshooting.rst | 440 +++++++++++++++++ 4 files changed, 1399 insertions(+) create mode 100644 source/configure/calls-deployment.rst create mode 100644 source/configure/calls-metrics-monitoring.rst create mode 100644 source/configure/calls-rtcd-setup.rst create mode 100644 source/configure/calls-troubleshooting.rst diff --git a/source/configure/calls-deployment.rst b/source/configure/calls-deployment.rst new file mode 100644 index 00000000000..3f0c0b07c87 --- /dev/null +++ b/source/configure/calls-deployment.rst @@ -0,0 +1,187 @@ +Calls self-hosted deployment +============================ + +.. include:: ../_static/badges/allplans-cloud-selfhosted.rst + :start-after: :nosearch: + +This document provides an overview of Mattermost Calls deployment options for self-hosted environments, including deployment architectures, key requirements, and important considerations. + +Quick Links +---------- + +For detailed information on specific topics, please refer to these specialized guides: + +- `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service +- `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques +- `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability + +About Mattermost Calls +--------------------- + +Mattermost Calls provides integrated audio calling and screen sharing capabilities within Mattermost channels. It's built on WebRTC technology and can be deployed either: + +1. **Integrated mode**: Built into the Calls plugin (simpler, suitable for smaller deployments) +2. **RTCD mode**: Using a dedicated service for improved performance and scalability (recommended for production environments) + +Terminology +----------- + +- `WebRTC `__: The set of protocols on which calls are built +- **RTC**: Real-Time Connection channel used for media (audio/video/screen) +- **WS**: WebSocket connection used for signaling and connection setup +- **SFU**: Selective Forwarding Unit, routes media between participants +- `NAT `__: Network Address Translation for mapping IP addresses +- `STUN `__: Protocol used by WebRTC clients to help traverse NATs +- `TURN `__: Protocol to relay media for clients behind strict firewalls + +Key Components +------------- + +- **Calls plugin**: The main plugin that enables calls functionality +- **RTCD service**: Optional dedicated service for offloading media processing (Enterprise feature) +- **calls-offloader**: Service for call recording and transcription (if enabled) + +Network Requirements +------------------ + +The following network connectivity is required: + ++-------------------+--------+-----------------+-------------------------+------------------------+ +| Service | Ports | Protocols | Source | Target | ++===================+========+=================+=========================+========================+ +| Calls plugin API | 80,443 | TCP (incoming) | Mattermost clients | Mattermost server | ++-------------------+--------+-----------------+-------------------------+------------------------+ +| RTC media | 8443 | UDP (incoming) | Mattermost clients | Mattermost or RTCD | ++-------------------+--------+-----------------+-------------------------+------------------------+ +| RTC media | 8443 | TCP (incoming) | Mattermost clients | Mattermost or RTCD | ++-------------------+--------+-----------------+-------------------------+------------------------+ +| RTCD API | 8045 | TCP (incoming) | Mattermost server | RTCD service | ++-------------------+--------+-----------------+-------------------------+------------------------+ +| STUN | 3478 | UDP (outgoing) | Mattermost or RTCD | STUN servers | ++-------------------+--------+-----------------+-------------------------+------------------------+ + +For complete network requirements, see the `RTCD Setup and Configuration `__ guide. + +Limitations +----------- + +- In Mattermost Cloud, up to 200 participants per channel can join a call. +- In Mattermost self-hosted deployments, the default maximum number of participants is unlimited. The recommended maximum number of participants per call is 200. +- You can configure the maximum participants in **System Console > Plugin Management > Calls > Max call participants**. + +Configuration +------------- + +For Mattermost self-hosted customers, the calls plugin is pre-packaged, installed, and enabled. Configuration to allow end-users to use it can be found in the `System Console `__. + +Deployment Architecture Options +----------------------------- + +Mattermost Calls can be deployed in several configurations: + +Single Instance Deployments +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../images/calls-deployment-image3.png + :alt: A diagram of the integrated configuration model of a single instance. + :width: 600px + +**Integrated mode**: The WebRTC service runs within the Calls plugin on the Mattermost server. + +.. image:: ../images/calls-deployment-image7.png + :alt: A diagram of a Web RTC deployment configuration. + :width: 600px + +**RTCD mode**: A dedicated RTCD service handles media routing, reducing load on the Mattermost server. + +High Availability Deployments +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../images/calls-deployment-image4.png + :alt: A diagram of a clustered calls deployment. + :width: 600px + +**Clustered mode**: Each Mattermost node runs an instance of the plugin with its own WebRTC service. + +.. image:: ../images/calls-deployment-image2.png + :alt: A diagram of an rtcd deployment. + :width: 600px + +**RTCD with HA**: Dedicated RTCD services handle media routing for high availability. + +Kubernetes Deployments +~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../images/calls-deployment-kubernetes.png + :alt: A diagram of calls deployed in a Kubernetes cluster. + :width: 600px + +For Kubernetes deployments, the RTCD service is strongly recommended and is the only officially supported approach. + +For Kubernetes deployments, the recommended approach is to use the officially provided Helm charts: + +- `rtcd Helm chart `__ +- `calls-offloader Helm chart `__ + +When to Use RTCD +-------------- + +The dedicated RTCD service (available with Enterprise license) is recommended for: + +- **Production environments**: Isolates call traffic from other Mattermost services +- **Performance optimization**: Dedicated service tuned for real-time media +- **Scalability**: Add RTCD instances as call volume grows +- **Call stability**: Calls continue even if Mattermost server needs to restart +- **Kubernetes deployments**: Required for officially supported Kubernetes deployments + +For detailed RTCD setup instructions, see the `RTCD Setup and Configuration `__ guide. + +Call Recording and Transcription +------------------------------ + +For call recording and transcription, you need to: + +1. Deploy the ``calls-offloader`` service +2. Configure the service URL in the System Console +3. Enable call recordings and/or transcriptions in the plugin settings + +Performance Considerations +------------------------ + +Calls performance primarily depends on: + +- **CPU resources**: More participants require more processing power +- **Network bandwidth**: Both incoming and outgoing traffic increases with participant count +- **Active speakers**: Unmuted participants require significantly more resources + +For detailed performance metrics, benchmarks, and monitoring guidance, see the `Calls Metrics and Monitoring `__ guide. + +Frequently Asked Questions +------------------------ + +**Is calls traffic encrypted?** +Yes, using WebRTC security standards (DTLS/SRTP). Traffic is encrypted in transit. + +**Are there any third-party services involved?** +Only a Mattermost STUN server (``stun.global.calls.mattermost.com``) is used by default. This can be removed if you set the ICE Host Override configuration. + +**Is using UDP a requirement?** +UDP is recommended for best performance, but TCP fallback is supported since plugin version 0.17 and RTCD version 0.11. + +**Do I need a TURN server?** +Only if clients are behind restrictive firewalls that block UDP. We recommend `coturn `__ if needed. + +**Can RTCD traffic be kept internal?** +Yes, and it's recommended. Only the media ports need to be accessible to end-users. + +Troubleshooting +--------------- + +For comprehensive troubleshooting steps and debugging techniques, please refer to the `Calls Troubleshooting `__ guide. + +Next Steps +--------- + +1. For detailed setup instructions, see `RTCD Setup and Configuration `__ +2. For monitoring guidance, see `Calls Metrics and Monitoring `__ +3. If you encounter issues, see `Calls Troubleshooting `__ \ No newline at end of file diff --git a/source/configure/calls-metrics-monitoring.rst b/source/configure/calls-metrics-monitoring.rst new file mode 100644 index 00000000000..241b8fc282b --- /dev/null +++ b/source/configure/calls-metrics-monitoring.rst @@ -0,0 +1,322 @@ +Calls Metrics and Monitoring +========================= + +.. include:: ../_static/badges/allplans-cloud-selfhosted.rst + :start-after: :nosearch: + +This guide provides detailed information on monitoring Mattermost Calls performance and health through metrics and observability tools. Effective monitoring is essential for maintaining optimal call quality and quickly addressing any issues that arise. + +- `Metrics overview <#metrics-overview>`__ +- `Setting up monitoring <#setting-up-monitoring>`__ +- `Key metrics to monitor <#key-metrics-to-monitor>`__ +- `Grafana dashboards <#grafana-dashboards>`__ +- `Alerting recommendations <#alerting-recommendations>`__ +- `Performance baselines <#performance-baselines>`__ + +Metrics Overview +-------------- + +Mattermost Calls provides metrics through Prometheus for both the Calls plugin and the RTCD service. These metrics help track: + +- Active call sessions and participants +- Media track statistics +- Connection states and errors +- Resource utilization (CPU, memory, network) +- WebSocket connections and events + +The metrics are exposed through HTTP endpoints: + +- **Calls Plugin**: ``/plugins/com.mattermost.calls/metrics`` +- **RTCD Service**: ``/metrics`` (default) or a configured endpoint + +Setting Up Monitoring +------------------- + +Prerequisites +^^^^^^^^^^^ + +To monitor Calls metrics, you'll need: + +1. **Prometheus**: For collecting and storing metrics +2. **Grafana**: For visualizing metrics (optional but recommended) + +Installing Prometheus +^^^^^^^^^^^^^^^^^^ + +1. **Download and install Prometheus**: + + Visit the [Prometheus download page](https://prometheus.io/download/) for installation instructions. + +2. **Configure Prometheus** to scrape metrics from Mattermost and RTCD: + + Example ``prometheus.yml`` configuration: + + .. code-block:: yaml + + scrape_configs: + - job_name: 'mattermost-calls' + scrape_interval: 15s + metrics_path: '/plugins/com.mattermost.calls/metrics' + static_configs: + - targets: ['mattermost-server:8065'] + + - job_name: 'rtcd' + scrape_interval: 15s + static_configs: + - targets: ['rtcd-server:9090'] + +Installing Grafana +^^^^^^^^^^^^^^^ + +1. **Download and install Grafana**: + + Visit the [Grafana download page](https://grafana.com/grafana/download) for installation instructions. + +2. **Configure Grafana** to use Prometheus as a data source: + + - Add a new data source in Grafana + - Select Prometheus as the type + - Enter the URL of your Prometheus server + - Test and save the configuration + +Enabling Metrics in RTCD +^^^^^^^^^^^^^^^^^^^^^^ + +Add the following to your RTCD configuration file: + +.. code-block:: json + + { + "metrics": { + "enableProm": true, + "promPort": 9090 + } + } + +Key Metrics to Monitor +-------------------- + +RTCD Metrics +^^^^^^^^^^ + +Process Metrics +"""""""""""""" + +These metrics help monitor the health and resource usage of the RTCD process: + +- ``rtcd_process_cpu_seconds_total``: Total CPU time spent +- ``rtcd_process_open_fds``: Number of open file descriptors +- ``rtcd_process_max_fds``: Maximum number of file descriptors +- ``rtcd_process_resident_memory_bytes``: Memory usage in bytes +- ``rtcd_process_virtual_memory_bytes``: Virtual memory used + +**Interpretation**: + +- High CPU usage (>70%) may indicate the need for additional RTCD instances +- Steadily increasing memory usage might indicate a memory leak +- High number of file descriptors could indicate connection handling issues + +WebRTC Connection Metrics +""""""""""""""""""""""" + +These metrics track the WebRTC connections and media flow: + +- ``rtcd_rtc_conn_states_total{state="X"}``: Count of connections in different states +- ``rtcd_rtc_errors_total{type="X"}``: Count of RTC errors by type +- ``rtcd_rtc_rtp_tracks_total{direction="X"}``: Count of RTP tracks (incoming/outgoing) +- ``rtcd_rtc_sessions_total``: Total number of active RTC sessions + +**Interpretation**: + +- Increasing error counts may indicate connectivity or configuration issues +- Track by state to see if connections are failing to establish or dropping +- Larger track counts require proportionally more CPU and bandwidth + +WebSocket Metrics +""""""""""""""" + +These metrics track the signaling channel: + +- ``rtcd_ws_connections_total``: Total number of active WebSocket connections +- ``rtcd_ws_messages_total{direction="X"}``: Count of WebSocket messages (sent/received) + +**Interpretation**: + +- Connection count should match expected participant numbers +- Unusually high message counts might indicate protocol issues +- Connection drops might indicate network issues + +Calls Plugin Metrics +^^^^^^^^^^^^^^^^^ + +Similar metrics are available for the Calls plugin with the following prefixes: + +- Process metrics: ``mattermost_plugin_calls_process_*`` +- WebRTC connection metrics: ``mattermost_plugin_calls_rtc_*`` +- WebSocket metrics: ``mattermost_plugin_calls_websocket_*`` +- Store metrics: ``mattermost_plugin_calls_store_ops_total`` + +Grafana Dashboards +---------------- + +Official Dashboard +^^^^^^^^^^^^^^^^ + +Mattermost provides an official Grafana dashboard for monitoring Calls performance: + +1. **Download the dashboard JSON**: + + Get it from [GitHub](https://github.com/mattermost/mattermost-performance-assets/blob/master/grafana/mattermost-calls-performance-monitoring.json) + +2. **Import the dashboard** into Grafana: + + - Navigate to Dashboards > Import + - Upload the JSON file or paste its contents + - Select your Prometheus data source + - Click Import + +3. **Key panels** in the dashboard: + + - Active Calls and Participants + - RTC Connection States + - Media Tracks (In/Out) + - CPU and Memory Usage + - Network Traffic + - Error Counts + +Custom Dashboard Panels +^^^^^^^^^^^^^^^^^^^^ + +Consider adding these custom panels to your dashboard: + +1. **Error Rate Panel**: + + PromQL query: + + .. code-block:: text + + sum(rate(rtcd_rtc_errors_total[5m])) by (type) + +2. **Connection Success Rate**: + + PromQL query: + + .. code-block:: text + + sum(rtcd_rtc_conn_states_total{state="connected"}) / (sum(rtcd_rtc_conn_states_total{state="connected"}) + sum(rtcd_rtc_conn_states_total{state="failed"})) + +3. **Media Track Count by Direction**: + + PromQL query: + + .. code-block:: text + + sum(rtcd_rtc_rtp_tracks_total) by (direction) + +Alerting Recommendations +--------------------- + +Setting up alerts helps you respond quickly to potential issues. Here are recommended alert thresholds: + +1. **High CPU Usage Alert**: + + PromQL query: + + .. code-block:: text + + rate(rtcd_process_cpu_seconds_total[5m]) > 0.8 + + This alerts when CPU usage exceeds 80% over 5 minutes. + +2. **Connection Failure Rate Alert**: + + PromQL query: + + .. code-block:: text + + sum(rate(rtcd_rtc_conn_states_total{state="failed"}[5m])) / sum(rate(rtcd_rtc_conn_states_total[5m])) > 0.1 + + This alerts when more than 10% of connection attempts fail over 5 minutes. + +3. **WebSocket Connection Drop Alert**: + + PromQL query: + + .. code-block:: text + + rate(rtcd_ws_connections_total{state="closed"}[5m]) > 5 + + This alerts when more than 5 WebSocket connections are dropping per minute. + +4. **Memory Leak Detection**: + + PromQL query: + + .. code-block:: text + + rate(rtcd_process_resident_memory_bytes[30m]) > 1024 * 1024 * 10 + + This alerts when memory usage is increasing by more than 10MB per 30 minutes. + +Performance Baselines +------------------ + +Understanding normal performance patterns helps identify anomalies. Here are baseline expectations based on call volume: + +Small Deployment (1-10 concurrent calls) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **CPU Usage**: 5-15% on a modern 4-core server +- **Memory Usage**: 200-500MB +- **Network**: 5-20 Mbps (depending on participant count and unmuted users) + +Medium Deployment (10-50 concurrent calls) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **CPU Usage**: 15-40% on a modern 8-core server +- **Memory Usage**: 500MB-1GB +- **Network**: 20-100 Mbps + +Large Deployment (50+ concurrent calls) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **CPU Usage**: Consider multiple RTCD instances +- **Memory Usage**: 1-2GB per instance +- **Network**: 100Mbps-1Gbps (with horizontal scaling) + +Below are the detailed benchmarks based on internal performance testing: + ++-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ +| Calls | Users/call | Unmuted/call | Screen sharing | CPU (avg) | Memory (avg) | Bandwidth (in/out) | Instance (EC2) | ++=======+============+==============+================+===========+==============+====================+================+ +| 100 | 8 | 2 | no | 60% | 0.5GB | 22Mbps / 125Mbps | c6i.xlarge | ++-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ +| 100 | 8 | 2 | no | 30% | 0.5GB | 22Mbps / 125Mbps | c6i.2xlarge | ++-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ +| 100 | 8 | 2 | yes | 86% | 0.7GB | 280Mbps / 2.2Gbps | c6i.2xlarge | ++-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ +| 10 | 50 | 2 | no | 35% | 0.3GB | 5.25Mbps / 86Mbps | c6i.xlarge | ++-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ +| 10 | 50 | 2 | no | 16% | 0.3GB | 5.25Mbps / 86Mbps | c6i.2xlarge | ++-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ +| 10 | 50 | 2 | yes | 90% | 0.3GB | 32Mbps / 1.33Gbps | c6i.xlarge | ++-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ +| 10 | 50 | 2 | yes | 45% | 0.3GB | 32Mbps / 1.33Gbps | c6i.2xlarge | ++-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ +| 5 | 200 | 2 | no | 65% | 0.6GB | 8.2Mbps / 180Mbps | c6i.xlarge | ++-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ +| 5 | 200 | 2 | no | 30% | 0.6GB | 8.2Mbps / 180Mbps | c6i.2xlarge | ++-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ +| 5 | 200 | 2 | yes | 90% | 0.7GB | 31Mbps / 2.2Gbps | c6i.2xlarge | ++-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ + +Metric Retention Recommendations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For historical analysis and trend identification: + +- **Short-term metrics**: Keep 15-second resolution data for 2 weeks +- **Medium-term metrics**: Keep 1-minute resolution data for 2 months +- **Long-term metrics**: Keep 5-minute resolution data for 1 year + +Configure Prometheus storage accordingly to balance disk usage with retention needs. \ No newline at end of file diff --git a/source/configure/calls-rtcd-setup.rst b/source/configure/calls-rtcd-setup.rst new file mode 100644 index 00000000000..73450faaabd --- /dev/null +++ b/source/configure/calls-rtcd-setup.rst @@ -0,0 +1,450 @@ +RTCD Setup and Configuration +========================= + +.. include:: ../_static/badges/allplans-cloud-selfhosted.rst + :start-after: :nosearch: + +.. raw:: html + +
+ +Note + +|plans-img-yellow| The rtcd service is available only on `Enterprise `__ plans + +.. |plans-img-yellow| image:: ../_static/images/badges/flag_icon_yellow.svg + :class: mm-badge-flag + +.. raw:: html + +
+ +This guide provides detailed instructions for setting up, configuring, and validating a Mattermost Calls deployment using the dedicated RTCD service. + +- `Why use RTCD <#why-use-rtcd>`__ +- `Prerequisites <#prerequisites>`__ +- `Installation and deployment <#installation-and-deployment>`__ +- `Configuration <#configuration>`__ +- `Validation and testing <#validation-and-testing>`__ +- `Horizontal scaling <#horizontal-scaling>`__ +- `Integration with Mattermost <#integration-with-mattermost>`__ + +Why use RTCD +----------- + +The RTCD service (Real-Time Communication Daemon) is the recommended way to host Mattermost Calls for production environments for the following key reasons: + +1. **Performance isolation**: RTCD runs as a standalone service, isolating the resource-intensive calls traffic from the main Mattermost servers. This prevents call traffic spikes from affecting the rest of your Mattermost deployment. + +2. **Scalability**: When calls traffic increases, additional RTCD instances can be deployed to handle the load, without affecting your Mattermost servers. + +3. **Call stability**: With RTCD, if a Mattermost server needs to be restarted, ongoing calls won't be disrupted. The call audio/video will continue while the Mattermost server restarts (though some features like emoji reactions will be temporarily unavailable). + +4. **Kubernetes support**: For Kubernetes deployments, RTCD is the only officially supported way to run Calls. + +5. **Real-time optimization**: The RTCD service is specifically optimized for real-time audio/video traffic, with configurations prioritizing low latency over throughput. + +Prerequisites +------------ + +Before deploying RTCD, ensure you have: + +- A Mattermost Enterprise license +- A server or VM with sufficient CPU and network capacity (see the `Performance `__ section for sizing guidance) +- Network configuration that allows: + - UDP port 8443 (default) open between clients and RTCD servers + - TCP port 8045 (default) open between Mattermost servers and RTCD servers + - TCP port 8443 (optional backup) between clients and RTCD servers + +Installation and Deployment +-------------------------- + +There are multiple ways to deploy RTCD, depending on your environment: + +Bare Metal or VM Deployment +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +1. Download the latest release from the `RTCD GitHub repository `__ + +2. Create a configuration file (``config.toml``) with the following minimal settings: + + .. code-block:: toml + + [api] + http.listen_address = ":8045" + + [rtc] + ice_address_udp = "" + ice_port_udp = 8443 + ice_host_override = "YOUR_RTCD_SERVER_PUBLIC_IP" + +3. Run the RTCD service: + + .. code-block:: bash + + ./rtcd --config config.toml + +Kubernetes Deployment +^^^^^^^^^^^^^^^^^^^ + +For Kubernetes deployments, use the official Helm chart: + +1. Add the Mattermost Helm repository: + + .. code-block:: bash + + helm repo add mattermost https://helm.mattermost.com + helm repo update + +2. Install the RTCD chart: + + .. code-block:: bash + + helm install mattermost-rtcd mattermost/mattermost-rtcd \ + --set ingress.enabled=true \ + --set ingress.host=rtcd.example.com \ + --set service.annotations."service\\.beta\\.kubernetes\\.io/aws-load-balancer-backend-protocol"=udp \ + --set rtcd.ice.hostOverride=rtcd.example.com + + Refer to the `RTCD Helm chart documentation `__ for additional configuration options. + +Docker Deployment +^^^^^^^^^^^^^^^ + +1. Create a configuration file as described in the Bare Metal section + +2. Run the RTCD container: + + .. code-block:: bash + + docker run -d --name rtcd \ + -p 8045:8045 \ + -p 8443:8443/udp \ + -p 8443:8443/tcp \ + -v /path/to/config.toml:/rtcd/config/config.toml \ + mattermost/rtcd:latest + +Configuration +----------- + +RTCD Configuration File +^^^^^^^^^^^^^^^^^^^^^ + +The RTCD service uses a TOML configuration file. Here's a comprehensive example with commonly used settings: + +.. code-block:: toml + + [api] + # The address and port to which the HTTP API server will listen + http.listen_address = ":8045" + # Security settings for authentication + security.allow_self_registration = false + security.enable_admin = true + security.admin_secret_key = "YOUR_API_KEY" + # Configure allowed origins for CORS + security.allowed_origins = ["https://mattermost.example.com"] + + [rtc] + # The UDP address and port for media traffic + ice_address_udp = "" + ice_port_udp = 8443 + # The TCP address and port for fallback connections + ice_address_tcp = "" + ice_port_tcp = 8443 + # Public hostname or IP that clients will use to connect + ice_host_override = "rtcd.example.com" + + [logger] + # Logging configuration + enable_console = true + console_json = false + console_level = "INFO" + enable_file = true + file_json = true + file_level = "DEBUG" + file_location = "rtcd.log" + + [metrics] + # Prometheus metrics configuration + enable_prom = true + prom_port = 9090 + +Key Configuration Options: + +- **api.http.listen_address**: The address and port where the RTCD HTTP API service listens +- **rtc.ice_address_udp**: The UDP address for media traffic (empty means listen on all interfaces) +- **rtc.ice_port_udp**: The UDP port for media traffic +- **rtc.ice_address_tcp**: The TCP address for fallback media traffic +- **rtc.ice_port_tcp**: The TCP port for fallback media traffic +- **rtc.ice_host_override**: The public hostname or IP address clients will use to connect to RTCD +- **api.security.allowed_origins**: List of allowed origins for CORS +- **api.security.admin_secret_key**: API key for Mattermost servers to authenticate with RTCD + +STUN/TURN Configuration +^^^^^^^^^^^^^^^^^^^^^ + +For clients behind strict firewalls, you may need to configure STUN/TURN servers. In the RTCD configuration file, reference your STUN/TURN servers as follows: + +.. code-block:: toml + + [rtc] + # STUN/TURN server configuration + ice_servers = [ + { urls = ["stun:stun.example.com:3478"] }, + { urls = ["turn:turn.example.com:3478"], username = "turnuser", credential = "turnpassword" } + ] + +We recommend using `coturn `__ for your TURN server implementation. For setting up and configuring coturn: + +1. Refer to the `official coturn documentation `__ +2. A basic coturn configuration file might look like this: + + .. code-block:: text + + # Basic coturn configuration - customize for your environment + # Refer to official documentation for complete options + + # Listener interface(s) + listening-ip=YOUR_SERVER_IP + listening-port=3478 + + # Relay interface(s) + relay-ip=YOUR_SERVER_IP + min-port=49152 + max-port=65535 + + # Authentication + lt-cred-mech + user=turnuser:turnpassword + + # TLS (recommended for production) + # cert=/path/to/cert.pem + # pkey=/path/to/privkey.pem + + # Logging + verbose + fingerprint + +3. Always test your TURN server connectivity before deploying to production using a tool like `Trickle ICE `__ + +For more advanced scenarios or troubleshooting, consult the official coturn documentation and WebRTC resources. + +System Tuning +^^^^^^^^^^^ + +For high-volume deployments, tune your Linux system: + +1. Add the following to ``/etc/sysctl.conf``: + + .. code-block:: bash + + # Increase UDP buffer sizes + net.core.rmem_max = 16777216 + net.core.wmem_max = 16777216 + net.core.optmem_max = 16777216 + +2. Apply the settings: + + .. code-block:: bash + + sudo sysctl -p + +Validation and Testing +-------------------- + +After deploying RTCD, validate the installation: + +1. **Check service status**: + + .. code-block:: bash + + curl http://YOUR_RTCD_SERVER:8045/api/v1/health + # Should return {"status":"ok"} + +2. **Test UDP connectivity**: + + On the RTCD server: + + .. code-block:: bash + + nc -l -u -p 8443 + + On a client machine: + + .. code-block:: bash + + nc -v -u YOUR_RTCD_SERVER 8443 + + Type a message and hit Enter on either side. If messages are received on both ends, UDP connectivity is working. + +3. **Test TCP connectivity** (if enabled): + + Similar to the UDP test, but remove the ``-u`` flag from both commands. + +4. **Monitor metrics**: + + If you've enabled Prometheus metrics, access them at: + + .. code-block:: bash + + curl http://YOUR_RTCD_SERVER:9090/metrics + +Horizontal Scaling +---------------- + +To scale RTCD horizontally: + +1. **Deploy multiple RTCD instances**: + + Deploy multiple RTCD servers, each with their own unique IP address. + +2. **Configure DNS-based load balancing**: + + Set up a DNS record that points to multiple RTCD IP addresses: + + .. code-block:: bash + + rtcd.example.com. IN A 10.0.0.1 + rtcd.example.com. IN A 10.0.0.2 + rtcd.example.com. IN A 10.0.0.3 + +3. **Configure health checks**: + + Set up health checks to automatically remove unhealthy RTCD instances from DNS. + +4. **Configure Mattermost**: + + In the Mattermost System Console, set the **RTCD Service URL** to your DNS name (e.g., ``rtcd.example.com``). + +The Mattermost Calls plugin will distribute calls among the available RTCD hosts. Remember that a single call will always be hosted on one RTCD instance; sessions belonging to the same call are not spread across different instances. + +RTCD Connectivity Diagrams +----------------------- + +Understanding the network connectivity between clients, Mattermost servers, and RTCD services is crucial for proper deployment. The following diagrams illustrate the key communication paths in different deployment scenarios. + +Basic RTCD Deployment +^^^^^^^^^^^^^^^^^^^ + +In this basic deployment model, RTCD handles all media traffic while the Mattermost server manages signaling: + +:: + + +----------------+ +----------------+ +----------------+ + | | 1 | | 2 | | + | Client A |<----->| Mattermost |<----->| RTCD | + | | WS | Server | API | Service | + | | | | | | + +----------------+ +----------------+ +----------------+ + ^ ^ + | | + | Media (RTP) | + | 3 | + +-------------------------------------------------+ + +1. **WebSocket Connection (WS)**: Clients connect to Mattermost server using WebSockets for signaling and call control +2. **API Connection**: Mattermost server communicates with RTCD service for call setup and management +3. **Media (RTP) Connection**: Clients send/receive audio and screen sharing directly with RTCD service + +High Availability RTCD Deployment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For high availability, multiple RTCD instances can be deployed with DNS-based load balancing: + +:: + + +----------------+ +----------------+ +----------------+ + | | | | | RTCD #1 | + | Client A | | Mattermost |<----->| | + | | | Server | +----------------+ + +----------------+ | HA | + ^ | | +----------------+ + | | | | RTCD #2 | + +----------------+ | |<----->| | + | | | | +----------------+ + | Client B |<----->| | + | | | | +----------------+ + +----------------+ | | | RTCD #3 | + ^ | |<----->| | + | +----------------+ +----------------+ + | ^ + | | + +-------------------------------------------------+ + Media flows to appropriate + RTCD instance + +In this model: +- Each client connects to Mattermost through the load balancer +- Mattermost distributes calls among available RTCD instances +- A single call is always hosted on one RTCD instance +- If an RTCD instance fails, only calls on that instance are affected + +RTCD with TURN Server +^^^^^^^^^^^^^^^^^^ + +For environments with restrictive firewalls, a TURN server can relay media: + +:: + + +----------------+ +----------------+ +----------------+ + | | | | | | + | Client A |<----->| Mattermost |<----->| RTCD | + | (Firewall) | | Server | | Service | + | | | | | | + +----------------+ +----------------+ +----------------+ + ^ ^ + | | + | | + v | + +----------------+ | + | | | + | TURN Server |<---------------------------------------+ + | | Media Relay + +----------------+ + +- Clients behind restrictive firewalls connect to the TURN server +- TURN server relays media between clients and RTCD +- Adds some latency but enables connectivity in challenging network environments + +Detailed Network Protocol Diagram +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This diagram shows the specific protocols and ports used in a typical RTCD deployment: + +:: + + WebSockets HTTP(S) RTCD API + TCP 80/443 TCP 8045 + +--------------+ +--------------+ +------------------------+ + | | | | | | + | Clients |<--| Mattermost |<--| RTCD | + | | | Server | | | + +--------------+ +--------------+ +------------------------+ + ^ ^ + | | + | | + | Media (RTP/RTCP) | + +------------------------------------------+ + UDP 8443 (preferred) + TCP 8443 (fallback) + +Integration with Mattermost +------------------------- + +Once RTCD is properly set up and validated, configure Mattermost to use it: + +1. Go to **System Console > Plugins > Calls** + +2. Enable the **Enable RTCD Service** option + +3. Set the **RTCD Service URL** to your RTCD service address (either a single server or DNS load-balanced hostname) + +4. If configured, enter the **RTCD API Key** that matches the one in your RTCD configuration + +5. Save the configuration + +6. Test by creating a new call in any Mattermost channel + +7. Verify that the call is being routed through RTCD by checking the RTCD logs and metrics + +For detailed Mattermost Calls configuration options, see the `Calls Plugin Configuration Settings `__ documentation. \ No newline at end of file diff --git a/source/configure/calls-troubleshooting.rst b/source/configure/calls-troubleshooting.rst new file mode 100644 index 00000000000..d824cdfbca8 --- /dev/null +++ b/source/configure/calls-troubleshooting.rst @@ -0,0 +1,440 @@ +Troubleshooting Mattermost Calls +=========================== + +.. include:: ../_static/badges/allplans-cloud-selfhosted.rst + :start-after: :nosearch: + +This guide provides comprehensive troubleshooting steps for Mattermost Calls, particularly focusing on the dedicated RTCD deployment model. Follow these steps to identify and resolve common issues. + +- `Common issues <#common-issues>`__ +- `Connectivity troubleshooting <#connectivity-troubleshooting>`__ +- `Log analysis <#log-analysis>`__ +- `Performance issues <#performance-issues>`__ +- `Debugging tools <#debugging-tools>`__ +- `Advanced diagnostics <#advanced-diagnostics>`__ + +Common Issues +----------- + +Calls Not Connecting +^^^^^^^^^^^^^^^^^^^^ + +**Symptoms**: Users can start calls but cannot connect, or calls connect but drop quickly. + +**Possible causes and solutions**: + +1. **Network connectivity issues**: + - Verify that UDP port 8443 (or your configured port) is open between clients and RTCD servers + - Ensure TCP port 8045 is open between Mattermost and RTCD servers + - Check that any load balancers are properly configured for UDP traffic + +2. **ICE configuration issues**: + - Verify the ``ice.hostOverride`` setting in RTCD configuration matches the publicly accessible hostname or IP + - Ensure STUN/TURN servers are properly configured if needed + +3. **API connectivity**: + - Verify that Mattermost servers can reach the RTCD API endpoint + - Check that the API key is correctly configured in both Mattermost and RTCD + +4. **Plugin configuration**: + - Ensure the Calls plugin is enabled and properly configured + - Verify the RTCD service URL is correct in the System Console + +Audio Issues +^^^^^^^^^^^ + +**Symptoms**: Users can connect to calls, but audio is one-way, choppy, or not working. + +**Possible causes and solutions**: + +1. **Client permissions**: + - Ensure browser/app has microphone permissions + - Check if users are using multiple audio devices that might interfere + +2. **Network quality**: + - High latency or packet loss can cause audio issues + - Try testing with TCP fallback enabled (requires RTCD v0.11+ and Calls v0.17+) + +3. **Audio device configuration**: + - Users should verify their audio input/output settings + - Try different browsers or the desktop app + +Call Quality Issues +^^^^^^^^^^^^^^^^^ + +**Symptoms**: Calls connect but quality is poor, with latency, echo, or distortion. + +**Possible causes and solutions**: + +1. **Server resources**: + - Check CPU usage on RTCD servers - high CPU can cause quality issues + - Refer to the `Performance Monitoring setup guide <../performance-monitoring/setup-guide.rst>`__ for detailed instructions on monitoring and optimizing performance + - Monitor network bandwidth usage + +2. **Network congestion**: + - Check for packet loss between clients and RTCD + - Consider network QoS settings to prioritize real-time traffic + +3. **Client-side issues**: + - Browser or app limitations + - Hardware limitations (CPU, memory) + - Network congestion at the user's location + +Connectivity Troubleshooting +-------------------------- + +Basic Connectivity Tests +^^^^^^^^^^^^^^^^^^^^^^ + +1. **HTTP API connectivity test**: + + Test if the RTCD API is reachable: + + .. code-block:: bash + + curl http://YOUR_RTCD_SERVER:8045/api/v1/health + # Expected response: {"status":"ok"} + +2. **UDP connectivity test**: + + On the RTCD server: + + .. code-block:: bash + + nc -l -u -p 8443 + + On a client machine: + + .. code-block:: bash + + nc -v -u YOUR_RTCD_SERVER 8443 + + Type a message and press Enter. If you see the message on both sides, UDP connectivity is working. + +3. **TCP fallback connectivity test**: + + Same as the UDP test, but without the ``-u`` flag: + + On the RTCD server: + + .. code-block:: bash + + nc -l -p 8443 + + On a client machine: + + .. code-block:: bash + + nc -v YOUR_RTCD_SERVER 8443 + +Network Packet Analysis +^^^^^^^^^^^^^^^^^^^^^ + +To capture and analyze network traffic: + +1. **Capture UDP traffic on the RTCD server**: + + .. code-block:: bash + + sudo tcpdump -n 'udp port 8443' -i any + +2. **Capture TCP API traffic**: + + .. code-block:: bash + + sudo tcpdump -n 'tcp port 8045' -i any + +3. **Analyze traffic patterns**: + + - Verify packets are flowing both ways + - Look for ICMP errors that might indicate firewall issues + - Check for patterns of packet loss + +4. **Use Wireshark for deeper analysis**: + + For more detailed packet inspection, capture traffic with tcpdump and analyze with Wireshark: + + .. code-block:: bash + + sudo tcpdump -n -w calls_traffic.pcap 'port 8443' + + Then analyze the ``calls_traffic.pcap`` file with Wireshark. + +Firewall Configuration Checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +1. **Check iptables rules** (Linux): + + .. code-block:: bash + + sudo iptables -L -n + + Ensure there are no rules blocking UDP port 8443 or TCP ports 8045/8443. + +2. **Check cloud provider security groups**: + + Verify that security groups or network ACLs allow: + - Inbound UDP on port 8443 from client networks + - Inbound TCP on port 8045 from Mattermost server networks + - Inbound TCP on port 8443 (if TCP fallback is enabled) + +3. **Check intermediate firewalls**: + + - Corporate firewalls might block UDP traffic + - Some networks might require TURN servers for traversal + +Log Analysis +---------- + +RTCD Logs +^^^^^^^^ + +The RTCD service logs important events and errors. Set the log level to "debug" for troubleshooting: + +1. **In the configuration file**: + + .. code-block:: json + + { + "log": { + "level": "debug", + "json": true + } + } + +2. **Common log patterns to look for**: + + - **Connection errors**: Look for "failed to connect" or "connection error" messages + - **ICE negotiation failures**: Look for "ICE failed" or "ICE timeout" messages + - **API authentication issues**: Look for "unauthorized" or "invalid API key" messages + +Mattermost Logs +^^^^^^^^^^^^^ + +Check the Mattermost server logs for Calls plugin related issues: + +1. **Enable debug logging** in System Console > Environment > Logging > File Log Level + +2. **Filter for Calls-related logs**: + + .. code-block:: bash + + grep -i "calls" /path/to/mattermost.log + +3. **Look for common patterns**: + + - Connection errors to RTCD + - Plugin initialization issues + - WebSocket connection problems + +Browser Console Logs +^^^^^^^^^^^^^^^^^ + +Instruct users to check their browser console logs: + +1. **In Chrome/Edge**: + - Press F12 to open Developer Tools + - Go to the Console tab + - Look for errors related to WebRTC, Calls, or media permissions + +2. **Specific patterns to look for**: + + - "getUserMedia" errors (microphone permission issues) + - "ICE connection" failures + - WebSocket connection errors + +Performance Issues +--------------- + +Diagnosing High CPU Usage +^^^^^^^^^^^^^^^^^^^^^^^ + +If RTCD servers show high CPU usage: + +1. **Check concurrent calls and participants**: + + - Access the Prometheus metrics endpoint to see active sessions + - Compare with the benchmark data in the documentation + +2. **Profile CPU usage** (Linux): + + .. code-block:: bash + + top -p $(pgrep rtcd) + + Or for detailed per-thread usage: + + .. code-block:: bash + + ps -eLo pid,ppid,tid,pcpu,comm | grep rtcd + +3. **Enable pprof profiling** (if needed): + + Add to your RTCD configuration: + + .. code-block:: json + + { + "debug": { + "pprof": true, + "pprofPort": 6060 + } + } + + Then capture a CPU profile: + + .. code-block:: bash + + curl http://localhost:6060/debug/pprof/profile > cpu.profile + + Analyze with: + + .. code-block:: bash + + go tool pprof -http=:8080 cpu.profile + +Diagnosing Network Bottlenecks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you suspect network bandwidth issues: + +1. **Monitor network utilization**: + + .. code-block:: bash + + iftop -n + +2. **Check for packet drops**: + + .. code-block:: bash + + netstat -su | grep -E 'drop|error' + +3. **Verify system network buffers**: + + .. code-block:: bash + + sysctl -a | grep net.core.rmem + sysctl -a | grep net.core.wmem + + Ensure these match the recommended values: + + .. code-block:: bash + + net.core.rmem_max = 16777216 + net.core.wmem_max = 16777216 + net.core.optmem_max = 16777216 + +Debugging Tools +------------ + +WebRTC Internals (Chrome) +^^^^^^^^^^^^^^^^^^^^^^^^ + +For in-depth WebRTC diagnostics in Chrome: + +1. **Access chrome://webrtc-internals** in a new browser tab while on a call + +2. **Examine the connection details**: + + - ICE connection state + - Selected candidate pairs + - DTLS/SRTP setup + - Bandwidth estimation + +3. **Look for specific issues**: + + - Candidate gathering delays + - Failed ICE connections + - Bandwidth limitations + +Prometheus Metrics Analysis +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use Prometheus metrics for real-time and historical performance data: + +1. **Key metrics to monitor**: + + - ``rtcd_rtc_sessions_total``: Number of active RTC sessions + - ``rtcd_rtc_conn_states_total``: Connection state transitions + - ``rtcd_rtc_errors_total``: Error counts + - ``rtcd_rtc_rtp_tracks_total``: Media track count + - ``rtcd_process_cpu_seconds_total``: CPU usage + +2. **Set up Grafana dashboards**: + + Import the official [Mattermost Calls dashboard](https://github.com/mattermost/mattermost-performance-assets/blob/master/grafana/mattermost-calls-performance-monitoring.json) into Grafana for visualization. + +Advanced Diagnostics +----------------- + +WebRTC Diagnostic Commands +^^^^^^^^^^^^^^^^^^^^^^^^ + +For detailed WebRTC diagnostics: + +1. **Test STUN server connectivity**: + + .. code-block:: bash + + # Using stun-client (you may need to install it) + stun-client stun.global.calls.mattermost.com + + This should return your public IP address if STUN is working correctly. + +2. **Verify TURN server**: + + .. code-block:: bash + + # Using turnutils_uclient (part of coturn) + turnutils_uclient -v -s your-turn-server -u username -p password + + This tests if your TURN server is correctly configured. + +3. **Test end-to-end latency**: + + Between client locations and RTCD server: + + .. code-block:: bash + + ping -c 10 your-rtcd-server + + Look for consistent, low latency (<100ms ideally for voice calls). + +Client-Side Testing Tools +^^^^^^^^^^^^^^^^^^^^^^^ + +Tools to help diagnose client-side issues: + +1. **WebRTC Troubleshooter**: + + Direct users to [WebRTC Troubleshooter](https://test.webrtc.org/) for browser capability testing. + +2. **Network Quality Tests**: + + Use [Speedtest](https://www.speedtest.net/) or similar to check internet connection quality. + +3. **Browser-Specific WebRTC Info**: + + - Chrome: chrome://webrtc-internals + - Firefox: about:webrtc + +When to Contact Support +^^^^^^^^^^^^^^^^^^^^ + +Consider contacting Mattermost Support when: + +1. You've tried basic troubleshooting steps without resolution +2. You're experiencing persistent connection failures across multiple clients +3. You notice unexpected or degraded performance despite proper configuration +4. You need help interpreting diagnostic information +5. You suspect a bug in the Calls plugin or RTCD service + +When contacting support, please include: + +- RTCD version and configuration (with sensitive information redacted) +- Mattermost server version +- Calls plugin version +- Client environments (browsers, OS versions) +- Relevant logs and diagnostic information +- Detailed description of the issue and steps to reproduce \ No newline at end of file From 982164a737c937b9f29b17805af02432abf66ce9 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Thu, 8 May 2025 09:47:38 -0400 Subject: [PATCH 02/37] Refactored Kubernetes content to a separate page. I'm concerned about the degree of 'hallucination` on some of the K8s/Helm config that Claude came up with --- source/configure/calls-deployment.rst | 53 ++++---- source/configure/calls-kubernetes.rst | 175 ++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 24 deletions(-) create mode 100644 source/configure/calls-kubernetes.rst diff --git a/source/configure/calls-deployment.rst b/source/configure/calls-deployment.rst index 3f0c0b07c87..7113efdc332 100644 --- a/source/configure/calls-deployment.rst +++ b/source/configure/calls-deployment.rst @@ -14,6 +14,7 @@ For detailed information on specific topics, please refer to these specialized g - `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service - `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques - `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability +- `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments About Mattermost Calls --------------------- @@ -66,8 +67,8 @@ Limitations ----------- - In Mattermost Cloud, up to 200 participants per channel can join a call. -- In Mattermost self-hosted deployments, the default maximum number of participants is unlimited. The recommended maximum number of participants per call is 200. -- You can configure the maximum participants in **System Console > Plugin Management > Calls > Max call participants**. +- In Mattermost self-hosted deployments, the default maximum number of participants is unlimited. The recommended maximum number of participants per call is 200. This setting can be changed in **System Console > Plugin Management > Calls > Max call participants**. There's no limit to the total number of participants across all calls as the supported value greatly depends on instance resources. +- For more information on capacity planning, see the `Performance Considerations <#performance-considerations>`__ section below. Configuration ------------- @@ -82,46 +83,49 @@ Mattermost Calls can be deployed in several configurations: Single Instance Deployments ~~~~~~~~~~~~~~~~~~~~~~~~~~ +Integrated Mode +^^^^^^^^^^^^^ + +The WebRTC service runs within the Calls plugin on the Mattermost server. This is the default mode when first installing the plugin on a single Mattermost instance setup. The WebRTC service is integrated in the plugin itself and runs alongside the Mattermost server. + .. image:: ../images/calls-deployment-image3.png - :alt: A diagram of the integrated configuration model of a single instance. + :alt: Integrated configuration model of a single instance :width: 600px -**Integrated mode**: The WebRTC service runs within the Calls plugin on the Mattermost server. +RTCD Mode +^^^^^^^^ + +A dedicated RTCD service handles media routing, reducing load on the Mattermost server. .. image:: ../images/calls-deployment-image7.png - :alt: A diagram of a Web RTC deployment configuration. + :alt: Web RTC deployment configuration :width: 600px -**RTCD mode**: A dedicated RTCD service handles media routing, reducing load on the Mattermost server. - High Availability Deployments ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. image:: ../images/calls-deployment-image4.png - :alt: A diagram of a clustered calls deployment. - :width: 600px +Clustered Mode +^^^^^^^^^^^^ -**Clustered mode**: Each Mattermost node runs an instance of the plugin with its own WebRTC service. +This is the default mode when running the plugin in a high availability cluster-based deployment. Every Mattermost node will run an instance of the plugin that includes a WebRTC service. Calls are distributed across all available nodes through the existing load-balancer: a call is hosted on the instance where the initiating websocket connection (first client to join) is made. A single call will be hosted on a single cluster node. -.. image:: ../images/calls-deployment-image2.png - :alt: A diagram of an rtcd deployment. +.. image:: ../images/calls-deployment-image5.png + :alt: Clustered calls deployment :width: 600px -**RTCD with HA**: Dedicated RTCD services handle media routing for high availability. +RTCD with High Availability +^^^^^^^^^^ -Kubernetes Deployments -~~~~~~~~~~~~~~~~~~~~ +Dedicated RTCD services handle media routing for high availability. -.. image:: ../images/calls-deployment-kubernetes.png - :alt: A diagram of calls deployed in a Kubernetes cluster. +.. image:: ../images/calls-deployment-image2.png + :alt: RTCD deployment with high availability :width: 600px -For Kubernetes deployments, the RTCD service is strongly recommended and is the only officially supported approach. - -For Kubernetes deployments, the recommended approach is to use the officially provided Helm charts: +Kubernetes Deployments +~~~~~~~~~~~~~~~~~~~~ -- `rtcd Helm chart `__ -- `calls-offloader Helm chart `__ +RTCD is the only officially supported approach for Kubernetes deployments. For detailed information on deploying Mattermost Calls in Kubernetes environments, including Helm chart configurations, resource requirements, and scaling considerations, see the `Calls Deployment on Kubernetes `__ guide. When to Use RTCD -------------- @@ -184,4 +188,5 @@ Next Steps 1. For detailed setup instructions, see `RTCD Setup and Configuration `__ 2. For monitoring guidance, see `Calls Metrics and Monitoring `__ -3. If you encounter issues, see `Calls Troubleshooting `__ \ No newline at end of file +3. If you encounter issues, see `Calls Troubleshooting `__ +4. For Kubernetes deployments, see `Calls Deployment on Kubernetes `__ \ No newline at end of file diff --git a/source/configure/calls-kubernetes.rst b/source/configure/calls-kubernetes.rst new file mode 100644 index 00000000000..1f66f3a492b --- /dev/null +++ b/source/configure/calls-kubernetes.rst @@ -0,0 +1,175 @@ +Calls deployment on Kubernetes +=========================== + +.. include:: ../_static/badges/allplans-cloud-selfhosted.rst + :start-after: :nosearch: + +This guide provides detailed information for deploying Mattermost Calls on Kubernetes environments. + +Overview +-------- + +Mattermost Calls has been designed to integrate well with Kubernetes to offer improved scalability and control over the deployment. For Kubernetes deployments, the RTCD service is strongly recommended and is the only officially supported approach. + +Architecture +----------- + +.. image:: ../images/calls-deployment-kubernetes.png + :alt: Calls deployed in a Kubernetes cluster + :width: 600px + +This diagram shows how the RTCD standalone service can be deployed in a Kubernetes cluster. In this architecture: + +1. Calls traffic is handled by dedicated RTCD pods +2. RTCD services are exposed through load balancers +3. Scaling is managed through Kubernetes deployment configurations +4. Call recording and transcription is handled by the calls-offloader service + +If Mattermost isn't already deployed in your Kubernetes cluster and you want to use this deployment type, visit the `Kubernetes operator guide `__. + +Helm Chart Deployment +------------------- + +The recommended way to deploy Calls-related components in a Kubernetes environment is to use the officially provided Helm charts: + +RTCD Helm Chart +^^^^^^^^^^^^^ + +The RTCD Helm chart deploys the RTCD service needed for call media handling: + +.. code-block:: bash + + helm repo add mattermost https://helm.mattermost.com + helm repo update + + helm install mattermost-rtcd mattermost/mattermost-rtcd \ + --set ingress.enabled=true \ + --set ingress.host=rtcd.example.com \ + --set service.annotations."service\\.beta\\.kubernetes\\.io/aws-load-balancer-backend-protocol"=udp \ + --set rtcd.ice.hostOverride=rtcd.example.com + +For complete configuration options, see the `RTCD Helm chart documentation `__. + +Calls-Offloader Helm Chart +^^^^^^^^^^^^^^^^^^^^^^^^ + +If you need call recording and transcription capabilities, deploy the calls-offloader service: + +.. code-block:: bash + + helm install mattermost-calls-offloader mattermost/mattermost-calls-offloader \ + --set ingress.enabled=true \ + --set ingress.host=calls-offloader.example.com + +For complete configuration options, see the `Calls-Offloader Helm chart documentation `__. + +Kubernetes-Specific Configuration +------------------------------- + +Network Configuration +^^^^^^^^^^^^^^^^^^ + +For Kubernetes deployments, you need to ensure: + +1. UDP traffic is properly routed to RTCD pods (for media) +2. TCP traffic can reach both the Mattermost pods and RTCD pods +3. Load balancers are properly configured to handle UDP traffic +4. Network policies allow the required communications between services + +Recommended annotations for AWS environments: + +.. code-block:: yaml + + service.beta.kubernetes.io/aws-load-balancer-backend-protocol: udp + service.beta.kubernetes.io/aws-load-balancer-type: nlb + +Resource Requirements +^^^^^^^^^^^^^^^^^^ + +For optimal performance in Kubernetes environments: + +1. **CPU**: At least 2 CPU cores per RTCD pod +2. **Memory**: At least 1GB RAM per RTCD pod +3. **Network**: Sufficient bandwidth for expected call volume (see benchmarks) + +We recommend setting resource limits and requests in your deployment: + +.. code-block:: yaml + + resources: + requests: + cpu: 1000m + memory: 1Gi + limits: + cpu: 2000m + memory: 2Gi + +Scaling Considerations +^^^^^^^^^^^^^^^^^^ + +Horizontal scaling of RTCD pods is possible, but remember: + +1. Each call is hosted entirely on a single RTCD pod +2. DNS-based load balancing should be used to distribute calls among pods +3. Health checks should ensure that only healthy pods receive new calls +4. Calls remain on their assigned pod for their entire duration + +Limitations +^^^^^^^^^^ + +Due to the inherent complexities of hosting a WebRTC service, some limitations apply when deploying Calls in a Kubernetes environment. + +One key requirement is that each ``rtcd`` process must live in a dedicated Kubernetes node. This is necessary to forward the data correctly while allowing for horizontal scaling. Data should generally not go through a standard ingress but directly to the pod running the ``rtcd`` process. + +The general recommendation is to expose one external IP address per ``rtcd`` instance (Kubernetes node). This makes it simpler to scale as the application is able to detect its own external address (through STUN) and advertise it to clients to achieve connectivity with minimal configuration. + +If, for some reason, exposing multiple IP addresses is not possible in your environment, port mapping (NAT) can be used. In this scenario different ports are used to map the respective ``rtcd`` nodes behind the single external IP. Example: + +.. code-block:: text + + EXT_IP:8443 -> rtcdA:8443 + EXT_IP:8444 -> rtcdB:8443 + EXT_IP:8445 -> rtcdC:8443 + +This case requires a couple of extra configurations: + +* NAT mappings need to be in place for every ``rtcd`` node. This is usually done at the ingress point (e.g., ELB, NLB, etc). + +* The ``RTCD_RTC_ICEHOSTPORTOVERRIDE`` config should be used to pass a full mapping of node IPs and their respective port. + + * Example: ``RTCD_RTC_ICEHOSTPORTOVERRIDE=rtcdA_IP/8443,rtcdB_IP/8444,rtcdC_IP/8445`` + +* The ``RTCD_RTC_ICEHOSTOVERRIDE`` should be used to set the external IP address. + +.. note:: + One option to limit these static mappings is to reduce the size of the local subnet (e.g., to ``/29``). + +Monitoring and Metrics +^^^^^^^^^^^^^^^^^^^ + +We recommend deploying Prometheus and Grafana alongside your Calls deployment: + +1. Configure Prometheus to scrape metrics from both Mattermost and RTCD pods +2. Import the official Mattermost Calls dashboard to Grafana +3. Set up alerts for CPU usage, connection failures, and error rates + +For detailed information on metrics collection and monitoring, see the `Calls Metrics and Monitoring `__ guide. + +Troubleshooting +-------------- + +For Kubernetes-specific troubleshooting: + +1. Check pod logs: `kubectl logs -f deployment/mattermost-rtcd` +2. Verify service connectivity: `kubectl port-forward service/mattermost-rtcd 8045:8045` +3. Ensure UDP traffic is properly routed through your ingress/load balancer +4. Verify network policies allow required communication paths + +For detailed troubleshooting steps, see the `Calls Troubleshooting `__ guide. + +Next Steps +--------- + +1. For RTCD configuration details, see `RTCD Setup and Configuration `__ +2. For monitoring guidance, see `Calls Metrics and Monitoring `__ +3. If you encounter issues, see `Calls Troubleshooting `__ \ No newline at end of file From 25c7c0ab966a9b41b6a5069fd0044c05f03975e1 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Tue, 3 Jun 2025 15:31:24 -0400 Subject: [PATCH 03/37] Listing TODOs. Some minor tweaks. Getting the working directory committed to start using Claude to go through the TODOs. --- .gitignore | 2 + PULL_REQUEST_REVIEW_ITEMS.md | 30 +++++ source/configure/calls-deployment.rst | 6 +- source/configure/calls-metrics-monitoring.rst | 2 +- source/configure/calls-rtcd-setup.rst | 125 +++++++++++------- source/configure/calls-troubleshooting.rst | 9 +- 6 files changed, 122 insertions(+), 52 deletions(-) create mode 100644 PULL_REQUEST_REVIEW_ITEMS.md diff --git a/.gitignore b/.gitignore index 985ddd5e949..4c3c6194eb2 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,5 @@ source/developer/localization.md *.key *.crt .aider* + +**/.claude/settings.local.json diff --git a/PULL_REQUEST_REVIEW_ITEMS.md b/PULL_REQUEST_REVIEW_ITEMS.md new file mode 100644 index 00000000000..9e1bc015231 --- /dev/null +++ b/PULL_REQUEST_REVIEW_ITEMS.md @@ -0,0 +1,30 @@ +# Mattermost Calls Documentation TODOs + +This file contains a list of documentation improvements identified for the Mattermost Calls feature. + +## Completed Items + +- ✅ Update `calls-rtcd-setup.rst` with new version endpoint information +- ✅ Update `calls-troubleshooting.rst` ICE configuration section with correct parameter name +- ✅ Add client and RTCD log examples to ICE configuration issues +- ✅ Remove 'Why Use RTCD' section from calls-rtcd-setup.rst + +## High Priority Items + +- Re-order RTCD setup recommendations to prioritize "Bare Metal/VM", then "Docker", then "Kubernetes". I'll share my example systemd unit file and configuration. +- Create documentation for the calls-offloader service used for Calls Recording and Transcription +- Expand CORS documentation or document that `AllowCORSFrom` needs to be set to the SiteURL +- Add call-out/special note that the Calls metrics scraping has issues with `labels` setup in `prometheus.yml` and that the Calls dashboard expects : format, with an example. +- Document a common flow for each piece of the setup: Calls plugin curl/test/telemetry, RTCD curl/test/telemetry, calls-offloader curl/test/telemetry +- Document troubleshooting steps for the error "failed to create recording job: max concurrent jobs reached". + +## Medium Priority Items + +- Document the command `curl http://localhost:9090/api/v1/label/__name__/values | jq '.' | grep rtcd` as a troubleshooting tool in `calls-metrics-monitoring.rst` +- Document that Node Exporter service needs to be bound to the right address +- Document how to check Prometheus Scrape targets using the 'Targets' menu option for status +- Document this command for debugging calls-offloader: `docker ps --format "{{.ID}} {{.Image}}" | grep "calls" | awk '{print $1}' | xargs -I {} docker logs -f {}` +- Document the command `docker ps -a --filter "status=exited"` to view completed calls-offloader job containers +- Improve formatting for Network Requirements table to avoid small side-scrolling view +- Document experimental screen sharing audio feature with limitations and how to enable with `/call experimental on` slash command +- Provide documentation on what settings changes require what service/plugin restarts diff --git a/source/configure/calls-deployment.rst b/source/configure/calls-deployment.rst index 7113efdc332..3713272fd7c 100644 --- a/source/configure/calls-deployment.rst +++ b/source/configure/calls-deployment.rst @@ -38,9 +38,9 @@ Terminology Key Components ------------- -- **Calls plugin**: The main plugin that enables calls functionality -- **RTCD service**: Optional dedicated service for offloading media processing (Enterprise feature) -- **calls-offloader**: Service for call recording and transcription (if enabled) +- **Calls plugin**: The main plugin that enables calls functionality. Installed by default in Mattermost self-hosted deployments. +- **RTCD service**: Optional dedicated service for offloading media processing (Enterprise feature). Typically deployed to dedicated servers or containers. +- **calls-offloader**: Service for call recording and transcription (if enabled). Typically deployed to dedicated servers. Network Requirements ------------------ diff --git a/source/configure/calls-metrics-monitoring.rst b/source/configure/calls-metrics-monitoring.rst index 241b8fc282b..7c48930bb09 100644 --- a/source/configure/calls-metrics-monitoring.rst +++ b/source/configure/calls-metrics-monitoring.rst @@ -63,7 +63,7 @@ Installing Prometheus - job_name: 'rtcd' scrape_interval: 15s static_configs: - - targets: ['rtcd-server:9090'] + - targets: ['rtcd-server:8045'] Installing Grafana ^^^^^^^^^^^^^^^ diff --git a/source/configure/calls-rtcd-setup.rst b/source/configure/calls-rtcd-setup.rst index 73450faaabd..383ab9f88ba 100644 --- a/source/configure/calls-rtcd-setup.rst +++ b/source/configure/calls-rtcd-setup.rst @@ -21,7 +21,6 @@ Note This guide provides detailed instructions for setting up, configuring, and validating a Mattermost Calls deployment using the dedicated RTCD service. -- `Why use RTCD <#why-use-rtcd>`__ - `Prerequisites <#prerequisites>`__ - `Installation and deployment <#installation-and-deployment>`__ - `Configuration <#configuration>`__ @@ -29,21 +28,6 @@ This guide provides detailed instructions for setting up, configuring, and valid - `Horizontal scaling <#horizontal-scaling>`__ - `Integration with Mattermost <#integration-with-mattermost>`__ -Why use RTCD ------------ - -The RTCD service (Real-Time Communication Daemon) is the recommended way to host Mattermost Calls for production environments for the following key reasons: - -1. **Performance isolation**: RTCD runs as a standalone service, isolating the resource-intensive calls traffic from the main Mattermost servers. This prevents call traffic spikes from affecting the rest of your Mattermost deployment. - -2. **Scalability**: When calls traffic increases, additional RTCD instances can be deployed to handle the load, without affecting your Mattermost servers. - -3. **Call stability**: With RTCD, if a Mattermost server needs to be restarted, ongoing calls won't be disrupted. The call audio/video will continue while the Mattermost server restarts (though some features like emoji reactions will be temporarily unavailable). - -4. **Kubernetes support**: For Kubernetes deployments, RTCD is the only officially supported way to run Calls. - -5. **Real-time optimization**: The RTCD service is specifically optimized for real-time audio/video traffic, with configurations prioritizing low latency over throughput. - Prerequisites ------------ @@ -51,38 +35,79 @@ Before deploying RTCD, ensure you have: - A Mattermost Enterprise license - A server or VM with sufficient CPU and network capacity (see the `Performance `__ section for sizing guidance) -- Network configuration that allows: - - UDP port 8443 (default) open between clients and RTCD servers - - TCP port 8045 (default) open between Mattermost servers and RTCD servers - - TCP port 8443 (optional backup) between clients and RTCD servers + +Network Requirements +------------------ + +The following network connectivity is required: + ++-------------------+--------+-----------------+-------------------------+------------------------+ +| Service | Ports | Protocols | Source | Target | ++===================+========+=================+=========================+========================+ +| Calls plugin API | 80,443 | TCP (incoming) | Mattermost clients | Mattermost server | ++-------------------+--------+-----------------+-------------------------+------------------------+ +| RTC media | 8443 | UDP (incoming) | Mattermost clients | Mattermost or RTCD | ++-------------------+--------+-----------------+-------------------------+------------------------+ +| RTC media | 8443 | TCP (incoming) | Mattermost clients | Mattermost or RTCD | ++-------------------+--------+-----------------+-------------------------+------------------------+ +| RTCD API | 8045 | TCP (incoming) | Mattermost server | RTCD service | ++-------------------+--------+-----------------+-------------------------+------------------------+ +| STUN | 3478 | UDP (outgoing) | Mattermost or RTCD | STUN servers | ++-------------------+--------+-----------------+-------------------------+------------------------+ Installation and Deployment -------------------------- There are multiple ways to deploy RTCD, depending on your environment: -Bare Metal or VM Deployment -^^^^^^^^^^^^^^^^^^^^^^^^^^ +Docker Deployment +^^^^^^^^^^^^^^^ -1. Download the latest release from the `RTCD GitHub repository `__ +The simplest way to deploy RTCD with Docker is to use environment variables for configuration: -2. Create a configuration file (``config.toml``) with the following minimal settings: +1. Run the RTCD container with basic configuration: - .. code-block:: toml + .. code-block:: bash - [api] - http.listen_address = ":8045" + docker run -d --name rtcd \ + -e "RTCD_LOGGER_ENABLEFILE=false" \ + -e "RTCD_API_SECURITY_ALLOWSELFREGISTRATION=true" \ + -p 8443:8443/udp \ + -p 8443:8443/tcp \ + -p 8045:8045/tcp \ + mattermost/rtcd:latest - [rtc] - ice_address_udp = "" - ice_port_udp = 8443 - ice_host_override = "YOUR_RTCD_SERVER_PUBLIC_IP" +2. For debugging purposes, you can enable more detailed logging: -3. Run the RTCD service: + .. code-block:: bash + + docker run -d --name rtcd \ + -e "RTCD_LOGGER_ENABLEFILE=false" \ + -e "RTCD_LOGGER_CONSOLELEVEL=DEBUG" \ + -e "RTCD_API_SECURITY_ALLOWSELFREGISTRATION=true" \ + -p 8443:8443/udp \ + -p 8443:8443/tcp \ + -p 8045:8045/tcp \ + mattermost/rtcd:latest + + To view the logs: .. code-block:: bash - ./rtcd --config config.toml + docker logs -f rtcd + +You can also use a mounted configuration file instead of environment variables: + +.. code-block:: bash + + docker run -d --name rtcd \ + -p 8045:8045 \ + -p 8443:8443/udp \ + -p 8443:8443/tcp \ + -v /path/to/config.toml:/rtcd/config/config.toml \ + mattermost/rtcd:latest + +For a complete sample configuration file, see the `RTCD config.sample.toml `__ in the official repository. Kubernetes Deployment ^^^^^^^^^^^^^^^^^^^ @@ -108,21 +133,28 @@ For Kubernetes deployments, use the official Helm chart: Refer to the `RTCD Helm chart documentation `__ for additional configuration options. -Docker Deployment -^^^^^^^^^^^^^^^ +Bare Metal or VM Deployment +^^^^^^^^^^^^^^^^^^^^^^^^^^ -1. Create a configuration file as described in the Bare Metal section +1. Download the latest release from the `RTCD GitHub repository `__ + +2. Create a configuration file (``config.toml``) with the following minimal settings: -2. Run the RTCD container: + .. code-block:: toml + + [api] + http.listen_address = ":8045" + + [rtc] + ice_address_udp = "" + ice_port_udp = 8443 + ice_host_override = "YOUR_RTCD_SERVER_PUBLIC_IP" + +3. Run the RTCD service: .. code-block:: bash - docker run -d --name rtcd \ - -p 8045:8045 \ - -p 8443:8443/udp \ - -p 8443:8443/tcp \ - -v /path/to/config.toml:/rtcd/config/config.toml \ - mattermost/rtcd:latest + ./rtcd --config config.toml Configuration ----------- @@ -254,12 +286,13 @@ Validation and Testing After deploying RTCD, validate the installation: -1. **Check service status**: +1. **Check service status and version**: .. code-block:: bash - curl http://YOUR_RTCD_SERVER:8045/api/v1/health - # Should return {"status":"ok"} + curl http://YOUR_RTCD_SERVER:8045/version + # Should return a JSON object with service information + # Example: {"build_hash":"abc123","build_date":"2023-01-15T12:00:00Z","build_version":"0.11.0","goVersion":"go1.20.4"} 2. **Test UDP connectivity**: diff --git a/source/configure/calls-troubleshooting.rst b/source/configure/calls-troubleshooting.rst index d824cdfbca8..e498d3c5afc 100644 --- a/source/configure/calls-troubleshooting.rst +++ b/source/configure/calls-troubleshooting.rst @@ -29,8 +29,13 @@ Calls Not Connecting - Check that any load balancers are properly configured for UDP traffic 2. **ICE configuration issues**: - - Verify the ``ice.hostOverride`` setting in RTCD configuration matches the publicly accessible hostname or IP - - Ensure STUN/TURN servers are properly configured if needed + - Verify the ``rtc.ice_host_override`` setting in RTCD configuration matches the publicly accessible hostname or IP of the RTCD server + - If this setting is incorrect, client browser console may show errors like: ``com.mattermost.calls: peer error timed out waiting for rtc connection`` + - Meanwhile, RTCD `trace` level logs might show internal IP addresses in ICE connection logs: + + .. code-block:: json + + {"timestamp":"2025-05-14 10:29:08.935 Z","level":"trace","msg":"Ping STUN from udp4 host 172.31.29.117:8443 (resolved: 172.31.29.117:8443) to udp4 host 192.168.64.1:59737 (resolved: 192.168.64.1:59737)","caller":"rtc/logger.go:54","origin":"ice/v4.(*Agent).sendBindingRequest github.com/pion/ice/v4@v4.0.3/agent.go:921"} 3. **API connectivity**: - Verify that Mattermost servers can reach the RTCD API endpoint From 4a9b03da403e3c8b53b169fbc22f0ec72464377b Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Tue, 3 Jun 2025 17:05:30 -0400 Subject: [PATCH 04/37] Added Calls Offloader logs --- source/configure/calls-deployment.rst | 5 +- source/configure/calls-kubernetes.rst | 2 +- source/configure/calls-metrics-monitoring.rst | 3 + source/configure/calls-offloader-setup.rst | 372 ++++++++++++++++++ source/configure/calls-rtcd-setup.rst | 113 ++++-- source/configure/calls-troubleshooting.rst | 41 +- 6 files changed, 476 insertions(+), 60 deletions(-) create mode 100644 source/configure/calls-offloader-setup.rst diff --git a/source/configure/calls-deployment.rst b/source/configure/calls-deployment.rst index 3713272fd7c..e00e0568c60 100644 --- a/source/configure/calls-deployment.rst +++ b/source/configure/calls-deployment.rst @@ -12,6 +12,7 @@ Quick Links For detailed information on specific topics, please refer to these specialized guides: - `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service +- `Calls Offloader Setup and Configuration `__: Comprehensive guide for setting up the calls-offloader service for recording and transcription - `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques - `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability - `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments @@ -39,8 +40,8 @@ Key Components ------------- - **Calls plugin**: The main plugin that enables calls functionality. Installed by default in Mattermost self-hosted deployments. -- **RTCD service**: Optional dedicated service for offloading media processing (Enterprise feature). Typically deployed to dedicated servers or containers. -- **calls-offloader**: Service for call recording and transcription (if enabled). Typically deployed to dedicated servers. +- **RTCD service**: Optional dedicated service for offloading media processing (Enterprise feature). Typically deployed to dedicated servers or containers. See `RTCD Setup and Configuration `__ for details. +- **calls-offloader**: Service for call recording and transcription (if enabled). Typically deployed to dedicated servers. See `Calls Offloader Setup and Configuration `__ for setup and troubleshooting details. Network Requirements ------------------ diff --git a/source/configure/calls-kubernetes.rst b/source/configure/calls-kubernetes.rst index 1f66f3a492b..dca52a68f73 100644 --- a/source/configure/calls-kubernetes.rst +++ b/source/configure/calls-kubernetes.rst @@ -23,7 +23,7 @@ This diagram shows how the RTCD standalone service can be deployed in a Kubernet 1. Calls traffic is handled by dedicated RTCD pods 2. RTCD services are exposed through load balancers 3. Scaling is managed through Kubernetes deployment configurations -4. Call recording and transcription is handled by the calls-offloader service +4. Call recording and transcription is handled by the calls-offloader service (see `Calls Offloader Setup and Configuration `__) If Mattermost isn't already deployed in your Kubernetes cluster and you want to use this deployment type, visit the `Kubernetes operator guide `__. diff --git a/source/configure/calls-metrics-monitoring.rst b/source/configure/calls-metrics-monitoring.rst index 7c48930bb09..7a96c499640 100644 --- a/source/configure/calls-metrics-monitoring.rst +++ b/source/configure/calls-metrics-monitoring.rst @@ -65,6 +65,9 @@ Installing Prometheus static_configs: - targets: ['rtcd-server:8045'] + .. important:: + **Metrics Configuration Notice**: The Calls dashboard expects targets to be in ``:`` format. Avoid using ``labels`` in your Prometheus configuration for Calls metrics, as this can cause compatibility issues with the dashboard. For example, use ``targets: ['rtcd-server:8045']`` instead of labels-based targeting. + Installing Grafana ^^^^^^^^^^^^^^^ diff --git a/source/configure/calls-offloader-setup.rst b/source/configure/calls-offloader-setup.rst new file mode 100644 index 00000000000..6c467377329 --- /dev/null +++ b/source/configure/calls-offloader-setup.rst @@ -0,0 +1,372 @@ +Calls Offloader Setup and Configuration +======================================= + +.. include:: ../_static/badges/allplans-cloud-selfhosted.rst + :start-after: :nosearch: + +.. raw:: html + +
+ +Note + +|plans-img-yellow| The calls-offloader service is available only on `Enterprise `__ plans + +.. |plans-img-yellow| image:: ../_static/images/badges/flag_icon_yellow.svg + :class: mm-badge-flag + +.. raw:: html + +
+ +This guide provides detailed instructions for setting up, configuring, and validating the Mattermost calls-offloader service used for call recording and transcription features. + +- `Overview <#overview>`__ +- `Prerequisites <#prerequisites>`__ +- `Installation and deployment <#installation-and-deployment>`__ +- `Configuration <#configuration>`__ +- `Validation and testing <#validation-and-testing>`__ +- `Integration with Mattermost <#integration-with-mattermost>`__ +- `Troubleshooting <#troubleshooting>`__ + +Overview +-------- + +The calls-offloader service is a dedicated microservice that handles resource-intensive tasks for Mattermost Calls, including: + +- **Call recording**: Captures audio and screen sharing content from calls +- **Call transcription**: Provides automated transcription of recorded calls +- **Live captions** (Experimental): Real-time transcription during active calls + +By offloading these tasks to a dedicated service, the main Mattermost server and RTCD service can focus on core functionality while maintaining optimal performance. + +Prerequisites +------------- + +Before deploying calls-offloader, ensure you have: + +- A Mattermost Enterprise license +- A properly configured Mattermost Calls deployment (either integrated or with RTCD) +- Docker installed and running (for Docker-based job execution) +- Sufficient storage space for recordings (see `Storage Requirements <#storage-requirements>`__) +- A server or container environment with adequate resources + +System Requirements +^^^^^^^^^^^^^^^^^ + +For detailed system requirements and performance recommendations, refer to the `calls-offloader performance documentation `__. + +Storage Requirements +^^^^^^^^^^^^^^^^^^ + +Call recordings can consume significant storage space: + +- Audio-only recordings: ~1MB per minute per participant +- Screen sharing recordings: ~10-50MB per minute depending on content + +Installation and Deployment +--------------------------- + +Bare Metal or VM Deployment +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +1. Download the latest release from the `calls-offloader GitHub repository `__ + +2. Create the necessary directories: + + .. code-block:: bash + + sudo mkdir -p /opt/calls-offloader/data/db + sudo useradd --system --home /opt/calls-offloader calls-offloader + sudo chown -R calls-offloader:calls-offloader /opt/calls-offloader + +3. Create a configuration file (``/opt/calls-offloader/config.toml``): + + .. code-block:: toml + + [api] + http.listen_address = ":4545" + http.tls.enable = false + http.tls.cert_file = "" + http.tls.cert_key = "" + security.allow_self_registration = true + security.enable_admin = true + security.admin_secret_key = "changeme" + security.session_cache.expiration_minutes = 1440 + + [store] + data_source = "/opt/calls-offloader/data/db" + + [jobs] + api_type = "docker" + max_concurrent_jobs = 2 + failed_jobs_retention_time = "7d" + image_registry = "mattermost" + + [logger] + enable_console = true + console_json = false + console_level = "INFO" + enable_file = true + file_json = true + file_level = "INFO" + file_location = "/opt/calls-offloader/calls-offloader.log" + enable_color = true + +4. Create a systemd service file (``/etc/systemd/system/calls-offloader.service``): + + .. code-block:: ini + + [Unit] + Description=Mattermost Calls Offloader Service + After=network.target docker.service + Requires=docker.service + + [Service] + Type=simple + User=calls-offloader + WorkingDirectory=/opt/calls-offloader + ExecStart=/opt/calls-offloader/calls-offloader --config /opt/calls-offloader/config.toml + Restart=always + RestartSec=10 + LimitNOFILE=65536 + + [Install] + WantedBy=multi-user.target + +5. Enable and start the service: + + .. code-block:: bash + + sudo systemctl daemon-reload + sudo systemctl enable calls-offloader + sudo systemctl start calls-offloader + +6. Check the service status: + + .. code-block:: bash + + sudo systemctl status calls-offloader + +7. Verify the service is responding: + + .. code-block:: bash + + curl http://localhost:4545/version + # Example output: + # {"buildDate":"2025-03-10 19:13","buildVersion":"v0.9.2","buildHash":"a4bd418","goVersion":"go1.23.6"} + + +Configuration +------------- + +API Configuration +^^^^^^^^^^^^^^^ + +The API section controls how the service accepts requests: + +- **http.listen_address**: The address and port where the service listens (default: ``:4545``) +- **http.tls.enable**: Whether to use TLS encryption for the API +- **security.allow_self_registration**: Allow clients to self-register for job management +- **security.enable_admin**: Enable admin functionality +- **security.admin_secret_key**: Secret key for admin authentication (change from default!) + +Store Configuration +^^^^^^^^^^^^^^^^^ + +Controls persistent data storage: + +- **data_source**: Path to directory for storing job metadata and state + +Jobs Configuration +^^^^^^^^^^^^^^^^ + +Controls job processing behavior: + +- **api_type**: Job execution backend (``docker`` or ``kubernetes``) +- **max_concurrent_jobs**: Maximum number of simultaneous recording/transcription jobs +- **failed_jobs_retention_time**: How long to keep failed job data before cleanup +- **image_registry**: Docker registry for job runner images (typically ``mattermost``) + +Logger Configuration +^^^^^^^^^^^^^^^^^^ + +Controls logging output: + +- **enable_console**: Log to console output +- **console_json**: Use JSON format for console logs +- **console_level**: Log level for console (DEBUG, INFO, WARN, ERROR) +- **enable_file**: Log to file +- **file_location**: Path to log file +- **enable_color**: Use colored output for console logs + +Private Network Configuration +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When the Mattermost deployment is running in a private network, additional configuration may be necessary for the jobs spawned by the calls-offloader service to reach the Mattermost server. + +In such cases, you can override the site URL used by recorder jobs or transcriber jobs to connect to Mattermost by setting the following environment variables on the Mattermost server: + +- **MM_CALLS_RECORDER_SITE_URL**: Override the site URL used by recording jobs +- **MM_CALLS_TRANSCRIBER_SITE_URL**: Override the site URL used by transcription jobs + +Example configuration: + +Create or edit the Mattermost environment file (``/opt/mattermost/config/mattermost.environment``): + +.. code-block:: bash + + MM_CALLS_RECORDER_SITE_URL="http://internal-mattermost-server:8065" + MM_CALLS_TRANSCRIBER_SITE_URL="http://internal-mattermost-server:8065" + +Then ensure your Mattermost systemd service references this environment file: + +.. code-block:: ini + + [Unit] + Description=Mattermost + After=network.target + + [Service] + Type=notify + EnvironmentFile=/opt/mattermost/config/mattermost.environment + ExecStart=/opt/mattermost/bin/mattermost + TimeoutStartSec=3600 + KillMode=mixed + Restart=always + RestartSec=10 + WorkingDirectory=/opt/mattermost + User=mattermost + Group=mattermost + + [Install] + WantedBy=multi-user.target + +This is particularly useful when: + +- The calls-offloader service runs in a different network segment than clients +- Internal DNS resolution differs from external URLs +- You need to use internal load balancer endpoints for job communication + +Validation and Testing +--------------------- + +After deploying calls-offloader, validate the installation: + +1. **Check service status**: + + .. code-block:: bash + + # For systemd + sudo systemctl status calls-offloader + + +2. **Test API connectivity**: + + .. code-block:: bash + + curl http://localhost:4545/version + # Should return version information + +3. **Verify Docker integration** (if using docker api_type): + + .. code-block:: bash + + # Check that system user running calls-offloader can access Docker + sudo -u calls-offloader docker ps + +Integration with Mattermost +--------------------------- + +Once calls-offloader is properly set up and validated, configure Mattermost to use it: + +1. Go to **System Console > Plugins > Calls** + +2. In the **Job Service** section: + + - Set **Job Service URL** to your calls-offloader service (e.g., ``http://calls-offloader-server:4545``) + - Configure **Job Service Secret** to match your ``admin_secret_key`` if enabled + +3. Enable recording and transcription features as needed: + + - **Enable Call Recordings**: Toggle to allow call recordings + - **Enable Call Transcriptions**: Toggle to allow call transcriptions + - **Enable Live Captions** (Experimental): Toggle to allow real-time transcription + +4. Save the configuration + +5. Test by starting a call and enabling recording or live captions + +Troubleshooting +--------------- + +Common Issues +^^^^^^^^^^^ + +**"failed to create recording job: max concurrent jobs reached"** + +This error occurs when the calls-offloader service has reached its configured job limit. + +Solutions: + +- Increase ``max_concurrent_jobs`` in the configuration +- Check if jobs are hanging and restart the service +- Monitor system resources and scale up if needed + +**Jobs not processing** + +Check the following: + +- Verify the calls-offloader service is running: ``sudo systemctl status calls-offloader`` +- Ensure network connectivity between Mattermost and calls-offloader +- Check Docker daemon is running and accessible by the user running `calls-offloader`: ``docker ps`` +- Verify authentication configuration matches between services +- Review service logs for specific error messages + +**Docker permission issues** + +If using Docker API and seeing permission errors: + +.. code-block:: bash + + # Add calls-offloader user to docker group + sudo usermod -a -G docker calls-offloader + sudo systemctl restart calls-offloader + +Debugging Commands +^^^^^^^^^^^^^^^^ + +Monitor calls-offloader job containers: + +.. code-block:: bash + + # View running job containers + docker ps --format "{{.ID}} {{.Image}}" | grep "calls" + + # Follow logs for debugging + docker ps --format "{{.ID}} {{.Image}}" | grep "calls" | awk '{print $1}' | xargs -I {} docker logs -f {} + + # View completed job containers + docker ps -a --filter "status=exited" + +Monitor service health: + +.. code-block:: bash + + # Check service version and health + curl http://localhost:4545/version + +Check service logs: + +.. code-block:: bash + + # View recent logs + sudo journalctl -u calls-offloader -f + + # View log file (if file logging enabled) + tail -f /opt/calls-offloader/calls-offloader.log + +Performance Monitoring +^^^^^^^^^^^^^^^^^^^^ + +For detailed performance tuning and monitoring recommendations, refer to the `calls-offloader performance documentation `__. \ No newline at end of file diff --git a/source/configure/calls-rtcd-setup.rst b/source/configure/calls-rtcd-setup.rst index 383ab9f88ba..9cc2e484090 100644 --- a/source/configure/calls-rtcd-setup.rst +++ b/source/configure/calls-rtcd-setup.rst @@ -58,12 +58,98 @@ The following network connectivity is required: Installation and Deployment -------------------------- -There are multiple ways to deploy RTCD, depending on your environment: +There are multiple ways to deploy RTCD, depending on your environment. We recommend the following order based on production readiness and operational control: + +Bare Metal or VM Deployment (Recommended) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This is the recommended deployment method for production environments as it provides the best performance and operational control. + +1. Download the latest release from the `RTCD GitHub repository `__ + +2. Create a configuration file (``/opt/rtcd/rtcd.toml``) with the following settings: + + .. code-block:: toml + + [api] + http.listen_address = ":8045" + security.allow_self_registration = true + + [rtc] + ice_address_udp = "" + ice_port_udp = 8443 + ice_address_tcp = "" + ice_port_tcp = 8443 + ice_host_override = "YOUR_RTCD_SERVER_PUBLIC_IP" + + # UDP port range for WebRTC connections + ice.port_range.min = 9000 + ice.port_range.max = 10000 + + # STUN/TURN server configuration + ice_servers = [ + { urls = ["stun:stun.l.google.com:19302"] } + ] + + [store] + data_source = "/opt/rtcd/data/db" + + [logger] + enable_console = true + console_json = true + console_level = "INFO" + enable_file = true + file_json = true + file_level = "INFO" + file_location = "/opt/rtcd/rtcd.log" + enable_color = true + + [mattermost] + host = "http://YOUR_MATTERMOST_SERVER:8065" + +3. Create the data directory: + + .. code-block:: bash + + sudo mkdir -p /opt/rtcd/data/db + +4. Create a systemd service file (``/etc/systemd/system/rtcd.service``): + + .. code-block:: ini + + [Unit] + Description=Mattermost RTCD Server + After=network.target + + [Service] + Type=simple + User=root + ExecStart=/opt/rtcd/rtcd --config /opt/rtcd/rtcd.toml + Restart=always + RestartSec=10 + LimitNOFILE=65536 + + [Install] + WantedBy=multi-user.target + +5. Enable and start the service: + + .. code-block:: bash + + sudo systemctl daemon-reload + sudo systemctl enable rtcd + sudo systemctl start rtcd + +6. Check the service status: + + .. code-block:: bash + + sudo systemctl status rtcd Docker Deployment ^^^^^^^^^^^^^^^ -The simplest way to deploy RTCD with Docker is to use environment variables for configuration: +Docker deployment is suitable for development, testing, or containerized production environments: 1. Run the RTCD container with basic configuration: @@ -133,29 +219,6 @@ For Kubernetes deployments, use the official Helm chart: Refer to the `RTCD Helm chart documentation `__ for additional configuration options. -Bare Metal or VM Deployment -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -1. Download the latest release from the `RTCD GitHub repository `__ - -2. Create a configuration file (``config.toml``) with the following minimal settings: - - .. code-block:: toml - - [api] - http.listen_address = ":8045" - - [rtc] - ice_address_udp = "" - ice_port_udp = 8443 - ice_host_override = "YOUR_RTCD_SERVER_PUBLIC_IP" - -3. Run the RTCD service: - - .. code-block:: bash - - ./rtcd --config config.toml - Configuration ----------- diff --git a/source/configure/calls-troubleshooting.rst b/source/configure/calls-troubleshooting.rst index e498d3c5afc..7437cd765ae 100644 --- a/source/configure/calls-troubleshooting.rst +++ b/source/configure/calls-troubleshooting.rst @@ -330,45 +330,21 @@ If you suspect network bandwidth issues: net.core.wmem_max = 16777216 net.core.optmem_max = 16777216 -Debugging Tools ------------- - -WebRTC Internals (Chrome) -^^^^^^^^^^^^^^^^^^^^^^^^ - -For in-depth WebRTC diagnostics in Chrome: +Recording and Transcription Issues +---------------------------------- -1. **Access chrome://webrtc-internals** in a new browser tab while on a call +For troubleshooting calls-offloader service issues including recording and transcription problems, see the `Calls Offloader Setup and Configuration `__ guide. -2. **Examine the connection details**: - - - ICE connection state - - Selected candidate pairs - - DTLS/SRTP setup - - Bandwidth estimation +Debugging Tools +------------ -3. **Look for specific issues**: - - - Candidate gathering delays - - Failed ICE connections - - Bandwidth limitations Prometheus Metrics Analysis ^^^^^^^^^^^^^^^^^^^^^^^^^ Use Prometheus metrics for real-time and historical performance data: -1. **Key metrics to monitor**: - - - ``rtcd_rtc_sessions_total``: Number of active RTC sessions - - ``rtcd_rtc_conn_states_total``: Connection state transitions - - ``rtcd_rtc_errors_total``: Error counts - - ``rtcd_rtc_rtp_tracks_total``: Media track count - - ``rtcd_process_cpu_seconds_total``: CPU usage - -2. **Set up Grafana dashboards**: - - Import the official [Mattermost Calls dashboard](https://github.com/mattermost/mattermost-performance-assets/blob/master/grafana/mattermost-calls-performance-monitoring.json) into Grafana for visualization. +Import the official [Mattermost Calls dashboard](https://github.com/mattermost/mattermost-performance-assets/blob/master/grafana/mattermost-calls-performance-monitoring.json) into Grafana for visualization. Advanced Diagnostics ----------------- @@ -429,7 +405,7 @@ When to Contact Support Consider contacting Mattermost Support when: -1. You've tried basic troubleshooting steps without resolution +1. You've tried troubleshooting steps without resolution 2. You're experiencing persistent connection failures across multiple clients 3. You notice unexpected or degraded performance despite proper configuration 4. You need help interpreting diagnostic information @@ -442,4 +418,5 @@ When contacting support, please include: - Calls plugin version - Client environments (browsers, OS versions) - Relevant logs and diagnostic information -- Detailed description of the issue and steps to reproduce \ No newline at end of file +- Detailed description of the issue and steps to reproduce +- Monitoring dashboards screenshots \ No newline at end of file From 2e75095b96c8757f65b85310bdda70c343d40e51 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 6 Jun 2025 14:40:15 -0400 Subject: [PATCH 05/37] Added bottom navigation to get to all sections --- source/configure/calls-kubernetes.rst | 13 ++++++++----- source/configure/calls-metrics-monitoring.rst | 9 +++++++++ source/configure/calls-offloader-setup.rst | 11 +++++++++++ source/configure/calls-rtcd-setup.rst | 11 ++++++++++- source/configure/calls-troubleshooting.rst | 9 +++++++++ 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/source/configure/calls-kubernetes.rst b/source/configure/calls-kubernetes.rst index dca52a68f73..b9936db5cb0 100644 --- a/source/configure/calls-kubernetes.rst +++ b/source/configure/calls-kubernetes.rst @@ -167,9 +167,12 @@ For Kubernetes-specific troubleshooting: For detailed troubleshooting steps, see the `Calls Troubleshooting `__ guide. -Next Steps ---------- - -1. For RTCD configuration details, see `RTCD Setup and Configuration `__ -2. For monitoring guidance, see `Calls Metrics and Monitoring `__ +Other Calls Documentation +---------------- + +- `Calls Overview `__: Overview of deployment options and architecture +- `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service +- `Calls Offloader Setup and Configuration `__: Setup guide for call recording and transcription +- `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability +- `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques 3. If you encounter issues, see `Calls Troubleshooting `__ \ No newline at end of file diff --git a/source/configure/calls-metrics-monitoring.rst b/source/configure/calls-metrics-monitoring.rst index 7a96c499640..a3ae4c992cd 100644 --- a/source/configure/calls-metrics-monitoring.rst +++ b/source/configure/calls-metrics-monitoring.rst @@ -322,4 +322,13 @@ For historical analysis and trend identification: - **Medium-term metrics**: Keep 1-minute resolution data for 2 months - **Long-term metrics**: Keep 5-minute resolution data for 1 year +Other Calls Documentation +---------------- + +- `Calls Overview `__: Overview of deployment options and architecture +- `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service +- `Calls Offloader Setup and Configuration `__: Setup guide for call recording and transcription +- `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments +- `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques + Configure Prometheus storage accordingly to balance disk usage with retention needs. \ No newline at end of file diff --git a/source/configure/calls-offloader-setup.rst b/source/configure/calls-offloader-setup.rst index 6c467377329..a8e45adbdf5 100644 --- a/source/configure/calls-offloader-setup.rst +++ b/source/configure/calls-offloader-setup.rst @@ -369,4 +369,15 @@ Check service logs: Performance Monitoring ^^^^^^^^^^^^^^^^^^^^ +Monitor calls-offloader performance and resource usage to ensure optimal operation. Set up alerts for high CPU/memory usage, failed jobs, or extended processing times. + +Other Calls Documentation +---------------- + +- `Calls Overview `__: Overview of deployment options and architecture +- `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service +- `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability +- `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments +- `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques + For detailed performance tuning and monitoring recommendations, refer to the `calls-offloader performance documentation `__. \ No newline at end of file diff --git a/source/configure/calls-rtcd-setup.rst b/source/configure/calls-rtcd-setup.rst index 9cc2e484090..908c2bf4cd3 100644 --- a/source/configure/calls-rtcd-setup.rst +++ b/source/configure/calls-rtcd-setup.rst @@ -88,7 +88,7 @@ This is the recommended deployment method for production environments as it prov # STUN/TURN server configuration ice_servers = [ - { urls = ["stun:stun.l.google.com:19302"] } + { urls = ["stun:stun.global.calls.mattermost.com:3478"] } ] [store] @@ -543,4 +543,13 @@ Once RTCD is properly set up and validated, configure Mattermost to use it: 7. Verify that the call is being routed through RTCD by checking the RTCD logs and metrics +Other Calls Documentation +---------------- + +- `Calls Overview `__: Overview of deployment options and architecture +- `Calls Offloader Setup and Configuration `__: Setup guide for call recording and transcription +- `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability +- `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments +- `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques + For detailed Mattermost Calls configuration options, see the `Calls Plugin Configuration Settings `__ documentation. \ No newline at end of file diff --git a/source/configure/calls-troubleshooting.rst b/source/configure/calls-troubleshooting.rst index 7437cd765ae..238ca1d8ca7 100644 --- a/source/configure/calls-troubleshooting.rst +++ b/source/configure/calls-troubleshooting.rst @@ -419,4 +419,13 @@ When contacting support, please include: - Client environments (browsers, OS versions) - Relevant logs and diagnostic information - Detailed description of the issue and steps to reproduce + +Other Calls Documentation +---------------- + +- `Calls Overview `__: Overview of deployment options and architecture +- `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service +- `Calls Offloader Setup and Configuration `__: Setup guide for call recording and transcription +- `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability +- `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments - Monitoring dashboards screenshots \ No newline at end of file From e1456b68106595879b647f8b4ce758bff96684a9 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 6 Jun 2025 14:46:49 -0400 Subject: [PATCH 06/37] Add toctree for sidebar navigation --- source/configure/calls-deployment.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/source/configure/calls-deployment.rst b/source/configure/calls-deployment.rst index e00e0568c60..816f0f6e384 100644 --- a/source/configure/calls-deployment.rst +++ b/source/configure/calls-deployment.rst @@ -6,6 +6,16 @@ Calls self-hosted deployment This document provides an overview of Mattermost Calls deployment options for self-hosted environments, including deployment architectures, key requirements, and important considerations. +.. toctree:: + :maxdepth: 1 + :hidden: + + calls-rtcd-setup + calls-offloader-setup + calls-metrics-monitoring + calls-kubernetes + calls-troubleshooting + Quick Links ---------- From 0fff7b7012dd91b9d5fe96ffa60470da4d60280c Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 6 Jun 2025 14:56:12 -0400 Subject: [PATCH 07/37] Removed Claude's extrapolation to COTURN config --- source/configure/calls-rtcd-setup.rst | 146 +------------------------- 1 file changed, 1 insertion(+), 145 deletions(-) diff --git a/source/configure/calls-rtcd-setup.rst b/source/configure/calls-rtcd-setup.rst index 908c2bf4cd3..88ff81757c2 100644 --- a/source/configure/calls-rtcd-setup.rst +++ b/source/configure/calls-rtcd-setup.rst @@ -289,41 +289,7 @@ For clients behind strict firewalls, you may need to configure STUN/TURN servers { urls = ["turn:turn.example.com:3478"], username = "turnuser", credential = "turnpassword" } ] -We recommend using `coturn `__ for your TURN server implementation. For setting up and configuring coturn: - -1. Refer to the `official coturn documentation `__ -2. A basic coturn configuration file might look like this: - - .. code-block:: text - - # Basic coturn configuration - customize for your environment - # Refer to official documentation for complete options - - # Listener interface(s) - listening-ip=YOUR_SERVER_IP - listening-port=3478 - - # Relay interface(s) - relay-ip=YOUR_SERVER_IP - min-port=49152 - max-port=65535 - - # Authentication - lt-cred-mech - user=turnuser:turnpassword - - # TLS (recommended for production) - # cert=/path/to/cert.pem - # pkey=/path/to/privkey.pem - - # Logging - verbose - fingerprint - -3. Always test your TURN server connectivity before deploying to production using a tool like `Trickle ICE `__ - -For more advanced scenarios or troubleshooting, consult the official coturn documentation and WebRTC resources. - +We recommend using `coturn `__ for your TURN server implementation. System Tuning ^^^^^^^^^^^ @@ -414,116 +380,6 @@ To scale RTCD horizontally: The Mattermost Calls plugin will distribute calls among the available RTCD hosts. Remember that a single call will always be hosted on one RTCD instance; sessions belonging to the same call are not spread across different instances. -RTCD Connectivity Diagrams ------------------------ - -Understanding the network connectivity between clients, Mattermost servers, and RTCD services is crucial for proper deployment. The following diagrams illustrate the key communication paths in different deployment scenarios. - -Basic RTCD Deployment -^^^^^^^^^^^^^^^^^^^ - -In this basic deployment model, RTCD handles all media traffic while the Mattermost server manages signaling: - -:: - - +----------------+ +----------------+ +----------------+ - | | 1 | | 2 | | - | Client A |<----->| Mattermost |<----->| RTCD | - | | WS | Server | API | Service | - | | | | | | - +----------------+ +----------------+ +----------------+ - ^ ^ - | | - | Media (RTP) | - | 3 | - +-------------------------------------------------+ - -1. **WebSocket Connection (WS)**: Clients connect to Mattermost server using WebSockets for signaling and call control -2. **API Connection**: Mattermost server communicates with RTCD service for call setup and management -3. **Media (RTP) Connection**: Clients send/receive audio and screen sharing directly with RTCD service - -High Availability RTCD Deployment -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For high availability, multiple RTCD instances can be deployed with DNS-based load balancing: - -:: - - +----------------+ +----------------+ +----------------+ - | | | | | RTCD #1 | - | Client A | | Mattermost |<----->| | - | | | Server | +----------------+ - +----------------+ | HA | - ^ | | +----------------+ - | | | | RTCD #2 | - +----------------+ | |<----->| | - | | | | +----------------+ - | Client B |<----->| | - | | | | +----------------+ - +----------------+ | | | RTCD #3 | - ^ | |<----->| | - | +----------------+ +----------------+ - | ^ - | | - +-------------------------------------------------+ - Media flows to appropriate - RTCD instance - -In this model: -- Each client connects to Mattermost through the load balancer -- Mattermost distributes calls among available RTCD instances -- A single call is always hosted on one RTCD instance -- If an RTCD instance fails, only calls on that instance are affected - -RTCD with TURN Server -^^^^^^^^^^^^^^^^^^ - -For environments with restrictive firewalls, a TURN server can relay media: - -:: - - +----------------+ +----------------+ +----------------+ - | | | | | | - | Client A |<----->| Mattermost |<----->| RTCD | - | (Firewall) | | Server | | Service | - | | | | | | - +----------------+ +----------------+ +----------------+ - ^ ^ - | | - | | - v | - +----------------+ | - | | | - | TURN Server |<---------------------------------------+ - | | Media Relay - +----------------+ - -- Clients behind restrictive firewalls connect to the TURN server -- TURN server relays media between clients and RTCD -- Adds some latency but enables connectivity in challenging network environments - -Detailed Network Protocol Diagram -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This diagram shows the specific protocols and ports used in a typical RTCD deployment: - -:: - - WebSockets HTTP(S) RTCD API - TCP 80/443 TCP 8045 - +--------------+ +--------------+ +------------------------+ - | | | | | | - | Clients |<--| Mattermost |<--| RTCD | - | | | Server | | | - +--------------+ +--------------+ +------------------------+ - ^ ^ - | | - | | - | Media (RTP/RTCP) | - +------------------------------------------+ - UDP 8443 (preferred) - TCP 8443 (fallback) - Integration with Mattermost ------------------------- From 3321c25f7993e951b91c9102111d8828adc53d8e Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 6 Jun 2025 15:10:57 -0400 Subject: [PATCH 08/37] Clean up from review and validation --- source/configure/calls-rtcd-setup.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/source/configure/calls-rtcd-setup.rst b/source/configure/calls-rtcd-setup.rst index 88ff81757c2..18dc0001f6c 100644 --- a/source/configure/calls-rtcd-setup.rst +++ b/source/configure/calls-rtcd-setup.rst @@ -290,6 +290,7 @@ For clients behind strict firewalls, you may need to configure STUN/TURN servers ] We recommend using `coturn `__ for your TURN server implementation. + System Tuning ^^^^^^^^^^^ @@ -339,17 +340,20 @@ After deploying RTCD, validate the installation: Type a message and hit Enter on either side. If messages are received on both ends, UDP connectivity is working. + Note: This test must be run with the RTCD service stopped, as it binds to the same port. + + .. code-block:: bash + + sudo systemctl stop rtcd + + 3. **Test TCP connectivity** (if enabled): Similar to the UDP test, but remove the ``-u`` flag from both commands. 4. **Monitor metrics**: - If you've enabled Prometheus metrics, access them at: - - .. code-block:: bash - - curl http://YOUR_RTCD_SERVER:9090/metrics + Refer to `Calls Metrics and Monitoring `__ for setting up Calls metrics and monitoring. Horizontal Scaling ---------------- From 7fe4b70aac9b34ca23f21d831157ccd00b4748a9 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 6 Jun 2025 15:23:20 -0400 Subject: [PATCH 09/37] Expanded API connectivity testing instructions --- source/configure/calls-offloader-setup.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/source/configure/calls-offloader-setup.rst b/source/configure/calls-offloader-setup.rst index a8e45adbdf5..813b621b8a5 100644 --- a/source/configure/calls-offloader-setup.rst +++ b/source/configure/calls-offloader-setup.rst @@ -263,10 +263,27 @@ After deploying calls-offloader, validate the installation: 2. **Test API connectivity**: + **From the calls-offloader server (localhost test)**: + .. code-block:: bash curl http://localhost:4545/version # Should return version information + # Example: {"buildDate":"2025-03-10 19:13","buildVersion":"v0.9.2","buildHash":"a4bd418","goVersion":"go1.23.6"} + + **From the Mattermost server**: + + .. code-block:: bash + + curl http://YOUR_CALLS_OFFLOADER_SERVER:4545/version + # Should return the same version information + # This confirms network connectivity from Mattermost to calls-offloader + + If the localhost test works but the Mattermost server test fails, check: + + - Firewall rules or SELinux policies on the calls-offloader server (port 4545 must be accessible) + - Network connectivity between Mattermost and calls-offloader servers + - calls-offloader service binding configuration (ensure it's not bound to localhost only) 3. **Verify Docker integration** (if using docker api_type): From 13c5724c1efd21b479b08192ea3c91b615ec043e Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 6 Jun 2025 15:27:20 -0400 Subject: [PATCH 10/37] Removed reference to job service secret We don't really talk about it enough in the docs anyway. Leave that for another revision --- source/configure/calls-offloader-setup.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/source/configure/calls-offloader-setup.rst b/source/configure/calls-offloader-setup.rst index 813b621b8a5..34773f4683b 100644 --- a/source/configure/calls-offloader-setup.rst +++ b/source/configure/calls-offloader-setup.rst @@ -302,7 +302,6 @@ Once calls-offloader is properly set up and validated, configure Mattermost to u 2. In the **Job Service** section: - Set **Job Service URL** to your calls-offloader service (e.g., ``http://calls-offloader-server:4545``) - - Configure **Job Service Secret** to match your ``admin_secret_key`` if enabled 3. Enable recording and transcription features as needed: From 22057ab41bad0a4f1bbc72122e69f4792f84f2c0 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 6 Jun 2025 15:28:41 -0400 Subject: [PATCH 11/37] Added plugin restart guidance when configuring offloader --- source/configure/calls-offloader-setup.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/source/configure/calls-offloader-setup.rst b/source/configure/calls-offloader-setup.rst index 34773f4683b..e9d11a3b576 100644 --- a/source/configure/calls-offloader-setup.rst +++ b/source/configure/calls-offloader-setup.rst @@ -311,7 +311,13 @@ Once calls-offloader is properly set up and validated, configure Mattermost to u 4. Save the configuration -5. Test by starting a call and enabling recording or live captions +5. Restart the Calls plugin to re-establish state: + + - Go to **System Console > Plugins > Plugin Management** + - Find the **Calls** plugin and click **Disable** + - Wait a few seconds, then click **Enable** + +6. Test by starting a call and enabling recording or live captions Troubleshooting --------------- From 1d682d5527a7c5b62d383aea24800c1efd488fb4 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 6 Jun 2025 16:15:48 -0400 Subject: [PATCH 12/37] Tweaked footer links --- source/configure/calls-offloader-setup.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/configure/calls-offloader-setup.rst b/source/configure/calls-offloader-setup.rst index e9d11a3b576..195020f787f 100644 --- a/source/configure/calls-offloader-setup.rst +++ b/source/configure/calls-offloader-setup.rst @@ -391,7 +391,7 @@ Check service logs: Performance Monitoring ^^^^^^^^^^^^^^^^^^^^ -Monitor calls-offloader performance and resource usage to ensure optimal operation. Set up alerts for high CPU/memory usage, failed jobs, or extended processing times. +Monitor calls-offloader performance and resource usage to ensure optimal operation. See `Calls Metrics and Monitoring `__ for details on setting up metrics and observability. Other Calls Documentation ---------------- @@ -401,5 +401,4 @@ Other Calls Documentation - `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability - `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments - `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques - -For detailed performance tuning and monitoring recommendations, refer to the `calls-offloader performance documentation `__. \ No newline at end of file +- `calls-offloader performance documentation `__: Detailed performance tuning and monitoring recommendations \ No newline at end of file From 1caaf3eb33cf40ce77732e2a692bff27b68813fa Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 6 Jun 2025 16:22:12 -0400 Subject: [PATCH 13/37] Tweaks and validation of metrics --- source/configure/calls-metrics-monitoring.rst | 126 +++--------------- 1 file changed, 18 insertions(+), 108 deletions(-) diff --git a/source/configure/calls-metrics-monitoring.rst b/source/configure/calls-metrics-monitoring.rst index a3ae4c992cd..b64c04db395 100644 --- a/source/configure/calls-metrics-monitoring.rst +++ b/source/configure/calls-metrics-monitoring.rst @@ -176,116 +176,35 @@ Mattermost provides an official Grafana dashboard for monitoring Calls performan - Navigate to Dashboards > Import - Upload the JSON file or paste its contents - Select your Prometheus data source + - Confirm the correct rtcd (`5045`) and calls plugin (`8065`) targets are set - Click Import -3. **Key panels** in the dashboard: - - - Active Calls and Participants - - RTC Connection States - - Media Tracks (In/Out) - - CPU and Memory Usage - - Network Traffic - - Error Counts - -Custom Dashboard Panels -^^^^^^^^^^^^^^^^^^^^ - -Consider adding these custom panels to your dashboard: - -1. **Error Rate Panel**: - - PromQL query: - - .. code-block:: text - - sum(rate(rtcd_rtc_errors_total[5m])) by (type) - -2. **Connection Success Rate**: - - PromQL query: - - .. code-block:: text - - sum(rtcd_rtc_conn_states_total{state="connected"}) / (sum(rtcd_rtc_conn_states_total{state="connected"}) + sum(rtcd_rtc_conn_states_total{state="failed"})) - -3. **Media Track Count by Direction**: - - PromQL query: - - .. code-block:: text - - sum(rtcd_rtc_rtp_tracks_total) by (direction) - -Alerting Recommendations ---------------------- - -Setting up alerts helps you respond quickly to potential issues. Here are recommended alert thresholds: - -1. **High CPU Usage Alert**: - - PromQL query: - - .. code-block:: text - - rate(rtcd_process_cpu_seconds_total[5m]) > 0.8 - - This alerts when CPU usage exceeds 80% over 5 minutes. - -2. **Connection Failure Rate Alert**: - - PromQL query: - - .. code-block:: text - - sum(rate(rtcd_rtc_conn_states_total{state="failed"}[5m])) / sum(rate(rtcd_rtc_conn_states_total[5m])) > 0.1 - - This alerts when more than 10% of connection attempts fail over 5 minutes. - -3. **WebSocket Connection Drop Alert**: - - PromQL query: - - .. code-block:: text - - rate(rtcd_ws_connections_total{state="closed"}[5m]) > 5 - - This alerts when more than 5 WebSocket connections are dropping per minute. - -4. **Memory Leak Detection**: - - PromQL query: - - .. code-block:: text - - rate(rtcd_process_resident_memory_bytes[30m]) > 1024 * 1024 * 10 - - This alerts when memory usage is increasing by more than 10MB per 30 minutes. - Performance Baselines ------------------ -Understanding normal performance patterns helps identify anomalies. Here are baseline expectations based on call volume: +The following performance benchmarks provide baseline metrics for RTCD deployments under various load conditions and configurations. -Small Deployment (1-10 concurrent calls) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Deployment specifications** -- **CPU Usage**: 5-15% on a modern 4-core server -- **Memory Usage**: 200-500MB -- **Network**: 5-20 Mbps (depending on participant count and unmuted users) +- 1x r6i.large nginx proxy +- 3x c5.large MM app nodes (HA) +- 2x db.x2g.xlarge RDS Aurora MySQL v8 (one writer, one reader) +- 1x (c7i.xlarge, c7i.2xlarge, c7i.4xlarge) RTCD +- 2x c7i.2xlarge load-test agents -Medium Deployment (10-50 concurrent calls) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**App specifications** -- **CPU Usage**: 15-40% on a modern 8-core server -- **Memory Usage**: 500MB-1GB -- **Network**: 20-100 Mbps +- Mattermost v9.6 +- Mattermost Calls v0.28.0 +- RTCD v0.16.0 +- load-test agent v0.28.0 -Large Deployment (50+ concurrent calls) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Media specifications** -- **CPU Usage**: Consider multiple RTCD instances -- **Memory Usage**: 1-2GB per instance -- **Network**: 100Mbps-1Gbps (with horizontal scaling) +- Speech sample bitrate: 80Kbps +- Screen sharing sample bitrate: 1.6Mbps + +**Results** Below are the detailed benchmarks based on internal performance testing: @@ -313,15 +232,6 @@ Below are the detailed benchmarks based on internal performance testing: | 5 | 200 | 2 | yes | 90% | 0.7GB | 31Mbps / 2.2Gbps | c6i.2xlarge | +-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ -Metric Retention Recommendations -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -For historical analysis and trend identification: - -- **Short-term metrics**: Keep 15-second resolution data for 2 weeks -- **Medium-term metrics**: Keep 1-minute resolution data for 2 months -- **Long-term metrics**: Keep 5-minute resolution data for 1 year - Other Calls Documentation ---------------- From 42dfc7272c3654c7ec94553062f60654b75888da Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 6 Jun 2025 16:26:57 -0400 Subject: [PATCH 14/37] Fleshed out prometheus target setup docs --- source/configure/calls-metrics-monitoring.rst | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/source/configure/calls-metrics-monitoring.rst b/source/configure/calls-metrics-monitoring.rst index b64c04db395..35d2c80935a 100644 --- a/source/configure/calls-metrics-monitoring.rst +++ b/source/configure/calls-metrics-monitoring.rst @@ -47,27 +47,60 @@ Installing Prometheus Visit the [Prometheus download page](https://prometheus.io/download/) for installation instructions. -2. **Configure Prometheus** to scrape metrics from Mattermost and RTCD: +2. **Configure Prometheus** to scrape metrics from all Calls-related services: - Example ``prometheus.yml`` configuration: + Complete ``prometheus.yml`` configuration for Calls monitoring: .. code-block:: yaml + global: + scrape_interval: 15s + evaluation_interval: 15s + scrape_configs: - - job_name: 'mattermost-calls' - scrape_interval: 15s - metrics_path: '/plugins/com.mattermost.calls/metrics' + - job_name: 'prometheus' + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'mattermost' + metrics_path: /metrics static_configs: - - targets: ['mattermost-server:8065'] - + - targets: ['MATTERMOST_SERVER_IP:8067'] + + - job_name: 'calls' + metrics_path: /plugins/com.mattermost.calls/metrics + static_configs: + - targets: ['MATTERMOST_SERVER_IP:8067'] + - job_name: 'rtcd' - scrape_interval: 15s + metrics_path: /metrics static_configs: - - targets: ['rtcd-server:8045'] + - targets: ['RTCD_SERVER_IP:8045'] + + - job_name: 'node_exporter' + metrics_path: /metrics + static_configs: + - targets: ['RTCD_SERVER_IP:9100'] + + - job_name: 'calls-offloader' + metrics_path: /metrics + static_configs: + - targets: ['CALLS_OFFLOADER_SERVER_IP:4545'] + + Replace the placeholder IP addresses with your actual server addresses: + + - ``MATTERMOST_SERVER_IP``: IP address of your Mattermost server + - ``RTCD_SERVER_IP``: IP address of your RTCD server + - ``CALLS_OFFLOADER_SERVER_IP``: IP address of your calls-offloader server (if deployed) .. important:: **Metrics Configuration Notice**: The Calls dashboard expects targets to be in ``:`` format. Avoid using ``labels`` in your Prometheus configuration for Calls metrics, as this can cause compatibility issues with the dashboard. For example, use ``targets: ['rtcd-server:8045']`` instead of labels-based targeting. + .. note:: + - **node_exporter**: Optional but recommended for system-level metrics (CPU, memory, disk, network). See `node_exporter setup guide `__ for installation instructions. + - **calls-offloader**: Only needed if you have call recording/transcription enabled + - **Port 8067**: Default Mattermost metrics port (configurable in System Console) + Installing Grafana ^^^^^^^^^^^^^^^ From 06f3e2fd639a62ec6d36d96ceba408d6e0dec043 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 6 Jun 2025 16:27:26 -0400 Subject: [PATCH 15/37] Removed Claude halucinated detail --- source/configure/calls-metrics-monitoring.rst | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/source/configure/calls-metrics-monitoring.rst b/source/configure/calls-metrics-monitoring.rst index 35d2c80935a..a4aaec68634 100644 --- a/source/configure/calls-metrics-monitoring.rst +++ b/source/configure/calls-metrics-monitoring.rst @@ -58,10 +58,6 @@ Installing Prometheus evaluation_interval: 15s scrape_configs: - - job_name: 'prometheus' - static_configs: - - targets: ['localhost:9090'] - - job_name: 'mattermost' metrics_path: /metrics static_configs: @@ -115,19 +111,6 @@ Installing Grafana - Enter the URL of your Prometheus server - Test and save the configuration -Enabling Metrics in RTCD -^^^^^^^^^^^^^^^^^^^^^^ - -Add the following to your RTCD configuration file: - -.. code-block:: json - - { - "metrics": { - "enableProm": true, - "promPort": 9090 - } - } Key Metrics to Monitor -------------------- From 7e63612a3a146190317560ba156b82e64b48082d Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Wed, 11 Jun 2025 10:10:58 -0400 Subject: [PATCH 16/37] Docs refinements from further testing and experience --- source/configure/calls-deployment.rst | 62 ++++++++++++++-- source/configure/calls-metrics-monitoring.rst | 66 ++++++++++-------- source/configure/calls-troubleshooting.rst | 6 +- .../images/calls-channel-enable-disable.png | Bin 0 -> 194225 bytes 4 files changed, 96 insertions(+), 38 deletions(-) create mode 100644 source/images/calls-channel-enable-disable.png diff --git a/source/configure/calls-deployment.rst b/source/configure/calls-deployment.rst index 816f0f6e384..d7379ca87ce 100644 --- a/source/configure/calls-deployment.rst +++ b/source/configure/calls-deployment.rst @@ -77,9 +77,10 @@ For complete network requirements, see the `RTCD Setup and Configuration Plugin Management > Calls > Max call participants**. There's no limit to the total number of participants across all calls as the supported value greatly depends on instance resources. -- For more information on capacity planning, see the `Performance Considerations <#performance-considerations>`__ section below. +- All Mattermost customers can start, join, and participate in 1:1 audio calls with optional screen sharing. +- For group calls up to 50 concurrent users, Mattermost Enterprise, Professional, or Mattermost Cloud is required. +- Enterprise customers can also `record calls `__, enable `live text captions `__ during calls, and `transcribe recorded calls `__. We recommend that Enterprise self-hosted customers looking for group calls beyond 50 concurrent users consider using the `dedicated RTCD service <#when-to-use-rtcd>`__. +- For Mattermost self-hosted deployments, System admins need to enable and configure the plugin `using the System Console `__. The default maximum number of participants is unlimited; however, we recommend a maximum of 50 participants per call. Maximum call participants is configurable by going to **System Console > Plugin Management > Calls > Max call participants**. Call participant limits greatly depends on instance resources. For more details, refer to the `Performance Considerations <#performance-considerations>`__ section below. Configuration ------------- @@ -160,6 +161,15 @@ For call recording and transcription, you need to: 2. Configure the service URL in the System Console 3. Enable call recordings and/or transcriptions in the plugin settings +Air-Gapped Deployments +--------------------- + +Mattermost Calls can function in air-gapped environments. Exposing Calls to the public internet is only necessary when users need to connect from outside the local network, and no existing method supports that connection. In such setups: + +- Users should connect from within the private/local network. This can be done on-premises, through a VPN, or via virtual machines. +- Configuring a STUN server is unnecessary, as all connections occur within the local network. +- The ICE Host Override configuration setting can be optionally set with a local IP address (e.g., 192.168.1.45), depending on the specific network configuration and topology. + Performance Considerations ------------------------ @@ -181,14 +191,56 @@ Yes, using WebRTC security standards (DTLS/SRTP). Traffic is encrypted in transi Only a Mattermost STUN server (``stun.global.calls.mattermost.com``) is used by default. This can be removed if you set the ICE Host Override configuration. **Is using UDP a requirement?** -UDP is recommended for best performance, but TCP fallback is supported since plugin version 0.17 and RTCD version 0.11. +UDP is recommended protocol to serve real-time media as it allows for the lowest latency between peers, but TCP fallback is supported since plugin version 0.17 and RTCD version 0.11. + +If clients are unable to connect using UDP (due to limitations or strict firewalls), you have a few options: + +- Since plugin version 0.17 and `rtcd` version 0.11 the RTC service will listen for TCP connections in addition to UDP ones. If configured correctly (e.g. using commonly allowed ports such as 80 or 443) it's possible to have clients connect directly through TCP when unable to do it through the preferred UDP channel. + +- Run calls through an external TURN server that listens on TCP and relays all media traffic between peers. However, this is a sub-optimal solution that should be avoided if possible as it will introduce extra latency along with added infrastructural cost. **Do I need a TURN server?** -Only if clients are behind restrictive firewalls that block UDP. We recommend `coturn `__ if needed. +Only if clients are behind restrictive firewalls that block UDP. We recommend (and officially support) `coturn `__ if needed. **Can RTCD traffic be kept internal?** Yes, and it's recommended. Only the media ports need to be accessible to end-users. +**How will this work with an existing reverse proxy sitting in front of Mattermost?** + +Generally clients should connect directly to either Mattermost or, if deployed, the dedicated ``rtcd`` service through the configured UDP port. However, it's also possible to route the traffic through an existing load balancer as long as this has support for routing the UDP protocol (e.g. nginx). Of course this will require additional configuration and potential changes to how the plugin is run as it won't be possible to load balance the UDP flow across multiple instances like it happens for HTTP. + +**Do calls require a dedicated server to work or can they run alongside Mattermost?** + +The plugin can function in different modes. By default calls are handled completely by the plugin which runs as part of Mattermost. It's also possible to use a dedicated service to offload the computational and bandwidth costs and scale further (Enterprise only). + +See RTCD Setup and Configuration for more details on the dedicated RTCD service. + +**Can the traffic between Mattermost and ``rtcd`` be kept internal or should it be opened to the public?** + +When possible, it's recommended to keep communication between the Mattermost cluster and the dedicated ``rtcd`` service under the same private network as this can greatly simplify deployment and security. There's no requirement to expose ``rtcd``'s HTTP API to the public internet. + +**Can Calls be rolled out on a per-channel basis?** + +.. include:: ../_static/badges/selfhosted-only.rst + :start-after: :nosearch: + +Yes. Mattermost system admins running self-hosted deployments can enable or disable call functionality per channel. Once `test mode `__ is enabled for Mattermost Calls: + +1. **Navigate to the channel** where you want to enable or disable Calls +2. **Access the channel menu** by clicking the channel name at the top of the channel +3. **Select the Calls option** from the dropdown menu: + - Select **Enable calls** for each channel where you want Calls enabled + - Select **Disable calls** for all channels where you want Calls disabled + +.. image:: ../images/calls-channel-enable-disable.png + :alt: Channel menu showing Enable/Disable calls options + :width: 400px + +Once Calls is enabled for specific channels, users can start making calls in those channels. + +.. note:: + When `test mode `__ is disabled for Mattermost Calls, users in any Mattermost channel can make a call. + Troubleshooting --------------- diff --git a/source/configure/calls-metrics-monitoring.rst b/source/configure/calls-metrics-monitoring.rst index a4aaec68634..81d29f74e99 100644 --- a/source/configure/calls-metrics-monitoring.rst +++ b/source/configure/calls-metrics-monitoring.rst @@ -45,7 +45,7 @@ Installing Prometheus 1. **Download and install Prometheus**: - Visit the [Prometheus download page](https://prometheus.io/download/) for installation instructions. + Visit the `Prometheus download page `__ for installation instructions. 2. **Configure Prometheus** to scrape metrics from all Calls-related services: @@ -58,51 +58,66 @@ Installing Prometheus evaluation_interval: 15s scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['PROMETHEUS_IP:9090'] + - job_name: 'mattermost' metrics_path: /metrics static_configs: - targets: ['MATTERMOST_SERVER_IP:8067'] - - job_name: 'calls' + - job_name: 'calls-plugin' metrics_path: /plugins/com.mattermost.calls/metrics static_configs: - targets: ['MATTERMOST_SERVER_IP:8067'] + labels: + service_name: 'calls-plugin' - job_name: 'rtcd' metrics_path: /metrics static_configs: - targets: ['RTCD_SERVER_IP:8045'] - - - job_name: 'node_exporter' + labels: + service_name: 'rtcd' + + - job_name: 'rtcd-node-exporter' metrics_path: /metrics static_configs: - targets: ['RTCD_SERVER_IP:9100'] - - - job_name: 'calls-offloader' + labels: + service_name: 'rtcd' + + - job_name: 'calls_offloader-node-exporter' metrics_path: /metrics static_configs: - - targets: ['CALLS_OFFLOADER_SERVER_IP:4545'] + - targets: ['CALLS_OFFLOADER_SERVER_IP:9100'] + labels: + service_name: 'offloader' Replace the placeholder IP addresses with your actual server addresses: - ``MATTERMOST_SERVER_IP``: IP address of your Mattermost server - ``RTCD_SERVER_IP``: IP address of your RTCD server - ``CALLS_OFFLOADER_SERVER_IP``: IP address of your calls-offloader server (if deployed) + - ``PROMETHEUS_IP``: IP address of your Prometheus server + - **Note**: The configuration above uses the default ports (RTCD: ``8045``, Mattermost metrics: ``8067``, etc.). Adjust these ports in ``prometheus.yml`` if you have customized them. + .. important:: + **Metrics Path**: Ensure the metrics paths are correct. The RTCD service exposes metrics at ``/metrics`` by default, and the Calls plugin at ``/plugins/com.mattermost.calls/metrics``. .. important:: - **Metrics Configuration Notice**: The Calls dashboard expects targets to be in ``:`` format. Avoid using ``labels`` in your Prometheus configuration for Calls metrics, as this can cause compatibility issues with the dashboard. For example, use ``targets: ['rtcd-server:8045']`` instead of labels-based targeting. + **Metrics Configuration Notice**: Use the ``service_name`` labels as shown in the configuration above. These labels help organize metrics in dashboards and enable proper service identification. .. note:: - **node_exporter**: Optional but recommended for system-level metrics (CPU, memory, disk, network). See `node_exporter setup guide `__ for installation instructions. - **calls-offloader**: Only needed if you have call recording/transcription enabled - - **Port 8067**: Default Mattermost metrics port (configurable in System Console) Installing Grafana ^^^^^^^^^^^^^^^ 1. **Download and install Grafana**: - Visit the [Grafana download page](https://grafana.com/grafana/download) for installation instructions. + Visit the `Grafana download page `__ for installation instructions. 2. **Configure Grafana** to use Prometheus as a data source: @@ -111,6 +126,17 @@ Installing Grafana - Enter the URL of your Prometheus server - Test and save the configuration +3. **Import the Mattermost Calls dashboard**: + + - Navigate to Dashboards > Import in Grafana + - Enter dashboard ID: ``23225`` or use the direct link: `Mattermost Calls Performance Monitoring `__ + - Select your Prometheus data source, and enter values for the + - Confirm the port used for RTCD metrics (default is ``8045``), and the port used for the Calls plugin metrics (default is ``8067``) + - Click Import to add the dashboard to your Grafana instance + + .. note:: + The dashboard is also available as JSON source from the `Mattermost performance assets repository `__ for manual import or customization. + Key Metrics to Monitor -------------------- @@ -175,26 +201,6 @@ Similar metrics are available for the Calls plugin with the following prefixes: - WebSocket metrics: ``mattermost_plugin_calls_websocket_*`` - Store metrics: ``mattermost_plugin_calls_store_ops_total`` -Grafana Dashboards ----------------- - -Official Dashboard -^^^^^^^^^^^^^^^^ - -Mattermost provides an official Grafana dashboard for monitoring Calls performance: - -1. **Download the dashboard JSON**: - - Get it from [GitHub](https://github.com/mattermost/mattermost-performance-assets/blob/master/grafana/mattermost-calls-performance-monitoring.json) - -2. **Import the dashboard** into Grafana: - - - Navigate to Dashboards > Import - - Upload the JSON file or paste its contents - - Select your Prometheus data source - - Confirm the correct rtcd (`5045`) and calls plugin (`8065`) targets are set - - Click Import - Performance Baselines ------------------ diff --git a/source/configure/calls-troubleshooting.rst b/source/configure/calls-troubleshooting.rst index 238ca1d8ca7..f54dfff24d0 100644 --- a/source/configure/calls-troubleshooting.rst +++ b/source/configure/calls-troubleshooting.rst @@ -344,7 +344,7 @@ Prometheus Metrics Analysis Use Prometheus metrics for real-time and historical performance data: -Import the official [Mattermost Calls dashboard](https://github.com/mattermost/mattermost-performance-assets/blob/master/grafana/mattermost-calls-performance-monitoring.json) into Grafana for visualization. +Import the official `Mattermost Calls dashboard `__ into Grafana for visualization. Advanced Diagnostics ----------------- @@ -389,11 +389,11 @@ Tools to help diagnose client-side issues: 1. **WebRTC Troubleshooter**: - Direct users to [WebRTC Troubleshooter](https://test.webrtc.org/) for browser capability testing. + Direct users to `WebRTC Troubleshooter `__ for browser capability testing. 2. **Network Quality Tests**: - Use [Speedtest](https://www.speedtest.net/) or similar to check internet connection quality. + Use `Speedtest `__ or similar to check internet connection quality. 3. **Browser-Specific WebRTC Info**: diff --git a/source/images/calls-channel-enable-disable.png b/source/images/calls-channel-enable-disable.png new file mode 100644 index 0000000000000000000000000000000000000000..a2751779dadbd39f0b8f5802fdc4815449b9d40e GIT binary patch literal 194225 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

E9?VoxA8jFbd6Zw;V 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

5STlcc`wz|yWu*?``gO2-&NY6 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%0Dk>eF7hbFi+nd!*qdxcREpPd0Bb8ao( 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 G{Qn;wuFRwW literal 0 HcmV?d00001 From 15f73b1393bd6de1e0e7f6111d9831f54b1449dc Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Wed, 11 Jun 2025 11:02:23 -0400 Subject: [PATCH 17/37] Transitioned all Calls docs to Markdown Attempted to merge some edits that happened in the time between I switched to RST and back to MD. Need to double check those changes make it to the final merge. --- source/configure/calls-deployment.md | 514 +++++-------------- source/configure/calls-kubernetes.md | 163 ++++++ source/configure/calls-metrics-monitoring.md | 245 +++++++++ source/configure/calls-offloader-setup.md | 368 +++++++++++++ source/configure/calls-rtcd-setup.md | 378 ++++++++++++++ source/configure/calls-troubleshooting.md | 406 +++++++++++++++ 6 files changed, 1676 insertions(+), 398 deletions(-) create mode 100644 source/configure/calls-kubernetes.md create mode 100644 source/configure/calls-metrics-monitoring.md create mode 100644 source/configure/calls-offloader-setup.md create mode 100644 source/configure/calls-rtcd-setup.md create mode 100644 source/configure/calls-troubleshooting.md diff --git a/source/configure/calls-deployment.md b/source/configure/calls-deployment.md index 6ec820a9c7d..bd54c0911e7 100644 --- a/source/configure/calls-deployment.md +++ b/source/configure/calls-deployment.md @@ -1,30 +1,56 @@ -# Calls self-hosted deployment +# Calls Deployment Overview -```{include} ../_static/badges/allplans-selfhosted.md + +```{include} ../_static/badges/allplans-cloud-selfhosted.md ``` -Mattermost Calls is an excellent option for organizations demanding enhanced security and control over their communication infrastructure. Calls is designed to operate securely in self-hosted deployments, including [air-gapped environments](https://docs.mattermost.com/configure/calls-deployment.html#air-gapped-deployments), ensuring private communication without reliance on public internet connectivity with flexible configuration options for complex network requirements. +This document provides an overview of Mattermost Calls deployment options for self-hosted environments, including [air-gapped environments](https://docs.mattermost.com/configure/calls-deployment.html#air-gapped-deployments), ensuring private communication without reliance on public internet connectivity with flexible configuration options for complex network requirements. -This document provides information on how to successfully make the Calls plugin work on self-hosted deployments. It also outlines some of the most common deployment strategies with example diagrams, and provides the deployment guidelines for the recording, transcription, and live captions service. +```{toctree} +:maxdepth: 1 +:hidden: -## Terminology +calls-rtcd-setup.md +calls-offloader-setup.md +calls-metrics-monitoring.md +calls-kubernetes.md +calls-troubleshooting.md +``` + +## Quick Links + +For detailed information on specific topics, please refer to these specialized guides: -- [WebRTC](https://bloggeek.me/webrtcglossary/webrtc-2/): The set of underlying protocols/specifications on top of which calls are implemented. -- **RTC (Real Time Connection)**: The real-time connection. This is the channel used to send media tracks (audio/video/screen). -- **WS (WebSocket)**: The WebSocket connection. This is the channel used to set up a connection (signaling process). -- [NAT (Network Address Translation)](https://bloggeek.me/webrtcglossary/nat/): A networking technique to map IP addresses. -- [STUN (Session Traversal Utilities for NAT)](https://bloggeek.me/webrtcglossary/stun/): A protocol/service used by WebRTC clients to help traversing NATs. On the server side it's mainly used to figure out the public IP of the instance. -- [TURN (Traversal Using Relays around NAT)](https://bloggeek.me/webrtcglossary/turn/): A protocol/service used to help WebRTC clients behind strict firewalls connect to a call through media relay. +- [RTCD Setup and Configuration](calls-rtcd-setup.html): Comprehensive guide for setting up the dedicated RTCD service +- [Calls Offloader Setup and Configuration](calls-offloader-setup.html): Comprehensive guide for setting up the calls-offloader service for recording and transcription +- [Calls Troubleshooting](calls-troubleshooting.html): Detailed troubleshooting steps and debugging techniques +- [Calls Metrics and Monitoring](calls-metrics-monitoring.html): Guide to monitoring Calls performance using metrics and observability +- [Calls Deployment on Kubernetes](calls-kubernetes.html): Detailed guide for deploying Calls in Kubernetes environments -## Plugin components +## About Mattermost Calls -- **Calls plugin**: This is the main entry point and a requirement to enable channel calls. +Mattermost Calls provides integrated audio calling and screen sharing capabilities within Mattermost channels. It's built on WebRTC technology and can be deployed either: -- **rtcd**: This is an optional service that can be deployed to offload all the functionality and data processing involved with the WebRTC connections. Read more about when and why to use [rtcd](#the-rtcd-service) below. +1. **Integrated mode**: Built into the Calls plugin (simpler, suitable for smaller deployments) +2. **RTCD mode**: Using a dedicated service for improved performance and scalability (recommended for production environments) -## Requirements +## Terminology + +- [WebRTC](https://bloggeek.me/webrtcglossary/webrtc-2/): The set of protocols on which calls are built +- **RTC**: Real-Time Connection channel used for media (audio/video/screen) +- **WS**: WebSocket connection used for signaling and connection setup +- **SFU**: Selective Forwarding Unit, routes media between participants +- [NAT](https://bloggeek.me/webrtcglossary/nat/): Network Address Translation for mapping IP addresses +- [STUN](https://bloggeek.me/webrtcglossary/stun/): Protocol used by WebRTC clients to help traverse NATs +- [TURN](https://bloggeek.me/webrtcglossary/turn/): Protocol to relay media for clients behind strict firewalls + +## Key Components -### Server +- **Calls plugin**: The main plugin that enables calls functionality. Installed by default in Mattermost self-hosted deployments. +- **RTCD service**: Optional dedicated service for offloading media processing (Enterprise feature). Typically deployed to dedicated servers or containers. See [RTCD Setup and Configuration](calls-rtcd-setup.html) for details. +- **calls-offloader**: Service for call recording and transcription (if enabled). Typically deployed to dedicated servers. See [Calls Offloader Setup and Configuration](calls-offloader-setup.html) for setup and troubleshooting details. + +## Network Requirements - Run Mattermost server on a secure (HTTPs) connection. This is a necessary requirement on the client to allow capturing devices (e.g., microphone, screen). See the [config TLS](https://docs.mattermost.com/deploy/server/setup-tls.html) section for more info. - See [network requirements](#network) below. @@ -119,6 +145,8 @@ This document provides information on how to successfully make the Calls plugin +For complete network requirements, see the [RTCD Setup and Configuration](calls-rtcd-setup.html) guide. + #### Air-gapped deployments Mattermost Calls can function in air-gapped environments. Exposing Calls to the public internet is only necessary when users need to connect from outside the local network, and no existing method supports that connection. In such setups: @@ -131,81 +159,72 @@ Mattermost Calls can function in air-gapped environments. Exposing Calls to the - All Mattermost customers can start, join, and participate in 1:1 audio calls with optional screen sharing. - For group calls up to 50 concurrent users, Mattermost Enterprise, Professional, or Mattermost Cloud is required. -- Enterprise customers can also [record calls](https://docs.mattermost.com/collaborate/make-calls.html#record-a-call), enable [live text captions](https://docs.mattermost.com/collaborate/make-calls.html#live-captions-during-calls) during calls, and [transcribe recorded calls](https://docs.mattermost.com/collaborate/make-calls.html#transcribe-recorded-calls). We recommend that Enterprise self-hosted customers looking for group calls beyond 50 concurrent users consider using the [dedicated rtcd service](#the-rtcd-service). -- For Mattermost self-hosted deployments, System admins need to enable and configure the plugin [using the System Console](https://docs.mattermost.com/configure/plugins-configuration-settings.html#calls). The default maximum number of participants is unlimited; however, we recommend a maximum of 50 participants per call. Maximum call participants is configurable by going to **System Console > Plugin Management > Calls > Max call participants**. Call participant limits greatly depends on instance resources. For more details, refer to the [performance section](#performance) below. +- Enterprise customers can also [record calls](https://docs.mattermost.com/collaborate/make-calls.html#record-a-call), enable [live text captions](https://docs.mattermost.com/collaborate/make-calls.html#live-captions-during-calls) during calls, and [transcribe recorded calls](https://docs.mattermost.com/collaborate/make-calls.html#transcribe-recorded-calls). We recommend that Enterprise self-hosted customers looking for group calls beyond 50 concurrent users consider using the [dedicated RTCD service](#when-to-use-rtcd). +- For Mattermost self-hosted deployments, System admins need to enable and configure the plugin [using the System Console](https://docs.mattermost.com/configure/plugins-configuration-settings.html#calls). The default maximum number of participants is unlimited; however, we recommend a maximum of 50 participants per call. Maximum call participants is configurable by going to **System Console > Plugin Management > Calls > Max call participants**. Call participant limits greatly depends on instance resources. For more details, refer to the [Performance Considerations](#performance-considerations) section below. ## Configuration -For Mattermost self-hosted customers, the calls plugin is pre-packaged, installed, and enabled. Configuration to allow end-users to use it can be found in the [System Console](https://docs.mattermost.com/configure/plugins-configuration-settings.html#calls). +For Mattermost self-hosted customers, the calls plugin is pre-packaged, installed, and enabled. Configuration to allow end-users to use it can be found in the [System Console](/configure/plugins-configuration-settings.html#calls). -## Modes of operation +## Deployment Architecture Options -Depending on how the Mattermost server is running, there are several modes under which the Calls plugin can operate. Please refer to the section below on [the rtcd service](#the-rtcd-service) to learn about the `rtcd` and the Selective Forwarding Unit (SFU). +Mattermost Calls can be deployed in several configurations: -| Mattermost deployment | SFU | SFU deployment | -|-----------------------|-----|----------------| -| Single instance | integrated | | -| Single instance | rtcd | | -| High availability cluster-based | integrated | clustered | -| High availability cluster-based | rtcd | | +### Single Instance Deployments -### Single instance +#### Integrated Mode -#### Integrated +The WebRTC service runs within the Calls plugin on the Mattermost server. This is the default mode when first installing the plugin on a single Mattermost instance setup. The WebRTC service is integrated in the plugin itself and runs alongside the Mattermost server. -This is the default mode when first installing the plugin on a single Mattermost instance setup. The WebRTC service is integrated in the plugin itself and runs alongside the Mattermost server. +![Integrated configuration model of a single instance](../images/calls-deployment-image3.png) -![A diagram of the integrated configuration model of a single instance.](/images/calls-deployment-image3.png) +#### RTCD Mode -#### rtcd +A dedicated RTCD service handles media routing, reducing load on the Mattermost server. -An external, dedicated and scalable WebRTC service (`rtcd`) is used to handle all calls media routing. +![Web RTC deployment configuration](../images/calls-deployment-image7.png) -![A diagram of a Web RTC deployment configuration.](/images/calls-deployment-image7.png) +### High Availability Deployments -### High availability cluster-based - -#### Clustered +#### Clustered Mode This is the default mode when running the plugin in a high availability cluster-based deployment. Every Mattermost node will run an instance of the plugin that includes a WebRTC service. Calls are distributed across all available nodes through the existing load-balancer: a call is hosted on the instance where the initiating websocket connection (first client to join) is made. A single call will be hosted on a single cluster node. -![A diagram of a clustered calls deployment.](/images/calls-deployment-image5.png) +![Clustered calls deployment](../images/calls-deployment-image5.png) -#### rtcd (High Availability) +#### RTCD with High Availability -![A diagram of an rtcd deployment.](/images/calls-deployment-image2.png) +Dedicated RTCD services handle media routing for high availability. -## Performance +![RTCD deployment with high availability](../images/calls-deployment-image2.png) -Calls performance primarily depends on two resources: CPU and bandwidth (both network latency and overall throughput). The final consumption exhibits quadratic growth with the number of clients transmitting and receiving media. +### Kubernetes Deployments -As an example, a single call with 10 participants of which two are unmuted (transmitting voice data) will generally consume double the resources than the same call with a single participant unmuted. What ultimately counts towards performance is the overall number of concurrent media flows (in/out) across the server. +RTCD is the only officially supported approach for Kubernetes deployments. For detailed information on deploying Mattermost Calls in Kubernetes environments, including Helm chart configurations, resource requirements, and scaling considerations, see the [Calls Deployment on Kubernetes](calls-kubernetes.html) guide. -### Benchmarks +## When to Use RTCD -Here are the results from internally conducted performance and ceiling tests on a dedicated `rtcd` instance: +The dedicated RTCD service (available with Enterprise license) is recommended for: -#### Deployment specifications +- **Production environments**: Isolates call traffic from other Mattermost services +- **Performance optimization**: Dedicated service tuned for real-time media +- **Scalability**: Add RTCD instances as call volume grows +- **Call stability**: Calls continue even if Mattermost server needs to restart +- **Kubernetes deployments**: Required for officially supported Kubernetes deployments -- 1x r6i.large nginx proxy -- 3x c5.large MM app nodes (HA) -- 2x db.x2g.xlarge RDS Aurora MySQL v8 (one writer, one reader) -- 1x (c7i.xlarge, c7i.2xlarge, c7i.4xlarge) RTCD -- 2x c7i.2xlarge load-test agents +For detailed RTCD setup instructions, see the [RTCD Setup and Configuration](calls-rtcd-setup.html) guide. -#### App specifications +## Call Recording and Transcription -- Mattermost v9.6 -- Mattermost Calls v0.28.0 -- RTCD v0.16.0 -- load-test agent v0.28.0 +For call recording and transcription, you need to: -#### Media specifications +1. Deploy the `calls-offloader` service +2. Configure the service URL in the System Console +3. Enable call recordings and/or transcriptions in the plugin settings -- Speech sample bitrate: 80Kbps -- Screen sharing sample bitrate: 1.6Mbps +## Air-Gapped Deployments -#### Results +Mattermost Calls can function in air-gapped environments. Exposing Calls to the public internet is only necessary when users need to connect from outside the local network, and no existing method supports that connection. In such setups: @@ -414,329 +433,65 @@ Here are the results from internally conducted performance and ceiling tests on
-```{note} -- The tests focused on a single, vertically scaled RTCD instance to understand the processing limits within a single node. Scaling the RTCD service horizontally should be sufficient to support a higher number of calls. -- RTCD processes were executed with all performance profiling enabled (including block and mutex). This resulted in some computational overhead. -- Both speech and screen samples have slightly higher bitrates than the average produced by a real client (e.g., a browser). This gives us some safety margin over real-world deployments. -``` - -### Dedicated service - -For Enterprise customers we offer a way to offload performance costs through a [dedicated service](https://github.com/mattermost/rtcd) that can be used to further scale up calls. - -### Load testing - -We provide a [load-test tool](https://github.com/mattermost/mattermost-plugin-calls/tree/main/lt) that can be used to simulate and measure the performance impact of calls. - -### Monitoring - -Both the plugin and the external `rtcd` service expose some Prometheus metrics to monitor performance. We provide an [official dashboard](https://grafana.com/grafana/dashboards/23225-mattermost-calls-performance-monitoring/) that can be imported in Grafana. You can refer to [Performance monitoring](https://docs.mattermost.com/scale/deploy-prometheus-grafana-for-performance-monitoring.html) for more information on how to set up Prometheus and visualize metrics through Grafana. - -#### Calls plugin metrics - -Metrics for the calls plugin are exposed through the `/plugins/com.mattermost.calls/metrics` subpath under the existing Mattermost server metrics endpoint. This is controlled by the [Listen address for performance](https://docs.mattermost.com/configure/environment-configuration-settings.html#listen-address-for-performance) configuration setting. It defaults to port `8067`. - -```{note} -- The [Metrics plugin](https://docs.mattermost.com/scale/collect-performance-metrics.html) collects application-level metrics only and does not make system or OS-level calls. As a result, data typically derived from system-level metrics may be missing in the Grafana panel. -- On Mattermost versions prior to v9.5, plugin metrics were exposed through the public `/plugins/com.mattermost.calls/metrics` API endpoint controlled by the [Web server listen address](https://docs.mattermost.com/configure/environment-configuration-settings.html#web-server-listen-address) configuration setting. This defaults to port `8065`. -``` - -**Process** - -- `mattermost_plugin_calls_process_cpu_seconds_total`: Total user and system CPU time spent in seconds. -- `mattermost_plugin_calls_process_max_fds`: Maximum number of open file descriptors. -- `mattermost_plugin_calls_process_open_fds`: Number of open file descriptors. -- `mattermost_plugin_calls_process_resident_memory_bytes`: Resident memory size in bytes. -- `mattermost_plugin_calls_process_virtual_memory_bytes`: Virtual memory size in bytes. - -**WebRTC connection** - -- `mattermost_plugin_calls_rtc_conn_states_total`: Total number of RTC connection state changes. -- `mattermost_plugin_calls_rtc_errors_total`: Total number of RTC errors. -- `mattermost_plugin_calls_rtc_rtp_bytes_total`: Total number of sent/received RTP packets in bytes. - - - Note: removed as of v0.16.0 - -- `mattermost_plugin_calls_rtc_rtp_packets_total`: Total number of sent/received RTP packets. - - - Note: removed as of v0.16.0 - -- `mattermost_plugin_calls_rtc_rtp_tracks_total`: Total number of incoming/outgoing RTP tracks. - - - Note: added as of v0.16.0 - -- `mattermost_plugin_calls_rtc_sessions_total`: Total number of active RTC sessions. - -**Application** - -- `mattermost_plugin_calls_app_handlers_time_bucket`: Time taken to execute app handlers. - - - `mattermost_plugin_calls_app_handlers_time_sum` - - - `mattermost_plugin_calls_app_handlers_time_count` - -**Database** +Calls performance primarily depends on: -- `mattermost_plugin_calls_store_ops_total`: Total number of db store operations. -- `mattermost_plugin_calls_store_methods_time_bucket`: Time taken to execute store methods. +- **CPU resources**: More participants require more processing power +- **Network bandwidth**: Both incoming and outgoing traffic increases with participant count +- **Active speakers**: Unmuted participants require significantly more resources - - `mattermost_plugin_calls_store_methods_time_sum` +For detailed performance metrics, benchmarks, and monitoring guidance, see the [Calls Metrics and Monitoring](calls-metrics-monitoring.html) guide. - - `mattermost_plugin_calls_store_methods_time_count` -- `mattermost_plugin_calls_cluster_mutex_grab_time_bucket`: Time taken to grab global mutexes. +## Frequently Asked Questions - - `mattermost_plugin_calls_cluster_mutex_grab_time_sum` - - - `mattermost_plugin_calls_cluster_mutex_grab_time_count` -- `mattermost_plugin_calls_cluster_mutex_locked_time_bucket`: Time spent locked in global mutexes. - - - `mattermost_plugin_calls_cluster_mutex_locked_time_sum` - - - `mattermost_plugin_calls_cluster_mutex_locked_time_count` - -**WebSocket** - -- `mattermost_plugin_calls_websocket_connections_total`: Total number of active WebSocket connections. -- `mattermost_plugin_calls_websocket_events_total`: Total number of WebSocket events. - -**Jobs** - -- `mattermost_plugin_calls_jobs_live_captions_new_audio_len_ms_bucket`: Duration (in ms) of new audio transcribed for live captions. - - - `mattermost_plugin_calls_jobs_live_captions_new_audio_len_ms_sum` - - - `mattermost_plugin_calls_jobs_live_captions_new_audio_len_ms_count` -- `mattermost_plugin_calls_jobs_live_captions_pktPayloadCh_buf_full`: Total packets of audio data dropped due to full channel. -- `mattermost_plugin_calls_jobs_live_captions_window_dropped`: Total windows of audio data dropped due to pressure on the transcriber. - -#### WebRTC service metrics - -Metrics for the `rtcd` service are exposed through the `/metrics` API endpoint under the `rtcd` API listener controlled by the `api.http.listen_address` configuration setting. It defaults to port `8045`. - -**Process** - -- `rtcd_process_cpu_seconds_total`: Total user and system CPU time spent in seconds. -- `rtcd_plugin_calls_process_max_fds`: Maximum number of open file descriptors. -- `rtcd_plugin_calls_process_open_fds`: Number of open file descriptors. -- `rtcd_plugin_calls_process_resident_memory_bytes`: Resident memory size in bytes. -- `rtcd_plugin_calls_process_virtual_memory_bytes`: Virtual memory size in bytes. - -**WebRTC Connection** - -- `rtcd_rtc_conn_states_total`: Total number of RTC connection state changes. -- `rtcd_rtc_errors_total`: Total number of RTC errors. -- `rtcd_rtc_rtp_bytes_total`: Total number of sent/received RTP packets in bytes. -- `rtcd_rtc_rtp_packets_total`: Total number of sent/received RTP packets. -- `rtcd_rtc_rtp_tracks_total`: Total number of incoming/outgoing RTP tracks. -- `rtcd_rtc_sessions_total`: Total number of active RTC sessions. -- `rtcd_rtc_rtp_tracks_writes_time_bucket`: Time taken to write to outgoing RTP tracks. - - - `rtcd_rtc_rtp_tracks_writes_time_sum` - - - `rtcd_rtc_rtp_tracks_writes_time_count` - -**WebSocket** - -- `rtcd_ws_connections_total`: Total number of active WebSocket connections. -- `rtcd_ws_messages_total`: Total number of received/sent WebSocket messages. - -#### Configuration - -A sample Prometheus configuration to scrape both plugin and `rtcd` metrics could look like this: - -``` -scrape_configs: -- job_name: node - static_configs: - - targets: ['rtcd-0:9100','rtcd-1:9100', 'calls-offloader-1:9100', 'calls-offloader-2:9100'] -- job_name: calls - metrics_path: /plugins/com.mattermost.calls/metrics - static_configs: - - targets: ['app-0:8067','app-1:8067','app-2:8067'] -- job_name: rtcd - static_configs: - - targets: ['rtcd-0:8045', 'rtcd-1:8045'] -``` - -### System tunings - -If you want to host many calls or calls with a large number of participants, take a look at the following platform specific (Linux) tunings (this is the only officially supported target for the plugin right now): - -``` -# Setting the maximum buffer size of the receiving UDP buffer to 16MB -net.core.rmem_max = 16777216 - -# Setting the maximum buffer size of the sending UDP buffer to 16MB -net.core.wmem_max = 16777216 - -# Allow to allocate more memory as needed for more control messages that need to be sent for each socket connected -net.core.optmem_max = 16777216 -``` - -## The rtcd service - -```{include} ./calls-rtcd-ent-only.md -``` - -The Calls plugin has a built-in [Selective Forwarding Unit (SFU)](https://bloggeek.me/webrtcglossary/sfu/) to route audio and screensharing data. This is the `integrated` option described in the [Modes of operation](#modes-of-operation) section above. But this SFU functionality can be deployed separately as an external `rtcd` instance. - -### Reasons to use the `rtcd` service - -This section will help you understand when and why your organization would want to use `rtcd`. - -```{note} -`rtcd` is a standalone service, which adds operational complexity, maintenance costs, and requires an enterprise licence. For those who are evaluating Calls, and for many small instances of Mattermost, the integrated SFU (the one included in the Calls plugin) may be sufficient initially. -``` +**Is calls traffic encrypted?** +Yes, using WebRTC security standards (DTLS/SRTP). Traffic is encrypted in transit. -The `rtcd` service is the recommended way to host Calls for the following reasons: +**Are there any third-party services involved?** +Only a Mattermost STUN server (`stun.global.calls.mattermost.com`) is used by default. This can be removed if you set the ICE Host Override configuration. -- **Performance of the main Mattermost server(s).** When the Calls plugin runs the SFU, calls traffic is added to the processing load of the server running the rest of your Mattermost services. If Calls traffic spikes, it can negatively affect the responsiveness of these services. Using an rtcd service isolates the calls traffic processing to those rtcd instances, and also reduces costs by minimizing CPU usage spikes. +**Is using UDP a requirement?** +UDP is recommended protocol to serve real-time media as it allows for the lowest latency between peers, but TCP fallback is supported since plugin version 0.17 and RTCD version 0.11. -- **Performance, scalability, and stability of the Calls product.** If Calls traffic spikes, or more overall capacity is needed, `rtcd` servers can be added to balance the load. As an added benefit, if the Mattermost traffic spikes, or if a Mattermost instance needs to be restarted, those people in a current call will not be affected - current calls won't be dropped. - -Some caveats apply here. Web socket events (for example: emoji reactions, hand raising, muting/unmuting) will not be transmitted while the main Mattermost server is down. But the call itself will continue while the main server restarts. - -- **Kubernetes deployments.** In a Kubernetes deployment, `rtcd` is strongly recommended; it is currently the only officially supported way to run Calls. -- **Technical benefits.** The dedicated `rtcd` service has been optimized and tuned at the system/network level for real-time audio/video traffic, where latency is generally more important than throughput. - -In general, `rtcd` is the preferred solution for a performant and scalable deployment. With `rtcd`, the Mattermost server will be minimally impacted when hosting a high number of calls. - -See the [Mattermost rtcd repository documentation](https://github.com/mattermost/rtcd/blob/master/README.md) on GitHub for details on [how to run calls through the service](https://github.com/mattermost/rtcd/blob/master/docs/getting_started.md), as well as: - -- [Key implementation details](https://github.com/mattermost/rtcd/blob/master/docs/implementation.md) -- [Project structure](https://github.com/mattermost/rtcd/blob/master/docs/project_structure.md) -- [Configuration overrides](https://github.com/mattermost/rtcd/blob/master/docs/env_config.md) -- [Authentication flow](https://github.com/mattermost/rtcd/blob/master/docs/security.md) - -### Horizontal scalability - -The supported way to enable horizontal scalability for Calls is through a form of DNS based load balancing. This can be achieved regardless of how the `rtcd` service is deployed (bare bone instance, Kubernetes, or an alternate way). - -In order for this to work, the [RTCD Service URL](https://docs.mattermost.com/configure/plugins-configuration-settings.html#rtcd-service-url) should point to a hostname that resolves to multiple IP addresses, each pointing to a running `rtcd` instance. The Mattermost Calls plugin will then automatically distribute calls amongst the available hosts. - -The expected requirements are the following: - -- When a new `rtcd` instance is deployed, it should be added to the DNS record. The plugin side will then be able to pick it up and start assigning calls to the new host. -- If a `rtcd` instance goes down, it should be removed from the DNS record. The plugin side can then detect the change and stop assigning new calls to that host. - -```{note} -- Load balancing is done at the call level. This means that a single call will always live on a single `rtcd` instance. -- There's currently no support for spreading sessions belonging to the same call across a fleet of instances. -``` - -## Configure recording, transcriptions, and live captions - -Before you can start recording, transcribing, and live captioning calls, you need to configure the `calls-offloader` job service. See the [calls-offloader](https://github.com/mattermost/calls-offloader/blob/master/docs/getting_started.md) documentation on GitHub for details on deploying and running this service. [Performance and scalability recommendations](https://github.com/mattermost/calls-offloader/blob/master/docs/performance.md) related to this service are also available on GitHub. - -```{note} -If deploying the service in a Kubernetes cluster, refer to the later section on [Helm charts](#helm-charts). -``` - -Once the `calls-offloader` service is running, recordings should be explicitly enabled through the [Enable call recordings](https://docs.mattermost.com/configure/plugins-configuration-settings.html#enable-call-recordings) config setting and the service's URL should be configured using [Job service URL](https://docs.mattermost.com/configure/plugins-configuration-settings.html#job-service-url). - -Call transcriptions can be enabled through the [Enable call transcriptions](https://docs.mattermost.com/configure/plugins-configuration-settings.html#enable-call-transcriptions) configuration setting. - -Live captions can be enabled through the [Enable live captions](https://docs.mattermost.com/configure/plugins-configuration-settings.html#enable-live-captions) configuration setting. - -```{note} -- The call transcriptions functionality is available starting in Calls version v0.22.0. -- The live captions functionality is available starting in Calls version v0.26.2. -``` - -## Kubernetes deployments - -The Calls plugin has been designed to integrate well with Kubernetes to offer improved scalability and control over the deployment. - -This is a sample diagram showing how the `rtcd` standalone service can be deployed in a Kubernetes cluster: - -![A diagram of calls deployed in a Kubernetes cluster.](/images/calls-deployment-kubernetes.png) - -If Mattermost isn't deployed in a Kubernetes cluster, and you want to use this deployment type, see the [Deploy Mattermost on Kubernetes](https://docs.mattermost.com/install/install-kubernetes.html) documentation. - -### Helm Charts - -The recommended way to deploy Calls related components and services in a Kubernetes deployment is to use the officially provided Helm charts. Related documentation including detailed information on how to deploy these services can be found in our `mattermost-helm` repository: - -- [rtcd Helm chart](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-rtcd) - -- [calls-offloader Helm chart](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-calls-offloader) - -### Limitations - -Due to the inherent complexities of hosting a WebRTC service, some limitations apply when deploying Calls in a Kubernetes environment. - -One key requirement is that each `rtcd` process live in a dedicated Kubernetes node. This is necessary to forward the data correctly while allowing for horizontal scaling. Data should generally not go through a standard ingress but directly to the pod running the `rtcd` process. - -The general recommendation is to expose one external IP address per `rtcd` instance (Kubernetes node). This makes it simpler to scale as the application is able to detect its own external address (through STUN) and advertise it to clients to achieve connectivity with minimal configuration. - -If, for some reason, exposing multiple IP addresses is not possible in your environment, port mapping (NAT) can be used. In this scenario different ports are used to map the respective `rtcd` nodes behind the single external IP. Example: - -```sh -EXT_IP:8443 -> rtcdA:8443 -EXT_IP:8444 -> rtcdB:8443 -EXT_IP:8445 -> rtcdC:8443 -``` - -This case requires a couple of extra configurations: - -- NAT mappings need to be in place for every `rtcd` node. This is usually done at the ingress point (e.g., ELB, NLB, etc). -- The `RTCD_RTC_ICEHOSTPORTOVERRIDE` config should be used to pass a full mapping of node IPs and their respective port. - - Example: `RTCD_RTC_ICEHOSTPORTOVERRIDE=rtcdA_IP/8443,rtcdB_IP/8444,rtcdC_IP/8445` -- The `RTCD_RTC_ICEHOSTOVERRIDE` should be used to set the external IP address. - -```{note} -One option to limit these static mappings is to reduce the size of the local subnet (e.g., to `/29`). -``` - -## Frequently asked questions - -### Is there encryption? - -Media (audio/video) is encrypted using security standards as part of WebRTC. It's mainly a combination of DTLS and SRTP. It's not e2e encrypted in the sense that in the current design all media needs to go through Mattermost which acts as a media router and has complete access to it. Media is then encrypted back to the clients so it's secured during transit. In short: only the participant clients and the Mattermost server have access to unencrypted call data. - -### Are there any third-party services involved? - -The only external service used is a Mattermost official STUN server (`stun.global.calls.mattermost.com`) which is configured as default. This is primarily used to find the public address of the Mattermost instance if none is provided through the [ICE Host Override](https://docs.mattermost.com/configure/plugins-configuration-settings.html#ice-host-override) option. The only information sent to this service is the IP addresses of clients connecting as no other traffic goes through it. It can be removed in cases where the [ICE Host Override](https://docs.mattermost.com/configure/plugins-configuration-settings.html#ice-host-override) setting is provided. - -```{note} -In air-gapped deployments, using STUN servers is not necessary since all connections remain within the local network. -``` - -### Is using UDP a requirement? - -Yes, UDP is the recommended protocol to serve real-time media as it allows for the lowest latency between peers. However, there are a couple of possible solutions to cover clients that due to limitations or strict firewalls are unable to use UDP: +If clients are unable to connect using UDP (due to limitations or strict firewalls), you have a few options: - Since plugin version 0.17 and `rtcd` version 0.11 the RTC service will listen for TCP connections in addition to UDP ones. If configured correctly (e.g. using commonly allowed ports such as 80 or 443) it's possible to have clients connect directly through TCP when unable to do it through the preferred UDP channel. - Run calls through an external TURN server that listens on TCP and relays all media traffic between peers. However, this is a sub-optimal solution that should be avoided if possible as it will introduce extra latency along with added infrastructural cost. -### Do I need a TURN server? - -TURN becomes necessary when you expect to have clients that are unable to connect through the configured UDP port. This can happen due to very restrictive firewalls that either block non standard ports even in the outgoing direction or don't allow the use of the UDP protocol altogether (e.g. some corporate firewalls). In such cases TURN is needed to allow connectivity. +**Do I need a TURN server?** +Only if clients are behind restrictive firewalls that block UDP. We recommend (and officially support) [coturn](https://github.com/coturn/coturn) if needed. -We officially support and recommend using [coturn](https://github.com/coturn/coturn) for a stable and performant TURN service implementation. +**Can RTCD traffic be kept internal?** +Yes, and it's recommended. Only the media ports need to be accessible to end-users. -### How will this work with an existing reverse proxy sitting in front of Mattermost? +**How will this work with an existing reverse proxy sitting in front of Mattermost?** -Generally clients should connect directly to either Mattermost or, if deployed, the dedicated `rtcd` service through the configured UDP port . However, it's also possible to route the traffic through an existing load balancer as long as this has support for routing the UDP protocol (e.g. nginx). Of course this will require additional configuration and potential changes to how the plugin is run as it won't be possible to load balance the UDP flow across multiple instances like it happens for HTTP. +Generally clients should connect directly to either Mattermost or, if deployed, the dedicated `rtcd` service through the configured UDP port. However, it's also possible to route the traffic through an existing load balancer as long as this has support for routing the UDP protocol (e.g. nginx). Of course this will require additional configuration and potential changes to how the plugin is run as it won't be possible to load balance the UDP flow across multiple instances like it happens for HTTP. -### Do calls require a dedicated server to work or can they run alongside Mattermost? +**Do calls require a dedicated server to work or can they run alongside Mattermost?** The plugin can function in different modes. By default calls are handled completely by the plugin which runs as part of Mattermost. It's also possible to use a dedicated service to offload the computational and bandwidth costs and scale further (Enterprise only). -### Can the traffic between Mattermost and `rtcd` be kept internal or should it be opened to the public? +See [RTCD Setup and Configuration](calls-rtcd-setup.html) for more details on the dedicated RTCD service. + +**Can the traffic between Mattermost and `rtcd` be kept internal or should it be opened to the public?** When possible, it's recommended to keep communication between the Mattermost cluster and the dedicated `rtcd` service under the same private network as this can greatly simplify deployment and security. There's no requirement to expose `rtcd`'s HTTP API to the public internet. -### Can Calls be rolled out on a per-channel basis? +**Can Calls be rolled out on a per-channel basis?** ```{include} ../_static/badges/selfhosted-only.md ``` Yes. Mattermost system admins running self-hosted deployments can enable or disable call functionality per channel. Once [test mode](https://docs.mattermost.com/configure/plugins-configuration-settings.html#test-mode) is enabled for Mattermost Calls: -- Select **Enable calls** for each channel where you want Calls enabled -- Select **Disable calls** for all channels where you want Calls disabled. +1. **Navigate to the channel** where you want to enable or disable Calls +2. **Access the channel menu** by clicking the channel name at the top of the channel +3. **Select the Calls option** from the dropdown menu: + - Select **Enable calls** for each channel where you want Calls enabled + - Select **Disable calls** for all channels where you want Calls disabled + +![Channel menu showing Enable/Disable calls options](../images/calls-channel-enable-disable.png) Once Calls is enabled for specific channels, users can start making calls in those channels. @@ -746,48 +501,11 @@ When [test mode](https://docs.mattermost.com/configure/plugins-configuration-set ## Troubleshooting -### Connectivity issues - -If calls are failing to connect or timing out, it's likely there could be a misconfiguration at either the plugin config or networking level. +For comprehensive troubleshooting steps and debugging techniques, please refer to the [Calls Troubleshooting](calls-troubleshooting.html) guide. -For example, the [RTC Server Port (UDP)](https://docs.mattermost.com/configure/plugins-configuration-settings.html#rtc-server-port-udp) or the [RTC Server Port (TCP)](https://docs.mattermost.com/configure/plugins-configuration-settings.html#rtc-server-port-tcp) may not be open or forwarded correctly. - -#### Connectivity checks - -An easy way to check whether data can go through is to perform some tests using the `netcat` command line tool. - -On the host running Calls (could be the Mattermost instance itself or the one running `rtcd` depending on the chosen setup), run the following: - -```sh -nc -l -u -p 8443 -``` - -On the client side (i.e., the machine you would normally use to run the Mattermost desktop app or browser), run the following: - -```sh -nc -v -u HOST_IP 8443 -``` - -If connection succeeds, you should be able to send and receive text messages by typing and hitting enter on either side. - -```{note} -`HOST_IP` should generally be the public (client facing) IP of the Mattermost -(or `rtcd`) instance hosting the calls. When set, it should be the value of the [ICE Host Override](https://docs.mattermost.com/configure/plugins-configuration-settings.html#ice-host-override) -config setting. - -`8443` should be changed with the port configured in [RTC Server Port](https://docs.mattermost.com/configure/plugins-configuration-settings.html#rtc-server-port-udp). - -The same checks can be performed to test connectivity through the TCP port using the same commands with `-u` flag removed. -``` - -#### Network packets debugging - -A more advanced way to debug networking issues is to use the `tcpdump` command line utility to temporaily monitor network packets flowing in and out of the instance hosting calls. - -On the server side, run the following: - -```sh -sudo tcpdump -n port 8443 -``` +## Next Steps -This command will output information (i.e. source and destination addresses) for all the network packets being sent or received through port `8443`. This is a good way to check whether data is getting in and out of the instance and can be used to quickly identify network configuration issues. +1. For detailed setup instructions, see [RTCD Setup and Configuration](calls-rtcd-setup.html) +2. For monitoring guidance, see [Calls Metrics and Monitoring](calls-metrics-monitoring.html) +3. If you encounter issues, see [Calls Troubleshooting](calls-troubleshooting.html) +4. For Kubernetes deployments, see [Calls Deployment on Kubernetes](calls-kubernetes.html) diff --git a/source/configure/calls-kubernetes.md b/source/configure/calls-kubernetes.md new file mode 100644 index 00000000000..627bff23982 --- /dev/null +++ b/source/configure/calls-kubernetes.md @@ -0,0 +1,163 @@ +# Calls deployment on Kubernetes + +```{include} ../_static/badges/allplans-cloud-selfhosted.md +``` + +This guide provides detailed information for deploying Mattermost Calls on Kubernetes environments. + +## Overview + +Mattermost Calls has been designed to integrate well with Kubernetes to offer improved scalability and control over the deployment. For Kubernetes deployments, the RTCD service is strongly recommended and is the only officially supported approach. + +## Architecture + +![Calls deployed in a Kubernetes cluster](../images/calls-deployment-kubernetes.png) + +This diagram shows how the RTCD standalone service can be deployed in a Kubernetes cluster. In this architecture: + +1. Calls traffic is handled by dedicated RTCD pods +2. RTCD services are exposed through load balancers +3. Scaling is managed through Kubernetes deployment configurations +4. Call recording and transcription is handled by the calls-offloader service (see [Calls Offloader Setup and Configuration](calls-offloader-setup.html)) + +If Mattermost isn't already deployed in your Kubernetes cluster and you want to use this deployment type, visit the [Kubernetes operator guide](/install/mattermost-kubernetes-operator.html). + +## Helm Chart Deployment + +The recommended way to deploy Calls-related components in a Kubernetes environment is to use the officially provided Helm charts: + +### RTCD Helm Chart + +The RTCD Helm chart deploys the RTCD service needed for call media handling: + +```bash +helm repo add mattermost https://helm.mattermost.com +helm repo update + +helm install mattermost-rtcd mattermost/mattermost-rtcd \ + --set ingress.enabled=true \ + --set ingress.host=rtcd.example.com \ + --set service.annotations."service\\.beta\\.kubernetes\\.io/aws-load-balancer-backend-protocol"=udp \ + --set rtcd.ice.hostOverride=rtcd.example.com +``` + +For complete configuration options, see the [RTCD Helm chart documentation](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-rtcd). + +### Calls-Offloader Helm Chart + +If you need call recording and transcription capabilities, deploy the calls-offloader service: + +```bash +helm install mattermost-calls-offloader mattermost/mattermost-calls-offloader \ + --set ingress.enabled=true \ + --set ingress.host=calls-offloader.example.com +``` + +For complete configuration options, see the [Calls-Offloader Helm chart documentation](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-calls-offloader). + +## Kubernetes-Specific Configuration + +### Network Configuration + +For Kubernetes deployments, you need to ensure: + +1. UDP traffic is properly routed to RTCD pods (for media) +2. TCP traffic can reach both the Mattermost pods and RTCD pods +3. Load balancers are properly configured to handle UDP traffic +4. Network policies allow the required communications between services + +Recommended annotations for AWS environments: + +```yaml +service.beta.kubernetes.io/aws-load-balancer-backend-protocol: udp +service.beta.kubernetes.io/aws-load-balancer-type: nlb +``` + +### Resource Requirements + +For optimal performance in Kubernetes environments: + +1. **CPU**: At least 2 CPU cores per RTCD pod +2. **Memory**: At least 1GB RAM per RTCD pod +3. **Network**: Sufficient bandwidth for expected call volume (see benchmarks) + +We recommend setting resource limits and requests in your deployment: + +```yaml +resources: + requests: + cpu: 1000m + memory: 1Gi + limits: + cpu: 2000m + memory: 2Gi +``` + +### Scaling Considerations + +Horizontal scaling of RTCD pods is possible, but remember: + +1. Each call is hosted entirely on a single RTCD pod +2. DNS-based load balancing should be used to distribute calls among pods +3. Health checks should ensure that only healthy pods receive new calls +4. Calls remain on their assigned pod for their entire duration + +### Limitations + +Due to the inherent complexities of hosting a WebRTC service, some limitations apply when deploying Calls in a Kubernetes environment. + +One key requirement is that each `rtcd` process must live in a dedicated Kubernetes node. This is necessary to forward the data correctly while allowing for horizontal scaling. Data should generally not go through a standard ingress but directly to the pod running the `rtcd` process. + +The general recommendation is to expose one external IP address per `rtcd` instance (Kubernetes node). This makes it simpler to scale as the application is able to detect its own external address (through STUN) and advertise it to clients to achieve connectivity with minimal configuration. + +If, for some reason, exposing multiple IP addresses is not possible in your environment, port mapping (NAT) can be used. In this scenario different ports are used to map the respective `rtcd` nodes behind the single external IP. Example: + +```text +EXT_IP:8443 -> rtcdA:8443 +EXT_IP:8444 -> rtcdB:8443 +EXT_IP:8445 -> rtcdC:8443 +``` + +This case requires a couple of extra configurations: + +* NAT mappings need to be in place for every `rtcd` node. This is usually done at the ingress point (e.g., ELB, NLB, etc). + +* The `RTCD_RTC_ICEHOSTPORTOVERRIDE` config should be used to pass a full mapping of node IPs and their respective port. + + * Example: `RTCD_RTC_ICEHOSTPORTOVERRIDE=rtcdA_IP/8443,rtcdB_IP/8444,rtcdC_IP/8445` + +* The `RTCD_RTC_ICEHOSTOVERRIDE` should be used to set the external IP address. + +```{note} +One option to limit these static mappings is to reduce the size of the local subnet (e.g., to `/29`). +``` + +## Monitoring and Metrics + +We recommend deploying Prometheus and Grafana alongside your Calls deployment: + +1. Configure Prometheus to scrape metrics from both Mattermost and RTCD pods +2. Import the official Mattermost Calls dashboard to Grafana +3. Set up alerts for CPU usage, connection failures, and error rates + +For detailed information on metrics collection and monitoring, see the [Calls Metrics and Monitoring](calls-metrics-monitoring.html) guide. + +## Troubleshooting + +For Kubernetes-specific troubleshooting: + +1. Check pod logs: `kubectl logs -f deployment/mattermost-rtcd` +2. Verify service connectivity: `kubectl port-forward service/mattermost-rtcd 8045:8045` +3. Ensure UDP traffic is properly routed through your ingress/load balancer +4. Verify network policies allow required communication paths + +For detailed troubleshooting steps, see the [Calls Troubleshooting](calls-troubleshooting.html) guide. + +## Other Calls Documentation + +- [Calls Overview](calls-deployment.html): Overview of deployment options and architecture +- [RTCD Setup and Configuration](calls-rtcd-setup.html): Comprehensive guide for setting up the dedicated RTCD service +- [Calls Offloader Setup and Configuration](calls-offloader-setup.html): Setup guide for call recording and transcription +- [Calls Metrics and Monitoring](calls-metrics-monitoring.html): Guide to monitoring Calls performance using metrics and observability +- [Calls Troubleshooting](calls-troubleshooting.html): Detailed troubleshooting steps and debugging techniques +3. If you encounter issues, see [Calls Troubleshooting](calls-troubleshooting.html) \ No newline at end of file diff --git a/source/configure/calls-metrics-monitoring.md b/source/configure/calls-metrics-monitoring.md new file mode 100644 index 00000000000..b93bbfa0bd7 --- /dev/null +++ b/source/configure/calls-metrics-monitoring.md @@ -0,0 +1,245 @@ +# Calls Metrics and Monitoring + +```{include} ../_static/badges/ent-only.md +``` + +This guide provides detailed information on monitoring Mattermost Calls performance and health through metrics and observability tools. Effective monitoring is essential for maintaining optimal call quality and quickly addressing any issues that arise. + +- [Metrics overview](#metrics-overview) +- [Setting up monitoring](#setting-up-monitoring) +- [Key metrics to monitor](#key-metrics-to-monitor) +- [Grafana dashboards](#grafana-dashboards) +- [Alerting recommendations](#alerting-recommendations) +- [Performance baselines](#performance-baselines) + +## Metrics Overview + +Mattermost Calls provides metrics through Prometheus for both the Calls plugin and the RTCD service. These metrics help track: + +- Active call sessions and participants +- Media track statistics +- Connection states and errors +- Resource utilization (CPU, memory, network) +- WebSocket connections and events + +The metrics are exposed through HTTP endpoints: + +- **Calls Plugin**: `/plugins/com.mattermost.calls/metrics` +- **RTCD Service**: `/metrics` (default) or a configured endpoint + +## Setting Up Monitoring + +### Prerequisites + +To monitor Calls metrics, you'll need: + +1. **Prometheus**: For collecting and storing metrics +2. **Grafana**: For visualizing metrics (optional but recommended) + +### Installing Prometheus + +1. **Download and install Prometheus**: + + Visit the [Prometheus download page](https://prometheus.io/download/) for installation instructions. + +2. **Configure Prometheus** to scrape metrics from all Calls-related services: + + Complete `prometheus.yml` configuration for Calls monitoring: + + ```yaml + global: + scrape_interval: 15s + evaluation_interval: 15s + + scrape_configs: + - job_name: 'prometheus' + static_configs: + - targets: ['PROMETHEUS_IP:9090'] + + - job_name: 'mattermost' + metrics_path: /metrics + static_configs: + - targets: ['MATTERMOST_SERVER_IP:8067'] + + - job_name: 'calls-plugin' + metrics_path: /plugins/com.mattermost.calls/metrics + static_configs: + - targets: ['MATTERMOST_SERVER_IP:8067'] + labels: + service_name: 'calls-plugin' + + - job_name: 'rtcd' + metrics_path: /metrics + static_configs: + - targets: ['RTCD_SERVER_IP:8045'] + labels: + service_name: 'rtcd' + + - job_name: 'rtcd-node-exporter' + metrics_path: /metrics + static_configs: + - targets: ['RTCD_SERVER_IP:9100'] + labels: + service_name: 'rtcd' + + - job_name: 'calls_offloader-node-exporter' + metrics_path: /metrics + static_configs: + - targets: ['CALLS_OFFLOADER_SERVER_IP:9100'] + labels: + service_name: 'offloader' + ``` + + Replace the placeholder IP addresses with your actual server addresses: + + - `MATTERMOST_SERVER_IP`: IP address of your Mattermost server + - `RTCD_SERVER_IP`: IP address of your RTCD server + - `CALLS_OFFLOADER_SERVER_IP`: IP address of your calls-offloader server (if deployed) + - `PROMETHEUS_IP`: IP address of your Prometheus server + - **Note**: The configuration above uses the default ports (RTCD: `8045`, Mattermost metrics: `8067`, etc.). Adjust these ports in `prometheus.yml` if you have customized them. + + ```{important} + **Metrics Path**: Ensure the metrics paths are correct. The RTCD service exposes metrics at `/metrics` by default, and the Calls plugin at `/plugins/com.mattermost.calls/metrics`. + ``` + + ```{important} + **Metrics Configuration Notice**: Use the `service_name` labels as shown in the configuration above. These labels help organize metrics in dashboards and enable proper service identification. + ``` + + ```{note} + - **node_exporter**: Optional but recommended for system-level metrics (CPU, memory, disk, network). See [node_exporter setup guide](https://prometheus.io/docs/guides/node-exporter/) for installation instructions. + - **calls-offloader**: Only needed if you have call recording/transcription enabled + ``` + +### Installing Grafana + +1. **Download and install Grafana**: + + Visit the [Grafana download page](https://grafana.com/grafana/download) for installation instructions. + +2. **Configure Grafana** to use Prometheus as a data source: + + - Add a new data source in Grafana + - Select Prometheus as the type + - Enter the URL of your Prometheus server + - Test and save the configuration + +3. **Import the Mattermost Calls dashboard**: + + - Navigate to Dashboards > Import in Grafana + - Enter dashboard ID: `23225` or use the direct link: [Mattermost Calls Performance Monitoring](https://grafana.com/grafana/dashboards/23225-mattermost-calls-performance-monitoring/) + - Select your Prometheus data source, and enter values for the + - Confirm the port used for RTCD metrics (default is `8045`), and the port used for the Calls plugin metrics (default is `8067`) + - Click Import to add the dashboard to your Grafana instance + + ```{note} + The dashboard is also available as JSON source from the [Mattermost performance assets repository](https://github.com/mattermost/mattermost-performance-assets/blob/master/grafana/mattermost-calls-performance-monitoring.json) for manual import or customization. + ``` + +## Key Metrics to Monitor + +### RTCD Metrics + +#### Process Metrics + +These metrics help monitor the health and resource usage of the RTCD process: + +- `rtcd_process_cpu_seconds_total`: Total CPU time spent +- `rtcd_process_open_fds`: Number of open file descriptors +- `rtcd_process_max_fds`: Maximum number of file descriptors +- `rtcd_process_resident_memory_bytes`: Memory usage in bytes +- `rtcd_process_virtual_memory_bytes`: Virtual memory used + +**Interpretation**: + +- High CPU usage (>70%) may indicate the need for additional RTCD instances +- Steadily increasing memory usage might indicate a memory leak +- High number of file descriptors could indicate connection handling issues + +#### WebRTC Connection Metrics + +These metrics track the WebRTC connections and media flow: + +- `rtcd_rtc_conn_states_total{state="X"}`: Count of connections in different states +- `rtcd_rtc_errors_total{type="X"}`: Count of RTC errors by type +- `rtcd_rtc_rtp_tracks_total{direction="X"}`: Count of RTP tracks (incoming/outgoing) +- `rtcd_rtc_sessions_total`: Total number of active RTC sessions + +**Interpretation**: + +- Increasing error counts may indicate connectivity or configuration issues +- Track by state to see if connections are failing to establish or dropping +- Larger track counts require proportionally more CPU and bandwidth + +#### WebSocket Metrics + +These metrics track the signaling channel: + +- `rtcd_ws_connections_total`: Total number of active WebSocket connections +- `rtcd_ws_messages_total{direction="X"}`: Count of WebSocket messages (sent/received) + +**Interpretation**: + +- Connection count should match expected participant numbers +- Unusually high message counts might indicate protocol issues +- Connection drops might indicate network issues + +### Calls Plugin Metrics + +Similar metrics are available for the Calls plugin with the following prefixes: + +- Process metrics: `mattermost_plugin_calls_process_*` +- WebRTC connection metrics: `mattermost_plugin_calls_rtc_*` +- WebSocket metrics: `mattermost_plugin_calls_websocket_*` +- Store metrics: `mattermost_plugin_calls_store_ops_total` + +## Performance Baselines + +The following performance benchmarks provide baseline metrics for RTCD deployments under various load conditions and configurations. + +**Deployment specifications** + +- 1x r6i.large nginx proxy +- 3x c5.large MM app nodes (HA) +- 2x db.x2g.xlarge RDS Aurora MySQL v8 (one writer, one reader) +- 1x (c7i.xlarge, c7i.2xlarge, c7i.4xlarge) RTCD +- 2x c7i.2xlarge load-test agents + +**App specifications** + +- Mattermost v9.6 +- Mattermost Calls v0.28.0 +- RTCD v0.16.0 +- load-test agent v0.28.0 + +**Media specifications** + +- Speech sample bitrate: 80Kbps +- Screen sharing sample bitrate: 1.6Mbps + +**Results** + +Below are the detailed benchmarks based on internal performance testing: + +| Calls | Users/call | Unmuted/call | Screen sharing | CPU (avg) | Memory (avg) | Bandwidth (in/out) | Instance (EC2) | +|-------|------------|--------------|----------------|-----------|--------------|--------------------|--------------| +| 100 | 8 | 2 | no | 60% | 0.5GB | 22Mbps / 125Mbps | c6i.xlarge | +| 100 | 8 | 2 | no | 30% | 0.5GB | 22Mbps / 125Mbps | c6i.2xlarge | +| 100 | 8 | 2 | yes | 86% | 0.7GB | 280Mbps / 2.2Gbps | c6i.2xlarge | +| 10 | 50 | 2 | no | 35% | 0.3GB | 5.25Mbps / 86Mbps | c6i.xlarge | +| 10 | 50 | 2 | no | 16% | 0.3GB | 5.25Mbps / 86Mbps | c6i.2xlarge | +| 10 | 50 | 2 | yes | 90% | 0.3GB | 32Mbps / 1.33Gbps | c6i.xlarge | +| 10 | 50 | 2 | yes | 45% | 0.3GB | 32Mbps / 1.33Gbps | c6i.2xlarge | +| 5 | 200 | 2 | no | 65% | 0.6GB | 8.2Mbps / 180Mbps | c6i.xlarge | +| 5 | 200 | 2 | no | 30% | 0.6GB | 8.2Mbps / 180Mbps | c6i.2xlarge | +| 5 | 200 | 2 | yes | 90% | 0.7GB | 31Mbps / 2.2Gbps | c6i.2xlarge | + +## Other Calls Documentation + +- [Calls Overview](calls-deployment.html): Overview of deployment options and architecture +- [RTCD Setup and Configuration](calls-rtcd-setup.html): Comprehensive guide for setting up the dedicated RTCD service +- [Calls Offloader Setup and Configuration](calls-offloader-setup.html): Setup guide for call recording and transcription +- [Calls Deployment on Kubernetes](calls-kubernetes.html): Detailed guide for deploying Calls in Kubernetes environments +- [Calls Troubleshooting](calls-troubleshooting.html): Detailed troubleshooting steps and debugging techniques + +Configure Prometheus storage accordingly to balance disk usage with retention needs. \ No newline at end of file diff --git a/source/configure/calls-offloader-setup.md b/source/configure/calls-offloader-setup.md new file mode 100644 index 00000000000..79dd55e3c0c --- /dev/null +++ b/source/configure/calls-offloader-setup.md @@ -0,0 +1,368 @@ +# Calls Offloader Setup and Configuration + +```{include} ../_static/badges/ent-only.md +``` + + +This guide provides detailed instructions for setting up, configuring, and validating the Mattermost calls-offloader service used for call recording and transcription features. + +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [Installation and deployment](#installation-and-deployment) +- [Configuration](#configuration) +- [Validation and testing](#validation-and-testing) +- [Integration with Mattermost](#integration-with-mattermost) +- [Troubleshooting](#troubleshooting) + +## Overview + +The calls-offloader service is a dedicated microservice that handles resource-intensive tasks for Mattermost Calls, including: + +- **Call recording**: Captures audio and screen sharing content from calls +- **Call transcription**: Provides automated transcription of recorded calls +- **Live captions** (Experimental): Real-time transcription during active calls + +By offloading these tasks to a dedicated service, the main Mattermost server and RTCD service can focus on core functionality while maintaining optimal performance. + +## Prerequisites + +Before deploying calls-offloader, ensure you have: + +- A Mattermost Enterprise license +- A properly configured Mattermost Calls deployment (either integrated or with RTCD) +- Docker installed and running (for Docker-based job execution) +- Sufficient storage space for recordings (see [Storage Requirements](#storage-requirements)) +- A server or container environment with adequate resources + +### System Requirements + +For detailed system requirements and performance recommendations, refer to the [calls-offloader performance documentation](https://github.com/mattermost/calls-offloader/blob/master/docs/performance.md). + +### Storage Requirements + +Call recordings can consume significant storage space: + +- Audio-only recordings: ~1MB per minute per participant +- Screen sharing recordings: ~10-50MB per minute depending on content + +## Installation and Deployment + +### Bare Metal or VM Deployment + +1. Download the latest release from the [calls-offloader GitHub repository](https://github.com/mattermost/calls-offloader/releases) + +2. Create the necessary directories: + + ```bash + sudo mkdir -p /opt/calls-offloader/data/db + sudo useradd --system --home /opt/calls-offloader calls-offloader + sudo chown -R calls-offloader:calls-offloader /opt/calls-offloader + ``` + +3. Create a configuration file (`/opt/calls-offloader/config.toml`): + + ```toml + [api] + http.listen_address = ":4545" + http.tls.enable = false + http.tls.cert_file = "" + http.tls.cert_key = "" + security.allow_self_registration = true + security.enable_admin = true + security.admin_secret_key = "changeme" + security.session_cache.expiration_minutes = 1440 + + [store] + data_source = "/opt/calls-offloader/data/db" + + [jobs] + api_type = "docker" + max_concurrent_jobs = 2 + failed_jobs_retention_time = "7d" + image_registry = "mattermost" + + [logger] + enable_console = true + console_json = false + console_level = "INFO" + enable_file = true + file_json = true + file_level = "INFO" + file_location = "/opt/calls-offloader/calls-offloader.log" + enable_color = true + ``` + +4. Create a systemd service file (`/etc/systemd/system/calls-offloader.service`): + + ```ini + [Unit] + Description=Mattermost Calls Offloader Service + After=network.target docker.service + Requires=docker.service + + [Service] + Type=simple + User=calls-offloader + WorkingDirectory=/opt/calls-offloader + ExecStart=/opt/calls-offloader/calls-offloader --config /opt/calls-offloader/config.toml + Restart=always + RestartSec=10 + LimitNOFILE=65536 + + [Install] + WantedBy=multi-user.target + ``` + +5. Enable and start the service: + + ```bash + sudo systemctl daemon-reload + sudo systemctl enable calls-offloader + sudo systemctl start calls-offloader + ``` + +6. Check the service status: + + ```bash + sudo systemctl status calls-offloader + ``` + +7. Verify the service is responding: + + ```bash + curl http://localhost:4545/version + # Example output: + # {"buildDate":"2025-03-10 19:13","buildVersion":"v0.9.2","buildHash":"a4bd418","goVersion":"go1.23.6"} + ``` + +## Configuration + +### API Configuration + +The API section controls how the service accepts requests: + +- **http.listen_address**: The address and port where the service listens (default: `:4545`) +- **http.tls.enable**: Whether to use TLS encryption for the API +- **security.allow_self_registration**: Allow clients to self-register for job management +- **security.enable_admin**: Enable admin functionality +- **security.admin_secret_key**: Secret key for admin authentication (change from default!) + +### Store Configuration + +Controls persistent data storage: + +- **data_source**: Path to directory for storing job metadata and state + +### Jobs Configuration + +Controls job processing behavior: + +- **api_type**: Job execution backend (`docker` or `kubernetes`) +- **max_concurrent_jobs**: Maximum number of simultaneous recording/transcription jobs +- **failed_jobs_retention_time**: How long to keep failed job data before cleanup +- **image_registry**: Docker registry for job runner images (typically `mattermost`) + +### Logger Configuration + +Controls logging output: + +- **enable_console**: Log to console output +- **console_json**: Use JSON format for console logs +- **console_level**: Log level for console (DEBUG, INFO, WARN, ERROR) +- **enable_file**: Log to file +- **file_location**: Path to log file +- **enable_color**: Use colored output for console logs + +### Private Network Configuration + +When the Mattermost deployment is running in a private network, additional configuration may be necessary for the jobs spawned by the calls-offloader service to reach the Mattermost server. + +In such cases, you can override the site URL used by recorder jobs or transcriber jobs to connect to Mattermost by setting the following environment variables on the Mattermost server: + +- **MM_CALLS_RECORDER_SITE_URL**: Override the site URL used by recording jobs +- **MM_CALLS_TRANSCRIBER_SITE_URL**: Override the site URL used by transcription jobs + +Example configuration: + +Create or edit the Mattermost environment file (`/opt/mattermost/config/mattermost.environment`): + +```bash +MM_CALLS_RECORDER_SITE_URL="http://internal-mattermost-server:8065" +MM_CALLS_TRANSCRIBER_SITE_URL="http://internal-mattermost-server:8065" +``` + +Then ensure your Mattermost systemd service references this environment file: + +```ini +[Unit] +Description=Mattermost +After=network.target + +[Service] +Type=notify +EnvironmentFile=/opt/mattermost/config/mattermost.environment +ExecStart=/opt/mattermost/bin/mattermost +TimeoutStartSec=3600 +KillMode=mixed +Restart=always +RestartSec=10 +WorkingDirectory=/opt/mattermost +User=mattermost +Group=mattermost + +[Install] +WantedBy=multi-user.target +``` + +This is particularly useful when: + +- The calls-offloader service runs in a different network segment than clients +- Internal DNS resolution differs from external URLs +- You need to use internal load balancer endpoints for job communication + +## Validation and Testing + +After deploying calls-offloader, validate the installation: + +1. **Check service status**: + + ```bash + # For systemd + sudo systemctl status calls-offloader + ``` + +2. **Test API connectivity**: + + **From the calls-offloader server (localhost test)**: + + ```bash + curl http://localhost:4545/version + # Should return version information + # Example: {"buildDate":"2025-03-10 19:13","buildVersion":"v0.9.2","buildHash":"a4bd418","goVersion":"go1.23.6"} + ``` + + **From the Mattermost server**: + + ```bash + curl http://YOUR_CALLS_OFFLOADER_SERVER:4545/version + # Should return the same version information + # This confirms network connectivity from Mattermost to calls-offloader + ``` + + If the localhost test works but the Mattermost server test fails, check: + + - Firewall rules or SELinux policies on the calls-offloader server (port 4545 must be accessible) + - Network connectivity between Mattermost and calls-offloader servers + - calls-offloader service binding configuration (ensure it's not bound to localhost only) + +3. **Verify Docker integration** (if using docker api_type): + + ```bash + # Check that system user running calls-offloader can access Docker + sudo -u calls-offloader docker ps + ``` + +## Integration with Mattermost + +Once calls-offloader is properly set up and validated, configure Mattermost to use it: + +1. Go to **System Console > Plugins > Calls** + +2. In the **Job Service** section: + + - Set **Job Service URL** to your calls-offloader service (e.g., `http://calls-offloader-server:4545`) + +3. Enable recording and transcription features as needed: + + - **Enable Call Recordings**: Toggle to allow call recordings + - **Enable Call Transcriptions**: Toggle to allow call transcriptions + - **Enable Live Captions** (Experimental): Toggle to allow real-time transcription + +4. Save the configuration + +5. Restart the Calls plugin to re-establish state: + + - Go to **System Console > Plugins > Plugin Management** + - Find the **Calls** plugin and click **Disable** + - Wait a few seconds, then click **Enable** + +6. Test by starting a call and enabling recording or live captions + +## Troubleshooting + +### Common Issues + +**"failed to create recording job: max concurrent jobs reached"** + +This error occurs when the calls-offloader service has reached its configured job limit. + +Solutions: + +- Increase `max_concurrent_jobs` in the configuration +- Check if jobs are hanging and restart the service +- Monitor system resources and scale up if needed + +**Jobs not processing** + +Check the following: + +- Verify the calls-offloader service is running: `sudo systemctl status calls-offloader` +- Ensure network connectivity between Mattermost and calls-offloader +- Check Docker daemon is running and accessible by the user running `calls-offloader`: `docker ps` +- Verify authentication configuration matches between services +- Review service logs for specific error messages + +**Docker permission issues** + +If using Docker API and seeing permission errors: + +```bash +# Add calls-offloader user to docker group +sudo usermod -a -G docker calls-offloader +sudo systemctl restart calls-offloader +``` + +### Debugging Commands + +Monitor calls-offloader job containers: + +```bash +# View running job containers +docker ps --format "{{.ID}} {{.Image}}" | grep "calls" + +# Follow logs for debugging +docker ps --format "{{.ID}} {{.Image}}" | grep "calls" | awk '{print $1}' | xargs -I {} docker logs -f {} + +# View completed job containers +docker ps -a --filter "status=exited" +``` + +Monitor service health: + +```bash +# Check service version and health +curl http://localhost:4545/version +``` + +Check service logs: + +```bash +# View recent logs +sudo journalctl -u calls-offloader -f + +# View log file (if file logging enabled) +tail -f /opt/calls-offloader/calls-offloader.log +``` + +### Performance Monitoring + +Monitor calls-offloader performance and resource usage to ensure optimal operation. See [Calls Metrics and Monitoring](calls-metrics-monitoring.html) for details on setting up metrics and observability. + +## Other Calls Documentation + +- [Calls Overview](calls-deployment.html): Overview of deployment options and architecture +- [RTCD Setup and Configuration](calls-rtcd-setup.html): Comprehensive guide for setting up the dedicated RTCD service +- [Calls Metrics and Monitoring](calls-metrics-monitoring.html): Guide to monitoring Calls performance using metrics and observability +- [Calls Deployment on Kubernetes](calls-kubernetes.html): Detailed guide for deploying Calls in Kubernetes environments +- [Calls Troubleshooting](calls-troubleshooting.html): Detailed troubleshooting steps and debugging techniques +- [calls-offloader performance documentation](https://github.com/mattermost/calls-offloader/blob/master/docs/performance.md): Detailed performance tuning and monitoring recommendations \ No newline at end of file diff --git a/source/configure/calls-rtcd-setup.md b/source/configure/calls-rtcd-setup.md new file mode 100644 index 00000000000..c7fd09e4f44 --- /dev/null +++ b/source/configure/calls-rtcd-setup.md @@ -0,0 +1,378 @@ +# RTCD Setup and Configuration + +```{include} ../_static/badges/ent-only.md +``` + +This guide provides detailed instructions for setting up, configuring, and validating a Mattermost Calls deployment using the dedicated RTCD service. + +- [Prerequisites](#prerequisites) +- [Installation and deployment](#installation-and-deployment) +- [Configuration](#configuration) +- [Validation and testing](#validation-and-testing) +- [Horizontal scaling](#horizontal-scaling) +- [Integration with Mattermost](#integration-with-mattermost) + +## Prerequisites + +Before deploying RTCD, ensure you have: + +- A Mattermost Enterprise license +- A server or VM with sufficient CPU and network capacity (see the [Performance](calls-deployment.html#performance) section for sizing guidance) + +## Network Requirements + +The following network connectivity is required: + +| Service | Ports | Protocols | Source | Target | +|-------------------|--------|-----------------|-------------------------|------------------------| +| Calls plugin API | 80,443 | TCP (incoming) | Mattermost clients | Mattermost server | +| RTC media | 8443 | UDP (incoming) | Mattermost clients | Mattermost or RTCD | +| RTC media | 8443 | TCP (incoming) | Mattermost clients | Mattermost or RTCD | +| RTCD API | 8045 | TCP (incoming) | Mattermost server | RTCD service | +| STUN | 3478 | UDP (outgoing) | Mattermost or RTCD | STUN servers | + +## Installation and Deployment + +There are multiple ways to deploy RTCD, depending on your environment. We recommend the following order based on production readiness and operational control: + +### Bare Metal or VM Deployment (Recommended) + +This is the recommended deployment method for production environments as it provides the best performance and operational control. + +1. Download the latest release from the [RTCD GitHub repository](https://github.com/mattermost/rtcd/releases) + +2. Create a configuration file (`/opt/rtcd/rtcd.toml`) with the following settings: + + ```toml + [api] + http.listen_address = ":8045" + security.allow_self_registration = true + + [rtc] + ice_address_udp = "" + ice_port_udp = 8443 + ice_address_tcp = "" + ice_port_tcp = 8443 + ice_host_override = "YOUR_RTCD_SERVER_PUBLIC_IP" + + # UDP port range for WebRTC connections + ice.port_range.min = 9000 + ice.port_range.max = 10000 + + # STUN/TURN server configuration + ice_servers = [ + { urls = ["stun:stun.global.calls.mattermost.com:3478"] } + ] + + [store] + data_source = "/opt/rtcd/data/db" + + [logger] + enable_console = true + console_json = true + console_level = "INFO" + enable_file = true + file_json = true + file_level = "INFO" + file_location = "/opt/rtcd/rtcd.log" + enable_color = true + + [mattermost] + host = "http://YOUR_MATTERMOST_SERVER:8065" + ``` + +3. Create the data directory: + + ```bash + sudo mkdir -p /opt/rtcd/data/db + ``` + +4. Create a systemd service file (`/etc/systemd/system/rtcd.service`): + + ```ini + [Unit] + Description=Mattermost RTCD Server + After=network.target + + [Service] + Type=simple + User=root + ExecStart=/opt/rtcd/rtcd --config /opt/rtcd/rtcd.toml + Restart=always + RestartSec=10 + LimitNOFILE=65536 + + [Install] + WantedBy=multi-user.target + ``` + +5. Enable and start the service: + + ```bash + sudo systemctl daemon-reload + sudo systemctl enable rtcd + sudo systemctl start rtcd + ``` + +6. Check the service status: + + ```bash + sudo systemctl status rtcd + ``` + +### Docker Deployment + +Docker deployment is suitable for development, testing, or containerized production environments: + +1. Run the RTCD container with basic configuration: + + ```bash + docker run -d --name rtcd \ + -e "RTCD_LOGGER_ENABLEFILE=false" \ + -e "RTCD_API_SECURITY_ALLOWSELFREGISTRATION=true" \ + -p 8443:8443/udp \ + -p 8443:8443/tcp \ + -p 8045:8045/tcp \ + mattermost/rtcd:latest + ``` + +2. For debugging purposes, you can enable more detailed logging: + + ```bash + docker run -d --name rtcd \ + -e "RTCD_LOGGER_ENABLEFILE=false" \ + -e "RTCD_LOGGER_CONSOLELEVEL=DEBUG" \ + -e "RTCD_API_SECURITY_ALLOWSELFREGISTRATION=true" \ + -p 8443:8443/udp \ + -p 8443:8443/tcp \ + -p 8045:8045/tcp \ + mattermost/rtcd:latest + ``` + + To view the logs: + + ```bash + docker logs -f rtcd + ``` + +You can also use a mounted configuration file instead of environment variables: + +```bash +docker run -d --name rtcd \ + -p 8045:8045 \ + -p 8443:8443/udp \ + -p 8443:8443/tcp \ + -v /path/to/config.toml:/rtcd/config/config.toml \ + mattermost/rtcd:latest +``` + +For a complete sample configuration file, see the [RTCD config.sample.toml](https://github.com/mattermost/rtcd/blob/master/config/config.sample.toml) in the official repository. + +### Kubernetes Deployment + +For Kubernetes deployments, use the official Helm chart: + +1. Add the Mattermost Helm repository: + + ```bash + helm repo add mattermost https://helm.mattermost.com + helm repo update + ``` + +2. Install the RTCD chart: + + ```bash + helm install mattermost-rtcd mattermost/mattermost-rtcd \ + --set ingress.enabled=true \ + --set ingress.host=rtcd.example.com \ + --set service.annotations."service\\.beta\\.kubernetes\\.io/aws-load-balancer-backend-protocol"=udp \ + --set rtcd.ice.hostOverride=rtcd.example.com + ``` + + Refer to the [RTCD Helm chart documentation](https://github.com/mattermost/mattermost-helm/tree/master/charts/mattermost-rtcd) for additional configuration options. + +## Configuration + +### RTCD Configuration File + +The RTCD service uses a TOML configuration file. Here's a comprehensive example with commonly used settings: + +```toml +[api] +# The address and port to which the HTTP API server will listen +http.listen_address = ":8045" +# Security settings for authentication +security.allow_self_registration = false +security.enable_admin = true +security.admin_secret_key = "YOUR_API_KEY" +# Configure allowed origins for CORS +security.allowed_origins = ["https://mattermost.example.com"] + +[rtc] +# The UDP address and port for media traffic +ice_address_udp = "" +ice_port_udp = 8443 +# The TCP address and port for fallback connections +ice_address_tcp = "" +ice_port_tcp = 8443 +# Public hostname or IP that clients will use to connect +ice_host_override = "rtcd.example.com" + +[logger] +# Logging configuration +enable_console = true +console_json = false +console_level = "INFO" +enable_file = true +file_json = true +file_level = "DEBUG" +file_location = "rtcd.log" + +[metrics] +# Prometheus metrics configuration +enable_prom = true +prom_port = 9090 +``` + +Key Configuration Options: + +- **api.http.listen_address**: The address and port where the RTCD HTTP API service listens +- **rtc.ice_address_udp**: The UDP address for media traffic (empty means listen on all interfaces) +- **rtc.ice_port_udp**: The UDP port for media traffic +- **rtc.ice_address_tcp**: The TCP address for fallback media traffic +- **rtc.ice_port_tcp**: The TCP port for fallback media traffic +- **rtc.ice_host_override**: The public hostname or IP address clients will use to connect to RTCD +- **api.security.allowed_origins**: List of allowed origins for CORS +- **api.security.admin_secret_key**: API key for Mattermost servers to authenticate with RTCD + +### STUN/TURN Configuration + +For clients behind strict firewalls, you may need to configure STUN/TURN servers. In the RTCD configuration file, reference your STUN/TURN servers as follows: + +```toml +[rtc] +# STUN/TURN server configuration +ice_servers = [ + { urls = ["stun:stun.example.com:3478"] }, + { urls = ["turn:turn.example.com:3478"], username = "turnuser", credential = "turnpassword" } +] +``` + +We recommend using [coturn](https://github.com/coturn/coturn) for your TURN server implementation. + +### System Tuning + +For high-volume deployments, tune your Linux system: + +1. Add the following to `/etc/sysctl.conf`: + + ```bash + # Increase UDP buffer sizes + net.core.rmem_max = 16777216 + net.core.wmem_max = 16777216 + net.core.optmem_max = 16777216 + ``` + +2. Apply the settings: + + ```bash + sudo sysctl -p + ``` + +## Validation and Testing + +After deploying RTCD, validate the installation: + +1. **Check service status and version**: + + ```bash + curl http://YOUR_RTCD_SERVER:8045/version + # Should return a JSON object with service information + # Example: {"build_hash":"abc123","build_date":"2023-01-15T12:00:00Z","build_version":"0.11.0","goVersion":"go1.20.4"} + ``` + +2. **Test UDP connectivity**: + + On the RTCD server: + + ```bash + nc -l -u -p 8443 + ``` + + On a client machine: + + ```bash + nc -v -u YOUR_RTCD_SERVER 8443 + ``` + + Type a message and hit Enter on either side. If messages are received on both ends, UDP connectivity is working. + + Note: This test must be run with the RTCD service stopped, as it binds to the same port. + + ```bash + sudo systemctl stop rtcd + ``` + +3. **Test TCP connectivity** (if enabled): + + Similar to the UDP test, but remove the `-u` flag from both commands. + +4. **Monitor metrics**: + + Refer to [Calls Metrics and Monitoring](calls-metrics-monitoring.html) for setting up Calls metrics and monitoring. + +## Horizontal Scaling + +To scale RTCD horizontally: + +1. **Deploy multiple RTCD instances**: + + Deploy multiple RTCD servers, each with their own unique IP address. + +2. **Configure DNS-based load balancing**: + + Set up a DNS record that points to multiple RTCD IP addresses: + + ```bash + rtcd.example.com. IN A 10.0.0.1 + rtcd.example.com. IN A 10.0.0.2 + rtcd.example.com. IN A 10.0.0.3 + ``` + +3. **Configure health checks**: + + Set up health checks to automatically remove unhealthy RTCD instances from DNS. + +4. **Configure Mattermost**: + + In the Mattermost System Console, set the **RTCD Service URL** to your DNS name (e.g., `rtcd.example.com`). + +The Mattermost Calls plugin will distribute calls among the available RTCD hosts. Remember that a single call will always be hosted on one RTCD instance; sessions belonging to the same call are not spread across different instances. + +## Integration with Mattermost + +Once RTCD is properly set up and validated, configure Mattermost to use it: + +1. Go to **System Console > Plugins > Calls** + +2. Enable the **Enable RTCD Service** option + +3. Set the **RTCD Service URL** to your RTCD service address (either a single server or DNS load-balanced hostname) + +4. If configured, enter the **RTCD API Key** that matches the one in your RTCD configuration + +5. Save the configuration + +6. Test by creating a new call in any Mattermost channel + +7. Verify that the call is being routed through RTCD by checking the RTCD logs and metrics + +## Other Calls Documentation + +- [Calls Overview](calls-deployment.html): Overview of deployment options and architecture +- [Calls Offloader Setup and Configuration](calls-offloader-setup.html): Setup guide for call recording and transcription +- [Calls Metrics and Monitoring](calls-metrics-monitoring.html): Guide to monitoring Calls performance using metrics and observability +- [Calls Deployment on Kubernetes](calls-kubernetes.html): Detailed guide for deploying Calls in Kubernetes environments +- [Calls Troubleshooting](calls-troubleshooting.html): Detailed troubleshooting steps and debugging techniques + +For detailed Mattermost Calls configuration options, see the [Calls Plugin Configuration Settings](plugins-configuration-settings.html#calls) documentation. \ No newline at end of file diff --git a/source/configure/calls-troubleshooting.md b/source/configure/calls-troubleshooting.md new file mode 100644 index 00000000000..bf25fbe7b12 --- /dev/null +++ b/source/configure/calls-troubleshooting.md @@ -0,0 +1,406 @@ +# Troubleshooting Mattermost Calls + +```{include} ../_static/badges/allplans-cloud-selfhosted.md +``` + +This guide provides comprehensive troubleshooting steps for Mattermost Calls, particularly focusing on the dedicated RTCD deployment model. Follow these steps to identify and resolve common issues. + +- [Common issues](#common-issues) +- [Connectivity troubleshooting](#connectivity-troubleshooting) +- [Log analysis](#log-analysis) +- [Performance issues](#performance-issues) +- [Debugging tools](#debugging-tools) +- [Advanced diagnostics](#advanced-diagnostics) + +## Common Issues + +### Calls Not Connecting + +**Symptoms**: Users can start calls but cannot connect, or calls connect but drop quickly. + +**Possible causes and solutions**: + +1. **Network connectivity issues**: + - Verify that UDP port 8443 (or your configured port) is open between clients and RTCD servers + - Ensure TCP port 8045 is open between Mattermost and RTCD servers + - Check that any load balancers are properly configured for UDP traffic + +2. **ICE configuration issues**: + - Verify the `rtc.ice_host_override` setting in RTCD configuration matches the publicly accessible hostname or IP of the RTCD server + - If this setting is incorrect, client browser console may show errors like: `com.mattermost.calls: peer error timed out waiting for rtc connection` + - Meanwhile, RTCD `trace` level logs might show internal IP addresses in ICE connection logs: + + ```json + {"timestamp":"2025-05-14 10:29:08.935 Z","level":"trace","msg":"Ping STUN from udp4 host 172.31.29.117:8443 (resolved: 172.31.29.117:8443) to udp4 host 192.168.64.1:59737 (resolved: 192.168.64.1:59737)","caller":"rtc/logger.go:54","origin":"ice/v4.(*Agent).sendBindingRequest github.com/pion/ice/v4@v4.0.3/agent.go:921"} + ``` + +3. **API connectivity**: + - Verify that Mattermost servers can reach the RTCD API endpoint + - Check that the API key is correctly configured in both Mattermost and RTCD + +4. **Plugin configuration**: + - Ensure the Calls plugin is enabled and properly configured + - Verify the RTCD service URL is correct in the System Console + +### Audio Issues + +**Symptoms**: Users can connect to calls, but audio is one-way, choppy, or not working. + +**Possible causes and solutions**: + +1. **Client permissions**: + - Ensure browser/app has microphone permissions + - Check if users are using multiple audio devices that might interfere + +2. **Network quality**: + - High latency or packet loss can cause audio issues + - Try testing with TCP fallback enabled (requires RTCD v0.11+ and Calls v0.17+) + +3. **Audio device configuration**: + - Users should verify their audio input/output settings + - Try different browsers or the desktop app + +### Call Quality Issues + +**Symptoms**: Calls connect but quality is poor, with latency, echo, or distortion. + +**Possible causes and solutions**: + +1. **Server resources**: + - Check CPU usage on RTCD servers - high CPU can cause quality issues + - Refer to the [Calls Metrics and Monitoring](calls-metrics-monitoring.html) guide for detailed instructions on monitoring and optimizing performance + - Monitor network bandwidth usage + +2. **Network congestion**: + - Check for packet loss between clients and RTCD + - Consider network QoS settings to prioritize real-time traffic + +3. **Client-side issues**: + - Browser or app limitations + - Hardware limitations (CPU, memory) + - Network congestion at the user's location + +## Connectivity Troubleshooting + +### Basic Connectivity Tests + +1. **HTTP API connectivity test**: + + Test if the RTCD API is reachable: + + ```bash + curl http://YOUR_RTCD_SERVER:8045/api/v1/health + # Expected response: {"status":"ok"} + ``` + +2. **UDP connectivity test**: + + On the RTCD server: + + ```bash + nc -l -u -p 8443 + ``` + + On a client machine: + + ```bash + nc -v -u YOUR_RTCD_SERVER 8443 + ``` + + Type a message and press Enter. If you see the message on both sides, UDP connectivity is working. + +3. **TCP fallback connectivity test**: + + Same as the UDP test, but without the `-u` flag: + + On the RTCD server: + + ```bash + nc -l -p 8443 + ``` + + On a client machine: + + ```bash + nc -v YOUR_RTCD_SERVER 8443 + ``` + +### Network Packet Analysis + +To capture and analyze network traffic: + +1. **Capture UDP traffic on the RTCD server**: + + ```bash + sudo tcpdump -n 'udp port 8443' -i any + ``` + +2. **Capture TCP API traffic**: + + ```bash + sudo tcpdump -n 'tcp port 8045' -i any + ``` + +3. **Analyze traffic patterns**: + + - Verify packets are flowing both ways + - Look for ICMP errors that might indicate firewall issues + - Check for patterns of packet loss + +4. **Use Wireshark for deeper analysis**: + + For more detailed packet inspection, capture traffic with tcpdump and analyze with Wireshark: + + ```bash + sudo tcpdump -n -w calls_traffic.pcap 'port 8443' + ``` + + Then analyze the `calls_traffic.pcap` file with Wireshark. + +### Firewall Configuration Checks + +1. **Check iptables rules** (Linux): + + ```bash + sudo iptables -L -n + ``` + + Ensure there are no rules blocking UDP port 8443 or TCP ports 8045/8443. + +2. **Check cloud provider security groups**: + + Verify that security groups or network ACLs allow: + - Inbound UDP on port 8443 from client networks + - Inbound TCP on port 8045 from Mattermost server networks + - Inbound TCP on port 8443 (if TCP fallback is enabled) + +3. **Check intermediate firewalls**: + + - Corporate firewalls might block UDP traffic + - Some networks might require TURN servers for traversal + +## Log Analysis + +### RTCD Logs + +The RTCD service logs important events and errors. Set the log level to "debug" for troubleshooting: + +1. **In the configuration file**: + + ```json + { + "log": { + "level": "debug", + "json": true + } + } + ``` + +2. **Common log patterns to look for**: + + - **Connection errors**: Look for "failed to connect" or "connection error" messages + - **ICE negotiation failures**: Look for "ICE failed" or "ICE timeout" messages + - **API authentication issues**: Look for "unauthorized" or "invalid API key" messages + +### Mattermost Logs + +Check the Mattermost server logs for Calls plugin related issues: + +1. **Enable debug logging** in System Console > Environment > Logging > File Log Level + +2. **Filter for Calls-related logs**: + + ```bash + grep -i "calls" /path/to/mattermost.log + ``` + +3. **Look for common patterns**: + + - Connection errors to RTCD + - Plugin initialization issues + - WebSocket connection problems + +### Browser Console Logs + +Instruct users to check their browser console logs: + +1. **In Chrome/Edge**: + - Press F12 to open Developer Tools + - Go to the Console tab + - Look for errors related to WebRTC, Calls, or media permissions + +2. **Specific patterns to look for**: + + - "getUserMedia" errors (microphone permission issues) + - "ICE connection" failures + - WebSocket connection errors + +## Performance Issues + +### Diagnosing High CPU Usage + +If RTCD servers show high CPU usage: + +1. **Check concurrent calls and participants**: + + - Access the Prometheus metrics endpoint to see active sessions + - Compare with the benchmark data in the documentation + +2. **Profile CPU usage** (Linux): + + ```bash + top -p $(pgrep rtcd) + ``` + + Or for detailed per-thread usage: + + ```bash + ps -eLo pid,ppid,tid,pcpu,comm | grep rtcd + ``` + +3. **Enable pprof profiling** (if needed): + + Add to your RTCD configuration: + + ```json + { + "debug": { + "pprof": true, + "pprofPort": 6060 + } + } + ``` + + Then capture a CPU profile: + + ```bash + curl http://localhost:6060/debug/pprof/profile > cpu.profile + ``` + + Analyze with: + + ```bash + go tool pprof -http=:8080 cpu.profile + ``` + +### Diagnosing Network Bottlenecks + +If you suspect network bandwidth issues: + +1. **Monitor network utilization**: + + ```bash + iftop -n + ``` + +2. **Check for packet drops**: + + ```bash + netstat -su | grep -E 'drop|error' + ``` + +3. **Verify system network buffers**: + + ```bash + sysctl -a | grep net.core.rmem + sysctl -a | grep net.core.wmem + ``` + + Ensure these match the recommended values: + + ```bash + net.core.rmem_max = 16777216 + net.core.wmem_max = 16777216 + net.core.optmem_max = 16777216 + ``` + +## Recording and Transcription Issues + +For troubleshooting calls-offloader service issues including recording and transcription problems, see the [Calls Offloader Setup and Configuration](calls-offloader-setup.html#troubleshooting) guide. + +## Debugging Tools + +### Prometheus Metrics Analysis + +Use Prometheus metrics for real-time and historical performance data: + +Import the official [Mattermost Calls dashboard](https://github.com/mattermost/mattermost-performance-assets/blob/master/grafana/mattermost-calls-performance-monitoring.json) into Grafana for visualization. + +## Advanced Diagnostics + +### WebRTC Diagnostic Commands + +For detailed WebRTC diagnostics: + +1. **Test STUN server connectivity**: + + ```bash + # Using stun-client (you may need to install it) + stun-client stun.global.calls.mattermost.com + ``` + + This should return your public IP address if STUN is working correctly. + +2. **Verify TURN server**: + + ```bash + # Using turnutils_uclient (part of coturn) + turnutils_uclient -v -s your-turn-server -u username -p password + ``` + + This tests if your TURN server is correctly configured. + +3. **Test end-to-end latency**: + + Between client locations and RTCD server: + + ```bash + ping -c 10 your-rtcd-server + ``` + + Look for consistent, low latency (<100ms ideally for voice calls). + +### Client-Side Testing Tools + +Tools to help diagnose client-side issues: + +1. **WebRTC Troubleshooter**: + + Direct users to [WebRTC Troubleshooter](https://test.webrtc.org/) for browser capability testing. + +2. **Network Quality Tests**: + + Use [Speedtest](https://www.speedtest.net/) or similar to check internet connection quality. + +3. **Browser-Specific WebRTC Info**: + + - Chrome: chrome://webrtc-internals + - Firefox: about:webrtc + +### When to Contact Support + +Consider contacting Mattermost Support when: + +1. You've tried troubleshooting steps without resolution +2. You're experiencing persistent connection failures across multiple clients +3. You notice unexpected or degraded performance despite proper configuration +4. You need help interpreting diagnostic information +5. You suspect a bug in the Calls plugin or RTCD service + +When contacting support, please include: + +- RTCD version and configuration (with sensitive information redacted) +- Mattermost server version +- Calls plugin version +- Client environments (browsers, OS versions) +- Relevant logs and diagnostic information +- Detailed description of the issue and steps to reproduce + +## Other Calls Documentation + +- [Calls Overview](calls-deployment.html): Overview of deployment options and architecture +- [RTCD Setup and Configuration](calls-rtcd-setup.html): Comprehensive guide for setting up the dedicated RTCD service +- [Calls Offloader Setup and Configuration](calls-offloader-setup.html): Setup guide for call recording and transcription +- [Calls Metrics and Monitoring](calls-metrics-monitoring.html): Guide to monitoring Calls performance using metrics and observability +- [Calls Deployment on Kubernetes](calls-kubernetes.html): Detailed guide for deploying Calls in Kubernetes environments +- Monitoring dashboards screenshots \ No newline at end of file From 3ca52ce1bf997accbca4466ae374255288aa4d77 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Wed, 11 Jun 2025 11:25:17 -0400 Subject: [PATCH 18/37] Cleaned up old rst files, fixed bad RTCD health check endpoint --- source/configure/calls-deployment.rst | 255 ----------- source/configure/calls-kubernetes.rst | 178 -------- source/configure/calls-metrics-monitoring.rst | 266 ----------- source/configure/calls-offloader-setup.rst | 404 ---------------- source/configure/calls-rtcd-setup.rst | 415 ----------------- source/configure/calls-troubleshooting.md | 5 +- source/configure/calls-troubleshooting.rst | 431 ------------------ 7 files changed, 2 insertions(+), 1952 deletions(-) delete mode 100644 source/configure/calls-deployment.rst delete mode 100644 source/configure/calls-kubernetes.rst delete mode 100644 source/configure/calls-metrics-monitoring.rst delete mode 100644 source/configure/calls-offloader-setup.rst delete mode 100644 source/configure/calls-rtcd-setup.rst delete mode 100644 source/configure/calls-troubleshooting.rst diff --git a/source/configure/calls-deployment.rst b/source/configure/calls-deployment.rst deleted file mode 100644 index d7379ca87ce..00000000000 --- a/source/configure/calls-deployment.rst +++ /dev/null @@ -1,255 +0,0 @@ -Calls self-hosted deployment -============================ - -.. include:: ../_static/badges/allplans-cloud-selfhosted.rst - :start-after: :nosearch: - -This document provides an overview of Mattermost Calls deployment options for self-hosted environments, including deployment architectures, key requirements, and important considerations. - -.. toctree:: - :maxdepth: 1 - :hidden: - - calls-rtcd-setup - calls-offloader-setup - calls-metrics-monitoring - calls-kubernetes - calls-troubleshooting - -Quick Links ----------- - -For detailed information on specific topics, please refer to these specialized guides: - -- `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service -- `Calls Offloader Setup and Configuration `__: Comprehensive guide for setting up the calls-offloader service for recording and transcription -- `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques -- `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability -- `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments - -About Mattermost Calls ---------------------- - -Mattermost Calls provides integrated audio calling and screen sharing capabilities within Mattermost channels. It's built on WebRTC technology and can be deployed either: - -1. **Integrated mode**: Built into the Calls plugin (simpler, suitable for smaller deployments) -2. **RTCD mode**: Using a dedicated service for improved performance and scalability (recommended for production environments) - -Terminology ------------ - -- `WebRTC `__: The set of protocols on which calls are built -- **RTC**: Real-Time Connection channel used for media (audio/video/screen) -- **WS**: WebSocket connection used for signaling and connection setup -- **SFU**: Selective Forwarding Unit, routes media between participants -- `NAT `__: Network Address Translation for mapping IP addresses -- `STUN `__: Protocol used by WebRTC clients to help traverse NATs -- `TURN `__: Protocol to relay media for clients behind strict firewalls - -Key Components -------------- - -- **Calls plugin**: The main plugin that enables calls functionality. Installed by default in Mattermost self-hosted deployments. -- **RTCD service**: Optional dedicated service for offloading media processing (Enterprise feature). Typically deployed to dedicated servers or containers. See `RTCD Setup and Configuration `__ for details. -- **calls-offloader**: Service for call recording and transcription (if enabled). Typically deployed to dedicated servers. See `Calls Offloader Setup and Configuration `__ for setup and troubleshooting details. - -Network Requirements ------------------- - -The following network connectivity is required: - -+-------------------+--------+-----------------+-------------------------+------------------------+ -| Service | Ports | Protocols | Source | Target | -+===================+========+=================+=========================+========================+ -| Calls plugin API | 80,443 | TCP (incoming) | Mattermost clients | Mattermost server | -+-------------------+--------+-----------------+-------------------------+------------------------+ -| RTC media | 8443 | UDP (incoming) | Mattermost clients | Mattermost or RTCD | -+-------------------+--------+-----------------+-------------------------+------------------------+ -| RTC media | 8443 | TCP (incoming) | Mattermost clients | Mattermost or RTCD | -+-------------------+--------+-----------------+-------------------------+------------------------+ -| RTCD API | 8045 | TCP (incoming) | Mattermost server | RTCD service | -+-------------------+--------+-----------------+-------------------------+------------------------+ -| STUN | 3478 | UDP (outgoing) | Mattermost or RTCD | STUN servers | -+-------------------+--------+-----------------+-------------------------+------------------------+ - -For complete network requirements, see the `RTCD Setup and Configuration `__ guide. - -Limitations ------------ - -- All Mattermost customers can start, join, and participate in 1:1 audio calls with optional screen sharing. -- For group calls up to 50 concurrent users, Mattermost Enterprise, Professional, or Mattermost Cloud is required. -- Enterprise customers can also `record calls `__, enable `live text captions `__ during calls, and `transcribe recorded calls `__. We recommend that Enterprise self-hosted customers looking for group calls beyond 50 concurrent users consider using the `dedicated RTCD service <#when-to-use-rtcd>`__. -- For Mattermost self-hosted deployments, System admins need to enable and configure the plugin `using the System Console `__. The default maximum number of participants is unlimited; however, we recommend a maximum of 50 participants per call. Maximum call participants is configurable by going to **System Console > Plugin Management > Calls > Max call participants**. Call participant limits greatly depends on instance resources. For more details, refer to the `Performance Considerations <#performance-considerations>`__ section below. - -Configuration -------------- - -For Mattermost self-hosted customers, the calls plugin is pre-packaged, installed, and enabled. Configuration to allow end-users to use it can be found in the `System Console `__. - -Deployment Architecture Options ------------------------------ - -Mattermost Calls can be deployed in several configurations: - -Single Instance Deployments -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Integrated Mode -^^^^^^^^^^^^^ - -The WebRTC service runs within the Calls plugin on the Mattermost server. This is the default mode when first installing the plugin on a single Mattermost instance setup. The WebRTC service is integrated in the plugin itself and runs alongside the Mattermost server. - -.. image:: ../images/calls-deployment-image3.png - :alt: Integrated configuration model of a single instance - :width: 600px - -RTCD Mode -^^^^^^^^ - -A dedicated RTCD service handles media routing, reducing load on the Mattermost server. - -.. image:: ../images/calls-deployment-image7.png - :alt: Web RTC deployment configuration - :width: 600px - -High Availability Deployments -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Clustered Mode -^^^^^^^^^^^^ - -This is the default mode when running the plugin in a high availability cluster-based deployment. Every Mattermost node will run an instance of the plugin that includes a WebRTC service. Calls are distributed across all available nodes through the existing load-balancer: a call is hosted on the instance where the initiating websocket connection (first client to join) is made. A single call will be hosted on a single cluster node. - -.. image:: ../images/calls-deployment-image5.png - :alt: Clustered calls deployment - :width: 600px - -RTCD with High Availability -^^^^^^^^^^ - -Dedicated RTCD services handle media routing for high availability. - -.. image:: ../images/calls-deployment-image2.png - :alt: RTCD deployment with high availability - :width: 600px - -Kubernetes Deployments -~~~~~~~~~~~~~~~~~~~~ - -RTCD is the only officially supported approach for Kubernetes deployments. For detailed information on deploying Mattermost Calls in Kubernetes environments, including Helm chart configurations, resource requirements, and scaling considerations, see the `Calls Deployment on Kubernetes `__ guide. - -When to Use RTCD --------------- - -The dedicated RTCD service (available with Enterprise license) is recommended for: - -- **Production environments**: Isolates call traffic from other Mattermost services -- **Performance optimization**: Dedicated service tuned for real-time media -- **Scalability**: Add RTCD instances as call volume grows -- **Call stability**: Calls continue even if Mattermost server needs to restart -- **Kubernetes deployments**: Required for officially supported Kubernetes deployments - -For detailed RTCD setup instructions, see the `RTCD Setup and Configuration `__ guide. - -Call Recording and Transcription ------------------------------- - -For call recording and transcription, you need to: - -1. Deploy the ``calls-offloader`` service -2. Configure the service URL in the System Console -3. Enable call recordings and/or transcriptions in the plugin settings - -Air-Gapped Deployments ---------------------- - -Mattermost Calls can function in air-gapped environments. Exposing Calls to the public internet is only necessary when users need to connect from outside the local network, and no existing method supports that connection. In such setups: - -- Users should connect from within the private/local network. This can be done on-premises, through a VPN, or via virtual machines. -- Configuring a STUN server is unnecessary, as all connections occur within the local network. -- The ICE Host Override configuration setting can be optionally set with a local IP address (e.g., 192.168.1.45), depending on the specific network configuration and topology. - -Performance Considerations ------------------------- - -Calls performance primarily depends on: - -- **CPU resources**: More participants require more processing power -- **Network bandwidth**: Both incoming and outgoing traffic increases with participant count -- **Active speakers**: Unmuted participants require significantly more resources - -For detailed performance metrics, benchmarks, and monitoring guidance, see the `Calls Metrics and Monitoring `__ guide. - -Frequently Asked Questions ------------------------- - -**Is calls traffic encrypted?** -Yes, using WebRTC security standards (DTLS/SRTP). Traffic is encrypted in transit. - -**Are there any third-party services involved?** -Only a Mattermost STUN server (``stun.global.calls.mattermost.com``) is used by default. This can be removed if you set the ICE Host Override configuration. - -**Is using UDP a requirement?** -UDP is recommended protocol to serve real-time media as it allows for the lowest latency between peers, but TCP fallback is supported since plugin version 0.17 and RTCD version 0.11. - -If clients are unable to connect using UDP (due to limitations or strict firewalls), you have a few options: - -- Since plugin version 0.17 and `rtcd` version 0.11 the RTC service will listen for TCP connections in addition to UDP ones. If configured correctly (e.g. using commonly allowed ports such as 80 or 443) it's possible to have clients connect directly through TCP when unable to do it through the preferred UDP channel. - -- Run calls through an external TURN server that listens on TCP and relays all media traffic between peers. However, this is a sub-optimal solution that should be avoided if possible as it will introduce extra latency along with added infrastructural cost. - -**Do I need a TURN server?** -Only if clients are behind restrictive firewalls that block UDP. We recommend (and officially support) `coturn `__ if needed. - -**Can RTCD traffic be kept internal?** -Yes, and it's recommended. Only the media ports need to be accessible to end-users. - -**How will this work with an existing reverse proxy sitting in front of Mattermost?** - -Generally clients should connect directly to either Mattermost or, if deployed, the dedicated ``rtcd`` service through the configured UDP port. However, it's also possible to route the traffic through an existing load balancer as long as this has support for routing the UDP protocol (e.g. nginx). Of course this will require additional configuration and potential changes to how the plugin is run as it won't be possible to load balance the UDP flow across multiple instances like it happens for HTTP. - -**Do calls require a dedicated server to work or can they run alongside Mattermost?** - -The plugin can function in different modes. By default calls are handled completely by the plugin which runs as part of Mattermost. It's also possible to use a dedicated service to offload the computational and bandwidth costs and scale further (Enterprise only). - -See RTCD Setup and Configuration for more details on the dedicated RTCD service. - -**Can the traffic between Mattermost and ``rtcd`` be kept internal or should it be opened to the public?** - -When possible, it's recommended to keep communication between the Mattermost cluster and the dedicated ``rtcd`` service under the same private network as this can greatly simplify deployment and security. There's no requirement to expose ``rtcd``'s HTTP API to the public internet. - -**Can Calls be rolled out on a per-channel basis?** - -.. include:: ../_static/badges/selfhosted-only.rst - :start-after: :nosearch: - -Yes. Mattermost system admins running self-hosted deployments can enable or disable call functionality per channel. Once `test mode `__ is enabled for Mattermost Calls: - -1. **Navigate to the channel** where you want to enable or disable Calls -2. **Access the channel menu** by clicking the channel name at the top of the channel -3. **Select the Calls option** from the dropdown menu: - - Select **Enable calls** for each channel where you want Calls enabled - - Select **Disable calls** for all channels where you want Calls disabled - -.. image:: ../images/calls-channel-enable-disable.png - :alt: Channel menu showing Enable/Disable calls options - :width: 400px - -Once Calls is enabled for specific channels, users can start making calls in those channels. - -.. note:: - When `test mode `__ is disabled for Mattermost Calls, users in any Mattermost channel can make a call. - -Troubleshooting ---------------- - -For comprehensive troubleshooting steps and debugging techniques, please refer to the `Calls Troubleshooting `__ guide. - -Next Steps ---------- - -1. For detailed setup instructions, see `RTCD Setup and Configuration `__ -2. For monitoring guidance, see `Calls Metrics and Monitoring `__ -3. If you encounter issues, see `Calls Troubleshooting `__ -4. For Kubernetes deployments, see `Calls Deployment on Kubernetes `__ \ No newline at end of file diff --git a/source/configure/calls-kubernetes.rst b/source/configure/calls-kubernetes.rst deleted file mode 100644 index b9936db5cb0..00000000000 --- a/source/configure/calls-kubernetes.rst +++ /dev/null @@ -1,178 +0,0 @@ -Calls deployment on Kubernetes -=========================== - -.. include:: ../_static/badges/allplans-cloud-selfhosted.rst - :start-after: :nosearch: - -This guide provides detailed information for deploying Mattermost Calls on Kubernetes environments. - -Overview --------- - -Mattermost Calls has been designed to integrate well with Kubernetes to offer improved scalability and control over the deployment. For Kubernetes deployments, the RTCD service is strongly recommended and is the only officially supported approach. - -Architecture ------------ - -.. image:: ../images/calls-deployment-kubernetes.png - :alt: Calls deployed in a Kubernetes cluster - :width: 600px - -This diagram shows how the RTCD standalone service can be deployed in a Kubernetes cluster. In this architecture: - -1. Calls traffic is handled by dedicated RTCD pods -2. RTCD services are exposed through load balancers -3. Scaling is managed through Kubernetes deployment configurations -4. Call recording and transcription is handled by the calls-offloader service (see `Calls Offloader Setup and Configuration `__) - -If Mattermost isn't already deployed in your Kubernetes cluster and you want to use this deployment type, visit the `Kubernetes operator guide `__. - -Helm Chart Deployment -------------------- - -The recommended way to deploy Calls-related components in a Kubernetes environment is to use the officially provided Helm charts: - -RTCD Helm Chart -^^^^^^^^^^^^^ - -The RTCD Helm chart deploys the RTCD service needed for call media handling: - -.. code-block:: bash - - helm repo add mattermost https://helm.mattermost.com - helm repo update - - helm install mattermost-rtcd mattermost/mattermost-rtcd \ - --set ingress.enabled=true \ - --set ingress.host=rtcd.example.com \ - --set service.annotations."service\\.beta\\.kubernetes\\.io/aws-load-balancer-backend-protocol"=udp \ - --set rtcd.ice.hostOverride=rtcd.example.com - -For complete configuration options, see the `RTCD Helm chart documentation `__. - -Calls-Offloader Helm Chart -^^^^^^^^^^^^^^^^^^^^^^^^ - -If you need call recording and transcription capabilities, deploy the calls-offloader service: - -.. code-block:: bash - - helm install mattermost-calls-offloader mattermost/mattermost-calls-offloader \ - --set ingress.enabled=true \ - --set ingress.host=calls-offloader.example.com - -For complete configuration options, see the `Calls-Offloader Helm chart documentation `__. - -Kubernetes-Specific Configuration -------------------------------- - -Network Configuration -^^^^^^^^^^^^^^^^^^ - -For Kubernetes deployments, you need to ensure: - -1. UDP traffic is properly routed to RTCD pods (for media) -2. TCP traffic can reach both the Mattermost pods and RTCD pods -3. Load balancers are properly configured to handle UDP traffic -4. Network policies allow the required communications between services - -Recommended annotations for AWS environments: - -.. code-block:: yaml - - service.beta.kubernetes.io/aws-load-balancer-backend-protocol: udp - service.beta.kubernetes.io/aws-load-balancer-type: nlb - -Resource Requirements -^^^^^^^^^^^^^^^^^^ - -For optimal performance in Kubernetes environments: - -1. **CPU**: At least 2 CPU cores per RTCD pod -2. **Memory**: At least 1GB RAM per RTCD pod -3. **Network**: Sufficient bandwidth for expected call volume (see benchmarks) - -We recommend setting resource limits and requests in your deployment: - -.. code-block:: yaml - - resources: - requests: - cpu: 1000m - memory: 1Gi - limits: - cpu: 2000m - memory: 2Gi - -Scaling Considerations -^^^^^^^^^^^^^^^^^^ - -Horizontal scaling of RTCD pods is possible, but remember: - -1. Each call is hosted entirely on a single RTCD pod -2. DNS-based load balancing should be used to distribute calls among pods -3. Health checks should ensure that only healthy pods receive new calls -4. Calls remain on their assigned pod for their entire duration - -Limitations -^^^^^^^^^^ - -Due to the inherent complexities of hosting a WebRTC service, some limitations apply when deploying Calls in a Kubernetes environment. - -One key requirement is that each ``rtcd`` process must live in a dedicated Kubernetes node. This is necessary to forward the data correctly while allowing for horizontal scaling. Data should generally not go through a standard ingress but directly to the pod running the ``rtcd`` process. - -The general recommendation is to expose one external IP address per ``rtcd`` instance (Kubernetes node). This makes it simpler to scale as the application is able to detect its own external address (through STUN) and advertise it to clients to achieve connectivity with minimal configuration. - -If, for some reason, exposing multiple IP addresses is not possible in your environment, port mapping (NAT) can be used. In this scenario different ports are used to map the respective ``rtcd`` nodes behind the single external IP. Example: - -.. code-block:: text - - EXT_IP:8443 -> rtcdA:8443 - EXT_IP:8444 -> rtcdB:8443 - EXT_IP:8445 -> rtcdC:8443 - -This case requires a couple of extra configurations: - -* NAT mappings need to be in place for every ``rtcd`` node. This is usually done at the ingress point (e.g., ELB, NLB, etc). - -* The ``RTCD_RTC_ICEHOSTPORTOVERRIDE`` config should be used to pass a full mapping of node IPs and their respective port. - - * Example: ``RTCD_RTC_ICEHOSTPORTOVERRIDE=rtcdA_IP/8443,rtcdB_IP/8444,rtcdC_IP/8445`` - -* The ``RTCD_RTC_ICEHOSTOVERRIDE`` should be used to set the external IP address. - -.. note:: - One option to limit these static mappings is to reduce the size of the local subnet (e.g., to ``/29``). - -Monitoring and Metrics -^^^^^^^^^^^^^^^^^^^ - -We recommend deploying Prometheus and Grafana alongside your Calls deployment: - -1. Configure Prometheus to scrape metrics from both Mattermost and RTCD pods -2. Import the official Mattermost Calls dashboard to Grafana -3. Set up alerts for CPU usage, connection failures, and error rates - -For detailed information on metrics collection and monitoring, see the `Calls Metrics and Monitoring `__ guide. - -Troubleshooting --------------- - -For Kubernetes-specific troubleshooting: - -1. Check pod logs: `kubectl logs -f deployment/mattermost-rtcd` -2. Verify service connectivity: `kubectl port-forward service/mattermost-rtcd 8045:8045` -3. Ensure UDP traffic is properly routed through your ingress/load balancer -4. Verify network policies allow required communication paths - -For detailed troubleshooting steps, see the `Calls Troubleshooting `__ guide. - -Other Calls Documentation ----------------- - -- `Calls Overview `__: Overview of deployment options and architecture -- `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service -- `Calls Offloader Setup and Configuration `__: Setup guide for call recording and transcription -- `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability -- `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques -3. If you encounter issues, see `Calls Troubleshooting `__ \ No newline at end of file diff --git a/source/configure/calls-metrics-monitoring.rst b/source/configure/calls-metrics-monitoring.rst deleted file mode 100644 index 81d29f74e99..00000000000 --- a/source/configure/calls-metrics-monitoring.rst +++ /dev/null @@ -1,266 +0,0 @@ -Calls Metrics and Monitoring -========================= - -.. include:: ../_static/badges/allplans-cloud-selfhosted.rst - :start-after: :nosearch: - -This guide provides detailed information on monitoring Mattermost Calls performance and health through metrics and observability tools. Effective monitoring is essential for maintaining optimal call quality and quickly addressing any issues that arise. - -- `Metrics overview <#metrics-overview>`__ -- `Setting up monitoring <#setting-up-monitoring>`__ -- `Key metrics to monitor <#key-metrics-to-monitor>`__ -- `Grafana dashboards <#grafana-dashboards>`__ -- `Alerting recommendations <#alerting-recommendations>`__ -- `Performance baselines <#performance-baselines>`__ - -Metrics Overview --------------- - -Mattermost Calls provides metrics through Prometheus for both the Calls plugin and the RTCD service. These metrics help track: - -- Active call sessions and participants -- Media track statistics -- Connection states and errors -- Resource utilization (CPU, memory, network) -- WebSocket connections and events - -The metrics are exposed through HTTP endpoints: - -- **Calls Plugin**: ``/plugins/com.mattermost.calls/metrics`` -- **RTCD Service**: ``/metrics`` (default) or a configured endpoint - -Setting Up Monitoring -------------------- - -Prerequisites -^^^^^^^^^^^ - -To monitor Calls metrics, you'll need: - -1. **Prometheus**: For collecting and storing metrics -2. **Grafana**: For visualizing metrics (optional but recommended) - -Installing Prometheus -^^^^^^^^^^^^^^^^^^ - -1. **Download and install Prometheus**: - - Visit the `Prometheus download page `__ for installation instructions. - -2. **Configure Prometheus** to scrape metrics from all Calls-related services: - - Complete ``prometheus.yml`` configuration for Calls monitoring: - - .. code-block:: yaml - - global: - scrape_interval: 15s - evaluation_interval: 15s - - scrape_configs: - - job_name: 'prometheus' - static_configs: - - targets: ['PROMETHEUS_IP:9090'] - - - job_name: 'mattermost' - metrics_path: /metrics - static_configs: - - targets: ['MATTERMOST_SERVER_IP:8067'] - - - job_name: 'calls-plugin' - metrics_path: /plugins/com.mattermost.calls/metrics - static_configs: - - targets: ['MATTERMOST_SERVER_IP:8067'] - labels: - service_name: 'calls-plugin' - - - job_name: 'rtcd' - metrics_path: /metrics - static_configs: - - targets: ['RTCD_SERVER_IP:8045'] - labels: - service_name: 'rtcd' - - - job_name: 'rtcd-node-exporter' - metrics_path: /metrics - static_configs: - - targets: ['RTCD_SERVER_IP:9100'] - labels: - service_name: 'rtcd' - - - job_name: 'calls_offloader-node-exporter' - metrics_path: /metrics - static_configs: - - targets: ['CALLS_OFFLOADER_SERVER_IP:9100'] - labels: - service_name: 'offloader' - - Replace the placeholder IP addresses with your actual server addresses: - - - ``MATTERMOST_SERVER_IP``: IP address of your Mattermost server - - ``RTCD_SERVER_IP``: IP address of your RTCD server - - ``CALLS_OFFLOADER_SERVER_IP``: IP address of your calls-offloader server (if deployed) - - ``PROMETHEUS_IP``: IP address of your Prometheus server - - **Note**: The configuration above uses the default ports (RTCD: ``8045``, Mattermost metrics: ``8067``, etc.). Adjust these ports in ``prometheus.yml`` if you have customized them. - .. important:: - **Metrics Path**: Ensure the metrics paths are correct. The RTCD service exposes metrics at ``/metrics`` by default, and the Calls plugin at ``/plugins/com.mattermost.calls/metrics``. - - .. important:: - **Metrics Configuration Notice**: Use the ``service_name`` labels as shown in the configuration above. These labels help organize metrics in dashboards and enable proper service identification. - - .. note:: - - **node_exporter**: Optional but recommended for system-level metrics (CPU, memory, disk, network). See `node_exporter setup guide `__ for installation instructions. - - **calls-offloader**: Only needed if you have call recording/transcription enabled - -Installing Grafana -^^^^^^^^^^^^^^^ - -1. **Download and install Grafana**: - - Visit the `Grafana download page `__ for installation instructions. - -2. **Configure Grafana** to use Prometheus as a data source: - - - Add a new data source in Grafana - - Select Prometheus as the type - - Enter the URL of your Prometheus server - - Test and save the configuration - -3. **Import the Mattermost Calls dashboard**: - - - Navigate to Dashboards > Import in Grafana - - Enter dashboard ID: ``23225`` or use the direct link: `Mattermost Calls Performance Monitoring `__ - - Select your Prometheus data source, and enter values for the - - Confirm the port used for RTCD metrics (default is ``8045``), and the port used for the Calls plugin metrics (default is ``8067``) - - Click Import to add the dashboard to your Grafana instance - - .. note:: - The dashboard is also available as JSON source from the `Mattermost performance assets repository `__ for manual import or customization. - - -Key Metrics to Monitor --------------------- - -RTCD Metrics -^^^^^^^^^^ - -Process Metrics -"""""""""""""" - -These metrics help monitor the health and resource usage of the RTCD process: - -- ``rtcd_process_cpu_seconds_total``: Total CPU time spent -- ``rtcd_process_open_fds``: Number of open file descriptors -- ``rtcd_process_max_fds``: Maximum number of file descriptors -- ``rtcd_process_resident_memory_bytes``: Memory usage in bytes -- ``rtcd_process_virtual_memory_bytes``: Virtual memory used - -**Interpretation**: - -- High CPU usage (>70%) may indicate the need for additional RTCD instances -- Steadily increasing memory usage might indicate a memory leak -- High number of file descriptors could indicate connection handling issues - -WebRTC Connection Metrics -""""""""""""""""""""""" - -These metrics track the WebRTC connections and media flow: - -- ``rtcd_rtc_conn_states_total{state="X"}``: Count of connections in different states -- ``rtcd_rtc_errors_total{type="X"}``: Count of RTC errors by type -- ``rtcd_rtc_rtp_tracks_total{direction="X"}``: Count of RTP tracks (incoming/outgoing) -- ``rtcd_rtc_sessions_total``: Total number of active RTC sessions - -**Interpretation**: - -- Increasing error counts may indicate connectivity or configuration issues -- Track by state to see if connections are failing to establish or dropping -- Larger track counts require proportionally more CPU and bandwidth - -WebSocket Metrics -""""""""""""""" - -These metrics track the signaling channel: - -- ``rtcd_ws_connections_total``: Total number of active WebSocket connections -- ``rtcd_ws_messages_total{direction="X"}``: Count of WebSocket messages (sent/received) - -**Interpretation**: - -- Connection count should match expected participant numbers -- Unusually high message counts might indicate protocol issues -- Connection drops might indicate network issues - -Calls Plugin Metrics -^^^^^^^^^^^^^^^^^ - -Similar metrics are available for the Calls plugin with the following prefixes: - -- Process metrics: ``mattermost_plugin_calls_process_*`` -- WebRTC connection metrics: ``mattermost_plugin_calls_rtc_*`` -- WebSocket metrics: ``mattermost_plugin_calls_websocket_*`` -- Store metrics: ``mattermost_plugin_calls_store_ops_total`` - -Performance Baselines ------------------- - -The following performance benchmarks provide baseline metrics for RTCD deployments under various load conditions and configurations. - -**Deployment specifications** - -- 1x r6i.large nginx proxy -- 3x c5.large MM app nodes (HA) -- 2x db.x2g.xlarge RDS Aurora MySQL v8 (one writer, one reader) -- 1x (c7i.xlarge, c7i.2xlarge, c7i.4xlarge) RTCD -- 2x c7i.2xlarge load-test agents - -**App specifications** - -- Mattermost v9.6 -- Mattermost Calls v0.28.0 -- RTCD v0.16.0 -- load-test agent v0.28.0 - -**Media specifications** - -- Speech sample bitrate: 80Kbps -- Screen sharing sample bitrate: 1.6Mbps - -**Results** - -Below are the detailed benchmarks based on internal performance testing: - -+-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ -| Calls | Users/call | Unmuted/call | Screen sharing | CPU (avg) | Memory (avg) | Bandwidth (in/out) | Instance (EC2) | -+=======+============+==============+================+===========+==============+====================+================+ -| 100 | 8 | 2 | no | 60% | 0.5GB | 22Mbps / 125Mbps | c6i.xlarge | -+-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ -| 100 | 8 | 2 | no | 30% | 0.5GB | 22Mbps / 125Mbps | c6i.2xlarge | -+-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ -| 100 | 8 | 2 | yes | 86% | 0.7GB | 280Mbps / 2.2Gbps | c6i.2xlarge | -+-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ -| 10 | 50 | 2 | no | 35% | 0.3GB | 5.25Mbps / 86Mbps | c6i.xlarge | -+-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ -| 10 | 50 | 2 | no | 16% | 0.3GB | 5.25Mbps / 86Mbps | c6i.2xlarge | -+-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ -| 10 | 50 | 2 | yes | 90% | 0.3GB | 32Mbps / 1.33Gbps | c6i.xlarge | -+-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ -| 10 | 50 | 2 | yes | 45% | 0.3GB | 32Mbps / 1.33Gbps | c6i.2xlarge | -+-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ -| 5 | 200 | 2 | no | 65% | 0.6GB | 8.2Mbps / 180Mbps | c6i.xlarge | -+-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ -| 5 | 200 | 2 | no | 30% | 0.6GB | 8.2Mbps / 180Mbps | c6i.2xlarge | -+-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ -| 5 | 200 | 2 | yes | 90% | 0.7GB | 31Mbps / 2.2Gbps | c6i.2xlarge | -+-------+------------+--------------+----------------+-----------+--------------+--------------------+----------------+ - -Other Calls Documentation ----------------- - -- `Calls Overview `__: Overview of deployment options and architecture -- `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service -- `Calls Offloader Setup and Configuration `__: Setup guide for call recording and transcription -- `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments -- `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques - -Configure Prometheus storage accordingly to balance disk usage with retention needs. \ No newline at end of file diff --git a/source/configure/calls-offloader-setup.rst b/source/configure/calls-offloader-setup.rst deleted file mode 100644 index 195020f787f..00000000000 --- a/source/configure/calls-offloader-setup.rst +++ /dev/null @@ -1,404 +0,0 @@ -Calls Offloader Setup and Configuration -======================================= - -.. include:: ../_static/badges/allplans-cloud-selfhosted.rst - :start-after: :nosearch: - -.. raw:: html - -

- -This guide provides detailed instructions for setting up, configuring, and validating the Mattermost calls-offloader service used for call recording and transcription features. - -- `Overview <#overview>`__ -- `Prerequisites <#prerequisites>`__ -- `Installation and deployment <#installation-and-deployment>`__ -- `Configuration <#configuration>`__ -- `Validation and testing <#validation-and-testing>`__ -- `Integration with Mattermost <#integration-with-mattermost>`__ -- `Troubleshooting <#troubleshooting>`__ - -Overview --------- - -The calls-offloader service is a dedicated microservice that handles resource-intensive tasks for Mattermost Calls, including: - -- **Call recording**: Captures audio and screen sharing content from calls -- **Call transcription**: Provides automated transcription of recorded calls -- **Live captions** (Experimental): Real-time transcription during active calls - -By offloading these tasks to a dedicated service, the main Mattermost server and RTCD service can focus on core functionality while maintaining optimal performance. - -Prerequisites -------------- - -Before deploying calls-offloader, ensure you have: - -- A Mattermost Enterprise license -- A properly configured Mattermost Calls deployment (either integrated or with RTCD) -- Docker installed and running (for Docker-based job execution) -- Sufficient storage space for recordings (see `Storage Requirements <#storage-requirements>`__) -- A server or container environment with adequate resources - -System Requirements -^^^^^^^^^^^^^^^^^ - -For detailed system requirements and performance recommendations, refer to the `calls-offloader performance documentation `__. - -Storage Requirements -^^^^^^^^^^^^^^^^^^ - -Call recordings can consume significant storage space: - -- Audio-only recordings: ~1MB per minute per participant -- Screen sharing recordings: ~10-50MB per minute depending on content - -Installation and Deployment ---------------------------- - -Bare Metal or VM Deployment -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -1. Download the latest release from the `calls-offloader GitHub repository `__ - -2. Create the necessary directories: - - .. code-block:: bash - - sudo mkdir -p /opt/calls-offloader/data/db - sudo useradd --system --home /opt/calls-offloader calls-offloader - sudo chown -R calls-offloader:calls-offloader /opt/calls-offloader - -3. Create a configuration file (``/opt/calls-offloader/config.toml``): - - .. code-block:: toml - - [api] - http.listen_address = ":4545" - http.tls.enable = false - http.tls.cert_file = "" - http.tls.cert_key = "" - security.allow_self_registration = true - security.enable_admin = true - security.admin_secret_key = "changeme" - security.session_cache.expiration_minutes = 1440 - - [store] - data_source = "/opt/calls-offloader/data/db" - - [jobs] - api_type = "docker" - max_concurrent_jobs = 2 - failed_jobs_retention_time = "7d" - image_registry = "mattermost" - - [logger] - enable_console = true - console_json = false - console_level = "INFO" - enable_file = true - file_json = true - file_level = "INFO" - file_location = "/opt/calls-offloader/calls-offloader.log" - enable_color = true - -4. Create a systemd service file (``/etc/systemd/system/calls-offloader.service``): - - .. code-block:: ini - - [Unit] - Description=Mattermost Calls Offloader Service - After=network.target docker.service - Requires=docker.service - - [Service] - Type=simple - User=calls-offloader - WorkingDirectory=/opt/calls-offloader - ExecStart=/opt/calls-offloader/calls-offloader --config /opt/calls-offloader/config.toml - Restart=always - RestartSec=10 - LimitNOFILE=65536 - - [Install] - WantedBy=multi-user.target - -5. Enable and start the service: - - .. code-block:: bash - - sudo systemctl daemon-reload - sudo systemctl enable calls-offloader - sudo systemctl start calls-offloader - -6. Check the service status: - - .. code-block:: bash - - sudo systemctl status calls-offloader - -7. Verify the service is responding: - - .. code-block:: bash - - curl http://localhost:4545/version - # Example output: - # {"buildDate":"2025-03-10 19:13","buildVersion":"v0.9.2","buildHash":"a4bd418","goVersion":"go1.23.6"} - - -Configuration -------------- - -API Configuration -^^^^^^^^^^^^^^^ - -The API section controls how the service accepts requests: - -- **http.listen_address**: The address and port where the service listens (default: ``:4545``) -- **http.tls.enable**: Whether to use TLS encryption for the API -- **security.allow_self_registration**: Allow clients to self-register for job management -- **security.enable_admin**: Enable admin functionality -- **security.admin_secret_key**: Secret key for admin authentication (change from default!) - -Store Configuration -^^^^^^^^^^^^^^^^^ - -Controls persistent data storage: - -- **data_source**: Path to directory for storing job metadata and state - -Jobs Configuration -^^^^^^^^^^^^^^^^ - -Controls job processing behavior: - -- **api_type**: Job execution backend (``docker`` or ``kubernetes``) -- **max_concurrent_jobs**: Maximum number of simultaneous recording/transcription jobs -- **failed_jobs_retention_time**: How long to keep failed job data before cleanup -- **image_registry**: Docker registry for job runner images (typically ``mattermost``) - -Logger Configuration -^^^^^^^^^^^^^^^^^^ - -Controls logging output: - -- **enable_console**: Log to console output -- **console_json**: Use JSON format for console logs -- **console_level**: Log level for console (DEBUG, INFO, WARN, ERROR) -- **enable_file**: Log to file -- **file_location**: Path to log file -- **enable_color**: Use colored output for console logs - -Private Network Configuration -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When the Mattermost deployment is running in a private network, additional configuration may be necessary for the jobs spawned by the calls-offloader service to reach the Mattermost server. - -In such cases, you can override the site URL used by recorder jobs or transcriber jobs to connect to Mattermost by setting the following environment variables on the Mattermost server: - -- **MM_CALLS_RECORDER_SITE_URL**: Override the site URL used by recording jobs -- **MM_CALLS_TRANSCRIBER_SITE_URL**: Override the site URL used by transcription jobs - -Example configuration: - -Create or edit the Mattermost environment file (``/opt/mattermost/config/mattermost.environment``): - -.. code-block:: bash - - MM_CALLS_RECORDER_SITE_URL="http://internal-mattermost-server:8065" - MM_CALLS_TRANSCRIBER_SITE_URL="http://internal-mattermost-server:8065" - -Then ensure your Mattermost systemd service references this environment file: - -.. code-block:: ini - - [Unit] - Description=Mattermost - After=network.target - - [Service] - Type=notify - EnvironmentFile=/opt/mattermost/config/mattermost.environment - ExecStart=/opt/mattermost/bin/mattermost - TimeoutStartSec=3600 - KillMode=mixed - Restart=always - RestartSec=10 - WorkingDirectory=/opt/mattermost - User=mattermost - Group=mattermost - - [Install] - WantedBy=multi-user.target - -This is particularly useful when: - -- The calls-offloader service runs in a different network segment than clients -- Internal DNS resolution differs from external URLs -- You need to use internal load balancer endpoints for job communication - -Validation and Testing ---------------------- - -After deploying calls-offloader, validate the installation: - -1. **Check service status**: - - .. code-block:: bash - - # For systemd - sudo systemctl status calls-offloader - - -2. **Test API connectivity**: - - **From the calls-offloader server (localhost test)**: - - .. code-block:: bash - - curl http://localhost:4545/version - # Should return version information - # Example: {"buildDate":"2025-03-10 19:13","buildVersion":"v0.9.2","buildHash":"a4bd418","goVersion":"go1.23.6"} - - **From the Mattermost server**: - - .. code-block:: bash - - curl http://YOUR_CALLS_OFFLOADER_SERVER:4545/version - # Should return the same version information - # This confirms network connectivity from Mattermost to calls-offloader - - If the localhost test works but the Mattermost server test fails, check: - - - Firewall rules or SELinux policies on the calls-offloader server (port 4545 must be accessible) - - Network connectivity between Mattermost and calls-offloader servers - - calls-offloader service binding configuration (ensure it's not bound to localhost only) - -3. **Verify Docker integration** (if using docker api_type): - - .. code-block:: bash - - # Check that system user running calls-offloader can access Docker - sudo -u calls-offloader docker ps - -Integration with Mattermost ---------------------------- - -Once calls-offloader is properly set up and validated, configure Mattermost to use it: - -1. Go to **System Console > Plugins > Calls** - -2. In the **Job Service** section: - - - Set **Job Service URL** to your calls-offloader service (e.g., ``http://calls-offloader-server:4545``) - -3. Enable recording and transcription features as needed: - - - **Enable Call Recordings**: Toggle to allow call recordings - - **Enable Call Transcriptions**: Toggle to allow call transcriptions - - **Enable Live Captions** (Experimental): Toggle to allow real-time transcription - -4. Save the configuration - -5. Restart the Calls plugin to re-establish state: - - - Go to **System Console > Plugins > Plugin Management** - - Find the **Calls** plugin and click **Disable** - - Wait a few seconds, then click **Enable** - -6. Test by starting a call and enabling recording or live captions - -Troubleshooting ---------------- - -Common Issues -^^^^^^^^^^^ - -**"failed to create recording job: max concurrent jobs reached"** - -This error occurs when the calls-offloader service has reached its configured job limit. - -Solutions: - -- Increase ``max_concurrent_jobs`` in the configuration -- Check if jobs are hanging and restart the service -- Monitor system resources and scale up if needed - -**Jobs not processing** - -Check the following: - -- Verify the calls-offloader service is running: ``sudo systemctl status calls-offloader`` -- Ensure network connectivity between Mattermost and calls-offloader -- Check Docker daemon is running and accessible by the user running `calls-offloader`: ``docker ps`` -- Verify authentication configuration matches between services -- Review service logs for specific error messages - -**Docker permission issues** - -If using Docker API and seeing permission errors: - -.. code-block:: bash - - # Add calls-offloader user to docker group - sudo usermod -a -G docker calls-offloader - sudo systemctl restart calls-offloader - -Debugging Commands -^^^^^^^^^^^^^^^^ - -Monitor calls-offloader job containers: - -.. code-block:: bash - - # View running job containers - docker ps --format "{{.ID}} {{.Image}}" | grep "calls" - - # Follow logs for debugging - docker ps --format "{{.ID}} {{.Image}}" | grep "calls" | awk '{print $1}' | xargs -I {} docker logs -f {} - - # View completed job containers - docker ps -a --filter "status=exited" - -Monitor service health: - -.. code-block:: bash - - # Check service version and health - curl http://localhost:4545/version - -Check service logs: - -.. code-block:: bash - - # View recent logs - sudo journalctl -u calls-offloader -f - - # View log file (if file logging enabled) - tail -f /opt/calls-offloader/calls-offloader.log - -Performance Monitoring -^^^^^^^^^^^^^^^^^^^^ - -Monitor calls-offloader performance and resource usage to ensure optimal operation. See `Calls Metrics and Monitoring `__ for details on setting up metrics and observability. - -Other Calls Documentation ----------------- - -- `Calls Overview `__: Overview of deployment options and architecture -- `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service -- `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability -- `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments -- `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques -- `calls-offloader performance documentation `__: Detailed performance tuning and monitoring recommendations \ No newline at end of file diff --git a/source/configure/calls-rtcd-setup.rst b/source/configure/calls-rtcd-setup.rst deleted file mode 100644 index 18dc0001f6c..00000000000 --- a/source/configure/calls-rtcd-setup.rst +++ /dev/null @@ -1,415 +0,0 @@ -RTCD Setup and Configuration -========================= - -.. include:: ../_static/badges/allplans-cloud-selfhosted.rst - :start-after: :nosearch: - -.. raw:: html - -
- -Note - -|plans-img-yellow| The rtcd service is available only on `Enterprise `__ plans - -.. |plans-img-yellow| image:: ../_static/images/badges/flag_icon_yellow.svg - :class: mm-badge-flag - -.. raw:: html - -
- -This guide provides detailed instructions for setting up, configuring, and validating a Mattermost Calls deployment using the dedicated RTCD service. - -- `Prerequisites <#prerequisites>`__ -- `Installation and deployment <#installation-and-deployment>`__ -- `Configuration <#configuration>`__ -- `Validation and testing <#validation-and-testing>`__ -- `Horizontal scaling <#horizontal-scaling>`__ -- `Integration with Mattermost <#integration-with-mattermost>`__ - -Prerequisites ------------- - -Before deploying RTCD, ensure you have: - -- A Mattermost Enterprise license -- A server or VM with sufficient CPU and network capacity (see the `Performance `__ section for sizing guidance) - -Network Requirements ------------------- - -The following network connectivity is required: - -+-------------------+--------+-----------------+-------------------------+------------------------+ -| Service | Ports | Protocols | Source | Target | -+===================+========+=================+=========================+========================+ -| Calls plugin API | 80,443 | TCP (incoming) | Mattermost clients | Mattermost server | -+-------------------+--------+-----------------+-------------------------+------------------------+ -| RTC media | 8443 | UDP (incoming) | Mattermost clients | Mattermost or RTCD | -+-------------------+--------+-----------------+-------------------------+------------------------+ -| RTC media | 8443 | TCP (incoming) | Mattermost clients | Mattermost or RTCD | -+-------------------+--------+-----------------+-------------------------+------------------------+ -| RTCD API | 8045 | TCP (incoming) | Mattermost server | RTCD service | -+-------------------+--------+-----------------+-------------------------+------------------------+ -| STUN | 3478 | UDP (outgoing) | Mattermost or RTCD | STUN servers | -+-------------------+--------+-----------------+-------------------------+------------------------+ - -Installation and Deployment --------------------------- - -There are multiple ways to deploy RTCD, depending on your environment. We recommend the following order based on production readiness and operational control: - -Bare Metal or VM Deployment (Recommended) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This is the recommended deployment method for production environments as it provides the best performance and operational control. - -1. Download the latest release from the `RTCD GitHub repository `__ - -2. Create a configuration file (``/opt/rtcd/rtcd.toml``) with the following settings: - - .. code-block:: toml - - [api] - http.listen_address = ":8045" - security.allow_self_registration = true - - [rtc] - ice_address_udp = "" - ice_port_udp = 8443 - ice_address_tcp = "" - ice_port_tcp = 8443 - ice_host_override = "YOUR_RTCD_SERVER_PUBLIC_IP" - - # UDP port range for WebRTC connections - ice.port_range.min = 9000 - ice.port_range.max = 10000 - - # STUN/TURN server configuration - ice_servers = [ - { urls = ["stun:stun.global.calls.mattermost.com:3478"] } - ] - - [store] - data_source = "/opt/rtcd/data/db" - - [logger] - enable_console = true - console_json = true - console_level = "INFO" - enable_file = true - file_json = true - file_level = "INFO" - file_location = "/opt/rtcd/rtcd.log" - enable_color = true - - [mattermost] - host = "http://YOUR_MATTERMOST_SERVER:8065" - -3. Create the data directory: - - .. code-block:: bash - - sudo mkdir -p /opt/rtcd/data/db - -4. Create a systemd service file (``/etc/systemd/system/rtcd.service``): - - .. code-block:: ini - - [Unit] - Description=Mattermost RTCD Server - After=network.target - - [Service] - Type=simple - User=root - ExecStart=/opt/rtcd/rtcd --config /opt/rtcd/rtcd.toml - Restart=always - RestartSec=10 - LimitNOFILE=65536 - - [Install] - WantedBy=multi-user.target - -5. Enable and start the service: - - .. code-block:: bash - - sudo systemctl daemon-reload - sudo systemctl enable rtcd - sudo systemctl start rtcd - -6. Check the service status: - - .. code-block:: bash - - sudo systemctl status rtcd - -Docker Deployment -^^^^^^^^^^^^^^^ - -Docker deployment is suitable for development, testing, or containerized production environments: - -1. Run the RTCD container with basic configuration: - - .. code-block:: bash - - docker run -d --name rtcd \ - -e "RTCD_LOGGER_ENABLEFILE=false" \ - -e "RTCD_API_SECURITY_ALLOWSELFREGISTRATION=true" \ - -p 8443:8443/udp \ - -p 8443:8443/tcp \ - -p 8045:8045/tcp \ - mattermost/rtcd:latest - -2. For debugging purposes, you can enable more detailed logging: - - .. code-block:: bash - - docker run -d --name rtcd \ - -e "RTCD_LOGGER_ENABLEFILE=false" \ - -e "RTCD_LOGGER_CONSOLELEVEL=DEBUG" \ - -e "RTCD_API_SECURITY_ALLOWSELFREGISTRATION=true" \ - -p 8443:8443/udp \ - -p 8443:8443/tcp \ - -p 8045:8045/tcp \ - mattermost/rtcd:latest - - To view the logs: - - .. code-block:: bash - - docker logs -f rtcd - -You can also use a mounted configuration file instead of environment variables: - -.. code-block:: bash - - docker run -d --name rtcd \ - -p 8045:8045 \ - -p 8443:8443/udp \ - -p 8443:8443/tcp \ - -v /path/to/config.toml:/rtcd/config/config.toml \ - mattermost/rtcd:latest - -For a complete sample configuration file, see the `RTCD config.sample.toml `__ in the official repository. - -Kubernetes Deployment -^^^^^^^^^^^^^^^^^^^ - -For Kubernetes deployments, use the official Helm chart: - -1. Add the Mattermost Helm repository: - - .. code-block:: bash - - helm repo add mattermost https://helm.mattermost.com - helm repo update - -2. Install the RTCD chart: - - .. code-block:: bash - - helm install mattermost-rtcd mattermost/mattermost-rtcd \ - --set ingress.enabled=true \ - --set ingress.host=rtcd.example.com \ - --set service.annotations."service\\.beta\\.kubernetes\\.io/aws-load-balancer-backend-protocol"=udp \ - --set rtcd.ice.hostOverride=rtcd.example.com - - Refer to the `RTCD Helm chart documentation `__ for additional configuration options. - -Configuration ------------ - -RTCD Configuration File -^^^^^^^^^^^^^^^^^^^^^ - -The RTCD service uses a TOML configuration file. Here's a comprehensive example with commonly used settings: - -.. code-block:: toml - - [api] - # The address and port to which the HTTP API server will listen - http.listen_address = ":8045" - # Security settings for authentication - security.allow_self_registration = false - security.enable_admin = true - security.admin_secret_key = "YOUR_API_KEY" - # Configure allowed origins for CORS - security.allowed_origins = ["https://mattermost.example.com"] - - [rtc] - # The UDP address and port for media traffic - ice_address_udp = "" - ice_port_udp = 8443 - # The TCP address and port for fallback connections - ice_address_tcp = "" - ice_port_tcp = 8443 - # Public hostname or IP that clients will use to connect - ice_host_override = "rtcd.example.com" - - [logger] - # Logging configuration - enable_console = true - console_json = false - console_level = "INFO" - enable_file = true - file_json = true - file_level = "DEBUG" - file_location = "rtcd.log" - - [metrics] - # Prometheus metrics configuration - enable_prom = true - prom_port = 9090 - -Key Configuration Options: - -- **api.http.listen_address**: The address and port where the RTCD HTTP API service listens -- **rtc.ice_address_udp**: The UDP address for media traffic (empty means listen on all interfaces) -- **rtc.ice_port_udp**: The UDP port for media traffic -- **rtc.ice_address_tcp**: The TCP address for fallback media traffic -- **rtc.ice_port_tcp**: The TCP port for fallback media traffic -- **rtc.ice_host_override**: The public hostname or IP address clients will use to connect to RTCD -- **api.security.allowed_origins**: List of allowed origins for CORS -- **api.security.admin_secret_key**: API key for Mattermost servers to authenticate with RTCD - -STUN/TURN Configuration -^^^^^^^^^^^^^^^^^^^^^ - -For clients behind strict firewalls, you may need to configure STUN/TURN servers. In the RTCD configuration file, reference your STUN/TURN servers as follows: - -.. code-block:: toml - - [rtc] - # STUN/TURN server configuration - ice_servers = [ - { urls = ["stun:stun.example.com:3478"] }, - { urls = ["turn:turn.example.com:3478"], username = "turnuser", credential = "turnpassword" } - ] - -We recommend using `coturn `__ for your TURN server implementation. - -System Tuning -^^^^^^^^^^^ - -For high-volume deployments, tune your Linux system: - -1. Add the following to ``/etc/sysctl.conf``: - - .. code-block:: bash - - # Increase UDP buffer sizes - net.core.rmem_max = 16777216 - net.core.wmem_max = 16777216 - net.core.optmem_max = 16777216 - -2. Apply the settings: - - .. code-block:: bash - - sudo sysctl -p - -Validation and Testing --------------------- - -After deploying RTCD, validate the installation: - -1. **Check service status and version**: - - .. code-block:: bash - - curl http://YOUR_RTCD_SERVER:8045/version - # Should return a JSON object with service information - # Example: {"build_hash":"abc123","build_date":"2023-01-15T12:00:00Z","build_version":"0.11.0","goVersion":"go1.20.4"} - -2. **Test UDP connectivity**: - - On the RTCD server: - - .. code-block:: bash - - nc -l -u -p 8443 - - On a client machine: - - .. code-block:: bash - - nc -v -u YOUR_RTCD_SERVER 8443 - - Type a message and hit Enter on either side. If messages are received on both ends, UDP connectivity is working. - - Note: This test must be run with the RTCD service stopped, as it binds to the same port. - - .. code-block:: bash - - sudo systemctl stop rtcd - - -3. **Test TCP connectivity** (if enabled): - - Similar to the UDP test, but remove the ``-u`` flag from both commands. - -4. **Monitor metrics**: - - Refer to `Calls Metrics and Monitoring `__ for setting up Calls metrics and monitoring. - -Horizontal Scaling ----------------- - -To scale RTCD horizontally: - -1. **Deploy multiple RTCD instances**: - - Deploy multiple RTCD servers, each with their own unique IP address. - -2. **Configure DNS-based load balancing**: - - Set up a DNS record that points to multiple RTCD IP addresses: - - .. code-block:: bash - - rtcd.example.com. IN A 10.0.0.1 - rtcd.example.com. IN A 10.0.0.2 - rtcd.example.com. IN A 10.0.0.3 - -3. **Configure health checks**: - - Set up health checks to automatically remove unhealthy RTCD instances from DNS. - -4. **Configure Mattermost**: - - In the Mattermost System Console, set the **RTCD Service URL** to your DNS name (e.g., ``rtcd.example.com``). - -The Mattermost Calls plugin will distribute calls among the available RTCD hosts. Remember that a single call will always be hosted on one RTCD instance; sessions belonging to the same call are not spread across different instances. - -Integration with Mattermost -------------------------- - -Once RTCD is properly set up and validated, configure Mattermost to use it: - -1. Go to **System Console > Plugins > Calls** - -2. Enable the **Enable RTCD Service** option - -3. Set the **RTCD Service URL** to your RTCD service address (either a single server or DNS load-balanced hostname) - -4. If configured, enter the **RTCD API Key** that matches the one in your RTCD configuration - -5. Save the configuration - -6. Test by creating a new call in any Mattermost channel - -7. Verify that the call is being routed through RTCD by checking the RTCD logs and metrics - -Other Calls Documentation ----------------- - -- `Calls Overview `__: Overview of deployment options and architecture -- `Calls Offloader Setup and Configuration `__: Setup guide for call recording and transcription -- `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability -- `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments -- `Calls Troubleshooting `__: Detailed troubleshooting steps and debugging techniques - -For detailed Mattermost Calls configuration options, see the `Calls Plugin Configuration Settings `__ documentation. \ No newline at end of file diff --git a/source/configure/calls-troubleshooting.md b/source/configure/calls-troubleshooting.md index bf25fbe7b12..29d7d9620fc 100644 --- a/source/configure/calls-troubleshooting.md +++ b/source/configure/calls-troubleshooting.md @@ -89,9 +89,8 @@ This guide provides comprehensive troubleshooting steps for Mattermost Calls, pa Test if the RTCD API is reachable: ```bash - curl http://YOUR_RTCD_SERVER:8045/api/v1/health - # Expected response: {"status":"ok"} - ``` + curl http://YOUR_RTCD_SERVER:8045/version + # Example response: {"buildDate":"2025-04-02 21:33","buildVersion":"v1.1.0","buildHash":"7bc1f7a","goVersion":"go1.23.6","goOS":"linux","goArch":"amd64"} ``` 2. **UDP connectivity test**: diff --git a/source/configure/calls-troubleshooting.rst b/source/configure/calls-troubleshooting.rst deleted file mode 100644 index f54dfff24d0..00000000000 --- a/source/configure/calls-troubleshooting.rst +++ /dev/null @@ -1,431 +0,0 @@ -Troubleshooting Mattermost Calls -=========================== - -.. include:: ../_static/badges/allplans-cloud-selfhosted.rst - :start-after: :nosearch: - -This guide provides comprehensive troubleshooting steps for Mattermost Calls, particularly focusing on the dedicated RTCD deployment model. Follow these steps to identify and resolve common issues. - -- `Common issues <#common-issues>`__ -- `Connectivity troubleshooting <#connectivity-troubleshooting>`__ -- `Log analysis <#log-analysis>`__ -- `Performance issues <#performance-issues>`__ -- `Debugging tools <#debugging-tools>`__ -- `Advanced diagnostics <#advanced-diagnostics>`__ - -Common Issues ------------ - -Calls Not Connecting -^^^^^^^^^^^^^^^^^^^^ - -**Symptoms**: Users can start calls but cannot connect, or calls connect but drop quickly. - -**Possible causes and solutions**: - -1. **Network connectivity issues**: - - Verify that UDP port 8443 (or your configured port) is open between clients and RTCD servers - - Ensure TCP port 8045 is open between Mattermost and RTCD servers - - Check that any load balancers are properly configured for UDP traffic - -2. **ICE configuration issues**: - - Verify the ``rtc.ice_host_override`` setting in RTCD configuration matches the publicly accessible hostname or IP of the RTCD server - - If this setting is incorrect, client browser console may show errors like: ``com.mattermost.calls: peer error timed out waiting for rtc connection`` - - Meanwhile, RTCD `trace` level logs might show internal IP addresses in ICE connection logs: - - .. code-block:: json - - {"timestamp":"2025-05-14 10:29:08.935 Z","level":"trace","msg":"Ping STUN from udp4 host 172.31.29.117:8443 (resolved: 172.31.29.117:8443) to udp4 host 192.168.64.1:59737 (resolved: 192.168.64.1:59737)","caller":"rtc/logger.go:54","origin":"ice/v4.(*Agent).sendBindingRequest github.com/pion/ice/v4@v4.0.3/agent.go:921"} - -3. **API connectivity**: - - Verify that Mattermost servers can reach the RTCD API endpoint - - Check that the API key is correctly configured in both Mattermost and RTCD - -4. **Plugin configuration**: - - Ensure the Calls plugin is enabled and properly configured - - Verify the RTCD service URL is correct in the System Console - -Audio Issues -^^^^^^^^^^^ - -**Symptoms**: Users can connect to calls, but audio is one-way, choppy, or not working. - -**Possible causes and solutions**: - -1. **Client permissions**: - - Ensure browser/app has microphone permissions - - Check if users are using multiple audio devices that might interfere - -2. **Network quality**: - - High latency or packet loss can cause audio issues - - Try testing with TCP fallback enabled (requires RTCD v0.11+ and Calls v0.17+) - -3. **Audio device configuration**: - - Users should verify their audio input/output settings - - Try different browsers or the desktop app - -Call Quality Issues -^^^^^^^^^^^^^^^^^ - -**Symptoms**: Calls connect but quality is poor, with latency, echo, or distortion. - -**Possible causes and solutions**: - -1. **Server resources**: - - Check CPU usage on RTCD servers - high CPU can cause quality issues - - Refer to the `Performance Monitoring setup guide <../performance-monitoring/setup-guide.rst>`__ for detailed instructions on monitoring and optimizing performance - - Monitor network bandwidth usage - -2. **Network congestion**: - - Check for packet loss between clients and RTCD - - Consider network QoS settings to prioritize real-time traffic - -3. **Client-side issues**: - - Browser or app limitations - - Hardware limitations (CPU, memory) - - Network congestion at the user's location - -Connectivity Troubleshooting --------------------------- - -Basic Connectivity Tests -^^^^^^^^^^^^^^^^^^^^^^ - -1. **HTTP API connectivity test**: - - Test if the RTCD API is reachable: - - .. code-block:: bash - - curl http://YOUR_RTCD_SERVER:8045/api/v1/health - # Expected response: {"status":"ok"} - -2. **UDP connectivity test**: - - On the RTCD server: - - .. code-block:: bash - - nc -l -u -p 8443 - - On a client machine: - - .. code-block:: bash - - nc -v -u YOUR_RTCD_SERVER 8443 - - Type a message and press Enter. If you see the message on both sides, UDP connectivity is working. - -3. **TCP fallback connectivity test**: - - Same as the UDP test, but without the ``-u`` flag: - - On the RTCD server: - - .. code-block:: bash - - nc -l -p 8443 - - On a client machine: - - .. code-block:: bash - - nc -v YOUR_RTCD_SERVER 8443 - -Network Packet Analysis -^^^^^^^^^^^^^^^^^^^^^ - -To capture and analyze network traffic: - -1. **Capture UDP traffic on the RTCD server**: - - .. code-block:: bash - - sudo tcpdump -n 'udp port 8443' -i any - -2. **Capture TCP API traffic**: - - .. code-block:: bash - - sudo tcpdump -n 'tcp port 8045' -i any - -3. **Analyze traffic patterns**: - - - Verify packets are flowing both ways - - Look for ICMP errors that might indicate firewall issues - - Check for patterns of packet loss - -4. **Use Wireshark for deeper analysis**: - - For more detailed packet inspection, capture traffic with tcpdump and analyze with Wireshark: - - .. code-block:: bash - - sudo tcpdump -n -w calls_traffic.pcap 'port 8443' - - Then analyze the ``calls_traffic.pcap`` file with Wireshark. - -Firewall Configuration Checks -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -1. **Check iptables rules** (Linux): - - .. code-block:: bash - - sudo iptables -L -n - - Ensure there are no rules blocking UDP port 8443 or TCP ports 8045/8443. - -2. **Check cloud provider security groups**: - - Verify that security groups or network ACLs allow: - - Inbound UDP on port 8443 from client networks - - Inbound TCP on port 8045 from Mattermost server networks - - Inbound TCP on port 8443 (if TCP fallback is enabled) - -3. **Check intermediate firewalls**: - - - Corporate firewalls might block UDP traffic - - Some networks might require TURN servers for traversal - -Log Analysis ----------- - -RTCD Logs -^^^^^^^^ - -The RTCD service logs important events and errors. Set the log level to "debug" for troubleshooting: - -1. **In the configuration file**: - - .. code-block:: json - - { - "log": { - "level": "debug", - "json": true - } - } - -2. **Common log patterns to look for**: - - - **Connection errors**: Look for "failed to connect" or "connection error" messages - - **ICE negotiation failures**: Look for "ICE failed" or "ICE timeout" messages - - **API authentication issues**: Look for "unauthorized" or "invalid API key" messages - -Mattermost Logs -^^^^^^^^^^^^^ - -Check the Mattermost server logs for Calls plugin related issues: - -1. **Enable debug logging** in System Console > Environment > Logging > File Log Level - -2. **Filter for Calls-related logs**: - - .. code-block:: bash - - grep -i "calls" /path/to/mattermost.log - -3. **Look for common patterns**: - - - Connection errors to RTCD - - Plugin initialization issues - - WebSocket connection problems - -Browser Console Logs -^^^^^^^^^^^^^^^^^ - -Instruct users to check their browser console logs: - -1. **In Chrome/Edge**: - - Press F12 to open Developer Tools - - Go to the Console tab - - Look for errors related to WebRTC, Calls, or media permissions - -2. **Specific patterns to look for**: - - - "getUserMedia" errors (microphone permission issues) - - "ICE connection" failures - - WebSocket connection errors - -Performance Issues ---------------- - -Diagnosing High CPU Usage -^^^^^^^^^^^^^^^^^^^^^^^ - -If RTCD servers show high CPU usage: - -1. **Check concurrent calls and participants**: - - - Access the Prometheus metrics endpoint to see active sessions - - Compare with the benchmark data in the documentation - -2. **Profile CPU usage** (Linux): - - .. code-block:: bash - - top -p $(pgrep rtcd) - - Or for detailed per-thread usage: - - .. code-block:: bash - - ps -eLo pid,ppid,tid,pcpu,comm | grep rtcd - -3. **Enable pprof profiling** (if needed): - - Add to your RTCD configuration: - - .. code-block:: json - - { - "debug": { - "pprof": true, - "pprofPort": 6060 - } - } - - Then capture a CPU profile: - - .. code-block:: bash - - curl http://localhost:6060/debug/pprof/profile > cpu.profile - - Analyze with: - - .. code-block:: bash - - go tool pprof -http=:8080 cpu.profile - -Diagnosing Network Bottlenecks -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If you suspect network bandwidth issues: - -1. **Monitor network utilization**: - - .. code-block:: bash - - iftop -n - -2. **Check for packet drops**: - - .. code-block:: bash - - netstat -su | grep -E 'drop|error' - -3. **Verify system network buffers**: - - .. code-block:: bash - - sysctl -a | grep net.core.rmem - sysctl -a | grep net.core.wmem - - Ensure these match the recommended values: - - .. code-block:: bash - - net.core.rmem_max = 16777216 - net.core.wmem_max = 16777216 - net.core.optmem_max = 16777216 - -Recording and Transcription Issues ----------------------------------- - -For troubleshooting calls-offloader service issues including recording and transcription problems, see the `Calls Offloader Setup and Configuration `__ guide. - -Debugging Tools ------------- - - -Prometheus Metrics Analysis -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Use Prometheus metrics for real-time and historical performance data: - -Import the official `Mattermost Calls dashboard `__ into Grafana for visualization. - -Advanced Diagnostics ------------------ - -WebRTC Diagnostic Commands -^^^^^^^^^^^^^^^^^^^^^^^^ - -For detailed WebRTC diagnostics: - -1. **Test STUN server connectivity**: - - .. code-block:: bash - - # Using stun-client (you may need to install it) - stun-client stun.global.calls.mattermost.com - - This should return your public IP address if STUN is working correctly. - -2. **Verify TURN server**: - - .. code-block:: bash - - # Using turnutils_uclient (part of coturn) - turnutils_uclient -v -s your-turn-server -u username -p password - - This tests if your TURN server is correctly configured. - -3. **Test end-to-end latency**: - - Between client locations and RTCD server: - - .. code-block:: bash - - ping -c 10 your-rtcd-server - - Look for consistent, low latency (<100ms ideally for voice calls). - -Client-Side Testing Tools -^^^^^^^^^^^^^^^^^^^^^^^ - -Tools to help diagnose client-side issues: - -1. **WebRTC Troubleshooter**: - - Direct users to `WebRTC Troubleshooter `__ for browser capability testing. - -2. **Network Quality Tests**: - - Use `Speedtest `__ or similar to check internet connection quality. - -3. **Browser-Specific WebRTC Info**: - - - Chrome: chrome://webrtc-internals - - Firefox: about:webrtc - -When to Contact Support -^^^^^^^^^^^^^^^^^^^^ - -Consider contacting Mattermost Support when: - -1. You've tried troubleshooting steps without resolution -2. You're experiencing persistent connection failures across multiple clients -3. You notice unexpected or degraded performance despite proper configuration -4. You need help interpreting diagnostic information -5. You suspect a bug in the Calls plugin or RTCD service - -When contacting support, please include: - -- RTCD version and configuration (with sensitive information redacted) -- Mattermost server version -- Calls plugin version -- Client environments (browsers, OS versions) -- Relevant logs and diagnostic information -- Detailed description of the issue and steps to reproduce - -Other Calls Documentation ----------------- - -- `Calls Overview `__: Overview of deployment options and architecture -- `RTCD Setup and Configuration `__: Comprehensive guide for setting up the dedicated RTCD service -- `Calls Offloader Setup and Configuration `__: Setup guide for call recording and transcription -- `Calls Metrics and Monitoring `__: Guide to monitoring Calls performance using metrics and observability -- `Calls Deployment on Kubernetes `__: Detailed guide for deploying Calls in Kubernetes environments -- Monitoring dashboards screenshots \ No newline at end of file From 0b62a410262aac98fff02626e84a883116d69167 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Wed, 11 Jun 2025 13:19:49 -0400 Subject: [PATCH 19/37] Updating RTCD Debug log config, fixed broken links --- source/collaborate/make-calls.rst | 2 +- source/configure/calls-troubleshooting.md | 13 ++++++------- source/configure/plugins-configuration-settings.rst | 8 ++++---- source/deploy/server/containers/install-docker.rst | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/source/collaborate/make-calls.rst b/source/collaborate/make-calls.rst index 831bc31108d..fd942c06983 100644 --- a/source/collaborate/make-calls.rst +++ b/source/collaborate/make-calls.rst @@ -10,7 +10,7 @@ Using a web browser, the desktop app, or the mobile app, you can `join a call <# - All Mattermost customers can start, join, and participate in 1:1 audio calls with optional screen sharing. - For group calls up to 50 concurrent users, Mattermost Enterprise, Professional, or Mattermost Cloud is required. - - Enterprise customers can also `record calls <#record-a-call>`__, enable :ref:`live text captions ` during calls, and `transcribe recorded calls <#transcribe-recorded-calls>`__. We recommend that Enterprise self-hosted customers looking for group calls beyond 50 concurrent users consider using the :ref:`dedicated rtcd service `. + - Enterprise customers can also `record calls <#record-a-call>`__, enable :ref:`live text captions ` during calls, and `transcribe recorded calls <#transcribe-recorded-calls>`__. We recommend that Enterprise self-hosted customers looking for group calls beyond 50 concurrent users consider using the :doc:`dedicated RTCD service `. - Mattermost Cloud users can start calling right out of the box. For Mattermost self-hosted deployments, System admins need to enable and configure the plugin :ref:`using the System Console `. .. include:: ../_static/badges/academy-calls.rst diff --git a/source/configure/calls-troubleshooting.md b/source/configure/calls-troubleshooting.md index 29d7d9620fc..4824e8b1a2b 100644 --- a/source/configure/calls-troubleshooting.md +++ b/source/configure/calls-troubleshooting.md @@ -186,15 +186,14 @@ The RTCD service logs important events and errors. Set the log level to "debug" 1. **In the configuration file**: - ```json - { - "log": { - "level": "debug", - "json": true - } - } + ```toml + [logger] + enable_file = true + file_level = "DEBUG" ``` + Restart the RTCD service after making these changes + 2. **Common log patterns to look for**: - **Connection errors**: Look for "failed to connect" or "connection error" messages diff --git a/source/configure/plugins-configuration-settings.rst b/source/configure/plugins-configuration-settings.rst index fed0238ea68..d92c4159a26 100644 --- a/source/configure/plugins-configuration-settings.rst +++ b/source/configure/plugins-configuration-settings.rst @@ -532,7 +532,7 @@ ICE servers configurations - The configurations above, containing STUN and TURN servers, are sent to the clients and used to generate local candidates. - If hosting calls through the plugin (i.e. not using the |rtcd_service|) any configured STUN server may also be used to find the instance's public IP when none is provided through the |ice_host_override_link| option. -.. |rtcd_service| replace:: :ref:`rtcd service ` +.. |rtcd_service| replace:: :doc:`RTCD service ` **Example** @@ -764,7 +764,7 @@ Call recording quality +-----------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+ .. note:: - The quality setting will affect the performance of the job service and the file size of recordings. Refer to the :ref:`deployment section ` for more information. + The quality setting will affect the performance of the job service and the file size of recordings. Refer to the :ref:`call recording and transcription section ` for more information. .. config:setting:: enable-pluginscalltranscriptions :displayname: Enable call transcriptions (Plugins - Calls) @@ -814,7 +814,7 @@ Transcriber model size +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------+ .. note:: - This setting is available starting in plugin version 0.22. The model size setting will affect the performance of the job service. Refer to the :ref:`configure call recordings, transcriptions, and live captions ` documentation for more information. + This setting is available starting in plugin version 0.22. The model size setting will affect the performance of the job service. Refer to the :ref:`call recording and transcription section ` documentation for more information. .. config:setting:: call-transcriber-threads :displayname: Call transcriber threads (Plugins - Calls) @@ -837,7 +837,7 @@ Call transcriber threads +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------+ .. note:: - The call transcriber threads setting will affect the performance of the job service. Refer to the :ref:`configure call recordings, transcriptions, and live captions ` documentation for more information. This setting is available starting in plugin version 0.26.2. + The call transcriber threads setting will affect the performance of the job service. Refer to the :ref:`call recording and transcription section ` documentation for more information. This setting is available starting in plugin version 0.26.2. .. config:setting:: enable-pluginslivecaptions :displayname: (Experimental) Enable live captions (Plugins - Calls) diff --git a/source/deploy/server/containers/install-docker.rst b/source/deploy/server/containers/install-docker.rst index 84d29e0d35d..dfd60ddb921 100644 --- a/source/deploy/server/containers/install-docker.rst +++ b/source/deploy/server/containers/install-docker.rst @@ -183,7 +183,7 @@ Looking for a way to evaluate Mattermost on a single local machine using Docker? - This local image is self-contained (i.e., it has an internal database and works out of the box). Dropping a container using this image removes data and configuration as expected. You can see the :doc:`configuration settings ` documentation to learn more about customizing your trial deployment. - **Preview Mode** shouldn't be used in a production environment, as it uses a known password string, contains other non-production configuration settings, has email disabled, keeps no persistent data (all data lives inside the container), and doesn't support upgrades. - - If you are planning to use the calling functionality in **Preview Mode** on a non-local environment, you should ensure that the server is running on a secure (HTTPs) connection and that the :ref:`network requirements ` to run calls are met. + - If you are planning to use the calling functionality in **Preview Mode** on a non-local environment, you should ensure that the server is running on a secure (HTTPs) connection and that the :ref:`network requirements ` to run calls are met. 1. Install `Docker `__. From a4ce710307a36d8a7b824f226981a928b306ab6a Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Wed, 11 Jun 2025 13:43:29 -0400 Subject: [PATCH 20/37] New debug steps, CORS MM config --- source/configure/calls-metrics-monitoring.md | 32 +++++++++++++++++ source/configure/calls-rtcd-setup.md | 36 +++++++++++++++++++ source/configure/calls-troubleshooting.md | 37 ++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/source/configure/calls-metrics-monitoring.md b/source/configure/calls-metrics-monitoring.md index b93bbfa0bd7..43e6a7a735b 100644 --- a/source/configure/calls-metrics-monitoring.md +++ b/source/configure/calls-metrics-monitoring.md @@ -234,6 +234,38 @@ Below are the detailed benchmarks based on internal performance testing: | 5 | 200 | 2 | no | 30% | 0.6GB | 8.2Mbps / 180Mbps | c6i.2xlarge | | 5 | 200 | 2 | yes | 90% | 0.7GB | 31Mbps / 2.2Gbps | c6i.2xlarge | +## Troubleshooting Metrics Collection + +### Verify RTCD Metrics are Being Collected + +To verify that Prometheus is successfully collecting RTCD metrics, use this command: + +```bash +curl http://PROMETHEUS_IP:9090/api/v1/label/__name__/values | jq '.' | grep rtcd +``` + +This command queries Prometheus for all available metric names and filters for RTCD-related metrics. + +If no RTCD metrics appear, check: +1. RTCD is running +2. Prometheus is configured to scrape the RTCD metrics endpoint +3. RTCD metrics port is accessible from Prometheus (default: 8045) + +### Check Prometheus Scrape Targets + +To verify all Calls-related services are being scraped successfully: + +1. Open the Prometheus web interface (typically `http://PROMETHEUS_IP:9090`) +2. Navigate to **Status > Targets** +3. Look for your configured Calls services: + - Mattermost server (for Calls plugin metrics) + - RTCD service + +Each target should show status "UP" in green. If a target shows "DOWN" or errors: +- Verify the service is running +- Check network connectivity between Prometheus and the target +- Verify the metrics endpoint is accessible + ## Other Calls Documentation - [Calls Overview](calls-deployment.html): Overview of deployment options and architecture diff --git a/source/configure/calls-rtcd-setup.md b/source/configure/calls-rtcd-setup.md index c7fd09e4f44..d63ed4a94a4 100644 --- a/source/configure/calls-rtcd-setup.md +++ b/source/configure/calls-rtcd-setup.md @@ -245,6 +245,42 @@ Key Configuration Options: - **api.security.allowed_origins**: List of allowed origins for CORS - **api.security.admin_secret_key**: API key for Mattermost servers to authenticate with RTCD +### Required Mattermost Server Configuration + +When using RTCD, you must configure the Mattermost server's CORS settings to allow proper communication between the server and the RTCD service. + +#### CORS Configuration + +The `AllowCorsFrom` setting must include your SiteURL and, if using calls-offloader in a private network, the Mattermost server's private IP address: + +**Using mmctl:** +```bash +# Basic RTCD configuration - include your SiteURL +mmctl config set ServiceSettings.AllowCorsFrom "https://your-domain.com" + +# If using calls-offloader in a private network, also include Mattermost's private IP with port 8065 +mmctl config set ServiceSettings.AllowCorsFrom "https://your-domain.com http://192.168.1.100:8065" +``` + +**Using System Console:** +1. Go to **System Console > Environment > Web Server** +2. Set **Allow cross-origin requests from** to include: + - Your SiteURL (e.g., `https://your-domain.com`) + - If using calls-offloader in a private network: Also include Mattermost's private IP with port 8065 (e.g., `http://192.168.1.100:8065`) + +**Using config.json:** +```json +{ + "ServiceSettings": { + "AllowCorsFrom": "https://your-domain.com http://192.168.1.100:8065" + } +} +``` + +```{important} +This CORS configuration is specifically required for RTCD deployments and is not needed for integrated mode deployments. Multiple origins should be separated by spaces. +``` + ### STUN/TURN Configuration For clients behind strict firewalls, you may need to configure STUN/TURN servers. In the RTCD configuration file, reference your STUN/TURN servers as follows: diff --git a/source/configure/calls-troubleshooting.md b/source/configure/calls-troubleshooting.md index 4824e8b1a2b..3c533e63311 100644 --- a/source/configure/calls-troubleshooting.md +++ b/source/configure/calls-troubleshooting.md @@ -316,6 +316,43 @@ If you suspect network bandwidth issues: For troubleshooting calls-offloader service issues including recording and transcription problems, see the [Calls Offloader Setup and Configuration](calls-offloader-setup.html#troubleshooting) guide. +### Calls-Offloader Docker Debugging + +If you're running calls-offloader in Docker, use these commands for debugging: + +#### Monitor Live Logs + +To view real-time logs from calls-offloader containers: + +```bash +# Find and follow logs from all calls-related containers +docker ps --format "{{.ID}} {{.Image}}" | grep "calls" | awk '{print $1}' | xargs -I {} docker logs -f {} +``` + +This command finds all running containers with "calls" in the image name and follows their logs. + +#### View Completed Jobs + +To view completed calls-offloader job containers (useful for debugging failed jobs): + +```bash +# List all exited containers to see completed jobs +docker ps -a --filter "status=exited" +``` + +Look for containers with calls-offloader image names that have exited. You can then examine their logs: + +```bash +# View logs from a specific completed container +docker logs +``` + +#### Additional Docker Debugging Tips + +- **Check container resource usage**: `docker stats` to see if containers are hitting resource limits +- **Inspect container configuration**: `docker inspect ` for detailed container settings +- **Check container health**: `docker inspect | grep Health` if health checks are configured + ## Debugging Tools ### Prometheus Metrics Analysis From 6dfd85e729d10747bbba192e494e529673cdf3f9 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Wed, 11 Jun 2025 13:44:24 -0400 Subject: [PATCH 21/37] Deleted incorrectly added TODO file --- PULL_REQUEST_REVIEW_ITEMS.md | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 PULL_REQUEST_REVIEW_ITEMS.md diff --git a/PULL_REQUEST_REVIEW_ITEMS.md b/PULL_REQUEST_REVIEW_ITEMS.md deleted file mode 100644 index 9e1bc015231..00000000000 --- a/PULL_REQUEST_REVIEW_ITEMS.md +++ /dev/null @@ -1,30 +0,0 @@ -# Mattermost Calls Documentation TODOs - -This file contains a list of documentation improvements identified for the Mattermost Calls feature. - -## Completed Items - -- ✅ Update `calls-rtcd-setup.rst` with new version endpoint information -- ✅ Update `calls-troubleshooting.rst` ICE configuration section with correct parameter name -- ✅ Add client and RTCD log examples to ICE configuration issues -- ✅ Remove 'Why Use RTCD' section from calls-rtcd-setup.rst - -## High Priority Items - -- Re-order RTCD setup recommendations to prioritize "Bare Metal/VM", then "Docker", then "Kubernetes". I'll share my example systemd unit file and configuration. -- Create documentation for the calls-offloader service used for Calls Recording and Transcription -- Expand CORS documentation or document that `AllowCORSFrom` needs to be set to the SiteURL -- Add call-out/special note that the Calls metrics scraping has issues with `labels` setup in `prometheus.yml` and that the Calls dashboard expects : format, with an example. -- Document a common flow for each piece of the setup: Calls plugin curl/test/telemetry, RTCD curl/test/telemetry, calls-offloader curl/test/telemetry -- Document troubleshooting steps for the error "failed to create recording job: max concurrent jobs reached". - -## Medium Priority Items - -- Document the command `curl http://localhost:9090/api/v1/label/__name__/values | jq '.' | grep rtcd` as a troubleshooting tool in `calls-metrics-monitoring.rst` -- Document that Node Exporter service needs to be bound to the right address -- Document how to check Prometheus Scrape targets using the 'Targets' menu option for status -- Document this command for debugging calls-offloader: `docker ps --format "{{.ID}} {{.Image}}" | grep "calls" | awk '{print $1}' | xargs -I {} docker logs -f {}` -- Document the command `docker ps -a --filter "status=exited"` to view completed calls-offloader job containers -- Improve formatting for Network Requirements table to avoid small side-scrolling view -- Document experimental screen sharing audio feature with limitations and how to enable with `/call experimental on` slash command -- Provide documentation on what settings changes require what service/plugin restarts From 0235ab0b4f5e7d6a30df88023424838e43935424 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Wed, 11 Jun 2025 16:12:39 -0400 Subject: [PATCH 22/37] Adapting the nav structure Mixing of rst and markdown caused issues with the LHS navigation --- source/configure/calls-deployment.md | 10 ---------- source/configure/calls-overview.rst | 16 ++++++++++++++++ source/deploy/server/preparations.rst | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 source/configure/calls-overview.rst diff --git a/source/configure/calls-deployment.md b/source/configure/calls-deployment.md index bd54c0911e7..f5bdea37b62 100644 --- a/source/configure/calls-deployment.md +++ b/source/configure/calls-deployment.md @@ -6,16 +6,6 @@ This document provides an overview of Mattermost Calls deployment options for self-hosted environments, including [air-gapped environments](https://docs.mattermost.com/configure/calls-deployment.html#air-gapped-deployments), ensuring private communication without reliance on public internet connectivity with flexible configuration options for complex network requirements. -```{toctree} -:maxdepth: 1 -:hidden: - -calls-rtcd-setup.md -calls-offloader-setup.md -calls-metrics-monitoring.md -calls-kubernetes.md -calls-troubleshooting.md -``` ## Quick Links diff --git a/source/configure/calls-overview.rst b/source/configure/calls-overview.rst new file mode 100644 index 00000000000..8adcf1c79ad --- /dev/null +++ b/source/configure/calls-overview.rst @@ -0,0 +1,16 @@ +Mattermost Calls +================ + +.. include:: ../_static/badges/allplans-cloud-selfhosted.rst + :start-after: :nosearch: + +Mattermost Calls provides integrated audio calling and screen sharing capabilities within Mattermost channels. This section covers deployment, configuration, and management of Mattermost Calls. + +**Calls Documentation:** + +* :doc:`Calls Deployment and Configuration ` - Main deployment overview and architecture guide for Mattermost Calls +* :doc:`RTCD Setup and Configuration ` - Real-time communication daemon setup for enterprise deployments +* :doc:`Calls Offloader Setup and Configuration ` - Configure call recording and transcription services +* :doc:`Calls Metrics and Monitoring ` - Performance monitoring with Prometheus and Grafana +* :doc:`Calls Deployment on Kubernetes ` - Kubernetes deployment guide for scalable Calls infrastructure +* :doc:`Calls Troubleshooting ` - Comprehensive troubleshooting guide for common issues \ No newline at end of file diff --git a/source/deploy/server/preparations.rst b/source/deploy/server/preparations.rst index 7a0e8c10661..d693905ae12 100644 --- a/source/deploy/server/preparations.rst +++ b/source/deploy/server/preparations.rst @@ -10,7 +10,7 @@ This guide outlines the key preparation steps required before installing the Mat Review software and hardware requirements Set up an NGINX proxy - Configure Mattermost Calls + Configure Mattermost Calls Set up TLS Use an image proxy From 732e0f72fa5bdd97df65e53445ae58dd687b6c93 Mon Sep 17 00:00:00 2001 From: "Carrie Warner (Mattermost)" <74422101+cwarnermm@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:45:50 -0400 Subject: [PATCH 23/37] Fixed nav structure, moved Ent badge, applied build fixes --- .../badges}/calls-rtcd-ent-only.md | 0 source/collaborate/make-calls.rst | 2 +- source/configure/calls-deployment.md | 3 +-- source/configure/calls-kubernetes.md | 2 +- source/configure/calls-overview.rst | 23 ++++++++++++++----- source/configure/calls-rtcd-setup.md | 9 +------- 6 files changed, 21 insertions(+), 18 deletions(-) rename source/{configure => _static/badges}/calls-rtcd-ent-only.md (100%) diff --git a/source/configure/calls-rtcd-ent-only.md b/source/_static/badges/calls-rtcd-ent-only.md similarity index 100% rename from source/configure/calls-rtcd-ent-only.md rename to source/_static/badges/calls-rtcd-ent-only.md diff --git a/source/collaborate/make-calls.rst b/source/collaborate/make-calls.rst index fd942c06983..2441fbc97e8 100644 --- a/source/collaborate/make-calls.rst +++ b/source/collaborate/make-calls.rst @@ -10,7 +10,7 @@ Using a web browser, the desktop app, or the mobile app, you can `join a call <# - All Mattermost customers can start, join, and participate in 1:1 audio calls with optional screen sharing. - For group calls up to 50 concurrent users, Mattermost Enterprise, Professional, or Mattermost Cloud is required. - - Enterprise customers can also `record calls <#record-a-call>`__, enable :ref:`live text captions ` during calls, and `transcribe recorded calls <#transcribe-recorded-calls>`__. We recommend that Enterprise self-hosted customers looking for group calls beyond 50 concurrent users consider using the :doc:`dedicated RTCD service `. + - Enterprise customers can also `record calls <#record-a-call>`__, enable :ref:`live text captions ` during calls, and `transcribe recorded calls <#transcribe-recorded-calls>`__. We recommend that Enterprise self-hosted customers looking for group calls beyond 50 concurrent users consider using the :doc:`dedicated RTCD service `. - Mattermost Cloud users can start calling right out of the box. For Mattermost self-hosted deployments, System admins need to enable and configure the plugin :ref:`using the System Console `. .. include:: ../_static/badges/academy-calls.rst diff --git a/source/configure/calls-deployment.md b/source/configure/calls-deployment.md index f5bdea37b62..888f925e5a1 100644 --- a/source/configure/calls-deployment.md +++ b/source/configure/calls-deployment.md @@ -1,5 +1,4 @@ -# Calls Deployment Overview - +# Deploy Mattermost Calls ```{include} ../_static/badges/allplans-cloud-selfhosted.md ``` diff --git a/source/configure/calls-kubernetes.md b/source/configure/calls-kubernetes.md index 627bff23982..92d1d924e0a 100644 --- a/source/configure/calls-kubernetes.md +++ b/source/configure/calls-kubernetes.md @@ -1,4 +1,4 @@ -# Calls deployment on Kubernetes +# Deploy Calls Kubernetes ```{include} ../_static/badges/allplans-cloud-selfhosted.md ``` diff --git a/source/configure/calls-overview.rst b/source/configure/calls-overview.rst index 8adcf1c79ad..5ba7f5fc8db 100644 --- a/source/configure/calls-overview.rst +++ b/source/configure/calls-overview.rst @@ -6,11 +6,22 @@ Mattermost Calls Mattermost Calls provides integrated audio calling and screen sharing capabilities within Mattermost channels. This section covers deployment, configuration, and management of Mattermost Calls. +.. toctree:: + :maxdepth: 1 + :hidden: + + Deploy Mattermost Calls + RTCD Setup and Configuration + Calls Offloader Setup and Configuration + Calls Metrics and Monitoring + Deploy Calls on Kubernetes + Calls Troubleshooting + **Calls Documentation:** -* :doc:`Calls Deployment and Configuration ` - Main deployment overview and architecture guide for Mattermost Calls -* :doc:`RTCD Setup and Configuration ` - Real-time communication daemon setup for enterprise deployments -* :doc:`Calls Offloader Setup and Configuration ` - Configure call recording and transcription services -* :doc:`Calls Metrics and Monitoring ` - Performance monitoring with Prometheus and Grafana -* :doc:`Calls Deployment on Kubernetes ` - Kubernetes deployment guide for scalable Calls infrastructure -* :doc:`Calls Troubleshooting ` - Comprehensive troubleshooting guide for common issues \ No newline at end of file +* :doc:`Calls Deployment and Configuration ` - Main deployment overview and architecture guide for Mattermost Calls +* :doc:`RTCD Setup and Configuration ` - Real-time communication daemon setup for enterprise deployments +* :doc:`Calls Offloader Setup and Configuration ` - Configure call recording and transcription services +* :doc:`Calls Metrics and Monitoring ` - Performance monitoring with Prometheus and Grafana +* :doc:`Calls Deployment on Kubernetes ` - Kubernetes deployment guide for scalable Calls infrastructure +* :doc:`Calls Troubleshooting ` - Comprehensive troubleshooting guide for common issues \ No newline at end of file diff --git a/source/configure/calls-rtcd-setup.md b/source/configure/calls-rtcd-setup.md index d63ed4a94a4..6f7fef5d46e 100644 --- a/source/configure/calls-rtcd-setup.md +++ b/source/configure/calls-rtcd-setup.md @@ -1,17 +1,10 @@ # RTCD Setup and Configuration -```{include} ../_static/badges/ent-only.md +```{include} ../_static/badges/calls-rtcd-ent-only.md ``` This guide provides detailed instructions for setting up, configuring, and validating a Mattermost Calls deployment using the dedicated RTCD service. -- [Prerequisites](#prerequisites) -- [Installation and deployment](#installation-and-deployment) -- [Configuration](#configuration) -- [Validation and testing](#validation-and-testing) -- [Horizontal scaling](#horizontal-scaling) -- [Integration with Mattermost](#integration-with-mattermost) - ## Prerequisites Before deploying RTCD, ensure you have: From 5112c11740e5efcfbdec3f8aeefc8fa52f04427c Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Wed, 11 Jun 2025 16:40:54 -0400 Subject: [PATCH 24/37] Fixing navigation breaks From MD to RST to MD transition --- source/configure/calls-deployment.md | 36 ++++----- source/configure/calls-kubernetes.md | 20 ++--- source/configure/calls-metrics-monitoring.md | 13 ++-- source/configure/calls-offloader-setup.md | 12 +-- source/configure/calls-overview.rst | 12 +-- source/configure/calls-rtcd-setup.md | 14 ++-- source/configure/calls-troubleshooting.md | 78 +++----------------- 7 files changed, 64 insertions(+), 121 deletions(-) diff --git a/source/configure/calls-deployment.md b/source/configure/calls-deployment.md index 888f925e5a1..8b7424c678b 100644 --- a/source/configure/calls-deployment.md +++ b/source/configure/calls-deployment.md @@ -1,4 +1,4 @@ -# Deploy Mattermost Calls +# Calls Deployment Overview ```{include} ../_static/badges/allplans-cloud-selfhosted.md ``` @@ -10,11 +10,11 @@ This document provides an overview of Mattermost Calls deployment options for se For detailed information on specific topics, please refer to these specialized guides: -- [RTCD Setup and Configuration](calls-rtcd-setup.html): Comprehensive guide for setting up the dedicated RTCD service -- [Calls Offloader Setup and Configuration](calls-offloader-setup.html): Comprehensive guide for setting up the calls-offloader service for recording and transcription -- [Calls Troubleshooting](calls-troubleshooting.html): Detailed troubleshooting steps and debugging techniques -- [Calls Metrics and Monitoring](calls-metrics-monitoring.html): Guide to monitoring Calls performance using metrics and observability -- [Calls Deployment on Kubernetes](calls-kubernetes.html): Detailed guide for deploying Calls in Kubernetes environments +- {doc}`RTCD Setup and Configuration `: Comprehensive guide for setting up the dedicated RTCD service +- {doc}`Calls Offloader Setup and Configuration `: Comprehensive guide for setting up the calls-offloader service for recording and transcription +- {doc}`Calls Troubleshooting `: Detailed troubleshooting steps and debugging techniques +- {doc}`Calls Metrics and Monitoring `: Guide to monitoring Calls performance using metrics and observability +- {doc}`Calls Deployment on Kubernetes `: Detailed guide for deploying Calls in Kubernetes environments ## About Mattermost Calls @@ -36,8 +36,8 @@ Mattermost Calls provides integrated audio calling and screen sharing capabiliti ## Key Components - **Calls plugin**: The main plugin that enables calls functionality. Installed by default in Mattermost self-hosted deployments. -- **RTCD service**: Optional dedicated service for offloading media processing (Enterprise feature). Typically deployed to dedicated servers or containers. See [RTCD Setup and Configuration](calls-rtcd-setup.html) for details. -- **calls-offloader**: Service for call recording and transcription (if enabled). Typically deployed to dedicated servers. See [Calls Offloader Setup and Configuration](calls-offloader-setup.html) for setup and troubleshooting details. +- **RTCD service**: Optional dedicated service for offloading media processing (Enterprise feature). Typically deployed to dedicated servers or containers. See [RTCD Setup and Configuration](calls-rtcd-setup.md) for details. +- **calls-offloader**: Service for call recording and transcription (if enabled). Typically deployed to dedicated servers. See [Calls Offloader Setup and Configuration](calls-offloader-setup.md) for setup and troubleshooting details. ## Network Requirements @@ -134,7 +134,7 @@ Mattermost Calls provides integrated audio calling and screen sharing capabiliti -For complete network requirements, see the [RTCD Setup and Configuration](calls-rtcd-setup.html) guide. +For complete network requirements, see the [RTCD Setup and Configuration](calls-rtcd-setup.md) guide. #### Air-gapped deployments @@ -189,7 +189,7 @@ Dedicated RTCD services handle media routing for high availability. ### Kubernetes Deployments -RTCD is the only officially supported approach for Kubernetes deployments. For detailed information on deploying Mattermost Calls in Kubernetes environments, including Helm chart configurations, resource requirements, and scaling considerations, see the [Calls Deployment on Kubernetes](calls-kubernetes.html) guide. +RTCD is the only officially supported approach for Kubernetes deployments. For detailed information on deploying Mattermost Calls in Kubernetes environments, including Helm chart configurations, resource requirements, and scaling considerations, see the [Calls Deployment on Kubernetes](calls-kubernetes.md) guide. ## When to Use RTCD @@ -201,7 +201,7 @@ The dedicated RTCD service (available with Enterprise license) is recommended fo - **Call stability**: Calls continue even if Mattermost server needs to restart - **Kubernetes deployments**: Required for officially supported Kubernetes deployments -For detailed RTCD setup instructions, see the [RTCD Setup and Configuration](calls-rtcd-setup.html) guide. +For detailed RTCD setup instructions, see the [RTCD Setup and Configuration](calls-rtcd-setup.md) guide. ## Call Recording and Transcription @@ -428,7 +428,7 @@ Calls performance primarily depends on: - **Network bandwidth**: Both incoming and outgoing traffic increases with participant count - **Active speakers**: Unmuted participants require significantly more resources -For detailed performance metrics, benchmarks, and monitoring guidance, see the [Calls Metrics and Monitoring](calls-metrics-monitoring.html) guide. +For detailed performance metrics, benchmarks, and monitoring guidance, see the [Calls Metrics and Monitoring](calls-metrics-monitoring.md) guide. ## Frequently Asked Questions @@ -461,7 +461,7 @@ Generally clients should connect directly to either Mattermost or, if deployed, The plugin can function in different modes. By default calls are handled completely by the plugin which runs as part of Mattermost. It's also possible to use a dedicated service to offload the computational and bandwidth costs and scale further (Enterprise only). -See [RTCD Setup and Configuration](calls-rtcd-setup.html) for more details on the dedicated RTCD service. +See [RTCD Setup and Configuration](calls-rtcd-setup.md) for more details on the dedicated RTCD service. **Can the traffic between Mattermost and `rtcd` be kept internal or should it be opened to the public?** @@ -490,11 +490,11 @@ When [test mode](https://docs.mattermost.com/configure/plugins-configuration-set ## Troubleshooting -For comprehensive troubleshooting steps and debugging techniques, please refer to the [Calls Troubleshooting](calls-troubleshooting.html) guide. +For comprehensive troubleshooting steps and debugging techniques, please refer to the [Calls Troubleshooting](calls-troubleshooting.md) guide. ## Next Steps -1. For detailed setup instructions, see [RTCD Setup and Configuration](calls-rtcd-setup.html) -2. For monitoring guidance, see [Calls Metrics and Monitoring](calls-metrics-monitoring.html) -3. If you encounter issues, see [Calls Troubleshooting](calls-troubleshooting.html) -4. For Kubernetes deployments, see [Calls Deployment on Kubernetes](calls-kubernetes.html) +1. For detailed setup instructions, see [RTCD Setup and Configuration](calls-rtcd-setup.md) +2. For monitoring guidance, see [Calls Metrics and Monitoring](calls-metrics-monitoring.md) +3. If you encounter issues, see [Calls Troubleshooting](calls-troubleshooting.md) +4. For Kubernetes deployments, see [Calls Deployment on Kubernetes](calls-kubernetes.md) diff --git a/source/configure/calls-kubernetes.md b/source/configure/calls-kubernetes.md index 92d1d924e0a..b45fda1461c 100644 --- a/source/configure/calls-kubernetes.md +++ b/source/configure/calls-kubernetes.md @@ -18,9 +18,9 @@ This diagram shows how the RTCD standalone service can be deployed in a Kubernet 1. Calls traffic is handled by dedicated RTCD pods 2. RTCD services are exposed through load balancers 3. Scaling is managed through Kubernetes deployment configurations -4. Call recording and transcription is handled by the calls-offloader service (see [Calls Offloader Setup and Configuration](calls-offloader-setup.html)) +4. Call recording and transcription is handled by the calls-offloader service (see [Calls Offloader Setup and Configuration](calls-offloader-setup.md)) -If Mattermost isn't already deployed in your Kubernetes cluster and you want to use this deployment type, visit the [Kubernetes operator guide](/install/mattermost-kubernetes-operator.html). +If Mattermost isn't already deployed in your Kubernetes cluster and you want to use this deployment type, visit the [Kubernetes operator guide](/install/mattermost-kubernetes-operator.md). ## Helm Chart Deployment @@ -140,7 +140,7 @@ We recommend deploying Prometheus and Grafana alongside your Calls deployment: 2. Import the official Mattermost Calls dashboard to Grafana 3. Set up alerts for CPU usage, connection failures, and error rates -For detailed information on metrics collection and monitoring, see the [Calls Metrics and Monitoring](calls-metrics-monitoring.html) guide. +For detailed information on metrics collection and monitoring, see the [Calls Metrics and Monitoring](calls-metrics-monitoring.md) guide. ## Troubleshooting @@ -151,13 +151,13 @@ For Kubernetes-specific troubleshooting: 3. Ensure UDP traffic is properly routed through your ingress/load balancer 4. Verify network policies allow required communication paths -For detailed troubleshooting steps, see the [Calls Troubleshooting](calls-troubleshooting.html) guide. +For detailed troubleshooting steps, see the [Calls Troubleshooting](calls-troubleshooting.md) guide. ## Other Calls Documentation -- [Calls Overview](calls-deployment.html): Overview of deployment options and architecture -- [RTCD Setup and Configuration](calls-rtcd-setup.html): Comprehensive guide for setting up the dedicated RTCD service -- [Calls Offloader Setup and Configuration](calls-offloader-setup.html): Setup guide for call recording and transcription -- [Calls Metrics and Monitoring](calls-metrics-monitoring.html): Guide to monitoring Calls performance using metrics and observability -- [Calls Troubleshooting](calls-troubleshooting.html): Detailed troubleshooting steps and debugging techniques -3. If you encounter issues, see [Calls Troubleshooting](calls-troubleshooting.html) \ No newline at end of file +- [Calls Overview](calls-deployment.md): Overview of deployment options and architecture +- [RTCD Setup and Configuration](calls-rtcd-setup.md): Comprehensive guide for setting up the dedicated RTCD service +- [Calls Offloader Setup and Configuration](calls-offloader-setup.md): Setup guide for call recording and transcription +- [Calls Metrics and Monitoring](calls-metrics-monitoring.md): Guide to monitoring Calls performance using metrics and observability +- [Calls Troubleshooting](calls-troubleshooting.md): Detailed troubleshooting steps and debugging techniques +3. If you encounter issues, see [Calls Troubleshooting](calls-troubleshooting.md) \ No newline at end of file diff --git a/source/configure/calls-metrics-monitoring.md b/source/configure/calls-metrics-monitoring.md index 43e6a7a735b..ed56b878717 100644 --- a/source/configure/calls-metrics-monitoring.md +++ b/source/configure/calls-metrics-monitoring.md @@ -8,9 +8,8 @@ This guide provides detailed information on monitoring Mattermost Calls performa - [Metrics overview](#metrics-overview) - [Setting up monitoring](#setting-up-monitoring) - [Key metrics to monitor](#key-metrics-to-monitor) -- [Grafana dashboards](#grafana-dashboards) -- [Alerting recommendations](#alerting-recommendations) - [Performance baselines](#performance-baselines) +- [Troubleshooting metrics collection](#troubleshooting-metrics-collection) ## Metrics Overview @@ -268,10 +267,10 @@ Each target should show status "UP" in green. If a target shows "DOWN" or errors ## Other Calls Documentation -- [Calls Overview](calls-deployment.html): Overview of deployment options and architecture -- [RTCD Setup and Configuration](calls-rtcd-setup.html): Comprehensive guide for setting up the dedicated RTCD service -- [Calls Offloader Setup and Configuration](calls-offloader-setup.html): Setup guide for call recording and transcription -- [Calls Deployment on Kubernetes](calls-kubernetes.html): Detailed guide for deploying Calls in Kubernetes environments -- [Calls Troubleshooting](calls-troubleshooting.html): Detailed troubleshooting steps and debugging techniques +- [Calls Overview](calls-deployment.md): Overview of deployment options and architecture +- [RTCD Setup and Configuration](calls-rtcd-setup.md): Comprehensive guide for setting up the dedicated RTCD service +- [Calls Offloader Setup and Configuration](calls-offloader-setup.md): Setup guide for call recording and transcription +- [Calls Deployment on Kubernetes](calls-kubernetes.md): Detailed guide for deploying Calls in Kubernetes environments +- [Calls Troubleshooting](calls-troubleshooting.md): Detailed troubleshooting steps and debugging techniques Configure Prometheus storage accordingly to balance disk usage with retention needs. \ No newline at end of file diff --git a/source/configure/calls-offloader-setup.md b/source/configure/calls-offloader-setup.md index 79dd55e3c0c..e32c2b32e32 100644 --- a/source/configure/calls-offloader-setup.md +++ b/source/configure/calls-offloader-setup.md @@ -356,13 +356,13 @@ tail -f /opt/calls-offloader/calls-offloader.log ### Performance Monitoring -Monitor calls-offloader performance and resource usage to ensure optimal operation. See [Calls Metrics and Monitoring](calls-metrics-monitoring.html) for details on setting up metrics and observability. +Monitor calls-offloader performance and resource usage to ensure optimal operation. See [Calls Metrics and Monitoring](calls-metrics-monitoring.md) for details on setting up metrics and observability. ## Other Calls Documentation -- [Calls Overview](calls-deployment.html): Overview of deployment options and architecture -- [RTCD Setup and Configuration](calls-rtcd-setup.html): Comprehensive guide for setting up the dedicated RTCD service -- [Calls Metrics and Monitoring](calls-metrics-monitoring.html): Guide to monitoring Calls performance using metrics and observability -- [Calls Deployment on Kubernetes](calls-kubernetes.html): Detailed guide for deploying Calls in Kubernetes environments -- [Calls Troubleshooting](calls-troubleshooting.html): Detailed troubleshooting steps and debugging techniques +- [Calls Overview](calls-deployment.md): Overview of deployment options and architecture +- [RTCD Setup and Configuration](calls-rtcd-setup.md): Comprehensive guide for setting up the dedicated RTCD service +- [Calls Metrics and Monitoring](calls-metrics-monitoring.md): Guide to monitoring Calls performance using metrics and observability +- [Calls Deployment on Kubernetes](calls-kubernetes.md): Detailed guide for deploying Calls in Kubernetes environments +- [Calls Troubleshooting](calls-troubleshooting.md): Detailed troubleshooting steps and debugging techniques - [calls-offloader performance documentation](https://github.com/mattermost/calls-offloader/blob/master/docs/performance.md): Detailed performance tuning and monitoring recommendations \ No newline at end of file diff --git a/source/configure/calls-overview.rst b/source/configure/calls-overview.rst index 5ba7f5fc8db..e73f5bd7015 100644 --- a/source/configure/calls-overview.rst +++ b/source/configure/calls-overview.rst @@ -19,9 +19,9 @@ Mattermost Calls provides integrated audio calling and screen sharing capabiliti **Calls Documentation:** -* :doc:`Calls Deployment and Configuration ` - Main deployment overview and architecture guide for Mattermost Calls -* :doc:`RTCD Setup and Configuration ` - Real-time communication daemon setup for enterprise deployments -* :doc:`Calls Offloader Setup and Configuration ` - Configure call recording and transcription services -* :doc:`Calls Metrics and Monitoring ` - Performance monitoring with Prometheus and Grafana -* :doc:`Calls Deployment on Kubernetes ` - Kubernetes deployment guide for scalable Calls infrastructure -* :doc:`Calls Troubleshooting ` - Comprehensive troubleshooting guide for common issues \ No newline at end of file +* :doc:`Calls Deployment Overview ` - Main deployment overview and architecture guide for Mattermost Calls +* :doc:`RTCD Setup and Configuration ` - Real-time communication daemon setup for enterprise deployments +* :doc:`Calls Offloader Setup and Configuration ` - Configure call recording and transcription services +* :doc:`Calls Metrics and Monitoring ` - Performance monitoring with Prometheus and Grafana +* :doc:`Calls Deployment on Kubernetes ` - Kubernetes deployment guide for scalable Calls infrastructure +* :doc:`Calls Troubleshooting ` - Comprehensive troubleshooting guide for common issues diff --git a/source/configure/calls-rtcd-setup.md b/source/configure/calls-rtcd-setup.md index 6f7fef5d46e..1feb89e9423 100644 --- a/source/configure/calls-rtcd-setup.md +++ b/source/configure/calls-rtcd-setup.md @@ -348,7 +348,7 @@ After deploying RTCD, validate the installation: 4. **Monitor metrics**: - Refer to [Calls Metrics and Monitoring](calls-metrics-monitoring.html) for setting up Calls metrics and monitoring. + Refer to [Calls Metrics and Monitoring](calls-metrics-monitoring.md) for setting up Calls metrics and monitoring. ## Horizontal Scaling @@ -398,10 +398,10 @@ Once RTCD is properly set up and validated, configure Mattermost to use it: ## Other Calls Documentation -- [Calls Overview](calls-deployment.html): Overview of deployment options and architecture -- [Calls Offloader Setup and Configuration](calls-offloader-setup.html): Setup guide for call recording and transcription -- [Calls Metrics and Monitoring](calls-metrics-monitoring.html): Guide to monitoring Calls performance using metrics and observability -- [Calls Deployment on Kubernetes](calls-kubernetes.html): Detailed guide for deploying Calls in Kubernetes environments -- [Calls Troubleshooting](calls-troubleshooting.html): Detailed troubleshooting steps and debugging techniques +- [Calls Overview](calls-deployment.md): Overview of deployment options and architecture +- [Calls Offloader Setup and Configuration](calls-offloader-setup.md): Setup guide for call recording and transcription +- [Calls Metrics and Monitoring](calls-metrics-monitoring.md): Guide to monitoring Calls performance using metrics and observability +- [Calls Deployment on Kubernetes](calls-kubernetes.md): Detailed guide for deploying Calls in Kubernetes environments +- [Calls Troubleshooting](calls-troubleshooting.md): Detailed troubleshooting steps and debugging techniques -For detailed Mattermost Calls configuration options, see the [Calls Plugin Configuration Settings](plugins-configuration-settings.html#calls) documentation. \ No newline at end of file +For detailed Mattermost Calls configuration options, see the [Calls Plugin Configuration Settings](plugins-configuration-settings.rst#calls) documentation. \ No newline at end of file diff --git a/source/configure/calls-troubleshooting.md b/source/configure/calls-troubleshooting.md index 3c533e63311..e2389ce0d77 100644 --- a/source/configure/calls-troubleshooting.md +++ b/source/configure/calls-troubleshooting.md @@ -9,8 +9,6 @@ This guide provides comprehensive troubleshooting steps for Mattermost Calls, pa - [Connectivity troubleshooting](#connectivity-troubleshooting) - [Log analysis](#log-analysis) - [Performance issues](#performance-issues) -- [Debugging tools](#debugging-tools) -- [Advanced diagnostics](#advanced-diagnostics) ## Common Issues @@ -68,7 +66,7 @@ This guide provides comprehensive troubleshooting steps for Mattermost Calls, pa 1. **Server resources**: - Check CPU usage on RTCD servers - high CPU can cause quality issues - - Refer to the [Calls Metrics and Monitoring](calls-metrics-monitoring.html) guide for detailed instructions on monitoring and optimizing performance + - Refer to the [Calls Metrics and Monitoring](calls-metrics-monitoring.md) guide for detailed instructions on monitoring and optimizing performance - Monitor network bandwidth usage 2. **Network congestion**: @@ -242,7 +240,7 @@ If RTCD servers show high CPU usage: 1. **Check concurrent calls and participants**: - Access the Prometheus metrics endpoint to see active sessions - - Compare with the benchmark data in the documentation + - Compare with the benchmark data in the {doc}`Calls Metrics and Monitoring ` documentation's Performance Baselines section 2. **Profile CPU usage** (Linux): @@ -314,7 +312,7 @@ If you suspect network bandwidth issues: ## Recording and Transcription Issues -For troubleshooting calls-offloader service issues including recording and transcription problems, see the [Calls Offloader Setup and Configuration](calls-offloader-setup.html#troubleshooting) guide. +For troubleshooting calls-offloader service issues including recording and transcription problems, see the [Calls Offloader Setup and Configuration](calls-offloader-setup.md#troubleshooting) guide. ### Calls-Offloader Docker Debugging @@ -353,66 +351,13 @@ docker logs - **Inspect container configuration**: `docker inspect ` for detailed container settings - **Check container health**: `docker inspect | grep Health` if health checks are configured -## Debugging Tools - -### Prometheus Metrics Analysis +## Prometheus Metrics Analysis Use Prometheus metrics for real-time and historical performance data: -Import the official [Mattermost Calls dashboard](https://github.com/mattermost/mattermost-performance-assets/blob/master/grafana/mattermost-calls-performance-monitoring.json) into Grafana for visualization. - -## Advanced Diagnostics - -### WebRTC Diagnostic Commands - -For detailed WebRTC diagnostics: - -1. **Test STUN server connectivity**: - - ```bash - # Using stun-client (you may need to install it) - stun-client stun.global.calls.mattermost.com - ``` - - This should return your public IP address if STUN is working correctly. - -2. **Verify TURN server**: - - ```bash - # Using turnutils_uclient (part of coturn) - turnutils_uclient -v -s your-turn-server -u username -p password - ``` - - This tests if your TURN server is correctly configured. - -3. **Test end-to-end latency**: - - Between client locations and RTCD server: - - ```bash - ping -c 10 your-rtcd-server - ``` - - Look for consistent, low latency (<100ms ideally for voice calls). - -### Client-Side Testing Tools - -Tools to help diagnose client-side issues: - -1. **WebRTC Troubleshooter**: - - Direct users to [WebRTC Troubleshooter](https://test.webrtc.org/) for browser capability testing. - -2. **Network Quality Tests**: - - Use [Speedtest](https://www.speedtest.net/) or similar to check internet connection quality. - -3. **Browser-Specific WebRTC Info**: - - - Chrome: chrome://webrtc-internals - - Firefox: about:webrtc +For detailed setup instructions on configuring Prometheus and Grafana for Calls monitoring, see the {doc}`Calls Metrics and Monitoring ` guide. -### When to Contact Support +## When to Contact Support Consider contacting Mattermost Support when: @@ -433,9 +378,8 @@ When contacting support, please include: ## Other Calls Documentation -- [Calls Overview](calls-deployment.html): Overview of deployment options and architecture -- [RTCD Setup and Configuration](calls-rtcd-setup.html): Comprehensive guide for setting up the dedicated RTCD service -- [Calls Offloader Setup and Configuration](calls-offloader-setup.html): Setup guide for call recording and transcription -- [Calls Metrics and Monitoring](calls-metrics-monitoring.html): Guide to monitoring Calls performance using metrics and observability -- [Calls Deployment on Kubernetes](calls-kubernetes.html): Detailed guide for deploying Calls in Kubernetes environments -- Monitoring dashboards screenshots \ No newline at end of file +- [Calls Overview](calls-deployment.md): Overview of deployment options and architecture +- [RTCD Setup and Configuration](calls-rtcd-setup.md): Comprehensive guide for setting up the dedicated RTCD service +- [Calls Offloader Setup and Configuration](calls-offloader-setup.md): Setup guide for call recording and transcription +- [Calls Metrics and Monitoring](calls-metrics-monitoring.md): Guide to monitoring Calls performance using metrics and observability +- [Calls Deployment on Kubernetes](calls-kubernetes.md): Detailed guide for deploying Calls in Kubernetes environments \ No newline at end of file From c9ff6d238a6d46c3b4f84a6f47b58dcf4dd43df9 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Sat, 14 Jun 2025 17:56:17 -0400 Subject: [PATCH 25/37] Add air-gap Docker registry setup for Calls offloader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhanced calls documentation with comprehensive air-gapped deployment guidance: - Added Docker registry setup overview to calls-deployment.md - Added detailed air-gap configuration section to calls-offloader-setup.md - Includes setup scripts, manual configuration, verification, and troubleshooting - Addresses Docker image requirements for recording and transcription services 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- source/configure/calls-deployment.md | 24 +++ source/configure/calls-offloader-setup.md | 206 ++++++++++++++++++++++ 2 files changed, 230 insertions(+) diff --git a/source/configure/calls-deployment.md b/source/configure/calls-deployment.md index 8b7424c678b..24928674374 100644 --- a/source/configure/calls-deployment.md +++ b/source/configure/calls-deployment.md @@ -421,6 +421,30 @@ Mattermost Calls can function in air-gapped environments. Exposing Calls to the +- Users should connect from within the private/local network. This can be done on-premises, through a VPN, or via virtual machines. +- Configuring a STUN server is unnecessary, as all connections occur within the local network. +- The ICE Host Override configuration setting can be optionally set with a local IP address (e.g., 192.168.1.45), depending on the specific network configuration and topology. + +### Docker Registry Setup for Air-Gapped Environments + +When using the calls-offloader service for call recording and transcription in air-gapped environments, you need to set up a local Docker registry since the service creates Docker containers that normally pull images from Docker Hub. + +**Required Docker Images:** +- `mattermost/calls-offloader:v0.9.3` (or latest version) +- `mattermost/calls-transcriber:latest` +- `registry:2` (for the local Docker registry) + +**Setup Overview:** +1. **Preparation phase** (on internet-connected machine): Download and prepare Docker images +2. **Air-gap deployment phase**: Transfer and deploy the local registry with pre-loaded images + +**Key Configuration Changes:** +- Configure Docker daemon to allow insecure registries: `{"insecure-registries": ["localhost:5000"]}` +- Update calls-offloader configuration to use local registry: `image_registry = "localhost:5000/mattermost"` + +For detailed step-by-step instructions, including setup scripts, manual configuration, and troubleshooting, see the comprehensive air-gap setup guide in the [Calls Offloader Setup and Configuration](calls-offloader-setup.md#air-gapped-deployments) documentation. + +## Performance Considerations Calls performance primarily depends on: diff --git a/source/configure/calls-offloader-setup.md b/source/configure/calls-offloader-setup.md index e32c2b32e32..cb7d6ddb5f3 100644 --- a/source/configure/calls-offloader-setup.md +++ b/source/configure/calls-offloader-setup.md @@ -358,6 +358,212 @@ tail -f /opt/calls-offloader/calls-offloader.log Monitor calls-offloader performance and resource usage to ensure optimal operation. See [Calls Metrics and Monitoring](calls-metrics-monitoring.md) for details on setting up metrics and observability. +## Air-Gapped Deployments + +When deploying calls-offloader in air-gapped environments, you need to set up a local Docker registry since the service creates Docker containers that normally pull images from Docker Hub. + +### Overview + +The calls-offloader service creates Docker containers to handle: +- **Call Recording**: Creates containers to record audio/video from calls +- **Call Transcription**: Creates containers to transcribe recorded calls using speech-to-text + +These containers are typically pulled from the `mattermost` registry on Docker Hub, but in air-gapped networks, you need to: +1. Set up a local Docker registry +2. Pre-load the required Docker images +3. Configure the calls-offloader to use the local registry + +### Required Docker Images + +The following Docker images are needed for full calls functionality: + +- `mattermost/calls-offloader:v0.9.3` (or latest version) +- `mattermost/calls-transcriber:latest` +- `registry:2` (for the local Docker registry) + +### Setup Process + +#### Phase 1: Preparation (Internet-Connected Environment) + +Run this phase on a machine with internet access to download and prepare the Docker images. + +1. **Run the setup script**: + ```bash + chmod +x air-gap-docker-registry-setup.sh + sudo ./air-gap-docker-registry-setup.sh + ``` + +2. **What the script does**: + - Sets up a local Docker registry on port 5000 + - Downloads required Mattermost Docker images + - Pushes images to the local registry + - Configures Docker daemon for insecure registry access + - Creates deployment scripts for the air-gapped environment + +3. **Export the registry data**: + ```bash + # Create an archive of the registry data + sudo tar -czf docker-registry-data.tar.gz -C /opt/docker-registry/data . + + # Also backup the registry container image + docker save registry:2 | gzip > registry-image.tar.gz + ``` + +#### Phase 2: Air-Gap Deployment + +Transfer the following files to your air-gapped network: +- `docker-registry-data.tar.gz` +- `registry-image.tar.gz` +- `deploy-airgap-calls.sh` (created by setup script) + +1. **Load the registry container**: + ```bash + gunzip -c registry-image.tar.gz | docker load + ``` + +2. **Set up the registry data**: + ```bash + sudo mkdir -p /opt/docker-registry/data + sudo tar -xzf docker-registry-data.tar.gz -C /opt/docker-registry/data + ``` + +3. **Start the local registry**: + ```bash + docker run -d \ + --name local-registry \ + --restart=always \ + -p 5000:5000 \ + -v /opt/docker-registry/data:/var/lib/registry \ + registry:2 + ``` + +4. **Configure Docker and calls-offloader**: + ```bash + sudo /opt/deploy-airgap-calls.sh + ``` + +### Manual Configuration + +If you prefer to configure manually instead of using the scripts: + +#### 1. Docker Daemon Configuration + +Create or update `/etc/docker/daemon.json`: +```json +{ + "insecure-registries": ["localhost:5000"] +} +``` + +Restart Docker: +```bash +sudo systemctl restart docker +``` + +#### 2. Calls-Offloader Configuration + +Update `/opt/calls-offloader/calls-offloader.toml`: + +```toml +[jobs] +# Change this line: +image_registry = "mattermost" + +# To this: +image_registry = "localhost:5000/mattermost" +``` + +Restart the calls-offloader service: +```bash +sudo systemctl restart calls-offloader +``` + +### Verification + +#### Test Registry Access +```bash +# List available repositories +curl http://localhost:5000/v2/_catalog + +# Test pulling an image +docker pull localhost:5000/mattermost/calls-offloader:latest +``` + +#### Test Calls Functionality + +1. **Check calls-offloader logs**: + ```bash + sudo journalctl -u calls-offloader -f + ``` + +2. **Verify calls-offloader API**: + ```bash + curl http://localhost:4545/version + ``` + +3. **Test recording job creation** (requires proper Mattermost integration): + - Start a call in Mattermost + - Enable recording + - Check that Docker containers are created for recording jobs + +### Troubleshooting Air-Gap Deployments + +#### Common Issues + +1. **Registry not accessible**: + - Check that the registry container is running: `docker ps | grep registry` + - Verify Docker daemon configuration includes insecure registry + - Check firewall settings on port 5000 + +2. **Image pull failures**: + - Verify images are in the registry: `curl http://localhost:5000/v2/_catalog` + - Check Docker daemon logs: `sudo journalctl -u docker` + +3. **calls-offloader fails to create jobs**: + - Check calls-offloader logs: `sudo journalctl -u calls-offloader` + - Verify the `image_registry` configuration in calls-offloader.toml + - Ensure the calls-offloader service can reach the registry + +#### Log Locations + +- Setup script logs: `/tmp/air-gap-registry-setup.log` +- calls-offloader logs: `/opt/calls-offloader/calls-offloader.log` +- Docker daemon logs: `sudo journalctl -u docker` +- Registry container logs: `docker logs local-registry` + +#### Security Considerations + +1. **Insecure Registry**: The setup uses an insecure HTTP registry for simplicity. For production, consider: + - Setting up TLS certificates for the registry + - Implementing authentication + - Using proper firewall rules + +2. **Network Access**: Ensure the registry is only accessible within your private network + +3. **Image Verification**: Consider implementing image signing and verification processes + +#### Advanced Configuration + +**Using a Different Registry Host** + +If you want to run the registry on a different host: + +```bash +export REGISTRY_HOST="registry.internal.domain" +export REGISTRY_PORT="5000" +./air-gap-docker-registry-setup.sh +``` + +**Custom Image Versions** + +To use specific versions of the calls images: + +```bash +export CALLS_OFFLOADER_VERSION="v0.8.0" +export CALLS_TRANSCRIBER_VERSION="v1.2.0" +./air-gap-docker-registry-setup.sh +``` + ## Other Calls Documentation - [Calls Overview](calls-deployment.md): Overview of deployment options and architecture From 9d55df900993e4bb1d28a7d9c3b4f68a0ee1f176 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Sat, 14 Jun 2025 18:12:16 -0400 Subject: [PATCH 26/37] Replace script references with direct Docker commands in air-gap setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed references to non-existent setup scripts and replaced with: - Direct Docker commands for registry setup and image management - Manual configuration steps instead of script dependencies - Concrete examples for custom registry hosts and image versions Makes the air-gap setup more reliable and self-contained. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- source/configure/calls-offloader-setup.md | 70 ++++++++++++++++------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/source/configure/calls-offloader-setup.md b/source/configure/calls-offloader-setup.md index cb7d6ddb5f3..a76ec1c148d 100644 --- a/source/configure/calls-offloader-setup.md +++ b/source/configure/calls-offloader-setup.md @@ -387,18 +387,34 @@ The following Docker images are needed for full calls functionality: Run this phase on a machine with internet access to download and prepare the Docker images. -1. **Run the setup script**: +1. **Set up a local Docker registry**: ```bash - chmod +x air-gap-docker-registry-setup.sh - sudo ./air-gap-docker-registry-setup.sh + # Create registry data directory + sudo mkdir -p /opt/docker-registry/data + + # Start a local Docker registry + docker run -d \ + --name local-registry \ + --restart=always \ + -p 5000:5000 \ + -v /opt/docker-registry/data:/var/lib/registry \ + registry:2 ``` -2. **What the script does**: - - Sets up a local Docker registry on port 5000 - - Downloads required Mattermost Docker images - - Pushes images to the local registry - - Configures Docker daemon for insecure registry access - - Creates deployment scripts for the air-gapped environment +2. **Download and push required images**: + ```bash + # Pull required images from Docker Hub + docker pull mattermost/calls-offloader:v0.9.3 + docker pull mattermost/calls-transcriber:latest + + # Tag images for local registry + docker tag mattermost/calls-offloader:v0.9.3 localhost:5000/mattermost/calls-offloader:v0.9.3 + docker tag mattermost/calls-transcriber:latest localhost:5000/mattermost/calls-transcriber:latest + + # Push images to local registry + docker push localhost:5000/mattermost/calls-offloader:v0.9.3 + docker push localhost:5000/mattermost/calls-transcriber:latest + ``` 3. **Export the registry data**: ```bash @@ -413,8 +429,7 @@ Run this phase on a machine with internet access to download and prepare the Doc Transfer the following files to your air-gapped network: - `docker-registry-data.tar.gz` -- `registry-image.tar.gz` -- `deploy-airgap-calls.sh` (created by setup script) +- `registry-image.tar.gz` 1. **Load the registry container**: ```bash @@ -437,14 +452,17 @@ Transfer the following files to your air-gapped network: registry:2 ``` -4. **Configure Docker and calls-offloader**: +4. **Configure Docker daemon for insecure registry access**: ```bash - sudo /opt/deploy-airgap-calls.sh + # Create or update Docker daemon configuration + sudo mkdir -p /etc/docker + echo '{"insecure-registries": ["localhost:5000"]}' | sudo tee /etc/docker/daemon.json + sudo systemctl restart docker ``` ### Manual Configuration -If you prefer to configure manually instead of using the scripts: +For reference, here are the individual configuration steps: #### 1. Docker Daemon Configuration @@ -546,22 +564,30 @@ docker pull localhost:5000/mattermost/calls-offloader:latest **Using a Different Registry Host** -If you want to run the registry on a different host: +If you want to run the registry on a different host, replace `localhost:5000` with your registry host in all commands: ```bash -export REGISTRY_HOST="registry.internal.domain" -export REGISTRY_PORT="5000" -./air-gap-docker-registry-setup.sh +# Example: using a dedicated registry server +REGISTRY_HOST="registry.internal.domain:5000" + +# Update Docker daemon configuration +echo "{\"insecure-registries\": [\"$REGISTRY_HOST\"]}" | sudo tee /etc/docker/daemon.json + +# Update calls-offloader configuration +sed -i "s|localhost:5000|$REGISTRY_HOST|g" /opt/calls-offloader/calls-offloader.toml ``` **Custom Image Versions** -To use specific versions of the calls images: +To use specific versions of the calls images, update the version tags in the docker commands: ```bash -export CALLS_OFFLOADER_VERSION="v0.8.0" -export CALLS_TRANSCRIBER_VERSION="v1.2.0" -./air-gap-docker-registry-setup.sh +# Example: using specific versions +OFFLOADER_VERSION="v0.8.0" +TRANSCRIBER_VERSION="v1.2.0" + +docker pull mattermost/calls-offloader:$OFFLOADER_VERSION +docker pull mattermost/calls-transcriber:$TRANSCRIBER_VERSION ``` ## Other Calls Documentation From ad56133a8c4064921ec9e12a07d0c8e28de91256 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Sun, 22 Jun 2025 09:34:17 -0400 Subject: [PATCH 27/37] Updates after testing the process to move images into Air-Gap w Day2 operations --- scripts/air-gap-docker-registry-setup.sh | 366 +++++++++++++++++++++ source/configure/calls-offloader-setup.md | 378 ++++++++++++++++++++-- 2 files changed, 726 insertions(+), 18 deletions(-) create mode 100755 scripts/air-gap-docker-registry-setup.sh diff --git a/scripts/air-gap-docker-registry-setup.sh b/scripts/air-gap-docker-registry-setup.sh new file mode 100755 index 00000000000..52553102863 --- /dev/null +++ b/scripts/air-gap-docker-registry-setup.sh @@ -0,0 +1,366 @@ +#!/bin/bash + +# Air-Gap Docker Registry Setup for Mattermost Calls Offloader +# This script sets up a local Docker registry and pre-loads required images +# for air-gapped network deployments +# +# Usage: ./air-gap-docker-registry-setup.sh +# Example: ./air-gap-docker-registry-setup.sh v0.8.5 v0.6.3 + +set -e + +# Function to display usage +usage() { + echo "Usage: $0 " + echo "" + echo "Arguments:" + echo " recorder-version Version of mattermost/calls-recorder image (e.g., v0.8.5)" + echo " transcriber-version Version of mattermost/calls-transcriber image (e.g., v0.6.3)" + echo "" + echo "Examples:" + echo " $0 v0.8.5 v0.6.3" + echo " $0 v0.9.0 v0.7.0" + echo "" + echo "To find the correct versions for your Calls plugin:" + echo "1. Check your Calls plugin version in System Console > Plugins > Plugin Management" + echo "2. Visit: https://github.com/mattermost/mattermost-plugin-calls/blob/v/plugin.json" + echo "3. Look for 'RecorderImage' and 'TranscriberImage' entries (near the bottom)" + echo "" + echo "Environment variables (optional):" + echo " REGISTRY_HOST Docker registry host (default: localhost)" + echo " REGISTRY_PORT Docker registry port (default: 5000)" + echo " REGISTRY_DATA_DIR Registry data directory (default: /opt/docker-registry/data)" + exit 1 +} + +# Check if required arguments are provided +if [ $# -ne 2 ]; then + echo "ERROR: Missing required arguments" + echo "" + usage +fi + +# Validate version format (should start with 'v' followed by semantic version) +validate_version() { + local version=$1 + local image_name=$2 + + if [[ ! $version =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "ERROR: Invalid version format for $image_name: $version" + echo "Expected format: vX.Y.Z (e.g., v0.8.5)" + exit 1 + fi +} + +# Parse and validate arguments +CALLS_RECORDER_VERSION="$1" +CALLS_TRANSCRIBER_VERSION="$2" + +validate_version "$CALLS_RECORDER_VERSION" "calls-recorder" +validate_version "$CALLS_TRANSCRIBER_VERSION" "calls-transcriber" + +# Configuration variables +REGISTRY_HOST="${REGISTRY_HOST:-localhost}" +REGISTRY_PORT="${REGISTRY_PORT:-5000}" +REGISTRY_DATA_DIR="${REGISTRY_DATA_DIR:-/opt/docker-registry/data}" +REGISTRY_CONFIG_DIR="${REGISTRY_CONFIG_DIR:-/opt/docker-registry/config}" + +LOG_FILE=/tmp/air-gap-registry-setup.log + +echo "Setting up Air-Gap Docker Registry for Mattermost Calls" | tee $LOG_FILE +echo "Recorder version: $CALLS_RECORDER_VERSION" | tee -a $LOG_FILE +echo "Transcriber version: $CALLS_TRANSCRIBER_VERSION" | tee -a $LOG_FILE + +# Function to check if running with internet access (for image pulling phase) +check_internet() { + if ! curl -s --connect-timeout 5 https://hub.docker.com > /dev/null 2>&1; then + echo "ERROR: This script requires internet access during the image preparation phase." | tee -a $LOG_FILE + echo "Please run this script on a machine with internet access first, then transfer the images." | tee -a $LOG_FILE + return 1 + fi +} + +# Function to setup local Docker registry +setup_registry() { + echo "Setting up local Docker registry..." | tee -a $LOG_FILE + + # Create directories + sudo mkdir -p $REGISTRY_DATA_DIR + sudo mkdir -p $REGISTRY_CONFIG_DIR + + # Create registry configuration + cat > /tmp/registry-config.yml << EOF +version: 0.1 +log: + level: info +storage: + filesystem: + rootdirectory: /var/lib/registry +http: + addr: 0.0.0.0:5000 + headers: + X-Content-Type-Options: [nosniff] +EOF + + sudo mv /tmp/registry-config.yml $REGISTRY_CONFIG_DIR/config.yml + + # Pull and run registry container + echo "Starting Docker registry container..." | tee -a $LOG_FILE + docker pull registry:2 >> $LOG_FILE 2>&1 + + # Stop existing registry if running + docker stop local-registry 2>/dev/null || true + docker rm local-registry 2>/dev/null || true + + # Start registry container + docker run -d \ + --name local-registry \ + --restart=always \ + -p $REGISTRY_PORT:5000 \ + -v $REGISTRY_DATA_DIR:/var/lib/registry \ + -v $REGISTRY_CONFIG_DIR/config.yml:/etc/docker/registry/config.yml \ + registry:2 >> $LOG_FILE 2>&1 + + # Wait for registry to be ready + echo "Waiting for registry to be ready..." | tee -a $LOG_FILE + sleep 10 + + # Test registry + if curl -s http://$REGISTRY_HOST:$REGISTRY_PORT/v2/ > /dev/null; then + echo "Local Docker registry is running at $REGISTRY_HOST:$REGISTRY_PORT" | tee -a $LOG_FILE + else + echo "ERROR: Failed to start local Docker registry" | tee -a $LOG_FILE + return 1 + fi +} + +# Function to download and push Mattermost images +setup_mattermost_images() { + echo "Downloading and pushing Mattermost Calls images..." | tee -a $LOG_FILE + + # Images to process + declare -A IMAGES=( + ["mattermost/calls-recorder"]="$CALLS_RECORDER_VERSION" + ["mattermost/calls-transcriber"]="$CALLS_TRANSCRIBER_VERSION" + ) + + for image in "${!IMAGES[@]}"; do + version="${IMAGES[$image]}" + echo "Processing $image:$version..." | tee -a $LOG_FILE + + # Pull from Docker Hub + echo " Pulling $image:$version from Docker Hub..." | tee -a $LOG_FILE + docker pull $image:$version >> $LOG_FILE 2>&1 + + # Tag for local registry + local_tag="$REGISTRY_HOST:$REGISTRY_PORT/$image:$version" + echo " Tagging as $local_tag..." | tee -a $LOG_FILE + docker tag $image:$version $local_tag >> $LOG_FILE 2>&1 + + # Push to local registry + echo " Pushing to local registry..." | tee -a $LOG_FILE + docker push $local_tag >> $LOG_FILE 2>&1 + + # Also tag as 'latest' for convenience + if [ "$version" != "latest" ]; then + latest_tag="$REGISTRY_HOST:$REGISTRY_PORT/$image:latest" + docker tag $image:$version $latest_tag >> $LOG_FILE 2>&1 + docker push $latest_tag >> $LOG_FILE 2>&1 + fi + + echo " Successfully pushed $image:$version to local registry" | tee -a $LOG_FILE + done + + # Create registry data archive for transfer + echo "Creating registry data archive..." | tee -a $LOG_FILE + sudo tar -czf docker-registry-data.tar.gz -C $REGISTRY_DATA_DIR . + + # Create registry container image archive + echo "Creating registry container image archive..." | tee -a $LOG_FILE + docker save -o registry-image.tar registry:2 + gzip registry-image.tar + + echo "Created archives for air-gap transfer:" | tee -a $LOG_FILE + echo " - docker-registry-data.tar.gz" | tee -a $LOG_FILE + echo " - registry-image.tar.gz" | tee -a $LOG_FILE +} + +# Function to configure calls-offloader for local registry +configure_calls_offloader() { + echo "Configuring calls-offloader for local registry..." | tee -a $LOG_FILE + + # Create a modified calls-offloader config + if [ -f "/opt/calls-offloader/calls-offloader.toml" ]; then + sudo cp /opt/calls-offloader/calls-offloader.toml /opt/calls-offloader/calls-offloader.toml.backup + + # Update image_registry setting + sudo sed -i "s/image_registry = \"mattermost\"/image_registry = \"$REGISTRY_HOST:$REGISTRY_PORT\/mattermost\"/" /opt/calls-offloader/calls-offloader.toml + + echo "Updated calls-offloader configuration to use local registry" | tee -a $LOG_FILE + echo "Backup created at /opt/calls-offloader/calls-offloader.toml.backup" | tee -a $LOG_FILE + else + echo "Warning: calls-offloader.toml not found. You'll need to manually configure:" | tee -a $LOG_FILE + echo " image_registry = \"$REGISTRY_HOST:$REGISTRY_PORT/mattermost\"" | tee -a $LOG_FILE + fi +} + +# Function to create docker daemon configuration for insecure registry +configure_docker_daemon() { + echo "Configuring Docker daemon for insecure registry..." | tee -a $LOG_FILE + + # Create or update Docker daemon configuration + sudo mkdir -p /etc/docker + + # Check if daemon.json exists + if [ -f "/etc/docker/daemon.json" ]; then + sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.backup + echo "Backed up existing daemon.json" | tee -a $LOG_FILE + fi + + # Create new daemon.json with insecure registry configuration + cat > /tmp/daemon.json << EOF +{ + "insecure-registries": ["$REGISTRY_HOST:$REGISTRY_PORT"] +} +EOF + + sudo mv /tmp/daemon.json /etc/docker/daemon.json + + # Restart Docker daemon + echo "Restarting Docker daemon..." | tee -a $LOG_FILE + sudo systemctl restart docker >> $LOG_FILE 2>&1 + + # Wait for Docker to restart + sleep 10 + + echo "Docker daemon configured for insecure registry access" | tee -a $LOG_FILE +} + +# Function to verify setup +verify_setup() { + echo "Verifying air-gap setup..." | tee -a $LOG_FILE + + # Check registry is accessible + if curl -s http://$REGISTRY_HOST:$REGISTRY_PORT/v2/_catalog | grep -q repositories; then + echo "✓ Local registry is accessible" | tee -a $LOG_FILE + else + echo "✗ Local registry is not accessible" | tee -a $LOG_FILE + return 1 + fi + + # List available images + echo "Available images in local registry:" | tee -a $LOG_FILE + curl -s http://$REGISTRY_HOST:$REGISTRY_PORT/v2/_catalog | jq '.repositories[]' 2>/dev/null || echo "Could not list repositories (jq not available)" | tee -a $LOG_FILE + + # Test pulling from local registry + test_image="$REGISTRY_HOST:$REGISTRY_PORT/mattermost/calls-offloader:latest" + echo "Testing pull from local registry: $test_image" | tee -a $LOG_FILE + if docker pull $test_image >> $LOG_FILE 2>&1; then + echo "✓ Successfully pulled test image from local registry" | tee -a $LOG_FILE + else + echo "✗ Failed to pull test image from local registry" | tee -a $LOG_FILE + return 1 + fi +} + +# Function to create air-gap deployment script +create_airgap_deployment_script() { + echo "Creating air-gap deployment script..." | tee -a $LOG_FILE + + cat > /tmp/deploy-airgap-calls.sh << 'EOF' +#!/bin/bash + +# Air-Gap Calls Deployment Script +# Run this script on the air-gapped network after setting up the local registry + +REGISTRY_HOST="${REGISTRY_HOST:-localhost}" +REGISTRY_PORT="${REGISTRY_PORT:-5000}" + +echo "Deploying Mattermost Calls in air-gapped environment..." + +# Configure Docker for local registry +sudo mkdir -p /etc/docker +cat > /tmp/daemon.json << EOD +{ + "insecure-registries": ["$REGISTRY_HOST:$REGISTRY_PORT"] +} +EOD +sudo mv /tmp/daemon.json /etc/docker/daemon.json +sudo systemctl restart docker +sleep 10 + +# Update calls-offloader configuration +if [ -f "/opt/calls-offloader/calls-offloader.toml" ]; then + sudo sed -i "s/image_registry = \"mattermost\"/image_registry = \"$REGISTRY_HOST:$REGISTRY_PORT\/mattermost\"/" /opt/calls-offloader/calls-offloader.toml + + # Restart calls-offloader service + sudo systemctl restart calls-offloader + + echo "Calls-offloader configured for air-gap deployment" +else + echo "Warning: /opt/calls-offloader/calls-offloader.toml not found" + echo "Please manually configure image_registry = \"$REGISTRY_HOST:$REGISTRY_PORT/mattermost\"" +fi + +echo "Air-gap deployment configuration complete" +echo "" +echo "IMPORTANT: Additional configuration required on Mattermost server:" +echo "On your Mattermost server, add this environment variable:" +echo " MM_CALLS_JOB_SERVICE_IMAGE_REGISTRY=\"$REGISTRY_HOST:$REGISTRY_PORT/mattermost\"" +echo "" +echo "Add it to /opt/mattermost/config/mattermost.environment and restart Mattermost:" +echo " echo 'MM_CALLS_JOB_SERVICE_IMAGE_REGISTRY=\"$REGISTRY_HOST:$REGISTRY_PORT/mattermost\"' | sudo tee -a /opt/mattermost/config/mattermost.environment" +echo " sudo systemctl restart mattermost" +EOF + + chmod +x /tmp/deploy-airgap-calls.sh + mv /tmp/deploy-airgap-calls.sh ./deploy-airgap-calls.sh + + echo "Air-gap deployment script created at ./deploy-airgap-calls.sh" | tee -a $LOG_FILE +} + +# Main execution +main() { + echo "Starting air-gap Docker registry setup..." | tee -a $LOG_FILE + + # Check if we have internet access for image pulling + if ! check_internet; then + echo "Skipping image download phase - run this script with internet access first" | tee -a $LOG_FILE + else + # Setup local registry + setup_registry + + # Download and push images + setup_mattermost_images + fi + + # Configure Docker daemon + configure_docker_daemon + + # Configure calls-offloader + configure_calls_offloader + + # Verify setup + verify_setup + + # Create air-gap deployment script + create_airgap_deployment_script + + echo "" | tee -a $LOG_FILE + echo "=== Air-Gap Setup Complete ===" | tee -a $LOG_FILE + echo "Local registry running at: http://$REGISTRY_HOST:$REGISTRY_PORT" | tee -a $LOG_FILE + echo "Registry data stored at: $REGISTRY_DATA_DIR" | tee -a $LOG_FILE + echo "" | tee -a $LOG_FILE + echo "Next steps for air-gapped deployment:" | tee -a $LOG_FILE + echo "1. Transfer these files to your air-gapped network:" | tee -a $LOG_FILE + echo " - docker-registry-data.tar.gz" | tee -a $LOG_FILE + echo " - registry-image.tar.gz" | tee -a $LOG_FILE + echo " - deploy-airgap-calls.sh" | tee -a $LOG_FILE + echo "2. Run the local registry container in the air-gapped network" | tee -a $LOG_FILE + echo "3. Execute ./deploy-airgap-calls.sh on the air-gapped systems" | tee -a $LOG_FILE + echo "" | tee -a $LOG_FILE + echo "Log file: $LOG_FILE" | tee -a $LOG_FILE +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/source/configure/calls-offloader-setup.md b/source/configure/calls-offloader-setup.md index a76ec1c148d..ea007010bbe 100644 --- a/source/configure/calls-offloader-setup.md +++ b/source/configure/calls-offloader-setup.md @@ -13,6 +13,7 @@ This guide provides detailed instructions for setting up, configuring, and valid - [Validation and testing](#validation-and-testing) - [Integration with Mattermost](#integration-with-mattermost) - [Troubleshooting](#troubleshooting) +- [Air-Gapped Deployments](#air-gapped-deployments) ## Overview @@ -377,16 +378,73 @@ These containers are typically pulled from the `mattermost` registry on Docker H The following Docker images are needed for full calls functionality: -- `mattermost/calls-offloader:v0.9.3` (or latest version) -- `mattermost/calls-transcriber:latest` +- `mattermost/calls-recorder:v0.8.5` (or version matching your plugin) +- `mattermost/calls-transcriber:v0.6.3` (or version matching your plugin) - `registry:2` (for the local Docker registry) +```{warning} +**Disk Space Requirements**: Ensure you have sufficient disk space before starting the setup process. The Docker images can be quite large: +- **calls-recorder image**: ~1.5-2GB +- **calls-transcriber image**: ~1.5-2GB +- **Registry container + data**: ~500MB + +**Total recommended free space**: At least 5GB to accommodate image downloads, local registry data, and archive creation. +``` + +#### Determining the Correct Image Versions + +**Important**: The exact versions of `calls-offloader` and `calls-transcriber` images must match what your installed Calls plugin expects. These versions are defined in the Calls plugin source code. + +To find the correct versions for your Calls plugin: + +1. **Determine your Calls plugin version**: + - In Mattermost, go to **System Console > Plugins > Plugin Management** + - Find the **Calls** plugin and note the version number (e.g., `v1.9.0`) + +2. **Look up the required image versions**: + - Visit the Calls plugin repository: https://github.com/mattermost/mattermost-plugin-calls + - Navigate to the tag or branch corresponding to your plugin version + - Open the `plugin.json` file + - Find the `RecorderImage` and `TranscriberImage` entries (around line 719-720) + + Example from plugin.json: + ```json + "RecorderImage": "mattermost/calls-recorder:v0.8.5", + "TranscriberImage": "mattermost/calls-transcriber:v0.6.3" + ``` + +3. **Use these exact versions** in your air-gap setup instead of `latest` tags + +**Direct link format**: For plugin version `v1.9.0`, the plugin.json would be at: +`https://github.com/mattermost/mattermost-plugin-calls/blob/v1.9.0/plugin.json` + +**Why this matters**: Using mismatched image versions can cause recording and transcription jobs to fail in air-gapped environments where the calls-offloader cannot automatically pull the correct images. + ### Setup Process #### Phase 1: Preparation (Internet-Connected Environment) Run this phase on a machine with internet access to download and prepare the Docker images. +**Automated Setup Script** + +For convenience, you can use the automated setup script: + +```bash +# Download the setup script +curl -O https://docs.mattermost.com/scripts/air-gap-docker-registry-setup.sh +chmod +x air-gap-docker-registry-setup.sh + +# Run the setup script +sudo ./air-gap-docker-registry-setup.sh +``` + +The script will automatically create the required archive files (`docker-registry-data.tar.gz` and `registry-image.tar.gz`) for transfer to your air-gapped environment. + +**Manual Setup Steps** + +If you prefer to set up manually or need to customize the process: + 1. **Set up a local Docker registry**: ```bash # Create registry data directory @@ -404,16 +462,16 @@ Run this phase on a machine with internet access to download and prepare the Doc 2. **Download and push required images**: ```bash # Pull required images from Docker Hub - docker pull mattermost/calls-offloader:v0.9.3 - docker pull mattermost/calls-transcriber:latest + docker pull mattermost/calls-recorder:v0.8.5 + docker pull mattermost/calls-transcriber:v0.6.3 # Tag images for local registry - docker tag mattermost/calls-offloader:v0.9.3 localhost:5000/mattermost/calls-offloader:v0.9.3 - docker tag mattermost/calls-transcriber:latest localhost:5000/mattermost/calls-transcriber:latest + docker tag mattermost/calls-recorder:v0.8.5 localhost:5000/mattermost/calls-recorder:v0.8.5 + docker tag mattermost/calls-transcriber:v0.6.3 localhost:5000/mattermost/calls-transcriber:v0.6.3 # Push images to local registry - docker push localhost:5000/mattermost/calls-offloader:v0.9.3 - docker push localhost:5000/mattermost/calls-transcriber:latest + docker push localhost:5000/mattermost/calls-recorder:v0.8.5 + docker push localhost:5000/mattermost/calls-transcriber:v0.6.3 ``` 3. **Export the registry data**: @@ -428,22 +486,31 @@ Run this phase on a machine with internet access to download and prepare the Doc #### Phase 2: Air-Gap Deployment Transfer the following files to your air-gapped network: -- `docker-registry-data.tar.gz` -- `registry-image.tar.gz` +- `docker-registry-data.tar.gz` (contains the registry data with pre-loaded images) +- `registry-image.tar.gz` (contains the Docker registry container image) +- `deploy-airgap-calls.sh` (deployment script created by the setup script) -1. **Load the registry container**: +**Complete Air-Gap Deployment Steps:** + +1. **Load the registry container image**: ```bash - gunzip -c registry-image.tar.gz | docker load + # Extract and load the registry container from the gzipped archive + gunzip registry-image.tar.gz + docker load -i registry-image.tar ``` -2. **Set up the registry data**: +2. **Set up the registry data directory**: ```bash + # Create the registry data directory sudo mkdir -p /opt/docker-registry/data + + # Extract the pre-loaded registry data sudo tar -xzf docker-registry-data.tar.gz -C /opt/docker-registry/data ``` -3. **Start the local registry**: +3. **Start the local registry with pre-loaded data**: ```bash + # Start the registry container with the extracted data docker run -d \ --name local-registry \ --restart=always \ @@ -460,6 +527,24 @@ Transfer the following files to your air-gapped network: sudo systemctl restart docker ``` +5. **Configure Mattermost server environment variable**: + ```bash + # Add the registry configuration to Mattermost environment + echo 'MM_CALLS_JOB_SERVICE_IMAGE_REGISTRY="localhost:5000/mattermost"' | sudo tee -a /opt/mattermost/config/mattermost.environment + + # Restart Mattermost to apply the environment variable + sudo systemctl restart mattermost + ``` + +6. **Run the air-gap deployment script** (if using the automated setup): + ```bash + # Make the deployment script executable and run it + chmod +x deploy-airgap-calls.sh + sudo ./deploy-airgap-calls.sh + ``` + + Or configure calls-offloader manually (see Manual Configuration section below). + ### Manual Configuration For reference, here are the individual configuration steps: @@ -504,7 +589,7 @@ sudo systemctl restart calls-offloader curl http://localhost:5000/v2/_catalog # Test pulling an image -docker pull localhost:5000/mattermost/calls-offloader:latest +docker pull localhost:5000/mattermost/calls-recorder:latest ``` #### Test Calls Functionality @@ -542,6 +627,41 @@ docker pull localhost:5000/mattermost/calls-offloader:latest - Verify the `image_registry` configuration in calls-offloader.toml - Ensure the calls-offloader service can reach the registry +4. **"invalid Runner value: failed to validate runner" error**: + This error occurs when calls-offloader cannot validate Docker images from the local registry. + + **Common causes and solutions:** + - **Image not found**: Verify the exact image names and tags in your local registry: + ```bash + curl http://localhost:5000/v2/_catalog + curl http://localhost:5000/v2/mattermost/calls-recorder/tags/list + curl http://localhost:5000/v2/mattermost/calls-transcriber/tags/list + ``` + + - **Registry configuration mismatch**: Ensure the `image_registry` setting in calls-offloader.toml matches your registry: + ```bash + grep image_registry /opt/calls-offloader/calls-offloader.toml + # Should show: image_registry = "localhost:5000/mattermost" + ``` + + - **Docker daemon can't reach registry**: Test that Docker can pull from the local registry: + ```bash + docker pull localhost:5000/mattermost/calls-recorder:latest + docker pull localhost:5000/mattermost/calls-transcriber:latest + ``` + + - **Image tag mismatch**: The calls-offloader may be looking for specific image tags. Check what the plugin expects vs what's in your registry: + ```bash + # Check what tags are available + curl http://localhost:5000/v2/mattermost/calls-recorder/tags/list + # Compare with what your plugin.json specifies + ``` + + **Solution steps:** + 1. Restart calls-offloader after confirming registry configuration: `sudo systemctl restart calls-offloader` + 2. If the issue persists, check the exact image names and versions expected by your Calls plugin version + 3. Ensure both versioned tags (e.g., `v0.8.5`) and `latest` tags are present in your local registry + #### Log Locations - Setup script logs: `/tmp/air-gap-registry-setup.log` @@ -583,13 +703,235 @@ To use specific versions of the calls images, update the version tags in the doc ```bash # Example: using specific versions -OFFLOADER_VERSION="v0.8.0" -TRANSCRIBER_VERSION="v1.2.0" +RECORDER_VERSION="v0.8.5" +TRANSCRIBER_VERSION="v0.6.3" -docker pull mattermost/calls-offloader:$OFFLOADER_VERSION +docker pull mattermost/calls-recorder:$RECORDER_VERSION docker pull mattermost/calls-transcriber:$TRANSCRIBER_VERSION ``` +### Day 2 Operations: Upgrading Images in Air-Gap Environments + +When you upgrade your Mattermost Calls plugin to a newer version, you'll need to update the Docker images in your air-gapped registry to match the new plugin requirements. + +#### Upgrade Process Overview + +**When to upgrade**: After upgrading the Calls plugin in Mattermost, you must update the Docker images to match the versions expected by the new plugin version. + +**Two approaches available**: + +1. **Complete Rebuild Method**: Re-run the entire air-gap setup process with new image versions +2. **Incremental Update Method**: Transfer only the new images to minimize data transfer + +#### Method 1: Complete Rebuild (Recommended for Major Updates) + +This approach rebuilds the entire registry dataset with new image versions: + +1. **On internet-connected machine**, run the air-gap setup script with new versions: + ```bash + # Find new versions from your updated plugin.json + ./air-gap-docker-registry-setup.sh v0.8.5 v0.6.3 + ``` + +2. **Transfer new archives** to air-gapped environment: + - `docker-registry-data.tar.gz` (contains all images including new versions) + - `deploy-airgap-calls.sh` (updated deployment script) + +3. **In air-gapped environment**, replace the registry data: + ```bash + # Stop the existing registry + docker stop local-registry + docker rm local-registry + + # Backup existing data (optional) + sudo mv /opt/docker-registry/data /opt/docker-registry/data.backup + + # Extract new registry data + sudo mkdir -p /opt/docker-registry/data + sudo tar -xzf docker-registry-data.tar.gz -C /opt/docker-registry/data + + # Restart registry with new data + docker run -d \ + --name local-registry \ + --restart=always \ + -p 5000:5000 \ + -v /opt/docker-registry/data:/var/lib/registry \ + registry:2 + ``` + +#### Method 2: Incremental Update (Efficient for Minor Updates) + +This approach transfers only the new image versions without rebuilding the entire registry: + +**Phase 1: Preparation (Internet-Connected Environment)** + +1. **Download new images**: + ```bash + # Determine new versions from updated plugin.json + NEW_RECORDER_VERSION="v0.8.5" + NEW_TRANSCRIBER_VERSION="v0.6.3" + + # Pull new images + docker pull mattermost/calls-recorder:$NEW_RECORDER_VERSION + docker pull mattermost/calls-transcriber:$NEW_TRANSCRIBER_VERSION + ``` + +2. **Create individual image archives**: + ```bash + # Save each image as a separate tar file + docker save mattermost/calls-recorder:$NEW_RECORDER_VERSION -o calls-recorder-$NEW_RECORDER_VERSION.tar + docker save mattermost/calls-transcriber:$NEW_TRANSCRIBER_VERSION -o calls-transcriber-$NEW_TRANSCRIBER_VERSION.tar + + # Compress the files for transfer + gzip calls-recorder-$NEW_RECORDER_VERSION.tar + gzip calls-transcriber-$NEW_TRANSCRIBER_VERSION.tar + ``` + +3. **Create update script**: + ```bash + cat > update-air-gap-images.sh << 'EOF' + #!/bin/bash + + # Air-Gap Image Update Script + set -e + + NEW_RECORDER_VERSION="v0.8.5" + NEW_TRANSCRIBER_VERSION="v0.6.3" + REGISTRY_HOST="${REGISTRY_HOST:-localhost}" + REGISTRY_PORT="${REGISTRY_PORT:-5000}" + + echo "Updating air-gap registry with new image versions..." + echo "Recorder: $NEW_RECORDER_VERSION" + echo "Transcriber: $NEW_TRANSCRIBER_VERSION" + + # Load new images into Docker + echo "Loading new images..." + gunzip calls-recorder-$NEW_RECORDER_VERSION.tar.gz + gunzip calls-transcriber-$NEW_TRANSCRIBER_VERSION.tar.gz + + docker load -i calls-recorder-$NEW_RECORDER_VERSION.tar + docker load -i calls-transcriber-$NEW_TRANSCRIBER_VERSION.tar + + # Tag images for local registry + echo "Tagging images for local registry..." + docker tag mattermost/calls-recorder:$NEW_RECORDER_VERSION $REGISTRY_HOST:$REGISTRY_PORT/mattermost/calls-recorder:$NEW_RECORDER_VERSION + docker tag mattermost/calls-transcriber:$NEW_TRANSCRIBER_VERSION $REGISTRY_HOST:$REGISTRY_PORT/mattermost/calls-transcriber:$NEW_TRANSCRIBER_VERSION + + # Also update 'latest' tags + docker tag mattermost/calls-recorder:$NEW_RECORDER_VERSION $REGISTRY_HOST:$REGISTRY_PORT/mattermost/calls-recorder:latest + docker tag mattermost/calls-transcriber:$NEW_TRANSCRIBER_VERSION $REGISTRY_HOST:$REGISTRY_PORT/mattermost/calls-transcriber:latest + + # Push to local registry + echo "Pushing images to local registry..." + docker push $REGISTRY_HOST:$REGISTRY_PORT/mattermost/calls-recorder:$NEW_RECORDER_VERSION + docker push $REGISTRY_HOST:$REGISTRY_PORT/mattermost/calls-transcriber:$NEW_TRANSCRIBER_VERSION + docker push $REGISTRY_HOST:$REGISTRY_PORT/mattermost/calls-recorder:latest + docker push $REGISTRY_HOST:$REGISTRY_PORT/mattermost/calls-transcriber:latest + + echo "Image update complete!" + echo "" + echo "Verification commands:" + echo "curl http://$REGISTRY_HOST:$REGISTRY_PORT/v2/mattermost/calls-recorder/tags/list" + echo "curl http://$REGISTRY_HOST:$REGISTRY_PORT/v2/mattermost/calls-transcriber/tags/list" + + # Clean up temporary files + rm calls-recorder-$NEW_RECORDER_VERSION.tar + rm calls-transcriber-$NEW_TRANSCRIBER_VERSION.tar + + echo "" + echo "Next steps:" + echo "1. Restart calls-offloader service: sudo systemctl restart calls-offloader" + echo "2. Test call recording functionality to verify the update" + EOF + + chmod +x update-air-gap-images.sh + ``` + +**Phase 2: Air-Gap Update** + +1. **Transfer files to air-gapped environment**: + - `calls-recorder-v0.8.5.tar.gz` + - `calls-transcriber-v0.6.3.tar.gz` + - `update-air-gap-images.sh` + +2. **Run the update script**: + ```bash + chmod +x update-air-gap-images.sh + sudo ./update-air-gap-images.sh + ``` + +3. **Restart calls-offloader**: + ```bash + sudo systemctl restart calls-offloader + ``` + +4. **Verify the update**: + ```bash + # Check available image tags + curl http://localhost:5000/v2/mattermost/calls-recorder/tags/list + curl http://localhost:5000/v2/mattermost/calls-transcriber/tags/list + + # Test calls-offloader functionality + curl http://localhost:4545/version + ``` + +#### Advantages of Each Method + +**Complete Rebuild Method:** +- ✅ Ensures clean state with no leftover data +- ✅ Recommended for major version upgrades +- ✅ Simpler process (reuse existing automation) +- ❌ Larger data transfer requirements +- ❌ More downtime during registry replacement + +**Incremental Update Method:** +- ✅ Minimal data transfer (only new images) +- ✅ Faster deployment process +- ✅ Preserves existing registry data +- ✅ Less downtime (registry stays running) +- ❌ More complex process +- ❌ Potential for version conflicts if not managed carefully + +#### Choosing the Right Method + +**Use Complete Rebuild when:** +- Upgrading across major plugin versions (e.g., v1.8.x to v1.9.x) +- Registry has accumulated significant old/unused images +- You want to ensure a completely clean state +- Data transfer size is not a primary concern + +**Use Incremental Update when:** +- Applying minor version updates (e.g., v1.9.0 to v1.9.1) +- Bandwidth or transfer time is limited +- You need to minimize downtime +- The registry is working correctly and just needs new image versions + +#### Post-Update Verification + +After either upgrade method, perform these verification steps: + +1. **Test recording functionality**: + - Start a call in Mattermost + - Enable call recording + - Verify recording starts without errors + +2. **Check job container creation**: + ```bash + # Monitor for new job containers during recording + docker ps --format "{{.ID}} {{.Image}}" | grep calls + ``` + +3. **Monitor calls-offloader logs**: + ```bash + sudo journalctl -u calls-offloader -f + ``` + +4. **Verify image versions**: + ```bash + # Check that the new image versions are being used + docker ps --format "{{.Image}}" | grep calls + ``` + ## Other Calls Documentation - [Calls Overview](calls-deployment.md): Overview of deployment options and architecture From 5c26d6223f88c00f73c25dde784210eac5c2347c Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Tue, 24 Jun 2025 10:13:57 -0400 Subject: [PATCH 28/37] Edits from reviews and rebase conflict resolution --- source/configure/calls-deployment.md | 244 +------------------ source/configure/calls-metrics-monitoring.md | 218 ++++++++++++++++- 2 files changed, 213 insertions(+), 249 deletions(-) diff --git a/source/configure/calls-deployment.md b/source/configure/calls-deployment.md index 24928674374..705eb144546 100644 --- a/source/configure/calls-deployment.md +++ b/source/configure/calls-deployment.md @@ -103,7 +103,7 @@ Mattermost Calls provides integrated audio calling and screen sharing capabiliti RTC (Calls plugin or rtcd) 8443 UDP (incoming) -Mattermost clients (Web/Desktop/Mobile) +Mattermost clients (Web/Desktop/Mobile) and calls-offloader Mattermost instance or rtcd service To allow clients to establish connections that transport calls related media (e.g. audio, video). This should be open on any network component (e.g. NAT, firewalls) in between the instance running the plugin (or rtcd) and the clients joining calls so that UDP traffic is correctly routed both ways (from/to clients). @@ -111,7 +111,7 @@ Mattermost Calls provides integrated audio calling and screen sharing capabiliti RTC (Calls plugin or rtcd) 8443 TCP (incoming) -Mattermost clients (Web/Desktop/Mobile) +Mattermost clients (Web/Desktop/Mobile) and calls-offloader Mattermost instance or rtcd service To allow clients to establish connections that transport calls related media (e.g. audio, video). This should be open on any network component (e.g. NAT, firewalls) in between the instance running the plugin (or rtcd) and the clients joining calls so that TCP traffic is correctly routed both ways (from/to clients). This can be used as a backup channel in case clients are unable to connect using UDP. It requires rtcd version >= v0.11 and Calls version >= v0.17. @@ -136,14 +136,6 @@ Mattermost Calls provides integrated audio calling and screen sharing capabiliti For complete network requirements, see the [RTCD Setup and Configuration](calls-rtcd-setup.md) guide. -#### Air-gapped deployments - -Mattermost Calls can function in air-gapped environments. Exposing Calls to the public internet is only necessary when users need to connect from outside the local network, and no existing method supports that connection. In such setups: - -- Users should connect from within the private/local network. This can be done on-premises, through a VPN, or via virtual machines. -- Configuring a STUN server is unnecessary, as all connections occur within the local network. -- The [ICE Host Override](https://docs.mattermost.com/configure/plugins-configuration-settings.html#ice-host-override) configuration setting can be optionally set with a local IP address (e.g., 192.168.1.45), depending on the specific network configuration and topology. - ## Limitations - All Mattermost customers can start, join, and participate in 1:1 audio calls with optional screen sharing. @@ -153,7 +145,7 @@ Mattermost Calls can function in air-gapped environments. Exposing Calls to the ## Configuration -For Mattermost self-hosted customers, the calls plugin is pre-packaged, installed, and enabled. Configuration to allow end-users to use it can be found in the [System Console](/configure/plugins-configuration-settings.html#calls). +For Mattermost self-hosted customers, the calls plugin is pre-packaged, installed, and enabled. Configuration to allow end-users to use it can be found in ``System Console > Plugins > Calls``. ## Deployment Architecture Options @@ -211,238 +203,16 @@ For call recording and transcription, you need to: 2. Configure the service URL in the System Console 3. Enable call recordings and/or transcriptions in the plugin settings +For detailed setup instructions, see the [Calls Offloader Setup and Configuration](calls-offloader-setup.md) guide. + ## Air-Gapped Deployments Mattermost Calls can function in air-gapped environments. Exposing Calls to the public internet is only necessary when users need to connect from outside the local network, and no existing method supports that connection. In such setups: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CallsParticipants/callUnmuted/callScreen sharingCPU (avg)Memory (avg)Bandwidth (in/out)Instance type (RTCD)
110002no47%1.46GB1Mbps / 194Mbpsc7i.xlarge
18001yes64%1.43GB2.7Mbps / 1.36Gbpsc7i.xlarge
110001yes79%1.54GB2.9Mbps / 1.68Gbpsc7i.xlarge
101001yes74%1.56GB18.2Mbps / 1.68Gbpsc7i.xlarge
100102no49%1.46GB18.7Mbps / 175Mbpsc7i.xlarge
100101yes84%1.73GB171Mbps / 1.53Gbpsc7i.xlarge
110002no20%1.44GB1.4Mbps / 194Mbpsc7i.2xlarge
110002yes49%1.53GB3.6Mbps / 1.79Gbpsc7i.2xlarge
210001yes73%2.38GB5.7Mbps / 3.06Gbpsc7i.2xlarge
100102yes60%1.74GB181Mbps / 1.62Gbpsc7i.2xlarge
150101yes72%2.26GB257Mbps / 2.30Gbpsc7i.2xlarge
150102yes79%2.34GB271Mbps / 2.41Gbpsc7i.2xlarge
250102no58%2.66GB47Mbps / 439Mbpsc7i.2xlarge
100022no78%2.31GB178Mbps / 195Mbpsc7i.2xlarge
210002yes41%2.6GB7.23Mbps / 3.60Gbpsc7i.4xlarge
310002yes63%3.53GB10.9Mbps / 5.38Gbpsc7i.4xlarge
410002yes83%4.40GB14.5Mbps / 7.17Gbpsc7i.4xlarge
250102yes79%3.49GB431Mbps / 3.73Gbpsc7i.4xlarge
50022yes71%2.54GB896Mbps / 919Mbpsc7i.4xlarge
- Users should connect from within the private/local network. This can be done on-premises, through a VPN, or via virtual machines. - Configuring a STUN server is unnecessary, as all connections occur within the local network. -- The ICE Host Override configuration setting can be optionally set with a local IP address (e.g., 192.168.1.45), depending on the specific network configuration and topology. - -### Docker Registry Setup for Air-Gapped Environments - -When using the calls-offloader service for call recording and transcription in air-gapped environments, you need to set up a local Docker registry since the service creates Docker containers that normally pull images from Docker Hub. - -**Required Docker Images:** -- `mattermost/calls-offloader:v0.9.3` (or latest version) -- `mattermost/calls-transcriber:latest` -- `registry:2` (for the local Docker registry) - -**Setup Overview:** -1. **Preparation phase** (on internet-connected machine): Download and prepare Docker images -2. **Air-gap deployment phase**: Transfer and deploy the local registry with pre-loaded images - -**Key Configuration Changes:** -- Configure Docker daemon to allow insecure registries: `{"insecure-registries": ["localhost:5000"]}` -- Update calls-offloader configuration to use local registry: `image_registry = "localhost:5000/mattermost"` - -For detailed step-by-step instructions, including setup scripts, manual configuration, and troubleshooting, see the comprehensive air-gap setup guide in the [Calls Offloader Setup and Configuration](calls-offloader-setup.md#air-gapped-deployments) documentation. +- The [ICE Host Override](https://docs.mattermost.com/configure/plugins-configuration-settings.html#ice-host-override) configuration setting can be optionally set with a local IP address (e.g., 192.168.1.45), depending on the specific network configuration and topology. +- For call recording and transcription in air-gapped environments, see the [Air-Gapped Deployments](calls-offloader-setup.md#air-gapped-deployments) section in the Calls Offloader Setup documentation. ## Performance Considerations diff --git a/source/configure/calls-metrics-monitoring.md b/source/configure/calls-metrics-monitoring.md index ed56b878717..fdb2bc6ff16 100644 --- a/source/configure/calls-metrics-monitoring.md +++ b/source/configure/calls-metrics-monitoring.md @@ -220,18 +220,212 @@ The following performance benchmarks provide baseline metrics for RTCD deploymen Below are the detailed benchmarks based on internal performance testing: -| Calls | Users/call | Unmuted/call | Screen sharing | CPU (avg) | Memory (avg) | Bandwidth (in/out) | Instance (EC2) | -|-------|------------|--------------|----------------|-----------|--------------|--------------------|--------------| -| 100 | 8 | 2 | no | 60% | 0.5GB | 22Mbps / 125Mbps | c6i.xlarge | -| 100 | 8 | 2 | no | 30% | 0.5GB | 22Mbps / 125Mbps | c6i.2xlarge | -| 100 | 8 | 2 | yes | 86% | 0.7GB | 280Mbps / 2.2Gbps | c6i.2xlarge | -| 10 | 50 | 2 | no | 35% | 0.3GB | 5.25Mbps / 86Mbps | c6i.xlarge | -| 10 | 50 | 2 | no | 16% | 0.3GB | 5.25Mbps / 86Mbps | c6i.2xlarge | -| 10 | 50 | 2 | yes | 90% | 0.3GB | 32Mbps / 1.33Gbps | c6i.xlarge | -| 10 | 50 | 2 | yes | 45% | 0.3GB | 32Mbps / 1.33Gbps | c6i.2xlarge | -| 5 | 200 | 2 | no | 65% | 0.6GB | 8.2Mbps / 180Mbps | c6i.xlarge | -| 5 | 200 | 2 | no | 30% | 0.6GB | 8.2Mbps / 180Mbps | c6i.2xlarge | -| 5 | 200 | 2 | yes | 90% | 0.7GB | 31Mbps / 2.2Gbps | c6i.2xlarge | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CallsParticipants/callUnmuted/callScreen sharingCPU (avg)Memory (avg)Bandwidth (in/out)Instance type (RTCD)
110002no47%1.46GB1Mbps / 194Mbpsc7i.xlarge
18001yes64%1.43GB2.7Mbps / 1.36Gbpsc7i.xlarge
110001yes79%1.54GB2.9Mbps / 1.68Gbpsc7i.xlarge
101001yes74%1.56GB18.2Mbps / 1.68Gbpsc7i.xlarge
100102no49%1.46GB18.7Mbps / 175Mbpsc7i.xlarge
100101yes84%1.73GB171Mbps / 1.53Gbpsc7i.xlarge
110002no20%1.44GB1.4Mbps / 194Mbpsc7i.2xlarge
110002yes49%1.53GB3.6Mbps / 1.79Gbpsc7i.2xlarge
210001yes73%2.38GB5.7Mbps / 3.06Gbpsc7i.2xlarge
100102yes60%1.74GB181Mbps / 1.62Gbpsc7i.2xlarge
150101yes72%2.26GB257Mbps / 2.30Gbpsc7i.2xlarge
150102yes79%2.34GB271Mbps / 2.41Gbpsc7i.2xlarge
250102no58%2.66GB47Mbps / 439Mbpsc7i.2xlarge
100022no78%2.31GB178Mbps / 195Mbpsc7i.2xlarge
210002yes41%2.6GB7.23Mbps / 3.60Gbpsc7i.4xlarge
310002yes63%3.53GB10.9Mbps / 5.38Gbpsc7i.4xlarge
410002yes83%4.40GB14.5Mbps / 7.17Gbpsc7i.4xlarge
250102yes79%3.49GB431Mbps / 3.73Gbpsc7i.4xlarge
50022yes71%2.54GB896Mbps / 919Mbpsc7i.4xlarge
## Troubleshooting Metrics Collection From aa3202d18e294c3c2c1b45b59f5eeb47870acf54 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Tue, 24 Jun 2025 11:23:53 -0400 Subject: [PATCH 29/37] Minor tweaks and adjustments from review --- source/configure/calls-offloader-setup.md | 21 +--- source/configure/calls-rtcd-setup.md | 137 +++++++++++++++++----- 2 files changed, 109 insertions(+), 49 deletions(-) diff --git a/source/configure/calls-offloader-setup.md b/source/configure/calls-offloader-setup.md index ea007010bbe..7eb7652ff44 100644 --- a/source/configure/calls-offloader-setup.md +++ b/source/configure/calls-offloader-setup.md @@ -309,7 +309,7 @@ Check the following: - Verify the calls-offloader service is running: `sudo systemctl status calls-offloader` - Ensure network connectivity between Mattermost and calls-offloader -- Check Docker daemon is running and accessible by the user running `calls-offloader`: `docker ps` +- Check Docker daemon is running and accessible by the user running the Calls Offloader service (E.g., user: ``calls-offloader``) - Verify authentication configuration matches between services - Review service logs for specific error messages @@ -418,8 +418,6 @@ To find the correct versions for your Calls plugin: **Direct link format**: For plugin version `v1.9.0`, the plugin.json would be at: `https://github.com/mattermost/mattermost-plugin-calls/blob/v1.9.0/plugin.json` -**Why this matters**: Using mismatched image versions can cause recording and transcription jobs to fail in air-gapped environments where the calls-offloader cannot automatically pull the correct images. - ### Setup Process #### Phase 1: Preparation (Internet-Connected Environment) @@ -435,8 +433,8 @@ For convenience, you can use the automated setup script: curl -O https://docs.mattermost.com/scripts/air-gap-docker-registry-setup.sh chmod +x air-gap-docker-registry-setup.sh -# Run the setup script -sudo ./air-gap-docker-registry-setup.sh +# Run the setup script with the required image versions +sudo ./air-gap-docker-registry-setup.sh ``` The script will automatically create the required archive files (`docker-registry-data.tar.gz` and `registry-image.tar.gz`) for transfer to your air-gapped environment. @@ -650,7 +648,7 @@ docker pull localhost:5000/mattermost/calls-recorder:latest docker pull localhost:5000/mattermost/calls-transcriber:latest ``` - - **Image tag mismatch**: The calls-offloader may be looking for specific image tags. Check what the plugin expects vs what's in your registry: + - **Image tag mismatch**: The calls-offloader will be looking for specific image tags. Check what the plugin expects vs what's in your registry: ```bash # Check what tags are available curl http://localhost:5000/v2/mattermost/calls-recorder/tags/list @@ -669,17 +667,6 @@ docker pull localhost:5000/mattermost/calls-recorder:latest - Docker daemon logs: `sudo journalctl -u docker` - Registry container logs: `docker logs local-registry` -#### Security Considerations - -1. **Insecure Registry**: The setup uses an insecure HTTP registry for simplicity. For production, consider: - - Setting up TLS certificates for the registry - - Implementing authentication - - Using proper firewall rules - -2. **Network Access**: Ensure the registry is only accessible within your private network - -3. **Image Verification**: Consider implementing image signing and verification processes - #### Advanced Configuration **Using a Different Registry Host** diff --git a/source/configure/calls-rtcd-setup.md b/source/configure/calls-rtcd-setup.md index 1feb89e9423..89571355371 100644 --- a/source/configure/calls-rtcd-setup.md +++ b/source/configure/calls-rtcd-setup.md @@ -16,13 +16,88 @@ Before deploying RTCD, ensure you have: The following network connectivity is required: -| Service | Ports | Protocols | Source | Target | -|-------------------|--------|-----------------|-------------------------|------------------------| -| Calls plugin API | 80,443 | TCP (incoming) | Mattermost clients | Mattermost server | -| RTC media | 8443 | UDP (incoming) | Mattermost clients | Mattermost or RTCD | -| RTC media | 8443 | TCP (incoming) | Mattermost clients | Mattermost or RTCD | -| RTCD API | 8045 | TCP (incoming) | Mattermost server | RTCD service | -| STUN | 3478 | UDP (outgoing) | Mattermost or RTCD | STUN servers | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ServicePortsProtocolsSourceTargetPurpose
API (Calls plugin)80,443TCP (incoming)Mattermost clients (web/desktop/mobile)Mattermost instance (Calls plugin)To allow for HTTP and WebSocket connectivity from clients to Calls plugin. This API is exposed on the same connection as Mattermost, so there's likely no need to change anything.
RTC (Calls plugin or rtcd)8443UDP (incoming)Mattermost clients (Web/Desktop/Mobile) and calls-offloaderMattermost instance or rtcd serviceTo allow clients to establish connections that transport calls related media (e.g. audio, video). This should be open on any network component (e.g. NAT, firewalls) in between the instance running the plugin (or rtcd) and the clients joining calls so that UDP traffic is correctly routed both ways (from/to clients).
RTC (Calls plugin or rtcd)8443TCP (incoming)Mattermost clients (Web/Desktop/Mobile) and calls-offloaderMattermost instance or rtcd serviceTo allow clients to establish connections that transport calls related media (e.g. audio, video). This should be open on any network component (e.g. NAT, firewalls) in between the instance running the plugin (or rtcd) and the clients joining calls so that TCP traffic is correctly routed both ways (from/to clients). This can be used as a backup channel in case clients are unable to connect using UDP. It requires rtcd version >= v0.11 and Calls version >= v0.17.
API (rtcd)8045TCP (incoming)Mattermost instance(s) (Calls plugin)rtcd serviceTo allow for HTTP/WebSocket connectivity from Calls plugin to rtcd service. Can be expose internally as the service only needs to be reachable by the instance(s) running the Mattermost server.
STUN (Calls plugin or rtcd)3478UDP (outgoing)Mattermost Instance(s) (Calls plugin) or rtcd serviceConfigured STUN servers(Optional) To allow for either Calls plugin or rtcd service to discover their instance public IP. Only needed if configuring STUN/TURN servers. This requirement does not apply when manually setting an IP or hostname through the ICE Host Override config option.
## Installation and Deployment @@ -30,7 +105,7 @@ There are multiple ways to deploy RTCD, depending on your environment. We recomm ### Bare Metal or VM Deployment (Recommended) -This is the recommended deployment method for production environments as it provides the best performance and operational control. +This is the recommended deployment method for non-Kubernetes production environments as it provides the best performance and operational control. For Kubernetes deployments, see the [Calls Deployment on Kubernetes](calls-kubernetes.md) guide. 1. Download the latest release from the [RTCD GitHub repository](https://github.com/mattermost/rtcd/releases) @@ -69,18 +144,22 @@ This is the recommended deployment method for production environments as it prov file_level = "INFO" file_location = "/opt/rtcd/rtcd.log" enable_color = true + ``` + +3. Create a dedicated user for the RTCD service: - [mattermost] - host = "http://YOUR_MATTERMOST_SERVER:8065" + ```bash + sudo useradd --system --no-create-home --shell /bin/false mattermost ``` -3. Create the data directory: +4. Create the data directory and set ownership: ```bash sudo mkdir -p /opt/rtcd/data/db + sudo chown -R mattermost:mattermost /opt/rtcd ``` -4. Create a systemd service file (`/etc/systemd/system/rtcd.service`): +5. Create a systemd service file (`/etc/systemd/system/rtcd.service`): ```ini [Unit] @@ -89,7 +168,8 @@ This is the recommended deployment method for production environments as it prov [Service] Type=simple - User=root + User=mattermost + Group=mattermost ExecStart=/opt/rtcd/rtcd --config /opt/rtcd/rtcd.toml Restart=always RestartSec=10 @@ -121,7 +201,7 @@ Docker deployment is suitable for development, testing, or containerized product ```bash docker run -d --name rtcd \ - -e "RTCD_LOGGER_ENABLEFILE=false" \ + -e "RTCD_LOGGER_ENABLEFILE=true" \ -e "RTCD_API_SECURITY_ALLOWSELFREGISTRATION=true" \ -p 8443:8443/udp \ -p 8443:8443/tcp \ @@ -133,7 +213,7 @@ Docker deployment is suitable for development, testing, or containerized product ```bash docker run -d --name rtcd \ - -e "RTCD_LOGGER_ENABLEFILE=false" \ + -e "RTCD_LOGGER_ENABLEFILE=true" \ -e "RTCD_LOGGER_CONSOLELEVEL=DEBUG" \ -e "RTCD_API_SECURITY_ALLOWSELFREGISTRATION=true" \ -p 8443:8443/udp \ @@ -188,18 +268,16 @@ For Kubernetes deployments, use the official Helm chart: ### RTCD Configuration File -The RTCD service uses a TOML configuration file. Here's a comprehensive example with commonly used settings: +The RTCD service uses a TOML configuration file. Here's an example with commonly used settings: ```toml [api] # The address and port to which the HTTP API server will listen http.listen_address = ":8045" # Security settings for authentication -security.allow_self_registration = false +security.allow_self_registration = true security.enable_admin = true security.admin_secret_key = "YOUR_API_KEY" -# Configure allowed origins for CORS -security.allowed_origins = ["https://mattermost.example.com"] [rtc] # The UDP address and port for media traffic @@ -209,7 +287,7 @@ ice_port_udp = 8443 ice_address_tcp = "" ice_port_tcp = 8443 # Public hostname or IP that clients will use to connect -ice_host_override = "rtcd.example.com" +ice_host_override = "RTCD_SERVER_PUBLIC_IP" [logger] # Logging configuration @@ -218,13 +296,8 @@ console_json = false console_level = "INFO" enable_file = true file_json = true -file_level = "DEBUG" -file_location = "rtcd.log" - -[metrics] -# Prometheus metrics configuration -enable_prom = true -prom_port = 9090 +file_level = "INFO" +file_location = "/opt/rtcd/rtcd.log" ``` Key Configuration Options: @@ -235,7 +308,6 @@ Key Configuration Options: - **rtc.ice_address_tcp**: The TCP address for fallback media traffic - **rtc.ice_port_tcp**: The TCP port for fallback media traffic - **rtc.ice_host_override**: The public hostname or IP address clients will use to connect to RTCD -- **api.security.allowed_origins**: List of allowed origins for CORS - **api.security.admin_secret_key**: API key for Mattermost servers to authenticate with RTCD ### Required Mattermost Server Configuration @@ -281,10 +353,11 @@ For clients behind strict firewalls, you may need to configure STUN/TURN servers ```toml [rtc] # STUN/TURN server configuration -ice_servers = [ - { urls = ["stun:stun.example.com:3478"] }, - { urls = ["turn:turn.example.com:3478"], username = "turnuser", credential = "turnpassword" } -] + ice_servers = [ + { urls = ["stun:stun.global.calls.mattermost.com:3478"] } + # { urls = ["turn:turn.example.com:3478"], username = "turnuser", credential = "turnpassword" } + ] + ``` We recommend using [coturn](https://github.com/coturn/coturn) for your TURN server implementation. From 4048a349c2055dc419357de75df33af7255e8d7c Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 27 Jun 2025 08:35:01 -0400 Subject: [PATCH 30/37] Expanding direction for installing RTCD binary --- source/configure/calls-rtcd-setup.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/source/configure/calls-rtcd-setup.md b/source/configure/calls-rtcd-setup.md index 89571355371..6fece97e085 100644 --- a/source/configure/calls-rtcd-setup.md +++ b/source/configure/calls-rtcd-setup.md @@ -107,9 +107,28 @@ There are multiple ways to deploy RTCD, depending on your environment. We recomm This is the recommended deployment method for non-Kubernetes production environments as it provides the best performance and operational control. For Kubernetes deployments, see the [Calls Deployment on Kubernetes](calls-kubernetes.md) guide. -1. Download the latest release from the [RTCD GitHub repository](https://github.com/mattermost/rtcd/releases) +1. **Download and install the RTCD binary**: -2. Create a configuration file (`/opt/rtcd/rtcd.toml`) with the following settings: + Download the latest release from the [RTCD GitHub repository](https://github.com/mattermost/rtcd/releases): + + ```bash + # Create the RTCD directory structure + sudo mkdir -p /opt/rtcd + + # Download the latest RTCD binary (adjust URL for your architecture) + # For Linux x86_64: + wget https://github.com/mattermost/rtcd/releases/latest/download/rtcd-linux-amd64 + + # Make the binary executable and move it to the installation directory + chmod +x rtcd-linux-amd64 + sudo mv rtcd-linux-amd64 /opt/rtcd/rtcd + ``` + + ```{note} + Replace `rtcd-linux-amd64` with the appropriate binary for your system architecture (e.g., `rtcd-linux-arm64` for ARM64 systems). The binary should be placed at `/opt/rtcd/rtcd` as this is the expected location referenced in systemd service files and other documentation. + ``` + +2. **Create a configuration file** (`/opt/rtcd/rtcd.toml`) with the following settings: ```toml [api] From 90d72d9f981c3ced1f3599330de0c9265e8bd855 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Wed, 9 Jul 2025 11:07:09 -0400 Subject: [PATCH 31/37] Minor edits to config example and section header --- source/configure/calls-offloader-setup.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/configure/calls-offloader-setup.md b/source/configure/calls-offloader-setup.md index 7eb7652ff44..01218fcddcd 100644 --- a/source/configure/calls-offloader-setup.md +++ b/source/configure/calls-offloader-setup.md @@ -69,8 +69,8 @@ Call recordings can consume significant storage space: http.tls.cert_file = "" http.tls.cert_key = "" security.allow_self_registration = true - security.enable_admin = true - security.admin_secret_key = "changeme" + security.enable_admin = false + security.admin_secret_key = "" security.session_cache.expiration_minutes = 1440 [store] @@ -79,7 +79,7 @@ Call recordings can consume significant storage space: [jobs] api_type = "docker" max_concurrent_jobs = 2 - failed_jobs_retention_time = "7d" + failed_jobs_retention_time = "30d" image_registry = "mattermost" [logger] @@ -256,7 +256,7 @@ After deploying calls-offloader, validate the installation: - Network connectivity between Mattermost and calls-offloader servers - calls-offloader service binding configuration (ensure it's not bound to localhost only) -3. **Verify Docker integration** (if using docker api_type): +3. **Verify Docker service** (if using docker api_type): ```bash # Check that system user running calls-offloader can access Docker From 0f32682c02c97e1cff4279a78df23dc1f7be20b0 Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Thu, 10 Jul 2025 10:12:11 -0400 Subject: [PATCH 32/37] Edits from @streamer45 review --- source/configure/calls-deployment.md | 33 ++++++++++++++++------- source/configure/calls-kubernetes.md | 18 +++++-------- source/configure/calls-troubleshooting.md | 11 ++++---- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/source/configure/calls-deployment.md b/source/configure/calls-deployment.md index 705eb144546..38308fc53cb 100644 --- a/source/configure/calls-deployment.md +++ b/source/configure/calls-deployment.md @@ -103,7 +103,7 @@ Mattermost Calls provides integrated audio calling and screen sharing capabiliti RTC (Calls plugin or rtcd) 8443 UDP (incoming) -Mattermost clients (Web/Desktop/Mobile) and calls-offloader +Mattermost clients (Web/Desktop/Mobile) and ``calls-offloader`` spawned jobs (Recorder, Transcriber) Mattermost instance or rtcd service To allow clients to establish connections that transport calls related media (e.g. audio, video). This should be open on any network component (e.g. NAT, firewalls) in between the instance running the plugin (or rtcd) and the clients joining calls so that UDP traffic is correctly routed both ways (from/to clients). @@ -111,7 +111,7 @@ Mattermost Calls provides integrated audio calling and screen sharing capabiliti RTC (Calls plugin or rtcd) 8443 TCP (incoming) -Mattermost clients (Web/Desktop/Mobile) and calls-offloader +Mattermost clients (Web/Desktop/Mobile) and ``calls-offloader`` spawned jobs (Recorder, Transcriber) Mattermost instance or rtcd service To allow clients to establish connections that transport calls related media (e.g. audio, video). This should be open on any network component (e.g. NAT, firewalls) in between the instance running the plugin (or rtcd) and the clients joining calls so that TCP traffic is correctly routed both ways (from/to clients). This can be used as a backup channel in case clients are unable to connect using UDP. It requires rtcd version >= v0.11 and Calls version >= v0.17. @@ -185,13 +185,25 @@ RTCD is the only officially supported approach for Kubernetes deployments. For d ## When to Use RTCD -The dedicated RTCD service (available with Enterprise license) is recommended for: +This section will help you understand when and why your organization would want to use the dedicated RTCD service. -- **Production environments**: Isolates call traffic from other Mattermost services -- **Performance optimization**: Dedicated service tuned for real-time media -- **Scalability**: Add RTCD instances as call volume grows -- **Call stability**: Calls continue even if Mattermost server needs to restart -- **Kubernetes deployments**: Required for officially supported Kubernetes deployments +```{note} +RTCD is a standalone service, which adds operational complexity, maintenance costs, and requires an Enterprise license. For those who are evaluating Calls, and for many small instances of Mattermost, the integrated SFU (the one included in the Calls plugin) may be sufficient initially. +``` + +The RTCD service is the recommended way to host Calls for the following reasons: + +- **Performance of the main Mattermost server(s)**: When the Calls plugin runs the SFU, calls traffic is added to the processing load of the server running the rest of your Mattermost services. If Calls traffic spikes, it can negatively affect the responsiveness of these services. Using an RTCD service isolates the calls traffic processing to those RTCD instances, and also reduces costs by minimizing CPU usage spikes. + +- **Performance, scalability, and stability of the Calls product**: If Calls traffic spikes, or more overall capacity is needed, RTCD servers can be added to balance the load. As an added benefit, if the Mattermost traffic spikes, or if a Mattermost instance needs to be restarted, those people in a current call will not be affected - current calls won't be dropped. + + Some caveats apply here. WebSocket events (for example: emoji reactions, hand raising, muting/unmuting) will not be transmitted while the main Mattermost server is down. But the call itself will continue while the main server restarts. + +- **Kubernetes deployments**: In a Kubernetes deployment, RTCD is strongly recommended; it is currently the only officially supported way to run Calls. + +- **Technical benefits**: The dedicated RTCD service has been optimized and tuned at the system/network level for real-time audio/video traffic, where latency is generally more important than throughput. + +In general, RTCD is the preferred solution for a performant and scalable deployment. With RTCD, the Mattermost server will be minimally impacted when hosting a high number of calls. For detailed RTCD setup instructions, see the [RTCD Setup and Configuration](calls-rtcd-setup.md) guide. @@ -219,15 +231,16 @@ Mattermost Calls can function in air-gapped environments. Exposing Calls to the Calls performance primarily depends on: - **CPU resources**: More participants require more processing power -- **Network bandwidth**: Both incoming and outgoing traffic increases with participant count +- **Network bandwidth**: Both incoming and outgoing traffic increases with participant count. Due to the nature of the service, the bottleneck is always going to be the outgoing/egress path - **Active speakers**: Unmuted participants require significantly more resources +- **Presenters**: Screen sharing participants require even more resources than active speakers For detailed performance metrics, benchmarks, and monitoring guidance, see the [Calls Metrics and Monitoring](calls-metrics-monitoring.md) guide. ## Frequently Asked Questions **Is calls traffic encrypted?** -Yes, using WebRTC security standards (DTLS/SRTP). Traffic is encrypted in transit. +Media (audio/video) is encrypted using security standards as part of WebRTC. It's mainly a combination of DTLS and SRTP. It's not e2e encrypted in the sense that in the current design all media needs to go through Mattermost which acts as a media router and has complete access to it. Media is then encrypted back to the clients so it's secured during transit. In short: only the participant clients and the Mattermost server have access to unencrypted call data. **Are there any third-party services involved?** Only a Mattermost STUN server (`stun.global.calls.mattermost.com`) is used by default. This can be removed if you set the ICE Host Override configuration. diff --git a/source/configure/calls-kubernetes.md b/source/configure/calls-kubernetes.md index b45fda1461c..a8519f1dfc8 100644 --- a/source/configure/calls-kubernetes.md +++ b/source/configure/calls-kubernetes.md @@ -59,19 +59,13 @@ For complete configuration options, see the [Calls-Offloader Helm chart document ### Network Configuration -For Kubernetes deployments, you need to ensure: +For Kubernetes deployments, you need to ensure specific connectivity paths: -1. UDP traffic is properly routed to RTCD pods (for media) -2. TCP traffic can reach both the Mattermost pods and RTCD pods -3. Load balancers are properly configured to handle UDP traffic -4. Network policies allow the required communications between services - -Recommended annotations for AWS environments: - -```yaml -service.beta.kubernetes.io/aws-load-balancer-backend-protocol: udp -service.beta.kubernetes.io/aws-load-balancer-type: nlb -``` +1. **Client to RTCD connectivity**: UDP traffic on port 8443 is properly routed from clients to RTCD pods (for media) +2. **Mattermost to RTCD API connectivity**: TCP traffic on port 8045 must have a clear connectivity path from Mattermost pods to RTCD pods (for API communication) +3. **Client to RTCD TCP fallback**: TCP traffic on port 8443 can reach RTCD pods (for fallback connections when UDP fails) +4. **Load balancer configuration**: Load balancers must be properly configured to handle UDP traffic routing to RTCD pods +5. **Network policies**: Network policies must allow the required communications between Mattermost and RTCD services ### Resource Requirements diff --git a/source/configure/calls-troubleshooting.md b/source/configure/calls-troubleshooting.md index e2389ce0d77..d964a24d086 100644 --- a/source/configure/calls-troubleshooting.md +++ b/source/configure/calls-troubleshooting.md @@ -320,14 +320,15 @@ If you're running calls-offloader in Docker, use these commands for debugging: #### Monitor Live Logs -To view real-time logs from calls-offloader containers: +For easier log management, configure calls-offloader to capture job logs directly in the calls-offloader log file by adding the following to your `calls-offloader.toml`: -```bash -# Find and follow logs from all calls-related containers -docker ps --format "{{.ID}} {{.Image}}" | grep "calls" | awk '{print $1}' | xargs -I {} docker logs -f {} +```toml +[jobs.docker] +# Whether to output job logs to the console. Default is false. +output_logs = true ``` -This command finds all running containers with "calls" in the image name and follows their logs. +With this configuration enabled, all job logs will be written to the main calls-offloader log file. #### View Completed Jobs From a3c879d21fcfcea41f9372b16eb35b9a06405b12 Mon Sep 17 00:00:00 2001 From: Stuart Doherty Date: Fri, 11 Jul 2025 09:40:01 -0400 Subject: [PATCH 33/37] Update source/configure/calls-troubleshooting.md Co-authored-by: Christopher Poile --- source/configure/calls-troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/configure/calls-troubleshooting.md b/source/configure/calls-troubleshooting.md index d964a24d086..68702369ad6 100644 --- a/source/configure/calls-troubleshooting.md +++ b/source/configure/calls-troubleshooting.md @@ -328,7 +328,7 @@ For easier log management, configure calls-offloader to capture job logs directl output_logs = true ``` -With this configuration enabled, all job logs will be written to the main calls-offloader log file. +With this configuration enabled, all logs from recorder and transcriber docker containers will be written to the main calls-offloader log file. #### View Completed Jobs From 6093b67f4ca69832d5610568d3eb7b58c5a44a44 Mon Sep 17 00:00:00 2001 From: Stuart Doherty Date: Fri, 11 Jul 2025 09:40:19 -0400 Subject: [PATCH 34/37] Update source/configure/calls-troubleshooting.md Co-authored-by: Christopher Poile --- source/configure/calls-troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/configure/calls-troubleshooting.md b/source/configure/calls-troubleshooting.md index 68702369ad6..f42420b1862 100644 --- a/source/configure/calls-troubleshooting.md +++ b/source/configure/calls-troubleshooting.md @@ -339,7 +339,7 @@ To view completed calls-offloader job containers (useful for debugging failed jo docker ps -a --filter "status=exited" ``` -Look for containers with calls-offloader image names that have exited. You can then examine their logs: +Look for containers with recorder and transcriber image names that have exited. You can then examine their logs: ```bash # View logs from a specific completed container From ff5b61d09d4712f284c8341b8ee6586687fec60b Mon Sep 17 00:00:00 2001 From: Stu Doherty Date: Fri, 11 Jul 2025 10:57:41 -0400 Subject: [PATCH 35/37] format update --- source/configure/calls-deployment.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/configure/calls-deployment.md b/source/configure/calls-deployment.md index 38308fc53cb..dff70c3f03c 100644 --- a/source/configure/calls-deployment.md +++ b/source/configure/calls-deployment.md @@ -103,7 +103,7 @@ Mattermost Calls provides integrated audio calling and screen sharing capabiliti RTC (Calls plugin or rtcd) 8443 UDP (incoming) -Mattermost clients (Web/Desktop/Mobile) and ``calls-offloader`` spawned jobs (Recorder, Transcriber) +Mattermost clients (Web/Desktop/Mobile) and jobs spawned by Calls Offloader (Recorder, Transcriber) Mattermost instance or rtcd service To allow clients to establish connections that transport calls related media (e.g. audio, video). This should be open on any network component (e.g. NAT, firewalls) in between the instance running the plugin (or rtcd) and the clients joining calls so that UDP traffic is correctly routed both ways (from/to clients). @@ -111,7 +111,7 @@ Mattermost Calls provides integrated audio calling and screen sharing capabiliti RTC (Calls plugin or rtcd) 8443 TCP (incoming) -Mattermost clients (Web/Desktop/Mobile) and ``calls-offloader`` spawned jobs (Recorder, Transcriber) +Mattermost clients (Web/Desktop/Mobile) and jobs spawned by Calls Offloader (Recorder, Transcriber) Mattermost instance or rtcd service To allow clients to establish connections that transport calls related media (e.g. audio, video). This should be open on any network component (e.g. NAT, firewalls) in between the instance running the plugin (or rtcd) and the clients joining calls so that TCP traffic is correctly routed both ways (from/to clients). This can be used as a backup channel in case clients are unable to connect using UDP. It requires rtcd version >= v0.11 and Calls version >= v0.17. From 8d9205652f6a561189e27820c9a23d47b0850300 Mon Sep 17 00:00:00 2001 From: Stuart Doherty Date: Fri, 11 Jul 2025 12:24:45 -0400 Subject: [PATCH 36/37] Update source/configure/calls-offloader-setup.md Co-authored-by: Christopher Poile --- source/configure/calls-offloader-setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/configure/calls-offloader-setup.md b/source/configure/calls-offloader-setup.md index 01218fcddcd..420c3fee48b 100644 --- a/source/configure/calls-offloader-setup.md +++ b/source/configure/calls-offloader-setup.md @@ -287,7 +287,7 @@ Once calls-offloader is properly set up and validated, configure Mattermost to u - Find the **Calls** plugin and click **Disable** - Wait a few seconds, then click **Enable** -6. Test by starting a call and enabling recording or live captions +6. Test by starting a call and starting a recording ## Troubleshooting From 88b0d4cdb1fc5adad51a1701a8581c8774f4c712 Mon Sep 17 00:00:00 2001 From: Stuart Doherty Date: Fri, 11 Jul 2025 16:20:27 -0400 Subject: [PATCH 37/37] Update source/configure/calls-metrics-monitoring.md Co-authored-by: Claudio Costa --- source/configure/calls-metrics-monitoring.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/configure/calls-metrics-monitoring.md b/source/configure/calls-metrics-monitoring.md index fdb2bc6ff16..d5028896d27 100644 --- a/source/configure/calls-metrics-monitoring.md +++ b/source/configure/calls-metrics-monitoring.md @@ -81,7 +81,7 @@ To monitor Calls metrics, you'll need: labels: service_name: 'rtcd' - - job_name: 'calls_offloader-node-exporter' + - job_name: 'calls-offloader-node-exporter' metrics_path: /metrics static_configs: - targets: ['CALLS_OFFLOADER_SERVER_IP:9100']