]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add playback metric endpoint sent to OTEL
authorChocobozzz <me@florianbigard.com>
Fri, 12 Aug 2022 14:41:29 +0000 (16:41 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 16 Aug 2022 08:33:27 +0000 (10:33 +0200)
35 files changed:
client/src/app/+videos/+video-watch/video-watch.component.ts
client/src/assets/player/peertube-player-manager.ts
client/src/assets/player/shared/control-bar/p2p-info-button.ts
client/src/assets/player/shared/manager-options/manager-options-builder.ts
client/src/assets/player/shared/metrics/index.ts [new file with mode: 0644]
client/src/assets/player/shared/metrics/metrics-plugin.ts [new file with mode: 0644]
client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts
client/src/assets/player/shared/peertube/peertube-plugin.ts
client/src/assets/player/shared/stats/stats-card.ts
client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
client/src/assets/player/types/manager-options.ts
client/src/assets/player/types/peertube-videojs-typings.ts
client/src/standalone/videos/shared/player-manager-options.ts
config/test.yaml
package.json
server/controllers/api/index.ts
server/controllers/api/metrics.ts [new file with mode: 0644]
server/helpers/custom-validators/metrics.ts [new file with mode: 0644]
server/lib/opentelemetry/metric-helpers/index.ts
server/lib/opentelemetry/metric-helpers/nodejs-observers-builder.ts
server/lib/opentelemetry/metric-helpers/playback-metrics.ts [new file with mode: 0644]
server/lib/opentelemetry/metrics.ts
server/middlewares/validators/index.ts
server/middlewares/validators/metrics.ts [new file with mode: 0644]
server/tests/api/check-params/index.ts
server/tests/api/check-params/metrics.ts [new file with mode: 0644]
server/tests/api/server/open-telemetry.ts
shared/models/index.ts
shared/models/metrics/index.ts [new file with mode: 0644]
shared/models/metrics/playback-metric-create.model.ts [new file with mode: 0644]
shared/server-commands/server/index.ts
shared/server-commands/server/metrics-command.ts [new file with mode: 0644]
shared/server-commands/server/server.ts
support/doc/api/openapi.yaml
yarn.lock

index 8d9c08ab36edf9659f617443b64bb1bdd5656850..9ae6f9f12bc7c6b7babb91a27fbb467039dbec1d 100644 (file)
@@ -628,6 +628,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
           : null,
         authorizationHeader: this.authService.getRequestHeaderValue(),
 
+        metricsUrl: environment.apiUrl + '/api/v1/metrics/playback',
+
         embedUrl: video.embedUrl,
         embedTitle: video.name,
         instanceName: this.serverConfig.instance.name,
index b9077dcae520ccd6264502b99a0f5713890c3af5..0d4acc3d91e576054f45cfed29baca455e61f088 100644 (file)
@@ -22,6 +22,7 @@ import './shared/playlist/playlist-plugin'
 import './shared/mobile/peertube-mobile-plugin'
 import './shared/mobile/peertube-mobile-buttons'
 import './shared/hotkeys/peertube-hotkeys-plugin'
+import './shared/metrics/metrics-plugin'
 import videojs from 'video.js'
 import { logger } from '@root-helpers/logger'
 import { PluginsManager } from '@root-helpers/plugins-manager'
index 36517e125dac5b7a2ab2ed05a189028a6fa50a32..1979654adfe4bf81afda03125a5ffd98a569f27b 100644 (file)
@@ -87,9 +87,9 @@ class P2pInfoButton extends Button {
       const httpStats = data.http
 
       const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed)
-      const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed)
+      const uploadSpeed = bytes(p2pStats.uploadSpeed)
       const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded)
-      const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded)
+      const totalUploaded = bytes(p2pStats.uploaded)
       const numPeers = p2pStats.numPeers
 
       subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n'
index bc70bb12fc1cec28333a5c27bb3ad04efb8b0d27..07678493d7d6a230cf7cceeac81002289d8566c1 100644 (file)
@@ -44,6 +44,14 @@ export class ManagerOptionsBuilder {
           'isLive',
           'videoUUID'
         ])
+      },
+      metrics: {
+        mode: this.mode,
+
+        ...pick(commonOptions, [
+          'metricsUrl',
+          'videoUUID'
+        ])
       }
     }
 
diff --git a/client/src/assets/player/shared/metrics/index.ts b/client/src/assets/player/shared/metrics/index.ts
new file mode 100644 (file)
index 0000000..85d75cd
--- /dev/null
@@ -0,0 +1 @@
+export * from './metrics-plugin'
diff --git a/client/src/assets/player/shared/metrics/metrics-plugin.ts b/client/src/assets/player/shared/metrics/metrics-plugin.ts
new file mode 100644 (file)
index 0000000..1b2349e
--- /dev/null
@@ -0,0 +1,128 @@
+import videojs from 'video.js'
+import { PlaybackMetricCreate } from '../../../../../../shared/models'
+import { MetricsPluginOptions, PlayerMode, PlayerNetworkInfo } from '../../types'
+
+const Plugin = videojs.getPlugin('plugin')
+
+class MetricsPlugin extends Plugin {
+  private readonly metricsUrl: string
+  private readonly videoUUID: string
+  private readonly mode: PlayerMode
+
+  private downloadedBytesP2P = 0
+  private downloadedBytesHTTP = 0
+  private uploadedBytesP2P = 0
+
+  private resolutionChanges = 0
+  private errors = 0
+
+  private lastPlayerNetworkInfo: PlayerNetworkInfo
+
+  private metricsInterval: any
+
+  private readonly CONSTANTS = {
+    METRICS_INTERVAL: 15000
+  }
+
+  constructor (player: videojs.Player, options: MetricsPluginOptions) {
+    super(player)
+
+    this.metricsUrl = options.metricsUrl
+    this.videoUUID = options.videoUUID
+    this.mode = options.mode
+
+    this.player.one('play', () => {
+      this.runMetricsInterval()
+
+      this.trackBytes()
+      this.trackResolutionChange()
+      this.trackErrors()
+    })
+  }
+
+  dispose () {
+    if (this.metricsInterval) clearInterval(this.metricsInterval)
+  }
+
+  private runMetricsInterval () {
+    this.metricsInterval = setInterval(() => {
+      let resolution: number
+      let fps: number
+
+      if (this.mode === 'p2p-media-loader') {
+        const level = this.player.p2pMediaLoader().getCurrentLevel()
+        if (!level) return
+
+        resolution = Math.min(level.height || 0, level.width || 0)
+
+        const framerate = level?.attrs['FRAME-RATE']
+        fps = framerate
+          ? parseInt(framerate, 10)
+          : undefined
+      } else { // webtorrent
+        const videoFile = this.player.webtorrent().getCurrentVideoFile()
+        if (!videoFile) return
+
+        resolution = videoFile.resolution.id
+        fps = videoFile.fps
+      }
+
+      const body: PlaybackMetricCreate = {
+        resolution,
+        fps,
+
+        playerMode: this.mode,
+
+        resolutionChanges: this.resolutionChanges,
+
+        errors: this.errors,
+
+        downloadedBytesP2P: this.downloadedBytesP2P,
+        downloadedBytesHTTP: this.downloadedBytesHTTP,
+
+        uploadedBytesP2P: this.uploadedBytesP2P,
+
+        videoId: this.videoUUID
+      }
+
+      this.resolutionChanges = 0
+
+      this.downloadedBytesP2P = 0
+      this.downloadedBytesHTTP = 0
+
+      this.uploadedBytesP2P = 0
+
+      this.errors = 0
+
+      const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' })
+
+      return fetch(this.metricsUrl, { method: 'POST', body: JSON.stringify(body), headers })
+    }, this.CONSTANTS.METRICS_INTERVAL)
+  }
+
+  private trackBytes () {
+    this.player.on('p2pInfo', (_event, data: PlayerNetworkInfo) => {
+      this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0)
+      this.downloadedBytesP2P += data.p2p.downloaded - (this.lastPlayerNetworkInfo?.p2p.downloaded || 0)
+
+      this.uploadedBytesP2P += data.p2p.uploaded - (this.lastPlayerNetworkInfo?.p2p.uploaded || 0)
+
+      this.lastPlayerNetworkInfo = data
+    })
+  }
+
+  private trackResolutionChange () {
+    this.player.on('engineResolutionChange', () => {
+      this.resolutionChanges++
+    })
+  }
+
+  private trackErrors () {
+    this.player.on('error', () => {
+      this.errors++
+    })
+  }
+}
+
+videojs.registerPlugin('metrics', MetricsPlugin)
+export { MetricsPlugin }
index e5f099deadce7bb14ed57fa5e05012e055aa2ae8..54d87aea579bf4afbb1fe78d96f7c30657cc8291 100644 (file)
@@ -2,10 +2,10 @@ import Hlsjs from 'hls.js'
 import videojs from 'video.js'
 import { Events, Segment } from '@peertube/p2p-media-loader-core'
 import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs'
