monitoring: refine mail stats and add send-limit usage

This commit is contained in:
Brad Stein 2026-01-06 02:06:20 -03:00
parent 1fb56bae70
commit d5d2fc66b9
8 changed files with 741 additions and 98 deletions

View File

@ -935,6 +935,15 @@ def build_overview():
{"color": "red", "value": 100}, {"color": "red", "value": 100},
], ],
} }
mail_limit_thresholds = {
"mode": "absolute",
"steps": [
{"color": "green", "value": None},
{"color": "yellow", "value": 70},
{"color": "orange", "value": 85},
{"color": "red", "value": 95},
],
}
mail_api_thresholds = { mail_api_thresholds = {
"mode": "absolute", "mode": "absolute",
"steps": [ "steps": [
@ -946,28 +955,28 @@ def build_overview():
( (
30, 30,
"Mail Bounce Rate (1d)", "Mail Bounce Rate (1d)",
'postmark_outbound_bounce_rate{window="1d"}', 'max(postmark_outbound_bounce_rate{window="1d"})',
"percent", "percent",
mail_bounce_rate_thresholds, mail_bounce_rate_thresholds,
), ),
( (
31, 31,
"Mail Bounced (1d)", "Mail Bounced (1d)",
'postmark_outbound_bounced{window="1d"}', 'max(postmark_outbound_bounced{window="1d"})',
"none", "none",
mail_bounce_count_thresholds, mail_bounce_count_thresholds,
), ),
( (
32, 32,
"Mail Sent (1d)", "Mail Limit Used (30d)",
'postmark_outbound_sent{window="1d"}', "max(postmark_sending_limit_used_percent)",
"none", "percent",
None, mail_limit_thresholds,
), ),
( (
33, 33,
"Postmark API Up", "Postmark API Up",
"postmark_api_up", "max(postmark_api_up)",
"none", "none",
mail_api_thresholds, mail_api_thresholds,
), ),
@ -1875,33 +1884,42 @@ def build_mail_dashboard():
{"color": "green", "value": 1}, {"color": "green", "value": 1},
], ],
} }
limit_thresholds = {
"mode": "absolute",
"steps": [
{"color": "green", "value": None},
{"color": "yellow", "value": 70},
{"color": "orange", "value": 85},
{"color": "red", "value": 95},
],
}
current_stats = [ current_stats = [
( (
1, 1,
"Bounce Rate (1d)", "Bounce Rate (1d)",
'postmark_outbound_bounce_rate{window="1d"}', 'max(postmark_outbound_bounce_rate{window="1d"})',
"percent", "percent",
bounce_rate_thresholds, bounce_rate_thresholds,
), ),
( (
2, 2,
"Bounce Rate (7d)", "Bounce Rate (7d)",
'postmark_outbound_bounce_rate{window="7d"}', 'max(postmark_outbound_bounce_rate{window="7d"})',
"percent", "percent",
bounce_rate_thresholds, bounce_rate_thresholds,
), ),
( (
3, 3,
"Bounced (1d)", "Bounced (1d)",
'postmark_outbound_bounced{window="1d"}', 'max(postmark_outbound_bounced{window="1d"})',
"none", "none",
bounce_count_thresholds, bounce_count_thresholds,
), ),
( (
4, 4,
"Bounced (7d)", "Bounced (7d)",
'postmark_outbound_bounced{window="7d"}', 'max(postmark_outbound_bounced{window="7d"})',
"none", "none",
bounce_count_thresholds, bounce_count_thresholds,
), ),
@ -1923,7 +1941,7 @@ def build_mail_dashboard():
stat_panel( stat_panel(
5, 5,
"Sent (1d)", "Sent (1d)",
'postmark_outbound_sent{window="1d"}', 'max(postmark_outbound_sent{window="1d"})',
{"h": 4, "w": 6, "x": 0, "y": 4}, {"h": 4, "w": 6, "x": 0, "y": 4},
decimals=0, decimals=0,
) )
@ -1932,7 +1950,7 @@ def build_mail_dashboard():
stat_panel( stat_panel(
6, 6,
"Sent (7d)", "Sent (7d)",
'postmark_outbound_sent{window="7d"}', 'max(postmark_outbound_sent{window="7d"})',
{"h": 4, "w": 6, "x": 6, "y": 4}, {"h": 4, "w": 6, "x": 6, "y": 4},
decimals=0, decimals=0,
) )
@ -1940,30 +1958,69 @@ def build_mail_dashboard():
panels.append( panels.append(
stat_panel( stat_panel(
7, 7,
"Postmark API Up", "Limit Used (30d)",
"postmark_api_up", "max(postmark_sending_limit_used_percent)",
{"h": 4, "w": 6, "x": 12, "y": 4}, {"h": 4, "w": 6, "x": 12, "y": 4},
thresholds=limit_thresholds,
unit="percent",
decimals=1,
)
)
panels.append(
stat_panel(
8,
"Send Limit (30d)",
"max(postmark_sending_limit)",
{"h": 4, "w": 6, "x": 18, "y": 4},
decimals=0,
)
)
panels.append(
stat_panel(
9,
"Postmark API Up",
"max(postmark_api_up)",
{"h": 4, "w": 6, "x": 0, "y": 8},
thresholds=api_thresholds, thresholds=api_thresholds,
decimals=0, decimals=0,
) )
) )
panels.append( panels.append(
stat_panel( stat_panel(
8, 10,
"Last Success", "Last Success",
"postmark_last_success_timestamp_seconds", "max(postmark_last_success_timestamp_seconds)",
{"h": 4, "w": 6, "x": 18, "y": 4}, {"h": 4, "w": 6, "x": 6, "y": 8},
unit="dateTimeAsIso", unit="dateTimeAsIso",
decimals=0, decimals=0,
) )
) )
panels.append(
stat_panel(
11,
"Exporter Errors",
"sum(postmark_request_errors_total)",
{"h": 4, "w": 6, "x": 12, "y": 8},
decimals=0,
)
)
panels.append(
stat_panel(
12,
"Limit Used (30d)",
"max(postmark_sending_limit_used)",
{"h": 4, "w": 6, "x": 18, "y": 8},
decimals=0,
)
)
panels.append( panels.append(
timeseries_panel( timeseries_panel(
9, 13,
"Bounce Rate (1d vs 7d)", "Bounce Rate (1d vs 7d)",
"postmark_outbound_bounce_rate", "max by (window) (postmark_outbound_bounce_rate)",
{"h": 8, "w": 12, "x": 0, "y": 8}, {"h": 8, "w": 12, "x": 0, "y": 12},
unit="percent", unit="percent",
legend="{{window}}", legend="{{window}}",
legend_display="table", legend_display="table",
@ -1972,10 +2029,10 @@ def build_mail_dashboard():
) )
panels.append( panels.append(
timeseries_panel( timeseries_panel(
10, 14,
"Bounced (1d vs 7d)", "Bounced (1d vs 7d)",
"postmark_outbound_bounced", "max by (window) (postmark_outbound_bounced)",
{"h": 8, "w": 12, "x": 12, "y": 8}, {"h": 8, "w": 12, "x": 12, "y": 12},
unit="none", unit="none",
legend="{{window}}", legend="{{window}}",
legend_display="table", legend_display="table",
@ -1984,10 +2041,10 @@ def build_mail_dashboard():
) )
panels.append( panels.append(
timeseries_panel( timeseries_panel(
11, 15,
"Sent (1d vs 7d)", "Sent (1d vs 7d)",
"postmark_outbound_sent", "max by (window) (postmark_outbound_sent)",
{"h": 8, "w": 12, "x": 0, "y": 16}, {"h": 8, "w": 12, "x": 0, "y": 20},
unit="none", unit="none",
legend="{{window}}", legend="{{window}}",
legend_display="table", legend_display="table",
@ -1996,10 +2053,10 @@ def build_mail_dashboard():
) )
panels.append( panels.append(
timeseries_panel( timeseries_panel(
12, 16,
"Exporter Errors", "Exporter Errors",
"postmark_request_errors_total", "sum(postmark_request_errors_total)",
{"h": 8, "w": 12, "x": 12, "y": 16}, {"h": 8, "w": 12, "x": 12, "y": 20},
unit="none", unit="none",
) )
) )

View File

@ -19,6 +19,7 @@ WINDOWS = [
Window("today", 0), Window("today", 0),
Window("1d", 1), Window("1d", 1),
Window("7d", 7), Window("7d", 7),
Window("30d", 30),
] ]
API_BASE = os.environ.get("POSTMARK_API_BASE", "https://api.postmarkapp.com").rstrip("/") API_BASE = os.environ.get("POSTMARK_API_BASE", "https://api.postmarkapp.com").rstrip("/")
@ -28,6 +29,12 @@ LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "8000"))
PRIMARY_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN", "").strip() PRIMARY_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN", "").strip()
FALLBACK_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN_FALLBACK", "").strip() FALLBACK_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN_FALLBACK", "").strip()
LIMIT_WINDOW = os.environ.get("POSTMARK_SENDING_LIMIT_WINDOW", "30d").strip()
LIMIT_RAW = os.environ.get("POSTMARK_SENDING_LIMIT", "").strip()
try:
SENDING_LIMIT = float(LIMIT_RAW) if LIMIT_RAW else 0.0
except ValueError:
SENDING_LIMIT = 0.0
EXPORTER_INFO = Info("postmark_exporter", "Exporter build info") EXPORTER_INFO = Info("postmark_exporter", "Exporter build info")
EXPORTER_INFO.info( EXPORTER_INFO.info(
@ -62,6 +69,18 @@ POSTMARK_OUTBOUND_BOUNCE_RATE = Gauge(
"Outbound bounce rate percentage within the selected window", "Outbound bounce rate percentage within the selected window",
labelnames=("window",), labelnames=("window",),
) )
POSTMARK_SENDING_LIMIT_GAUGE = Gauge(
"postmark_sending_limit",
"Configured Postmark sending limit for the active account",
)
POSTMARK_SENDING_LIMIT_USED = Gauge(
"postmark_sending_limit_used",
"Messages sent within the configured send limit window",
)
POSTMARK_SENDING_LIMIT_USED_PERCENT = Gauge(
"postmark_sending_limit_used_percent",
"Percent of the configured send limit used within the limit window",
)
def fetch_outbound_stats(token: str, window: Window) -> dict: def fetch_outbound_stats(token: str, window: Window) -> dict:
@ -83,15 +102,25 @@ def fetch_outbound_stats(token: str, window: Window) -> dict:
def update_metrics(token: str) -> None: def update_metrics(token: str) -> None:
sent_by_window = {}
for window in WINDOWS: for window in WINDOWS:
data = fetch_outbound_stats(token, window) data = fetch_outbound_stats(token, window)
sent = int(data.get("Sent", 0) or 0) sent = int(data.get("Sent", 0) or 0)
bounced = int(data.get("Bounced", 0) or 0) bounced = int(data.get("Bounced", 0) or 0)
rate = (bounced / sent * 100.0) if sent else 0.0 rate = (bounced / sent * 100.0) if sent else 0.0
sent_by_window[window.label] = sent
POSTMARK_OUTBOUND_SENT.labels(window=window.label).set(sent) POSTMARK_OUTBOUND_SENT.labels(window=window.label).set(sent)
POSTMARK_OUTBOUND_BOUNCED.labels(window=window.label).set(bounced) POSTMARK_OUTBOUND_BOUNCED.labels(window=window.label).set(bounced)
POSTMARK_OUTBOUND_BOUNCE_RATE.labels(window=window.label).set(rate) POSTMARK_OUTBOUND_BOUNCE_RATE.labels(window=window.label).set(rate)
POSTMARK_SENDING_LIMIT_GAUGE.set(SENDING_LIMIT)
limit_window_sent = sent_by_window.get(LIMIT_WINDOW, 0)
POSTMARK_SENDING_LIMIT_USED.set(limit_window_sent)
if SENDING_LIMIT:
POSTMARK_SENDING_LIMIT_USED_PERCENT.set(limit_window_sent / SENDING_LIMIT * 100.0)
else:
POSTMARK_SENDING_LIMIT_USED_PERCENT.set(0.0)
def main() -> None: def main() -> None:
if not PRIMARY_TOKEN and not FALLBACK_TOKEN: if not PRIMARY_TOKEN and not FALLBACK_TOKEN:

View File

@ -20,7 +20,7 @@
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounce_rate{window=\"1d\"}", "expr": "max(postmark_outbound_bounce_rate{window=\"1d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -89,7 +89,7 @@
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounce_rate{window=\"7d\"}", "expr": "max(postmark_outbound_bounce_rate{window=\"7d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -158,7 +158,7 @@
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounced{window=\"1d\"}", "expr": "max(postmark_outbound_bounced{window=\"1d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -227,7 +227,7 @@
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounced{window=\"7d\"}", "expr": "max(postmark_outbound_bounced{window=\"7d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -296,7 +296,7 @@
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_sent{window=\"1d\"}", "expr": "max(postmark_outbound_sent{window=\"1d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -357,7 +357,7 @@
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_sent{window=\"7d\"}", "expr": "max(postmark_outbound_sent{window=\"7d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -405,7 +405,7 @@
{ {
"id": 7, "id": 7,
"type": "stat", "type": "stat",
"title": "Postmark API Up", "title": "Limit Used (30d)",
"datasource": { "datasource": {
"type": "prometheus", "type": "prometheus",
"uid": "atlas-vm" "uid": "atlas-vm"
@ -418,7 +418,137 @@
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_api_up", "expr": "max(postmark_sending_limit_used_percent)",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "orange",
"value": 85
},
{
"color": "red",
"value": 95
}
]
},
"unit": "percent",
"custom": {
"displayMode": "auto"
},
"decimals": 1
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "value"
}
},
{
"id": 8,
"type": "stat",
"title": "Send Limit (30d)",
"datasource": {
"type": "prometheus",
"uid": "atlas-vm"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 18,
"y": 4
},
"targets": [
{
"expr": "max(postmark_sending_limit)",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgba(115, 115, 115, 1)",
"value": null
},
{
"color": "green",
"value": 1
}
]
},
"unit": "none",
"custom": {
"displayMode": "auto"
},
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "value"
}
},
{
"id": 9,
"type": "stat",
"title": "Postmark API Up",
"datasource": {
"type": "prometheus",
"uid": "atlas-vm"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 0,
"y": 8
},
"targets": [
{
"expr": "max(postmark_api_up)",
"refId": "A" "refId": "A"
} }
], ],
@ -464,7 +594,7 @@
} }
}, },
{ {
"id": 8, "id": 10,
"type": "stat", "type": "stat",
"title": "Last Success", "title": "Last Success",
"datasource": { "datasource": {
@ -474,12 +604,12 @@
"gridPos": { "gridPos": {
"h": 4, "h": 4,
"w": 6, "w": 6,
"x": 18, "x": 6,
"y": 4 "y": 8
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_last_success_timestamp_seconds", "expr": "max(postmark_last_success_timestamp_seconds)",
"refId": "A" "refId": "A"
} }
], ],
@ -525,7 +655,129 @@
} }
}, },
{ {
"id": 9, "id": 11,
"type": "stat",
"title": "Exporter Errors",
"datasource": {
"type": "prometheus",
"uid": "atlas-vm"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 12,
"y": 8
},
"targets": [
{
"expr": "sum(postmark_request_errors_total)",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgba(115, 115, 115, 1)",
"value": null
},
{
"color": "green",
"value": 1
}
]
},
"unit": "none",
"custom": {
"displayMode": "auto"
},
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "value"
}
},
{
"id": 12,
"type": "stat",
"title": "Limit Used (30d)",
"datasource": {
"type": "prometheus",
"uid": "atlas-vm"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 18,
"y": 8
},
"targets": [
{
"expr": "max(postmark_sending_limit_used)",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgba(115, 115, 115, 1)",
"value": null
},
{
"color": "green",
"value": 1
}
]
},
"unit": "none",
"custom": {
"displayMode": "auto"
},
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "value"
}
},
{
"id": 13,
"type": "timeseries", "type": "timeseries",
"title": "Bounce Rate (1d vs 7d)", "title": "Bounce Rate (1d vs 7d)",
"datasource": { "datasource": {
@ -536,11 +788,11 @@
"h": 8, "h": 8,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 8 "y": 12
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounce_rate", "expr": "max by (window) (postmark_outbound_bounce_rate)",
"refId": "A", "refId": "A",
"legendFormat": "{{window}}" "legendFormat": "{{window}}"
} }
@ -562,7 +814,7 @@
} }
}, },
{ {
"id": 10, "id": 14,
"type": "timeseries", "type": "timeseries",
"title": "Bounced (1d vs 7d)", "title": "Bounced (1d vs 7d)",
"datasource": { "datasource": {
@ -573,11 +825,11 @@
"h": 8, "h": 8,
"w": 12, "w": 12,
"x": 12, "x": 12,
"y": 8 "y": 12
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounced", "expr": "max by (window) (postmark_outbound_bounced)",
"refId": "A", "refId": "A",
"legendFormat": "{{window}}" "legendFormat": "{{window}}"
} }
@ -599,7 +851,7 @@
} }
}, },
{ {
"id": 11, "id": 15,
"type": "timeseries", "type": "timeseries",
"title": "Sent (1d vs 7d)", "title": "Sent (1d vs 7d)",
"datasource": { "datasource": {
@ -610,11 +862,11 @@
"h": 8, "h": 8,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 16 "y": 20
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_sent", "expr": "max by (window) (postmark_outbound_sent)",
"refId": "A", "refId": "A",
"legendFormat": "{{window}}" "legendFormat": "{{window}}"
} }
@ -636,7 +888,7 @@
} }
}, },
{ {
"id": 12, "id": 16,
"type": "timeseries", "type": "timeseries",
"title": "Exporter Errors", "title": "Exporter Errors",
"datasource": { "datasource": {
@ -647,11 +899,11 @@
"h": 8, "h": 8,
"w": 12, "w": 12,
"x": 12, "x": 12,
"y": 16 "y": 20
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_request_errors_total", "expr": "sum(postmark_request_errors_total)",
"refId": "A" "refId": "A"
} }
], ],

