diff --git a/package-lock.json b/package-lock.json index 8a8f19d..d302dc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "cron": "^2.4.3", "express": "^4.18.2", "prom-client": "^14.2.0", + "source-map-support": "^0.5.21", "ynab": "^1.32.0" }, "bin": { @@ -20,6 +21,7 @@ "devDependencies": { "@types/express": "^4.17.17", "@types/node": "^18.11.3", + "@types/node-schedule": "^2.1.0", "concurrently": "^8.2.1", "eslint": "^8.48.0", "eslint-plugin-prettier": "^5.0.0", @@ -599,6 +601,11 @@ "node": ">=8" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, "node_modules/bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -2858,6 +2865,23 @@ "node": ">=10" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/spawn-command": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", diff --git a/package.json b/package.json index 82caf30..45687a2 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "cron": "^2.4.3", "express": "^4.18.2", "prom-client": "^14.2.0", + "source-map-support": "^0.5.21", "ynab": "^1.32.0" }, "devDependencies": { diff --git a/src/api.ts b/src/api.ts index 21c3f6f..a295bd5 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,6 +1,15 @@ import {API} from "ynab"; -export async function ynabClient(): Promise { - const accessToken = process.env.YNAB_TOKEN!; - return new API(accessToken); -} \ No newline at end of file +export class YnabAPI { + private accessToken = process.env.YNAB_TOKEN!; + public client: API; + public budgetId: string = process.env.BUDGET_ID!; + + constructor() { + this.client = new API(this.accessToken); + } + + public async getAccountName(): Promise { + return (await this.client.budgets.getBudgetById(this.budgetId)).data.budget.name; + } +} diff --git a/src/collectors.ts b/src/collectors.ts index 29586ff..7ad4e03 100644 --- a/src/collectors.ts +++ b/src/collectors.ts @@ -1,15 +1,13 @@ import {Gauge, Registry} from "prom-client"; import {Account} from "ynab"; - -export class YNABMetrics { +export class YNABCollector { accountBalances: Account[] = []; - public async collectAccountBalanceMetrics(register: Registry) { console.log('Collecting account balance metrics..'); - const accountLabels = ['account_name', 'type']; + const accountLabels = ['account_name', 'type', 'closed']; const accountClearedBalanceGauge = new Gauge({ name: "ynab_cleared_account_balance", registers: [register], @@ -18,7 +16,7 @@ export class YNABMetrics { collect: async () => { console.log(`Collecting Cleared Balance for ${this.accountBalances.length} accounts`); this.accountBalances.forEach(a => { - accountClearedBalanceGauge.labels({account_name: a.name, type: a.type}).set(a.cleared_balance / 1000); + accountClearedBalanceGauge.labels({account_name: a.name, type: a.type, closed: String(a.closed)}).set(a.cleared_balance / 1000); }); } @@ -31,7 +29,7 @@ export class YNABMetrics { collect: async () => { console.log(`Collecting Uncleared Balance for ${this.accountBalances.length} accounts`); this.accountBalances.forEach(a => { - accountUnClearedBalanceGauge.labels({account_name: a.name, type: a.type}).set(a.uncleared_balance / 1000); + accountUnClearedBalanceGauge.labels({account_name: a.name, type: a.type, closed: String(a.closed)}).set(a.uncleared_balance / 1000); }); } diff --git a/src/index.ts b/src/index.ts index 760ca7d..d3af5bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,37 +1,37 @@ -import {Registry, Gauge, collectDefaultMetrics} from "prom-client"; -import express, {Express, Request, Response} from 'express'; -import {YNABMetrics} from "./collectors"; -import {scheduledAccountBalanceUpdate} from "./jobs/accounts"; - import {CronJob} from 'cron'; -import {Account} from "ynab"; -import {ynabClient} from "./api"; - +import express, {Express, Request, Response} from 'express'; +import {Registry} from "prom-client"; +import 'source-map-support/register'; +import {YnabAPI} from "./api"; +import {YNABCollector} from "./collectors"; +import {scheduledAccountBalanceUpdate} from "./jobs/accounts";; async function main() { - const ynab = await ynabClient(); + const ynab = new YnabAPI(); const register = new Registry(); - const port = process.env.PORT; + + const port = process.env.PORT || 9100; const app: Express = express(); - const ynabMetrics = new YNABMetrics(); + const ynabCollector = new YNABCollector(); new CronJob({ cronTime: "0 * * * * ", onTick: async () => { - ynabMetrics.accountBalances = (await scheduledAccountBalanceUpdate(ynab)).accounts; - console.log(`${ynabMetrics.accountBalances.length} accounts refreshed`); + ynabCollector.accountBalances = (await scheduledAccountBalanceUpdate(ynab)).accounts; + console.log(`${ynabCollector.accountBalances.length} accounts refreshed`); }, start: true, runOnInit: true }); - - ynabMetrics.collectAccountBalanceMetrics(register); - + register.setDefaultLabels({ + budget_name: await ynab.getAccountName() + }); + ynabCollector.collectAccountBalanceMetrics(register); app.get('/metrics', async (req: Request, res: Response) => { res.setHeader('Content-Type', register.contentType); @@ -40,8 +40,8 @@ async function main() { res.send(results); }); - app.listen(port || 9100, () => { - console.log('⚡ Hello'); + app.listen(port, () => { + console.log(`🔊 Publishing metrics on port ${port}`); }); } diff --git a/src/jobs/accounts.ts b/src/jobs/accounts.ts index 9c2df12..4922708 100644 --- a/src/jobs/accounts.ts +++ b/src/jobs/accounts.ts @@ -1,11 +1,11 @@ -import {API, AccountsResponseData, TransactionsResponse} from "ynab"; +import {AccountsResponseData, TransactionsResponse} from "ynab"; +import {YnabAPI} from "../api"; export type ynabTransactionResponse = (TransactionsResponse & {rateLimit: string | null;}) | undefined; -export const ynabBudgetId = process.env.BUDGET_ID!; -export async function scheduledAccountBalanceUpdate(ynab: API): Promise { +export async function scheduledAccountBalanceUpdate(ynab: YnabAPI): Promise { console.log(`Starting scheduled account balance update at ${new Date().toLocaleString()} ...`); - const accounts = await ynab.accounts.getAccounts(ynabBudgetId); + const accounts = await ynab.client.accounts.getAccounts(ynab.budgetId); console.log(`Fetched balances for ${accounts.data.accounts.length} accounts.`); console.log(`Rate limit: ${accounts.rateLimit}`); return accounts.data; diff --git a/tsconfig.json b/tsconfig.json index aecb69a..279c2e8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "target": "es2016", "moduleResolution": "node", "sourceMap": true, + "inlineSources": true, "outDir": "dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true,