+import { logger } from '@root-helpers/logger'
 import { timeToInt } from '@shared/core-utils'
 import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types'
 import { registerConfigPlugin, registerSourceHandler } from './hls-plugin'
-import { logger } from '@root-helpers/logger'
 
 registerConfigPlugin(videojs)
 registerSourceHandler(videojs)
@@ -29,9 +29,7 @@ class P2pMediaLoaderPlugin extends Plugin {
   }
   private statsHTTPBytes = {
     pendingDownload: [] as number[],
-    pendingUpload: [] as number[],
-    totalDownload: 0,
-    totalUpload: 0
+    totalDownload: 0
   }
   private startTime: number
 
@@ -123,6 +121,8 @@ class P2pMediaLoaderPlugin extends Plugin {
     this.statsP2PBytes.numPeers = 1 + this.options.redundancyUrlManager.countBaseUrls()
 
     this.runStats()
+
+    this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHED, () => this.player.trigger('engineResolutionChange'))
   }
 
   private runStats () {
@@ -134,10 +134,13 @@ class P2pMediaLoaderPlugin extends Plugin {
     })
 
     this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, _segment, bytes: number) => {
-      const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes
+      if (method !== 'p2p') {
+        logger.error(`Received upload from unknown method ${method}`)
+        return
+      }
 
-      elem.pendingUpload.push(bytes)
-      elem.totalUpload += bytes
+      this.statsP2PBytes.pendingUpload.push(bytes)
+      this.statsP2PBytes.totalUpload += bytes
     })
 
     this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++)
@@ -148,20 +151,16 @@ class P2pMediaLoaderPlugin extends Plugin {
       const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload)
 
       const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload)
-      const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload)
 
       this.statsP2PBytes.pendingDownload = []
       this.statsP2PBytes.pendingUpload = []
       this.statsHTTPBytes.pendingDownload = []