View File

@ -802,7 +802,7 @@
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounce_rate{window=\"1d\"}", "expr": "max(postmark_outbound_bounce_rate{window=\"1d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -878,7 +878,7 @@
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounced{window=\"1d\"}", "expr": "max(postmark_outbound_bounced{window=\"1d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -941,7 +941,7 @@
{ {
"id": 32, "id": 32,
"type": "stat", "type": "stat",
"title": "Mail Sent (1d)", "title": "Mail Limit Used (30d)",
"datasource": { "datasource": {
"type": "prometheus", "type": "prometheus",
"uid": "atlas-vm" "uid": "atlas-vm"
@ -954,7 +954,7 @@
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_sent{window=\"1d\"}", "expr": "max(postmark_sending_limit_used_percent)",
"refId": "A" "refId": "A"
} }
], ],
@ -968,20 +968,28 @@
"mode": "absolute", "mode": "absolute",
"steps": [ "steps": [
{ {
"color": "rgba(115, 115, 115, 1)", "color": "green",
"value": null "value": null
}, },
{ {
"color": "green", "color": "yellow",
"value": 1 "value": 70
},
{
"color": "orange",
"value": 85
},
{
"color": "red",
"value": 95
} }
] ]
}, },
"unit": "none", "unit": "percent",
"custom": { "custom": {
"displayMode": "auto" "displayMode": "auto"
}, },
"decimals": 0 "decimals": 1
}, },
"overrides": [] "overrides": []
}, },
@ -1022,7 +1030,7 @@
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_api_up", "expr": "max(postmark_api_up)",
"refId": "A" "refId": "A"
} }
], ],

