aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts2
-rw-r--r--client/src/assets/player/peertube-player-manager.ts1
-rw-r--r--client/src/assets/player/shared/control-bar/p2p-info-button.ts4
-rw-r--r--client/src/assets/player/shared/manager-options/manager-options-builder.ts8
-rw-r--r--client/src/assets/player/shared/metrics/index.ts1
-rw-r--r--client/src/assets/player/shared/metrics/metrics-plugin.ts128
-rw-r--r--client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts23
-rw-r--r--client/src/assets/player/shared/peertube/peertube-plugin.ts4
-rw-r--r--client/src/assets/player/shared/stats/stats-card.ts4
-rw-r--r--client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts6
-rw-r--r--client/src/assets/player/types/manager-options.ts2
-rw-r--r--client/src/assets/player/types/peertube-videojs-typings.ts10
-rw-r--r--client/src/standalone/videos/shared/player-manager-options.ts1
-rw-r--r--config/test.yaml5
-rw-r--r--package.json10
-rw-r--r--server/controllers/api/index.ts4
-rw-r--r--server/controllers/api/metrics.ts27
-rw-r--r--server/helpers/custom-validators/metrics.ts9
-rw-r--r--server/lib/opentelemetry/metric-helpers/index.ts1
-rw-r--r--server/lib/opentelemetry/metric-helpers/nodejs-observers-builder.ts18
-rw-r--r--server/lib/opentelemetry/metric-helpers/playback-metrics.ts59
-rw-r--r--server/lib/opentelemetry/metrics.ts20
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/metrics.ts56
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/check-params/metrics.ts183
-rw-r--r--server/tests/api/server/open-telemetry.ts31
-rw-r--r--shared/models/index.ts1
-rw-r--r--shared/models/metrics/index.ts1
-rw-r--r--shared/models/metrics/playback-metric-create.model.ts19
-rw-r--r--shared/server-commands/server/index.ts1
-rw-r--r--shared/server-commands/server/metrics-command.ts18
-rw-r--r--shared/server-commands/server/server.ts3
-rw-r--r--support/doc/api/openapi.yaml133
-rw-r--r--yarn.lock80
35 files changed, 748 insertions, 127 deletions
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 8d9c08ab3..9ae6f9f12 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -628,6 +628,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
628 : null, 628 : null,
629 authorizationHeader: this.authService.getRequestHeaderValue(), 629 authorizationHeader: this.authService.getRequestHeaderValue(),
630 630
631 metricsUrl: environment.apiUrl + '/api/v1/metrics/playback',
632
631 embedUrl: video.embedUrl, 633 embedUrl: video.embedUrl,
632 embedTitle: video.name, 634 embedTitle: video.name,
633 instanceName: this.serverConfig.instance.name, 635 instanceName: this.serverConfig.instance.name,
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index b9077dcae..0d4acc3d9 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -22,6 +22,7 @@ import './shared/playlist/playlist-plugin'
22import './shared/mobile/peertube-mobile-plugin' 22import './shared/mobile/peertube-mobile-plugin'
23import './shared/mobile/peertube-mobile-buttons' 23import './shared/mobile/peertube-mobile-buttons'
24import './shared/hotkeys/peertube-hotkeys-plugin' 24import './shared/hotkeys/peertube-hotkeys-plugin'
25import './shared/metrics/metrics-plugin'
25import videojs from 'video.js' 26import videojs from 'video.js'
26import { logger } from '@root-helpers/logger' 27import { logger } from '@root-helpers/logger'
27import { PluginsManager } from '@root-helpers/plugins-manager' 28import { PluginsManager } from '@root-helpers/plugins-manager'
diff --git a/client/src/assets/player/shared/control-bar/p2p-info-button.ts b/client/src/assets/player/shared/control-bar/p2p-info-button.ts
index 36517e125..1979654ad 100644
--- a/client/src/assets/player/shared/control-bar/p2p-info-button.ts
+++ b/client/src/assets/player/shared/control-bar/p2p-info-button.ts
@@ -87,9 +87,9 @@ class P2pInfoButton extends Button {
87 const httpStats = data.http 87 const httpStats = data.http
88 88
89 const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed) 89 const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed)
90 const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed) 90 const uploadSpeed = bytes(p2pStats.uploadSpeed)
91 const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded) 91 const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded)
92 const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) 92 const totalUploaded = bytes(p2pStats.uploaded)
93 const numPeers = p2pStats.numPeers 93 const numPeers = p2pStats.numPeers
94 94
95 subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' 95 subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n'
diff --git a/client/src/assets/player/shared/manager-options/manager-options-builder.ts b/client/src/assets/player/shared/manager-options/manager-options-builder.ts
index bc70bb12f..07678493d 100644
--- a/client/src/assets/player/shared/manager-options/manager-options-builder.ts
+++ b/client/src/assets/player/shared/manager-options/manager-options-builder.ts
@@ -44,6 +44,14 @@ export class ManagerOptionsBuilder {
44 'isLive', 44 'isLive',
45 'videoUUID' 45 'videoUUID'
46 ]) 46 ])
47 },
48 metrics: {
49 mode: this.mode,
50
51 ...pick(commonOptions, [
52 'metricsUrl',
53 'videoUUID'
54 ])
47 } 55 }
48 } 56 }
49 57
diff --git a/client/src/assets/player/shared/metrics/index.ts b/client/src/assets/player/shared/metrics/index.ts
new file mode 100644
index 000000000..85d75cdc7
--- /dev/null
+++ b/client/src/assets/player/shared/metrics/index.ts
@@ -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
index 000000000..1b2349eba
--- /dev/null
+++ b/client/src/assets/player/shared/metrics/metrics-plugin.ts
@@ -0,0 +1,128 @@
1import videojs from 'video.js'
2import { PlaybackMetricCreate } from '../../../../../../shared/models'
3import { MetricsPluginOptions, PlayerMode, PlayerNetworkInfo } from '../../types'
4
5const Plugin = videojs.getPlugin('plugin')
6
7class MetricsPlugin extends Plugin {
8 private readonly metricsUrl: string
9 private readonly videoUUID: string
10 private readonly mode: PlayerMode
11
12 private downloadedBytesP2P = 0
13 private downloadedBytesHTTP = 0
14 private uploadedBytesP2P = 0
15
16 private resolutionChanges = 0
17 private errors = 0
18
19 private lastPlayerNetworkInfo: PlayerNetworkInfo
20
21 private metricsInterval: any
22
23 private readonly CONSTANTS = {
24 METRICS_INTERVAL: 15000
25 }
26
27 constructor (player: videojs.Player, options: MetricsPluginOptions) {
28 super(player)
29
30 this.metricsUrl = options.metricsUrl
31 this.videoUUID = options.videoUUID
32 this.mode = options.mode
33
34 this.player.one('play', () => {
35 this.runMetricsInterval()
36
37 this.trackBytes()
38 this.trackResolutionChange()
39 this.trackErrors()
40 })
41 }
42
43 dispose () {
44 if (this.metricsInterval) clearInterval(this.metricsInterval)
45 }
46
47 private runMetricsInterval () {
48 this.metricsInterval = setInterval(() => {
49 let resolution: number
50 let fps: number
51
52 if (this.mode === 'p2p-media-loader') {
53 const level = this.player.p2pMediaLoader().getCurrentLevel()
54 if (!level) return
55
56 resolution = Math.min(level.height || 0, level.width || 0)
57
58 const framerate = level?.attrs['FRAME-RATE']
59 fps = framerate
60 ? parseInt(framerate, 10)
61 : undefined
62 } else { // webtorrent
63 const videoFile = this.player.webtorrent().getCurrentVideoFile()
64 if (!videoFile) return
65
66 resolution = videoFile.resolution.id
67 fps = videoFile.fps
68 }
69
70 const body: PlaybackMetricCreate = {
71 resolution,
72 fps,
73
74 playerMode: this.mode,
75
76 resolutionChanges: this.resolutionChanges,
77
78 errors: this.errors,
79
80 downloadedBytesP2P: this.downloadedBytesP2P,
81 downloadedBytesHTTP: this.downloadedBytesHTTP,
82
83 uploadedBytesP2P: this.uploadedBytesP2P,
84
85 videoId: this.videoUUID
86 }
87
88 this.resolutionChanges = 0
89
90 this.downloadedBytesP2P = 0
91 this.downloadedBytesHTTP = 0
92
93 this.uploadedBytesP2P = 0
94
95 this.errors = 0
96
97 const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' })
98
99 return fetch(this.metricsUrl, { method: 'POST', body: JSON.stringify(body), headers })
100 }, this.CONSTANTS.METRICS_INTERVAL)
101 }
102
103 private trackBytes () {
104 this.player.on('p2pInfo', (_event, data: PlayerNetworkInfo) => {
105 this.downloadedBytesHTTP += data.http.downloaded - (this.lastPlayerNetworkInfo?.http.downloaded || 0)
106 this.downloadedBytesP2P += data.p2p.downloaded - (this.lastPlayerNetworkInfo?.p2p.downloaded || 0)
107
108 this.uploadedBytesP2P += data.p2p.uploaded - (this.lastPlayerNetworkInfo?.p2p.uploaded || 0)
109
110 this.lastPlayerNetworkInfo = data
111 })
112 }
113
114 private trackResolutionChange () {
115 this.player.on('engineResolutionChange', () => {
116 this.resolutionChanges++
117 })
118 }
119
120 private trackErrors () {
121 this.player.on('error', () => {
122 this.errors++
123 })
124 }
125}
126
127videojs.registerPlugin('metrics', MetricsPlugin)
128export { MetricsPlugin }
diff --git a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts
index e5f099dea..54d87aea5 100644
--- a/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts
+++ b/client/src/assets/player/shared/p2p-media-loader/p2p-media-loader-plugin.ts
@@ -2,10 +2,10 @@ import Hlsjs from 'hls.js'
2import videojs from 'video.js' 2import videojs from 'video.js'
3import { Events, Segment } from '@peertube/p2p-media-loader-core' 3import { Events, Segment } from '@peertube/p2p-media-loader-core'
4import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs' 4import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from '@peertube/p2p-media-loader-hlsjs'
5import { logger } from '@root-helpers/logger'
5import { timeToInt } from '@shared/core-utils' 6import { timeToInt } from '@shared/core-utils'
6import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types' 7import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../../types'
7import { registerConfigPlugin, registerSourceHandler } from './hls-plugin' 8import { registerConfigPlugin, registerSourceHandler } from './hls-plugin'
8import { logger } from '@root-helpers/logger'
9 9
10registerConfigPlugin(videojs) 10registerConfigPlugin(videojs)
11registerSourceHandler(videojs) 11registerSourceHandler(videojs)
@@ -29,9 +29,7 @@ class P2pMediaLoaderPlugin extends Plugin {
29 } 29 }
30 private statsHTTPBytes = { 30 private statsHTTPBytes = {
31 pendingDownload: [] as number[], 31 pendingDownload: [] as number[],
32 pendingUpload: [] as number[], 32 totalDownload: 0
33 totalDownload: 0,
34 totalUpload: 0
35 } 33 }
36 private startTime: number 34 private startTime: number
37 35
@@ -123,6 +121,8 @@ class P2pMediaLoaderPlugin extends Plugin {
123 this.statsP2PBytes.numPeers = 1 + this.options.redundancyUrlManager.countBaseUrls() 121 this.statsP2PBytes.numPeers = 1 + this.options.redundancyUrlManager.countBaseUrls()
124 122
125 this.runStats() 123 this.runStats()
124
125 this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHED, () => this.player.trigger('engineResolutionChange'))
126 } 126 }
127 127
128 private runStats () { 128 private runStats () {
@@ -134,10 +134,13 @@ class P2pMediaLoaderPlugin extends Plugin {
134 }) 134 })
135 135
136 this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, _segment, bytes: number) => { 136 this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, _segment, bytes: number) => {
137 const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes 137 if (method !== 'p2p') {
138 logger.error(`Received upload from unknown method ${method}`)
139 return
140 }
138 141
139 elem.pendingUpload.push(bytes) 142 this.statsP2PBytes.pendingUpload.push(bytes)
140 elem.totalUpload += bytes 143 this.statsP2PBytes.totalUpload += bytes
141 }) 144 })
142 145
143 this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) 146 this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++)
@@ -148,20 +151,16 @@ class P2pMediaLoaderPlugin extends Plugin {
148 const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) 151 const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload)
149 152
150 const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload) 153 const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload)
151 const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload)
152 154
153 this.statsP2PBytes.pendingDownload = [] 155 this.statsP2PBytes.pendingDownload = []
154 this.statsP2PBytes.pendingUpload = [] 156 this.statsP2PBytes.pendingUpload = []
155 this.statsHTTPBytes.pendingDownload = [] 157 this.statsHTTPBytes.pendingDownload = []
156 this.statsHTTPBytes.pendingUpload = []
157 158
158 return this.player.trigger('p2pInfo', { 159 return this.player.trigger('p2pInfo', {
159 source: 'p2p-media-loader', 160 source: 'p2p-media-loader',
160 http: { 161 http: {
161 downloadSpeed: httpDownloadSpeed, 162 downloadSpeed: httpDownloadSpeed,
162 uploadSpeed: httpUploadSpeed, 163 downloaded: this.statsHTTPBytes.totalDownload
163 downloaded: this.statsHTTPBytes.totalDownload,
164 uploaded: this.statsHTTPBytes.totalUpload
165 }, 164 },
166 p2p: { 165 p2p: {
167 downloadSpeed: p2pDownloadSpeed, 166 downloadSpeed: p2pDownloadSpeed,
diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts
index 69a7b2d65..83c32415e 100644
--- a/client/src/assets/player/shared/peertube/peertube-plugin.ts
+++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts
@@ -144,6 +144,8 @@ class PeerTubePlugin extends Plugin {
144 this.listenFullScreenChange() 144 this.listenFullScreenChange()
145 } 145 }
146 146
147 // ---------------------------------------------------------------------------
148
147 private runUserViewing () { 149 private runUserViewing () {
148 let lastCurrentTime = this.startTime 150 let lastCurrentTime = this.startTime
149 let lastViewEvent: VideoViewEvent 151 let lastViewEvent: VideoViewEvent
@@ -205,6 +207,8 @@ class PeerTubePlugin extends Plugin {
205 return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) 207 return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers })
206 } 208 }
207 209
210 // ---------------------------------------------------------------------------
211
208 private listenFullScreenChange () { 212 private listenFullScreenChange () {
209 this.player.on('fullscreenchange', () => { 213 this.player.on('fullscreenchange', () => {
210 if (this.player.isFullscreen()) this.player.focus() 214 if (this.player.isFullscreen()) this.player.focus()
diff --git a/client/src/assets/player/shared/stats/stats-card.ts b/client/src/assets/player/shared/stats/stats-card.ts
index b65adcfca..1199d3285 100644
--- a/client/src/assets/player/shared/stats/stats-card.ts
+++ b/client/src/assets/player/shared/stats/stats-card.ts
@@ -95,9 +95,9 @@ class StatsCard extends Component {
95 const httpStats = data.http 95 const httpStats = data.http
96 96
97 this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ') 97 this.playerNetworkInfo.downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed).join(' ')
98 this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed).join(' ') 98 this.playerNetworkInfo.uploadSpeed = bytes(p2pStats.uploadSpeed).join(' ')
99 this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ') 99 this.playerNetworkInfo.totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded).join(' ')
100 this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded).join(' ') 100 this.playerNetworkInfo.totalUploaded = bytes(p2pStats.uploaded).join(' ')
101 this.playerNetworkInfo.numPeers = p2pStats.numPeers 101 this.playerNetworkInfo.numPeers = p2pStats.numPeers
102 this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s' 102 this.playerNetworkInfo.averageBandwidth = bytes(data.bandwidthEstimate).join(' ') + '/s'
103 103
diff --git a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
index 9fd5f593e..fa3f48a9a 100644
--- a/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
+++ b/client/src/assets/player/shared/webtorrent/webtorrent-plugin.ts
@@ -204,6 +204,8 @@ class WebTorrentPlugin extends Plugin {
204 } 204 }
205 205
206 this.updateVideoFile(newVideoFile, options) 206 this.updateVideoFile(newVideoFile, options)
207
208 this.player.trigger('engineResolutionChange')
207 } 209 }
208 210
209 flushVideoFile (videoFile: VideoFile, destroyRenderer = true) { 211 flushVideoFile (videoFile: VideoFile, destroyRenderer = true) {
@@ -506,9 +508,7 @@ class WebTorrentPlugin extends Plugin {
506 source: 'webtorrent', 508 source: 'webtorrent',
507 http: { 509 http: {
508 downloadSpeed: 0, 510 downloadSpeed: 0,
509 uploadSpeed: 0, 511 downloaded: 0
510 downloaded: 0,
511 uploaded: 0
512 }, 512 },
513 p2p: { 513 p2p: {
514 downloadSpeed: this.torrent.downloadSpeed, 514 downloadSpeed: this.torrent.downloadSpeed,
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts
index cadce739d..b4d9374c3 100644
--- a/client/src/assets/player/types/manager-options.ts
+++ b/client/src/assets/player/types/manager-options.ts
@@ -59,6 +59,8 @@ export interface CommonOptions extends CustomizationOptions {
59 videoViewUrl: string 59 videoViewUrl: string
60 authorizationHeader?: string 60 authorizationHeader?: string
61 61
62 metricsUrl: string
63
62 embedUrl: string 64 embedUrl: string
63 embedTitle: string 65 embedTitle: string
64 66
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts
index 115afb614..6df94992c 100644
--- a/client/src/assets/player/types/peertube-videojs-typings.ts
+++ b/client/src/assets/player/types/peertube-videojs-typings.ts
@@ -109,6 +109,12 @@ type PeerTubePluginOptions = {
109 videoUUID: string 109 videoUUID: string
110} 110}
111 111
112type MetricsPluginOptions = {
113 mode: PlayerMode
114 metricsUrl: string
115 videoUUID: string
116}
117
112type PlaylistPluginOptions = { 118type PlaylistPluginOptions = {
113 elements: VideoPlaylistElement[] 119 elements: VideoPlaylistElement[]
114 120
@@ -165,6 +171,7 @@ type VideoJSPluginOptions = {
165 playlist?: PlaylistPluginOptions 171 playlist?: PlaylistPluginOptions
166 172
167 peertube: PeerTubePluginOptions 173 peertube: PeerTubePluginOptions
174 metrics: MetricsPluginOptions
168 175
169 webtorrent?: WebtorrentPluginOptions 176 webtorrent?: WebtorrentPluginOptions
170 177
@@ -197,9 +204,7 @@ type PlayerNetworkInfo = {
197 204
198 http: { 205 http: {
199 downloadSpeed: number 206 downloadSpeed: number
200 uploadSpeed: number
201 downloaded: number 207 downloaded: number
202 uploaded: number
203 } 208 }
204 209
205 p2p: { 210 p2p: {
@@ -227,6 +232,7 @@ export {
227 ResolutionUpdateData, 232 ResolutionUpdateData,
228 AutoResolutionUpdateData, 233 AutoResolutionUpdateData,
229 PlaylistPluginOptions, 234 PlaylistPluginOptions,
235 MetricsPluginOptions,
230 VideoJSCaption, 236 VideoJSCaption,
231 PeerTubePluginOptions, 237 PeerTubePluginOptions,
232 WebtorrentPluginOptions, 238 WebtorrentPluginOptions,
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts
index 9cebdcd10..eed821994 100644
--- a/client/src/standalone/videos/shared/player-manager-options.ts
+++ b/client/src/standalone/videos/shared/player-manager-options.ts
@@ -203,6 +203,7 @@ export class PlayerManagerOptions {
203 videoCaptions, 203 videoCaptions,
204 inactivityTimeout: 2500, 204 inactivityTimeout: 2500,
205 videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), 205 videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid),
206 metricsUrl: window.location.origin + '/api/v1/metrics/playback',
206 207
207 videoShortUUID: video.shortUUID, 208 videoShortUUID: video.shortUUID,
208 videoUUID: video.uuid, 209 videoUUID: video.uuid,
diff --git a/config/test.yaml b/config/test.yaml
index 9b24d44c0..a87642bd8 100644
--- a/config/test.yaml
+++ b/config/test.yaml
@@ -148,3 +148,8 @@ geo_ip:
148 148
149video_studio: 149video_studio:
150 enabled: true 150 enabled: true
151
152open_telemetry:
153 metrics:
154 prometheus_exporter:
155 port: 9092
diff --git a/package.json b/package.json
index 64faf8355..be66e0744 100644
--- a/package.json
+++ b/package.json
@@ -86,18 +86,18 @@
86 "@babel/parser": "^7.17.8", 86 "@babel/parser": "^7.17.8",
87 "@node-oauth/oauth2-server": "^4.2.0", 87 "@node-oauth/oauth2-server": "^4.2.0",
88 "@opentelemetry/api": "^1.1.0", 88 "@opentelemetry/api": "^1.1.0",
89 "@opentelemetry/api-metrics": "^0.30.0", 89 "@opentelemetry/api-metrics": "^0.31.0",
90 "@opentelemetry/exporter-jaeger": "^1.3.1", 90 "@opentelemetry/exporter-jaeger": "^1.3.1",
91 "@opentelemetry/exporter-prometheus": "~0.30.0", 91 "@opentelemetry/exporter-prometheus": "~0.31.0",
92 "@opentelemetry/instrumentation": "^0.30.0", 92 "@opentelemetry/instrumentation": "^0.31.0",
93 "@opentelemetry/instrumentation-dns": "^0.29.0", 93 "@opentelemetry/instrumentation-dns": "^0.29.0",
94 "@opentelemetry/instrumentation-express": "^0.30.0", 94 "@opentelemetry/instrumentation-express": "^0.30.0",
95 "@opentelemetry/instrumentation-fs": "^0.4.0", 95 "@opentelemetry/instrumentation-fs": "^0.4.0",
96 "@opentelemetry/instrumentation-http": "^0.30.0", 96 "@opentelemetry/instrumentation-http": "^0.31.0",
97 "@opentelemetry/instrumentation-pg": "^0.30.0", 97 "@opentelemetry/instrumentation-pg": "^0.30.0",
98 "@opentelemetry/instrumentation-redis-4": "^0.31.0", 98 "@opentelemetry/instrumentation-redis-4": "^0.31.0",
99 "@opentelemetry/resources": "^1.3.1", 99 "@opentelemetry/resources": "^1.3.1",
100 "@opentelemetry/sdk-metrics-base": "~0.30.0", 100 "@opentelemetry/sdk-metrics-base": "~0.31.0",
101 "@opentelemetry/sdk-trace-base": "^1.3.1", 101 "@opentelemetry/sdk-trace-base": "^1.3.1",
102 "@opentelemetry/sdk-trace-node": "^1.3.1", 102 "@opentelemetry/sdk-trace-node": "^1.3.1",
103 "@opentelemetry/semantic-conventions": "^1.3.1", 103 "@opentelemetry/semantic-conventions": "^1.3.1",
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 8c8ebd061..e1d197c8a 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -11,6 +11,7 @@ import { bulkRouter } from './bulk'
11import { configRouter } from './config' 11import { configRouter } from './config'
12import { customPageRouter } from './custom-page' 12import { customPageRouter } from './custom-page'
13import { jobsRouter } from './jobs' 13import { jobsRouter } from './jobs'
14import { metricsRouter } from './metrics'
14import { oauthClientsRouter } from './oauth-clients' 15import { oauthClientsRouter } from './oauth-clients'
15import { overviewsRouter } from './overviews' 16import { overviewsRouter } from './overviews'
16import { pluginRouter } from './plugins' 17import { pluginRouter } from './plugins'
@@ -18,9 +19,9 @@ import { searchRouter } from './search'
18import { serverRouter } from './server' 19import { serverRouter } from './server'
19import { usersRouter } from './users' 20import { usersRouter } from './users'
20import { videoChannelRouter } from './video-channel' 21import { videoChannelRouter } from './video-channel'
22import { videoChannelSyncRouter } from './video-channel-sync'
21import { videoPlaylistRouter } from './video-playlist' 23import { videoPlaylistRouter } from './video-playlist'
22import { videosRouter } from './videos' 24import { videosRouter } from './videos'
23import { videoChannelSyncRouter } from './video-channel-sync'
24 25
25const apiRouter = express.Router() 26const apiRouter = express.Router()
26 27
@@ -48,6 +49,7 @@ apiRouter.use('/video-channel-syncs', videoChannelSyncRouter)
48apiRouter.use('/video-playlists', videoPlaylistRouter) 49apiRouter.use('/video-playlists', videoPlaylistRouter)
49apiRouter.use('/videos', videosRouter) 50apiRouter.use('/videos', videosRouter)
50apiRouter.use('/jobs', jobsRouter) 51apiRouter.use('/jobs', jobsRouter)
52apiRouter.use('/metrics', metricsRouter)
51apiRouter.use('/search', searchRouter) 53apiRouter.use('/search', searchRouter)
52apiRouter.use('/overviews', overviewsRouter) 54apiRouter.use('/overviews', overviewsRouter)
53apiRouter.use('/plugins', pluginRouter) 55apiRouter.use('/plugins', pluginRouter)
diff --git a/server/controllers/api/metrics.ts b/server/controllers/api/metrics.ts
new file mode 100644
index 000000000..578b023a1
--- /dev/null
+++ b/server/controllers/api/metrics.ts
@@ -0,0 +1,27 @@
1import express from 'express'
2import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics'
3import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models'
4import { addPlaybackMetricValidator, asyncMiddleware } from '../../middlewares'
5
6const metricsRouter = express.Router()
7
8metricsRouter.post('/playback',
9 asyncMiddleware(addPlaybackMetricValidator),
10 addPlaybackMetric
11)
12
13// ---------------------------------------------------------------------------
14
15export {
16 metricsRouter
17}
18
19// ---------------------------------------------------------------------------
20
21function addPlaybackMetric (req: express.Request, res: express.Response) {
22 const body: PlaybackMetricCreate = req.body
23
24 OpenTelemetryMetrics.Instance.observePlaybackMetric(res.locals.onlyImmutableVideo, body)
25
26 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
27}
diff --git a/server/helpers/custom-validators/metrics.ts b/server/helpers/custom-validators/metrics.ts
new file mode 100644
index 000000000..533f8988d
--- /dev/null
+++ b/server/helpers/custom-validators/metrics.ts
@@ -0,0 +1,9 @@
1function isValidPlayerMode (value: any) {
2 return value === 'webtorrent' || value === 'p2p-media-loader'
3}
4
5// ---------------------------------------------------------------------------
6
7export {
8 isValidPlayerMode
9}
diff --git a/server/lib/opentelemetry/metric-helpers/index.ts b/server/lib/opentelemetry/metric-helpers/index.ts
index 1b3813743..775d954ba 100644
--- a/server/lib/opentelemetry/metric-helpers/index.ts
+++ b/server/lib/opentelemetry/metric-helpers/index.ts
@@ -1,5 +1,6 @@
1export * from './lives-observers-builder' 1export * from './lives-observers-builder'
2export * from './job-queue-observers-builder' 2export * from './job-queue-observers-builder'
3export * from './nodejs-observers-builder' 3export * from './nodejs-observers-builder'
4export * from './playback-metrics'
4export * from './stats-observers-builder' 5export * from './stats-observers-builder'
5export * from './viewers-observers-builder' 6export * from './viewers-observers-builder'
diff --git a/server/lib/opentelemetry/metric-helpers/nodejs-observers-builder.ts b/server/lib/opentelemetry/metric-helpers/nodejs-observers-builder.ts
index 766cbe03b..473015e91 100644
--- a/server/lib/opentelemetry/metric-helpers/nodejs-observers-builder.ts
+++ b/server/lib/opentelemetry/metric-helpers/nodejs-observers-builder.ts
@@ -2,7 +2,7 @@ import { readdir } from 'fs-extra'
2import { constants, PerformanceObserver } from 'perf_hooks' 2import { constants, PerformanceObserver } from 'perf_hooks'
3import * as process from 'process' 3import * as process from 'process'
4import { Meter, ObservableResult } from '@opentelemetry/api-metrics' 4import { Meter, ObservableResult } from '@opentelemetry/api-metrics'
5import { ExplicitBucketHistogramAggregation, MeterProvider } from '@opentelemetry/sdk-metrics-base' 5import { ExplicitBucketHistogramAggregation } from '@opentelemetry/sdk-metrics-base'
6import { View } from '@opentelemetry/sdk-metrics-base/build/src/view/View' 6import { View } from '@opentelemetry/sdk-metrics-base/build/src/view/View'
7import { logger } from '@server/helpers/logger' 7import { logger } from '@server/helpers/logger'
8 8
@@ -12,7 +12,16 @@ import { logger } from '@server/helpers/logger'
12 12
13export class NodeJSObserversBuilder { 13export class NodeJSObserversBuilder {
14 14
15 constructor (private readonly meter: Meter, private readonly meterProvider: MeterProvider) { 15 constructor (private readonly meter: Meter) {
16 }
17
18 static getViews () {
19 return [
20 new View({
21 aggregation: new ExplicitBucketHistogramAggregation([ 0.001, 0.01, 0.1, 1, 2, 5 ]),
22 instrumentName: 'nodejs_gc_duration_seconds'
23 })
24 ]
16 } 25 }
17 26
18 buildObservers () { 27 buildObservers () {
@@ -91,11 +100,6 @@ export class NodeJSObserversBuilder {
91 [constants.NODE_PERFORMANCE_GC_WEAKCB]: 'weakcb' 100 [constants.NODE_PERFORMANCE_GC_WEAKCB]: 'weakcb'
92 } 101 }
93 102
94 this.meterProvider.addView(
95 new View({ aggregation: new ExplicitBucketHistogramAggregation([ 0.001, 0.01, 0.1, 1, 2, 5 ]) }),
96 { instrument: { name: 'nodejs_gc_duration_seconds' } }
97 )
98
99 const histogram = this.meter.createHistogram('nodejs_gc_duration_seconds', { 103 const histogram = this.meter.createHistogram('nodejs_gc_duration_seconds', {
100 description: 'Garbage collection duration by kind, one of major, minor, incremental or weakcb' 104 description: 'Garbage collection duration by kind, one of major, minor, incremental or weakcb'
101 }) 105 })
diff --git a/server/lib/opentelemetry/metric-helpers/playback-metrics.ts b/server/lib/opentelemetry/metric-helpers/playback-metrics.ts
new file mode 100644
index 000000000..d2abdee62
--- /dev/null
+++ b/server/lib/opentelemetry/metric-helpers/playback-metrics.ts
@@ -0,0 +1,59 @@
1import { Counter, Meter } from '@opentelemetry/api-metrics'
2import { MVideoImmutable } from '@server/types/models'
3import { PlaybackMetricCreate } from '@shared/models'
4
5export class PlaybackMetrics {
6 private errorsCounter: Counter
7 private resolutionChangesCounter: Counter
8
9 private downloadedBytesP2PCounter: Counter
10 private uploadedBytesP2PCounter: Counter
11
12 private downloadedBytesHTTPCounter: Counter
13
14 constructor (private readonly meter: Meter) {
15
16 }
17
18 buildCounters () {
19 this.errorsCounter = this.meter.createCounter('peertube_playback_errors_count', {
20 description: 'Errors collected from PeerTube player.'
21 })
22
23 this.resolutionChangesCounter = this.meter.createCounter('peertube_playback_resolution_changes_count', {
24 description: 'Resolution changes collected from PeerTube player.'
25 })
26
27 this.downloadedBytesHTTPCounter = this.meter.createCounter('peertube_playback_http_downloaded_bytes', {
28 description: 'Downloaded bytes with HTTP by PeerTube player.'
29 })
30 this.downloadedBytesP2PCounter = this.meter.createCounter('peertube_playback_p2p_downloaded_bytes', {
31 description: 'Downloaded bytes with P2P by PeerTube player.'
32 })
33
34 this.uploadedBytesP2PCounter = this.meter.createCounter('peertube_playback_p2p_uploaded_bytes', {
35 description: 'Uploaded bytes with P2P by PeerTube player.'
36 })
37 }
38
39 observe (video: MVideoImmutable, metrics: PlaybackMetricCreate) {
40 const attributes = {
41 videoOrigin: video.remote
42 ? 'remote'
43 : 'local',
44
45 playerMode: metrics.playerMode,
46
47 resolution: metrics.resolution + '',
48 fps: metrics.fps + ''
49 }
50
51 this.errorsCounter.add(metrics.errors, attributes)
52 this.resolutionChangesCounter.add(metrics.resolutionChanges, attributes)
53
54 this.downloadedBytesHTTPCounter.add(metrics.downloadedBytesHTTP, attributes)
55 this.downloadedBytesP2PCounter.add(metrics.downloadedBytesP2P, attributes)
56
57 this.uploadedBytesP2PCounter.add(metrics.uploadedBytesP2P, attributes)
58 }
59}
diff --git a/server/lib/opentelemetry/metrics.ts b/server/lib/opentelemetry/metrics.ts
index ffe493670..ba33c9505 100644
--- a/server/lib/opentelemetry/metrics.ts
+++ b/server/lib/opentelemetry/metrics.ts
@@ -4,10 +4,13 @@ import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'
4import { MeterProvider } from '@opentelemetry/sdk-metrics-base' 4import { MeterProvider } from '@opentelemetry/sdk-metrics-base'
5import { logger } from '@server/helpers/logger' 5import { logger } from '@server/helpers/logger'
6import { CONFIG } from '@server/initializers/config' 6import { CONFIG } from '@server/initializers/config'
7import { MVideoImmutable } from '@server/types/models'
8import { PlaybackMetricCreate } from '@shared/models'
7import { 9import {
8 JobQueueObserversBuilder, 10 JobQueueObserversBuilder,
9 LivesObserversBuilder, 11 LivesObserversBuilder,
10 NodeJSObserversBuilder, 12 NodeJSObserversBuilder,
13 PlaybackMetrics,
11 StatsObserversBuilder, 14 StatsObserversBuilder,
12 ViewersObserversBuilder 15 ViewersObserversBuilder
13} from './metric-helpers' 16} from './metric-helpers'
@@ -20,6 +23,8 @@ class OpenTelemetryMetrics {
20 23
21 private onRequestDuration: (req: Request, res: Response) => void 24 private onRequestDuration: (req: Request, res: Response) => void
22 25
26 private playbackMetrics: PlaybackMetrics
27
23 private constructor () {} 28 private constructor () {}
24 29
25 init (app: Application) { 30 init (app: Application) {
@@ -41,7 +46,11 @@ class OpenTelemetryMetrics {
41 46
42 logger.info('Registering Open Telemetry metrics') 47 logger.info('Registering Open Telemetry metrics')
43 48
44 const provider = new MeterProvider() 49 const provider = new MeterProvider({
50 views: [
51 ...NodeJSObserversBuilder.getViews()
52 ]
53 })
45 54
46 provider.addMetricReader(new PrometheusExporter({ port: CONFIG.OPEN_TELEMETRY.METRICS.PROMETHEUS_EXPORTER.PORT })) 55 provider.addMetricReader(new PrometheusExporter({ port: CONFIG.OPEN_TELEMETRY.METRICS.PROMETHEUS_EXPORTER.PORT }))
47 56
@@ -51,7 +60,10 @@ class OpenTelemetryMetrics {
51 60
52 this.buildRequestObserver() 61 this.buildRequestObserver()
53 62
54 const nodeJSObserversBuilder = new NodeJSObserversBuilder(this.meter, provider) 63 this.playbackMetrics = new PlaybackMetrics(this.meter)
64 this.playbackMetrics.buildCounters()
65
66 const nodeJSObserversBuilder = new NodeJSObserversBuilder(this.meter)
55 nodeJSObserversBuilder.buildObservers() 67 nodeJSObserversBuilder.buildObservers()
56 68
57 const jobQueueObserversBuilder = new JobQueueObserversBuilder(this.meter) 69 const jobQueueObserversBuilder = new JobQueueObserversBuilder(this.meter)
@@ -67,6 +79,10 @@ class OpenTelemetryMetrics {
67 viewersObserversBuilder.buildObservers() 79 viewersObserversBuilder.buildObservers()
68 } 80 }
69 81
82 observePlaybackMetric (video: MVideoImmutable, metrics: PlaybackMetricCreate) {
83 this.playbackMetrics.observe(video, metrics)
84 }
85
70 private buildRequestObserver () { 86 private buildRequestObserver () {
71 const requestDuration = this.meter.createHistogram('http_request_duration_ms', { 87 const requestDuration = this.meter.createHistogram('http_request_duration_ms', {
72 unit: 'milliseconds', 88 unit: 'milliseconds',
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index b0ad04819..ffadb3b49 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -10,6 +10,7 @@ export * from './express'
10export * from './feeds' 10export * from './feeds'
11export * from './follows' 11export * from './follows'
12export * from './jobs' 12export * from './jobs'
13export * from './metrics'
13export * from './logs' 14export * from './logs'
14export * from './oembed' 15export * from './oembed'
15export * from './pagination' 16export * from './pagination'
diff --git a/server/middlewares/validators/metrics.ts b/server/middlewares/validators/metrics.ts
new file mode 100644
index 000000000..b1dbec603
--- /dev/null
+++ b/server/middlewares/validators/metrics.ts
@@ -0,0 +1,56 @@
1import express from 'express'
2import { body } from 'express-validator'
3import { isValidPlayerMode } from '@server/helpers/custom-validators/metrics'
4import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc'
5import { CONFIG } from '@server/initializers/config'
6import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models'
7import { logger } from '../../helpers/logger'
8import { areValidationErrors, doesVideoExist } from './shared'
9
10const addPlaybackMetricValidator = [
11 body('resolution')
12 .isInt({ min: 0 }).withMessage('Invalid resolution'),
13 body('fps')
14 .optional()
15 .isInt({ min: 0 }).withMessage('Invalid fps'),
16 body('playerMode')
17 .custom(isValidPlayerMode).withMessage('Invalid playerMode'),
18
19 body('resolutionChanges')
20 .isInt({ min: 0 }).withMessage('Invalid resolutionChanges'),
21
22 body('errors')
23 .isInt({ min: 0 }).withMessage('Invalid errors'),
24
25 body('downloadedBytesP2P')
26 .isInt({ min: 0 }).withMessage('Invalid downloadedBytesP2P'),
27 body('downloadedBytesHTTP')
28 .isInt({ min: 0 }).withMessage('Invalid downloadedBytesHTTP'),
29
30 body('uploadedBytesP2P')
31 .isInt({ min: 0 }).withMessage('Invalid uploadedBytesP2P'),
32
33 body('videoId')
34 .customSanitizer(toCompleteUUID)
35 .optional()
36 .custom(isIdOrUUIDValid),
37
38 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
39 logger.debug('Checking addPlaybackMetricValidator parameters.', { parameters: req.query })
40
41 if (!CONFIG.OPEN_TELEMETRY.METRICS.ENABLED) return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
42
43 const body: PlaybackMetricCreate = req.body
44
45 if (areValidationErrors(req, res)) return
46 if (!await doesVideoExist(body.videoId, res, 'only-immutable-attributes')) return
47
48 return next()
49 }
50]
51
52// ---------------------------------------------------------------------------
53
54export {
55 addPlaybackMetricValidator
56}
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 149305f49..cd7a38459 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -10,6 +10,7 @@ import './follows'
10import './jobs' 10import './jobs'
11import './live' 11import './live'
12import './logs' 12import './logs'
13import './metrics'
13import './my-user' 14import './my-user'
14import './plugins' 15import './plugins'
15import './redundancy' 16import './redundancy'
diff --git a/server/tests/api/check-params/metrics.ts b/server/tests/api/check-params/metrics.ts
new file mode 100644
index 000000000..2d4509406
--- /dev/null
+++ b/server/tests/api/check-params/metrics.ts
@@ -0,0 +1,183 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { omit } from 'lodash'
5import { HttpStatusCode, PlaybackMetricCreate, VideoResolution } from '@shared/models'
6import { cleanupTests, createSingleServer, makePostBodyRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
7
8describe('Test metrics API validators', function () {
9 let server: PeerTubeServer
10 let videoUUID: string
11
12 // ---------------------------------------------------------------
13
14 before(async function () {
15 this.timeout(120000)
16
17 server = await createSingleServer(1, {
18 open_telemetry: {
19 metrics: {
20 enabled: true
21 }
22 }
23 })
24
25 await setAccessTokensToServers([ server ])
26
27 const { uuid } = await server.videos.quickUpload({ name: 'video' })
28 videoUUID = uuid
29 })
30
31 describe('When adding playback metrics', function () {
32 const path = '/api/v1/metrics/playback'
33 let baseParams: PlaybackMetricCreate
34
35 before(function () {
36 baseParams = {
37 playerMode: 'p2p-media-loader',
38 resolution: VideoResolution.H_1080P,
39 fps: 30,
40 resolutionChanges: 1,
41 errors: 2,
42 downloadedBytesP2P: 0,
43 downloadedBytesHTTP: 0,
44 uploadedBytesP2P: 0,
45 videoId: videoUUID
46 }
47 })
48
49 it('Should fail with an invalid resolution', async function () {
50 await makePostBodyRequest({
51 url: server.url,
52 path,
53 fields: { ...baseParams, resolution: 'toto' }
54 })
55 })
56
57 it('Should fail with an invalid fps', async function () {
58 await makePostBodyRequest({
59 url: server.url,
60 path,
61 fields: { ...baseParams, fps: 'toto' }
62 })
63 })
64
65 it('Should fail with a missing/invalid player mode', async function () {
66 await makePostBodyRequest({
67 url: server.url,
68 path,
69 fields: omit(baseParams, 'playerMode')
70 })
71
72 await makePostBodyRequest({
73 url: server.url,
74 path,
75 fields: { ...baseParams, playerMode: 'toto' }
76 })
77 })
78
79 it('Should fail with an missing/invalid resolution changes', async function () {
80 await makePostBodyRequest({
81 url: server.url,
82 path,
83 fields: omit(baseParams, 'resolutionChanges')
84 })
85
86 await makePostBodyRequest({
87 url: server.url,
88 path,
89 fields: { ...baseParams, resolutionChanges: 'toto' }
90 })
91 })
92
93 it('Should fail with a missing errors', async function () {
94
95 })
96
97 it('Should fail with an missing/invalid errors', async function () {
98 await makePostBodyRequest({
99 url: server.url,
100 path,
101 fields: omit(baseParams, 'errors')
102 })
103
104 await makePostBodyRequest({
105 url: server.url,
106 path,
107 fields: { ...baseParams, errors: 'toto' }
108 })
109 })
110
111 it('Should fail with an missing/invalid downloadedBytesP2P', async function () {
112 await makePostBodyRequest({
113 url: server.url,
114 path,
115 fields: omit(baseParams, 'downloadedBytesP2P')
116 })
117
118 await makePostBodyRequest({
119 url: server.url,
120 path,
121 fields: { ...baseParams, downloadedBytesP2P: 'toto' }
122 })
123 })
124
125 it('Should fail with an missing/invalid downloadedBytesHTTP', async function () {
126 await makePostBodyRequest({
127 url: server.url,
128 path,
129 fields: omit(baseParams, 'downloadedBytesHTTP')
130 })
131
132 await makePostBodyRequest({
133 url: server.url,
134 path,
135 fields: { ...baseParams, downloadedBytesHTTP: 'toto' }
136 })
137 })
138
139 it('Should fail with an missing/invalid uploadedBytesP2P', async function () {
140 await makePostBodyRequest({
141 url: server.url,
142 path,
143 fields: omit(baseParams, 'uploadedBytesP2P')
144 })
145
146 await makePostBodyRequest({
147 url: server.url,
148 path,
149 fields: { ...baseParams, uploadedBytesP2P: 'toto' }
150 })
151 })
152
153 it('Should fail with a bad video id', async function () {
154 await makePostBodyRequest({
155 url: server.url,
156 path,
157 fields: { ...baseParams, videoId: 'toto' }
158 })
159 })
160
161 it('Should fail with an unknown video', async function () {
162 await makePostBodyRequest({
163 url: server.url,
164 path,
165 fields: { ...baseParams, videoId: 42 },
166 expectedStatus: HttpStatusCode.NOT_FOUND_404
167 })
168 })
169
170 it('Should succeed with the correct params', async function () {
171 await makePostBodyRequest({
172 url: server.url,
173 path,
174 fields: baseParams,
175 expectedStatus: HttpStatusCode.NO_CONTENT_204
176 })
177 })
178 })
179
180 after(async function () {
181 await cleanupTests([ server ])
182 })
183})
diff --git a/server/tests/api/server/open-telemetry.ts b/server/tests/api/server/open-telemetry.ts
index 20909429f..3137a9eb6 100644
--- a/server/tests/api/server/open-telemetry.ts
+++ b/server/tests/api/server/open-telemetry.ts
@@ -2,14 +2,14 @@
2 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { expectLogContain, expectLogDoesNotContain, MockHTTP } from '@server/tests/shared' 4import { expectLogContain, expectLogDoesNotContain, MockHTTP } from '@server/tests/shared'
5import { HttpStatusCode, VideoPrivacy } from '@shared/models' 5import { HttpStatusCode, VideoPrivacy, VideoResolution } from '@shared/models'
6import { cleanupTests, createSingleServer, makeRawRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' 6import { cleanupTests, createSingleServer, makeRawRequest, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
7 7
8describe('Open Telemetry', function () { 8describe('Open Telemetry', function () {
9 let server: PeerTubeServer 9 let server: PeerTubeServer
10 10
11 describe('Metrics', function () { 11 describe('Metrics', function () {
12 const metricsUrl = 'http://localhost:9091/metrics' 12 const metricsUrl = 'http://localhost:9092/metrics'
13 13
14 it('Should not enable open telemetry metrics', async function () { 14 it('Should not enable open telemetry metrics', async function () {
15 server = await createSingleServer(1) 15 server = await createSingleServer(1)
@@ -36,8 +36,33 @@ describe('Open Telemetry', function () {
36 }) 36 })
37 37
38 const res = await makeRawRequest(metricsUrl, HttpStatusCode.OK_200) 38 const res = await makeRawRequest(metricsUrl, HttpStatusCode.OK_200)
39 expect(res.text).to.contain('peertube_job_queue_total') 39 expect(res.text).to.contain('peertube_job_queue_total{')
40 })
41
42 it('Should have playback metrics', async function () {
43 await setAccessTokensToServers([ server ])
44
45 const video = await server.videos.quickUpload({ name: 'video' })
46
47 await server.metrics.addPlaybackMetric({
48 metrics: {
49 playerMode: 'p2p-media-loader',
50 resolution: VideoResolution.H_1080P,
51 fps: 30,
52 resolutionChanges: 1,
53 errors: 2,
54 downloadedBytesP2P: 0,
55 downloadedBytesHTTP: 0,
56 uploadedBytesP2P: 5,
57 videoId: video.uuid
58 }
59 })
40 60
61 const res = await makeRawRequest(metricsUrl, HttpStatusCode.OK_200)
62 expect(res.text).to.contain('peertube_playback_http_uploaded_bytes_total{')
63 })
64
65 after(async function () {
41 await server.kill() 66 await server.kill()
42 }) 67 })
43 }) 68 })
diff --git a/shared/models/index.ts b/shared/models/index.ts
index 78723d830..439e9c8e1 100644
--- a/shared/models/index.ts
+++ b/shared/models/index.ts
@@ -6,6 +6,7 @@ export * from './custom-markup'
6export * from './feeds' 6export * from './feeds'
7export * from './http' 7export * from './http'
8export * from './joinpeertube' 8export * from './joinpeertube'
9export * from './metrics'
9export * from './moderation' 10export * from './moderation'
10export * from './overviews' 11export * from './overviews'
11export * from './plugins' 12export * from './plugins'
diff --git a/shared/models/metrics/index.ts b/shared/models/metrics/index.ts
new file mode 100644
index 000000000..24194cce3
--- /dev/null
+++ b/shared/models/metrics/index.ts
@@ -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
index 000000000..d669ab690
--- /dev/null
+++ b/shared/models/metrics/playback-metric-create.model.ts
@@ -0,0 +1,19 @@
1import { VideoResolution } from '../videos'
2
3export interface PlaybackMetricCreate {
4 playerMode: 'p2p-media-loader' | 'webtorrent'
5
6 resolution?: VideoResolution
7 fps?: number
8
9 resolutionChanges: number
10
11 errors: number
12
13 downloadedBytesP2P: number
14 downloadedBytesHTTP: number
15
16 uploadedBytesP2P: number
17
18 videoId: number | string
19}
diff --git a/shared/server-commands/server/index.ts b/shared/server-commands/server/index.ts
index 0a4b21fc4..9a2fbf8d3 100644
--- a/shared/server-commands/server/index.ts
+++ b/shared/server-commands/server/index.ts
@@ -5,6 +5,7 @@ export * from './follows-command'
5export * from './follows' 5export * from './follows'
6export * from './jobs' 6export * from './jobs'
7export * from './jobs-command' 7export * from './jobs-command'
8export * from './metrics-command'
8export * from './object-storage-command' 9export * from './object-storage-command'
9export * from './plugins-command' 10export * from './plugins-command'
10export * from './redundancy-command' 11export * 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
index 000000000..d22b4833d
--- /dev/null
+++ b/shared/server-commands/server/metrics-command.ts
@@ -0,0 +1,18 @@
1import { HttpStatusCode, PlaybackMetricCreate } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class MetricsCommand extends AbstractCommand {
5
6 addPlaybackMetric (options: OverrideCommandOptions & { metrics: PlaybackMetricCreate }) {
7 const path = '/api/v1/metrics/playback'
8
9 return this.postBodyRequest({
10 ...options,
11
12 path,
13 fields: options.metrics,
14 implicitToken: false,
15 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
16 })
17 }
18}
diff --git a/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts
index c05d16ad2..2b4c9c9f8 100644
--- a/shared/server-commands/server/server.ts
+++ b/shared/server-commands/server/server.ts
@@ -37,6 +37,7 @@ import { ContactFormCommand } from './contact-form-command'
37import { DebugCommand } from './debug-command' 37import { DebugCommand } from './debug-command'
38import { FollowsCommand } from './follows-command' 38import { FollowsCommand } from './follows-command'
39import { JobsCommand } from './jobs-command' 39import { JobsCommand } from './jobs-command'
40import { MetricsCommand } from './metrics-command'
40import { ObjectStorageCommand } from './object-storage-command' 41import { ObjectStorageCommand } from './object-storage-command'
41import { PluginsCommand } from './plugins-command' 42import { PluginsCommand } from './plugins-command'
42import { RedundancyCommand } from './redundancy-command' 43import { RedundancyCommand } from './redundancy-command'
@@ -104,6 +105,7 @@ export class PeerTubeServer {
104 debug?: DebugCommand 105 debug?: DebugCommand
105 follows?: FollowsCommand 106 follows?: FollowsCommand
106 jobs?: JobsCommand 107 jobs?: JobsCommand
108 metrics?: MetricsCommand
107 plugins?: PluginsCommand 109 plugins?: PluginsCommand
108 redundancy?: RedundancyCommand 110 redundancy?: RedundancyCommand
109 stats?: StatsCommand 111 stats?: StatsCommand
@@ -377,6 +379,7 @@ export class PeerTubeServer {
377 this.debug = new DebugCommand(this) 379 this.debug = new DebugCommand(this)
378 this.follows = new FollowsCommand(this) 380 this.follows = new FollowsCommand(this)
379 this.jobs = new JobsCommand(this) 381 this.jobs = new JobsCommand(this)
382 this.metrics = new MetricsCommand(this)
380 this.plugins = new PluginsCommand(this) 383 this.plugins = new PluginsCommand(this)
381 this.redundancy = new RedundancyCommand(this) 384 this.redundancy = new RedundancyCommand(this)
382 this.stats = new StatsCommand(this) 385 this.stats = new StatsCommand(this)
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 4402de954..5077f8d90 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -5009,6 +5009,21 @@ paths:
5009 '404': 5009 '404':
5010 description: plugin not found 5010 description: plugin not found
5011 5011
5012 /metrics/playback:
5013 post:
5014 summary: Create playback metrics
5015 description: These metrics are exposed by OpenTelemetry metrics exporter if enabled.
5016 tags:
5017 - Stats
5018 requestBody:
5019 content:
5020 application/json:
5021 schema:
5022 $ref: '#/components/schemas/PlaybackMetricCreate'
5023 responses:
5024 '204':
5025 description: successful operation
5026
5012servers: 5027servers:
5013 - url: 'https://peertube2.cpy.re/api/v1' 5028 - url: 'https://peertube2.cpy.re/api/v1'
5014 description: Live Test Server (live data - latest nightly version) 5029 description: Live Test Server (live data - latest nightly version)
@@ -8195,44 +8210,86 @@ components:
8195 format: binary 8210 format: binary
8196 8211
8197 LiveVideoSessionResponse: 8212 LiveVideoSessionResponse:
8198 properties: 8213 properties:
8199 id: 8214 id:
8200 type: integer 8215 type: integer
8201 startDate: 8216 startDate:
8202 type: string 8217 type: string
8203 format: date-time 8218 format: date-time
8204 description: Start date of the live session 8219 description: Start date of the live session
8205 endDate: 8220 endDate:
8206 type: string 8221 type: string
8207 format: date-time 8222 format: date-time
8208 nullable: true 8223 nullable: true
8209 description: End date of the live session 8224 description: End date of the live session
8210 error: 8225 error:
8211 type: integer 8226 type: integer
8212 enum: 8227 enum:
8213 - 1 8228 - 1
8214 - 2 8229 - 2
8215 - 3 8230 - 3
8216 - 4 8231 - 4
8217 - 5 8232 - 5
8218 nullable: true 8233 nullable: true
8219 description: > 8234 description: >
8220 Error type if an error occurred during the live session: 8235 Error type if an error occurred during the live session:
8221 - `1`: Bad socket health (transcoding is too slow) 8236 - `1`: Bad socket health (transcoding is too slow)
8222 - `2`: Max duration exceeded 8237 - `2`: Max duration exceeded
8223 - `3`: Quota exceeded 8238 - `3`: Quota exceeded
8224 - `4`: Quota FFmpeg error 8239 - `4`: Quota FFmpeg error
8225 - `5`: Video has been blacklisted during the live 8240 - `5`: Video has been blacklisted during the live
8226 replayVideo: 8241 replayVideo:
8227 type: object 8242 type: object
8228 description: Video replay information 8243 description: Video replay information
8229 properties: 8244 properties:
8230 id: 8245 id:
8231 type: number 8246 type: number
8232 uuid: 8247 uuid:
8233 $ref: '#/components/schemas/UUIDv4' 8248 $ref: '#/components/schemas/UUIDv4'
8234 shortUUID: 8249 shortUUID:
8235 $ref: '#/components/schemas/shortUUID' 8250 $ref: '#/components/schemas/shortUUID'
8251
8252 PlaybackMetricCreate:
8253 properties:
8254 playerMode:
8255 type: string
8256 enum:
8257 - 'p2p-media-loader'
8258 - 'webtorrent'
8259 resolution:
8260 type: number
8261 description: Current player video resolution
8262 fps:
8263 type: number
8264 description: Current player video fps
8265 resolutionChanges:
8266 type: number
8267 description: How many resolution changes occured since the last metric creation
8268 errors:
8269 type: number
8270 description: How many errors occured since the last metric creation
8271 downloadedBytesP2P:
8272 type: number
8273 description: How many bytes were downloaded with P2P since the last metric creation
8274 downloadedBytesHTTP:
8275 type: number
8276 description: How many bytes were downloaded with HTTP since the last metric creation
8277 uploadedBytesP2P:
8278 type: number
8279 description: How many bytes were uploaded with P2P since the last metric creation
8280 videoId:
8281 oneOf:
8282 - $ref: '#/components/schemas/id'
8283 - $ref: '#/components/schemas/UUIDv4'
8284 - $ref: '#/components/schemas/shortUUID'
8285 required:
8286 - playerMode
8287 - resolutionChanges
8288 - errors
8289 - downloadedBytesP2P
8290 - downloadedBytesHTTP
8291 - uploadedBytesP2P
8292 - videoId
8236 8293
8237 callbacks: 8294 callbacks:
8238 searchIndex: 8295 searchIndex:
diff --git a/yarn.lock b/yarn.lock
index d16fd026c..0a479ac57 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1616,10 +1616,10 @@
1616 dependencies: 1616 dependencies:
1617 "@opentelemetry/api" "^1.0.0" 1617 "@opentelemetry/api" "^1.0.0"
1618 1618
1619"@opentelemetry/api-metrics@0.30.0", "@opentelemetry/api-metrics@^0.30.0": 1619"@opentelemetry/api-metrics@0.31.0", "@opentelemetry/api-metrics@^0.31.0":
1620 version "0.30.0" 1620 version "0.31.0"
1621 resolved "https://registry.yarnpkg.com/@opentelemetry/api-metrics/-/api-metrics-0.30.0.tgz#b5defd10756e81d1c7ce8669ff8a8d2465ba0be8" 1621 resolved "https://registry.yarnpkg.com/@opentelemetry/api-metrics/-/api-metrics-0.31.0.tgz#0ed4cf4d7c731f968721c2b303eaf5e9fd42f736"
1622 integrity sha512-jSb7iiYPY+DSUKIyzfGt0a5K1QGzWY5fSWtUB8Alfi27NhQGHBeuYYC5n9MaBP/HNWw5GpEIhXGEYCF9Pf8IEg== 1622 integrity sha512-PcL1x0kZtMie7NsNy67OyMvzLEXqf3xd0TZJKHHPMGTe89oMpNVrD1zJB1kZcwXOxLlHHb6tz21G3vvXPdXyZg==
1623 dependencies: 1623 dependencies:
1624 "@opentelemetry/api" "^1.0.0" 1624 "@opentelemetry/api" "^1.0.0"
1625 1625
@@ -1633,13 +1633,6 @@
1633 resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.5.0.tgz#4955313e7f0ec0fe17c813328a2a7f39f262c0fa" 1633 resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.5.0.tgz#4955313e7f0ec0fe17c813328a2a7f39f262c0fa"
1634 integrity sha512-mhBPP0BU0RaH2HB8U4MDd5OjWA1y7SoLOovCT0iEpJAltaq2z04uxRJVzIs91vkpNnV0utUZowQQD3KElgU+VA== 1634 integrity sha512-mhBPP0BU0RaH2HB8U4MDd5OjWA1y7SoLOovCT0iEpJAltaq2z04uxRJVzIs91vkpNnV0utUZowQQD3KElgU+VA==
1635 1635
1636"@opentelemetry/core@1.4.0":
1637 version "1.4.0"
1638 resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.4.0.tgz#26839ab9e36583a174273a1e1c5b33336c163725"
1639 integrity sha512-faq50VFEdyC7ICAOlhSi+yYZ+peznnGjTJToha9R63i9fVopzpKrkZt7AIdXUmz2+L2OqXrcJs7EIdN/oDyr5w==
1640 dependencies:
1641 "@opentelemetry/semantic-conventions" "1.4.0"
1642
1643"@opentelemetry/core@1.5.0", "@opentelemetry/core@^1.0.0": 1636"@opentelemetry/core@1.5.0", "@opentelemetry/core@^1.0.0":
1644 version "1.5.0" 1637 version "1.5.0"
1645 resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.5.0.tgz#717bceee15d4c69d4c7321c1fe0f5a562b60eb81" 1638 resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.5.0.tgz#717bceee15d4c69d4c7321c1fe0f5a562b60eb81"
@@ -1657,14 +1650,14 @@
1657 "@opentelemetry/semantic-conventions" "1.5.0" 1650 "@opentelemetry/semantic-conventions" "1.5.0"
1658 jaeger-client "^3.15.0" 1651 jaeger-client "^3.15.0"
1659 1652
1660"@opentelemetry/exporter-prometheus@~0.30.0": 1653"@opentelemetry/exporter-prometheus@~0.31.0":
1661 version "0.30.0" 1654 version "0.31.0"
1662 resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.30.0.tgz#f81322d3cb000170e716bc76820600d5649be538" 1655 resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.31.0.tgz#b0696be42542a961ec1145f3754a845efbda942e"
1663 integrity sha512-y0SXvpzoKR+Tk/UL6F1f7vAcCzqpCDP/cTEa+Z7sX57aEG0HDXLQiLmAgK/BHqcEN5MFQMZ+MDVDsUrvpa6/Jw== 1656 integrity sha512-EfWFzoCu/THw0kZiaA2RUrk6XIQbfaJHJ26LRrVIK7INwosW8Q+x4pGfiJ5nxhglYiG9OTqGrQ6nQ4T9q1UMpg==
1664 dependencies: 1657 dependencies:
1665 "@opentelemetry/api-metrics" "0.30.0" 1658 "@opentelemetry/api-metrics" "0.31.0"
1666 "@opentelemetry/core" "1.4.0" 1659 "@opentelemetry/core" "1.5.0"
1667 "@opentelemetry/sdk-metrics-base" "0.30.0" 1660 "@opentelemetry/sdk-metrics-base" "0.31.0"
1668 1661
1669"@opentelemetry/instrumentation-dns@^0.29.0": 1662"@opentelemetry/instrumentation-dns@^0.29.0":
1670 version "0.29.0" 1663 version "0.29.0"
@@ -1694,14 +1687,14 @@
1694 "@opentelemetry/instrumentation" "^0.29.2" 1687 "@opentelemetry/instrumentation" "^0.29.2"
1695 "@opentelemetry/semantic-conventions" "^1.0.0" 1688 "@opentelemetry/semantic-conventions" "^1.0.0"
1696 1689
1697"@opentelemetry/instrumentation-http@^0.30.0": 1690"@opentelemetry/instrumentation-http@^0.31.0":
1698 version "0.30.0" 1691 version "0.31.0"
1699 resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.30.0.tgz#312ef25defbff750dd9082356bb9a9137ed5fd82" 1692 resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-http/-/instrumentation-http-0.31.0.tgz#5c6dea9cdb636543c6ed1f1a4e55d4422e50fa89"
1700 integrity sha512-OhiuzR2mhlTcaXD1dYW/dqnC/zjIKHp2NWMUyDHEd4xS6NZAiTU5mNDv57Y9on+/VwYXWUZZ2tB7AOVPsFUIOg== 1693 integrity sha512-DLw+H7UQZ+V3FX72iGXVMX4ylL4jV+GHraaUiVY0CIdxg1nrGmjLm4dPU5500IXlbgZUUoJ9jq02JDblujdKcQ==
1701 dependencies: 1694 dependencies:
1702 "@opentelemetry/core" "1.4.0" 1695 "@opentelemetry/core" "1.5.0"
1703 "@opentelemetry/instrumentation" "0.30.0" 1696 "@opentelemetry/instrumentation" "0.31.0"
1704 "@opentelemetry/semantic-conventions" "1.4.0" 1697 "@opentelemetry/semantic-conventions" "1.5.0"
1705 semver "^7.3.5" 1698 semver "^7.3.5"
1706 1699
1707"@opentelemetry/instrumentation-pg@^0.30.0": 1700"@opentelemetry/instrumentation-pg@^0.30.0":
@@ -1722,12 +1715,12 @@
1722 "@opentelemetry/instrumentation" "^0.29.2" 1715 "@opentelemetry/instrumentation" "^0.29.2"
1723 "@opentelemetry/semantic-conventions" "^1.0.0" 1716 "@opentelemetry/semantic-conventions" "^1.0.0"
1724 1717
1725"@opentelemetry/instrumentation@0.30.0", "@opentelemetry/instrumentation@^0.30.0": 1718"@opentelemetry/instrumentation@0.31.0", "@opentelemetry/instrumentation@^0.31.0":
1726 version "0.30.0" 1719 version "0.31.0"
1727 resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.30.0.tgz#97cca611bd276439cc4e01e0516e50cbbb1e3459" 1720 resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.31.0.tgz#bee0052a86e22f57be3901c44234f1a210bcfda8"
1728 integrity sha512-9bjRx81B6wbJ7CGWc/WCUfcb0QIG5UIcjnPTzwYIURjYPd8d0ZzRlrnqEdQG62jn4lSPEvnNqTlyC7qXtn9nAA== 1721 integrity sha512-b2hFebXPtBcut4d81b8Kg6GiCoAS8nxb8kYSronQYAXxwNSetqHwIJ2nKLo1slFH1UWUXn0zi3eDez2Sn/9uMQ==
1729 dependencies: 1722 dependencies:
1730 "@opentelemetry/api-metrics" "0.30.0" 1723 "@opentelemetry/api-metrics" "0.31.0"
1731 require-in-the-middle "^5.0.3" 1724 require-in-the-middle "^5.0.3"
1732 semver "^7.3.2" 1725 semver "^7.3.2"
1733 shimmer "^1.2.1" 1726 shimmer "^1.2.1"
@@ -1756,14 +1749,6 @@
1756 dependencies: 1749 dependencies:
1757 "@opentelemetry/core" "1.5.0" 1750 "@opentelemetry/core" "1.5.0"
1758 1751
1759"@opentelemetry/resources@1.4.0":
1760 version "1.4.0"
1761 resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.4.0.tgz#5e23b0d7976158861059dec17e0ee36a35a5ab85"
1762 integrity sha512-Q3pI5+pCM+Ur7YwK9GbG89UBipwJbfmuzSPAXTw964ZHFzSrz+JAgrETC9rqsUOYdUlj/V7LbRMG5bo72xE0Xw==
1763 dependencies:
1764 "@opentelemetry/core" "1.4.0"
1765 "@opentelemetry/semantic-conventions" "1.4.0"
1766
1767"@opentelemetry/resources@1.5.0", "@opentelemetry/resources@^1.3.1": 1752"@opentelemetry/resources@1.5.0", "@opentelemetry/resources@^1.3.1":
1768 version "1.5.0" 1753 version "1.5.0"
1769 resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.5.0.tgz#ce7fbdaec3494e41bc279ddbed3c478ee2570b03" 1754 resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.5.0.tgz#ce7fbdaec3494e41bc279ddbed3c478ee2570b03"
@@ -1772,14 +1757,14 @@
1772 "@opentelemetry/core" "1.5.0" 1757 "@opentelemetry/core" "1.5.0"
1773 "@opentelemetry/semantic-conventions" "1.5.0" 1758 "@opentelemetry/semantic-conventions" "1.5.0"
1774 1759
1775"@opentelemetry/sdk-metrics-base@0.30.0", "@opentelemetry/sdk-metrics-base@~0.30.0": 1760"@opentelemetry/sdk-metrics-base@0.31.0", "@opentelemetry/sdk-metrics-base@~0.31.0":
1776 version "0.30.0" 1761 version "0.31.0"
1777 resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics-base/-/sdk-metrics-base-0.30.0.tgz#242d9260a89a1ac2bf1e167b3fda758f3883c769" 1762 resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics-base/-/sdk-metrics-base-0.31.0.tgz#f797da702c8d9862a2fff55a1e7c70aa6845e535"
1778 integrity sha512-3BDg1MYDInDyGvy+bSH8OuCX5nsue7omH6Y2eidCGTTDYRPxDmq9tsRJxnTUepoMAvWX+1sTwZ4JqTFmc1z8Mw== 1763 integrity sha512-4R2Bjl3wlqIGcq4bCoI9/pD49ld+tEoM9n85UfFzr/aUe+2huY2jTPq/BP9SVB8d2Zfg7mGTIFeapcEvAdKK7g==
1779 dependencies: 1764 dependencies:
1780 "@opentelemetry/api-metrics" "0.30.0" 1765 "@opentelemetry/api-metrics" "0.31.0"
1781 "@opentelemetry/core" "1.4.0" 1766 "@opentelemetry/core" "1.5.0"
1782 "@opentelemetry/resources" "1.4.0" 1767 "@opentelemetry/resources" "1.5.0"
1783 lodash.merge "4.6.2" 1768 lodash.merge "4.6.2"
1784 1769
1785"@opentelemetry/sdk-trace-base@1.5.0", "@opentelemetry/sdk-trace-base@^1.3.1": 1770"@opentelemetry/sdk-trace-base@1.5.0", "@opentelemetry/sdk-trace-base@^1.3.1":
@@ -1803,11 +1788,6 @@
1803 "@opentelemetry/sdk-trace-base" "1.5.0" 1788 "@opentelemetry/sdk-trace-base" "1.5.0"
1804 semver "^7.3.5" 1789 semver "^7.3.5"
1805 1790
1806"@opentelemetry/semantic-conventions@1.4.0":
1807 version "1.4.0"
1808 resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.4.0.tgz#facf2c67d6063b9918d5a5e3fdf25f3a30d547b6"
1809 integrity sha512-Hzl8soGpmyzja9w3kiFFcYJ7n5HNETpplY6cb67KR4QPlxp4FTTresO06qXHgHDhyIInmbLJXuwARjjpsKYGuQ==
1810
1811"@opentelemetry/semantic-conventions@1.5.0", "@opentelemetry/semantic-conventions@^1.0.0", "@opentelemetry/semantic-conventions@^1.3.1": 1791"@opentelemetry/semantic-conventions@1.5.0", "@opentelemetry/semantic-conventions@^1.0.0", "@opentelemetry/semantic-conventions@^1.3.1":
1812 version "1.5.0" 1792 version "1.5.0"
1813 resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.5.0.tgz#cea9792bfcf556c87ded17c6ac729348697bb632" 1793 resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.5.0.tgz#cea9792bfcf556c87ded17c6ac729348697bb632"