Introduce bufferStalled playback metric

This commit is contained in:
Chocobozzz 2024-08-08 14:27:19 +02:00
parent f2887d29b8
commit 62bf86c186
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
9 changed files with 64 additions and 14 deletions

View File

@ -18,6 +18,8 @@ class MetricsPlugin extends Plugin {
private resolutionChanges = 0 private resolutionChanges = 0
private errors = 0 private errors = 0
private bufferStalled = 0
private p2pEnabled: boolean private p2pEnabled: boolean
private p2pPeers = 0 private p2pPeers = 0
@ -33,6 +35,7 @@ class MetricsPlugin extends Plugin {
this.trackBytes() this.trackBytes()
this.trackResolutionChange() this.trackResolutionChange()
this.trackErrors() this.trackErrors()
this.trackBufferStalled()
this.one('play', () => { this.one('play', () => {
this.player.on('video-change', () => { this.player.on('video-change', () => {
@ -56,6 +59,7 @@ class MetricsPlugin extends Plugin {
this.resolutionChanges = 0 this.resolutionChanges = 0
this.errors = 0 this.errors = 0
this.bufferStalled = 0
this.lastPlayerNetworkInfo = undefined this.lastPlayerNetworkInfo = undefined
@ -107,6 +111,7 @@ class MetricsPlugin extends Plugin {
resolutionChanges: this.resolutionChanges, resolutionChanges: this.resolutionChanges,
errors: this.errors, errors: this.errors,
bufferStalled: this.bufferStalled,
downloadedBytesHTTP: this.downloadedBytesHTTP, downloadedBytesHTTP: this.downloadedBytesHTTP,
@ -128,6 +133,8 @@ class MetricsPlugin extends Plugin {
this.errors = 0 this.errors = 0
this.bufferStalled = 0
const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' }) const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' })
return fetch(this.options_.metricsUrl(), { method: 'POST', body: JSON.stringify(body), headers }) return fetch(this.options_.metricsUrl(), { method: 'POST', body: JSON.stringify(body), headers })
@ -161,9 +168,19 @@ class MetricsPlugin extends Plugin {
private trackErrors () { private trackErrors () {
this.player.on('error', () => { this.player.on('error', () => {
debugLogger('Adding player error')
this.errors++ this.errors++
}) })
} }
private trackBufferStalled () {
this.player.on('buffer-stalled', () => {
debugLogger('Adding buffer stalled')
this.bufferStalled++
})
}
} }
videojs.registerPlugin('metrics', MetricsPlugin) videojs.registerPlugin('metrics', MetricsPlugin)

View File

@ -159,6 +159,15 @@ class P2pMediaLoaderPlugin extends Plugin {
this.player.trigger('video-ratio-changed', { ratio: level.width / level.height }) this.player.trigger('video-ratio-changed', { ratio: level.width / level.height })
} }
}) })
// Track buffer issues
this.hlsjs.on(Hlsjs.Events.ERROR, (_event, errorData) => {
if (errorData.type !== Hlsjs.ErrorTypes.MEDIA_ERROR) return
if (errorData.details === Hlsjs.ErrorDetails.BUFFER_STALLED_ERROR) {
this.player.trigger('buffer-stalled')
}
})
} }
private runStats () { private runStats () {

View File

@ -71,15 +71,11 @@ class Logger {
console.error('Cannot set tokens to client log sender.', { err }) console.error('Cannot set tokens to client log sender.', { err })
} }
try { fetch(serverUrl + '/api/v1/server/logs/client', {
fetch(serverUrl + '/api/v1/server/logs/client', { headers,
headers, method: 'POST',
method: 'POST', body: JSON.stringify(payload)
body: JSON.stringify(payload) }).catch(err => console.error('Cannot send client warn/error to server.', err))
})
} catch (err) {
console.error('Cannot send client warn/error to server.', err)
}
} }
private buildServerLogPayload (level: Extract<LoggerLevel, 'warn' | 'error'>, message: LoggerMessage, meta?: LoggerMeta) { private buildServerLogPayload (level: Extract<LoggerLevel, 'warn' | 'error'>, message: LoggerMessage, meta?: LoggerMeta) {

View File

@ -12,6 +12,7 @@ export interface PlaybackMetricCreate {
resolutionChanges: number resolutionChanges: number
errors: number errors: number
bufferStalled: number
downloadedBytesP2P: number downloadedBytesP2P: number
downloadedBytesHTTP: number downloadedBytesHTTP: number

View File

@ -48,6 +48,7 @@ describe('Test metrics API validators', function () {
downloadedBytesP2P: 0, downloadedBytesP2P: 0,
downloadedBytesHTTP: 0, downloadedBytesHTTP: 0,
uploadedBytesP2P: 0, uploadedBytesP2P: 0,
bufferStalled: 0,
videoId: videoUUID videoId: videoUUID
} }
}) })
@ -174,6 +175,14 @@ describe('Test metrics API validators', function () {
}) })
}) })
it('Should fail with an invalid bufferStalled', async function () {
await makePostBodyRequest({
url: server.url,
path,
fields: { ...baseParams, bufferStalled: 'toto' }
})
})
it('Should fail with a bad video id', async function () { it('Should fail with a bad video id', async function () {
await makePostBodyRequest({ await makePostBodyRequest({
url: server.url, url: server.url,

View File

@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai' import { HttpStatusCode, VideoPrivacy, VideoResolution } from '@peertube/peertube-models'
import { HttpStatusCode, PlaybackMetricCreate, VideoPrivacy, VideoResolution } from '@peertube/peertube-models'
import { import {
cleanupTests, cleanupTests,
createSingleServer, createSingleServer,
@ -9,8 +8,9 @@ import {
PeerTubeServer, PeerTubeServer,
setAccessTokensToServers setAccessTokensToServers
} from '@peertube/peertube-server-commands' } from '@peertube/peertube-server-commands'
import { expectLogDoesNotContain, expectLogContain } from '@tests/shared/checks.js' import { expectLogContain, expectLogDoesNotContain } from '@tests/shared/checks.js'
import { MockHTTP } from '@tests/shared/mock-servers/mock-http.js' import { MockHTTP } from '@tests/shared/mock-servers/mock-http.js'
import { expect } from 'chai'
describe('Open Telemetry', function () { describe('Open Telemetry', function () {
let server: PeerTubeServer let server: PeerTubeServer
@ -73,6 +73,7 @@ describe('Open Telemetry', function () {
downloadedBytesHTTP: 0, downloadedBytesHTTP: 0,
uploadedBytesP2P: 5, uploadedBytesP2P: 5,
p2pPeers: 1, p2pPeers: 1,
bufferStalled: 2,
p2pEnabled: false, p2pEnabled: false,
videoId: video.uuid videoId: video.uuid
} }
@ -91,7 +92,7 @@ describe('Open Telemetry', function () {
const video = await server.videos.quickUpload({ name: 'video' }) const video = await server.videos.quickUpload({ name: 'video' })
const metrics = { const metrics = {
playerMode: 'p2p-media-loader', playerMode: 'p2p-media-loader' as 'p2p-media-loader',
resolution: VideoResolution.H_1080P, resolution: VideoResolution.H_1080P,
fps: 30, fps: 30,
resolutionChanges: 1, resolutionChanges: 1,
@ -100,9 +101,10 @@ describe('Open Telemetry', function () {
downloadedBytesHTTP: 0, downloadedBytesHTTP: 0,
uploadedBytesP2P: 5, uploadedBytesP2P: 5,
p2pPeers: 7, p2pPeers: 7,
bufferStalled: 8,
p2pEnabled: false, p2pEnabled: false,
videoId: video.uuid videoId: video.uuid
} as PlaybackMetricCreate }
await server.metrics.addPlaybackMetric({ metrics }) await server.metrics.addPlaybackMetric({ metrics })

View File

@ -5,6 +5,7 @@ import { PlaybackMetricCreate } from '@peertube/peertube-models'
export class PlaybackMetrics { export class PlaybackMetrics {
private errorsCounter: Counter private errorsCounter: Counter
private resolutionChangesCounter: Counter private resolutionChangesCounter: Counter
private bufferStalledCounter: Counter
private downloadedBytesP2PCounter: Counter private downloadedBytesP2PCounter: Counter
private uploadedBytesP2PCounter: Counter private uploadedBytesP2PCounter: Counter
@ -29,6 +30,10 @@ export class PlaybackMetrics {
description: 'Resolution changes collected from PeerTube player.' description: 'Resolution changes collected from PeerTube player.'
}) })
this.bufferStalledCounter = this.meter.createCounter('peertube_playback_buffer_stalled_count', {
description: 'Number of times playback is stuck because buffer is running out of data, collected from PeerTube player.'
})
this.downloadedBytesHTTPCounter = this.meter.createCounter('peertube_playback_http_downloaded_bytes', { this.downloadedBytesHTTPCounter = this.meter.createCounter('peertube_playback_http_downloaded_bytes', {
description: 'Downloaded bytes with HTTP by PeerTube player.' description: 'Downloaded bytes with HTTP by PeerTube player.'
}) })
@ -75,6 +80,10 @@ export class PlaybackMetrics {
this.uploadedBytesP2PCounter.add(metrics.uploadedBytesP2P, attributes) this.uploadedBytesP2PCounter.add(metrics.uploadedBytesP2P, attributes)
if (metrics.bufferStalled) {
this.bufferStalledCounter.add(metrics.bufferStalled, attributes)
}
if (metrics.p2pPeers) { if (metrics.p2pPeers) {
this.peersP2PPeersGaugeBuffer.push({ this.peersP2PPeersGaugeBuffer.push({
value: metrics.p2pPeers, value: metrics.p2pPeers,

View File

@ -26,6 +26,10 @@ const addPlaybackMetricValidator = [
body('resolutionChanges') body('resolutionChanges')
.isInt({ min: 0 }), .isInt({ min: 0 }),
body('bufferStalled')
.optional()
.isInt({ min: 0 }),
body('errors') body('errors')
.isInt({ min: 0 }), .isInt({ min: 0 }),

View File

@ -10987,6 +10987,9 @@ components:
resolutionChanges: resolutionChanges:
type: number type: number
description: How many resolution changes occurred since the last metric creation description: How many resolution changes occurred since the last metric creation
bufferStalled:
type: number
description: How many times buffer has been stalled since the last metric creation
errors: errors:
type: number type: number
description: How many errors occurred since the last metric creation description: How many errors occurred since the last metric creation