diff --git a/helm-chart/.helmignore b/helm-chart/.helmignore index 696ed898..c7cbd8ed 100644 --- a/helm-chart/.helmignore +++ b/helm-chart/.helmignore @@ -22,4 +22,10 @@ *.tmproj .vscode/ -grafana_dashboards/postgres +grafana_dashboards/prometheus +grafana_dashboards/influxdb/v5 +grafana_dashboards/influxdb/v6 +grafana_dashboards/influxdb/v7 +grafana_dashboards/postgres/v5 +grafana_dashboards/postgres/v6 +grafana_dashboards/postgres/v7 diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml index ac5c565c..c38dc9f5 100644 --- a/helm-chart/Chart.yaml +++ b/helm-chart/Chart.yaml @@ -30,7 +30,7 @@ dependencies: - name: influxdb repository: https://helm.influxdata.com/ version: "4.11.0" - condition: storage == influxdb + condition: influxdb.enabled - name: metallb repository: https://metallb.github.io/metallb version: "0.12.1" diff --git a/helm-chart/templates/_helpers.tpl b/helm-chart/templates/_helpers.tpl index 5f0aa38c..d3207887 100644 --- a/helm-chart/templates/_helpers.tpl +++ b/helm-chart/templates/_helpers.tpl @@ -104,3 +104,12 @@ Return if ingress supports pathType. {{- define "pgwatch2.ingress.supportsPathType" -}} {{- or (eq (include "pgwatch2.ingress.isStable" .) "true") (and (eq (include "pgwatch2.ingress.apiVersion" .) "networking.k8s.io/v1beta1") (semverCompare ">= 1.18-0" .Capabilities.KubeVersion.Version)) -}} {{- end -}} + +{{- define "pgwatch2-storage" -}} +{{- if eq .Values.storage "influx" -}} +influxdb +{{- else -}} +{{- .Values.storage -}} +{{- end -}} +{{- end }} + diff --git a/helm-chart/templates/configmaps.yaml b/helm-chart/templates/configmaps.yaml index 6fa291ea..fddc187a 100644 --- a/helm-chart/templates/configmaps.yaml +++ b/helm-chart/templates/configmaps.yaml @@ -3,10 +3,219 @@ kind: ConfigMap metadata: name: {{ include "pgwatch2.fullname" . }}-initdb data: - initdb.sql: | + 00_initdb.sql: | CREATE USER {{ .Values.postgresql.user | default "pgwatch2" }} WITH PASSWORD '{{ .Values.postgresql.password | default "pgwatch2" }}'; - CREATE DATABASE {{ .Values.postgresql.database | default "pgwatch2" }}; + CREATE DATABASE {{ .Values.postgresql.database | default "pgwatch2" }} OWNER {{ .Values.postgresql.user | default "pgwatch2" }}; GRANT ALL PRIVILEGES ON DATABASE {{ .Values.postgresql.database | default "pgwatch2" }} TO {{ .Values.postgresql.user | default "pgwatch2" }}; + {{ if eq .Values.storage "postgres" }} + 01_create_metrics_db.sql: | + CREATE DATABASE {{ .Values.postgres_storage.database | default "pgwatch2_metrics" }} OWNER {{ .Values.postgresql.user | default "pgwatch2" }}; + \c {{ .Values.postgres_storage.database | default "pgwatch2_metrics" }} + + CREATE SCHEMA IF NOT EXISTS admin AUTHORIZATION {{ .Values.postgresql.user | default "pgwatch2" }}; + + GRANT ALL ON SCHEMA public TO {{ .Values.postgresql.user | default "pgwatch2" }}; + + DO $SQL$ + BEGIN + EXECUTE format($$ALTER ROLE {{ .Values.postgresql.user | default "pgwatch2" }} IN DATABASE %s SET statement_timeout TO '5min'$$, current_database()); + RAISE WARNING 'NB! Enabling asynchronous commit for pgwatch2 role - revert if possible data loss on crash is not acceptable!'; + EXECUTE format($$ALTER ROLE {{ .Values.postgresql.user | default "pgwatch2" }} IN DATABASE %s SET synchronous_commit TO off$$, current_database()); + END + $SQL$; + + CREATE EXTENSION IF NOT EXISTS btree_gin; + + SET ROLE TO {{ .Values.postgresql.user | default "pgwatch2" }}; + + create table admin.storage_schema_type ( + schema_type text not null, + initialized_on timestamptz not null default now(), + check (schema_type in ('metric', 'metric-time', 'metric-dbname-time', 'custom', 'timescale')) + ); + + comment on table admin.storage_schema_type is 'identifies storage schema for other pgwatch2 components'; + + create unique index max_one_row on admin.storage_schema_type ((1)); + + /* for the Grafana drop-down. managed by the gatherer */ + create table admin.all_distinct_dbname_metrics ( + dbname text not null, + metric text not null, + created_on timestamptz not null default now(), + primary key (dbname, metric) + ); + + /* currently only used to store TimescaleDB chunk interval */ + create table admin.config + ( + key text not null primary key, + value text not null, + created_on timestamptz not null default now(), + last_modified_on timestamptz + ); + + -- to later change the value call the admin.change_timescale_chunk_interval(interval) function! + -- as changing the row directly will only be effective for completely new tables (metrics). + insert into admin.config select 'timescale_chunk_interval', '2 days'; + insert into admin.config select 'timescale_compress_interval', '1 day'; + + create or replace function trg_config_modified() returns trigger + as $$ + begin + new.last_modified_on = now(); + return new; + end; + $$ + language plpgsql; + + create trigger config_modified before update on admin.config + for each row execute function trg_config_modified(); + + -- DROP FUNCTION IF EXISTS admin.ensure_dummy_metrics_table(text); + -- select * from admin.ensure_dummy_metrics_table('wal'); + CREATE OR REPLACE FUNCTION admin.ensure_dummy_metrics_table( + metric text + ) + RETURNS boolean AS + /* + creates a top level metric table if not already existing (non-existing tables show ugly warnings in Grafana). + expects the "metrics_template" table to exist. + */ + $SQL$ + DECLARE + l_schema_type text; + l_template_table text := 'admin.metrics_template'; + l_unlogged text := ''; + BEGIN + SELECT schema_type INTO l_schema_type FROM admin.storage_schema_type; + + IF NOT EXISTS (SELECT 1 + FROM pg_tables + WHERE tablename = metric + AND schemaname = 'public') + THEN + IF metric ~ 'realtime' THEN + l_template_table := 'admin.metrics_template_realtime'; + l_unlogged := 'UNLOGGED'; + END IF; + + IF l_schema_type = 'metric' THEN + EXECUTE format($$CREATE %s TABLE public."%s" (LIKE %s INCLUDING INDEXES)$$, l_unlogged, metric, l_template_table); + ELSIF l_schema_type = 'metric-time' THEN + EXECUTE format($$CREATE %s TABLE public."%s" (LIKE %s INCLUDING INDEXES) PARTITION BY RANGE (time)$$, l_unlogged, metric, l_template_table); + ELSIF l_schema_type = 'metric-dbname-time' THEN + EXECUTE format($$CREATE %s TABLE public."%s" (LIKE %s INCLUDING INDEXES) PARTITION BY LIST (dbname)$$, l_unlogged, metric, l_template_table); + ELSIF l_schema_type = 'timescale' THEN + IF metric ~ 'realtime' THEN + EXECUTE format($$CREATE TABLE public."%s" (LIKE %s INCLUDING INDEXES) PARTITION BY RANGE (time)$$, metric, l_template_table); + ELSE + PERFORM admin.ensure_partition_timescale(metric); + END IF; + END IF; + + EXECUTE format($$COMMENT ON TABLE public."%s" IS 'pgwatch2-generated-metric-lvl'$$, metric); + + RETURN true; + + END IF; + + RETURN false; + END; + $SQL$ LANGUAGE plpgsql; + GRANT EXECUTE ON FUNCTION admin.ensure_dummy_metrics_table(text) TO pgwatch2; + + /* + NB! When possible the partitioned versions ("metric_store_part_time.sql" + or "metric_store_part_dbname_time.sql") (assuming PG11+) should be used + as much less IO would be then performed when removing old data. + NB! A fresh separate DB, only for pgwatch2 metrics storage purposes, is assumed. + */ + + + create table admin.metrics_template ( + time timestamptz not null default now(), + dbname text not null, + data jsonb not null, + tag_data jsonb, + check (false) + ); + + comment on table admin.metrics_template is 'used as a template for all new metric definitions'; + + -- create index on admin.metrics_template using brin (dbname, time); /* consider BRIN instead for large data amounts */ + create index on admin.metrics_template (dbname, time); + create index on admin.metrics_template using gin (dbname, tag_data, time) where tag_data notnull; + + /* + something like below will be done by the gatherer AUTOMATICALLY: + + create table public."some-metric" + (LIKE admin.metrics_template INCLUDING INDEXES); + COMMENT ON TABLE public."some-metric" IS 'pgwatch2-generated-metric-lvl'; + + */ + + + /* "realtime" metrics are non-persistent and have 1d retention */ + + -- drop table if exists metrics_template_realtime; + create unlogged table admin.metrics_template_realtime ( + time timestamptz not null default now(), + dbname text not null, + data jsonb not null, + tag_data jsonb, -- no index! + check (false) + ); + + comment on table admin.metrics_template_realtime is 'used as a template for all new realtime metric definitions'; + + -- create index on admin.metrics_template using brin (dbname, time) with (pages_per_range=32); /* consider BRIN instead for large data amounts */ + create index on admin.metrics_template_realtime (dbname, time); + + + RESET ROLE; + + insert into admin.storage_schema_type select 'metric'; + + -- DROP FUNCTION IF EXISTS public.ensure_partition_metric(text); + -- select * from public.ensure_partition_metric('wal'); + + CREATE OR REPLACE FUNCTION admin.ensure_partition_metric( + metric text + ) + RETURNS void AS + /* + creates a top level metric table if not already existing. + expects the "metrics_template" table to exist. + */ + $SQL$ + DECLARE + l_template_table text := 'admin.metrics_template'; + l_unlogged text := ''; + BEGIN + + PERFORM pg_advisory_xact_lock(regexp_replace( md5(metric) , E'\\D', '', 'g')::varchar(10)::int8); + + IF NOT EXISTS (SELECT 1 + FROM pg_tables + WHERE tablename = metric + AND schemaname = 'public') + THEN + --RAISE NOTICE 'creating partition % ...', metric; + IF metric ~ 'realtime' THEN + l_template_table := 'admin.metrics_template_realtime'; + l_unlogged := 'UNLOGGED'; + END IF; + EXECUTE format($$CREATE %s TABLE IF NOT EXISTS public.%s (LIKE %s INCLUDING INDEXES)$$, l_unlogged, quote_ident(metric), l_template_table); + EXECUTE format($$COMMENT ON TABLE public.%s IS 'pgwatch2-generated-metric-lvl'$$, quote_ident(metric)); + END IF; + + END; + $SQL$ LANGUAGE plpgsql; + + GRANT EXECUTE ON FUNCTION admin.ensure_partition_metric(text) TO {{ .Values.postgresql.user | default "pgwatch2" }}; + {{ end }} --- apiVersion: v1 kind: ConfigMap diff --git a/helm-chart/templates/deployment.yaml b/helm-chart/templates/deployment.yaml index 92aab329..b1724806 100644 --- a/helm-chart/templates/deployment.yaml +++ b/helm-chart/templates/deployment.yaml @@ -29,7 +29,7 @@ spec: imagePullPolicy: {{ .Values.daemon.image.pullPolicy }} env: - name: PGHOST - value: {{ include "pgwatch2.fullname" . }}-postgresql + value: {{ .Values.postgresql.host | default (printf "%s-postgresql" (include "pgwatch2.fullname" .)) }} - name: PGPORT value: {{ .Values.postgresql.port | default "5432" | quote }} - name: PGDATABASE @@ -55,7 +55,7 @@ spec: name: config-volume env: - name: PGHOST - value: {{ include "pgwatch2.fullname" . }}-postgresql + value: {{ .Values.postgresql.host | default (printf "%s-postgresql" (include "pgwatch2.fullname" .)) }} - name: PGPORT value: {{ .Values.postgresql.port | default "5432" | quote }} - name: PGDATABASE @@ -82,7 +82,7 @@ spec: imagePullPolicy: {{ .Values.daemon.image.pullPolicy }} env: - name: PW2_PGHOST - value: {{ include "pgwatch2.fullname" . }}-postgresql + value: {{ .Values.postgresql.host | default (printf "%s-postgresql" (include "pgwatch2.fullname" .)) }} - name: PW2_PGPORT value: {{ .Values.postgresql.port | default "5432" | quote }} - name: PW2_PGDATABASE @@ -93,6 +93,7 @@ spec: value: {{ .Values.postgresql.password | default "pgwatch2" }} - name: PW2_PGSSL value: {{ .Values.postgresql.ssl | default "False" | quote }} + {{ if eq .Values.storage "influx" }} - name: PW2_IHOST value: {{ include "pgwatch2.fullname" . }}-influxdb - name: PW2_IPORT @@ -105,6 +106,14 @@ spec: value: {{ .Values.influxdb.password | default "pgwatch2" }} - name: PW2_ISSL value: {{ .Values.influxdb.ssl | default "False" | quote }} + {{ else if eq .Values.storage "postgres" }} + - name: PW2_DATASTORE + value: {{ .Values.storage | quote }} + - name: PW2_PG_METRIC_STORE_CONN_STR + value: {{ printf "postgresql://%s:%s@%s:%s/%s" (.Values.postgresql.user | default "pgwatch2") (.Values.postgresql.password | default "pgwatch2" ) ( .Values.postgresql.host | default (printf "%s-postgresql" (include "pgwatch2.fullname" .))) (.Values.postgresql.port | default "5432") (.Values.postgres_storage.database | default "pgwatch2_metrics" ) }} + - name: PW2_PG_RETENTION_DAYS + value: {{ .Values.postgres_storage.retention_days | default "14" | quote }} + {{ end }} - name: PW2_INTERNAL_STATS_PORT value: {{ .Values.daemon.port | default "8081" | quote }} - name: PW2_WEBNOANONYMOUS @@ -144,7 +153,7 @@ spec: imagePullPolicy: {{ .Values.webui.image.pullPolicy }} env: - name: PW2_PGHOST - value: {{ include "pgwatch2.fullname" . }}-postgresql + value: {{ .Values.postgresql.host | default (printf "%s-postgresql" (include "pgwatch2.fullname" .)) }} - name: PW2_PGPORT value: {{ .Values.postgresql.port | default "5432" | quote }} - name: PW2_PGDATABASE @@ -155,6 +164,7 @@ spec: value: {{ .Values.postgresql.password | default "pgwatch2" }} - name: PW2_PGSSL value: {{ .Values.postgresql.ssl | default "False" | quote}} + {{ if eq .Values.storage "influx" }} - name: PW2_IHOST value: {{ include "pgwatch2.fullname" . }}-influxdb - name: PW2_IPORT @@ -167,6 +177,14 @@ spec: value: {{ .Values.influxdb.password | default "pgwatch2" }} - name: PW2_ISSL value: {{ .Values.influxdb.ssl | default "False" | quote }} + {{ else if eq .Values.storage "postgres" }} + - name: PW2_DATASTORE + value: {{ .Values.storage | quote }} + - name: PW2_PG_METRIC_STORE_CONN_STR + value: {{ printf "postgresql://%s:%s@%s:%s/%s" (.Values.postgresql.user | default "pgwatch2") (.Values.postgresql.password | default "pgwatch2" ) ( .Values.postgresql.host | default (printf "%s-postgresql" (include "pgwatch2.fullname" .))) (.Values.postgresql.port | default "5432") (.Values.postgres_storage.database | default "pgwatch2_metrics" ) }} + - name: PW2_PG_RETENTION_DAYS + value: {{ .Values.postgres_storage.retention_days | default "14" | quote }} + {{ end }} - name: PW2_INTERNAL_STATS_PORT value: {{ .Values.daemon.port | default "8081" | quote }} - name: PW2_WEBNOANONYMOUS diff --git a/helm-chart/templates/grafana-dashboards.yaml b/helm-chart/templates/grafana-dashboards.yaml index 213d9864..cfc396d5 100644 --- a/helm-chart/templates/grafana-dashboards.yaml +++ b/helm-chart/templates/grafana-dashboards.yaml @@ -1,12 +1,12 @@ +{{ range $path, $_ := .Files.Glob (printf "grafana_dashboards/%s/v%s/**.json" (include "pgwatch2-storage" .) (substr 0 1 .Subcharts.grafana.Chart.AppVersion)) }} --- apiVersion: v1 kind: ConfigMap metadata: - name: grafana-dashboards + name: grafana-dashboards-{{ regexReplaceAll ".*/" ($path | replace "/dashboard.json" "") "" }} labels: grafana_dashboard: "1" data: - {{ range $path, $_ := .Files.Glob (printf "grafana_dashboards/influxdb/v%s/**.json" (substr 0 1 .Subcharts.grafana.Chart.AppVersion)) }} {{ regexReplaceAll ".*/" ($path | replace "/dashboard.json" ".json") "" }}: |{{ printf "\n" }} {{- $.Files.Get $path | indent 4}} - {{ end }} +{{ end }} diff --git a/helm-chart/templates/ingress.yaml b/helm-chart/templates/ingress.yaml index bf652a25..bd5c0178 100644 --- a/helm-chart/templates/ingress.yaml +++ b/helm-chart/templates/ingress.yaml @@ -7,7 +7,7 @@ {{- $ingressPath := .Values.webui.ingress.path -}} {{- $ingressPathType := .Values.webui.ingress.pathType -}} {{- $extraPaths := .Values.webui.ingress.extraPaths -}} -apiVersion: {{ include "webui.ingress.apiVersion" . }} +apiVersion: {{ include "pgwatch2.ingress.apiVersion" . }} kind: Ingress metadata: name: {{ $fullName }} diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index f78e521c..10ecac00 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -1,4 +1,7 @@ -storage: influxdb +storage: influx +# Only required if storage is postgres +postgres_storage: + database: &metricsdatabase pgwatch2_metrics metrics_preset: name: remotedba @@ -36,6 +39,8 @@ bootstrap: tag: latest postgresql: + user: &postgresuser pgwatch2 + password: &postgrespassword pgwatch2 primary: initdb: # This value file assumes that we are installing the helm chart with the name "pgwatch2" @@ -43,6 +48,7 @@ postgresql: scriptsConfigMap: pgwatch2-initdb influxdb: + enabled: true service: type: LoadBalancer @@ -95,6 +101,9 @@ webui: grafana: + sidecar: + dashboards: + enabled: true enabled: True service: type: LoadBalancer @@ -147,7 +156,7 @@ grafana: email = "email" grafana.ini: dashboards: - default_home_dashboard_path: "/var/lib/grafana/dashboards/default/health-check.json" + default_home_dashboard_path: "/tmp/dashboards/health-check.json" auth.ldap: enabled: true config_file: /etc/grafana/ldap.toml @@ -166,21 +175,23 @@ grafana: httpMode: GET secureJsonData: password: pgwatch2 - dashboardProviders: - dashboardproviders.yaml: - apiVersion: 1 - providers: - - name: 'default' - orgId: 1 - folder: '' - type: file - disableDeletion: false - editable: true - options: - path: /var/lib/grafana/dashboards/default - - dashboardsConfigMaps: - default: "grafana-dashboards" + ## Postgresql + # - name: Postgres + # type: postgres + # url: pgwatch2-postgresql:5432 + # isDefault: true + # database: *metricsdatabase + # user: *postgresuser + # secureJsonData: + # password: *postgrespassword + # jsonData: + # sslmode: 'disable' # disable/require/verify-ca/verify-full + # maxOpenConns: 0 # Grafana v5.4+ + # maxIdleConns: 2 # Grafana v5.4+ + # connMaxLifetime: 14400 # Grafana v5.4+ + # postgresVersion: 1200 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10 + # timescaledb: false + image: repository: grafana/grafana tag: latest