diff options
-rw-r--r-- | server/lib/opentelemetry/metric-helpers/index.ts | 2 | ||||
-rw-r--r-- | server/lib/opentelemetry/metric-helpers/lives-observers-builder.ts | 21 | ||||
-rw-r--r-- | server/lib/opentelemetry/metric-helpers/viewers-observers-builder.ts | 24 | ||||
-rw-r--r-- | server/lib/opentelemetry/metrics.ts | 14 | ||||
-rw-r--r-- | server/lib/views/shared/video-viewer-counters.ts | 31 | ||||
-rw-r--r-- | server/lib/views/video-views-manager.ts | 9 | ||||
-rw-r--r-- | server/middlewares/validators/videos/video-live.ts | 2 | ||||
-rw-r--r-- | server/models/video/video.ts | 17 |
8 files changed, 106 insertions, 14 deletions
diff --git a/server/lib/opentelemetry/metric-helpers/index.ts b/server/lib/opentelemetry/metric-helpers/index.ts index ff0aff9fd..1b3813743 100644 --- a/server/lib/opentelemetry/metric-helpers/index.ts +++ b/server/lib/opentelemetry/metric-helpers/index.ts | |||
@@ -1,3 +1,5 @@ | |||
1 | export * from './lives-observers-builder' | ||
1 | export * from './job-queue-observers-builder' | 2 | export * from './job-queue-observers-builder' |
2 | export * from './nodejs-observers-builder' | 3 | export * from './nodejs-observers-builder' |
3 | export * from './stats-observers-builder' | 4 | export * from './stats-observers-builder' |
5 | export * from './viewers-observers-builder' | ||
diff --git a/server/lib/opentelemetry/metric-helpers/lives-observers-builder.ts b/server/lib/opentelemetry/metric-helpers/lives-observers-builder.ts new file mode 100644 index 000000000..e27bd8548 --- /dev/null +++ b/server/lib/opentelemetry/metric-helpers/lives-observers-builder.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | import { Meter } from '@opentelemetry/api-metrics' | ||
2 | import { VideoModel } from '@server/models/video/video' | ||
3 | |||
4 | export class LivesObserversBuilder { | ||
5 | |||
6 | constructor (private readonly meter: Meter) { | ||
7 | |||
8 | } | ||
9 | |||
10 | buildObservers () { | ||
11 | this.meter.createObservableGauge('peertube_running_lives_total', { | ||
12 | description: 'Total running lives on the instance' | ||
13 | }).addCallback(async observableResult => { | ||
14 | const local = await VideoModel.countLives({ remote: false, mode: 'published' }) | ||
15 | const remote = await VideoModel.countLives({ remote: true, mode: 'published' }) | ||
16 | |||
17 | observableResult.observe(local, { liveOrigin: 'local' }) | ||
18 | observableResult.observe(remote, { liveOrigin: 'remote' }) | ||
19 | }) | ||
20 | } | ||
21 | } | ||
diff --git a/server/lib/opentelemetry/metric-helpers/viewers-observers-builder.ts b/server/lib/opentelemetry/metric-helpers/viewers-observers-builder.ts new file mode 100644 index 000000000..634e5bbc9 --- /dev/null +++ b/server/lib/opentelemetry/metric-helpers/viewers-observers-builder.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import { Meter } from '@opentelemetry/api-metrics' | ||
2 | import { VideoScope, ViewerScope } from '@server/lib/views/shared' | ||
3 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' | ||
4 | |||
5 | export class ViewersObserversBuilder { | ||
6 | |||
7 | constructor (private readonly meter: Meter) { | ||
8 | |||
9 | } | ||
10 | |||
11 | buildObservers () { | ||
12 | this.meter.createObservableGauge('peertube_viewers_total', { | ||
13 | description: 'Total viewers on the instance' | ||
14 | }).addCallback(observableResult => { | ||
15 | for (const viewerScope of [ 'local', 'remote' ] as ViewerScope[]) { | ||
16 | for (const videoScope of [ 'local', 'remote' ] as VideoScope[]) { | ||
17 | const result = VideoViewsManager.Instance.getTotalViewers({ viewerScope, videoScope }) | ||
18 | |||
19 | observableResult.observe(result, { viewerOrigin: viewerScope, videoOrigin: videoScope }) | ||
20 | } | ||
21 | } | ||
22 | }) | ||
23 | } | ||
24 | } | ||
diff --git a/server/lib/opentelemetry/metrics.ts b/server/lib/opentelemetry/metrics.ts index 149f421be..ffe493670 100644 --- a/server/lib/opentelemetry/metrics.ts +++ b/server/lib/opentelemetry/metrics.ts | |||
@@ -4,7 +4,13 @@ import { PrometheusExporter } from '@opentelemetry/exporter-prometheus' | |||
4 | import { MeterProvider } from '@opentelemetry/sdk-metrics-base' | 4 | import { MeterProvider } from '@opentelemetry/sdk-metrics-base' |
5 | import { logger } from '@server/helpers/logger' | 5 | import { logger } from '@server/helpers/logger' |
6 | import { CONFIG } from '@server/initializers/config' | 6 | import { CONFIG } from '@server/initializers/config' |
7 | import { JobQueueObserversBuilder, NodeJSObserversBuilder, StatsObserversBuilder } from './metric-helpers' | 7 | import { |
8 | JobQueueObserversBuilder, | ||
9 | LivesObserversBuilder, | ||
10 | NodeJSObserversBuilder, | ||
11 | StatsObserversBuilder, | ||
12 | ViewersObserversBuilder | ||
13 | } from './metric-helpers' | ||
8 | 14 | ||
9 | class OpenTelemetryMetrics { | 15 | class OpenTelemetryMetrics { |
10 | 16 | ||
@@ -53,6 +59,12 @@ class OpenTelemetryMetrics { | |||
53 | 59 | ||
54 | const statsObserversBuilder = new StatsObserversBuilder(this.meter) | 60 | const statsObserversBuilder = new StatsObserversBuilder(this.meter) |
55 | statsObserversBuilder.buildObservers() | 61 | statsObserversBuilder.buildObservers() |
62 | |||
63 | const livesObserversBuilder = new LivesObserversBuilder(this.meter) | ||
64 | livesObserversBuilder.buildObservers() | ||
65 | |||
66 | const viewersObserversBuilder = new ViewersObserversBuilder(this.meter) | ||
67 | viewersObserversBuilder.buildObservers() | ||
56 | } | 68 | } |
57 | 69 | ||
58 | private buildRequestObserver () { | 70 | private buildRequestObserver () { |
diff --git a/server/lib/views/shared/video-viewer-counters.ts b/server/lib/views/shared/video-viewer-counters.ts index f851ce050..f5b83130e 100644 --- a/server/lib/views/shared/video-viewer-counters.ts +++ b/server/lib/views/shared/video-viewer-counters.ts | |||
@@ -10,9 +10,14 @@ import { buildUUID, sha256 } from '@shared/extra-utils' | |||
10 | 10 | ||
11 | const lTags = loggerTagsFactory('views') | 11 | const lTags = loggerTagsFactory('views') |
12 | 12 | ||
13 | export type ViewerScope = 'local' | 'remote' | ||
14 | export type VideoScope = 'local' | 'remote' | ||
15 | |||
13 | type Viewer = { | 16 | type Viewer = { |
14 | expires: number | 17 | expires: number |
15 | id: string | 18 | id: string |
19 | viewerScope: ViewerScope | ||
20 | videoScope: VideoScope | ||
16 | lastFederation?: number | 21 | lastFederation?: number |
17 | } | 22 | } |
18 | 23 | ||
@@ -50,7 +55,7 @@ export class VideoViewerCounters { | |||
50 | return false | 55 | return false |
51 | } | 56 | } |
52 | 57 | ||
53 | const newViewer = await this.addViewerToVideo({ viewerId, video }) | 58 | const newViewer = await this.addViewerToVideo({ viewerId, video, viewerScope: 'local' }) |
54 | await this.federateViewerIfNeeded(video, newViewer) | 59 | await this.federateViewerIfNeeded(video, newViewer) |
55 | 60 | ||
56 | return true | 61 | return true |
@@ -65,13 +70,26 @@ export class VideoViewerCounters { | |||
65 | 70 | ||
66 | logger.debug('Adding remote viewer to video %s.', video.uuid, { ...lTags(video.uuid) }) | 71 | logger.debug('Adding remote viewer to video %s.', video.uuid, { ...lTags(video.uuid) }) |
67 | 72 | ||
68 | await this.addViewerToVideo({ video, viewerExpires, viewerId }) | 73 | await this.addViewerToVideo({ video, viewerExpires, viewerId, viewerScope: 'remote' }) |
69 | 74 | ||
70 | return true | 75 | return true |
71 | } | 76 | } |
72 | 77 | ||
73 | // --------------------------------------------------------------------------- | 78 | // --------------------------------------------------------------------------- |
74 | 79 | ||
80 | getTotalViewers (options: { | ||
81 | viewerScope: ViewerScope | ||
82 | videoScope: VideoScope | ||
83 | }) { | ||
84 | let total = 0 | ||
85 | |||
86 | for (const viewers of this.viewersPerVideo.values()) { | ||
87 | total += viewers.filter(v => v.viewerScope === options.viewerScope && v.videoScope === options.videoScope).length | ||
88 | } | ||
89 | |||
90 | return total | ||
91 | } | ||
92 | |||
75 | getViewers (video: MVideo) { | 93 | getViewers (video: MVideo) { |
76 | const viewers = this.viewersPerVideo.get(video.id) | 94 | const viewers = this.viewersPerVideo.get(video.id) |
77 | if (!viewers) return 0 | 95 | if (!viewers) return 0 |
@@ -88,9 +106,10 @@ export class VideoViewerCounters { | |||
88 | private async addViewerToVideo (options: { | 106 | private async addViewerToVideo (options: { |
89 | video: MVideoImmutable | 107 | video: MVideoImmutable |
90 | viewerId: string | 108 | viewerId: string |
109 | viewerScope: ViewerScope | ||
91 | viewerExpires?: Date | 110 | viewerExpires?: Date |
92 | }) { | 111 | }) { |
93 | const { video, viewerExpires, viewerId } = options | 112 | const { video, viewerExpires, viewerId, viewerScope } = options |
94 | 113 | ||
95 | let watchers = this.viewersPerVideo.get(video.id) | 114 | let watchers = this.viewersPerVideo.get(video.id) |
96 | 115 | ||
@@ -103,7 +122,11 @@ export class VideoViewerCounters { | |||
103 | ? viewerExpires.getTime() | 122 | ? viewerExpires.getTime() |
104 | : this.buildViewerExpireTime() | 123 | : this.buildViewerExpireTime() |
105 | 124 | ||
106 | const viewer = { id: viewerId, expires } | 125 | const videoScope: VideoScope = video.remote |
126 | ? 'remote' | ||
127 | : 'local' | ||
128 | |||
129 | const viewer = { id: viewerId, expires, videoScope, viewerScope } | ||
107 | watchers.push(viewer) | 130 | watchers.push(viewer) |
108 | 131 | ||
109 | this.idToViewer.set(viewerId, viewer) | 132 | this.idToViewer.set(viewerId, viewer) |
diff --git a/server/lib/views/video-views-manager.ts b/server/lib/views/video-views-manager.ts index 86758e8d8..c088dad5e 100644 --- a/server/lib/views/video-views-manager.ts +++ b/server/lib/views/video-views-manager.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
2 | import { MVideo, MVideoImmutable } from '@server/types/models' | 2 | import { MVideo, MVideoImmutable } from '@server/types/models' |
3 | import { VideoViewEvent } from '@shared/models' | 3 | import { VideoViewEvent } from '@shared/models' |
4 | import { VideoViewerCounters, VideoViewerStats, VideoViews } from './shared' | 4 | import { VideoScope, VideoViewerCounters, VideoViewerStats, VideoViews, ViewerScope } from './shared' |
5 | 5 | ||
6 | /** | 6 | /** |
7 | * If processing a local view: | 7 | * If processing a local view: |
@@ -79,6 +79,13 @@ export class VideoViewsManager { | |||
79 | return this.videoViewerCounters.getViewers(video) | 79 | return this.videoViewerCounters.getViewers(video) |
80 | } | 80 | } |
81 | 81 | ||
82 | getTotalViewers (options: { | ||
83 | viewerScope: ViewerScope | ||
84 | videoScope: VideoScope | ||
85 | }) { | ||
86 | return this.videoViewerCounters.getTotalViewers(options) | ||
87 | } | ||
88 | |||
82 | buildViewerExpireTime () { | 89 | buildViewerExpireTime () { |
83 | return this.videoViewerCounters.buildViewerExpireTime() | 90 | return this.videoViewerCounters.buildViewerExpireTime() |
84 | } | 91 | } |
diff --git a/server/middlewares/validators/videos/video-live.ts b/server/middlewares/validators/videos/video-live.ts index 59638d5e0..777b57e9a 100644 --- a/server/middlewares/validators/videos/video-live.ts +++ b/server/middlewares/validators/videos/video-live.ts | |||
@@ -119,7 +119,7 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([ | |||
119 | if (!await doesVideoChannelOfAccountExist(body.channelId, user, res)) return cleanUpReqFiles(req) | 119 | if (!await doesVideoChannelOfAccountExist(body.channelId, user, res)) return cleanUpReqFiles(req) |
120 | 120 | ||
121 | if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) { | 121 | if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) { |
122 | const totalInstanceLives = await VideoModel.countLocalLives() | 122 | const totalInstanceLives = await VideoModel.countLives({ remote: false, mode: 'not-ended' }) |
123 | 123 | ||
124 | if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) { | 124 | if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) { |
125 | cleanUpReqFiles(req) | 125 | cleanUpReqFiles(req) |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 27e605be6..924f12a5e 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1209,18 +1209,21 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1209 | return VideoModel.getAvailableForApi(queryOptions) | 1209 | return VideoModel.getAvailableForApi(queryOptions) |
1210 | } | 1210 | } |
1211 | 1211 | ||
1212 | static countLocalLives () { | 1212 | static countLives (options: { |
1213 | const options = { | 1213 | remote: boolean |
1214 | mode: 'published' | 'not-ended' | ||
1215 | }) { | ||
1216 | const query = { | ||
1214 | where: { | 1217 | where: { |
1215 | remote: false, | 1218 | remote: options.remote, |
1216 | isLive: true, | 1219 | isLive: true, |
1217 | state: { | 1220 | state: options.mode === 'not-ended' |
1218 | [Op.ne]: VideoState.LIVE_ENDED | 1221 | ? { [Op.ne]: VideoState.LIVE_ENDED } |
1219 | } | 1222 | : { [Op.eq]: VideoState.PUBLISHED } |
1220 | } | 1223 | } |
1221 | } | 1224 | } |
1222 | 1225 | ||
1223 | return VideoModel.count(options) | 1226 | return VideoModel.count(query) |
1224 | } | 1227 | } |
1225 | 1228 | ||
1226 | static countVideosUploadedByUserSince (userId: number, since: Date) { | 1229 | static countVideosUploadedByUserSince (userId: number, since: Date) { |