Skip to content

Emit Prometheus 0.0.4 format version in /metrics Content-Type - #29

Merged
jantman merged 1 commit into
mainfrom
robot-army/issue-26-metrics-endpoint-should-set-content
Sep 20, 2026
Merged

jantman merged 1 commit into
mainfrom
robot-army/issue-26-metrics-endpoint-should-set-content

Conversation

@jantman

@jantman jantman commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

Fixes #26.

Problem

/metrics returned Content-Type: text/plain; charset=utf-8, with no exposition format version. Prometheus v2 tolerated that and silently fell back to the 0.0.4 text format; Prometheus v3 fails the scrape outright when the Content-Type names no recognized format version.

Change

kiosk_show_replacement/metrics.py now responds with text/plain; version=0.0.4; charset=utf-8, held in a new module-level PROMETHEUS_CONTENT_TYPE constant.

Two deviations from the fix sketched in the issue, both deliberate:

  • No prometheus_client dependency. The issue suggested Response(generate_latest(registry), mimetype=CONTENT_TYPE_LATEST). This module hand-builds its exposition text and has no prometheus_client registry at all, so generate_latest() does not apply without rewriting the whole collector — well beyond what the issue asks for. Adding a runtime dependency solely to import a string constant did not seem worth it, so the value is defined locally with a comment recording that it is identical to CONTENT_TYPE_LATEST.
  • content_type= instead of mimetype=. Werkzeug appends its own charset to a mimetype, so passing the full value there duplicates the parameter. Worth noting: the previous code hit this too and was actually emitting text/plain; charset=utf-8; charset=utf-8.

Verification

  • New unit test asserts the exact header value.

  • Full backend suite: 687 passed, 1 skipped.

  • nox -s format / lint / type_check all clean (mypy: no issues in 36 files).

  • Checked against a running dev server:

    $ curl -si http://127.0.0.1:5000/metrics | head -4
    HTTP/1.1 200 OK
    Server: Werkzeug/3.1.5 Python/3.14.7
    Date: Sun, 20 Sep 2026 15:16:38 GMT
    Content-Type: text/plain; version=0.0.4; charset=utf-8
    

Not covered here

The issue's remaining two checkboxes are dm-puppet changes and live-infrastructure steps that belong in that repository, not this one:

  • release a new image and bump the dmpuppet::internals::kiosk_show tag
  • drop fallback_scrape_protocol: PrometheusText0.0.4 from the kiosk job in prometheus.yml, then confirm the target still scrapes on Prometheus v3

Those should follow once this merges and an image is cut.

🤖 Generated with Claude Code

https://claude.ai/code/session_019XAhg2bNjxS9PMFUtTVT1J

The /metrics endpoint returned `Content-Type: text/plain; charset=utf-8`,
with no exposition format version. Prometheus v2 tolerated that and fell
back to the 0.0.4 text format, but Prometheus v3 fails the scrape outright
when the Content-Type carries no recognized format version. This surfaced
during the DecaturMakers monitoring stack's v2 -> v3 upgrade, where the
`kiosk` scrape job needed a `fallback_scrape_protocol` shim to keep working.

The endpoint now sets `text/plain; version=0.0.4; charset=utf-8`, matching
prometheus_client's CONTENT_TYPE_LATEST, as a new module-level
PROMETHEUS_CONTENT_TYPE constant.

Two notes on the implementation:

- The issue suggested importing CONTENT_TYPE_LATEST from prometheus_client.
  This module hand-builds its exposition text and has no prometheus_client
  registry, so `generate_latest(registry)` does not apply without rewriting
  the whole collector. Rather than add a runtime dependency solely for a
  string constant, the value is defined locally with a comment recording
  that it is the same value.

- The response is now built with `content_type=` instead of `mimetype=`.
  Werkzeug appends its own charset to a mimetype, so passing the full value
  as a mimetype produced a duplicated parameter -- the previous code was in
  fact emitting `text/plain; charset=utf-8; charset=utf-8`.

Adds a test asserting the exact Content-Type header.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XAhg2bNjxS9PMFUtTVT1J
@github-actions

Copy link
Copy Markdown

Coverage

