snapshot of https://github.com/livekit/livekit.git @ 08c5433449c106943231037d0b892895f6e92d3b (2026-09-15)
Some checks failed
Release to Docker / docker (push) Failing after 2s
Release Test Server to Docker / docker (push) Failing after 3s
Test / test (push) Failing after 16s

This commit is contained in:
upstream-mirror
2026-09-15 16:14:22 +05:00
commit 78704103fc
422 changed files with 154805 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus
import (
"slices"
"strings"
"github.com/prometheus/client_golang/prometheus"
"github.com/livekit/protocol/livekit"
)
var (
promDataPacketStreamLabels = []string{"type", "mime_type"}
promDataPacketStreamMimeTypes = []string{"text", "image", "application", "audio", "video"}
promDataPacketStreamDestCount *prometheus.HistogramVec
promDataPacketStreamSize *prometheus.HistogramVec
)
func initDataPacketStats(nodeID string, nodeType livekit.NodeType) {
promDataPacketStreamDestCount = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "datapacket_stream",
Name: "dest_count",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: []float64{1, 2, 3, 4, 5, 10, 15, 25, 50},
}, promDataPacketStreamLabels)
promDataPacketStreamSize = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "datapacket_stream",
Name: "bytes",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: []float64{128, 512, 2048, 8192, 32768, 131072, 524288, 2097152, 8388608, 33554432},
}, promDataPacketStreamLabels)
prometheus.MustRegister(promDataPacketStreamDestCount)
prometheus.MustRegister(promDataPacketStreamSize)
}
func RecordDataPacketStream(h *livekit.DataStream_Header, destCount int) {
streamType := "unknown"
switch h.ContentHeader.(type) {
case *livekit.DataStream_Header_TextHeader:
streamType = "text"
case *livekit.DataStream_Header_ByteHeader:
streamType = "bytes"
}
mimeType := strings.ToLower(h.MimeType)
if i := strings.IndexByte(mimeType, '/'); i != -1 {
mimeType = mimeType[:i]
}
if !slices.Contains(promDataPacketStreamMimeTypes, mimeType) {
mimeType = "unknown"
}
promDataPacketStreamDestCount.WithLabelValues(streamType, mimeType).Observe(float64(destCount))
if h.TotalLength != nil {
promDataPacketStreamSize.WithLabelValues(streamType, mimeType).Observe(float64(*h.TotalLength))
}
}

View File

@@ -0,0 +1,40 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/livekit/protocol/livekit"
)
var (
refCounts *prometheus.GaugeVec
)
func initDebugStats(nodeID string, nodeType livekit.NodeType) {
refCounts = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: livekitNamespace,
Subsystem: "debug",
Name: "ref_count",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, []string{"referrer"})
prometheus.MustRegister(refCounts)
}
func AddRef(referrer string, n int) {
refCounts.WithLabelValues(referrer).Add(float64(n))
}

View File

