├── demo ├── grafanaConfig │ ├── .gitignore │ └── grafana.db ├── prometheusConfig │ └── prometheus.yml └── sampleApp │ └── index.html ├── serverSrc ├── .flowconfig ├── config.js ├── readMetricConfigs.js ├── aggregators │ ├── counter.js │ ├── util.js │ ├── __tests__ │ │ ├── counterTest.js │ │ └── histogramTest.js │ └── histogram.js ├── makeMetricsAggregator.js ├── server.js └── __tests__ │ └── reportingTests.js ├── SweetSlackOps.png ├── navigationtiming.png ├── PrometheusUserMonitoringArchitecture.png ├── runDevServer.sh ├── serverPackage.json ├── config ├── .babelrc └── metricConfigs │ ├── appMetricConfig.json │ └── performanceTimingMetricConfig.json ├── Dockerfile ├── Dockerfile-dev ├── docker-compose.yaml ├── clientSrc └── aggregatorClient.js ├── README.md └── LICENSE /demo/grafanaConfig/.gitignore: -------------------------------------------------------------------------------- 1 | sessions -------------------------------------------------------------------------------- /serverSrc/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | -------------------------------------------------------------------------------- /SweetSlackOps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peardeck/prometheus-user-metrics/HEAD/SweetSlackOps.png -------------------------------------------------------------------------------- /navigationtiming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peardeck/prometheus-user-metrics/HEAD/navigationtiming.png -------------------------------------------------------------------------------- /demo/grafanaConfig/grafana.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peardeck/prometheus-user-metrics/HEAD/demo/grafanaConfig/grafana.db -------------------------------------------------------------------------------- /PrometheusUserMonitoringArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peardeck/prometheus-user-metrics/HEAD/PrometheusUserMonitoringArchitecture.png -------------------------------------------------------------------------------- /runDevServer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | babel clientSrc/aggregatorClient.js --watch --out-file static/aggregatorClient.js & 4 | babel-watch serverSrc/server.js -------------------------------------------------------------------------------- /demo/prometheusConfig/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | # rule_files: 6 | # - /etc/config/rules 7 | # - /etc/config/alerts 8 | 9 | scrape_configs: 10 | - job_name: metrics-aggregator 11 | static_configs: 12 | - targets: 13 | - metrics-aggregator-dev-server:9102 -------------------------------------------------------------------------------- /serverPackage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metrics-aggregator", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "body-parser": "^1.17.0", 13 | "express": "^4.15.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /config/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015" ], 3 | "plugins": [ 4 | "transform-object-rest-spread", 5 | "syntax-async-functions", 6 | "transform-regenerator", 7 | "babel-plugin-transform-flow-strip-types", 8 | ["babel-plugin-transform-builtin-extend", { 9 | "globals": ["Error", "Array"] 10 | }] 11 | ] 12 | } -------------------------------------------------------------------------------- /serverSrc/config.js: -------------------------------------------------------------------------------- 1 | function env(key) { 2 | var val = process.env[key]; 3 | if (val && (val != 'false') && (val != 'null')) { 4 | return val; 5 | } else if (val === 'false') { 6 | return false; 7 | } else if (val === 'null') { 8 | return null; 9 | } else { 10 | throw new Error(`Missing required env variable ${key}`) 11 | } 12 | } 13 | 14 | export const configPath = env('PUM_CONFIG_PATH'); -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6 2 | 3 | RUN apt-get update && apt-get install -y libelf-dev python-all libicu-dev 4 | RUN node --version 5 | RUN npm --version 6 | 7 | RUN npm install -g babel-cli 8 | RUN npm install -g babel-watch 9 | RUN npm install -g babel-core 10 | RUN npm install -g babel-loader 11 | RUN npm install -g babel-register 12 | 13 | WORKDIR /stage 14 | 15 | RUN npm install babel-plugin-syntax-async-functions 16 | RUN npm install babel-plugin-transform-builtin-extend 17 | RUN npm install babel-plugin-transform-flow-strip-types 18 | RUN npm install babel-plugin-transform-object-rest-spread 19 | RUN npm install babel-plugin-transform-regenerator 20 | RUN npm install babel-preset-es2015 21 | 22 | COPY ./config/.babelrc /stage/.babelrc 23 | 24 | 25 | COPY ./serverPackage.json /stage/package.json 26 | 27 | RUN npm install 28 | 29 | RUN mkdir /stage/static 30 | RUN mkdir /stage/built 31 | 32 | # Build the server and put it in /stage/built 33 | COPY ./serverSrc /stage/serverSrc 34 | RUN babel --out-dir /stage/built /stage/serverSrc 35 | 36 | # Build the client lib and put it in /stage/static 37 | COPY ./clientSrc /stage/clientSrc 38 | RUN babel --out-file /stage/static/aggregatorClient.js /stage/clientSrc/aggregatorClient.js 39 | 40 | WORKDIR /stage/built 41 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /Dockerfile-dev: -------------------------------------------------------------------------------- 1 | FROM node:6 2 | 3 | RUN apt-get update && apt-get install -y libelf-dev python-all libicu-dev 4 | RUN node --version 5 | RUN npm --version 6 | 7 | RUN npm install -g babel-cli 8 | RUN npm install -g babel-watch 9 | RUN npm install -g babel-core 10 | RUN npm install -g babel-loader 11 | RUN npm install -g babel-register 12 | RUN npm install -g jest 13 | 14 | WORKDIR /stage 15 | 16 | RUN npm install babel-plugin-syntax-async-functions 17 | RUN npm install babel-plugin-transform-builtin-extend 18 | RUN npm install babel-plugin-transform-flow-strip-types 19 | RUN npm install babel-plugin-transform-object-rest-spread 20 | RUN npm install babel-plugin-transform-regenerator 21 | RUN npm install babel-preset-es2015 22 | 23 | RUN npm install jest-cli babel-jest 24 | 25 | COPY ./config/.babelrc /stage/.babelrc 26 | 27 | 28 | COPY ./serverPackage.json /stage/package.json 29 | 30 | RUN npm install 31 | 32 | RUN mkdir /stage/static 33 | 34 | COPY ./runDevServer.sh /stage/runDevServer.sh 35 | 36 | # At this point /stage is set up with serverPackage.json as package.json 37 | # and all node_modules installed. 38 | # 39 | # Add your own command in docker-compose.yaml, e.g.: 40 | # 1. mount source into /stage/src 41 | # 2. run `babel-watch src/server.js` or 2. run jest --watch 42 | 43 | RUN npm install -g flow-bin@0.40.0 -------------------------------------------------------------------------------- /serverSrc/readMetricConfigs.js: -------------------------------------------------------------------------------- 1 | import { configPath } from './config'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | const builtInConfigs = [ 6 | { 7 | "name": "metrics_aggregator_record_total", 8 | "help": "the number of times clients have posted to /record. Try rate(metrics_aggregator_record_total)*10 for a sense of how many clients are connected.", 9 | "type": "counter", 10 | "labels": [ 11 | ] 12 | } 13 | ]; 14 | 15 | export function readMetricConfigs() { 16 | return new Promise((resolve, reject) => { 17 | 18 | fs.readdir(configPath, (err, fileNames) => { 19 | const readPromises = fileNames.map(readConfigFile); 20 | 21 | Promise.all(readPromises).then((configs) => { 22 | return [].concat.apply([], configs); 23 | }) 24 | .then(resolve, reject); 25 | }); 26 | }) 27 | .then((configsReadFromFiles) => { 28 | return [...configsReadFromFiles, ...builtInConfigs]; 29 | }); 30 | } 31 | 32 | function readConfigFile(fileName) { 33 | return new Promise((resolve, reject) => { 34 | if (/.*\.json/.test(fileName)) { 35 | const spec = require(path.join(configPath, fileName)); 36 | 37 | const configs = spec.allowedMetrics; 38 | 39 | // sort the labels in each config 40 | configs.forEach((config) => { 41 | config.labels = config.labels.sort((a, b) => a.name < b.name ? -1 : 1); 42 | }); 43 | 44 | resolve(configs); 45 | } else { 46 | // not a json file, skip it 47 | resolve([]); 48 | } 49 | }); 50 | } -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | dev-server: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile-dev 7 | command: 8 | - bash 9 | - runDevServer.sh 10 | ports: 11 | - 3000:3000 12 | - 9102:9102 13 | environment: 14 | PUM_CONFIG_PATH: /stage/metricConfigs 15 | volumes: 16 | - ./serverSrc:/stage/serverSrc 17 | - ./clientSrc:/stage/clientSrc 18 | - ./config/metricConfigs:/stage/metricConfigs 19 | 20 | # prod-server: 21 | # build: 22 | # context: . 23 | # dockerfile: Dockerfile 24 | # ports: 25 | # - 3000:3000 26 | # # Note: in production you should not expose port 9102. It is exposed here just to make it easier to manually verify GET :9102/metrics 27 | # - 9102:9102 28 | # environment: 29 | # PUM_CONFIG_PATH: /stage/metricConfigs 30 | # volumes: 31 | # - ./config/metricConfigs:/stage/metricConfigs 32 | 33 | demo-prom: 34 | image: prom/prometheus 35 | command: 36 | - -config.file=/prometheusConfig/prometheus.yml 37 | ports: 38 | - 9090:9090 39 | volumes: 40 | - ./demo/prometheusConfig:/prometheusConfig 41 | links: 42 | - dev-server:metrics-aggregator-dev-server 43 | 44 | demo-grafana: 45 | image: grafana/grafana 46 | ports: 47 | - 3001:3000 48 | volumes: 49 | - ./demo/grafanaConfig:/grafana-data 50 | links: 51 | - demo-prom 52 | environment: 53 | GF_AUTH_BASIC_ENABLED: "false" 54 | GF_AUTH_ANONYMOUS_ENABLED: "true" 55 | GF_AUTH_ANONYMOUS_ORG_ROLE: Admin 56 | GF_PATHS_DATA: /grafana-data 57 | 58 | # This server actually serves the demo code 59 | sample-web-server: 60 | image: nginx 61 | ports: 62 | - 8080:80 63 | volumes: 64 | - ./demo/sampleApp:/usr/share/nginx/html:ro -------------------------------------------------------------------------------- /serverSrc/aggregators/counter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { getLabelPermutations, flattenLabels } from './util'; 4 | import type { CounterMetric, CounterEvent } from './util'; 5 | 6 | export type Counter = { 7 | record: (event: CounterEvent) => void, 8 | report: () => string, 9 | }; 10 | 11 | export function makeCounter(config: CounterMetric) : Counter { 12 | const { name, help, type } = config; 13 | 14 | const allowedLabelPermutations = getLabelPermutations(config.labels); 15 | const counterValues = {}; 16 | allowedLabelPermutations.forEach((permutation) => { 17 | // initialize all allowed permutations to zero. 18 | counterValues[permutation] = 0; 19 | 20 | // later, all other permutations will be rejected, so this 21 | // secures the counters against misbehaving clients who would 22 | // send unknown labels or values and crash poor prometheus. 23 | }); 24 | 25 | return { 26 | record(event: CounterEvent) { 27 | const labelPermutationKey = flattenLabels(event.labels); 28 | if (typeof counterValues[labelPermutationKey] === 'number') { 29 | counterValues[labelPermutationKey] += event.inc; 30 | } else { 31 | console.log(`Disallowed label permutation ${labelPermutationKey}`); 32 | } 33 | }, 34 | 35 | report() { 36 | let headerLines = [ 37 | `# HELP ${name} ${help}`, 38 | `# TYPE ${name} ${type}` 39 | ]; 40 | 41 | let bodyLines = Object.keys(counterValues).map((labelPermutationKey) => { 42 | const value = counterValues[labelPermutationKey]; 43 | return `${name}{${labelPermutationKey}} ${value}`; 44 | }); 45 | 46 | return headerLines.join('\n') + '\n' + bodyLines.join('\n'); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /serverSrc/aggregators/util.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type Label = { 4 | name: string, 5 | allowedValues: Array 6 | }; 7 | 8 | export type Metric = { 9 | name: string, 10 | help: string, 11 | labels: Array