Coverage Report
FileStmtsMissCoverMissing
__init__.py100100% 
app.py1383177%35, 75, 193, 235–236, 238–239, 241–244, 256–258, 261, 263, 273, 275, 278–279, 282–285, 289–292, 295–296, 299
database_utils.py17013023%30–36, 41–47, 57, 59–61, 64–65, 67–69, 72–74, 77, 79, 88, 90–93, 95, 116, 118, 121–123, 126, 129, 131–132, 134–135, 137–139, 142–146, 159, 161–164, 167, 177, 186–187, 190, 219, 247, 250, 283–284, 286–287, 289–292, 301, 303, 308, 310, 315, 318–319, 324, 326, 333, 336–337, 339–340, 347–348, 356, 361, 384–387, 390, 393–396, 399–401, 404–406, 408–409, 411–413, 426–429, 431–433, 436, 439, 442, 445–447, 449–450, 452–454
db_resilience.py122397%186, 256, 280
exceptions.py1100100% 
health.py1121685%81–83, 86–87, 98, 119–120, 134, 147, 158–160, 170–172
ical_parser.py113694%85, 120, 124, 127, 134, 197
ical_service.py1701889%80, 82–85, 87, 127–128, 130, 233, 342, 368–369, 375, 382, 454, 456, 460
logging_config.py81396%110, 112, 190
metrics.py237697%433–434, 478–479, 489–490
middleware.py36683%77, 95–96, 109–110, 117
migration_utils.py73730%8–9, 11–12, 15, 18–19, 26–32, 34–35, 46–48, 50–55, 57–58, 68–70, 72–77, 79–80, 90–96, 98–99, 105–108, 110–111, 117–120, 122–123, 133–138, 141, 144, 151, 153–154, 156, 161–162
sse.py1675865%45, 47–48, 50–51, 53–54, 57–59, 61–62, 65–66, 68, 118, 120–122, 204, 210–211, 213–214, 218, 225, 227–228, 230, 232, 234, 309, 322, 324, 332, 335–336, 338, 341, 345–346, 348, 352–353, 355, 358–359, 361, 365, 369–370, 372, 383, 395–398, 449
storage.py3314985%88–90, 156–157, 163, 168, 259–261, 489–491, 512, 526, 573–576, 580, 604–606, 617–619, 675–676, 680, 701–703, 743–746, 774–775, 777–783, 785, 832–833, 860
storage_resilience.py1635367%68–69, 203–204, 211, 213–219, 266–270, 303–305, 334, 347–348, 352–358, 362–364, 382, 384–389, 391–401, 403, 405
validation.py1381787%221, 223, 229, 263, 265–266, 268–269, 337–342, 348, 376, 413
api
   __init__.py10280%27, 38
   helpers.py772074%170, 179, 222, 230, 235, 243, 248, 256, 261, 270, 275, 283–286, 293, 301–304
   v1.py157664559%89, 177–179, 219, 225, 231, 240, 262, 290–292, 304, 341, 345, 361–364, 455, 488, 492, 495, 515–516, 519, 529, 533, 537, 656–658, 661, 675, 679, 683, 687, 752, 755–756, 761–762, 765–766, 768–769, 772–773, 776, 789, 800, 823–826, 842, 846, 850, 876–879, 889, 893, 897, 901, 905, 926, 933–935, 952–957, 975, 979, 998–1002, 1025, 1040–1042, 1057, 1064–1066, 1080, 1089–1091, 1108, 1119–1121, 1131, 1135, 1140, 1145, 1150, 1170–1171, 1187–1190, 1200, 1204, 1208–1210, 1220, 1224, 1228, 1244–1245, 1249, 1262, 1319–1322, 1329–1332, 1334–1336, 1338–1340, 1343–1344, 1347–1350, 1353–1354, 1357, 1365–1366, 1368, 1371, 1382, 1387, 1390, 1394–1398, 1401, 1411, 1415, 1426–1429, 1441–1444, 1446–1448, 1450–1451, 1453–1454, 1456, 1459, 1461–1464, 1471–1474, 1476–1478, 1480–1481, 1483–1485, 1487, 1490, 1492–1495, 1512, 1540–1542, 1549–1551, 1553, 1555–1557, 1576, 1578–1581, 1584, 1587–1592, 1595, 1598, 1600, 1602–1606, 1618–1621, 1623, 1626, 1628, 1630–1632, 1639–1642, 1644–1646, 1649–1650, 1653, 1656–1657, 1660, 1673–1674, 1676, 1679, 1683–1693, 1700–1703, 1705–1707, 1709–1710, 1712, 1714–1716, 1723–1726, 1728–1730, 1732–1733, 1735–1737, 1740, 1742, 1749–1751, 1753–1766, 1768–1769, 1771, 1774, 1776–1786, 1793–1796, 1798–1800, 1802–1803, 1805–1807, 1809, 1812, 1814–1817, 1828–1831, 1833–1835, 1837–1839, 1841–1842, 1844–1845, 1848–1850, 1852, 1855, 1857–1862, 1865, 1872–1875, 1877–1879, 1881–1882, 1884–1887, 1889–1891, 1893–1894, 1897–1899, 1901–1905, 1908, 1910–1911, 1914, 1916–1917, 1919–1920, 1922, 1924, 1928, 1935, 1939–1942, 1984, 2064–2066, 2069–2070, 2078, 2084, 2092, 2101, 2122, 2126, 2156–2158, 2160–2162, 2170, 2180, 2184, 2223–2225, 2227–2229, 2237, 2264, 2272, 2316–2318, 2320–2322, 2324–2326, 2330, 2357, 2377, 2387, 2405–2407, 2409–2411, 2419, 2457–2459, 2467, 2482, 2490, 2514, 2522, 2534–2536, 2546, 2550, 2554, 2559, 2564–2565, 2570, 2586, 2598–2600, 2642, 2650, 2677–2679, 2689, 2700–2701, 2709–2711, 2736, 2738, 2741, 2745, 2747, 2749–2754, 2760, 2762–2764, 2767, 2772–2773, 2775, 2779, 2781–2783, 2799, 2801, 2803, 2807, 2813–2814, 2816–2822, 2825–2826, 2829–2837, 2840–2848, 2850–2851, 2854–2855, 2858–2859, 2862–2868, 2872, 2874, 2876–2877, 2880, 2896, 2950–2952, 2959–2960, 2962–2964, 2966–2968, 2971, 2980, 2982, 2986, 2996–2998, 3021, 3050–3052, 3172–3173, 3180–3181, 3183–3185, 3187–3189, 3191, 3194, 3197, 3200–3202, 3204–3211, 3213–3214, 3216–3221, 3223, 3226–3227, 3229–3230, 3243, 3246–3247, 3251, 3253–3254, 3266–3267, 3271, 3275, 3286–3288, 3291
