Add stats route

pull/322/head
Chocobozzz 2018-02-28 18:04:46 +01:00
parent 22b59e8099
commit 09cababd79
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
26 changed files with 268 additions and 42 deletions

View File

@ -13,7 +13,7 @@ import {
TRANSCODING_THREADS TRANSCODING_THREADS
} from '@app/shared/forms/form-validators/custom-config' } from '@app/shared/forms/form-validators/custom-config'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { CustomConfig } from '../../../../../../shared/models/config/custom-config.model' import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
@Component({ @Component({
selector: 'my-edit-custom-config', selector: 'my-edit-custom-config',

View File

@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http' import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { CustomConfig } from '../../../../../../shared/models/config/custom-config.model' import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
import { environment } from '../../../../environments/environment' import { environment } from '../../../../environments/environment'
import { RestExtractor, RestService } from '../../../shared' import { RestExtractor, RestService } from '../../../shared'

View File

@ -6,7 +6,7 @@ import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable' import { Observable } from 'rxjs/Observable'
import { ResultList } from '../../../../../../shared' import { ResultList } from '../../../../../../shared'
import { JobState } from '../../../../../../shared/models' import { JobState } from '../../../../../../shared/models'
import { Job } from '../../../../../../shared/models/job.model' import { Job } from '../../../../../../shared/models/server/job.model'
import { environment } from '../../../../environments/environment' import { environment } from '../../../../environments/environment'
import { RestExtractor, RestPagination, RestService } from '../../../shared' import { RestExtractor, RestPagination, RestService } from '../../../shared'

View File

@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'
import 'rxjs/add/operator/do' import 'rxjs/add/operator/do'
import { ReplaySubject } from 'rxjs/ReplaySubject' import { ReplaySubject } from 'rxjs/ReplaySubject'
import { ServerConfig } from '../../../../../shared' import { ServerConfig } from '../../../../../shared'
import { About } from '../../../../../shared/models/config/about.model' import { About } from '../../../../../shared/models/server/about.model'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
@Injectable() @Injectable()

View File

@ -56,6 +56,8 @@ async function inboxController (req: express.Request, res: express.Response, nex
specificActor = res.locals.videoChannel specificActor = res.locals.videoChannel
} }
logger.info('Receiving inbox requests for %d activities by %s.', activities.length, res.locals.signature.actor)
await processActivities(activities, res.locals.signature.actor, specificActor) await processActivities(activities, res.locals.signature.actor, specificActor)
res.status(204).end() res.status(204).end()

View File

@ -1,8 +1,8 @@
import * as express from 'express' import * as express from 'express'
import { omit } from 'lodash' import { omit } from 'lodash'
import { ServerConfig, UserRight } from '../../../shared' import { ServerConfig, UserRight } from '../../../shared'
import { About } from '../../../shared/models/config/about.model' import { About } from '../../../shared/models/server/about.model'
import { CustomConfig } from '../../../shared/models/config/custom-config.model' import { CustomConfig } from '../../../shared/models/server/custom-config.model'
import { unlinkPromise, writeFilePromise } from '../../helpers/core-utils' import { unlinkPromise, writeFilePromise } from '../../helpers/core-utils'
import { isSignupAllowed } from '../../helpers/utils' import { isSignupAllowed } from '../../helpers/utils'
import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers' import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers'

View File

@ -1,9 +1,11 @@
import * as express from 'express' import * as express from 'express'
import { serverFollowsRouter } from './follows' import { serverFollowsRouter } from './follows'
import { statsRouter } from './stats'
const serverRouter = express.Router() const serverRouter = express.Router()
serverRouter.use('/', serverFollowsRouter) serverRouter.use('/', serverFollowsRouter)
serverRouter.use('/', statsRouter)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -0,0 +1,39 @@
import * as express from 'express'
import { ServerStats } from '../../../../shared/models/server/server-stats.model'
import { asyncMiddleware } from '../../../middlewares'
import { UserModel } from '../../../models/account/user'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { VideoModel } from '../../../models/video/video'
import { VideoCommentModel } from '../../../models/video/video-comment'
const statsRouter = express.Router()
statsRouter.get('/stats',
asyncMiddleware(getStats)
)
async function getStats (req: express.Request, res: express.Response, next: express.NextFunction) {
const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats()
const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats()
const { totalUsers } = await UserModel.getStats()
const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats()
const data: ServerStats = {
totalLocalVideos,
totalLocalVideoViews,
totalVideos,
totalLocalVideoComments,
totalVideoComments,
totalUsers,
totalInstanceFollowers,
totalInstanceFollowing
}
return res.json(data).end()
}
// ---------------------------------------------------------------------------
export {
statsRouter
}

View File

@ -13,6 +13,7 @@ import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
import { OAuthTokenModel } from '../oauth/oauth-token' import { OAuthTokenModel } from '../oauth/oauth-token'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
import { VideoChannelModel } from '../video/video-channel' import { VideoChannelModel } from '../video/video-channel'
import { VideoCommentModel } from '../video/video-comment'
import { AccountModel } from './account' import { AccountModel } from './account'
@DefaultScope({ @DefaultScope({
@ -226,6 +227,14 @@ export class UserModel extends Model<UserModel> {
}) })
} }
static async getStats () {
const totalUsers = await UserModel.count()
return {
totalUsers
}
}
hasRight (right: UserRight) { hasRight (right: UserRight) {
return hasUserRight(this.role, right) return hasUserRight(this.role, right)
} }

View File

@ -8,6 +8,7 @@ import {
import { FollowState } from '../../../shared/models/actors' import { FollowState } from '../../../shared/models/actors'
import { AccountFollow } from '../../../shared/models/actors/follow.model' import { AccountFollow } from '../../../shared/models/actors/follow.model'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { getServerActor } from '../../helpers/utils'
import { ACTOR_FOLLOW_SCORE } from '../../initializers' import { ACTOR_FOLLOW_SCORE } from '../../initializers'
import { FOLLOW_STATES } from '../../initializers/constants' import { FOLLOW_STATES } from '../../initializers/constants'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
@ -182,34 +183,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
return ActorFollowModel.findOne(query) return ActorFollowModel.findOne(query)
} }
static loadByFollowerInbox (url: string, t?: Sequelize.Transaction) {
const query = {
where: {
state: 'accepted'
},
include: [
{
model: ActorModel,
required: true,
as: 'ActorFollower',
where: {
[Sequelize.Op.or]: [
{
inboxUrl: url
},
{
sharedInboxUrl: url
}
]
}
}
],
transaction: t
} as any // FIXME: typings does not work
return ActorFollowModel.findOne(query)
}
static listFollowingForApi (id: number, start: number, count: number, sort: string) { static listFollowingForApi (id: number, start: number, count: number, sort: string) {
const query = { const query = {
distinct: true, distinct: true,
@ -296,6 +269,27 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count) return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
} }
static async getStats () {
const serverActor = await getServerActor()
const totalInstanceFollowing = await ActorFollowModel.count({
where: {
actorId: serverActor.id
}
})
const totalInstanceFollowers = await ActorFollowModel.count({
where: {
targetActorId: serverActor.id
}
})
return {
totalInstanceFollowing,
totalInstanceFollowers
}
}
private static async createListAcceptedFollowForApiQuery ( private static async createListAcceptedFollowForApiQuery (
type: 'followers' | 'following', type: 'followers' | 'following',
actorIds: number[], actorIds: number[],

View File

@ -326,6 +326,32 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
.findAll(query) .findAll(query)
} }
static async getStats () {
const totalLocalVideoComments = await VideoCommentModel.count({
include: [
{
model: AccountModel,
required: true,
include: [
{
model: ActorModel,
required: true,
where: {
serverId: null
}
}
]
}
]
})
const totalVideoComments = await VideoCommentModel.count()
return {
totalLocalVideoComments,
totalVideoComments
}
}
getThreadId (): number { getThreadId (): number {
return this.originCommentId || this.id return this.originCommentId || this.id
} }

View File

@ -761,6 +761,29 @@ export class VideoModel extends Model<VideoModel> {
.findOne(options) .findOne(options)
} }
static async getStats () {
const totalLocalVideos = await VideoModel.count({
where: {
remote: false
}
})
const totalVideos = await VideoModel.count()
let totalLocalVideoViews = await VideoModel.sum('views', {
where: {
remote: false
}
})
// Sequelize could return null...
if (!totalLocalVideoViews) totalLocalVideoViews = 0
return {
totalLocalVideos,
totalLocalVideoViews,
totalVideos
}
}
getOriginalFile () { getOriginalFile () {
if (Array.isArray(this.VideoFiles) === false) return undefined if (Array.isArray(this.VideoFiles) === false) return undefined

View File

@ -2,7 +2,7 @@
import { omit } from 'lodash' import { omit } from 'lodash'
import 'mocha' import 'mocha'
import { CustomConfig } from '../../../../shared/models/config/custom-config.model' import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
import { import {
createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo, createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo,

View File

@ -1,5 +1,5 @@
// Order of the tests we want to execute // Order of the tests we want to execute
import './server/config' import './server/stats'
import './check-params' import './check-params'
import './users/users' import './users/users'
import './videos/single-server' import './videos/single-server'
@ -10,3 +10,4 @@ import './videos/video-description'
import './videos/video-privacy' import './videos/video-privacy'
import './videos/services' import './videos/services'
import './server/email' import './server/email'
import './server/config'

View File

@ -1,5 +1,4 @@
// Order of the tests we want to execute // Order of the tests we want to execute
// import './multiple-servers'
import './videos/video-transcoder' import './videos/video-transcoder'
import './videos/multiple-servers' import './videos/multiple-servers'
import './server/follows' import './server/follows'

View File

@ -2,8 +2,8 @@
import 'mocha' import 'mocha'
import * as chai from 'chai' import * as chai from 'chai'
import { About } from '../../../../shared/models/config/about.model' import { About } from '../../../../shared/models/server/about.model'
import { CustomConfig } from '../../../../shared/models/config/custom-config.model' import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
import { deleteCustomConfig, getAbout, killallServers, reRunServer } from '../../utils' import { deleteCustomConfig, getAbout, killallServers, reRunServer } from '../../utils'
const expect = chai.expect const expect = chai.expect

View File

@ -0,0 +1,102 @@
/* tslint:disable:no-unused-expression */
import * as chai from 'chai'
import 'mocha'
import { ServerStats } from '../../../../shared/models/server/server-stats.model'
import {
createUser,
doubleFollow,
flushAndRunMultipleServers,
follow,
killallServers,
ServerInfo,
uploadVideo,
viewVideo,
wait
} from '../../utils'
import { flushTests, setAccessTokensToServers } from '../../utils/index'
import { getStats } from '../../utils/server/stats'
import { addVideoCommentThread } from '../../utils/videos/video-comments'
const expect = chai.expect
describe('Test stats', function () {
let servers: ServerInfo[] = []
before(async function () {
this.timeout(60000)
await flushTests()
servers = await flushAndRunMultipleServers(3)
await setAccessTokensToServers(servers)
await doubleFollow(servers[0], servers[1])
const user = {
username: 'user1',
password: 'super_password'
}
await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, {})
const videoUUID = resVideo.body.video.uuid
await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'comment')
await viewVideo(servers[0].url, videoUUID)
await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
await wait(5000)
})
it('Should have the correct stats on instance 1', async function () {
const res = await getStats(servers[0].url)
const data: ServerStats = res.body
expect(data.totalLocalVideoComments).to.equal(1)
expect(data.totalLocalVideos).to.equal(1)
expect(data.totalLocalVideoViews).to.equal(1)
expect(data.totalUsers).to.equal(2)
expect(data.totalVideoComments).to.equal(1)
expect(data.totalVideos).to.equal(1)
expect(data.totalInstanceFollowers).to.equal(2)
expect(data.totalInstanceFollowing).to.equal(1)
})
it('Should have the correct stats on instance 2', async function () {
const res = await getStats(servers[1].url)
const data: ServerStats = res.body
expect(data.totalLocalVideoComments).to.equal(0)
expect(data.totalLocalVideos).to.equal(0)
expect(data.totalLocalVideoViews).to.equal(0)
expect(data.totalUsers).to.equal(1)
expect(data.totalVideoComments).to.equal(1)
expect(data.totalVideos).to.equal(1)
expect(data.totalInstanceFollowers).to.equal(1)
expect(data.totalInstanceFollowing).to.equal(1)
})
it('Should have the correct stats on instance 3', async function () {
const res = await getStats(servers[2].url)
const data: ServerStats = res.body
expect(data.totalLocalVideoComments).to.equal(0)
expect(data.totalLocalVideos).to.equal(0)
expect(data.totalLocalVideoViews).to.equal(0)
expect(data.totalUsers).to.equal(1)
expect(data.totalVideoComments).to.equal(1)
expect(data.totalVideos).to.equal(1)
expect(data.totalInstanceFollowing).to.equal(1)
expect(data.totalInstanceFollowers).to.equal(0)
})
after(async function () {
killallServers(servers)
// Keep the logs if the test failed
if (this['ok']) {
await flushTests()
}
})
})

View File

@ -1,5 +1,5 @@
import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../' import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../'
import { CustomConfig } from '../../../../shared/models/config/custom-config.model' import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
function getConfig (url: string) { function getConfig (url: string) {
const path = '/api/v1/config' const path = '/api/v1/config'

View File

@ -0,0 +1,17 @@
import { makeGetRequest } from '../'
function getStats (url: string) {
const path = '/api/v1/server/stats'
return makeGetRequest({
url,
path,
statusCodeExpected: 200
})
}
// ---------------------------------------------------------------------------
export {
getStats
}

View File

@ -2,7 +2,7 @@ export * from './actors'
export * from './activitypub' export * from './activitypub'
export * from './users' export * from './users'
export * from './videos' export * from './videos'
export * from './job.model' export * from './server/job.model'
export * from './oauth-client-local.model' export * from './oauth-client-local.model'
export * from './result-list.model' export * from './result-list.model'
export * from './config/server-config.model' export * from './server/server-config.model'

View File

@ -0,0 +1,12 @@
export interface ServerStats {
totalUsers: number
totalLocalVideos: number
totalLocalVideoViews: number
totalLocalVideoComments: number
totalVideos: number
totalVideoComments: number
totalInstanceFollowers: number
totalInstanceFollowing: number
}