@@ -0,0 +1,310 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus
import (
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/twitchtv/twirp"
"go.uber.org/atomic"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/rpc"
"github.com/livekit/protocol/utils/hwstats"
"github.com/livekit/protocol/webhook"
)
const (
livekitNamespace string = "livekit"
)
var (
initialized atomic.Bool
promMessageCounter *prometheus.CounterVec
promServiceOperationCounter *prometheus.CounterVec
promTwirpRequestStatusCounter *prometheus.CounterVec
promTwirpRequestLatency *prometheus.HistogramVec
sysPacketsStart uint32
sysDroppedPacketsStart uint32
promSysPacketGauge *prometheus.GaugeVec
cpuStats *hwstats.CPUStats
memoryStats *hwstats.MemoryStats
)
func Init(nodeID string, nodeType livekit.NodeType) error {
if initialized.Swap(true) {
return nil
}
promMessageCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "node",
Name: "messages",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
},
[]string{"type", "status", "direction"},
)
promServiceOperationCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "node",
Name: "service_operation",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
},
[]string{"type", "status", "error_type"},
)
promTwirpRequestStatusCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "node",
Name: "twirp_request_status",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
},
[]string{"service", "method", "status", "code"},
)
promTwirpRequestLatency = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "node",
Name: "twirp_request_latency_ms",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000},
},
[]string{"service", "method", "status"},
)
promSysPacketGauge = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: livekitNamespace,
Subsystem: "node",
Name: "packet_total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Help: "System level packet count. Count starts at 0 when service is first started.",
},
[]string{"type"},
)
prometheus.MustRegister(promMessageCounter)
prometheus.MustRegister(promServiceOperationCounter)
prometheus.MustRegister(promTwirpRequestStatusCounter)
prometheus.MustRegister(promTwirpRequestLatency)
prometheus.MustRegister(promSysPacketGauge)
sysPacketsStart, sysDroppedPacketsStart, _ = getTCStats()
initPacketStats(nodeID, nodeType)
initRoomStats(nodeID, nodeType)
rpc.InitPSRPCStats(prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()})
webhook.InitWebhookStats(prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()})
initQualityStats(nodeID, nodeType)
initDataPacketStats(nodeID, nodeType)
initDebugStats(nodeID, nodeType)
var err error
cpuStats, err = hwstats.NewCPUStats(nil)
if err != nil {
return err
}
memoryStats, err = hwstats.NewMemoryStats()
if err != nil {
return err
}
return nil
}
func GetNodeStats(nodeStartedAt int64, prevStats []*livekit.NodeStats, rateIntervals []time.Duration) (*livekit.NodeStats, error) {
loadAvg, err := getLoadAvg()
if err != nil {
return nil, err
}
// On MacOS, get "\"vm_stat\": executable file not found in $PATH" although it is in /usr/bin
// So, do not error out. Use the information if it is available.
memUsed, memTotal, _ := memoryStats.GetMemory()
sysPackets, sysDroppedPackets, _ := getTCStats()
promSysPacketGauge.WithLabelValues("out").Set(float64(sysPackets - sysPacketsStart))
promSysPacketGauge.WithLabelValues("dropped").Set(float64(sysDroppedPackets - sysDroppedPacketsStart))
stats := &livekit.NodeStats{
StartedAt: nodeStartedAt,
UpdatedAt: time.Now().Unix(),
NumRooms: roomCurrent.Load(),
NumClients: participantCurrent.Load(),
NumTracksIn: trackPublishedCurrent.Load(),
NumTracksOut: trackSubscribedCurrent.Load(),
NumTrackPublishAttempts: trackPublishAttempts.Load(),
NumTrackPublishSuccess: trackPublishSuccess.Load(),
NumTrackPublishCancels: trackPublishCancels.Load(),
NumTrackSubscribeAttempts: trackSubscribeAttempts.Load(),
NumTrackSubscribeSuccess: trackSubscribeSuccess.Load(),
NumTrackSubscribeCancels: trackSubscribeCancels.Load(),
BytesIn: bytesIn.Load(),
BytesOut: bytesOut.Load(),
PacketsIn: packetsIn.Load(),
PacketsOut: packetsOut.Load(),
RetransmitBytesOut: retransmitBytes.Load(),
RetransmitPacketsOut: retransmitPackets.Load(),
NackTotal: nackTotal.Load(),
ParticipantSignalConnected: participantSignalConnected.Load(),
ParticipantRtcInit: participantRTCInit.Load(),
ParticipantRtcConnected: participantRTCConnected.Load(),
ParticipantRtcCanceled: participantRTCCanceled.Load(),
ParticipantRtcActive: participantRTCActive.Load(),
ForwardLatency: forwardLatency.Load(),
ForwardJitter: forwardJitter.Load(),
NumCpus: uint32(cpuStats.NumCPU()), // this will round down to the nearest integer
CpuLoad: float32(cpuStats.GetCPULoad()),
MemoryTotal: memTotal,
MemoryUsed: memUsed,
LoadAvgLast1Min: float32(loadAvg.Loadavg1),
LoadAvgLast5Min: float32(loadAvg.Loadavg5),
LoadAvgLast15Min: float32(loadAvg.Loadavg15),
SysPacketsOut: sysPackets,
SysPacketsDropped: sysDroppedPackets,
}
for _, rateInterval := range rateIntervals {
for idx := len(prevStats) - 1; idx >= 0; idx-- {
prev := prevStats[idx]
if prev == nil {
continue
}
if stats.UpdatedAt-prev.UpdatedAt >= int64(rateInterval.Seconds()) {
if rate := getNodeStatsRate(append(prevStats[idx:], stats)); rate != nil {
stats.Rates = append(stats.Rates, rate)
}
break
}
}
}
return stats, nil
}
func getNodeStatsRate(statsHistory []*livekit.NodeStats) *livekit.NodeStatsRate {
if len(statsHistory) == 0 {
return nil
}
elapsed := statsHistory[len(statsHistory)-1].UpdatedAt - statsHistory[0].UpdatedAt
if elapsed <= 0 {
return nil
}
// time weighted averages
var cpuLoad, memoryUsed, memoryTotal, memoryLoad float32
for idx := len(statsHistory) - 1; idx > 0; idx-- {
stats := statsHistory[idx]
prevStats := statsHistory[idx-1]
if stats == nil || prevStats == nil {
continue
}
spanElapsed := stats.UpdatedAt - prevStats.UpdatedAt
if spanElapsed <= 0 {
continue
}
cpuLoad += stats.CpuLoad * float32(spanElapsed)
memoryUsed += float32(stats.MemoryUsed) * float32(spanElapsed)
memoryTotal += float32(stats.MemoryTotal) * float32(spanElapsed)
if stats.MemoryTotal > 0 {
memoryLoad += float32(stats.MemoryUsed) / float32(stats.MemoryTotal) * float32(spanElapsed)
}
}
earlier := statsHistory[0]
later := statsHistory[len(statsHistory)-1]
rate := &livekit.NodeStatsRate{
StartedAt: earlier.UpdatedAt,
EndedAt: later.UpdatedAt,
Duration: elapsed,
BytesIn: perSec(earlier.BytesIn, later.BytesIn, elapsed),
BytesOut: perSec(earlier.BytesOut, later.BytesOut, elapsed),
PacketsIn: perSec(earlier.PacketsIn, later.PacketsIn, elapsed),
PacketsOut: perSec(earlier.PacketsOut, later.PacketsOut, elapsed),
RetransmitBytesOut: perSec(earlier.RetransmitBytesOut, later.RetransmitBytesOut, elapsed),
RetransmitPacketsOut: perSec(earlier.RetransmitPacketsOut, later.RetransmitPacketsOut, elapsed),
NackTotal: perSec(earlier.NackTotal, later.NackTotal, elapsed),
ParticipantSignalConnected: perSec(earlier.ParticipantSignalConnected, later.ParticipantSignalConnected, elapsed),
ParticipantSignalFailed: perSec(earlier.ParticipantSignalFailed, later.ParticipantSignalFailed, elapsed),
ParticipantSignalValidationFailed: perSec(earlier.ParticipantSignalValidationFailed, later.ParticipantSignalValidationFailed, elapsed),
ParticipantRtcInit: perSec(earlier.ParticipantRtcInit, later.ParticipantRtcInit, elapsed),
ParticipantRtcConnected: perSec(earlier.ParticipantRtcConnected, later.ParticipantRtcConnected, elapsed),
ParticipantRtcCanceled: perSec(earlier.ParticipantRtcCanceled, later.ParticipantRtcCanceled, elapsed),
ParticipantRtcActive: perSec(earlier.ParticipantRtcActive, later.ParticipantRtcActive, elapsed),
SysPacketsOut: perSec(uint64(earlier.SysPacketsOut), uint64(later.SysPacketsOut), elapsed),
SysPacketsDropped: perSec(uint64(earlier.SysPacketsDropped), uint64(later.SysPacketsDropped), elapsed),
TrackPublishAttempts: perSec(uint64(earlier.NumTrackPublishAttempts), uint64(later.NumTrackPublishAttempts), elapsed),
TrackPublishSuccess: perSec(uint64(earlier.NumTrackPublishSuccess), uint64(later.NumTrackPublishSuccess), elapsed),
TrackPublishCancels: perSec(uint64(earlier.NumTrackPublishCancels), uint64(later.NumTrackPublishCancels), elapsed),
TrackSubscribeAttempts: perSec(uint64(earlier.NumTrackSubscribeAttempts), uint64(later.NumTrackSubscribeAttempts), elapsed),
TrackSubscribeSuccess: perSec(uint64(earlier.NumTrackSubscribeSuccess), uint64(later.NumTrackSubscribeSuccess), elapsed),
TrackSubscribeCancels: perSec(uint64(earlier.NumTrackSubscribeCancels), uint64(later.NumTrackSubscribeCancels), elapsed),
CpuLoad: cpuLoad / float32(elapsed),
MemoryLoad: memoryLoad / float32(elapsed),
MemoryUsed: memoryUsed / float32(elapsed),
MemoryTotal: memoryTotal / float32(elapsed),
}
return rate
}
func perSec(prev, curr uint64, secs int64) float32 {
return float32(curr-prev) / float32(secs)
}
func RecordSignalRequestSuccess() {
promMessageCounter.WithLabelValues("signal", "success", "request").Add(1)
}
func RecordSignalRequestFailure() {
promMessageCounter.WithLabelValues("signal", "failure", "request").Add(1)
}
func RecordSignalResponseSuccess() {
promMessageCounter.WithLabelValues("signal", "success", "response").Add(1)
}
func RecordSignalResponseFailure() {
promMessageCounter.WithLabelValues("signal", "failure", "response").Add(1)
}
func RecordServiceOperationSuccess(op string) {
promServiceOperationCounter.WithLabelValues(op, "success", "").Add(1)
}
func RecordServiceOperationError(op string, error string) {
promServiceOperationCounter.WithLabelValues(op, "error", error).Add(1)
}
func RecordTwirpRequestStatus(service string, method string, statusFamily string, code twirp.ErrorCode) {
promTwirpRequestStatusCounter.WithLabelValues(service, method, statusFamily, string(code)).Add(1)
}
func RecordTwirpRequestLatency(service, method string, duration time.Duration, statusFamily string) {
promTwirpRequestLatency.WithLabelValues(service, method, statusFamily).Observe(float64(duration.Milliseconds()))
}