-      this.statsHTTPBytes.pendingUpload = []
 
       return this.player.trigger('p2pInfo', {
         source: 'p2p-media-loader',
         http: {
           downloadSpeed: httpDownloadSpeed,
-          uploadSpeed: httpUploadSpeed,
-          downloaded: this.statsHTTPBytes.totalDownload,
-          uploaded: this.statsHTTPBytes.totalUpload
+          downloaded: this.statsHTTPBytes.totalDownload
         },
         p2p: {
           downloadSpeed: p2pDownloadSpeed,
index 69a7b2d650347cb960571b6ef74027d7893d1153..83c32415e833f4c8bd1b8bd6c5422badb395b6c5 100644 (file)
@@ -144,6 +144,8 @@ class PeerTubePlugin extends Plugin {
     this.listenFullScreenChange()
   }
 
+  // ---------------------------------------------------------------------------
+
   private runUserViewing () {
     let lastCurrentTime = this.startTime
     let lastViewEvent: VideoViewEvent
@@ -205,6 +207,8 @@ class PeerTubePlugin extends Plugin {
     return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
   }
 
+  // ---------------------------------------------------------------------------
+
   private listenFullScreenChange () {
     this.player.on('fullscreenchange', () => {
       if (this.player.isFullscreen()) this.player.focus()
index b65adcfca381f19938b3c50cfcc17d8d0489b12c..1199d3285cc0084ca48f84396a0ce1332d5ef70d 100644 (file)
@@ -95,9 +95,9 @@ class StatsCard extends Component {
       const httpStats = data.http
 
       this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ')
-      this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ')
+      this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed).join(' ')
       this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
-      this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ')
+      this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded).join(' ')
       this.playerNetworkInfo.numPeers = p2pStats.numPeers
       this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
 
index 9fd5f593e5df2344a069020451cd2f00ab2d1836..fa3f48a9ae50f0235543cc3a207fc0ee44e33b30 100644 (file)
@@ -204,6 +204,8 @@ class WebTorrentPlugin extends Plugin {
     }
 
     this.updateVideoFile(newVideoFile, options)
+
+    this.player.trigger('engineResolutionChange')
   }
 
   flushVideoFile (videoFile: VideoFile, destroyRenderer = true) {
@@ -506,9 +508,7 @@ class WebTorrentPlugin extends Plugin {
         source: 'webtorrent',
         http: {
           downloadSpeed: 0,
-          uploadSpeed: 0,
-          downloaded: 0,
-          uploaded: 0
+          downloaded: 0
         },
         p2p: {
           downloadSpeed: this.torrent.downloadSpeed,
index cadce739dbbc297cc94cf49845160e6f2a47b5fc..b4d9374c3946457bc512d747b1d878809c40d278 100644 (file)
@@ -59,6 +59,8 @@ export interface CommonOptions extends CustomizationOptions {
   videoViewUrl: string
   authorizationHeader?: string
 
+  metricsUrl: string
+
   embedUrl: string
   embedTitle: string
 
index 115afb6141087a7defbce84e1438cca8be57f3be..6df94992cb6ae63fb01d40426e7634631d5ef9e6 100644 (file)
@@ -109,6 +109,12 @@ type PeerTubePluginOptions = {
   videoUUID: string
 }
 
+type MetricsPluginOptions = {
+  mode: PlayerMode
+  metricsUrl: string
+  videoUUID: string
+}
+
 type PlaylistPluginOptions = {
   elements: VideoPlaylistElement[]
 
@@ -165,6 +171,7 @@ type VideoJSPluginOptions = {
   playlist?: PlaylistPluginOptions
 
   peertube: PeerTubePluginOptions
+  metrics: MetricsPluginOptions
 
   webtorrent?: WebtorrentPluginOptions
 
@@ -197,9 +204,7 @@ type PlayerNetworkInfo = {
 
   http: {
     downloadSpeed: number
-    uploadSpeed: number
     downloaded: number
-    uploaded: number
   }
 
   p2p: {
@@ -227,6 +232,7 @@ export {
   ResolutionUpdateData,
   AutoResolutionUpdateData,
   PlaylistPluginOptions,
+  MetricsPluginOptions,
   VideoJSCaption,
   PeerTubePluginOptions,
   WebtorrentPluginOptions,
index 9cebdcd106de84ded85bc6578d78f9dfe43abae4..eed8219946e8602de5037906088c8bc4ab6cf3ba 100644 (file)
@@ -203,6 +203,7 @@ export class PlayerManagerOptions {
         videoCaptions,
         inactivityTimeout: 2500,
         videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid),
+        metricsUrl: window.location.origin + '/api/v1/metrics/playback',
 
         videoShortUUID: video.shortUUID,
         videoUUID: video.uuid,
index 9b24d44c02164ba5f5b88a60cc5bd5bd0d031d9b..a87642bd834493be38b513f472aac8e30b12b7e8 100644 (file)
@@ -148,3 +148,8 @@ geo_ip:
 
 video_studio:
   enabled: true
+
+open_telemetry:
+  metrics:
+    prometheus_exporter:
+      port: 9092
index 64faf835592f8f33cb0aa1978c49d75de60fb659..be66e0744cabe238c1516368464dfe1ddfba2d1f 100644 (file)
     "@babel/parser": "^7.17.8",
     "@node-oauth/oauth2-server": "^4.2.0",
     "@opentelemetry/api": "^1.1.0",
-    "@opentelemetry/api-metrics": "^0.30.0",
+    "@opentelemetry/api-metrics": "^0.31.0",
     "@opentelemetry/exporter-jaeger": "^1.3.1",
-    "@opentelemetry/exporter-prometheus": "~0.30.0",
-    "@opentelemetry/instrumentation": "^0.30.0",
+    "@opentelemetry/exporter-prometheus": "~0.31.0",
+    "@opentelemetry/instrumentation": "^0.31.0",
     "@opentelemetry/instrumentation-dns": "^0.29.0",
     "@opentelemetry/instrumentation-express": "^0.30.0",
     "@opentelemetry/instrumentation-fs": "^0.4.0",
-    "@opentelemetry/instrumentation-http": "^0.30.0",
+    "@opentelemetry/instrumentation-http": "^0.31.0",
     "@opentelemetry/instrumentation-pg": "^0.30.0",
     "@opentelemetry/instrumentation-redis-4": "^0.31.0",
     "@opentelemetry/resources": "^1.3.1",
-    "@opentelemetry/sdk-metrics-base": "~0.30.0",
+    "@opentelemetry/sdk-metrics-base": "~0.31.0",
     "@opentelemetry/sdk-trace-base": "^1.3.1",
     "@opentelemetry/sdk-trace-node": "^1.3.1",
     "@opentelemetry/semantic-conventions": "^1.3.1",
index 8c8ebd061464147a927001cf7e50584fb07ebce6..e1d197c8a4ea43c96b1d1175e5c04bf9c3bc5b26 100644 (file)
@@ -11,6 +11,7 @@ import { bulkRouter } from './bulk'
 import { configRouter } from './config'
 import { customPageRouter } from './custom-page'
 import { jobsRouter } from './jobs'
+import { metricsRouter } from './metrics'
 import { oauthClientsRouter } from './oauth-clients'
 import { overviewsRouter } from './overviews'
 import { pluginRouter } from './plugins'
@@ -18,9 +19,9 @@ import { searchRouter } from './search'
 import { serverRouter } from './server'
 import { usersRouter } from './users'
 import { videoChannelRouter } from './video-channel'
+import { videoChannelSyncRouter } from './video-channel-sync'
 import { videoPlaylistRouter } from './video-playlist'
 import { videosRouter } from './videos'
-import { videoChannelSyncRouter } from './video-channel-sync'
 
 const apiRouter = express.Router()
 
@@ -48,6 +49,7 @@ apiRouter.use('/video-channel-syncs', videoChannelSyncRouter)
 apiRouter.use('/video-playlists', videoPlaylistRouter)
 apiRouter.use('/videos', videosRouter)
 apiRouter.use('/jobs', jobsRouter)
+apiRouter.use('/metrics', metricsRouter)
 apiRouter.use('/search', searchRouter)
 apiRouter.use('/overviews', overviewsRouter)
 apiRouter.use('/plugins', pluginRouter)
diff --git a/server/controllers/api/metrics.ts b/server/controllers/api/metrics.ts
new file mode 100644 (file)
index 0000000..578b023
--- /dev/null
@@ -0,0 +1,27 @@
+import express from 'express'
+import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics'
+import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models'
+import { addPlaybackMetricValidator, asyncMiddleware } from '../../middlewares'
+
+const metricsRouter = express.Router()
+
+metricsRouter.post('/playback',
+  asyncMiddleware(addPlaybackMetricValidator),
+  addPlaybackMetric
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+  metricsRouter
+}
+
+// ---------------------------------------------------------------------------
+
+function addPlaybackMetric (req: express.Request, res: express.Response) {
+  const body: PlaybackMetricCreate = req.body
+
+  OpenTelemetryMetrics.Instance.observePlaybackMetric(res.locals.onlyImmutableVideo, body)
+
+  return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
+}
diff --git a/server/helpers/custom-validators/metrics.ts b/server/helpers/custom-validators/metrics.ts
new file mode 100644 (file)
index 0000000..533f898
--- /dev/null
@@ -0,0 +1,9 @@
+function isValidPlayerMode (value: any) {
+  return value === 'webtorrent' || value === 'p2p-media-loader'
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  isValidPlayerMode
+}
index 1b3813743ea000133a5b425a33d5816ef40ef6f9..775d954ba1e02349eb1e674aa9dbac6ec6c7b1cd 100644 (file)
@@ -1,5 +1,6 @@
 export * from './lives-observers-builder'
 export * from './job-queue-observers-builder'
 export * from './nodejs-observers-builder'
+export * from './playback-metrics'
 export * from './stats-observers-builder'
 export * from './viewers-observers-builder'
index 766cbe03be0b03b5bda73eda9db0a0ea1d061cac..473015e912faa72bbb368c05c719e83a1d907efa 100644 (file)
@@ -2,7 +2,7 @@ import { readdir } from 'fs-extra'
 import { constants, PerformanceObserver } from 'perf_hooks'
 import * as process from 'process'
 import { Meter, ObservableResult } from '@opentelemetry/api-metrics'
-import { ExplicitBucketHistogramAggregation, MeterProvider } from '@opentelemetry/sdk-metrics-base'
+import { ExplicitBucketHistogramAggregation } from '@opentelemetry/sdk-metrics-base'
 import { View } from '@opentelemetry/sdk-metrics-base/build/src/view/View'
 import { logger } from '@server/helpers/logger'
 
@@ -12,7 +12,16 @@ import { logger } from '@server/helpers/logger'
 
 export class NodeJSObserversBuilder {
 
-  constructor (private readonly meter: Meter, private readonly meterProvider: MeterProvider) {
+  constructor (private readonly meter: Meter) {
+  }
+
+  static getViews () {
+    return [
+      new View({
+        aggregation: new ExplicitBucketHistogramAggregation([ 0.001, 0.01, 0.1, 1, 2, 5 ]),
+        instrumentName: 'nodejs_gc_duration_seconds'
+      })
+    ]
   }
 
   buildObservers () {
@@ -91,11 +100,6 @@ export class NodeJSObserversBuilder {
       [constants.NODE_PERFORMANCE_GC_WEAKCB]: 'weakcb'
     }
 
-    this.meterProvider.addView(
-      new View({ aggregation: new ExplicitBucketHistogramAggregation([ 0.001, 0.01, 0.1, 1, 2, 5 ]) }),
-      { instrument: { name: 'nodejs_gc_duration_seconds' } }
-    )
-
     const histogram = this.meter.createHistogram('nodejs_gc_duration_seconds', {
       description: 'Garbage collection duration by kind, one of major, minor, incremental or weakcb'
     })
diff --git a/server/lib/opentelemetry/metric-helpers/playback-metrics.ts b/server/lib/opentelemetry/metric-helpers/playback-metrics.ts
new file mode 100644 (file)
index 0000000..d2abdee
--- /dev/null
@@ -0,0 +1,59 @@
+import { Counter, Meter } from '@opentelemetry/api-metrics'
+import { MVideoImmutable } from '@server/types/models'
+import { PlaybackMetricCreate } from '@shared/models'
+
+export class PlaybackMetrics {
+  private errorsCounter: Counter
+  private resolutionChangesCounter: Counter
+
+  private downloadedBytesP2PCounter: Counter
+  private uploadedBytesP2PCounter: Counter
+
+  private downloadedBytesHTTPCounter: Counter
+
+  constructor (private readonly meter: Meter) {
+
+  }
+
+  buildCounters () {
+    this.errorsCounter = this.meter.createCounter('peertube_playback_errors_count', {
+      description: 'Errors collected from PeerTube player.'
+    })
+
+    this.resolutionChangesCounter = this.meter.createCounter('peertube_playback_resolution_changes_count', {
+      description: 'Resolution changes collected from PeerTube player.'
+    })
+
+    this.downloadedBytesHTTPCounter = this.meter.createCounter('peertube_playback_http_downloaded_bytes', {
+      description: 'Downloaded bytes with HTTP by PeerTube player.'
+    })
+    this.downloadedBytesP2PCounter = this.meter.createCounter('peertube_playback_p2p_downloaded_bytes', {
+      description: 'Downloaded bytes with P2P by PeerTube player.'
+    })
+
+    this.uploadedBytesP2PCounter = this.meter.createCounter('peertube_playback_p2p_uploaded_bytes', {
+      description: 'Uploaded bytes with P2P by PeerTube player.'
+    })
+  }
+
+  observe (video: MVideoImmutable, metrics: PlaybackMetricCreate) {
+    const attributes = {
+      videoOrigin: video.remote
+        ? 'remote'
+        : 'local',
+
+      playerMode: metrics.playerMode,
+
+      resolution: metrics.resolution + '',
+      fps: metrics.fps + ''
+    }
+
+    this.errorsCounter.add(metrics.errors, attributes)
+    this.resolutionChangesCounter.add(metrics.resolutionChanges, attributes)
+
+    this.downloadedBytesHTTPCounter.add(metrics.downloadedBytesHTTP, attributes)
+    this.downloadedBytesP2PCounter.add(metrics.downloadedBytesP2P, attributes)
+
+    this.uploadedBytesP2PCounter.add(metrics.uploadedBytesP2P, attributes)
+  }
+}
index ffe49367024010c3c272db4390ac4ea935d1a36b..ba33c95059a60b1210e65765a36712a71649563a 100644 (file)
@@ -4,10 +4,13 @@ import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
 import { MeterProvider } from '@opentelemetry/sdk-metrics-base'
 import { logger } from '@server/helpers/logger'
 import { CONFIG } from '@server/initializers/config'
+import { MVideoImmutable } from '@server/types/models'
+import { PlaybackMetricCreate } from '@shared/models'
 import {
   JobQueueObserversBuilder,
   LivesObserversBuilder,
   NodeJSObserversBuilder,
+  PlaybackMetrics,
   StatsObserversBuilder,
   ViewersObserversBuilder
 } from './metric-helpers'
@@ -20,6 +23,8 @@ class OpenTelemetryMetrics {
 
   private onRequestDuration: (req: Request, res: Response) => void
 
+  private playbackMetrics: PlaybackMetrics
+
   private constructor () {}
 
   init (app: Application) {
@@ -41,7 +46,11 @@ class OpenTelemetryMetrics {
 
     logger.info('Registering Open Telemetry metrics')
 
-    const provider = new MeterProvider()
+    const provider = new MeterProvider({
+      views: [
+        ...NodeJSObserversBuilder.getViews()
+      ]
+    })
 
     provider.addMetricReader(new PrometheusExporter({ port: CONFIG.OPEN_TELEMETRY.METRICS.PROMETHEUS_EXPORTER.PORT }))
 
@@ -51,7 +60,10 @@ class OpenTelemetryMetrics {
 
     this.buildRequestObserver()
 
-    const nodeJSObserversBuilder = new NodeJSObserversBuilder(this.meter, provider)
+    this.playbackMetrics = new PlaybackMetrics(this.meter)
+    this.playbackMetrics.buildCounters()
+
+    const nodeJSObserversBuilder = new NodeJSObserversBuilder(this.meter)
     nodeJSObserversBuilder.buildObservers()
 
     const jobQueueObserversBuilder = new JobQueueObserversBuilder(this.meter)
@@ -67,6 +79,10 @@ class OpenTelemetryMetrics {
     viewersObserversBuilder.buildObservers()
   }
 
+  observePlaybackMetric (video: MVideoImmutable, metrics: PlaybackMetricCreate) {
+    this.playbackMetrics.observe(video, metrics)
+  }
+
   private buildRequestObserver () {
     const requestDuration = this.meter.createHistogram('http_request_duration_ms', {
       unit: 'milliseconds',
index b0ad04819cbcdbcc1c83b22c88fd395117938571..ffadb3b49001ed22562cb0f7ebed2ec4549e9cfd 100644 (file)
@@ -10,6 +10,7 @@ export * from './express'
 export * from './feeds'
 export * from './follows'
 export * from './jobs'
+export * from './metrics'
 export * from './logs'
 export * from './oembed'
 export * from './pagination'
diff --git a/server/middlewares/validators/metrics.ts b/server/middlewares/validators/metrics.ts
new file mode 100644 (file)
index 0000000..b1dbec6
--- /dev/null
@@ -0,0 +1,56 @@
+import express from 'express'
+import { body } from 'express-validator'
+import { isValidPlayerMode } from '@server/helpers/custom-validators/metrics'
+import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc'
+import { CONFIG } from '@server/initializers/config'
+import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models'
+import { logger } from '../../helpers/logger'
+import { areValidationErrors, doesVideoExist } from './shared'
+
+const addPlaybackMetricValidator = [
+  body('resolution')
+    .isInt({ min: 0 }).withMessage('Invalid resolution'),
+  body('fps')
+    .optional()
+    .isInt({ min: 0 }).withMessage('Invalid fps'),
+  body('playerMode')
+    .custom(isValidPlayerMode).withMessage('Invalid playerMode'),
+
+  body('resolutionChanges')
+    .isInt({ min: 0 }).withMessage('Invalid resolutionChanges'),
+
+  body('errors')
+    .isInt({ min: 0 }).withMessage('Invalid errors'),
+
+  body('downloadedBytesP2P')
+    .isInt({ min: 0 }).withMessage('Invalid downloadedBytesP2P'),
+  body('downloadedBytesHTTP')
+    .isInt({ min: 0 }).withMessage('Invalid downloadedBytesHTTP'),
+
+  body('uploadedBytesP2P')
+    .isInt({ min: 0 }).withMessage('Invalid uploadedBytesP2P'),
+
+  body('videoId')
+    .customSanitizer(toCompleteUUID)
+    .optional()
+    .custom(isIdOrUUIDValid),
+
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking addPlaybackMetricValidator parameters.', { parameters: req.query })
+
+    if (!CONFIG.OPEN_TELEMETRY.METRICS.ENABLED) return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
+
+    const body: PlaybackMetricCreate = req.body
+
+    if (areValidationErrors(req, res)) return
+    if (!await doesVideoExist(body.videoId, res, 'only-immutable-attributes')) return
+
+    return next()
+  }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+  addPlaybackMetricValidator
+}
index 149305f49e20fd469828a113d712cd7eabf14e40..cd7a38459085b5bc2698398337939cdb58deb99a 100644 (file)
@@ -10,6 +10,7 @@ import './follows'
 import './jobs'
 import './live'
 import './logs'
+import './metrics'
 import './my-user'
 import './plugins'
 import './redundancy'
diff --git a/server/tests/api/check-params/metrics.ts b/server/tests/api/check-params/metrics.ts
new file mode 100644 (file)
index 0000000..2d45094
--- /dev/null
@@ -0,0 +1,183 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+import { omit } from 'lodash'
+import { HttpStatusCode, PlaybackMetricCreate, VideoResolution } from '@shared/models'
+import { cleanupTests, createSingleServer, makePostBodyRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
+
+describe('Test metrics API validators', function () {
+  let server: PeerTubeServer
+  let videoUUID: string
+
+  // ---------------------------------------------------------------
+
+  before(async function () {
+    this.timeout(120000)
+
+    server = await createSingleServer(1, {
+      open_telemetry: {
+        metrics: {
+          enabled: true
+        }
+      }
+    })
+
+    await setAccessTokensToServers([ server ])
+
+    const { uuid } = await server.videos.quickUpload({ name: 'video' })
+    videoUUID = uuid
+  })
+
+  describe('When adding playback metrics', function () {
+    const path = '/api/v1/metrics/playback'
+    let baseParams: PlaybackMetricCreate
+
+    before(function () {
+      baseParams = {
+        playerMode: 'p2p-media-loader',
+        resolution: VideoResolution.H_1080P,
+        fps: 30,
+        resolutionChanges: 1,
+        errors: 2,
+        downloadedBytesP2P: 0,
+        downloadedBytesHTTP: 0,
+        uploadedBytesP2P: 0,
+        videoId: videoUUID
+      }
+    })
+
+    it('Should fail with an invalid resolution', async function () {
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: { ...baseParams, resolution: 'toto' }
+      })
+    })
+
+    it('Should fail with an invalid fps', async function () {
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: { ...baseParams, fps: 'toto' }
+      })
+    })
+
+    it('Should fail with a missing/invalid player mode', async function () {
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: omit(baseParams, 'playerMode')
+      })
+
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: { ...baseParams, playerMode: 'toto' }
+      })
+    })
+
+    it('Should fail with an missing/invalid resolution changes', async function () {
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: omit(baseParams, 'resolutionChanges')
+      })
+
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: { ...baseParams, resolutionChanges: 'toto' }
+      })
+    })
+
+    it('Should fail with a missing errors', async function () {
+
+    })
+
+    it('Should fail with an missing/invalid errors', async function () {
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: omit(baseParams, 'errors')
+      })
+
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: { ...baseParams, errors: 'toto' }
+      })
+    })
+
+    it('Should fail with an missing/invalid downloadedBytesP2P', async function () {
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: omit(baseParams, 'downloadedBytesP2P')
+      })
+
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: { ...baseParams, downloadedBytesP2P: 'toto' }
+      })
+    })
+
+    it('Should fail with an missing/invalid downloadedBytesHTTP', async function () {
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: omit(baseParams, 'downloadedBytesHTTP')
+      })
+
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: { ...baseParams, downloadedBytesHTTP: 'toto' }
+      })
+    })
+
+    it('Should fail with an missing/invalid uploadedBytesP2P', async function () {
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: omit(baseParams, 'uploadedBytesP2P')
+      })
+
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: { ...baseParams, uploadedBytesP2P: 'toto' }
+      })
+    })
+
+    it('Should fail with a bad video id', async function () {
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: { ...baseParams, videoId: 'toto' }
+      })
+    })
+
+    it('Should fail with an unknown video', async function () {
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: { ...baseParams, videoId: 42 },
+        expectedStatus: HttpStatusCode.NOT_FOUND_404
+      })
+    })
+
+    it('Should succeed with the correct params', async function () {
+      await makePostBodyRequest({
+        url: server.url,
+        path,
+        fields: baseParams,
+        expectedStatus: HttpStatusCode.NO_CONTENT_204
+      })
+    })
+  })
+
+  after(async function () {
+    await cleanupTests([ server ])
+  })
+})
index 20909429fcdf35157f62d90861c658d800682678..3137a9eb6089d0d48e34df8f4345f49f5dc21af0 100644 (file)
@@ -2,14 +2,14 @@
 
 import { expect } from 'chai'
 import { expectLogContain, expectLogDoesNotContain, MockHTTP } from '@server/tests/shared'
