Skip to content

Commit

Permalink
rewrite to make it cleaner (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
SathyaBhat committed Apr 25, 2024
1 parent 24f14fb commit d5dfee6
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 113 deletions.
11 changes: 5 additions & 6 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"printWidth": 120,
"singleQuote": true
}
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"printWidth": 120
}
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"scripts": {
"build": "npx tsc",
"dev": "pwd && source ./.dev.env && concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"",
"dev": "concurrently \"npx tsc --watch\" \"nodemon -q dist/index.js\"",
"start": "node dist/index.js"
},
"author": {
Expand All @@ -18,6 +18,7 @@
"license": "MIT",
"dependencies": {
"cron": "^2.4.3",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"loglevel": "^1.8.1",
"prom-client": "^14.2.0",
Expand Down
7 changes: 6 additions & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ export class YnabAPI {
this.client = new API(this.accessToken);
}


public async getAccountName(): Promise<string> {
return (await this.client.budgets.getBudgetById(this.budgetId)).data.budget.name;
}

public async getCategoryBudgets(): Promise<Category[]> {
const catGroups = (await this.client.categories.getCategories(this.budgetId)).data.category_groups;
const categories = (catGroups.map(g => g.categories)).flat(1);
log.debug(`Got ${categories.length} categories`);
log.info(`Got ${categories.length} categories`);
return categories;
}

public async getBudgetIds() {
return (await this.client.budgets.getBudgets().then(b => b.data.budgets)).map(b => this.budgetId);
}
}
118 changes: 44 additions & 74 deletions src/collectors.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,50 @@
import log from 'loglevel';
import {Gauge, Registry} from "prom-client";
import {Account, Category} from "ynab";
import * as metrics from './metrics';

export class YNABCollector {
accountBalances: Account[] = [];
categoryBalance: Category[] = [];


public async collectAccountBalanceMetrics(register: Registry) {
log.debug('Collecting account balance metrics..');

const accountLabels = ['account_name', 'type', 'closed'];
const accountClearedBalanceGauge = new Gauge({
name: "ynab_cleared_account_balance",
registers: [register],
help: "Account Cleared Balance amounts",
labelNames: accountLabels,
collect: async () => {
log.debug(`Collecting Cleared Balance for ${this.accountBalances.length} accounts`);
this.accountBalances.forEach(a => {
accountClearedBalanceGauge.labels({account_name: a.name, type: a.type, closed: String(a.closed)}).set(a.cleared_balance / 1000);
});
}

});
const accountUnClearedBalanceGauge = new Gauge({
name: "ynab_uncleared_account_balance",
help: "Account Uncleared Balance amounts",
registers: [register],
labelNames: accountLabels,
collect: async () => {
log.debug(`Collecting Uncleared Balance for ${this.accountBalances.length} accounts`);
this.accountBalances.forEach(a => {
accountUnClearedBalanceGauge.labels({account_name: a.name, type: a.type, closed: String(a.closed)}).set(a.uncleared_balance / 1000);
});
}
});
}

public convertMilliUnitsToUnits(amount: number): number {
return (amount / 1000);
}

public async collectCategoryBalanceMetrics(register: Registry) {
log.debug('Collecting category balance metrics..');
const self = this; // Store a reference to 'this', else typescript thinks this refers to the Gauge class, not the Collector class

const catLabels = ['name', 'category_group_name', 'budgeted_amount', 'activity_amount', 'balance_amount', 'hidden', 'deleted'];

// Define properties that differ between gauges
const gaugeProperties = [
{name: 'budgeted', help: 'Category Budgeted amount', suffix: 'budgeted_amount'},
{name: 'activity', help: 'Category Activity amount', suffix: 'activity_amount'},
{name: 'balance', help: 'Category Balance amount', suffix: 'balance_amount'},
{name: 'hidden', help: 'Category Balance amount', suffix: 'hidden'},
{name: 'deleted', help: 'Category Balance amount', suffix: 'deleted'},
];

for (const prop of gaugeProperties) {
new Gauge({
name: `ynab_category_${prop.suffix}`,
registers: [register],
help: prop.help,
labelNames: catLabels,
async collect() {
for (const cat of self.categoryBalance) {
this.labels({
name: cat.name,
category_group_name: cat.category_group_name,
hidden: String(cat.hidden),
deleted: String(cat.deleted)
}).set(self.convertMilliUnitsToUnits(Number(cat[prop.name as keyof Category]) || 0));

}
}
});
}
categoryBalance: Category[] = [];

public collectAccountBalanceMetrics(budgetName: string, accountBalances: Account[]) {
accountBalances.forEach(a => {
metrics.ynab_cleared_account_balance.labels({account_name: a.name, budget_name: budgetName, type: a.type, closed: String(a.closed)}).set(a.cleared_balance / 1000);
metrics.ynab_uncleared_account_balance.labels({account_name: a.name, budget_name: budgetName, type: a.type, closed: String(a.closed)}).set(a.uncleared_balance / 1000);
});
}

public convertMilliUnitsToUnits(amount: number): number {
return (amount / 1000);
}

public async collectCategoryBalanceMetrics(budgetName: string, categoryBalance: Category[]) {
for (const cat of categoryBalance) {
metrics.ynab_category_balance_amount.labels({
name: cat.name,
category_group_name: cat.category_group_name,
hidden: String(cat.hidden),
deleted: String(cat.deleted),
budget_name: budgetName,
}).set(
this.convertMilliUnitsToUnits(cat.balance)
);
metrics.ynab_category_budgeted_amount.labels({
name: cat.name,
category_group_name: cat.category_group_name,
hidden: String(cat.hidden),
deleted: String(cat.deleted),
budget_name: budgetName,
}).set(
this.convertMilliUnitsToUnits(cat.budgeted)
);
metrics.ynab_category_activity_amount.labels({
name: cat.name,
category_group_name: cat.category_group_name,
hidden: String(cat.hidden),
deleted: String(cat.deleted),
budget_name: budgetName,
}).set(
this.convertMilliUnitsToUnits(cat.activity)
);
}
}
}
34 changes: 14 additions & 20 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,41 @@


import {CronJob} from 'cron';
import dotenv from "dotenv";
import express, {Express, Request, Response} from 'express';
import {Registry} from "prom-client";
import log, {LogLevelDesc} from 'loglevel';
import 'source-map-support/register';
import {YnabAPI} from "./api";
import {YNABCollector} from "./collectors";
import {scheduledAccountBalanceUpdate, scheduledCategoryBalanceUpdate} from "./jobs/accounts";
import log, {LogLevelDesc} from 'loglevel';

import 'source-map-support/register';
import {registry} from './metrics';

async function main() {
dotenv.config();
const ynab = new YnabAPI();
const register = new Registry();

const port = process.env.PORT || 9100;
const app: Express = express();
const ynabCollector = new YNABCollector();
const budgetName = await ynab.getAccountName();

new CronJob({
cronTime: "*/15 * * * *",
onTick: async () => {
ynabCollector.accountBalances = (await scheduledAccountBalanceUpdate(ynab)).accounts;
log.info(`${ynabCollector.accountBalances.length} accounts refreshed`);

ynabCollector.categoryBalance = (await scheduledCategoryBalanceUpdate(ynab));
log.info(`${ynabCollector.categoryBalance.length} categories refreshed`);

log.info(`Refreshing YNAB data at ${new Date().toLocaleString()}...`);
const accountBalance = await scheduledAccountBalanceUpdate(ynab);
const catBalance = await scheduledCategoryBalanceUpdate(ynab);
ynabCollector.collectAccountBalanceMetrics(budgetName, accountBalance.data.accounts);
ynabCollector.collectCategoryBalanceMetrics(budgetName, catBalance);
},
start: true,
runOnInit: true
});

register.setDefaultLabels({
budget_name: await ynab.getAccountName()
});
ynabCollector.collectAccountBalanceMetrics(register);
ynabCollector.collectCategoryBalanceMetrics(register);

app.get('/metrics', async (req: Request, res: Response) => {
res.setHeader('Content-Type', register.contentType);
log.debug('getting metrics');
const results = await register.metrics();
res.send(results);
res.setHeader('Content-Type', registry.contentType);
res.send(await registry.metrics());
});

app.listen(port, () => {
Expand Down
16 changes: 5 additions & 11 deletions src/jobs/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import {AccountsResponseData, Category} from "ynab";
import {AccountsResponse, Category} from "ynab";
import {YnabAPI} from "../api";
import log from 'loglevel';

export async function scheduledAccountBalanceUpdate(ynab: YnabAPI): Promise<AccountsResponseData> {
log.info(`Starting scheduled account balance update at ${new Date().toLocaleString()} ...`);
const accounts = await ynab.client.accounts.getAccounts(ynab.budgetId);
log.info(`Fetched balances for ${accounts.data.accounts.length} accounts.`);
return accounts.data;
}
export async function scheduledAccountBalanceUpdate(ynab: YnabAPI): Promise<AccountsResponse> {
return ynab.client.accounts.getAccounts(ynab.budgetId);

}
export async function scheduledCategoryBalanceUpdate(ynab: YnabAPI): Promise<Category[]> {
log.info(`Starting scheduled category balance update at ${new Date().toLocaleString()} ...`);
const categories = await ynab.getCategoryBudgets();
return categories;
return ynab.getCategoryBudgets();
}
40 changes: 40 additions & 0 deletions src/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {Gauge, Registry, collectDefaultMetrics} from "prom-client";

const catLabels = ['name', 'category_group_name', 'budgeted_amount', 'activity_amount', 'balance_amount', 'hidden', 'deleted', 'budget_name'];
const accountLabels = ['budget_name', 'account_name', 'type', 'closed'];
export const registry = new Registry();

export const ynab_category_balance_amount = new Gauge({
name: 'ynab_category_balance_amount',
help: 'Category Balance amount',
registers: [registry],
labelNames: catLabels,
});

export const ynab_category_activity_amount = new Gauge({
name: 'ynab_category_activity_amount',
help: 'Category Activity amount',
registers: [registry],
labelNames: catLabels,
});

export const ynab_category_budgeted_amount = new Gauge({
name: 'ynab_category_budgeted_amount',
help: 'Category Budgeted amount',
registers: [registry],
labelNames: catLabels,
});

export const ynab_cleared_account_balance = new Gauge({
name: 'ynab_cleared_account_balance',
help: 'Account Cleared Balance amounts',
registers: [registry],
labelNames: accountLabels,
});

export const ynab_uncleared_account_balance = new Gauge({
name: 'ynab_uncleared_account_balance',
help: 'Account Uncleared Balance amounts',
registers: [registry],
labelNames: accountLabels
});

0 comments on commit d5dfee6

Please sign in to comment.