View File

@@ -0,0 +1,56 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
// +build linux
package prometheus
import (
"fmt"
"github.com/florianl/go-tc"
)
func getTCStats() (packets, drops uint32, err error) {
rtnl, err := tc.Open(&tc.Config{})
if err != nil {
err = fmt.Errorf("could not open rtnetlink socket: %v", err)
return
}
defer rtnl.Close()
qdiscs, err := rtnl.Qdisc().Get()
if err != nil {
err = fmt.Errorf("could not get qdiscs: %v", err)
return
}
for _, qdisc := range qdiscs {
// Newer kernels report stats via TCA_STATS2 (Stats2), while older kernels
// only populate the legacy TCA_STATS attribute (Stats). Prefer Stats2 and
// fall back to Stats so counters are collected on both.
//
// However, go-tc (through v0.4.8 and current main) mis-parses the nested TCA_STATS2
// as a flat struct, so Stats2.Packets/.Drops are garbage. The legacy
// TCA_STATS blob is a real flat struct and is parsed correctly, and modern
// kernels populate BOTH — so prefer Stats.
if qdisc.Stats != nil {
packets += qdisc.Stats.Packets
drops += qdisc.Stats.Drops
}
}
return
}

View File

@@ -0,0 +1,22 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !linux
package prometheus
func getTCStats() (packets, drops uint32, err error) {
// linux only
return
}