-import { HttpStatusCode, VideoPrivacy } from '@shared/models'
+import { HttpStatusCode, VideoPrivacy, VideoResolution } from '@shared/models'
 import { cleanupTests, createSingleServer, makeRawRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
 
 describe('Open Telemetry', function () {
   let server: PeerTubeServer
 
   describe('Metrics', function () {
-    const metricsUrl = 'http://localhost:9091/metrics'
+    const metricsUrl = 'http://localhost:9092/metrics'
 
     it('Should not enable open telemetry metrics', async function () {
       server = await createSingleServer(1)
@@ -36,8 +36,33 @@ describe('Open Telemetry', function () {
       })
 
       const res = await makeRawRequest(metricsUrl, HttpStatusCode.OK_200)
-      expect(res.text).to.contain('peertube_job_queue_total')
+      expect(res.text).to.contain('peertube_job_queue_total{')
+    })
+
+    it('Should have playback metrics', async function () {
+      await setAccessTokensToServers([ server ])
+
+      const video = await server.videos.quickUpload({ name: 'video' })
+
+      await server.metrics.addPlaybackMetric({
+        metrics: {
+          playerMode: 'p2p-media-loader',
+          resolution: VideoResolution.H_1080P,
+          fps: 30,
+          resolutionChanges: 1,
+          errors: 2,
+          downloadedBytesP2P: 0,
+          downloadedBytesHTTP: 0,
+          uploadedBytesP2P: 5,
+          videoId: video.uuid
+        }
+      })
 
+      const res = await makeRawRequest(metricsUrl, HttpStatusCode.OK_200)
+      expect(res.text).to.contain('peertube_playback_http_uploaded_bytes_total{')
+    })
+
+    after(async function () {
       await server.kill()
     })
   })
index 78723d83046223df4a9d21e2a3913a08e71827f2..439e9c8e167d517e30581b6f2684ae0a79263f82 100644 (file)
@@ -6,6 +6,7 @@ export * from './custom-markup'
 export * from './feeds'
 export * from './http'
 export * from './joinpeertube'
+export * from './metrics'
 export * from './moderation'
 export * from './overviews'
 export * from './plugins'
diff --git a/shared/models/metrics/index.ts b/shared/models/metrics/index.ts
new file mode 100644 (file)
index 0000000..24194cc
--- /dev/null
@@ -0,0 +1 @@
+export * from './playback-metric-create.model'
diff --git a/shared/models/metrics/playback-metric-create.model.ts b/shared/models/metrics/playback-metric-create.model.ts
new file mode 100644 (file)
index 0000000..d669ab6
--- /dev/null
@@ -0,0 +1,19 @@
+import { VideoResolution } from '../videos'
+
+export interface PlaybackMetricCreate {
+  playerMode: 'p2p-media-loader' | 'webtorrent'
+
+  resolution?: VideoResolution
+  fps?: number
+
+  resolutionChanges: number
+
+  errors: number
+
+  downloadedBytesP2P: number
+  downloadedBytesHTTP: number
+
+  uploadedBytesP2P: number
+
+  videoId: number | string
+}
index 0a4b21fc46965db6392f470b2b35d76fe4783aa6..9a2fbf8d33bfc08a7cba3709dcdc186183e438f0 100644 (file)
@@ -5,6 +5,7 @@ export * from './follows-command'
 export * from './follows'
 export * from './jobs'
 export * from './jobs-command'
+export * from './metrics-command'
 export * from './object-storage-command'
 export * from './plugins-command'
 export * from './redundancy-command'
diff --git a/shared/server-commands/server/metrics-command.ts b/shared/server-commands/server/metrics-command.ts
new file mode 100644 (file)
index 0000000..d22b483
--- /dev/null
@@ -0,0 +1,18 @@
+import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models'
+import { AbstractCommand, OverrideCommandOptions } from '../shared'
+
+export class MetricsCommand extends AbstractCommand {
+
+  addPlaybackMetric (options: OverrideCommandOptions & { metrics: PlaybackMetricCreate }) {
+    const path = '/api/v1/metrics/playback'
+
+    return this.postBodyRequest({
+      ...options,
+
+      path,
+      fields: options.metrics,
+      implicitToken: false,
+      defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
+    })
+  }
+}
index c05d16ad2cb87285248adff27a1f30aa4de135a3..2b4c9c9f83c4091b3ea631117017411ba3db37c9 100644 (file)
@@ -37,6 +37,7 @@ import { ContactFormCommand } from './contact-form-command'
 import { DebugCommand } from './debug-command'
 import { FollowsCommand } from './follows-command'
 import { JobsCommand } from './jobs-command'
+import { MetricsCommand } from './metrics-command'
 import { ObjectStorageCommand } from './object-storage-command'
 import { PluginsCommand } from './plugins-command'
 import { RedundancyCommand } from './redundancy-command'
@@ -104,6 +105,7 @@ export class PeerTubeServer {
   debug?: DebugCommand
   follows?: FollowsCommand
   jobs?: JobsCommand
+  metrics?: MetricsCommand
   plugins?: PluginsCommand
   redundancy?: RedundancyCommand
   stats?: StatsCommand
@@ -377,6 +379,7 @@ export class PeerTubeServer {
     this.debug = new DebugCommand(this)
     this.follows = new FollowsCommand(this)
     this.jobs = new JobsCommand(this)
+    this.metrics = new MetricsCommand(this)
     this.plugins = new PluginsCommand(this)
     this.redundancy = new RedundancyCommand(this)
     this.stats = new StatsCommand(this)
index 4402de954eb7d660eba4dbc6df7f7dc733473ecc..5077f8d90d0bd83796e0a2d8d7486a2e19fe6363 100644 (file)
@@ -5009,6 +5009,21 @@ paths:
         '404':
           description: plugin not found
 
+  /metrics/playback:
+    post:
+      summary: Create playback metrics
+      description: These metrics are exposed by OpenTelemetry metrics exporter if enabled.
+      tags:
+        - Stats
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/PlaybackMetricCreate'
+      responses:
+        '204':
+          description: successful operation
+
 servers:
   - url: 'https://peertube2.cpy.re/api/v1'
     description: Live Test Server (live data - latest nightly version)
@@ -8195,44 +8210,86 @@ components:
                     format: binary
 
     LiveVideoSessionResponse:
-        properties:
-          id:
-            type: integer
-          startDate:
-            type: string
-            format: date-time
-            description: Start date of the live session
-          endDate:
-            type: string
-            format: date-time
-            nullable: true
-            description: End date of the live session
-          error:
-            type: integer
-            enum:
-              - 1
-              - 2
-              - 3
-              - 4
-              - 5
-            nullable: true
-            description: >
-              Error type if an error occurred during the live session:
-                - `1`: Bad socket health (transcoding is too slow)
-                - `2`: Max duration exceeded
-                - `3`: Quota exceeded
-                - `4`: Quota FFmpeg error
-                - `5`: Video has been blacklisted during the live
-          replayVideo:
-            type: object
-            description: Video replay information
-            properties:
-              id:
-                type: number
-              uuid:
-                $ref: '#/components/schemas/UUIDv4'
-              shortUUID:
-                $ref: '#/components/schemas/shortUUID'
+      properties:
+        id:
+          type: integer
+        startDate:
+          type: string
+          format: date-time
+          description: Start date of the live session
+        endDate:
+          type: string
+          format: date-time
+          nullable: true
+          description: End date of the live session
+        error:
+          type: integer
+          enum:
+            - 1
+            - 2
+            - 3
+            - 4
+            - 5
+          nullable: true
+          description: >
+            Error type if an error occurred during the live session:
+              - `1`: Bad socket health (transcoding is too slow)
+              - `2`: Max duration exceeded
+              - `3`: Quota exceeded
+              - `4`: Quota FFmpeg error
+              - `5`: Video has been blacklisted during the live
+        replayVideo:
+          type: object
+          description: Video replay information
+          properties:
+            id:
+              type: number
+            uuid:
+              $ref: '#/components/schemas/UUIDv4'
+            shortUUID:
+              $ref: '#/components/schemas/shortUUID'
+
+    PlaybackMetricCreate:
+      properties:
+        playerMode:
+          type: string
+          enum:
+            - 'p2p-media-loader'
+            - 'webtorrent'
+        resolution:
+          type: number
+          description: Current player video resolution
+        fps:
+          type: number
+          description: Current player video fps
+        resolutionChanges:
+          type: number
+          description: How many resolution changes occured since the last metric creation
+        errors:
+          type: number
+          description: How many errors occured since the last metric creation
+        downloadedBytesP2P:
+          type: number
+          description: How many bytes were downloaded with P2P since the last metric creation
+        downloadedBytesHTTP:
+          type: number
+          description: How many bytes were downloaded with HTTP since the last metric creation
+        uploadedBytesP2P:
+          type: number
+          description: How many bytes were uploaded with P2P since the last metric creation
+        videoId:
+          oneOf:
+            - $ref: '#/components/schemas/id'
+            - $ref: '#/components/schemas/UUIDv4'
+            - $ref: '#/components/schemas/shortUUID'
+      required:
+        - playerMode
+        - resolutionChanges
+        - errors
+        - downloadedBytesP2P
+        - downloadedBytesHTTP
+        - uploadedBytesP2P
+        - videoId
 
   callbacks:
     searchIndex:
index d16fd026c2b51c28d5eb03c6b63b1c035ef38125..0a479ac57cd881baef777a5bcc35daabfaa39ac7 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   dependencies:
     "@opentelemetry/api" "^1.0.0"
 
-"@opentelemetry/api-metrics@0.30.0", "@opentelemetry/api-metrics@^0.30.0":
-  version "0.30.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/api-metrics/-/api-metrics-0.30.0.tgz#b5defd10756e81d1c7ce8669ff8a8d2465ba0be8"
-  integrity sha512-jSb7iiYPY+DSUKIyzfGt0a5K1QGzWY5fSWtUB8Alfi27NhQGHBeuYYC5n9MaBP/HNWw5GpEIhXGEYCF9Pf8IEg==
+"@opentelemetry/api-metrics@0.31.0", "@opentelemetry/api-metrics@^0.31.0":
+  version "0.31.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/api-metrics/-/api-metrics-0.31.0.tgz#0ed4cf4d7c731f968721c2b303eaf5e9fd42f736"
+  integrity sha512-PcL1x0kZtMie7NsNy67OyMvzLEXqf3xd0TZJKHHPMGTe89oMpNVrD1zJB1kZcwXOxLlHHb6tz21G3vvXPdXyZg==
   dependencies:
     "@opentelemetry/api" "^1.0.0"
 
   resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.5.0.tgz#4955313e7f0ec0fe17c813328a2a7f39f262c0fa"
   integrity sha512-mhBPP0BU0RaH2HB8U4MDd5OjWA1y7SoLOovCT0iEpJAltaq2z04uxRJVzIs91vkpNnV0utUZowQQD3KElgU+VA==
 
-"@opentelemetry/core@1.4.0":
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.4.0.tgz#26839ab9e36583a174273a1e1c5b33336c163725"
-  integrity sha512-faq50VFEdyC7ICAOlhSi+yYZ+peznnGjTJToha9R63i9fVopzpKrkZt7AIdXUmz2+L2OqXrcJs7EIdN/oDyr5w==
-  dependencies:
-    "@opentelemetry/semantic-conventions" "1.4.0"
-
 "@opentelemetry/core@1.5.0", "@opentelemetry/core@^1.0.0":
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.5.0.tgz#717bceee15d4c69d4c7321c1fe0f5a562b60eb81"
     "@opentelemetry/semantic-conventions" "1.5.0"
     jaeger-client "^3.15.0"
 
-"@opentelemetry/exporter-prometheus@~0.30.0":
-  version "0.30.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.30.0.tgz#f81322d3cb000170e716bc76820600d5649be538"
-  integrity sha512-y0SXvpzoKR+Tk/UL6F1f7vAcCzqpCDP/cTEa+Z7sX57aEG0HDXLQiLmAgK/BHqcEN5MFQMZ+MDVDsUrvpa6/Jw==
+"@opentelemetry/exporter-prometheus@~0.31.0":
+  version "0.31.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.31.0.tgz#b0696be42542a961ec1145f3754a845efbda942e"
+  integrity sha512-EfWFzoCu/THw0kZiaA2RUrk6XIQbfaJHJ26LRrVIK7INwosW8Q+x4pGfiJ5nxhglYiG9OTqGrQ6nQ4T9q1UMpg==
   dependencies:
-    "@opentelemetry/api-metrics" "0.30.0"
-    "@opentelemetry/core" "1.4.0"
-    "@opentelemetry/sdk-metrics-base" "0.30.0"
+    "@opentelemetry/api-metrics" "0.31.0"
+    "@opentelemetry/core" "1.5.0"
+    "@opentelemetry/sdk-metrics-base" "0.31.0"
 
 "@opentelemetry/instrumentation-dns@^0.29.0":
   version "0.29.0"
     "@opentelemetry/instrumentation" "^0.29.2"
     "@opentelemetry/semantic-conventions" "^1.0.0"
 
-"@opentelemetry/instrumentation-http@^0.30.0":
-  version "0.30.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.30.0.tgz#312ef25defbff750dd9082356bb9a9137ed5fd82"
-  integrity sha512-OhiuzR2mhlTcaXD1dYW/dqnC/zjIKHp2NWMUyDHEd4xS6NZAiTU5mNDv57Y9on+/VwYXWUZZ2tB7AOVPsFUIOg==
+"@opentelemetry/instrumentation-http@^0.31.0":
+  version "0.31.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.31.0.tgz#5c6dea9cdb636543c6ed1f1a4e55d4422e50fa89"
+  integrity sha512-DLw+H7UQZ+V3FX72iGXVMX4ylL4jV+GHraaUiVY0CIdxg1nrGmjLm4dPU5500IXlbgZUUoJ9jq02JDblujdKcQ==
   dependencies:
-    "@opentelemetry/core" "1.4.0"
-    "@opentelemetry/instrumentation" "0.30.0"
-    "@opentelemetry/semantic-conventions" "1.4.0"
+    "@opentelemetry/core" "1.5.0"
+    "@opentelemetry/instrumentation" "0.31.0"
+    "@opentelemetry/semantic-conventions" "1.5.0"
     semver "^7.3.5"
 
 "@opentelemetry/instrumentation-pg@^0.30.0":
     "@opentelemetry/instrumentation" "^0.29.2"
     "@opentelemetry/semantic-conventions" "^1.0.0"
 
-"@opentelemetry/instrumentation@0.30.0", "@opentelemetry/instrumentation@^0.30.0":
-  version "0.30.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.30.0.tgz#97cca611bd276439cc4e01e0516e50cbbb1e3459"
-  integrity sha512-9bjRx81B6wbJ7CGWc/WCUfcb0QIG5UIcjnPTzwYIURjYPd8d0ZzRlrnqEdQG62jn4lSPEvnNqTlyC7qXtn9nAA==
+"@opentelemetry/instrumentation@0.31.0", "@opentelemetry/instrumentation@^0.31.0":
+  version "0.31.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.31.0.tgz#bee0052a86e22f57be3901c44234f1a210bcfda8"
+  integrity sha512-b2hFebXPtBcut4d81b8Kg6GiCoAS8nxb8kYSronQYAXxwNSetqHwIJ2nKLo1slFH1UWUXn0zi3eDez2Sn/9uMQ==
   dependencies:
-    "@opentelemetry/api-metrics" "0.30.0"
+    "@opentelemetry/api-metrics" "0.31.0"
     require-in-the-middle "^5.0.3"
     semver "^7.3.2"
     shimmer "^1.2.1"
   dependencies:
     "@opentelemetry/core" "1.5.0"
 
-"@opentelemetry/resources@1.4.0":
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.4.0.tgz#5e23b0d7976158861059dec17e0ee36a35a5ab85"
-  integrity sha512-Q3pI5+pCM+Ur7YwK9GbG89UBipwJbfmuzSPAXTw964ZHFzSrz+JAgrETC9rqsUOYdUlj/V7LbRMG5bo72xE0Xw==
-  dependencies:
-    "@opentelemetry/core" "1.4.0"
-    "@opentelemetry/semantic-conventions" "1.4.0"
-
 "@opentelemetry/resources@1.5.0", "@opentelemetry/resources@^1.3.1":
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.5.0.tgz#ce7fbdaec3494e41bc279ddbed3c478ee2570b03"
     "@opentelemetry/core" "1.5.0"
     "@opentelemetry/semantic-conventions" "1.5.0"
 
-"@opentelemetry/sdk-metrics-base@0.30.0", "@opentelemetry/sdk-metrics-base@~0.30.0":
-  version "0.30.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics-base/-/sdk-metrics-base-0.30.0.tgz#242d9260a89a1ac2bf1e167b3fda758f3883c769"
-  integrity sha512-3BDg1MYDInDyGvy+bSH8OuCX5nsue7omH6Y2eidCGTTDYRPxDmq9tsRJxnTUepoMAvWX+1sTwZ4JqTFmc1z8Mw==
+"@opentelemetry/sdk-metrics-base@0.31.0", "@opentelemetry/sdk-metrics-base@~0.31.0":
+  version "0.31.0"
+  resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics-base/-/sdk-metrics-base-0.31.0.tgz#f797da702c8d9862a2fff55a1e7c70aa6845e535"
+  integrity sha512-4R2Bjl3wlqIGcq4bCoI9/pD49ld+tEoM9n85UfFzr/aUe+2huY2jTPq/BP9SVB8d2Zfg7mGTIFeapcEvAdKK7g==
   dependencies:
-    "@opentelemetry/api-metrics" "0.30.0"
-    "@opentelemetry/core" "1.4.0"
-    "@opentelemetry/resources" "1.4.0"
+    "@opentelemetry/api-metrics" "0.31.0"
+    "@opentelemetry/core" "1.5.0"
+    "@opentelemetry/resources" "1.5.0"
     lodash.merge "4.6.2"
 
 "@opentelemetry/sdk-trace-base@1.5.0", "@opentelemetry/sdk-trace-base@^1.3.1":
     "@opentelemetry/sdk-trace-base" "1.5.0"
     semver "^7.3.5"
 
-"@opentelemetry/semantic-conventions@1.4.0":
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.4.0.tgz#facf2c67d6063b9918d5a5e3fdf25f3a30d547b6"
-  integrity sha512-Hzl8soGpmyzja9w3kiFFcYJ7n5HNETpplY6cb67KR4QPlxp4FTTresO06qXHgHDhyIInmbLJXuwARjjpsKYGuQ==
-
 "@opentelemetry/semantic-conventions@1.5.0", "@opentelemetry/semantic-conventions@^1.0.0", "@opentelemetry/semantic-conventions@^1.3.1":
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.5.0.tgz#cea9792bfcf556c87ded17c6ac729348697bb632"