Skip to content

Commit

Permalink
feat: api metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos committed May 17, 2022
1 parent 03f7297 commit 7572044
Show file tree
Hide file tree
Showing 17 changed files with 450 additions and 21 deletions.
1 change: 1 addition & 0 deletions .env.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ DATABASE_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlh

# Postgres Database
DATABASE_CONNECTION=postgresql://postgres:postgres@localhost:5432/postgres
RO_DATABASE_CONNECTION=postgresql://postgres:postgres@localhost:5432/postgres
39 changes: 39 additions & 0 deletions .github/workflows/cron-metrics.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Cron Metrics

on:
schedule:
- cron: '*/20 * * * *'
workflow_dispatch:

jobs:
update:
name: Calculate metrics
runs-on: ubuntu-latest
strategy:
matrix:
env: ['staging', 'production']
timeout-minutes: 20
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Checkout latest cron release tag
run: |
LATEST_TAG=$(git describe --tags --abbrev=0 --match='cron-*')
git checkout $LATEST_TAG
- uses: pnpm/action-setup@v2.0.1
with:
version: 6.32.x
- uses: actions/setup-node@v2
with:
node-version: 16
- run: pnpm install
- name: Run job
env:
DEBUG: '*'
ENV: ${{ matrix.env }}
STAGING_DATABASE_CONNECTION: ${{ secrets.STAGING_DATABASE_CONNECTION }}
STAGING_RO_DATABASE_CONNECTION: ${{ secrets.STAGING_DATABASE_CONNECTION }} # no replica for staging
PROD_DATABASE_CONNECTION: ${{ secrets.PROD_DATABASE_CONNECTION }}
PROD_RO_DATABASE_CONNECTION: ${{ secrets.PROD_RO_DATABASE_CONNECTION }}
run: pnpm --filter cron start
26 changes: 26 additions & 0 deletions .github/workflows/cron.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: cron
on:
push:
branches:
- main
paths:
- 'packages/cron/**'
- '.github/workflows/cron.yml'
pull_request:
paths:
- 'packages/cron/**'
- '.github/workflows/cron.yml'
release:
name: Release
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: GoogleCloudPlatform/release-please-action@v3
id: tag-release
with:
path: packages/cron
token: ${{ secrets.GITHUB_TOKEN }}
release-type: node
monorepo-tags: true
package-name: cron
3 changes: 2 additions & 1 deletion packages/api/db/reset.sql
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
DROP TABLE IF EXISTS perma_cache;
DROP TABLE IF EXISTS perma_cache_event;
DROP TABLE IF EXISTS perma_cache_event;
DROP TABLE IF EXISTS metrics;
9 changes: 9 additions & 0 deletions packages/api/db/tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,12 @@ CREATE TABLE IF NOT EXISTS public.perma_cache_event
);

CREATE INDEX IF NOT EXISTS perma_cache_event_user_id_idx ON perma_cache_event (user_id);