auth
   __init__.py40100% 
   decorators.py481275%54, 106, 122, 132–134, 136, 148–149, 151, 153–154
   views.py831878%73, 75–77, 120–121, 162–163, 165–166, 172, 240–241, 244–245, 254, 258, 267
cli
   __init__.py10100% 
   init_db.py856721%41–46, 48–49, 52–55, 57–58, 61–62, 65–66, 68, 73, 75–79, 81–82, 85–92, 94, 96, 100, 102–103, 141–142, 145, 148–151, 153–157, 159, 162, 171–173, 176–177, 179–183, 185, 187–188
   main.py351557%17, 28, 30–32, 41, 43–44, 53–54, 56, 58–59, 61, 66
config
   __init__.py580100% 
dashboard
   __init__.py20100% 
   views.py561769%88–91, 151, 153–157, 159, 161, 163, 165, 177, 188–189
display
   __init__.py20100% 
   views.py1514768%31, 33, 38, 53, 171–172, 187, 195, 230–231, 239, 273, 275–276, 278, 280–281, 283, 285, 306–308, 311–312, 314, 325, 333–334, 343, 354, 356–357, 364, 366, 368, 375, 388, 391, 410, 413, 415–416, 423, 425, 427, 433, 444
main
   __init__.py17170%10, 12, 14, 17, 20–21, 30, 33–35, 42, 45, 52, 55–56, 63, 69
models
   __init__.py4794391%126, 141, 263, 405, 407, 414, 431, 559, 584, 592, 594, 601, 609, 617, 725, 792, 799, 827, 885, 890, 924–927, 930, 1096, 1112, 1115, 1117, 1165–1166, 1168–1173, 1179, 1181, 1200, 1203, 1210, 1213
slideshow
   __init__.py503432%43, 46, 52–54, 56–58, 61–64, 66, 73–77, 80–82, 84, 90–91, 97, 103, 105, 107–112, 114
static
   __init__.py00100% 
templates
   __init__.py00100% 
utils
   __init__.py523532%43–46, 49–52, 65, 67–69, 71–72, 75–79, 92–97, 110–111, 121, 135–137, 140, 143–144, 146
TOTAL4957144170% 

Tests Skipped Failures Errors Time
688 1 💤 0 ❌ 0 🔥 56.401s ⏱️

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code review
From Claude, via the Claude PR Review workflow -- hence github-actions[bot].

This PR fixes /metrics to emit Content-Type: text/plain; version=0.0.4; charset=utf-8 (via a new PROMETHEUS_CONTENT_TYPE constant and content_type= instead of mimetype=) so Prometheus v3 accepts the scrape, and adds a unit test asserting the exact header value. Four independent review passes (two CLAUDE.md compliance, two bug/security/logic) found no issues — the change is small, correct, and well-tested.

No issues found. Checked for bugs and CLAUDE.md compliance.


🔎 2m 17s · 11 turns · $0.6320 · run log

@jantman
jantman merged commit 9959e0b into main Sep 20, 2026
8 checks passed
@jantman
jantman deleted the robot-army/issue-26-metrics-endpoint-should-set-content branch September 20, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/metrics endpoint should set Content-Type: text/plain; version=0.0.4 (Prometheus exposition format)

1 participant