View File

@@ -0,0 +1,25 @@
//go:build !windows
/*
* Copyright 2023 LiveKit, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package prometheus
import "github.com/mackerelio/go-osstat/loadavg"
func getLoadAvg() (*loadavg.Stats, error) {
return loadavg.Get()
}

View File

@@ -0,0 +1,25 @@
//go:build windows
/*
* Copyright 2023 LiveKit, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package prometheus
import "github.com/mackerelio/go-osstat/loadavg"
func getLoadAvg() (*loadavg.Stats, error) {
return &loadavg.Stats{}, nil
}

View File

@@ -0,0 +1,391 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus
import (
"strconv"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/atomic"
"github.com/livekit/protocol/livekit"
)
type Direction string
const (
Incoming Direction = "incoming"
Outgoing Direction = "outgoing"
)
type TransmissionType string
const (
TransmissionInitial TransmissionType = "initial"
TransmissionRetransmit TransmissionType = "retransmit"
)
var (
bytesIn atomic.Uint64
bytesOut atomic.Uint64
packetsIn atomic.Uint64
packetsOut atomic.Uint64
nackTotal atomic.Uint64
retransmitBytes atomic.Uint64
retransmitPackets atomic.Uint64
participantSignalConnected atomic.Uint64
participantRTCConnected atomic.Uint64
participantRTCInit atomic.Uint64
participantRTCCanceled atomic.Uint64
participantRTCActive atomic.Uint64
forwardLatency atomic.Uint32
forwardJitter atomic.Uint32
promPacketLabels = []string{"direction", "transmission", "country"}
promPacketTotal *prometheus.CounterVec
promPacketBytes *prometheus.CounterVec
promRTCPLabels = []string{"direction", "country"}
promStreamLabels = []string{"direction", "source", "type", "country"}
promNackTotal *prometheus.CounterVec
promPliTotal *prometheus.CounterVec
promFirTotal *prometheus.CounterVec
promPacketLossTotal *prometheus.CounterVec
promPacketLoss *prometheus.HistogramVec
promPacketOutOfOrderTotal *prometheus.CounterVec
promPacketOutOfOrder *prometheus.HistogramVec
promJitter *prometheus.HistogramVec
promRTT *prometheus.HistogramVec
promParticipantJoin *prometheus.CounterVec
promConnections *prometheus.GaugeVec
promForwardLatency prometheus.Gauge
promForwardJitter prometheus.Gauge
promForwardLatencyHist prometheus.Histogram
)
func initPacketStats(nodeID string, nodeType livekit.NodeType) {
promPacketTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "packet",
Name: "total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, promPacketLabels)
promPacketBytes = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "packet",
Name: "bytes",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, promPacketLabels)
promNackTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "nack",
Name: "total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, promRTCPLabels)
promPliTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "pli",
Name: "total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, promRTCPLabels)
promFirTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "fir",
Name: "total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, promRTCPLabels)
promPacketLossTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "packet_loss",
Name: "total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, promStreamLabels)
promPacketLoss = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "packet_loss",
Name: "percent",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: []float64{0.0, 0.1, 0.3, 0.5, 0.7, 1, 5, 10, 40, 100},
}, promStreamLabels)
promPacketOutOfOrderTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "packet_out_of_order",
Name: "total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, promStreamLabels)
promPacketOutOfOrder = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "packet_out_of_order",
Name: "percent",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: []float64{0.0, 0.1, 0.3, 0.5, 0.7, 1, 5, 10, 40, 100},
}, promStreamLabels)
promJitter = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "jitter",
Name: "us",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
// 1ms, 10ms, 30ms, 50ms, 70ms, 100ms, 300ms, 600ms, 1s
Buckets: []float64{1000, 10000, 30000, 50000, 70000, 100000, 300000, 600000, 1000000},
}, promStreamLabels)
promRTT = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "rtt",
Name: "ms",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: []float64{50, 100, 150, 200, 250, 500, 750, 1000, 5000, 10000},
}, promStreamLabels)
promParticipantJoin = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "participant_join",
Name: "total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, []string{"state", "warp"})
promConnections = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: livekitNamespace,
Subsystem: "connection",
Name: "total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, []string{"kind"})
promForwardLatency = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: livekitNamespace,
Subsystem: "forward",
Name: "latency",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
})
promForwardJitter = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: livekitNamespace,
Subsystem: "forward",
Name: "jitter",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
})
promForwardLatencyHist = prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "forward_latency",
Name: "ns",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
// 50us, 100us, 250us, 500us, 1ms, 2ms, 3ms, 5ms, 10ms, 20ms
Buckets: []float64{
50 * 1000,
100 * 1000,
250 * 1000,
500 * 1000,
1 * 1000 * 1000,
2 * 1000 * 1000,
3 * 1000 * 1000,
5 * 1000 * 1000,
10 * 1000 * 1000,
20 * 1000 * 1000,
},
})
prometheus.MustRegister(promPacketTotal)
prometheus.MustRegister(promPacketBytes)
prometheus.MustRegister(promNackTotal)
prometheus.MustRegister(promPliTotal)
prometheus.MustRegister(promFirTotal)
prometheus.MustRegister(promPacketLossTotal)
prometheus.MustRegister(promPacketLoss)
prometheus.MustRegister(promPacketOutOfOrderTotal)
prometheus.MustRegister(promPacketOutOfOrder)
prometheus.MustRegister(promJitter)
prometheus.MustRegister(promRTT)
prometheus.MustRegister(promParticipantJoin)
prometheus.MustRegister(promConnections)
prometheus.MustRegister(promForwardLatency)
prometheus.MustRegister(promForwardJitter)
prometheus.MustRegister(promForwardLatencyHist)
}
func IncrementPackets(country string, direction Direction, count uint64, retransmit bool) {
var transmission TransmissionType
if retransmit {
transmission = TransmissionRetransmit
} else {
transmission = TransmissionInitial
}
promPacketTotal.WithLabelValues(string(direction), string(transmission), country).Add(float64(count))
if direction == Incoming {
packetsIn.Add(count)
} else {
packetsOut.Add(count)
if retransmit {
retransmitPackets.Add(count)
}
}
}
func IncrementBytes(country string, direction Direction, count uint64, retransmit bool) {
var transmission TransmissionType
if retransmit {
transmission = TransmissionRetransmit
} else {
transmission = TransmissionInitial
}
promPacketBytes.WithLabelValues(string(direction), string(transmission), country).Add(float64(count))
if direction == Incoming {
bytesIn.Add(count)
} else {
bytesOut.Add(count)
if retransmit {
retransmitBytes.Add(count)
}
}
}
func IncrementRTCP(country string, direction Direction, nack, pli, fir uint32) {
if nack > 0 {
promNackTotal.WithLabelValues(string(direction), country).Add(float64(nack))
nackTotal.Add(uint64(nack))
}
if pli > 0 {
promPliTotal.WithLabelValues(string(direction), country).Add(float64(pli))
}
if fir > 0 {
promFirTotal.WithLabelValues(string(direction), country).Add(float64(fir))
}
}
func RecordPacketLoss(
country string,
direction Direction,
trackSource livekit.TrackSource,
trackType livekit.TrackType,
lost uint32,
total uint32,
) {
if total > 0 {
promPacketLoss.WithLabelValues(string(direction), trackSource.String(), trackType.String(), country).Observe(float64(lost) / float64(total) * 100)
}
if lost > 0 {
promPacketLossTotal.WithLabelValues(string(direction), trackSource.String(), trackType.String(), country).Add(float64(lost))
}
}
func RecordPacketOutOfOrder(country string, direction Direction, trackSource livekit.TrackSource, trackType livekit.TrackType, ooo, total uint32) {
if total > 0 {
promPacketOutOfOrder.WithLabelValues(string(direction), trackSource.String(), trackType.String(), country).Observe(float64(ooo) / float64(total) * 100)
}
if ooo > 0 {
promPacketOutOfOrderTotal.WithLabelValues(string(direction), trackSource.String(), trackType.String(), country).Add(float64(ooo))
}
}
func RecordJitter(country string, direction Direction, trackSource livekit.TrackSource, trackType livekit.TrackType, jitter uint32) {
if jitter > 0 {
promJitter.WithLabelValues(string(direction), trackSource.String(), trackType.String(), country).Observe(float64(jitter))
}
}
func RecordRTT(country string, direction Direction, trackSource livekit.TrackSource, trackType livekit.TrackType, rtt uint32) {
if rtt > 0 {
promRTT.WithLabelValues(string(direction), trackSource.String(), trackType.String(), country).Observe(float64(rtt))
}
}
func IncrementParticipantJoin(join uint32) {
if join > 0 {
participantSignalConnected.Add(uint64(join))
promParticipantJoin.WithLabelValues("signal_connected", "").Add(float64(join))
}
}
func IncrementParticipantJoinFail(fail uint32) {
if fail > 0 {
promParticipantJoin.WithLabelValues("signal_failed", "").Add(float64(fail))
}
}
func IncrementParticipantJoinValidationFail(validationFail uint32) {
if validationFail > 0 {
promParticipantJoin.WithLabelValues("signal_validation_failed", "").Add(float64(validationFail))
}
}
func IncrementParticipantJoinUpgradeFail(upgradeFail uint32) {
if upgradeFail > 0 {
promParticipantJoin.WithLabelValues("signal_upgrade_failed", "").Add(float64(upgradeFail))
}
}
func IncrementParticipantJoinWriteInitialResponseFail(writeInitialResponseFail uint32) {
if writeInitialResponseFail > 0 {
promParticipantJoin.WithLabelValues("signal_write_initial_response_failed", "").Add(float64(writeInitialResponseFail))
}
}
func IncrementParticipantRtcInit(init uint32) {
if init > 0 {
participantRTCInit.Add(uint64(init))
promParticipantJoin.WithLabelValues("rtc_init", "").Add(float64(init))
}
}
func IncrementParticipantRtcConnected(connected uint32) {
if connected > 0 {
participantRTCConnected.Add(uint64(connected))
promParticipantJoin.WithLabelValues("rtc_connected", "").Add(float64(connected))
}
}
func IncrementParticipantRtcActive(active uint32, warp bool) {
if active > 0 {
participantRTCActive.Add(uint64(active))
promParticipantJoin.WithLabelValues("rtc_active", strconv.FormatBool(warp)).Add(float64(active))
}
}
func IncrementParticipantRtcCanceled(canceled uint64, warp bool) {
if canceled > 0 {
participantRTCCanceled.Add(canceled)
promParticipantJoin.WithLabelValues("rtc_canceled", strconv.FormatBool(warp)).Add(float64(canceled))
}
}
// todo: check if need to record warp to rtcSucc/Failure
func IncrementParticipantRtcSuccess(success uint64, warp bool) {
if success > 0 {
promParticipantJoin.WithLabelValues("rtc_success", strconv.FormatBool(warp)).Add(float64(success))
}
}
func IncrementParticipantRtcFailure(failure uint64, warp bool) {
if failure > 0 {
promParticipantJoin.WithLabelValues("rtc_failure", strconv.FormatBool(warp)).Add(float64(failure))
}
}
func AddConnection(direction Direction) {
promConnections.WithLabelValues(string(direction)).Add(1)
}
func SubConnection(direction Direction) {
promConnections.WithLabelValues(string(direction)).Sub(1)
}
func RecordForwardLatencySample(forwardLatency int64) {
promForwardLatencyHist.Observe(float64(forwardLatency))
}
func RecordForwardLatency(longTermLatencyAvg uint32) {
forwardLatency.Store(longTermLatencyAvg)
promForwardLatency.Set(float64(longTermLatencyAvg))
}
func RecordForwardJitter(longTermJitterAvg uint32) {
forwardJitter.Store(longTermJitterAvg)
promForwardJitter.Set(float64(longTermJitterAvg))
}

View File

@@ -0,0 +1,51 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/livekit/protocol/livekit"
)
var (
qualityRating prometheus.Histogram
qualityScore prometheus.Histogram
)
func initQualityStats(nodeID string, nodeType livekit.NodeType) {
qualityRating = prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "quality",
Name: "rating",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: []float64{0, 1, 2},
})
qualityScore = prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "quality",
Name: "score",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: []float64{1.0, 2.0, 2.5, 3.0, 3.25, 3.5, 3.75, 4.0, 4.25, 4.5},
})
prometheus.MustRegister(qualityRating)
prometheus.MustRegister(qualityScore)
}
func RecordQuality(rating livekit.ConnectionQuality, score float32) {
qualityRating.Observe(float64(rating))
qualityScore.Observe(float64(score))
}

View File

@@ -0,0 +1,304 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus
import (
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/atomic"
"github.com/livekit/protocol/livekit"
)
var (
roomCurrent atomic.Int32
participantCurrent atomic.Int32
trackPublishedCurrent atomic.Int32
trackSubscribedCurrent atomic.Int32
trackPublishAttempts atomic.Int32
trackPublishSuccess atomic.Int32
trackPublishCancels atomic.Int32
trackSubscribeAttempts atomic.Int32
trackSubscribeSuccess atomic.Int32
trackSubscribeCancels atomic.Int32
// count the number of failures that are due to user error (permissions, track doesn't exist), so we could compute
// success rate by subtracting this from total attempts
trackSubscribeUserError atomic.Int32
promRoomCurrent prometheus.Gauge
promRoomDuration prometheus.Histogram
promParticipantCurrent prometheus.Gauge
promTrackPublishedCurrent *prometheus.GaugeVec
promTrackSubscribedCurrent *prometheus.GaugeVec
promTrackPublishCounter *prometheus.CounterVec
promTrackSubscribeCounter *prometheus.CounterVec
promSessionJoinLatency *prometheus.HistogramVec
promSessionStartTime *prometheus.HistogramVec
promSessionDuration *prometheus.HistogramVec
promPubSubTime *prometheus.HistogramVec
promPeerConnection *prometheus.CounterVec
)
func initRoomStats(nodeID string, nodeType livekit.NodeType) {
promRoomCurrent = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: livekitNamespace,
Subsystem: "room",
Name: "total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
})
promRoomDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "room",
Name: "duration_seconds",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: []float64{
5, 10, 60, 5 * 60, 10 * 60, 30 * 60, 60 * 60, 2 * 60 * 60, 5 * 60 * 60, 10 * 60 * 60,
},
})
promParticipantCurrent = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: livekitNamespace,
Subsystem: "participant",
Name: "total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
})
promTrackPublishedCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: livekitNamespace,
Subsystem: "track",
Name: "published_total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, []string{"kind"})
promTrackSubscribedCurrent = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: livekitNamespace,
Subsystem: "track",
Name: "subscribed_total",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, []string{"kind"})
promTrackPublishCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "track",
Name: "publish_counter",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, []string{"kind", "state"})
promTrackSubscribeCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "track",
Name: "subscribe_counter",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, []string{"state", "error"})
promSessionJoinLatency = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "session",
Name: "join_latency_ms",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: prometheus.ExponentialBucketsRange(10, 10000, 15),
}, []string{"protocol_version"})
promSessionStartTime = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "session",
Name: "start_time_ms",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: prometheus.ExponentialBucketsRange(100, 10000, 15),
}, []string{"protocol_version", "warp"})
promSessionDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "session",
Name: "duration_ms",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: prometheus.ExponentialBucketsRange(100, 4*60*60*1000, 15),
}, []string{"protocol_version"})
promPubSubTime = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: livekitNamespace,
Subsystem: "pubsubtime",
Name: "ms",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
Buckets: []float64{100, 200, 500, 700, 1000, 5000, 10000},
}, append(promStreamLabels, "sdk", "kind", "count"))
promPeerConnection = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: livekitNamespace,
Subsystem: "peer_connection",
Name: "state",
ConstLabels: prometheus.Labels{"node_id": nodeID, "node_type": nodeType.String()},
}, []string{"transport", "state"})
prometheus.MustRegister(promRoomCurrent)
prometheus.MustRegister(promRoomDuration)
prometheus.MustRegister(promParticipantCurrent)
prometheus.MustRegister(promTrackPublishedCurrent)
prometheus.MustRegister(promTrackSubscribedCurrent)
prometheus.MustRegister(promTrackPublishCounter)
prometheus.MustRegister(promTrackSubscribeCounter)
prometheus.MustRegister(promSessionJoinLatency)
prometheus.MustRegister(promSessionStartTime)
prometheus.MustRegister(promSessionDuration)
prometheus.MustRegister(promPubSubTime)
prometheus.MustRegister(promPeerConnection)
}
func RoomStarted() {
promRoomCurrent.Add(1)
roomCurrent.Inc()
}
func RoomEnded(startedAt time.Time) {
if !startedAt.IsZero() {
promRoomDuration.Observe(float64(time.Since(startedAt)) / float64(time.Second))
}
promRoomCurrent.Sub(1)
roomCurrent.Dec()
}
func AddParticipant() {
promParticipantCurrent.Add(1)
participantCurrent.Inc()
}
func SubParticipant() {
promParticipantCurrent.Sub(1)
participantCurrent.Dec()
}
func AddPublishedTrack(kind string) {
promTrackPublishedCurrent.WithLabelValues(kind).Add(1)
trackPublishedCurrent.Inc()
}
func SubPublishedTrack(kind string) {
promTrackPublishedCurrent.WithLabelValues(kind).Sub(1)
trackPublishedCurrent.Dec()
}
func RecordTrackPublishAttempt(kind string) {
trackPublishAttempts.Inc()
promTrackPublishCounter.WithLabelValues(kind, "attempt").Inc()
}
func RecordTrackPublishSuccess(kind string) {
trackPublishSuccess.Inc()
promTrackPublishCounter.WithLabelValues(kind, "success").Inc()
}
func RecordTrackPublishCancels(kind string, numCancels int32) {
trackPublishCancels.Add(numCancels)
promTrackPublishCounter.WithLabelValues(kind, "cancel").Add(float64(numCancels))
}
func RecordPublishTime(
country string,
source livekit.TrackSource,
trackType livekit.TrackType,
d time.Duration,
sdk livekit.ClientInfo_SDK,
kind livekit.ParticipantInfo_Kind,
) {
recordPubSubStreamStartTime("publish", country, source, trackType, d, sdk, kind, 1)
}
func RecordSubscribeTime(
country string,
source livekit.TrackSource,
trackType livekit.TrackType,
d time.Duration,
sdk livekit.ClientInfo_SDK,
kind livekit.ParticipantInfo_Kind,
count int,
) {
recordPubSubStreamStartTime("subscribe", country, source, trackType, d, sdk, kind, count)
}
func RecordSubscribeStreamStartTime(
country string,
source livekit.TrackSource,
trackType livekit.TrackType,
d time.Duration,
sdk livekit.ClientInfo_SDK,
kind livekit.ParticipantInfo_Kind,
) {
recordPubSubStreamStartTime("subscribe-stream-start", country, source, trackType, d, sdk, kind, 1)
}
func recordPubSubStreamStartTime(
direction string,
country string,
source livekit.TrackSource,
trackType livekit.TrackType,
d time.Duration,
sdk livekit.ClientInfo_SDK,
kind livekit.ParticipantInfo_Kind,
count int,
) {
promPubSubTime.WithLabelValues(
direction,
source.String(),
trackType.String(),
country,
sdk.String(),
kind.String(),
strconv.Itoa(count),
).Observe(float64(d.Milliseconds()))
}
func RecordTrackSubscribeSuccess(kind string) {
// modify both current and total counters
promTrackSubscribedCurrent.WithLabelValues(kind).Add(1)
trackSubscribedCurrent.Inc()
promTrackSubscribeCounter.WithLabelValues("success", "").Inc()
trackSubscribeSuccess.Inc()
}
func RecordTrackUnsubscribed(kind string) {
// unsubscribed modifies current counter, but we leave the total values alone since they
// are used to compute rate
promTrackSubscribedCurrent.WithLabelValues(kind).Sub(1)
trackSubscribedCurrent.Dec()
}
func RecordTrackSubscribeAttempt() {
trackSubscribeAttempts.Inc()
promTrackSubscribeCounter.WithLabelValues("attempt", "").Inc()
}
func RecordTrackSubscribeFailure(err error, isUserError bool) {
promTrackSubscribeCounter.WithLabelValues("failure", err.Error()).Inc()
if isUserError {
trackSubscribeUserError.Inc()
trackSubscribeCancels.Inc()
}
}
func RecordTrackSubscribeCancels(numCancels int32) {
trackSubscribeCancels.Add(numCancels)
promTrackSubscribeCounter.WithLabelValues("cancel", "").Add(float64(numCancels))
}
func RecordSessionJoinLatency(protocolVersion int, d time.Duration) {
promSessionJoinLatency.WithLabelValues(strconv.Itoa(protocolVersion)).Observe(float64(d.Milliseconds()))
}
func RecordSessionStartTime(protocolVersion int, warp bool, d time.Duration) {
promSessionStartTime.WithLabelValues(strconv.Itoa(protocolVersion), strconv.FormatBool(warp)).Observe(float64(d.Milliseconds()))
}
func RecordSessionDuration(protocolVersion int, d time.Duration) {
promSessionDuration.WithLabelValues(strconv.Itoa(protocolVersion)).Observe(float64(d.Milliseconds()))
}
func RecordPeerConnectionState(transport livekit.SignalTarget, state string) {
promPeerConnection.WithLabelValues(transport.String(), state).Inc()
}