-- Metric contains the current values of collected metrics.
CREATE TABLE IF NOT EXISTS metric
(
name TEXT PRIMARY KEY,
value BIGINT NOT NULL,
inserted_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
2 changes: 2 additions & 0 deletions packages/api/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export const USER_TAGS = {
export const MAX_ALLOWED_URL_LENGTH = 460

export const HTTP_STATUS_CONFLICT = 409

export const METRICS_CACHE_MAX_AGE = 10 * 60 // in seconds (10 minutes)
10 changes: 8 additions & 2 deletions packages/api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ import {
withApiToken,
withSuperHotAuthorized,
} from './auth.js'
import { permaCachePost, permaCacheListGet, permaCacheStatusGet, permaCacheDelete } from './perma-cache/index.js'

import {
permaCachePost,
permaCacheListGet,
permaCacheStatusGet,
permaCacheDelete,
} from './perma-cache/index.js'
import { metricsGet } from './metrics.js'
import { addCorsHeaders, withCorsHeaders } from './cors.js'
import { errorHandler } from './error-handler.js'
import { envAll } from './env.js'
Expand All @@ -24,6 +29,7 @@ const auth = {

router
.all('*', envAll)
.get('/metrics', withCorsHeaders(metricsGet))
.get('/test', async (request, env, ctx) => {
const r = await env.SUPERHOT.get('0.csv')
return new Response(r.body)
Expand Down
50 changes: 50 additions & 0 deletions packages/api/src/metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* global Response caches */

import { METRICS_CACHE_MAX_AGE } from './constants.js'

/**
* Retrieve metrics in prometheus exposition format.
* https://prometheus.io/docs/instrumenting/exposition_formats/
* @param {Request} request
* @param {import('./env').Env} env
* @param {import('./index').Ctx} ctx
* @returns {Promise<Response>}
*/
export async function metricsGet(request, env, ctx) {
const cache = caches.default
let res = await cache.match(request)
if (res) {
return res
}

const [usersTotal, urlsTotal, eventsTotal, sizeTotal] = await Promise.all([
env.db.getMetricsValue('users_total'),
env.db.getMetricsValue('urls_total'),
env.db.getMetricsValue('events_total'),
env.db.getMetricsValue('size_total'),
])

const metrics = [
`# HELP nftlinkapi_permacache_urls_total Total perma cached urls.`,
`# TYPE nftlinkapi_permacache_urls_total counter`,
`nftlinkapi_permacache_urls_total ${urlsTotal}`,
`# HELP nftlinkapi_permacache_users_total Total number of users with perma cached urls.`,
`# TYPE nftlinkapi_permacache_users_total counter`,
`nftlinkapi_permacache_users_total ${usersTotal}`,
`# HELP nftlinkapi_permacache_size_total Total perma cached size.`,
`# TYPE nftlinkapi_permacache_size_total counter`,
`nftlinkapi_permacache_size_total ${sizeTotal}`,
`# HELP nftlinkapi_permacache_events_total Total perma cache events.`,
`# TYPE nftlinkapi_permacache_events_total counter`,
`nftlinkapi_permacache_events_total ${eventsTotal}`,
].join('\n')

res = new Response(metrics, {
headers: {
'Cache-Control': `public, max-age=${METRICS_CACHE_MAX_AGE}`,
},
})
ctx.waitUntil(cache.put(request, res.clone()))

return res
}
20 changes: 20 additions & 0 deletions packages/api/src/utils/db-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,26 @@ export class DBClient {
return data || 0
}

/**
* Get metrics for a given key.
*
* @param {string} key
*/
async getMetricsValue(key) {
const query = this._client.from('metric')
const { data, error } = await query.select('value').eq('name', key)

if (error) {
throw new DBError(error)
}

if (!data || !data.length) {
return 0
}

return data[0].value
}

/**
* Get user by did
*
Expand Down
25 changes: 25 additions & 0 deletions packages/api/test/metrics.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import test from 'ava'

import { getMiniflare } from './scripts/utils.js'

test.beforeEach((t) => {
// Create a new Miniflare environment for each test
t.context = {
mf: getMiniflare(),
}
})

test('Gets metrics content when empty state', async (t) => {
const { mf } = t.context

const response = await mf.dispatchFetch('https://localhost:8787/metrics')
const metricsResponse = await response.text()

t.is(metricsResponse.includes('nftlinkapi_permacache_urls_total 0'), true)
t.is(metricsResponse.includes('nftlinkapi_permacache_users_total 0'), true)
t.is(metricsResponse.includes('nftlinkapi_permacache_size_total 0'), true)
t.is(
metricsResponse.includes('nftlinkapi_permacache_purchases_total 0'),
true
)
})
26 changes: 26 additions & 0 deletions packages/cron/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<h1 align="center">⁂<br/>nftstorage.link</h1>
<p align="center">The cron jobs for housekeeping ✨</p>

## Getting started

Ensure you have all the dependencies, by running `pnpm i` in the root project.

The following jobs are available:

### metrics

Verify that the following are set in the `.env` file in root of the project monorepo.

```ini
ENV=dev

DATABASE_URL=http://localhost:3000
DATABASE_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYwMzk2ODgzNCwiZXhwIjoyNTUwNjUzNjM0LCJyb2xlIjoic2VydmljZV9yb2xlIn0.necIJaiP7X2T2QjGeV-FhpkizcNTX8HjDDBAxpgQTEI
DATABASE_CONNECTION=postgres://postgres:postgres@127.0.0.1:5432/postgres
```

Run the job:

```sh
npm run start:metrics
```
23 changes: 23 additions & 0 deletions packages/cron/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "cron",
"version": "0.0.0",
"description": "nftstorage.link Cron Jobs",
"private": true,
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "run-s start:*",
"start:metrics": "NODE_TLS_REJECT_UNAUTHORIZED=0 node src/bin/metrics.js"
},
"author": "Vasco Santos",
"license": "(Apache-2.0 OR MIT)",
"dependencies": {
"debug": "^4.3.1",
"dotenv": "^9.0.2",
"p-settle": "^5.0.0",
"pg": "^8.7.1"
},
"devDependencies": {
"npm-run-all": "^4.1.5"
}
}
20 changes: 20 additions & 0 deletions packages/cron/src/bin/metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env node

import { updateMetrics } from '../jobs/metrics.js'
import { envConfig } from '../lib/env.js'
import { getPgPool } from '../lib/utils.js'

async function main() {
const rwPg = getPgPool(process.env, 'rw')
const roPg = getPgPool(process.env, 'ro')

try {
await updateMetrics({ rwPg, roPg })
} finally {
await rwPg.end()
await roPg.end()
}
}

envConfig()
main()
Loading

0 comments on commit 7572044

Please sign in to comment.