fxa-auth-db-mysql/bin/metrics.js

191 строка
5.0 KiB
JavaScript
Executable File

#!/usr/bin/env node
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict'
var fs = require('fs')
module.exports = {
run: run,
countAccounts: [
'SELECT COUNT(*) AS count',
'FROM accounts',
'WHERE createdAt < ?;'
].join('\n'),
countVerifiedAccounts: [
'SELECT COUNT(*) AS count',
'FROM accounts',
'WHERE createdAt < ?',
'AND emailVerified = true;'
].join('\n'),
countAccountsWithTwoOrMoreDevices: [
'SELECT COUNT(*) AS count',
'FROM (',
' SELECT a.uid',
' FROM accounts AS a',
' INNER JOIN sessionTokens AS s',
' ON a.uid = s.uid',
' WHERE a.createdAt < ?',
' GROUP BY (a.uid)',
' HAVING COUNT(s.tokenId) > 1',
') AS sub;'
].join('\n'),
countAccountsWithThreeOrMoreDevices: [
'SELECT COUNT(*) AS count',
'FROM (',
' SELECT a.uid',
' FROM accounts AS a',
' INNER JOIN sessionTokens AS s',
' ON a.uid = s.uid',
' WHERE a.createdAt < ?',
' GROUP BY (a.uid)',
' HAVING COUNT(s.tokenId) > 2',
') AS sub;'
].join('\n'),
countAccountsWithMobileDevice: [
'SELECT COUNT(DISTINCT a.uid) AS count',
'FROM accounts AS a',
'INNER JOIN sessionTokens AS s',
'ON a.uid = s.uid',
'WHERE a.createdAt < ?',
'AND s.uaDeviceType = \'mobile\';'
].join('\n')
}
if (require.main === module) {
module.exports.run(
parseConfigFile(process.argv[2] || '/etc/gather_basic_metrics.conf')
)
}
function run (config, now) {
now = now || new Date()
var lastMidnight = Date.UTC(
now.getUTCFullYear(),
now.getUTCMonth(),
now.getUTCDate(),
0, 0, 0, 0
)
var os = require('os')
var log = require('../lib/logging')('bin.metrics')
var mysql = require('../lib/db/mysql')(log, require('../db-server').errors)
var self = this
var db
return mysql.connect({
master: {
host: config.General.db_dnsname,
user: config.General.db_username,
password: config.General.db_password,
database: config.General.db_name
},
slave: {
host: config.General.db_dnsname,
user: config.General.db_username,
password: config.General.db_password,
database: config.General.db_name
},
patchKey: 'schema-patch-level'
})
.then(function (result) {
db = result
return db.readMultiple([
{ sql: 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED' },
{ sql: 'START TRANSACTION' },
metricsQuery('countAccounts'),
metricsQuery('countVerifiedAccounts'),
metricsQuery('countAccountsWithTwoOrMoreDevices'),
metricsQuery('countAccountsWithThreeOrMoreDevices'),
metricsQuery('countAccountsWithMobileDevice'),
], { sql: 'COMMIT' })
})
.then(function (results) {
assertResults(results, 2, 6)
fs.appendFileSync('/media/ephemeral0/fxa-admin/basic_metrics.log', JSON.stringify({
hostname: os.hostname(),
pid: process.pid,
op: 'account_totals',
total_accounts: results[2][0].count,
total_verified_accounts: results[3][0].count,
total_accounts_with_two_or_more_devices: results[4][0].count,
total_accounts_with_three_or_more_devices: results[5][0].count,
total_accounts_with_mobile_device: results[6][0].count,
time: (new Date(lastMidnight)).toISOString(),
v: 0
}) + '\n')
db.close()
})
.catch(function (error) {
log.error('metrics.run', error)
db.close()
})
function metricsQuery (queryName) {
return { sql: self[queryName], params: [ lastMidnight ] }
}
}
function assertResults (results, firstIndex, lastIndex) {
results.filter(function (result, index) {
return index >= firstIndex && index <= lastIndex
}).forEach(assertResult)
}
function assertResult (result) {
if (Array.isArray(result) && result.length === 1 && result[0].count >= 0) {
return
}
throw new Error('unexpected metrics query result format, should be [ { count: n } ]')
}
// Very rudimentary parser for the following config file format:
// [General]
// db_dnsname: foo
// db_username: bar
// db_password: baz
// db_name: qux
function parseConfigFile (path) {
var currentSection
return fs.readFileSync(path, { encoding: 'utf8' })
.split('\n')
.map(trim)
.filter(filterConfig)
.reduce(reduceConfig, {})
function reduceConfig (parsed, line) {
var isSectionName = line.match(/^\[(.+)\]$/)
if (isSectionName) {
currentSection = isSectionName[1]
if (! parsed[currentSection]) {
parsed[currentSection] = {}
}
} else {
var setting = line.split(':').map(trim)
if (! currentSection || setting.length === 0 || setting.length > 2) {
throw new Error('unexpected config file format')
}
parsed[currentSection][setting[0]] = setting[1]
}
return parsed
}
}
function trim (string) {
return string.trim()
}
function filterConfig (line) {
return line && line[0] !== '#' && line[0] !== ';'
}