View File

@ -29,7 +29,7 @@ data:
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounce_rate{window=\"1d\"}", "expr": "max(postmark_outbound_bounce_rate{window=\"1d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -98,7 +98,7 @@ data:
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounce_rate{window=\"7d\"}", "expr": "max(postmark_outbound_bounce_rate{window=\"7d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -167,7 +167,7 @@ data:
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounced{window=\"1d\"}", "expr": "max(postmark_outbound_bounced{window=\"1d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -236,7 +236,7 @@ data:
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounced{window=\"7d\"}", "expr": "max(postmark_outbound_bounced{window=\"7d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -305,7 +305,7 @@ data:
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_sent{window=\"1d\"}", "expr": "max(postmark_outbound_sent{window=\"1d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -366,7 +366,7 @@ data:
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_sent{window=\"7d\"}", "expr": "max(postmark_outbound_sent{window=\"7d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -414,7 +414,7 @@ data:
{ {
"id": 7, "id": 7,
"type": "stat", "type": "stat",
"title": "Postmark API Up", "title": "Limit Used (30d)",
"datasource": { "datasource": {
"type": "prometheus", "type": "prometheus",
"uid": "atlas-vm" "uid": "atlas-vm"
@ -427,7 +427,137 @@ data:
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_api_up", "expr": "max(postmark_sending_limit_used_percent)",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "orange",
"value": 85
},
{
"color": "red",
"value": 95
}
]
},
"unit": "percent",
"custom": {
"displayMode": "auto"
},
"decimals": 1
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "value"
}
},
{
"id": 8,
"type": "stat",
"title": "Send Limit (30d)",
"datasource": {
"type": "prometheus",
"uid": "atlas-vm"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 18,
"y": 4
},
"targets": [
{
"expr": "max(postmark_sending_limit)",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgba(115, 115, 115, 1)",
"value": null
},
{
"color": "green",
"value": 1
}
]
},
"unit": "none",
"custom": {
"displayMode": "auto"
},
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "value"
}
},
{
"id": 9,
"type": "stat",
"title": "Postmark API Up",
"datasource": {
"type": "prometheus",
"uid": "atlas-vm"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 0,
"y": 8
},
"targets": [
{
"expr": "max(postmark_api_up)",
"refId": "A" "refId": "A"
} }
], ],
@ -473,7 +603,7 @@ data:
} }
}, },
{ {
"id": 8, "id": 10,
"type": "stat", "type": "stat",
"title": "Last Success", "title": "Last Success",
"datasource": { "datasource": {
@ -483,12 +613,12 @@ data:
"gridPos": { "gridPos": {
"h": 4, "h": 4,
"w": 6, "w": 6,
"x": 18, "x": 6,
"y": 4 "y": 8
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_last_success_timestamp_seconds", "expr": "max(postmark_last_success_timestamp_seconds)",
"refId": "A" "refId": "A"
} }
], ],
@ -534,7 +664,129 @@ data:
} }
}, },
{ {
"id": 9, "id": 11,
"type": "stat",
"title": "Exporter Errors",
"datasource": {
"type": "prometheus",
"uid": "atlas-vm"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 12,
"y": 8
},
"targets": [
{
"expr": "sum(postmark_request_errors_total)",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgba(115, 115, 115, 1)",
"value": null
},
{
"color": "green",
"value": 1
}
]
},
"unit": "none",
"custom": {
"displayMode": "auto"
},
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "value"
}
},
{
"id": 12,
"type": "stat",
"title": "Limit Used (30d)",
"datasource": {
"type": "prometheus",
"uid": "atlas-vm"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 18,
"y": 8
},
"targets": [
{
"expr": "max(postmark_sending_limit_used)",
"refId": "A"
}
],
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "rgba(115, 115, 115, 1)",
"value": null
},
{
"color": "green",
"value": 1
}
]
},
"unit": "none",
"custom": {
"displayMode": "auto"
},
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "value"
}
},
{
"id": 13,
"type": "timeseries", "type": "timeseries",
"title": "Bounce Rate (1d vs 7d)", "title": "Bounce Rate (1d vs 7d)",
"datasource": { "datasource": {
@ -545,11 +797,11 @@ data:
"h": 8, "h": 8,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 8 "y": 12
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounce_rate", "expr": "max by (window) (postmark_outbound_bounce_rate)",
"refId": "A", "refId": "A",
"legendFormat": "{{window}}" "legendFormat": "{{window}}"
} }
@ -571,7 +823,7 @@ data:
} }
}, },
{ {
"id": 10, "id": 14,
"type": "timeseries", "type": "timeseries",
"title": "Bounced (1d vs 7d)", "title": "Bounced (1d vs 7d)",
"datasource": { "datasource": {
@ -582,11 +834,11 @@ data:
"h": 8, "h": 8,
"w": 12, "w": 12,
"x": 12, "x": 12,
"y": 8 "y": 12
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounced", "expr": "max by (window) (postmark_outbound_bounced)",
"refId": "A", "refId": "A",
"legendFormat": "{{window}}" "legendFormat": "{{window}}"
} }
@ -608,7 +860,7 @@ data:
} }
}, },
{ {
"id": 11, "id": 15,
"type": "timeseries", "type": "timeseries",
"title": "Sent (1d vs 7d)", "title": "Sent (1d vs 7d)",
"datasource": { "datasource": {
@ -619,11 +871,11 @@ data:
"h": 8, "h": 8,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 16 "y": 20
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_sent", "expr": "max by (window) (postmark_outbound_sent)",
"refId": "A", "refId": "A",
"legendFormat": "{{window}}" "legendFormat": "{{window}}"
} }
@ -645,7 +897,7 @@ data:
} }
}, },
{ {
"id": 12, "id": 16,
"type": "timeseries", "type": "timeseries",
"title": "Exporter Errors", "title": "Exporter Errors",
"datasource": { "datasource": {
@ -656,11 +908,11 @@ data:
"h": 8, "h": 8,
"w": 12, "w": 12,
"x": 12, "x": 12,
"y": 16 "y": 20
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_request_errors_total", "expr": "sum(postmark_request_errors_total)",
"refId": "A" "refId": "A"
} }
], ],

