Skip to content

Instantly share code, notes, and snippets.

@alexklibisz
Last active October 30, 2022 12:12
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save alexklibisz/144d731e34e3d5c5e1112242eec84678 to your computer and use it in GitHub Desktop.
Save alexklibisz/144d731e34e3d5c5e1112242eec84678 to your computer and use it in GitHub Desktop.
Firefly + Grafana Example
# This is part of the docker-compose I use to run Firefly on a Digital Ocean droplet.
version: '3.9'
services:
firefly_postgresql:
container_name: firefly_postgresql
image: bitnami/postgresql:12.6.0
volumes:
- 'firefly_postgresql_data:/bitnami/postgresql'
- './postgresql/init.sh:/docker-entrypoint-initdb.d/init.sh'
ports:
- 5432:5432
environment:
# Standard bitnami/postgresql env vars.
- POSTGRESQL_POSTGRES_PASSWORD=${POSTGRESQL_POSTGRES_PASSWORD}
- POSTGRESQL_DATABASE=firefly
- POSTGRESQL_USERNAME=firefly
- POSTGRESQL_PASSWORD=${POSTGRESQL_FIREFLY_PASSWORD}
# Env vars used by init scripts.
- INIT_ANALYSIS_PASSWORD=${POSTGRESQL_ANALYSIS_PASSWORD}
grafana:
container_name: grafana
image: grafana/grafana:6.5.0
volumes:
- 'grafana_data:/var/lib/grafana'
- './grafana/datasources.yaml:/etc/grafana/provisioning/datasources/custom.yaml'
ports:
- 3000:3000
environment:
# Set this when starting from scratch.
# - GF_SECURITY_DISABLE_INITIAL_ADMIN_CREATION=false
- GF_SERVER_ROOT_URL=https://grafana.example.com
- POSTGRESQL_ANALYSIS_PASSWORD=${POSTGRESQL_ANALYSIS_PASSWORD}
volumes:
firefly_postgresql_data:
name: firefly_postgresql_data
driver: local
grafana_data:
name: grafana_data
driver: local
# Postgres Init script that creates a limited-access user for Grafana to read from the database.
#!/bin/sh
set -e
export POSTGRESQL_PASSWORD=$POSTGRESQL_POSTGRES_PASSWORD
psql -h localhost -p 5432 -U postgres -d $POSTGRESQL_DATABASE -v ON_ERROR_STOP=1 <<EOF
revoke all on schema public from public;
grant all on schema public to firefly;
-- user analysis can select from public schema and create tables in analysis schema.
create schema analysis;
create user analysis with password '$INIT_ANALYSIS_PASSWORD';
grant create on schema analysis to analysis;
grant all on schema analysis to analysis;
grant usage on schema public to analysis;
-- user firefly will create new tables in schema public; user analysis can select from them.
alter default privileges for role firefly in schema public grant select on tables to analysis;
EOF
# Datasources file used by grafana to read from Firefly Postgres container.
apiVersion: 1
datasources:
- name: firefly-postgres
type: postgres
url: firefly_postgresql:5432
database: firefly
user: analysis
secureJsonData:
password: $POSTGRESQL_ANALYSIS_PASSWORD
jsonData:
sslmode: "disable"
# Postgres views used in the Grafana dashboard.
# These are created by manually executing the queries... nothing fancy.
-- This view gives a single table of transactions which is more convenient to use in the dashboard.
create or replace view analysis.txs as (
select date(tj.date),
u.email,
a.name account,
c.name category,
case when c.name in ('Income',
'Investment 401k',
'Investment Roth IRA',
'Investment Foo',
'Investment Bar',
'Investment Baz',
'Charity'
) then c.name::text
when tt.type in ('Opening balance') then null
when c.name is not null then 'Expense'::text
end
as meta_category,
tt.type transaction_type,
round(t.amount, 2) amount
from transactions t
join transaction_journals tj on t.transaction_journal_id = tj.id
join transaction_types tt on tt.id = tj.transaction_type_id
left join category_transaction_journal ctj on tj.id = ctj.transaction_journal_id
left join categories c on ctj.category_id = c.id
join accounts a on a.id = t.account_id
join account_types at on a.account_type_id = at.id
join users u on a.user_id = u.id
where a.active = true
and a.deleted_at is null
and t.deleted_at is null
and at.type = 'Asset account'
order by tj.date desc
);
-- This view removes a couple category/account pairs which would otherwise cancel out to 0 due to double-entry accounting.
create or replace view analysis.cash_flow as (
select *
from txs
where not (meta_category in ('Investment Foo',
'Investment Bar',
'Investment Baz')
and account like 'INV%'
and transaction_type = 'Transfer')
);
# This is the grafana dashboard exported to JSON.
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 1,
"iteration": 1622481314456,
"links": [],
"panels": [
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": true,
"colors": [
"#d44a3a",
"rgba(237, 129, 40, 0.89)",
"#299c46"
],
"datasource": "firefly-postgres",
"decimals": null,
"description": "Total Net Worth at End of Period\nThis should equal the net worth on the Firefly home page.",
"format": "currencyUSD",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
"w": 4,
"x": 0,
"y": 0
},
"id": 37,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"options": {},
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "70%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false,
"ymax": null,
"ymin": null
},
"tableColumn": "",
"targets": [
{
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "select sum(amount)\nfrom txs\nwhere email = '$email'\n and date <= $__timeTo()\n",
"refId": "B",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"thresholds": "0,0",
"timeFrom": null,
"timeShift": null,
"title": "Net Worth",
"transparent": true,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "total"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": true,
"colors": [
"#d44a3a",
"rgba(237, 129, 40, 0.89)",
"#299c46"
],
"datasource": "firefly-postgres",
"description": "",
"format": "currencyUSD",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 4,
"w": 4,
"x": 4,
"y": 0
},
"id": 40,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"options": {},
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false,
"ymax": null,
"ymin": null
},
"tableColumn": "",
"targets": [
{
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "select sum(amount)\nfrom txs\nwhere meta_category not like 'Investment%'\n and email = '$email'\n and date <= $__timeTo()",
"refId": "A",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"thresholds": "0,0",
"timeFrom": null,
"timeShift": null,
"title": "Cash",
"transparent": true,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "first"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "firefly-postgres",
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 7,
"w": 16,
"x": 8,
"y": 0
},
"hiddenSeries": false,
"id": 34,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"nullPointMode": "null",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"format": "time_series",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "select $__time(d), sum(amount), 'Total'\nfrom generate_series($__timeTo(), '2019-01-01'::date, '-7 days'::interval) d\njoin txs on txs.date <= d\nwhere email = '$email'\ngroup by d\norder by d\n\n",
"refId": "B",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
},
{
"format": "time_series",
"group": [],
"hide": false,
"metricColumn": "none",
"rawQuery": true,
"rawSql": "select $__time(d), sum(amount), 'Cash' as tags\nfrom generate_series($__timeTo(), '2019-01-01'::date, '-7 days'::interval) d\njoin txs on txs.date <= d\nwhere meta_category not like 'Investment%'\n and email = '$email'\ngroup by d\norder by d\n\n",
"refId": "A",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Net Worth",
"tooltip": {
"shared": true,
"sort": 1,
"value_type": "individual"
},
"transparent": true,
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "currencyUSD",
"label": null,
"logBase": 1,
"max": null,
"min": "-1",
"show": true
},
{
"format": "currencyUSD",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": true,
"colors": [
"#d44a3a",
"rgba(237, 129, 40, 0.89)",
"#299c46"
],
"datasource": "firefly-postgres",
"decimals": null,
"description": "Total Net Worth at End of Period",
"format": "currencyUSD",
"gauge": {
"maxValue": 100,
"minValue": 0,
"show": false,
"thresholdLabels": false,
"thresholdMarkers": true
},
"gridPos": {
"h": 3,
"w": 8,
"x": 0,
"y": 4
},
"id": 38,
"interval": null,
"links": [],
"mappingType": 1,
"mappingTypes": [
{
"name": "value to text",
"value": 1
},
{
"name": "range to text",
"value": 2
}
],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"options": {},
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "70%",
"rangeMaps": [
{
"from": "null",
"text": "N/A",
"to": "null"
}
],
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false,
"ymax": null,
"ymin": null
},
"tableColumn": "",
"targets": [
{
"format": "table",
"group": [],
"hide": false,
"metricColumn": "none",
"rawQuery": true,
"rawSql": "select sum(amount)\n from txs\n where email = '$email'\n and $__timeFilter(date)",
"refId": "A",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"thresholds": "0,0",
"timeFrom": null,
"timeShift": null,
"title": "Net Worth Increase",
"transparent": true,
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "total"
},
{
"cacheTimeout": null,
"columns": [],
"datasource": "firefly-postgres",
"description": "Where does income go?",
"fontSize": "100%",
"gridPos": {
"h": 9,
"w": 8,
"x": 0,
"y": 7
},
"id": 21,
"links": [],
"options": {},
"pageSize": null,
"pluginVersion": "6.5.0",
"showHeader": true,
"sort": {
"col": 1,
"desc": true
},
"styles": [
{
"alias": "Meta Category",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"mappingType": 1,
"pattern": "meta_category",
"preserveFormat": false,
"type": "string"
},
{
"alias": "Amount",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "amount",
"thresholds": [],
"type": "number",
"unit": "currencyUSD"
},
{
"alias": "% of Income",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 0,
"mappingType": 1,
"pattern": "percentage_of_income",
"thresholds": [],
"type": "number",
"unit": "percent"
}
],
"targets": [
{
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "with grouped as (\nselect meta_category, sum(amount) amount\nfrom analysis.cash_flow\nwhere email = '$email'\n and $__timeFilter(date)\ngroup by meta_category\n)\nselect meta_category,\n amount,\n round(100.0 * abs(amount) / (select sum(amount) from grouped where meta_category = 'Income'))\n as percentage_of_income\nfrom grouped\nwhere meta_category = 'Income' or amount < 0\norder by 2 desc;",
"refId": "B",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"timeFrom": null,
"timeShift": null,
"title": "Cash Flow",
"transform": "table",
"transparent": true,
"type": "table"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "firefly-postgres",
"fill": 0,
"fillGradient": 0,
"gridPos": {
"h": 9,
"w": 16,
"x": 8,
"y": 7
},
"hiddenSeries": false,
"id": 33,
"legend": {
"alignAsTable": false,
"avg": false,
"current": false,
"max": false,
"min": false,
"rightSide": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"nullPointMode": "connected",
"options": {
"dataLinks": []
},
"percentage": false,
"pointradius": 1,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"format": "time_series",
"group": [],
"hide": false,
"metricColumn": "none",
"rawQuery": true,
"rawSql": "with\ndates as (\nselect generate_series($__timeFrom(), $__timeTo(), '7 days'::interval) d\n)\nselect date(d) as time, round(abs(sum(amount)), 2) amount, meta_category as tags\n from dates\n join analysis.cash_flow \n on date_trunc('month', date) = date_trunc('month', d)\n and meta_category is not null\n and email = '$email'\n group by d, meta_category\n order by d, meta_category;",
"refId": "A",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "Cash Flow (30 day totals)",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"transparent": true,
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "currencyUSD",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "currencyUSD",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": false
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"columns": [],
"datasource": "firefly-postgres",
"fontSize": "100%",
"gridPos": {
"h": 14,
"w": 8,
"x": 0,
"y": 16
},
"id": 16,
"options": {},
"pageSize": null,
"showHeader": true,
"sort": {
"col": 1,
"desc": true
},
"styles": [
{
"alias": "Account",
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "account",
"type": "string"
},
{
"alias": "Balance",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "balance",
"thresholds": [],
"type": "number",
"unit": "currencyUSD"
}
],
"targets": [
{
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "select account, sum(amount) as Balance\n from txs\n where email = '$email'\n and date <= $__timeTo()\ngroup by account\nhaving sum(amount) != 0\norder by 2 desc",
"refId": "A",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"timeFrom": null,
"timeShift": null,
"title": "Account Balances at End of Period",
"transform": "table",
"transparent": true,
"type": "table"
},
{
"columns": [],
"datasource": "firefly-postgres",
"description": "",
"fontSize": "100%",
"gridPos": {
"h": 16,
"w": 15,
"x": 8,
"y": 16
},
"id": 36,
"options": {},
"pageSize": 15,
"showHeader": true,
"sort": {
"col": 1,
"desc": false
},
"styles": [
{
"alias": "Category",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "category",
"thresholds": [],
"type": "string",
"unit": "short"
},
{
"alias": "Balance",
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"decimals": 2,
"mappingType": 1,
"pattern": "balance",
"thresholds": [],
"type": "number",
"unit": "currencyUSD"
}
],
"targets": [
{
"format": "table",
"group": [],
"metricColumn": "none",
"rawQuery": true,
"rawSql": "select category, sum(amount) as balance\nfrom txs\nwhere email = '$email'\n and category is not null\n and $__timeFilter(date)\ngroup by category\n",
"refId": "A",
"select": [
[
{
"params": [
"value"
],
"type": "column"
}
]
],
"timeColumn": "time",
"where": [
{
"name": "$__timeFilter",
"params": [],
"type": "macro"
}
]
}
],
"timeFrom": null,
"timeShift": null,
"title": "Balance by Category",
"transform": "table",
"transparent": true,
"type": "table"
}
],
"refresh": false,
"schemaVersion": 21,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"allValue": null,
"current": {
"tags": [],
"text": "foo@gmail.com",
"value": "foo@gmail.com"
},
"datasource": "firefly-postgres",
"definition": "select email from users order by email",
"hide": 0,
"includeAll": false,
"label": "Account Email",
"multi": false,
"name": "email",
"options": [
{
"selected": true,
"text": "foo@gmail.com",
"value": "foo@gmail.com"
},
{
"selected": false,
"text": "bar@gmail.com",
"value": "bar@gmail.com"
}
],
"query": "select email from users order by email",
"refresh": 0,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
},
"time": {
"from": "now-2y",
"to": "now"
},
"timepicker": {
"hidden": false,
"refresh_intervals": [
"1d"
]
},
"timezone": "browser",
"title": "Firefly Overview",
"uid": "6QlEG2qGk",
"version": 76
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment