Multi-Tenant - 16 - Exemplo de NFSe para o Ambiente Nacional em Golang
Boa, vamos fechar com a parte de dashboard no Grafana pra NFS-e 😄
Vou assumir:
-
Você vai usar o PostgreSQL como data source no Grafana.
-
As views já existem:
vw_nfse_dashboard_diariovw_nfse_dashboard_30d
A ideia é ter 1 dashboard NFSe com:
- Filtro por
tenant_id - Visão de volume
- Taxa de erro
- Quebra por tipo de erro técnico
- Lista operacional de pendências
1. Variáveis do dashboard (filtros)
1.1. Variável tenant
Tipo: Query
-
Data source: seu Postgres
-
Query:
SELECT DISTINCT tenant_id AS "__value", tenant_id::text AS "__text"
FROM public.nfse
ORDER BY tenant_id;
-
Multi-value: ON
-
Include “All”: ON
Assim você filtra o dashboard inteiro por um ou mais tenants.
2. Layout sugerido do dashboard
Sugestão de organização:
-
Linha 1 – KPI geral (últimos 30 dias)
-
Total de NFS-e
-
Total autorizado
-
Taxa de erro (%)
-
-
Linha 2 – Gráficos por dia
-
NFS-e emitidas por dia
-
Taxa de erro por dia
-
-
Linha 3 – Erros por substatus técnico
-
Barras por
substatus_tecnico -
Tabela de detalhe
-
-
Linha 4 – Operação
-
Tabela de NFS-e pendentes de transmissão
-
Tabela de últimas rejeitadas/erros internos
-
Vou te dar exemplos de queries para cada painel.
3. Painéis e queries (usando vw_nfse_dashboard_30d)
3.1. KPI: Total de NFS-e (últimos 30 dias)
Tipo de painel: Stat
Query:
SELECT
SUM(total_notas) AS total_nfse
FROM public.vw_nfse_dashboard_30d
WHERE (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}));
No Grafana, configure:
-
Value →
total_nfse -
Unidade: none (ou “short”).
3.2. KPI: Total Autorizadas (últimos 30 dias)
Tipo: Stat
SELECT
SUM(qtd_autorizadas) AS total_autorizadas
FROM public.vw_nfse_dashboard_30d
WHERE (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}));
3.3. KPI: Taxa de erro total (últimos 30 dias)
Tipo: Stat
SELECT
ROUND(
(SUM(qtd_rejeitadas + qtd_erro_interno)::numeric
/ NULLIFnumeric * 100
, 2) AS taxa_erro_percent
FROM public.vw_nfse_dashboard_30d
WHERE (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}));
Configura a unidade como percent (%).
4. Gráficos de linha por dia
4.1. NFS-e emitidas por dia (últimos 30 dias)
Tipo: Time series
Query:
SELECT
dia::timestamp AS time,
SUM(total_notas) AS value
FROM public.vw_nfse_dashboard_30d
WHERE (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}))
GROUP BY dia
ORDER BY dia;
-
Campo de tempo:
time -
Campo de valor:
value -
Legenda: “Total de NFS-e/dia”.
4.2. Taxa de erro por dia
Tipo: Time series
SELECT
dia::timestamp AS time,
ROUND(
(SUM(qtd_rejeitadas + qtd_erro_interno)::numeric
/ NULLIFnumeric * 100
, 2) AS value
FROM public.vw_nfse_dashboard_30d
WHERE (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}))
GROUP BY dia
ORDER BY dia;
-
Unidade: percent (%).
-
Dá um gráfico de “qualidade da emissão” ao longo do tempo.
5. Erros por substatus_tecnico
5.1. Gráfico de barras: erros internos por substatus
Tipo: Bar chart
Query:
SELECT
substatus_tecnico AS label,
SUM(qtd_erro_interno) AS value
FROM public.vw_nfse_dashboard_30d
WHERE situacao = 'erro_interno'
AND (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}))
GROUP BY substatus_tecnico
ORDER BY value DESC;
-
Eixo X:
label -
Eixo Y:
value
Isso te mostra rapidamente se está explodindo timeout_ws, ssl_error, config_invalida, etc.
5.2. Tabela de detalhe por substatus técnico
Tipo: Table
SELECT
dia,
tenant_id,
situacao,
substatus_tecnico,
total_notas,
qtd_erro_interno
FROM public.vw_nfse_dashboard_30d
WHERE situacao = 'erro_interno'
AND (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}))
ORDER BY dia DESC, qtd_erro_interno DESC;
Você pode:
-
Ordenar por dia desc.
-
Mostrar colunas:
dia,substatus_tecnico,qtd_erro_interno.
6. Painéis operacionais (pegando direto da nfse)
6.1. Tabela: NFS-e pendentes de transmissão
Tipo: Table
Query:
SELECT
id,
tenant_id,
numero_nfse,
serie,
data_emissao,
situacao,
substatus_tecnico,
detalhe_tecnico,
valor_servicos
FROM public.nfse
WHERE situacao = 'pendente_transmissao'
AND (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}))
ORDER BY data_emissao ASC
LIMIT 200;
- Isso te dá a “fila travada” se algo estiver errado.
6.2. Tabela: últimas rejeitadas / erro_interno
Tipo: Table
SELECT
id,
tenant_id,
numero_nfse,
serie,
data_emissao,
situacao,
substatus_tecnico,
codigo_mensagem_retorno,
mensagem_retorno,
detalhe_tecnico,
valor_servicos
FROM public.nfse
WHERE situacao IN ('rejeitada', 'erro_interno')
AND (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}))
ORDER BY data_emissao DESC
LIMIT 200;
Essa tabela é ouro para suporte: você enxerga o que falhou, como o provedor descreveu (mensagem/código) e ainda vê o substatus_tecnico da integração.
7. Extra: separando por multi-schema (se quiser no futuro)
Se lá na frente você tiver NFS-e em vários schemas (ex: erp_nfse, saas_nfse, etc.), você pode:
-
Criar views no schema
publicque só fazemUNION ALLdas tabelas de cada schema. -
Ou adicionar uma coluna
origemna view ('erp','saas', etc.) e usar mais uma variável no Grafana (origem).
Algo dessa linha:
CREATE OR REPLACE VIEW public.vw_nfse_all AS
SELECT 'erp' AS origem, * FROM erp_nfse.nfse
UNION ALL
SELECT 'saas' AS origem, * FROM saas_nfse.nfse;
E aí todas as views de dashboard passam a apontar para vw_nfse_all em vez de nfse direto.
Segue um modelo de dashboard JSON do Grafana já com:
-
Variável
tenant -
KPIs (total, autorizadas, taxa de erro)
-
Séries temporais (total/dia e taxa de erro/dia)
-
Erros por
substatus_tecnico -
Tabelas operacionais (pendentes e últimas com erro)
⚠️ IMPORTANTE:
Antes de importar no Grafana, você precisa:
-
Criar/identificar o data source PostgreSQL que aponta para seu banco.
-
Trocar em cada painel o campo
"uid": "YOUR_POSTGRES_UID"
pelouidreal do seu data source (pega em Configuration → Data sources).
{
"id": null,
"uid": "nfse-dashboard-001",
"title": "NFSe - Visão Geral",
"tags": ["nfse", "fiscal", "multi-tenant"],
"timezone": "browser",
"schemaVersion": 39,
"version": 1,
"refresh": "30s",
"style": "dark",
"editable": true,
"panels": [
{
"type": "row",
"title": "KPIs - Últimos 30 dias",
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
"collapsed": false,
"id": 1,
"panels": []
},
{
"id": 2,
"type": "stat",
"title": "Total de NFSe (30d)",
"gridPos": { "h": 4, "w": 8, "x": 0, "y": 1 },
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT\n SUM(total_notas) AS total_nfse\nFROM public.vw_nfse_dashboard_30d\nWHERE (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}));",
"hide": false
}
],
"options": {
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"orientation": "auto",
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"textMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
}
},
{
"id": 3,
"type": "stat",
"title": "NFSe autorizadas (30d)",
"gridPos": { "h": 4, "w": 8, "x": 8, "y": 1 },
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT\n SUM(qtd_autorizadas) AS total_autorizadas\nFROM public.vw_nfse_dashboard_30d\nWHERE (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}));",
"hide": false
}
],
"options": {
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"orientation": "auto",
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"textMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
}
},
{
"id": 4,
"type": "stat",
"title": "Taxa de erro (30d)",
"gridPos": { "h": 4, "w": 8, "x": 16, "y": 1 },
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT\n ROUND(\n (SUM(qtd_rejeitadas + qtd_erro_interno)::numeric / NULLIFnumeric) * 100\n , 2) AS taxa_erro_percent\nFROM public.vw_nfse_dashboard_30d\nWHERE (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw});",
"hide": false
}
],
"options": {
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"orientation": "auto",
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"textMode": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"decimals": 2
},
"overrides": []
}
},
{
"type": "row",
"title": "Séries temporais",
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
"collapsed": false,
"id": 5,
"panels": []
},
{
"id": 6,
"type": "timeseries",
"title": "NFSe emitidas por dia (30d)",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 },
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"refId": "A",
"format": "time_series",
"rawSql": "SELECT\n dia::timestamp AS \"time\",\n SUM(total_notas) AS value\nFROM public.vw_nfse_dashboard_30d\nWHERE (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}))\nGROUP BY dia\nORDER BY dia;",
"hide": false
}
]
},
{
"id": 7,
"type": "timeseries",
"title": "Taxa de erro por dia (30d)",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 },
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"decimals": 2
},
"overrides": []
},
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"refId": "A",
"format": "time_series",
"rawSql": "SELECT\n dia::timestamp AS \"time\",\n ROUND(\n (SUM(qtd_rejeitadas + qtd_erro_interno)::numeric / NULLIFnumeric) * 100\n , 2) AS value\nFROM public.vw_nfse_dashboard_30d\nWHERE (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw})\nGROUP BY dia\nORDER BY dia;",
"hide": false
}
]
},
{
"type": "row",
"title": "Erros técnicos",
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
"collapsed": false,
"id": 8,
"panels": []
},
{
"id": 9,
"type": "barchart",
"title": "Erros internos por substatus (30d)",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 15 },
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"options": {
"orientation": "horizontal",
"legend": {
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT\n substatus_tecnico AS label,\n SUM(qtd_erro_interno) AS value\nFROM public.vw_nfse_dashboard_30d\nWHERE situacao = 'erro_interno'\n AND (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}))\nGROUP BY substatus_tecnico\nORDER BY value DESC;",
"hide": false
}
]
},
{
"id": 10,
"type": "table",
"title": "Detalhe de erros internos (30d)",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 },
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"options": {
"showHeader": true
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT\n dia,\n tenant_id,\n situacao,\n substatus_tecnico,\n total_notas,\n qtd_erro_interno\nFROM public.vw_nfse_dashboard_30d\nWHERE situacao = 'erro_interno'\n AND (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}))\nORDER BY dia DESC, qtd_erro_interno DESC;",
"hide": false
}
]
},
{
"type": "row",
"title": "Operacional",
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
"collapsed": false,
"id": 11,
"panels": []
},
{
"id": 12,
"type": "table",
"title": "NFSe pendentes de transmissão",
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 24 },
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"options": {
"showHeader": true
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT\n id,\n tenant_id,\n numero_nfse,\n serie,\n data_emissao,\n situacao,\n substatus_tecnico,\n detalhe_tecnico,\n valor_servicos\nFROM public.nfse\nWHERE situacao = 'pendente_transmissao'\n AND (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}))\nORDER BY data_emissao ASC\nLIMIT 200;",
"hide": false
}
]
},
{
"id": 13,
"type": "table",
"title": "Últimas rejeitadas / erro interno",
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 24 },
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 0
},
"overrides": []
},
"options": {
"showHeader": true
},
"targets": [
{
"refId": "A",
"format": "table",
"rawSql": "SELECT\n id,\n tenant_id,\n numero_nfse,\n serie,\n data_emissao,\n situacao,\n substatus_tecnico,\n codigo_mensagem_retorno,\n mensagem_retorno,\n detalhe_tecnico,\n valor_servicos\nFROM public.nfse\nWHERE situacao IN ('rejeitada', 'erro_interno')\n AND (${tenant:raw} IS NULL OR tenant_id IN (${tenant:raw}))\nORDER BY data_emissao DESC\nLIMIT 200;",
"hide": false
}
]
}
],
"templating": {
"list": [
{
"name": "tenant",
"label": "Tenant",
"type": "query",
"datasource": {
"type": "postgres",
"uid": "YOUR_POSTGRES_UID"
},
"query": "SELECT DISTINCT tenant_id AS \"__value\", tenant_id::text AS \"__text\" FROM public.nfse ORDER BY tenant_id;",
"refresh": 1,
"includeAll": true,
"multi": true,
"allValue": null
}
]
},
"time": {
"from": "now-30d",
"to": "now"
},
"timepicker": {
"refresh_intervals": ["10s", "30s", "1m", "5m", "15m", "30m", "1h"]
},
"annotations": {
"list": []
}
}