View File

@ -811,7 +811,7 @@ data:
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounce_rate{window=\"1d\"}", "expr": "max(postmark_outbound_bounce_rate{window=\"1d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -887,7 +887,7 @@ data:
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_bounced{window=\"1d\"}", "expr": "max(postmark_outbound_bounced{window=\"1d\"})",
"refId": "A" "refId": "A"
} }
], ],
@ -950,7 +950,7 @@ data:
{ {
"id": 32, "id": 32,
"type": "stat", "type": "stat",
"title": "Mail Sent (1d)", "title": "Mail Limit Used (30d)",
"datasource": { "datasource": {
"type": "prometheus", "type": "prometheus",
"uid": "atlas-vm" "uid": "atlas-vm"
@ -963,7 +963,7 @@ data:
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_outbound_sent{window=\"1d\"}", "expr": "max(postmark_sending_limit_used_percent)",
"refId": "A" "refId": "A"
} }
], ],
@ -977,20 +977,28 @@ data:
"mode": "absolute", "mode": "absolute",
"steps": [ "steps": [
{ {
"color": "rgba(115, 115, 115, 1)", "color": "green",
"value": null "value": null
}, },
{ {
"color": "green", "color": "yellow",
"value": 1 "value": 70
},
{
"color": "orange",
"value": 85
},
{
"color": "red",
"value": 95
} }
] ]
}, },
"unit": "none", "unit": "percent",
"custom": { "custom": {
"displayMode": "auto" "displayMode": "auto"
}, },
"decimals": 0 "decimals": 1
}, },
"overrides": [] "overrides": []
}, },
@ -1031,7 +1039,7 @@ data:
}, },
"targets": [ "targets": [
{ {
"expr": "postmark_api_up", "expr": "max(postmark_api_up)",
"refId": "A" "refId": "A"
} }
], ],

View File

@ -39,6 +39,14 @@ spec:
secretKeyRef: secretKeyRef:
name: postmark-exporter name: postmark-exporter
key: relay-password key: relay-password
- name: POSTMARK_SENDING_LIMIT
valueFrom:
secretKeyRef:
name: postmark-exporter
key: sending-limit
optional: true
- name: POSTMARK_SENDING_LIMIT_WINDOW
value: "30d"
- name: POLL_INTERVAL_SECONDS - name: POLL_INTERVAL_SECONDS
value: "60" value: "60"
- name: LISTEN_PORT - name: LISTEN_PORT

View File

@ -26,6 +26,7 @@ data:
Window("today", 0), Window("today", 0),
Window("1d", 1), Window("1d", 1),
Window("7d", 7), Window("7d", 7),
Window("30d", 30),
] ]
API_BASE = os.environ.get("POSTMARK_API_BASE", "https://api.postmarkapp.com").rstrip("/") API_BASE = os.environ.get("POSTMARK_API_BASE", "https://api.postmarkapp.com").rstrip("/")
@ -35,6 +36,12 @@ data:
PRIMARY_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN", "").strip() PRIMARY_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN", "").strip()
FALLBACK_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN_FALLBACK", "").strip() FALLBACK_TOKEN = os.environ.get("POSTMARK_SERVER_TOKEN_FALLBACK", "").strip()
LIMIT_WINDOW = os.environ.get("POSTMARK_SENDING_LIMIT_WINDOW", "30d").strip()
LIMIT_RAW = os.environ.get("POSTMARK_SENDING_LIMIT", "").strip()
try:
SENDING_LIMIT = float(LIMIT_RAW) if LIMIT_RAW else 0.0
except ValueError:
SENDING_LIMIT = 0.0
EXPORTER_INFO = Info("postmark_exporter", "Exporter build info") EXPORTER_INFO = Info("postmark_exporter", "Exporter build info")
EXPORTER_INFO.info( EXPORTER_INFO.info(
@ -69,6 +76,18 @@ data:
"Outbound bounce rate percentage within the selected window", "Outbound bounce rate percentage within the selected window",
labelnames=("window",), labelnames=("window",),
) )
POSTMARK_SENDING_LIMIT_GAUGE = Gauge(
"postmark_sending_limit",
"Configured Postmark sending limit for the active account",
)
POSTMARK_SENDING_LIMIT_USED = Gauge(
"postmark_sending_limit_used",
"Messages sent within the configured send limit window",
)
POSTMARK_SENDING_LIMIT_USED_PERCENT = Gauge(
"postmark_sending_limit_used_percent",
"Percent of the configured send limit used within the limit window",
)
def fetch_outbound_stats(token: str, window: Window) -> dict: def fetch_outbound_stats(token: str, window: Window) -> dict:
@ -90,15 +109,25 @@ data:
def update_metrics(token: str) -> None: def update_metrics(token: str) -> None:
sent_by_window = {}
for window in WINDOWS: for window in WINDOWS:
data = fetch_outbound_stats(token, window) data = fetch_outbound_stats(token, window)
sent = int(data.get("Sent", 0) or 0) sent = int(data.get("Sent", 0) or 0)
bounced = int(data.get("Bounced", 0) or 0) bounced = int(data.get("Bounced", 0) or 0)
rate = (bounced / sent * 100.0) if sent else 0.0 rate = (bounced / sent * 100.0) if sent else 0.0
sent_by_window[window.label] = sent
POSTMARK_OUTBOUND_SENT.labels(window=window.label).set(sent) POSTMARK_OUTBOUND_SENT.labels(window=window.label).set(sent)
POSTMARK_OUTBOUND_BOUNCED.labels(window=window.label).set(bounced) POSTMARK_OUTBOUND_BOUNCED.labels(window=window.label).set(bounced)
POSTMARK_OUTBOUND_BOUNCE_RATE.labels(window=window.label).set(rate) POSTMARK_OUTBOUND_BOUNCE_RATE.labels(window=window.label).set(rate)
POSTMARK_SENDING_LIMIT_GAUGE.set(SENDING_LIMIT)
limit_window_sent = sent_by_window.get(LIMIT_WINDOW, 0)
POSTMARK_SENDING_LIMIT_USED.set(limit_window_sent)
if SENDING_LIMIT:
POSTMARK_SENDING_LIMIT_USED_PERCENT.set(limit_window_sent / SENDING_LIMIT * 100.0)
else:
POSTMARK_SENDING_LIMIT_USED_PERCENT.set(0.0)
def main() -> None: def main() -> None:
if not PRIMARY_TOKEN and not FALLBACK_TOKEN: if not PRIMARY_TOKEN and not FALLBACK_TOKEN: