)]}'
{"/COMMIT_MSG":[{"author":{"_account_id":7847,"name":"Alistair Coles","email":"alistairncoles@gmail.com","username":"acoles"},"change_message_id":"94af78cac5977ac70e611aaaf52dd8cd3d8cec79","unresolved":true,"context_lines":[{"line_number":4,"context_line":"Commit:     Shreeya Deshpande \u003cshreeyad@nvidia.com\u003e"},{"line_number":5,"context_line":"CommitDate: 2026-01-29 10:55:59 -0600"},{"line_number":6,"context_line":""},{"line_number":7,"context_line":"PrometheousClient"},{"line_number":8,"context_line":""},{"line_number":9,"context_line":"Change-Id: I7c12b4893855c3731a825ae7eefc8ee0b6e3f280"},{"line_number":10,"context_line":"Signed-off-by: Shreeya Deshpande \u003cshreeyad@nvidia.com\u003e"}],"source_content_type":"text/x-gerrit-commit-message","patch_set":7,"id":"5d10744f_fffbe130","line":7,"updated":"2026-01-30 17:34:38.000000000","message":"The spelling is \"prometheus\" (no second \u0027o\u0027)\nhttps://prometheus.io/","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"8ea433753461363516daad2f0a75b32496d00d18","unresolved":false,"context_lines":[{"line_number":4,"context_line":"Commit:     Shreeya Deshpande \u003cshreeyad@nvidia.com\u003e"},{"line_number":5,"context_line":"CommitDate: 2026-01-29 10:55:59 -0600"},{"line_number":6,"context_line":""},{"line_number":7,"context_line":"PrometheousClient"},{"line_number":8,"context_line":""},{"line_number":9,"context_line":"Change-Id: I7c12b4893855c3731a825ae7eefc8ee0b6e3f280"},{"line_number":10,"context_line":"Signed-off-by: Shreeya Deshpande \u003cshreeyad@nvidia.com\u003e"}],"source_content_type":"text/x-gerrit-commit-message","patch_set":7,"id":"97474e68_55af86e9","line":7,"in_reply_to":"5d10744f_fffbe130","updated":"2026-02-06 01:20:27.000000000","message":"Acknowledged","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"972b28563a02fa2ef2d7a8bf90e14b0818ffa7d4","unresolved":true,"context_lines":[{"line_number":4,"context_line":"Commit:     Shreeya Deshpande \u003cshreeyad@nvidia.com\u003e"},{"line_number":5,"context_line":"CommitDate: 2026-05-07 21:58:48 +0000"},{"line_number":6,"context_line":""},{"line_number":7,"context_line":"PrometheusClient"},{"line_number":8,"context_line":""},{"line_number":9,"context_line":"Change-Id: I7c12b4893855c3731a825ae7eefc8ee0b6e3f280"},{"line_number":10,"context_line":"Signed-off-by: Clay Gerrard \u003cclay.gerrard@gmail.com\u003e"}],"source_content_type":"text/x-gerrit-commit-message","patch_set":26,"id":"568940d5_81d77612","line":7,"range":{"start_line":7,"start_character":0,"end_line":7,"end_character":16},"updated":"2026-05-15 23:54:38.000000000","message":"This patch deserves a descriptive commit message","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a04019e2e8bb266360d9d0f587f3ed5868404632","unresolved":false,"context_lines":[{"line_number":4,"context_line":"Commit:     Shreeya Deshpande \u003cshreeyad@nvidia.com\u003e"},{"line_number":5,"context_line":"CommitDate: 2026-05-07 21:58:48 +0000"},{"line_number":6,"context_line":""},{"line_number":7,"context_line":"PrometheusClient"},{"line_number":8,"context_line":""},{"line_number":9,"context_line":"Change-Id: I7c12b4893855c3731a825ae7eefc8ee0b6e3f280"},{"line_number":10,"context_line":"Signed-off-by: Clay Gerrard \u003cclay.gerrard@gmail.com\u003e"}],"source_content_type":"text/x-gerrit-commit-message","patch_set":26,"id":"fc20e7bb_7f75835b","line":7,"range":{"start_line":7,"start_character":0,"end_line":7,"end_character":16},"in_reply_to":"568940d5_81d77612","updated":"2026-05-19 03:19:45.000000000","message":"Done","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"}],"/PATCHSET_LEVEL":[{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"8fb8665fbef2e8aa06ac54c1eaf90bc2dcbf1ca7","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":2,"id":"a4ee90d9_1dc37f8a","updated":"2025-12-17 15:54:43.000000000","message":"great start!","commit_id":"e66be0e8192282a9a92a4cc878e1fa3b1a48189d"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1f89dae221ca224f992884d4d85d0f14cb377f9f","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":5,"id":"796b74f4_e6132dba","updated":"2026-01-15 20:22:16.000000000","message":"I can\u0027t find what I would consider to be a GREAT example of a test that I\u0027d like to see used as a pattern for the prometheus client tests.\n\nThe statsd_client tests are a pretty starting point tho:\n\n```\n    def test_label_values_to_str(self):\n        # verify that simple non-str types can be passed as label values\n        conf \u003d {\n            \u0027log_statsd_host\u0027: \u0027myhost1\u0027,\n            \u0027log_statsd_port\u0027: 1235,\n            \u0027statsd_label_mode\u0027: \u0027librato\u0027,\n        }\n        client \u003d statsd_client.get_labeled_statsd_client(conf)\n        labels \u003d {\u0027bool\u0027: True, \u0027number\u0027: 42.1, \u0027null\u0027: None}\n        with mock.patch.object(client, \u0027_send_line\u0027) as mocked:\n            client.update_stats(\u0027metric\u0027, \u002710\u0027, labels\u003dlabels)\n        self.assertEqual(\n            [mock.call(\u0027metric#bool\u003dTrue,null\u003dNone,number\u003d42.1:10|c\u0027)],\n            mocked.call_args_list)\n```\n\nthey follow the standard unittest pattern of \"setup, crank turn, assert\" - simple and to the point!\n\nFor the prometheus-client tests we\u0027ll need a similar \"setup, crank turn, assert\" pattern - where e.g.\n\n * setup \u003d\u003e \"prepare the temp directory where the prom file will go \u0026 create/configure a prom client to write there\"\n * crank turn \u003d\u003e \"call `update_stats(metricname, value, labels\u003dlabels)` a couple times \u0026 then client dumps it\u0027s accumulated stats to a file\"\n * assert \u003d\u003e \"read the file and assert 1) it\u0027s valid prom 2) it has the expected stats\"\n \nThere\u0027s some relinker unit-tests that make assertions on recon data that *sort of* follow that pattern of \"configure recon_cache_path, turn the crank, assert recon looks as expected\":\n\nhttps://github.com/NVIDIA/swift/blob/master/test/unit/cli/test_relinker.py#L2642-L2686","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":7847,"name":"Alistair Coles","email":"alistairncoles@gmail.com","username":"acoles"},"change_message_id":"94af78cac5977ac70e611aaaf52dd8cd3d8cec79","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":7,"id":"4f4833e1_999174e8","updated":"2026-01-30 17:34:38.000000000","message":"I\u0027m not sure what the status of this patch is so I\u0027ve only left a few initial comments","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"6859e20067084277d9dd05383c8f3913bccb0bc7","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":10,"id":"b003f57d_9e298c1e","updated":"2026-02-12 01:42:12.000000000","message":"recheck","commit_id":"25579692835717672c7b2db372c66785b4a895ba"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"81ef57a92e060fb191cf1f5a45694c2aef25f9c6","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":11,"id":"fae8b2bd_3c34b072","updated":"2026-02-12 15:43:51.000000000","message":"recheck","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"795c971ab91775835a923645c604f184ea3f9df3","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":11,"id":"93d1a1f1_f2c1c663","updated":"2026-02-13 20:04:07.000000000","message":"this was in better shape than I thought - i was super confused by the \"metric\" input being a formatted string; but I think we see a path forward!","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"bb10abec2f1db98ebceb473dfcf63d3600208e8f","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":14,"id":"8117675d_792f348c","updated":"2026-03-10 23:04:54.000000000","message":"please go through and try to ack any comments you\u0027ve addressed as you deal with them; if in doing so you notice that some are not addressed please do another change set and try to ack all comments.\n\nIf you\u0027re not clear on what you should be addressed on any particular comment you can add a comment back asking for the information youneed.","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":34892,"name":"ASHWIN A NAIR","display_name":"indianwhocodes","email":"nairashwin952013@gmail.com","username":"indianwhocodes","status":"Nvidia"},"change_message_id":"5e1ceba9e9f643701ee4b8f894a6e5d0c06d6df0","unresolved":true,"context_lines":[],"source_content_type":"","patch_set":20,"id":"0790c879_de857b02","updated":"2026-04-12 00:38:07.000000000","message":"Not entirely done with the review; i might have more comments later on as well","commit_id":"16cb7e13309e42b272d67b3ac2bc8b6c762e3497"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"3b9dd1e8218ecea13dd3ef8ac6814cf4467dcca4","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":20,"id":"8d6a9d6f_866de685","in_reply_to":"0790c879_de857b02","updated":"2026-04-20 20:58:16.000000000","message":"Done","commit_id":"16cb7e13309e42b272d67b3ac2bc8b6c762e3497"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":21,"id":"d49eca87_e784e209","updated":"2026-04-14 19:10:20.000000000","message":"looks great - those improvements are awesome, I think I was not sufficiently clear about how I expect the `self._stats` accumulation and `self.flush()` behavior to work.\n\nI definitely don\u0027t want each call to the update stats methods to open and append to the filename\n\nI didn\u0027t look at tests","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":22,"id":"26761a49_caac34c2","updated":"2026-04-21 20:05:35.000000000","message":"I\u0027d suggest PromClient should have two public methods to pull the data out of `self._stats` - basically two methods `dump` and `write` similar to json|pickle serializers `dumps` and `dump` methods:\n\n```\nall_stats \u003d client.dump_stats()  # return a multi-line string\n```\n\nand\n\n```\ndef write_stats(self, filename\u003dNone):\n    filename \u003d filename or self.filename\n    if not filename:\n        raise ValueError(\u0027where should I write stats!?\u0027)\n    all_stats \u003d self.dump_stats()\n    _atomic_write(all_stats, filename)\n```\n\nN.B. `_atomic_write` doesn\u0027t need `all_stats` nor `filename` to be optional - they\u0027re both required strings and no longer depend on internal object state.","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"972b28563a02fa2ef2d7a8bf90e14b0818ffa7d4","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":26,"id":"f7ff8811_cbd25bf9","updated":"2026-05-15 23:54:38.000000000","message":"I think I\u0027m missing some context to better review this patch. I wasn\u0027t able to tell how one would use all methods in this module, especially `parse_metrics` and `format_metrics_batch`. Perhaps a description in a commit message would be helpful.","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"527e3ee49ac796e2322943a074744d9939f2f613","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":26,"id":"ea082009_ceb7a621","updated":"2026-05-07 22:02:19.000000000","message":"recheck","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":28,"id":"c7147859_4bc568c7","updated":"2026-05-27 05:49:00.000000000","message":"I think this looks great.  I really want see one of s-m-e or dispersion-report consumers get ready to merge and depend on this so have an excuse to merge it!\n\nI think we\u0027re going to want to add the `chmod` to the tempfile so our `.prom` droppings can be read by other process\u0027 users \n\nI think we should enforce quoting of label values from the jump so we don\u0027t have to worry about a migration later\n\nI think we should get rid of some of the un-used/basely-used helpers\n\nI think we could use `(name, value, label)` order consistently when dealing in tuples/*args\n\nI think we could make `PrometheusClient(conf\u003d{\u0027metrics_filename\u0027: \u0027test.prom\u0027}).write_all_stats()` do something useful instead of blow up.\n\nI think we could think harder about `#TYPE` annotations - but w/o a proposal/requirements I\u0027m not how far we\u0027d get.  IMHO, this will work fine.","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":7233,"name":"Matthew Oliver","email":"matt@oliver.net.au","username":"mattoliverau"},"change_message_id":"4a4577c4eaa5ddc710daffe26b37b38f3c602a01","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":30,"id":"d56efbab_6f1a9141","updated":"2026-06-11 01:42:14.000000000","message":"I\u0027m really excited to start having some prom in swift, thanks for working on this!\n\nJust some issue and questions inline.","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":6968,"name":"Christian Schwede","email":"cschwede@nvidia.com","username":"cschwede"},"change_message_id":"7df08d510768028de5814feea7442d796fcb9ae9","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":32,"id":"bb70c791_3f1c9bff","updated":"2026-07-09 12:35:27.000000000","message":"I agree with Clay and Nate - the metrics need to be escaped.\n\nProposal for a fix and a test (test fails without the fix): https://review.opendev.org/c/openstack/swift/+/996642\n\nFeel free to squash this into your patch.","commit_id":"6e25e5319f868f6ac0ac6f9520d58ed93bbc5dc3"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"60de0f1100b17a91300be1dd4aa6fcd105f0fb06","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":32,"id":"3543f5eb_23e0df4a","updated":"2026-07-08 22:45:08.000000000","message":"This is good! I just have one mention about line escaping","commit_id":"6e25e5319f868f6ac0ac6f9520d58ed93bbc5dc3"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"980a37deb79434391acc1b4c8cb829180051e732","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":34,"id":"21420eeb_b798adce","updated":"2026-07-14 22:40:50.000000000","message":"I think we still need a TINY amount of parsing, but I think after this I\u0027m good to +1! See my inline comments please!","commit_id":"b7e62281fb7e807745056fb6f58f589d19327a13"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"f147374cf02d5d9cd300f50c6d0917d9484aa244","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":34,"id":"43b54ee2_37de2975","updated":"2026-07-14 23:26:43.000000000","message":"See 997312: sq? add better parsing for label values, key, and metric names | https://review.opendev.org/c/openstack/swift/+/997312 that fixes the changes I mentioned!","commit_id":"b7e62281fb7e807745056fb6f58f589d19327a13"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"7fc280942f8db115b9316a5d142e0d6564470a85","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":36,"id":"2c9167c6_9041bf86","updated":"2026-07-15 21:05:00.000000000","message":"I should\u0027ve caught these in my last review. However, there is a case where a `label_value` that has `}` in it, does not parse.\n\nI also saw an edge case if the `label_value` has escaped strings in it as-well, pretty low hanging fruit I feel.\nI put squash patches that handle them.","commit_id":"1cc6bd70b44f8332cb1565aeb5812bc2ba24de1f"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"8369f4a3d8c4c3cf3e1363bc08c3b67b97513a34","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":38,"id":"1b572175_14d9c52a","updated":"2026-07-17 00:02:04.000000000","message":"We are getting closer! See my inline comments and 997660: sq? fix prom_metrics parsing issues | https://review.opendev.org/c/openstack/swift/+/997660\n\nNeedles to say, it is tricky to work with grammars and such, but we get closer with each review!","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":40,"id":"57823062_5f58d6fd","updated":"2026-07-21 02:05:10.000000000","message":"I think there\u0027s a good chance this is mergeable (I updated the commit message); I\u0027m mostly reserved influenced by my agent review here:\n\n```\n  Recommendation: -1.\n\n  The three most important issues are:\n\n  - swift/common/prom_metrics.py:295 hard-codes metrics-file mode to 0644, so every refresh can widen an operator’s intended permissions on the scrape file.\n  - swift/common/prom_metrics.py:228 documents config user_* labels as automatically added to all metrics, but record()/add() actually raise ValueError on caller collisions; the prose and runtime contract disagree.\n  - swift/common/prom_metrics.py:53 lands parser/readback branches with no production caller in-tree, and the defensive-contract worker showed the trailing-backslash branch remains uncovered by the new unit file.\n```\n\n1) go ahead and make `PROM_METRICS_FILE_MODE \u003d 0o644` - magic numbers in code suck, so maybe a comment about umask and LBYL both seeming tricky enough to defer until if and only if someone ever complains or has cause to patch the \"default\" prom metric file mode\n2) go ahead and make the user label namespace explicitly reserved (instead of only reserved in violation of configured duplicates) - on the write path duplicate labels should only come up as user-label configuration issue because labels as kwarg is natively a dict you can\u0027t express duplicates as input - on the read path it can continue to be a parse error\n3) I think the \"should blow up on invalid escapes\" WOULD be a reasonable reason to block (except we don\u0027t actually have a use-case for the parser outside of tests/verification), nevertheless - go ahead and give the [un]escape parser another spin, what do you think of something like:\n\n```\n  LITERAL_BACKSLASH \u003d \u0027\\\\\u0027\n\n  _ESCAPES \u003d {\n      LITERAL_BACKSLASH: r\u0027\\\\\u0027,\n      \u0027\"\u0027: r\u0027\\\"\u0027,\n      \u0027\\n\u0027: r\u0027\\n\u0027,\n  }\n\n  _UNESCAPES \u003d {\n      LITERAL_BACKSLASH: LITERAL_BACKSLASH,\n      \u0027\"\u0027: \u0027\"\u0027,\n      \u0027n\u0027: \u0027\\n\u0027,\n  }\n\n\n  def _escape_label_value(value):\n      return \u0027\u0027.join(_ESCAPES.get(ch, ch) for ch in str(value))\n\n\n  def _unescape_label_value(value):\n      out \u003d []\n      escaped \u003d False\n\n      for ch in value:\n          if not escaped:\n              if ch \u003d\u003d LITERAL_BACKSLASH:\n                  escaped \u003d True\n              else:\n                  out.append(ch)\n              continue\n\n          try:\n              out.append(_UNESCAPES[ch])\n          except KeyError:\n              raise ValueError(\n                  \u0027Invalid Prometheus escape sequence: %s%s\u0027\n                  % (LITERAL_BACKSLASH, ch))\n          escaped \u003d False\n\n      if escaped:\n          raise ValueError(\u0027Invalid Prometheus escape: trailing backslash\u0027)\n\n      return \u0027\u0027.join(out)\n\n```\n\nI also think it\u0027s worth it to have some targeted round-trip tests pointed directly at these helpers:\n\n```\nfor crazy_string in examples:\n    self.assertEqual(crazy_string, prom_metrics.unescape(\n        prom_metrics.escape(crazy_string)))\n```","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":38368,"name":"Christian Ohanaja","display_name":"Christian Ohanaja","email":"cohanaja@nvidia.com","username":"cohanaja"},"change_message_id":"42c95aa0519324ba4571a713317cf3958cf7297d","unresolved":false,"context_lines":[],"source_content_type":"","patch_set":40,"id":"b19aafb5_81fe23f7","updated":"2026-07-20 23:32:03.000000000","message":"really good stuff so far! I can see this making our prom flows much easier.","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"}],"swift/common/prom_metrics.py":[{"author":{"_account_id":34892,"name":"ASHWIN A NAIR","display_name":"indianwhocodes","email":"nairashwin952013@gmail.com","username":"indianwhocodes","status":"Nvidia"},"change_message_id":"558a201133e17190fc7a46457a2d50a3381d6c11","unresolved":true,"context_lines":[{"line_number":15,"context_line":""},{"line_number":16,"context_line":"\"\"\" Prometheous Metrics \"\"\""},{"line_number":17,"context_line":""},{"line_number":18,"context_line":"from test import metric_key"},{"line_number":19,"context_line":""},{"line_number":20,"context_line":""},{"line_number":21,"context_line":"def parse_metrics(line):"}],"source_content_type":"text/x-python","patch_set":20,"id":"43429e47_8f74de1a","line":18,"updated":"2026-04-12 00:01:38.000000000","message":"Weird to see a dependency on a test module.\n\ninstead why not DEFINE a private helper inside swift/common/prom_metrics.py (e.g. _prom_key(name, **labels)) or something with a better name which has same return value, so behavior matches read_metrics() without having production code having a `test/` dependency","commit_id":"16cb7e13309e42b272d67b3ac2bc8b6c762e3497"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"5caa9f5a203bdca5ca37c018ea381307cbd3ba3c","unresolved":true,"context_lines":[{"line_number":15,"context_line":""},{"line_number":16,"context_line":"\"\"\" Prometheous Metrics \"\"\""},{"line_number":17,"context_line":""},{"line_number":18,"context_line":"from test import metric_key"},{"line_number":19,"context_line":""},{"line_number":20,"context_line":""},{"line_number":21,"context_line":"def parse_metrics(line):"}],"source_content_type":"text/x-python","patch_set":20,"id":"73e40fc7_08dbdd6b","line":18,"in_reply_to":"43429e47_8f74de1a","updated":"2026-04-14 16:53:37.000000000","message":"i think we wanted to have a common metric_key to use. \nClay and i wrote this to 975948: add prom metrics test helpers | https://review.opendev.org/c/openstack/swift/+/975948 \n@clay.gerrard@gmail.com is there a better place to put this? Do we anticipate using metric_key just for prom metrics?","commit_id":"16cb7e13309e42b272d67b3ac2bc8b6c762e3497"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"7abd8df05b8081785b9541f05ff02331685d4b3f","unresolved":false,"context_lines":[{"line_number":15,"context_line":""},{"line_number":16,"context_line":"\"\"\" Prometheous Metrics \"\"\""},{"line_number":17,"context_line":""},{"line_number":18,"context_line":"from test import metric_key"},{"line_number":19,"context_line":""},{"line_number":20,"context_line":""},{"line_number":21,"context_line":"def parse_metrics(line):"}],"source_content_type":"text/x-python","patch_set":20,"id":"28c108a9_59ec47b3","line":18,"in_reply_to":"54ba9939_129c4cf3","updated":"2026-05-05 15:26:31.000000000","message":"Done","commit_id":"16cb7e13309e42b272d67b3ac2bc8b6c762e3497"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":15,"context_line":""},{"line_number":16,"context_line":"\"\"\" Prometheous Metrics \"\"\""},{"line_number":17,"context_line":""},{"line_number":18,"context_line":"from test import metric_key"},{"line_number":19,"context_line":""},{"line_number":20,"context_line":""},{"line_number":21,"context_line":"def parse_metrics(line):"}],"source_content_type":"text/x-python","patch_set":20,"id":"54ba9939_129c4cf3","line":18,"in_reply_to":"73e40fc7_08dbdd6b","updated":"2026-04-14 19:10:20.000000000","message":"I agree supa weird to see `swift.common` import from `test` - i\u0027m not even sure prod *packages* the test tree - i\u0027m sort of surprised this worked (?)\n\n\u003e behavior matches read_metrics()\n\nright, let\u0027s not duplicate.  Given that prom_metrics needs a helper to read metrics *already* I think it WOULD make sense to \"just\" define it `common.prom_metrics` AND \"re-use THAT\" in tests\n\nLet\u0027s remove the dependency on 975948: add prom metrics test helpers | https://review.opendev.org/c/openstack/swift/+/975948 - move (a refactored more flexible version of?) that code into `common.prom_metrics` in THIS change - and test and merge it as part of introducing `PrometheusClient`","commit_id":"16cb7e13309e42b272d67b3ac2bc8b6c762e3497"},{"author":{"_account_id":34892,"name":"ASHWIN A NAIR","display_name":"indianwhocodes","email":"nairashwin952013@gmail.com","username":"indianwhocodes","status":"Nvidia"},"change_message_id":"5e1ceba9e9f643701ee4b8f894a6e5d0c06d6df0","unresolved":true,"context_lines":[{"line_number":61,"context_line":"    \"\"\""},{"line_number":62,"context_line":"    labels \u003d labels or {}"},{"line_number":63,"context_line":""},{"line_number":64,"context_line":"    if not isinstance(name, str):"},{"line_number":65,"context_line":"        raise ValueError(f\"Metric name must be string, got {type(name)}\")"},{"line_number":66,"context_line":""},{"line_number":67,"context_line":"    if labels and isinstance(labels, dict):"}],"source_content_type":"text/x-python","patch_set":20,"id":"cfb2dacf_f0d2f59c","line":64,"updated":"2026-04-12 00:38:07.000000000","message":"This check could come before we assign labels such that:\n```\n    if not isinstance(name, str):\n        raise ValueError(f\"Metric name must be string, got {type(name)}\")\n    labels \u003d labels or {}\n```","commit_id":"16cb7e13309e42b272d67b3ac2bc8b6c762e3497"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"5caa9f5a203bdca5ca37c018ea381307cbd3ba3c","unresolved":false,"context_lines":[{"line_number":61,"context_line":"    \"\"\""},{"line_number":62,"context_line":"    labels \u003d labels or {}"},{"line_number":63,"context_line":""},{"line_number":64,"context_line":"    if not isinstance(name, str):"},{"line_number":65,"context_line":"        raise ValueError(f\"Metric name must be string, got {type(name)}\")"},{"line_number":66,"context_line":""},{"line_number":67,"context_line":"    if labels and isinstance(labels, dict):"}],"source_content_type":"text/x-python","patch_set":20,"id":"4b54c80b_2450fc73","line":64,"in_reply_to":"cfb2dacf_f0d2f59c","updated":"2026-04-14 16:53:37.000000000","message":"Acknowledged","commit_id":"16cb7e13309e42b272d67b3ac2bc8b6c762e3497"},{"author":{"_account_id":34892,"name":"ASHWIN A NAIR","display_name":"indianwhocodes","email":"nairashwin952013@gmail.com","username":"indianwhocodes","status":"Nvidia"},"change_message_id":"5e1ceba9e9f643701ee4b8f894a6e5d0c06d6df0","unresolved":true,"context_lines":[{"line_number":64,"context_line":"    if not isinstance(name, str):"},{"line_number":65,"context_line":"        raise ValueError(f\"Metric name must be string, got {type(name)}\")"},{"line_number":66,"context_line":""},{"line_number":67,"context_line":"    if labels and isinstance(labels, dict):"},{"line_number":68,"context_line":"        # Sort labels for consistent output"},{"line_number":69,"context_line":"        labels_str \u003d \u0027,\u0027.join("},{"line_number":70,"context_line":"            f\u0027{k}\u003d\"{v}\"\u0027 for k, v in sorted(labels.items())"}],"source_content_type":"text/x-python","patch_set":20,"id":"4fbe5172_dea271c4","line":67,"updated":"2026-04-12 00:38:07.000000000","message":"in L62 we have already initialized lablels as \"labels or {}\" so `isinstance(labels, dict)` seems like a redundant check!","commit_id":"16cb7e13309e42b272d67b3ac2bc8b6c762e3497"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"5caa9f5a203bdca5ca37c018ea381307cbd3ba3c","unresolved":false,"context_lines":[{"line_number":64,"context_line":"    if not isinstance(name, str):"},{"line_number":65,"context_line":"        raise ValueError(f\"Metric name must be string, got {type(name)}\")"},{"line_number":66,"context_line":""},{"line_number":67,"context_line":"    if labels and isinstance(labels, dict):"},{"line_number":68,"context_line":"        # Sort labels for consistent output"},{"line_number":69,"context_line":"        labels_str \u003d \u0027,\u0027.join("},{"line_number":70,"context_line":"            f\u0027{k}\u003d\"{v}\"\u0027 for k, v in sorted(labels.items())"}],"source_content_type":"text/x-python","patch_set":20,"id":"0ce110f9_9055b299","line":67,"in_reply_to":"4fbe5172_dea271c4","updated":"2026-04-14 16:53:37.000000000","message":"true true","commit_id":"16cb7e13309e42b272d67b3ac2bc8b6c762e3497"},{"author":{"_account_id":34892,"name":"ASHWIN A NAIR","display_name":"indianwhocodes","email":"nairashwin952013@gmail.com","username":"indianwhocodes","status":"Nvidia"},"change_message_id":"5e1ceba9e9f643701ee4b8f894a6e5d0c06d6df0","unresolved":false,"context_lines":[{"line_number":74,"context_line":"        return name + \u0027 \u0027 + str(value) + \u0027\\n\u0027"},{"line_number":75,"context_line":""},{"line_number":76,"context_line":""},{"line_number":77,"context_line":"def format_metrics_batch(metrics_list):"},{"line_number":78,"context_line":"    \"\"\""},{"line_number":79,"context_line":"    Format multiple metrics into a Prometheus text format string."},{"line_number":80,"context_line":""}],"source_content_type":"text/x-python","patch_set":20,"id":"03491edb_db92f735","line":77,"updated":"2026-04-12 00:38:07.000000000","message":"nice!","commit_id":"16cb7e13309e42b272d67b3ac2bc8b6c762e3497"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":42,"context_line":"    return name, labels, value"},{"line_number":43,"context_line":""},{"line_number":44,"context_line":""},{"line_number":45,"context_line":"def format_metric(name, value, labels\u003dNone):"},{"line_number":46,"context_line":"    \"\"\""},{"line_number":47,"context_line":"    Format a single metric into Prometheus text format string."},{"line_number":48,"context_line":""}],"source_content_type":"text/x-python","patch_set":21,"id":"d851fe29_c513c86b","line":45,"updated":"2026-04-14 19:10:20.000000000","message":"in `common.statsd_client` we call this something like `_build_line_parts` - we might want to keep the naming convention consistent-ish?","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":42,"context_line":"    return name, labels, value"},{"line_number":43,"context_line":""},{"line_number":44,"context_line":""},{"line_number":45,"context_line":"def format_metric(name, value, labels\u003dNone):"},{"line_number":46,"context_line":"    \"\"\""},{"line_number":47,"context_line":"    Format a single metric into Prometheus text format string."},{"line_number":48,"context_line":""}],"source_content_type":"text/x-python","patch_set":21,"id":"9a06d3d3_8c6aeef7","line":45,"in_reply_to":"d851fe29_c513c86b","updated":"2026-04-28 04:49:45.000000000","message":"Done","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":57,"context_line":"    :param name: Metric name (string)"},{"line_number":58,"context_line":"    :param value: Metric value (int or float)"},{"line_number":59,"context_line":"    :param labels: Optional dict of label key-value pairs"},{"line_number":60,"context_line":"    :returns: Prometheus-formatted metric string"},{"line_number":61,"context_line":"    \"\"\""},{"line_number":62,"context_line":""},{"line_number":63,"context_line":"    if not isinstance(name, str):"}],"source_content_type":"text/x-python","patch_set":21,"id":"16a160c1_94d53013","line":60,"updated":"2026-04-14 19:10:20.000000000","message":"... terminated in a newline\n\nI\u0027m ok with this b/c personally I like files that have even a single line in them to be terminated with a newline","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":57,"context_line":"    :param name: Metric name (string)"},{"line_number":58,"context_line":"    :param value: Metric value (int or float)"},{"line_number":59,"context_line":"    :param labels: Optional dict of label key-value pairs"},{"line_number":60,"context_line":"    :returns: Prometheus-formatted metric string"},{"line_number":61,"context_line":"    \"\"\""},{"line_number":62,"context_line":""},{"line_number":63,"context_line":"    if not isinstance(name, str):"}],"source_content_type":"text/x-python","patch_set":21,"id":"8bcb6461_bd51d276","line":60,"in_reply_to":"16a160c1_94d53013","updated":"2026-04-28 04:49:45.000000000","message":"Acknowledged","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":80,"context_line":"    Format multiple metrics into a Prometheus text format string."},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"    :param metrics_list: List of tuples (name, value) or"},{"line_number":83,"context_line":"                         (name, value, labels_dict)"},{"line_number":84,"context_line":"    :returns: Multi-line Prometheus-formatted string"},{"line_number":85,"context_line":"    \"\"\""},{"line_number":86,"context_line":"    lines \u003d []"}],"source_content_type":"text/x-python","patch_set":21,"id":"e7e61ac7_bd4cf53c","line":83,"updated":"2026-04-14 19:10:20.000000000","message":"nit: the \"length implies format\" is not *ideal* - maybe better to try and take advantage of the \"structured semantic coupling\" of `metrics_name` and `metrics_value` using the `metrics_key` helper (or something like it?)\n\nThen the list values are ALWAYS two tuples, `(metrics_key, metrics_value)` and `metrics_key` ALWAYS has a `metrics_name` and *sometimes* `metrics_key` has non-empty `metrics_labels`?","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ccfc40fce0c2d3d8e0bb87dbdc88aaf7ea4a150b","unresolved":false,"context_lines":[{"line_number":80,"context_line":"    Format multiple metrics into a Prometheus text format string."},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"    :param metrics_list: List of tuples (name, value) or"},{"line_number":83,"context_line":"                         (name, value, labels_dict)"},{"line_number":84,"context_line":"    :returns: Multi-line Prometheus-formatted string"},{"line_number":85,"context_line":"    \"\"\""},{"line_number":86,"context_line":"    lines \u003d []"}],"source_content_type":"text/x-python","patch_set":21,"id":"85c7ef4e_49e159cb","line":83,"in_reply_to":"e7e61ac7_bd4cf53c","updated":"2026-05-07 21:59:18.000000000","message":"Done","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":94,"context_line":"        else:"},{"line_number":95,"context_line":"            raise ValueError(f\"Invalid metric tuple: {metric_data}\")"},{"line_number":96,"context_line":""},{"line_number":97,"context_line":"    return \u0027\u0027.join(lines)"},{"line_number":98,"context_line":""},{"line_number":99,"context_line":""},{"line_number":100,"context_line":"def get_prometheus_client(conf, logger\u003dNone):"}],"source_content_type":"text/x-python","patch_set":21,"id":"94109df3_3bf576a9","line":97,"updated":"2026-04-14 19:10:20.000000000","message":"maybe worth a comment that `format_metrics` returns lines with the new-line attached\n\nalternatively if `format_metric` did NOT append a newline; we could return:\n\n```\n\u0027\\n\u0027.join(lines) + \u0027\\n\u0027\n```\n\n^ to ensure that even a single line file always gets the new line at the end.","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":94,"context_line":"        else:"},{"line_number":95,"context_line":"            raise ValueError(f\"Invalid metric tuple: {metric_data}\")"},{"line_number":96,"context_line":""},{"line_number":97,"context_line":"    return \u0027\u0027.join(lines)"},{"line_number":98,"context_line":""},{"line_number":99,"context_line":""},{"line_number":100,"context_line":"def get_prometheus_client(conf, logger\u003dNone):"}],"source_content_type":"text/x-python","patch_set":21,"id":"dc34de78_45c02055","line":97,"in_reply_to":"94109df3_3bf576a9","updated":"2026-04-28 04:49:45.000000000","message":"maybe worth a comment that format_metrics returns lines with the new-line attached - DONE\nthere will always be a new line in the end! all other called functions ensure that including render amd gen_metrics","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":100,"context_line":"def get_prometheus_client(conf, logger\u003dNone):"},{"line_number":101,"context_line":"    \"\"\""},{"line_number":102,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":103,"context_line":"    \"\"\""},{"line_number":104,"context_line":"    conf \u003d conf or {}"},{"line_number":105,"context_line":"    _stats \u003d {}"},{"line_number":106,"context_line":"    return PrometheusClient(conf, _stats, logger)"}],"source_content_type":"text/x-python","patch_set":21,"id":"c637e977_6274bb40","line":103,"updated":"2026-04-14 19:10:20.000000000","message":"please be thinking about a \"default labels\" configuration similar to how we support that for labeled statsd clients:\n\nhttps://github.com/NVIDIA/swift/blob/master/swift/common/statsd_client.py#L169-L195","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":100,"context_line":"def get_prometheus_client(conf, logger\u003dNone):"},{"line_number":101,"context_line":"    \"\"\""},{"line_number":102,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":103,"context_line":"    \"\"\""},{"line_number":104,"context_line":"    conf \u003d conf or {}"},{"line_number":105,"context_line":"    _stats \u003d {}"},{"line_number":106,"context_line":"    return PrometheusClient(conf, _stats, logger)"}],"source_content_type":"text/x-python","patch_set":21,"id":"54c3031e_28dcced3","line":103,"in_reply_to":"c637e977_6274bb40","updated":"2026-04-28 04:49:45.000000000","message":"Acknowledged","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":102,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":103,"context_line":"    \"\"\""},{"line_number":104,"context_line":"    conf \u003d conf or {}"},{"line_number":105,"context_line":"    _stats \u003d {}"},{"line_number":106,"context_line":"    return PrometheusClient(conf, _stats, logger)"},{"line_number":107,"context_line":""},{"line_number":108,"context_line":""}],"source_content_type":"text/x-python","patch_set":21,"id":"b994bea6_582791d4","line":105,"updated":"2026-04-14 19:10:20.000000000","message":"I don\u0027t think callers should have to pass this in(?) - the whole `metrics_key \u003d\u003e value` data structure is sort of an implementation detail?","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":102,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":103,"context_line":"    \"\"\""},{"line_number":104,"context_line":"    conf \u003d conf or {}"},{"line_number":105,"context_line":"    _stats \u003d {}"},{"line_number":106,"context_line":"    return PrometheusClient(conf, _stats, logger)"},{"line_number":107,"context_line":""},{"line_number":108,"context_line":""}],"source_content_type":"text/x-python","patch_set":21,"id":"ad2c4044_49f2cd9e","line":105,"in_reply_to":"b994bea6_582791d4","updated":"2026-04-28 04:49:45.000000000","message":"Acknowledged","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":108,"context_line":""},{"line_number":109,"context_line":"class AbstractPrometheusClient:"},{"line_number":110,"context_line":"    def __init__(self, conf, _stats, logger\u003dNone):"},{"line_number":111,"context_line":"        self.conf \u003d conf"},{"line_number":112,"context_line":"        self._stats \u003d {}"},{"line_number":113,"context_line":"        self.logger \u003d logger"},{"line_number":114,"context_line":""}],"source_content_type":"text/x-python","patch_set":21,"id":"5b918b14_1060f4ea","line":111,"updated":"2026-04-14 19:10:20.000000000","message":"we should do something like:\n\n```\nself.filename \u003d conf.get(\u0027metrics_filename\u0027)\n```\n\nThen you *can* specify filename in the conf; but it\u0027s not required.","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"3b9dd1e8218ecea13dd3ef8ac6814cf4467dcca4","unresolved":false,"context_lines":[{"line_number":108,"context_line":""},{"line_number":109,"context_line":"class AbstractPrometheusClient:"},{"line_number":110,"context_line":"    def __init__(self, conf, _stats, logger\u003dNone):"},{"line_number":111,"context_line":"        self.conf \u003d conf"},{"line_number":112,"context_line":"        self._stats \u003d {}"},{"line_number":113,"context_line":"        self.logger \u003d logger"},{"line_number":114,"context_line":""}],"source_content_type":"text/x-python","patch_set":21,"id":"31a41180_282edb42","line":111,"in_reply_to":"5b918b14_1060f4ea","updated":"2026-04-20 20:58:16.000000000","message":"Done","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":109,"context_line":"class AbstractPrometheusClient:"},{"line_number":110,"context_line":"    def __init__(self, conf, _stats, logger\u003dNone):"},{"line_number":111,"context_line":"        self.conf \u003d conf"},{"line_number":112,"context_line":"        self._stats \u003d {}"},{"line_number":113,"context_line":"        self.logger \u003d logger"},{"line_number":114,"context_line":""},{"line_number":115,"context_line":""}],"source_content_type":"text/x-python","patch_set":21,"id":"799fd949_89841652","line":112,"updated":"2026-04-14 19:10:20.000000000","message":"maybe don\u0027t accept/require this arg (since it has no effect anyway) - maybe for tests if needed?  sort of like `logger` where sometimes we pass in a `debug_logger`?  We don\u0027t use it (successfully?) right now, so I\u0027d be inclined to try and get rid of it - YAGNI.\n\nI have a notion that sometimes a client is going to want to get \"initialized\" from an existing metrics file (???) - but maybe most of the time not?  I think we could start with supporting only the use-case of \"the next time you call flush we overwrite the existing metric filenames with whatever you\u0027ve loaded into your prom client brain\" - and then if a daemon *needs* to \"keep metrics from previous run\" it can decide how to reload (some of?) that state (and maybe if a enough common use-case emerge we could consolidate support here)","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":109,"context_line":"class AbstractPrometheusClient:"},{"line_number":110,"context_line":"    def __init__(self, conf, _stats, logger\u003dNone):"},{"line_number":111,"context_line":"        self.conf \u003d conf"},{"line_number":112,"context_line":"        self._stats \u003d {}"},{"line_number":113,"context_line":"        self.logger \u003d logger"},{"line_number":114,"context_line":""},{"line_number":115,"context_line":""}],"source_content_type":"text/x-python","patch_set":21,"id":"2bbee81b_2668fd17","line":112,"in_reply_to":"799fd949_89841652","updated":"2026-04-28 04:49:45.000000000","message":"We can add this easily and work towards a buffer when needed in future. As of now, we probably don\u0027t need to reload existing metrics","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":110,"context_line":"    def __init__(self, conf, _stats, logger\u003dNone):"},{"line_number":111,"context_line":"        self.conf \u003d conf"},{"line_number":112,"context_line":"        self._stats \u003d {}"},{"line_number":113,"context_line":"        self.logger \u003d logger"},{"line_number":114,"context_line":""},{"line_number":115,"context_line":""},{"line_number":116,"context_line":"class PrometheusClient(AbstractPrometheusClient):"}],"source_content_type":"text/x-python","patch_set":21,"id":"843ea5a0_687d92cf","line":113,"updated":"2026-04-14 19:10:20.000000000","message":"since we only have a single concrete class - let\u0027s avoid the indirection of an abstract base class.\n\nThe statsd-client pattern makes sense b/c there\u0027s two concrete classes - one with labels and a legacy one w/o - we don\u0027t plan to have a prom client that doesn\u0027t support labels so we only need one class for now!","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"3b9dd1e8218ecea13dd3ef8ac6814cf4467dcca4","unresolved":false,"context_lines":[{"line_number":110,"context_line":"    def __init__(self, conf, _stats, logger\u003dNone):"},{"line_number":111,"context_line":"        self.conf \u003d conf"},{"line_number":112,"context_line":"        self._stats \u003d {}"},{"line_number":113,"context_line":"        self.logger \u003d logger"},{"line_number":114,"context_line":""},{"line_number":115,"context_line":""},{"line_number":116,"context_line":"class PrometheusClient(AbstractPrometheusClient):"}],"source_content_type":"text/x-python","patch_set":21,"id":"099d2729_1d52b71a","line":113,"in_reply_to":"843ea5a0_687d92cf","updated":"2026-04-20 20:58:16.000000000","message":"Acknowledged","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":160,"context_line":"        else:"},{"line_number":161,"context_line":"            return f\u0027{name} {value}\\n\u0027"},{"line_number":162,"context_line":""},{"line_number":163,"context_line":"    def write_stats(self, metric, filename\u003dNone):"},{"line_number":164,"context_line":"        prom_metric \u003d self._gen_str_metrics(metric)"},{"line_number":165,"context_line":""},{"line_number":166,"context_line":"        # Update internal stats dictionary"}],"source_content_type":"text/x-python","patch_set":21,"id":"b8e8c080_0e5223ea","line":163,"updated":"2026-04-14 19:10:20.000000000","message":"`wirte_stats` is called with a SINGLE `metric` - it\u0027s more like `write_stat` 🤔\n\nPlease put doc-strings on the methods that are part of the public API for this client.  Please prefix \"private\" implementation detail helper methods with `_` - they should not be expected to be called by clients, and only from with-in other \"public\" methods (that have doc strings).\n\nIn particular the poloy-morphic nature of the `metrics` param deserves a doc-string even if the method is \"private\"","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"3b9dd1e8218ecea13dd3ef8ac6814cf4467dcca4","unresolved":false,"context_lines":[{"line_number":160,"context_line":"        else:"},{"line_number":161,"context_line":"            return f\u0027{name} {value}\\n\u0027"},{"line_number":162,"context_line":""},{"line_number":163,"context_line":"    def write_stats(self, metric, filename\u003dNone):"},{"line_number":164,"context_line":"        prom_metric \u003d self._gen_str_metrics(metric)"},{"line_number":165,"context_line":""},{"line_number":166,"context_line":"        # Update internal stats dictionary"}],"source_content_type":"text/x-python","patch_set":21,"id":"f63c4c99_cd0f8a62","line":163,"in_reply_to":"b8e8c080_0e5223ea","updated":"2026-04-20 20:58:16.000000000","message":"Done","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":166,"context_line":"        # Update internal stats dictionary"},{"line_number":167,"context_line":"        if isinstance(metric, tuple) and len(metric) \u003d\u003d 3:"},{"line_number":168,"context_line":"            name, labels, value \u003d metric"},{"line_number":169,"context_line":"            metric_key_str \u003d metric_key(name, **labels)"},{"line_number":170,"context_line":"            self._stats[metric_key_str] \u003d value"},{"line_number":171,"context_line":"        elif isinstance(metric, str):"},{"line_number":172,"context_line":"            # Parse metric string to update internal state"}],"source_content_type":"text/x-python","patch_set":21,"id":"fd867e29_8bf87eec","line":169,"updated":"2026-04-14 19:10:20.000000000","message":"this isn\u0027t a `_str` - it\u0027s a `metrics_key` two-tuple","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"3b9dd1e8218ecea13dd3ef8ac6814cf4467dcca4","unresolved":false,"context_lines":[{"line_number":166,"context_line":"        # Update internal stats dictionary"},{"line_number":167,"context_line":"        if isinstance(metric, tuple) and len(metric) \u003d\u003d 3:"},{"line_number":168,"context_line":"            name, labels, value \u003d metric"},{"line_number":169,"context_line":"            metric_key_str \u003d metric_key(name, **labels)"},{"line_number":170,"context_line":"            self._stats[metric_key_str] \u003d value"},{"line_number":171,"context_line":"        elif isinstance(metric, str):"},{"line_number":172,"context_line":"            # Parse metric string to update internal state"}],"source_content_type":"text/x-python","patch_set":21,"id":"c9c0b62a_fc198e3e","line":169,"in_reply_to":"fd867e29_8bf87eec","updated":"2026-04-20 20:58:16.000000000","message":"Acknowledged","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":183,"context_line":"        if filename is None:"},{"line_number":184,"context_line":"            # If no filename provided,"},{"line_number":185,"context_line":"            # return the metric string without writing"},{"line_number":186,"context_line":"            return prom_metric"},{"line_number":187,"context_line":"        with open(filename, \"a\") as file:"},{"line_number":188,"context_line":"            file.write(prom_metric)"},{"line_number":189,"context_line":"        return"}],"source_content_type":"text/x-python","patch_set":21,"id":"ed5627af_0322cd4f","line":186,"updated":"2026-04-14 19:10:20.000000000","message":"who uses this?  Can filename be provided via configuration?","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"3b9dd1e8218ecea13dd3ef8ac6814cf4467dcca4","unresolved":false,"context_lines":[{"line_number":183,"context_line":"        if filename is None:"},{"line_number":184,"context_line":"            # If no filename provided,"},{"line_number":185,"context_line":"            # return the metric string without writing"},{"line_number":186,"context_line":"            return prom_metric"},{"line_number":187,"context_line":"        with open(filename, \"a\") as file:"},{"line_number":188,"context_line":"            file.write(prom_metric)"},{"line_number":189,"context_line":"        return"}],"source_content_type":"text/x-python","patch_set":21,"id":"c0b37d31_22cdac15","line":186,"in_reply_to":"ed5627af_0322cd4f","updated":"2026-04-20 20:58:16.000000000","message":"Done","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":185,"context_line":"            # return the metric string without writing"},{"line_number":186,"context_line":"            return prom_metric"},{"line_number":187,"context_line":"        with open(filename, \"a\") as file:"},{"line_number":188,"context_line":"            file.write(prom_metric)"},{"line_number":189,"context_line":"        return"},{"line_number":190,"context_line":""},{"line_number":191,"context_line":"    def write_all_stats(self, stats_list, filename\u003dNone):"}],"source_content_type":"text/x-python","patch_set":21,"id":"473701ad_042f95d8","line":188,"updated":"2026-04-14 19:10:20.000000000","message":"\"append\" is a weird behavior - if a client calls `gauge` twice with the same metric name and different values it ends up in the file TWICE\n\nI\u0027m not sure what a prometheus scraper would do in that case - but I don\u0027t think it\u0027s the interface/beahavior we want to support/encourage.","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"3b9dd1e8218ecea13dd3ef8ac6814cf4467dcca4","unresolved":false,"context_lines":[{"line_number":185,"context_line":"            # return the metric string without writing"},{"line_number":186,"context_line":"            return prom_metric"},{"line_number":187,"context_line":"        with open(filename, \"a\") as file:"},{"line_number":188,"context_line":"            file.write(prom_metric)"},{"line_number":189,"context_line":"        return"},{"line_number":190,"context_line":""},{"line_number":191,"context_line":"    def write_all_stats(self, stats_list, filename\u003dNone):"}],"source_content_type":"text/x-python","patch_set":21,"id":"3256e67a_2addef5f","line":188,"in_reply_to":"473701ad_042f95d8","updated":"2026-04-20 20:58:16.000000000","message":"Agree, gauge no longer writes in the file","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":195,"context_line":"        :param stats_list: List of metrics to write. Each metric can be:"},{"line_number":196,"context_line":"                          - Tuple: (name, value) or (name, value, labels_dict)"},{"line_number":197,"context_line":"                          - Dict: {\"name\": ..., \"labels\": {...}, \"value\": ...}"},{"line_number":198,"context_line":"                          - String: Prometheus format string"},{"line_number":199,"context_line":"        :param filename: Output file path"},{"line_number":200,"context_line":"                         (if not provided, metrics are not written to file)"},{"line_number":201,"context_line":"        :returns: Count of metrics written"}],"source_content_type":"text/x-python","patch_set":21,"id":"0c823a6c_36944c88","line":198,"updated":"2026-04-14 19:10:20.000000000","message":"oh wow, the list of values is polymorphic!?\n\nA *really useful* signature to support would be something like:\n\n```\nprom_client.write_all_stats(prom_client._stats.values(), filename\u003d\u0027/tmp/check.prom\u0027)\n```\n\n^ Actually I think there should be a kind of \"flush\" public API that handles the `self._stats.values()` part FOR you.\n\nThe `filename\u003d` part could maybe also come from configuration:\n\n```\ndaemon.metrics \u003d PromClient({\u0027filename\u0027\u003d\u0027/opt/ss/support/prom/daemon.prom\u0027})\ndaemon.metirics.gauge(\u0027swift_something\u0027, 10, labels\u003d{\u0027event\u0027: \u0027foo\u0027})\ndaemon.metrics.flush()\n```\n\nN.B. if you call `flush` and w/o specifying filename AND it\u0027s not configured that should error?\n\n```\nfilename \u003d filename or self.filename\nif not filename:\n   raise RuntimeError(\u0027you must specify filename\u0027)  # ???\n```","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"3b9dd1e8218ecea13dd3ef8ac6814cf4467dcca4","unresolved":false,"context_lines":[{"line_number":195,"context_line":"        :param stats_list: List of metrics to write. Each metric can be:"},{"line_number":196,"context_line":"                          - Tuple: (name, value) or (name, value, labels_dict)"},{"line_number":197,"context_line":"                          - Dict: {\"name\": ..., \"labels\": {...}, \"value\": ...}"},{"line_number":198,"context_line":"                          - String: Prometheus format string"},{"line_number":199,"context_line":"        :param filename: Output file path"},{"line_number":200,"context_line":"                         (if not provided, metrics are not written to file)"},{"line_number":201,"context_line":"        :returns: Count of metrics written"}],"source_content_type":"text/x-python","patch_set":21,"id":"2eaf05d5_32246df8","line":198,"in_reply_to":"0c823a6c_36944c88","updated":"2026-04-20 20:58:16.000000000","message":"Done","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":204,"context_line":"        for metric in stats_list:"},{"line_number":205,"context_line":"            self.write_stats(metric, filename\u003dfilename)"},{"line_number":206,"context_line":"            metrics_written +\u003d 1"},{"line_number":207,"context_line":"        return metrics_written"},{"line_number":208,"context_line":""},{"line_number":209,"context_line":"    def gauge(self, name, value, labels\u003dNone, filename\u003dNone):"},{"line_number":210,"context_line":"        \"\"\""}],"source_content_type":"text/x-python","patch_set":21,"id":"37fefc45_0c6c6b85","line":207,"updated":"2026-04-14 19:10:20.000000000","message":"if `filename\u003dNone` it seems weird to return `metrics_written \u003d len(stats_list)`\n\nAlso can *individual* metrics fail?\n\nI think we want this method to only open the file once and then write all the lines to the file.\n\nIdeally we\u0027d have temporary atomic renames:\n\n```\nfilename \u003d filename or self.filename\nstats_data \u003d self._render_all_stats()\nwith open(tempfile, \u0027w\u0027) as f:\n    f.write(statsd_data)\nos.rename(tempfile, filename)\n```\n\nSo that you never get a \"partically complete\" metrics file @ `self.filename` - it\u0027s all or nothing!  single `open`, single `write`, atomic `rename`","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"3b9dd1e8218ecea13dd3ef8ac6814cf4467dcca4","unresolved":false,"context_lines":[{"line_number":204,"context_line":"        for metric in stats_list:"},{"line_number":205,"context_line":"            self.write_stats(metric, filename\u003dfilename)"},{"line_number":206,"context_line":"            metrics_written +\u003d 1"},{"line_number":207,"context_line":"        return metrics_written"},{"line_number":208,"context_line":""},{"line_number":209,"context_line":"    def gauge(self, name, value, labels\u003dNone, filename\u003dNone):"},{"line_number":210,"context_line":"        \"\"\""}],"source_content_type":"text/x-python","patch_set":21,"id":"6f538054_3d52ba7c","line":207,"in_reply_to":"37fefc45_0c6c6b85","updated":"2026-04-20 20:58:16.000000000","message":"Acknowledged","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":206,"context_line":"            metrics_written +\u003d 1"},{"line_number":207,"context_line":"        return metrics_written"},{"line_number":208,"context_line":""},{"line_number":209,"context_line":"    def gauge(self, name, value, labels\u003dNone, filename\u003dNone):"},{"line_number":210,"context_line":"        \"\"\""},{"line_number":211,"context_line":"        Record a gauge metric (current value)."},{"line_number":212,"context_line":""}],"source_content_type":"text/x-python","patch_set":21,"id":"08c9696d_f4220400","line":209,"updated":"2026-04-14 19:10:20.000000000","message":"I don\u0027t think that \"record\" functions should need/want to individually override/specify the metrics `fillename`\n\nIt seems like you\u0027d mostly want to create a prometheus_client from a configured filename - and then use the client to update the metrics; and occasionally flush the updated metrics back to disk.","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":206,"context_line":"            metrics_written +\u003d 1"},{"line_number":207,"context_line":"        return metrics_written"},{"line_number":208,"context_line":""},{"line_number":209,"context_line":"    def gauge(self, name, value, labels\u003dNone, filename\u003dNone):"},{"line_number":210,"context_line":"        \"\"\""},{"line_number":211,"context_line":"        Record a gauge metric (current value)."},{"line_number":212,"context_line":""}],"source_content_type":"text/x-python","patch_set":21,"id":"a4e2a459_7a250acd","line":209,"in_reply_to":"08c9696d_f4220400","updated":"2026-04-28 04:49:45.000000000","message":"added filename \u003d filename or self.filename","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":217,"context_line":"        \"\"\""},{"line_number":218,"context_line":"        labels \u003d labels or {}"},{"line_number":219,"context_line":"        metric_data \u003d (name, labels, value)"},{"line_number":220,"context_line":"        self.write_stats(metric_data, filename\u003dfilename)"},{"line_number":221,"context_line":"        return self._gen_str_metrics(metric_data)"},{"line_number":222,"context_line":""},{"line_number":223,"context_line":"    def counter(self, name, value, labels\u003dNone, filename\u003dNone):"}],"source_content_type":"text/x-python","patch_set":21,"id":"a83231e8_2526071f","line":220,"updated":"2026-04-14 19:10:20.000000000","message":"I don\u0027t think we should (ever need to?) do disk io by default for every call to `gauge`\n\nBy default, we should just `record_stats` in `self._stats` ... and leave the caller/owner of the `prom_client` to deicde when it should flush to the filename.","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"3b9dd1e8218ecea13dd3ef8ac6814cf4467dcca4","unresolved":false,"context_lines":[{"line_number":217,"context_line":"        \"\"\""},{"line_number":218,"context_line":"        labels \u003d labels or {}"},{"line_number":219,"context_line":"        metric_data \u003d (name, labels, value)"},{"line_number":220,"context_line":"        self.write_stats(metric_data, filename\u003dfilename)"},{"line_number":221,"context_line":"        return self._gen_str_metrics(metric_data)"},{"line_number":222,"context_line":""},{"line_number":223,"context_line":"    def counter(self, name, value, labels\u003dNone, filename\u003dNone):"}],"source_content_type":"text/x-python","patch_set":21,"id":"ac0f08dc_58aed7cf","line":220,"in_reply_to":"a83231e8_2526071f","updated":"2026-04-20 20:58:16.000000000","message":"Done","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":40,"context_line":"        labels \u003d {}"},{"line_number":41,"context_line":"        value_str \u003d parts[1]"},{"line_number":42,"context_line":"    value \u003d float(value_str)"},{"line_number":43,"context_line":"    metrics[metric_key(name, **labels)] \u003d value"},{"line_number":44,"context_line":"    return name, labels, value"},{"line_number":45,"context_line":""},{"line_number":46,"context_line":""}],"source_content_type":"text/x-python","patch_set":22,"id":"5334c418_28c0b2e4","line":43,"updated":"2026-04-21 20:05:35.000000000","message":"why do we build the `metrics` dict - it\u0027s not used or returned?","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":40,"context_line":"        labels \u003d {}"},{"line_number":41,"context_line":"        value_str \u003d parts[1]"},{"line_number":42,"context_line":"    value \u003d float(value_str)"},{"line_number":43,"context_line":"    metrics[metric_key(name, **labels)] \u003d value"},{"line_number":44,"context_line":"    return name, labels, value"},{"line_number":45,"context_line":""},{"line_number":46,"context_line":""}],"source_content_type":"text/x-python","patch_set":22,"id":"d7d4c10e_af7d9893","line":43,"in_reply_to":"5334c418_28c0b2e4","updated":"2026-04-28 04:49:45.000000000","message":"right! never used!","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":126,"context_line":"        3. Dict: {\"name\": ..., \"labels\": {...}, \"value\": ...}"},{"line_number":127,"context_line":""},{"line_number":128,"context_line":"        :param metric: Metric data (string, tuple, or dict)"},{"line_number":129,"context_line":"        :returns: a string with prometheus format"},{"line_number":130,"context_line":"        \"\"\""},{"line_number":131,"context_line":"        # Handle string input - parse it"},{"line_number":132,"context_line":"        if isinstance(metric, str):"}],"source_content_type":"text/x-python","patch_set":22,"id":"112c5f34_8f85948c","line":129,"updated":"2026-04-21 20:05:35.000000000","message":"how can this not re-use `format_metrics` - we\u0027ve implemented this twice?","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":126,"context_line":"        3. Dict: {\"name\": ..., \"labels\": {...}, \"value\": ...}"},{"line_number":127,"context_line":""},{"line_number":128,"context_line":"        :param metric: Metric data (string, tuple, or dict)"},{"line_number":129,"context_line":"        :returns: a string with prometheus format"},{"line_number":130,"context_line":"        \"\"\""},{"line_number":131,"context_line":"        # Handle string input - parse it"},{"line_number":132,"context_line":"        if isinstance(metric, str):"}],"source_content_type":"text/x-python","patch_set":22,"id":"38bf09b9_8192fbb7","line":129,"in_reply_to":"112c5f34_8f85948c","updated":"2026-04-28 04:49:45.000000000","message":"Absolutely! Reused!","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":174,"context_line":"            labels_dict \u003d dict(labels_frozenset) if labels_frozenset else {}"},{"line_number":175,"context_line":"            line \u003d self._gen_str_metrics((name, labels_dict, value))"},{"line_number":176,"context_line":"            lines.append(line)"},{"line_number":177,"context_line":"        return \u0027\u0027.join(lines)"},{"line_number":178,"context_line":""},{"line_number":179,"context_line":"    def _update_stat_internal(self, metric):"},{"line_number":180,"context_line":"        \"\"\""}],"source_content_type":"text/x-python","patch_set":22,"id":"7c03d93c_1d25ff35","line":177,"updated":"2026-04-21 20:05:35.000000000","message":"FWIW an empty `self._stats` dict should return an empty string here; which may provide exactly the \"clear stale stats\" behavior I assume we want.","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":174,"context_line":"            labels_dict \u003d dict(labels_frozenset) if labels_frozenset else {}"},{"line_number":175,"context_line":"            line \u003d self._gen_str_metrics((name, labels_dict, value))"},{"line_number":176,"context_line":"            lines.append(line)"},{"line_number":177,"context_line":"        return \u0027\u0027.join(lines)"},{"line_number":178,"context_line":""},{"line_number":179,"context_line":"    def _update_stat_internal(self, metric):"},{"line_number":180,"context_line":"        \"\"\""}],"source_content_type":"text/x-python","patch_set":22,"id":"0282823c_3138d039","line":177,"in_reply_to":"7c03d93c_1d25ff35","updated":"2026-04-28 04:49:45.000000000","message":"Acknowledged","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":183,"context_line":"        :param metric: Metric data in one of these formats:"},{"line_number":184,"context_line":"                      - Tuple: (name, labels_dict, value)"},{"line_number":185,"context_line":"                      - String: Prometheus format string"},{"line_number":186,"context_line":"                      - Dict: {\"name\": ..., \"labels\": {...}, \"value\": ...}"},{"line_number":187,"context_line":"        \"\"\""},{"line_number":188,"context_line":"        if isinstance(metric, tuple) and len(metric) \u003d\u003d 3:"},{"line_number":189,"context_line":"            name, labels, value \u003d metric"}],"source_content_type":"text/x-python","patch_set":22,"id":"b2ed5378_1ba03598","line":186,"updated":"2026-04-21 20:05:35.000000000","message":"this polymorphism sits funny with me; I\u0027m not sure why we need it.\n\n```\n\u003e\u003e\u003e import this\nThe Zen of Python, by Tim Peters\n```\n\n\u003e There should be one-- and preferably only one --obvious way to do it.\n\nI also don\u0027t love:\n```\n        metric_data \u003d (name, labels, value)\n        self._update_stat_internal(metric_data)\n```\n\ncompared to just:\n\n```\nself._stats[metric_key(name, **labels)] \u003d value\n```","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":183,"context_line":"        :param metric: Metric data in one of these formats:"},{"line_number":184,"context_line":"                      - Tuple: (name, labels_dict, value)"},{"line_number":185,"context_line":"                      - String: Prometheus format string"},{"line_number":186,"context_line":"                      - Dict: {\"name\": ..., \"labels\": {...}, \"value\": ...}"},{"line_number":187,"context_line":"        \"\"\""},{"line_number":188,"context_line":"        if isinstance(metric, tuple) and len(metric) \u003d\u003d 3:"},{"line_number":189,"context_line":"            name, labels, value \u003d metric"}],"source_content_type":"text/x-python","patch_set":22,"id":"0aade15e_5b130aa1","line":186,"in_reply_to":"b2ed5378_1ba03598","updated":"2026-04-28 04:49:45.000000000","message":"Done","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":233,"context_line":"        stats_data \u003d self._render_all_stats()"},{"line_number":234,"context_line":"        "},{"line_number":235,"context_line":"        # Write atomically using temp file"},{"line_number":236,"context_line":"        temp_fd, tempfile_path \u003d tempfile.mkstemp(dir\u003dos.path.dirname(filename))"},{"line_number":237,"context_line":"        try:"},{"line_number":238,"context_line":"            with os.fdopen(temp_fd, \u0027w\u0027) as f:"},{"line_number":239,"context_line":"                f.write(stats_data)"}],"source_content_type":"text/x-python","patch_set":22,"id":"9e7669b6_49745ca2","line":236,"in_reply_to":"16d0101a_f376744f","updated":"2026-04-28 04:49:45.000000000","message":"Done","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":233,"context_line":"        stats_data \u003d self._render_all_stats()"},{"line_number":234,"context_line":"        "},{"line_number":235,"context_line":"        # Write atomically using temp file"},{"line_number":236,"context_line":"        temp_fd, tempfile_path \u003d tempfile.mkstemp(dir\u003dos.path.dirname(filename))"},{"line_number":237,"context_line":"        try:"},{"line_number":238,"context_line":"            with os.fdopen(temp_fd, \u0027w\u0027) as f:"},{"line_number":239,"context_line":"                f.write(stats_data)"}],"source_content_type":"text/x-python","patch_set":22,"id":"16d0101a_f376744f","line":236,"in_reply_to":"8c9845cb_1f72567f","updated":"2026-04-21 20:05:35.000000000","message":"I\u0027d recommend `NamedTemporaryFile` as a context manager:\n\n```\ntf \u003d None\ntry:\n   with temp as tf:\n       tf.write()\n   renamer(tf.name, filename)\nfinally:\n   if tf:\n       try:\n           os.unlink(tf.name)\n       except FileNotFoundError:\n           pass\n```\n\nread the implemention of `swift.common.utils.dump_recon_cache` for inspiration\n\nN.B. We don\u0027t currently need/want the locking, but I couldn\u0027t find another example of \"dump to temp file and rename into place\" - honestly you could probably extract that as a common primitive `swift.common.utils.atomic_dump`","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":252,"context_line":"        \"\"\""},{"line_number":253,"context_line":"        # If there are no stats to write, don\u0027t write to file"},{"line_number":254,"context_line":"        if not self._stats:"},{"line_number":255,"context_line":"            return"},{"line_number":256,"context_line":"        "},{"line_number":257,"context_line":"        # Render all accumulated stats"},{"line_number":258,"context_line":"        stats_data \u003d self._render_all_stats()"}],"source_content_type":"text/x-python","patch_set":22,"id":"82bb76d5_a73f5576","line":255,"updated":"2026-04-21 20:05:35.000000000","message":"I\u0027m not sure \"do nothing\" is more correct than \"truncate file\"\n\n... if you create a client and call dump_stats I wouldn\u0027t expect the \"stale\" stats from a previous run should stay there on disk.","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":252,"context_line":"        \"\"\""},{"line_number":253,"context_line":"        # If there are no stats to write, don\u0027t write to file"},{"line_number":254,"context_line":"        if not self._stats:"},{"line_number":255,"context_line":"            return"},{"line_number":256,"context_line":"        "},{"line_number":257,"context_line":"        # Render all accumulated stats"},{"line_number":258,"context_line":"        stats_data \u003d self._render_all_stats()"}],"source_content_type":"text/x-python","patch_set":22,"id":"609c769a_7a18ff8d","line":255,"in_reply_to":"82bb76d5_a73f5576","updated":"2026-04-28 04:49:45.000000000","message":"we might use write_all_stats multiple times write periodically. Using NamedTemporaryFile with delete\u003dFalse and os.rename() ensures the file is never in a partial/corrupted state. The temp file is written completely, then atomically renamed over the old file.\nAnd we also write stats everytime (not append) which is similar behavior to truncate!","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":300,"context_line":"                        os.remove(tempfile_path)"},{"line_number":301,"context_line":"                    raise"},{"line_number":302,"context_line":"        "},{"line_number":303,"context_line":"        return self._gen_str_metrics(metric_data)"},{"line_number":304,"context_line":""},{"line_number":305,"context_line":"    def counter(self, name, value, labels\u003dNone, filename\u003dNone):"},{"line_number":306,"context_line":"        \"\"\""}],"source_content_type":"text/x-python","patch_set":22,"id":"11cf65ad_796c0803","line":303,"updated":"2026-04-21 20:05:35.000000000","message":"I don\u0027t think this method needs to return anything - the statsd client somewhat arbitrarily returns the number of bytes sent to the socket\n\nThe disadvantage of baking this return value is two-fold\n\n1) it establishes an interface that we may be obliged to support even if the format of the rendered string evolves to include type annotations\n2) it\u0027s computational effort not-needed at this time; the only time we need to gen_str_metrics is in dump - and we don\u0027t want to dump every call to gauage\n\nThe best anaology I can think of in stdlib is a plain old dict udpate:\n\n```\n\u003e\u003e\u003e d \u003d {\u0027foo\u0027: \u0027bar\u0027}\n\u003e\u003e\u003e d.update({\u0027baz\u0027: \u0027buz\u0027})\n\u003e\u003e\u003e d\n{\u0027foo\u0027: \u0027bar\u0027, \u0027baz\u0027: \u0027buz\u0027}\n```\n\n`d.update` doesn\u0027t *return* anything - because it\u0027s updating in place","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"7abd8df05b8081785b9541f05ff02331685d4b3f","unresolved":false,"context_lines":[{"line_number":300,"context_line":"                        os.remove(tempfile_path)"},{"line_number":301,"context_line":"                    raise"},{"line_number":302,"context_line":"        "},{"line_number":303,"context_line":"        return self._gen_str_metrics(metric_data)"},{"line_number":304,"context_line":""},{"line_number":305,"context_line":"    def counter(self, name, value, labels\u003dNone, filename\u003dNone):"},{"line_number":306,"context_line":"        \"\"\""}],"source_content_type":"text/x-python","patch_set":22,"id":"98935426_31522144","line":303,"in_reply_to":"11cf65ad_796c0803","updated":"2026-05-05 15:26:31.000000000","message":"yep, totally!","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":312,"context_line":"        :param filename: Optional filename to write all accumulated stats to"},{"line_number":313,"context_line":"        \"\"\""},{"line_number":314,"context_line":"        labels \u003d labels or {}"},{"line_number":315,"context_line":"        metric_data \u003d (name, labels, value)"},{"line_number":316,"context_line":"        self._update_stat_internal(metric_data)"},{"line_number":317,"context_line":"        "},{"line_number":318,"context_line":"        if filename:"}],"source_content_type":"text/x-python","patch_set":22,"id":"a283cd68_700d8bf2","line":315,"updated":"2026-04-21 20:05:35.000000000","message":"how can this implementation be the same as `gauge` - where is the \"monotonically increasing\" part?\n\nperhaps:\n\n```\n        key \u003d metric_key(name, **labels)\n        value \u003d self._stats.get(key, 0) + value\n        self._stats[key] \u003d value\n```\n\n... I\u0027m not actually sure what the expected behavior should be; presumably this and you replace increment/decrement w/ `self.counter(name, [+/-]1, labels\u003dlabels)`?\n\nI guess classically a prometheus style \"counter\" should only ever go up - something like `http_requests_total`\n\n```\n# TYPE http_requests_total counter\nhttp_requests_total{method\u003d\"GET\"} 12345\n```\n\n^ would start a `zero` and only ever go up (getting reset to zero on process restart, which I guess prometheus collectors pay attention to and understand different compared to the value of a gauge)\n\nIn otel-land the API requires you to declare the type of a metric and it\u0027s affordances change the provided methods:\n\n```\nGauage.record(value)\nCounter.add(value)\nUpDownCounter.add(value)  # value can be negative\n```\n\nUltimately the only difference in how they\u0027re rendered is in the `TYPE` comment; so I think we can just rename our interface to follow otel convention:\n\n```\ngauge \u003d\u003e record\ncounter \u003d\u003e add\n```\n\n`increment` and `decrement` can stay for compatibility/familiarity with statsd_client - but should just wrap `self.add([+|-]1)`","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":312,"context_line":"        :param filename: Optional filename to write all accumulated stats to"},{"line_number":313,"context_line":"        \"\"\""},{"line_number":314,"context_line":"        labels \u003d labels or {}"},{"line_number":315,"context_line":"        metric_data \u003d (name, labels, value)"},{"line_number":316,"context_line":"        self._update_stat_internal(metric_data)"},{"line_number":317,"context_line":"        "},{"line_number":318,"context_line":"        if filename:"}],"source_content_type":"text/x-python","patch_set":22,"id":"b3685d4e_3abb5bb4","line":315,"in_reply_to":"a283cd68_700d8bf2","updated":"2026-04-28 04:49:45.000000000","message":"Done","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":360,"context_line":"                except Exception:"},{"line_number":361,"context_line":"                    if os.path.exists(tempfile_path):"},{"line_number":362,"context_line":"                        os.remove(tempfile_path)"},{"line_number":363,"context_line":"                    raise"},{"line_number":364,"context_line":"        "},{"line_number":365,"context_line":"        return self._gen_str_metrics(metric_data)"},{"line_number":366,"context_line":""}],"source_content_type":"text/x-python","patch_set":22,"id":"cf9ee47b_d43e89fc","line":363,"updated":"2026-04-21 20:05:35.000000000","message":"this behavior is duplicated three times - in addition to `write_all_stats` (which is the only place I expect to see it) - that\u0027s a smell\n\nI don\u0027t think we need `histogram_bucket` et al to support a `filename\u003d` kwarg, nor dump to stats immediately after adding the metrics.\n\nIf that behavior is needed I think callers could simply:\n\n```\nclient \u003d PromClient(filename\u003dmy_file)\nclient.histogram_bucket(\u0027my_hist\u0027, 5, 10)\nclient.dump_stats()\n```","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":360,"context_line":"                except Exception:"},{"line_number":361,"context_line":"                    if os.path.exists(tempfile_path):"},{"line_number":362,"context_line":"                        os.remove(tempfile_path)"},{"line_number":363,"context_line":"                    raise"},{"line_number":364,"context_line":"        "},{"line_number":365,"context_line":"        return self._gen_str_metrics(metric_data)"},{"line_number":366,"context_line":""}],"source_content_type":"text/x-python","patch_set":22,"id":"f50b47c1_755f2ad4","line":363,"in_reply_to":"cf9ee47b_d43e89fc","updated":"2026-04-28 04:49:45.000000000","message":"Do we not even want filename\u003dxyz for other ones like write ot flush?\nSafely, I\u0027ve added filename \u003d filename or self.filename everywhere!","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":365,"context_line":"        return self._gen_str_metrics(metric_data)"},{"line_number":366,"context_line":""},{"line_number":367,"context_line":"    def update_stats(self, metric, update_val, filename\u003dNone):"},{"line_number":368,"context_line":"        \"\"\"Update a metric with a new value.\"\"\""},{"line_number":369,"context_line":"        name, labels, value \u003d parse_metrics(metric)"},{"line_number":370,"context_line":"        metric_key_str \u003d metric_key(name, **labels)"},{"line_number":371,"context_line":"        self._stats[metric_key_str] \u003d update_val"}],"source_content_type":"text/x-python","patch_set":22,"id":"5eaacc28_41a501c0","line":368,"updated":"2026-04-21 20:05:35.000000000","message":"i don\u0027t know what this method is for - why would you ever have a \"metric_str\" already in hand from the caller?  This whole abstraction is so that callers don\u0027t have to think about rendered metrics and can instead just think in `(name, labels)`","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":365,"context_line":"        return self._gen_str_metrics(metric_data)"},{"line_number":366,"context_line":""},{"line_number":367,"context_line":"    def update_stats(self, metric, update_val, filename\u003dNone):"},{"line_number":368,"context_line":"        \"\"\"Update a metric with a new value.\"\"\""},{"line_number":369,"context_line":"        name, labels, value \u003d parse_metrics(metric)"},{"line_number":370,"context_line":"        metric_key_str \u003d metric_key(name, **labels)"},{"line_number":371,"context_line":"        self._stats[metric_key_str] \u003d update_val"}],"source_content_type":"text/x-python","patch_set":22,"id":"19d6107c_ad6c0416","line":368,"in_reply_to":"5eaacc28_41a501c0","updated":"2026-04-28 04:49:45.000000000","message":"Done","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":373,"context_line":"        prom_metric \u003d self._gen_str_metrics(metric_data)"},{"line_number":374,"context_line":"        self._write_stat(prom_metric, filename\u003dfilename)"},{"line_number":375,"context_line":""},{"line_number":376,"context_line":"    def increment(self, metric, filename\u003dNone):"},{"line_number":377,"context_line":"        \"\"\"Increment a metric value by 1.\"\"\""},{"line_number":378,"context_line":"        name, labels, value \u003d parse_metrics(metric)"},{"line_number":379,"context_line":"        metric_key_str \u003d metric_key(name, **labels)"}],"source_content_type":"text/x-python","patch_set":22,"id":"660e3c36_62c7f6ea","line":376,"updated":"2026-04-21 20:05:35.000000000","message":"remove the filename kwarg, add a label kwarg.\n\n`metric` here should be the `metric_name` not a `metric_str`","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":373,"context_line":"        prom_metric \u003d self._gen_str_metrics(metric_data)"},{"line_number":374,"context_line":"        self._write_stat(prom_metric, filename\u003dfilename)"},{"line_number":375,"context_line":""},{"line_number":376,"context_line":"    def increment(self, metric, filename\u003dNone):"},{"line_number":377,"context_line":"        \"\"\"Increment a metric value by 1.\"\"\""},{"line_number":378,"context_line":"        name, labels, value \u003d parse_metrics(metric)"},{"line_number":379,"context_line":"        metric_key_str \u003d metric_key(name, **labels)"}],"source_content_type":"text/x-python","patch_set":22,"id":"cf7dbef2_29e9df43","line":376,"in_reply_to":"660e3c36_62c7f6ea","updated":"2026-04-28 04:49:45.000000000","message":"reused this filename \u003d filename or self.filename!","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":36606,"name":"Yan Xiao","display_name":"Yan","email":"yanxiao@nvidia.com","username":"yanxiao"},"change_message_id":"9af175fc7017025ca53e038365aeeccc2afae5c8","unresolved":true,"context_lines":[{"line_number":109,"context_line":"    Format a single metric into Prometheus text format string."},{"line_number":110,"context_line":""},{"line_number":111,"context_line":"    Examples:"},{"line_number":112,"context_line":"        format_metric(\u0027http_requests_total\u0027, 42)"},{"line_number":113,"context_line":"        # Returns: \u0027http_requests_total 42\\n\u0027"},{"line_number":114,"context_line":""},{"line_number":115,"context_line":"        format_metric(\u0027http_requests_total\u0027, 42,"}],"source_content_type":"text/x-python","patch_set":26,"id":"82e5cdc5_31ce0a32","line":112,"updated":"2026-05-15 19:59:23.000000000","message":"should rename this in examples?","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a04019e2e8bb266360d9d0f587f3ed5868404632","unresolved":false,"context_lines":[{"line_number":109,"context_line":"    Format a single metric into Prometheus text format string."},{"line_number":110,"context_line":""},{"line_number":111,"context_line":"    Examples:"},{"line_number":112,"context_line":"        format_metric(\u0027http_requests_total\u0027, 42)"},{"line_number":113,"context_line":"        # Returns: \u0027http_requests_total 42\\n\u0027"},{"line_number":114,"context_line":""},{"line_number":115,"context_line":"        format_metric(\u0027http_requests_total\u0027, 42,"}],"source_content_type":"text/x-python","patch_set":26,"id":"4ec6859d_adefdc31","line":112,"in_reply_to":"82e5cdc5_31ce0a32","updated":"2026-05-19 03:19:45.000000000","message":"Acknowledged","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"972b28563a02fa2ef2d7a8bf90e14b0818ffa7d4","unresolved":true,"context_lines":[{"line_number":137,"context_line":"        return name + \u0027 \u0027 + str(value) + \u0027\\n\u0027"},{"line_number":138,"context_line":""},{"line_number":139,"context_line":""},{"line_number":140,"context_line":"def format_metrics_batch(metrics_list):"},{"line_number":141,"context_line":"    \"\"\""},{"line_number":142,"context_line":"    Format multiple metrics into a Prometheus text format string."},{"line_number":143,"context_line":""}],"source_content_type":"text/x-python","patch_set":26,"id":"5b300bf2_2f7bd077","line":140,"updated":"2026-05-15 23:54:38.000000000","message":"How is this method different from `PrometheusClient._render_all_stats`? It seems that both output the same thing but using slightly different input.","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a04019e2e8bb266360d9d0f587f3ed5868404632","unresolved":false,"context_lines":[{"line_number":137,"context_line":"        return name + \u0027 \u0027 + str(value) + \u0027\\n\u0027"},{"line_number":138,"context_line":""},{"line_number":139,"context_line":""},{"line_number":140,"context_line":"def format_metrics_batch(metrics_list):"},{"line_number":141,"context_line":"    \"\"\""},{"line_number":142,"context_line":"    Format multiple metrics into a Prometheus text format string."},{"line_number":143,"context_line":""}],"source_content_type":"text/x-python","patch_set":26,"id":"eb7f9cfd_092f43fd","line":140,"in_reply_to":"5b300bf2_2f7bd077","updated":"2026-05-19 03:19:45.000000000","message":"yes, totally! Ive modified write_all_stats to use format_metrics_batch","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"972b28563a02fa2ef2d7a8bf90e14b0818ffa7d4","unresolved":true,"context_lines":[{"line_number":170,"context_line":"        prom_user_label_hostname\u003dmyhost"},{"line_number":171,"context_line":"        prom_user_label_region\u003dus-west"},{"line_number":172,"context_line":""},{"line_number":173,"context_line":"    Default labels are automatically added to all metrics as \u0027user_*\u0027 labels."},{"line_number":174,"context_line":""},{"line_number":175,"context_line":"    :param conf: Configuration dictionary"},{"line_number":176,"context_line":"    :returns: PrometheusClient instance"}],"source_content_type":"text/x-python","patch_set":26,"id":"ffe4bdf4_948a9e97","line":173,"updated":"2026-05-15 23:54:38.000000000","message":"It took me a minute to realize that user labels and default labels are the same thing. I vote we stick to one of those throughout.","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a04019e2e8bb266360d9d0f587f3ed5868404632","unresolved":false,"context_lines":[{"line_number":170,"context_line":"        prom_user_label_hostname\u003dmyhost"},{"line_number":171,"context_line":"        prom_user_label_region\u003dus-west"},{"line_number":172,"context_line":""},{"line_number":173,"context_line":"    Default labels are automatically added to all metrics as \u0027user_*\u0027 labels."},{"line_number":174,"context_line":""},{"line_number":175,"context_line":"    :param conf: Configuration dictionary"},{"line_number":176,"context_line":"    :returns: PrometheusClient instance"}],"source_content_type":"text/x-python","patch_set":26,"id":"1968e011_e4b03535","line":173,"in_reply_to":"ffe4bdf4_948a9e97","updated":"2026-05-19 03:19:45.000000000","message":"Acknowledged","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"972b28563a02fa2ef2d7a8bf90e14b0818ffa7d4","unresolved":true,"context_lines":[{"line_number":182,"context_line":"    # Extract default labels from configuration"},{"line_number":183,"context_line":"    default_labels \u003d {}"},{"line_number":184,"context_line":"    for k, v in conf.items():"},{"line_number":185,"context_line":"        if not k.startswith(PROM_CONF_USER_LABEL_PREFIX):"},{"line_number":186,"context_line":"            continue"},{"line_number":187,"context_line":"        conf_label \u003d k[len(PROM_CONF_USER_LABEL_PREFIX):]"},{"line_number":188,"context_line":"        result \u003d USER_LABEL_PATTERN.search(conf_label)"}],"source_content_type":"text/x-python","patch_set":26,"id":"e21e38af_18544436","line":185,"updated":"2026-05-15 23:54:38.000000000","message":"It took me a minute to realize that user labels and default labels are the same thing. I vote we stick to one of those throughout.","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a04019e2e8bb266360d9d0f587f3ed5868404632","unresolved":false,"context_lines":[{"line_number":182,"context_line":"    # Extract default labels from configuration"},{"line_number":183,"context_line":"    default_labels \u003d {}"},{"line_number":184,"context_line":"    for k, v in conf.items():"},{"line_number":185,"context_line":"        if not k.startswith(PROM_CONF_USER_LABEL_PREFIX):"},{"line_number":186,"context_line":"            continue"},{"line_number":187,"context_line":"        conf_label \u003d k[len(PROM_CONF_USER_LABEL_PREFIX):]"},{"line_number":188,"context_line":"        result \u003d USER_LABEL_PATTERN.search(conf_label)"}],"source_content_type":"text/x-python","patch_set":26,"id":"1f318665_97d9ad7c","line":185,"in_reply_to":"e21e38af_18544436","updated":"2026-05-19 03:19:45.000000000","message":"Done","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":36606,"name":"Yan Xiao","display_name":"Yan","email":"yanxiao@nvidia.com","username":"yanxiao"},"change_message_id":"c757555c05ceda476908f7253accf8f9e86b3ebf","unresolved":true,"context_lines":[{"line_number":212,"context_line":"    def _render_all_stats(self):"},{"line_number":213,"context_line":"        \"\"\""},{"line_number":214,"context_line":"        Render all accumulated stats as a Prometheus-formatted string."},{"line_number":215,"context_line":""},{"line_number":216,"context_line":"        Each metric line ends with a newline character. If stats exist,"},{"line_number":217,"context_line":"        the returned string will always end with a newline."},{"line_number":218,"context_line":""}],"source_content_type":"text/x-python","patch_set":26,"id":"0eff02a6_c9fd770e","line":215,"updated":"2026-05-18 19:16:18.000000000","message":"IIRC from prometheus document, the #TYPE metadata comment is optional, although not sure if the counter or gauge types are required to load the text format","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":212,"context_line":"    def _render_all_stats(self):"},{"line_number":213,"context_line":"        \"\"\""},{"line_number":214,"context_line":"        Render all accumulated stats as a Prometheus-formatted string."},{"line_number":215,"context_line":""},{"line_number":216,"context_line":"        Each metric line ends with a newline character. If stats exist,"},{"line_number":217,"context_line":"        the returned string will always end with a newline."},{"line_number":218,"context_line":""}],"source_content_type":"text/x-python","patch_set":26,"id":"3cb6ee4d_4f4d73dd","line":215,"in_reply_to":"0eff02a6_c9fd770e","updated":"2026-05-27 05:49:00.000000000","message":"i\u0027m 100% sure they are optional; we have lots of scripts that output \"prom compatible metrics\" that we collect successfully without the `#TYPE` annotation comments.  (just go troll around `/opt/ss/support/metrics/` and look at all the `.prom` files)\n\nHOWEVER!  It would be at LEAST \"fine\" (and maybe GREAT?) if could figure out an interface that allowed us to render something reasonable!?\n\n```\n[root@prod ~]# curl -s  *:9102/metrics | grep TYPE | cut -d \u0027 \u0027 -f 4 | sort | uniq -c\n     24 counter\n      5 gauge\n      1 histogram\n      8 summary\n```\n\nRight now this client interface is very \"just give me whatever metrics_key you want and how you want to change the integer value\" - there\u0027s no real difference between how this client handles counter and gauge and it doesn\u0027t try to support histogram or those _timing/quantile \"summary\" metrics.\n\nAFAIK, that is sufficient for the current planned consumers.  If you have a use-case in mind for `PrometheusClient` where this implementation would NOT satisfy the requirements those would be to capture here!\n\nIf we wanted to support \"proper\" `#TYPE` annotations I think there\u0027s two main design problems with implementation:\n\n1) you have to somehow declare the type for each metric name and decide how to handle attempts to change it\n2) when doing the rending you want to produce one `#TYPE` annotation per metric *name* - you don\u0027t produce a `#TYPE` annotation for each metrics *line*\n\ncorrect:\n\n```\n# TYPE swift_proxy_server_response_body_bytes counter\nswift_proxy_server_response_body_bytes{api\u003d\"swift\",hostname\u003d\"s8k-host.example.com\",method\u003d\"GET\",status\u003d\"200\"} 90168\nswift_proxy_server_response_body_bytes{api\u003d\"swift\",hostname\u003d\"s8k-host.example.com\",method\u003d\"POST\",status\u003d\"200\"} 4.3382815854e+10\n```\n\nINCORRECT:\n\n```\n# TYPE swift_proxy_server_response_body_bytes counter\nswift_proxy_server_response_body_bytes{api\u003d\"swift\",hostname\u003d\"s8k-host.example.com\",method\u003d\"GET\",status\u003d\"200\"} 90168\n# TYPE swift_proxy_server_response_body_bytes counter\nswift_proxy_server_response_body_bytes{api\u003d\"swift\",hostname\u003d\"s8k-host.example.com\",method\u003d\"POST\",status\u003d\"200\"} 4.3382815854e+10\n```\n\notel\u0027s python bindings address this by making you declare your metrics up front:\n\nhttps://opentelemetry.io/docs/languages/python/instrumentation/#metrics\n\npersonally I find all the synchronous/asynchronous instruments a bit too heavy handed for my tastes and like the simplicity of this implementation and mostly importantly it\u0027s symmetry with how we instrument statsd_client metrics (in-line, ad-hoc, on-demand, when needed)\n\n... if we wanted something \"more fancy\" i\u0027d be more inclined to copy otel than invent our own different solution to the fundamental design requirements of pre-declared metrics with types and descriptions.","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":212,"context_line":"    def _render_all_stats(self):"},{"line_number":213,"context_line":"        \"\"\""},{"line_number":214,"context_line":"        Render all accumulated stats as a Prometheus-formatted string."},{"line_number":215,"context_line":""},{"line_number":216,"context_line":"        Each metric line ends with a newline character. If stats exist,"},{"line_number":217,"context_line":"        the returned string will always end with a newline."},{"line_number":218,"context_line":""}],"source_content_type":"text/x-python","patch_set":26,"id":"e1a2c5f1_ceb63240","line":215,"in_reply_to":"3cb6ee4d_4f4d73dd","updated":"2026-06-03 16:14:27.000000000","message":"Done","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"972b28563a02fa2ef2d7a8bf90e14b0818ffa7d4","unresolved":true,"context_lines":[{"line_number":220,"context_line":"                  (or empty string if no stats accumulated)"},{"line_number":221,"context_line":"        \"\"\""},{"line_number":222,"context_line":"        lines \u003d []"},{"line_number":223,"context_line":"        for metrics_key, value in sorted(self._stats.items()):"},{"line_number":224,"context_line":"            # metrics_key is (name, frozenset(labels))"},{"line_number":225,"context_line":"            name, labels_frozenset \u003d metrics_key"},{"line_number":226,"context_line":"            labels_dict \u003d dict(labels_frozenset) if labels_frozenset else {}"}],"source_content_type":"text/x-python","patch_set":26,"id":"c4b75279_9891f327","line":223,"updated":"2026-05-15 23:54:38.000000000","message":"Tests pass for me when we drop `sort`:\n```\ndiff --git a/swift/common/prom_metrics.py b/swift/common/prom_metrics.py\nindex 7a4114d80..c20d95a6c 100644\n--- a/swift/common/prom_metrics.py\n+++ b/swift/common/prom_metrics.py\n@@ -223 +223 @@ class PrometheusClient:\n-        for metrics_key, value in sorted(self._stats.items()):\n+        for metrics_key, value in self._stats.items():\n```","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a04019e2e8bb266360d9d0f587f3ed5868404632","unresolved":false,"context_lines":[{"line_number":220,"context_line":"                  (or empty string if no stats accumulated)"},{"line_number":221,"context_line":"        \"\"\""},{"line_number":222,"context_line":"        lines \u003d []"},{"line_number":223,"context_line":"        for metrics_key, value in sorted(self._stats.items()):"},{"line_number":224,"context_line":"            # metrics_key is (name, frozenset(labels))"},{"line_number":225,"context_line":"            name, labels_frozenset \u003d metrics_key"},{"line_number":226,"context_line":"            labels_dict \u003d dict(labels_frozenset) if labels_frozenset else {}"}],"source_content_type":"text/x-python","patch_set":26,"id":"cd709003_d16ddd3c","line":223,"in_reply_to":"c4b75279_9891f327","updated":"2026-05-19 03:19:45.000000000","message":"Acknowledged","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"972b28563a02fa2ef2d7a8bf90e14b0818ffa7d4","unresolved":true,"context_lines":[{"line_number":223,"context_line":"        for metrics_key, value in sorted(self._stats.items()):"},{"line_number":224,"context_line":"            # metrics_key is (name, frozenset(labels))"},{"line_number":225,"context_line":"            name, labels_frozenset \u003d metrics_key"},{"line_number":226,"context_line":"            labels_dict \u003d dict(labels_frozenset) if labels_frozenset else {}"},{"line_number":227,"context_line":"            line \u003d _build_line_parts(name, value, labels_dict)"},{"line_number":228,"context_line":"            lines.append(line)"},{"line_number":229,"context_line":"        return \u0027\u0027.join(lines)"}],"source_content_type":"text/x-python","patch_set":26,"id":"40ce8e73_7ef11f76","line":226,"updated":"2026-05-15 23:54:38.000000000","message":"I would\u0027ve imagined `else None` since that\u0027s what is implied by `_build_line_parts`\u0027s signature when we don\u0027t have labels.","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a04019e2e8bb266360d9d0f587f3ed5868404632","unresolved":false,"context_lines":[{"line_number":223,"context_line":"        for metrics_key, value in sorted(self._stats.items()):"},{"line_number":224,"context_line":"            # metrics_key is (name, frozenset(labels))"},{"line_number":225,"context_line":"            name, labels_frozenset \u003d metrics_key"},{"line_number":226,"context_line":"            labels_dict \u003d dict(labels_frozenset) if labels_frozenset else {}"},{"line_number":227,"context_line":"            line \u003d _build_line_parts(name, value, labels_dict)"},{"line_number":228,"context_line":"            lines.append(line)"},{"line_number":229,"context_line":"        return \u0027\u0027.join(lines)"}],"source_content_type":"text/x-python","patch_set":26,"id":"d7a8a66a_dc96f541","line":226,"in_reply_to":"40ce8e73_7ef11f76","updated":"2026-05-19 03:19:45.000000000","message":"Done","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"972b28563a02fa2ef2d7a8bf90e14b0818ffa7d4","unresolved":true,"context_lines":[{"line_number":232,"context_line":"        self._stats[metric_key(name, **(labels or {}))] \u003d value"},{"line_number":233,"context_line":""},{"line_number":234,"context_line":"    def write_all_stats(self, filename\u003dNone):"},{"line_number":235,"context_line":"        filename \u003d filename or self.filename"},{"line_number":236,"context_line":"        if not filename or not self._stats:"},{"line_number":237,"context_line":"            return"},{"line_number":238,"context_line":"        stats_data \u003d self._render_all_stats()"}],"source_content_type":"text/x-python","patch_set":26,"id":"bfd4044a_11b66c92","line":235,"updated":"2026-05-15 23:54:38.000000000","message":"This seems to be the only method that can be used to extract meaningful information out of this class. I don\u0027t see why you would to support `filename \u003d\u003d None`.","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a04019e2e8bb266360d9d0f587f3ed5868404632","unresolved":false,"context_lines":[{"line_number":232,"context_line":"        self._stats[metric_key(name, **(labels or {}))] \u003d value"},{"line_number":233,"context_line":""},{"line_number":234,"context_line":"    def write_all_stats(self, filename\u003dNone):"},{"line_number":235,"context_line":"        filename \u003d filename or self.filename"},{"line_number":236,"context_line":"        if not filename or not self._stats:"},{"line_number":237,"context_line":"            return"},{"line_number":238,"context_line":"        stats_data \u003d self._render_all_stats()"}],"source_content_type":"text/x-python","patch_set":26,"id":"afaac5d6_70bcaca9","line":235,"in_reply_to":"bfd4044a_11b66c92","updated":"2026-05-19 03:19:45.000000000","message":"Acknowledged","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"972b28563a02fa2ef2d7a8bf90e14b0818ffa7d4","unresolved":true,"context_lines":[{"line_number":233,"context_line":""},{"line_number":234,"context_line":"    def write_all_stats(self, filename\u003dNone):"},{"line_number":235,"context_line":"        filename \u003d filename or self.filename"},{"line_number":236,"context_line":"        if not filename or not self._stats:"},{"line_number":237,"context_line":"            return"},{"line_number":238,"context_line":"        stats_data \u003d self._render_all_stats()"},{"line_number":239,"context_line":"        tf \u003d None"}],"source_content_type":"text/x-python","patch_set":26,"id":"ec93795c_12431d01","line":236,"updated":"2026-05-15 23:54:38.000000000","message":"Why would we check `self._stats` here?","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a04019e2e8bb266360d9d0f587f3ed5868404632","unresolved":false,"context_lines":[{"line_number":233,"context_line":""},{"line_number":234,"context_line":"    def write_all_stats(self, filename\u003dNone):"},{"line_number":235,"context_line":"        filename \u003d filename or self.filename"},{"line_number":236,"context_line":"        if not filename or not self._stats:"},{"line_number":237,"context_line":"            return"},{"line_number":238,"context_line":"        stats_data \u003d self._render_all_stats()"},{"line_number":239,"context_line":"        tf \u003d None"}],"source_content_type":"text/x-python","patch_set":26,"id":"58ba0323_b46295a9","line":236,"in_reply_to":"ec93795c_12431d01","updated":"2026-05-19 03:19:45.000000000","message":"just checking if there are any stats to write.\nmodified it to:\nif not self._stats:\n            raise RuntimeError(\u0027no stats to write\u0027)","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":38767,"name":"Wael Halbawi","display_name":"Wael Halbawi","email":"whalbawi@nvidia.com","username":"whalbawi"},"change_message_id":"972b28563a02fa2ef2d7a8bf90e14b0818ffa7d4","unresolved":true,"context_lines":[{"line_number":250,"context_line":"                except FileNotFoundError:"},{"line_number":251,"context_line":"                    pass"},{"line_number":252,"context_line":""},{"line_number":253,"context_line":"    def record(self, name, value, labels\u003dNone):"},{"line_number":254,"context_line":"        all_labels \u003d dict(self.default_labels)"},{"line_number":255,"context_line":"        if labels is not None:"},{"line_number":256,"context_line":"            all_labels.update(labels)"}],"source_content_type":"text/x-python","patch_set":26,"id":"9f0b4bf4_8b82d32a","line":253,"updated":"2026-05-15 23:54:38.000000000","message":"Are we OK with this method overwriting the value of an existing key? I think we need more tests to assert expected behavior.","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a04019e2e8bb266360d9d0f587f3ed5868404632","unresolved":false,"context_lines":[{"line_number":250,"context_line":"                except FileNotFoundError:"},{"line_number":251,"context_line":"                    pass"},{"line_number":252,"context_line":""},{"line_number":253,"context_line":"    def record(self, name, value, labels\u003dNone):"},{"line_number":254,"context_line":"        all_labels \u003d dict(self.default_labels)"},{"line_number":255,"context_line":"        if labels is not None:"},{"line_number":256,"context_line":"            all_labels.update(labels)"}],"source_content_type":"text/x-python","patch_set":26,"id":"f160976a_73c402cb","line":253,"in_reply_to":"9f0b4bf4_8b82d32a","updated":"2026-05-19 03:19:45.000000000","message":"we are okay for now to overwrite the value of existing key","commit_id":"5e0d0d3c8322ec5a4e8d2fdb9c9c5be3ab246a07"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":27,"context_line":"PROM_CONF_USER_LABEL_PREFIX \u003d \u0027prom_user_label_\u0027"},{"line_number":28,"context_line":"PROM_USER_LABEL_NAMESPACE \u003d \u0027user_\u0027"},{"line_number":29,"context_line":"USER_LABEL_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_]\")"},{"line_number":30,"context_line":"USER_VALUE_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_.\\-]\")"},{"line_number":31,"context_line":""},{"line_number":32,"context_line":""},{"line_number":33,"context_line":"def parse_metrics(line):"}],"source_content_type":"text/x-python","patch_set":28,"id":"6d0ff3cf_1119db96","line":30,"updated":"2026-05-27 05:49:00.000000000","message":"we have some internal tickets that discussion how annoying it is that USER_VALUE_PATTERN in statsd doesn\u0027t support `-` in user labels\n\n... it would great to do a little drive-by to make our user-label handling consistent while at the same time *better*\n\nmaybe a common.metrics module that just defines a couple of re.compile?  Or a helpers for pulling user_labels from config dicts?","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"71d49bb95648321e170b6a229502aae05ca8c4b4","unresolved":false,"context_lines":[{"line_number":27,"context_line":"PROM_CONF_USER_LABEL_PREFIX \u003d \u0027prom_user_label_\u0027"},{"line_number":28,"context_line":"PROM_USER_LABEL_NAMESPACE \u003d \u0027user_\u0027"},{"line_number":29,"context_line":"USER_LABEL_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_]\")"},{"line_number":30,"context_line":"USER_VALUE_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_.\\-]\")"},{"line_number":31,"context_line":""},{"line_number":32,"context_line":""},{"line_number":33,"context_line":"def parse_metrics(line):"}],"source_content_type":"text/x-python","patch_set":28,"id":"ed294edf_d5ab139b","line":30,"in_reply_to":"6d0ff3cf_1119db96","updated":"2026-06-05 14:11:56.000000000","message":"991664: common: share metric user-label parsing between prom and statsd | https://review.opendev.org/c/openstack/swift/+/991664","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":39,"context_line":"    - Without labels: metric_name 42.0"},{"line_number":40,"context_line":""},{"line_number":41,"context_line":"    :param line: String in Prometheus text format"},{"line_number":42,"context_line":"    :returns: Tuple of (name, labels_dict, value)"},{"line_number":43,"context_line":"    :raises ValueError: If format is incorrect or unparseable"},{"line_number":44,"context_line":"    \"\"\""},{"line_number":45,"context_line":"    line \u003d line.strip()"}],"source_content_type":"text/x-python","patch_set":28,"id":"990d115f_13ab926c","line":42,"updated":"2026-05-27 05:49:00.000000000","message":"I think this is a really unfortunate asymmetry with the `_build_line_parts`\n\nwouldn\u0027t it be cute if these tests passed:\n\n```\ndiff --git a/test/unit/common/test_prom_metrics.py b/test/unit/common/test_prom_metrics.py\nindex b4db85f84..145305df1 100644\n--- a/test/unit/common/test_prom_metrics.py\n+++ b/test/unit/common/test_prom_metrics.py\n@@ -213,6 +213,14 @@ class TestPrometheusParsing(unittest.TestCase):\n             result,\n             \u0027metric{a_label\u003d\"a\",m_label\u003d\"m\",z_label\u003d\"z\"} 10\\n\u0027)\n \n+    def test_parse_build_line_parts_roundtrip(self):\n+        metric \u003d (\u0027http_requests_total\u0027, 42, {\u0027method\u0027: \u0027GET\u0027, \u0027endpoint\u0027: \u0027/api\u0027})\n+        self.assertEqual(metric, parse_metrics(_build_line_parts(*metric)))\n+\n+    def test_build_line_parts_parse_roundtrip(self):\n+        metric \u003d \u0027http_requests_total{endpoint\u003d\"/api\",method\u003d\"GET\"} 42\\n\u0027\n+        self.assertEqual(metric, _build_line_parts(*parse_metrics(metric)))\n+\n     def test_format_metrics_batch(self):\n         result \u003d format_metrics_batch([\n             (\u0027simple_metric\u0027, 42),\n```","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":39,"context_line":"    - Without labels: metric_name 42.0"},{"line_number":40,"context_line":""},{"line_number":41,"context_line":"    :param line: String in Prometheus text format"},{"line_number":42,"context_line":"    :returns: Tuple of (name, labels_dict, value)"},{"line_number":43,"context_line":"    :raises ValueError: If format is incorrect or unparseable"},{"line_number":44,"context_line":"    \"\"\""},{"line_number":45,"context_line":"    line \u003d line.strip()"}],"source_content_type":"text/x-python","patch_set":28,"id":"287507e4_32768a65","line":42,"in_reply_to":"990d115f_13ab926c","updated":"2026-06-03 16:14:27.000000000","message":"Done","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":87,"context_line":"            raise ValueError("},{"line_number":88,"context_line":"                f\"Invalid Prometheus format: {line!r} - \""},{"line_number":89,"context_line":"                \"expected format \u0027metric_name value\u0027 or \""},{"line_number":90,"context_line":"                \"\u0027metric_name{{labels}} value\u0027\""},{"line_number":91,"context_line":"            )"},{"line_number":92,"context_line":"        name \u003d parts[0]"},{"line_number":93,"context_line":"        labels \u003d {}"}],"source_content_type":"text/x-python","patch_set":28,"id":"4d9fda0b_1c939820","line":90,"updated":"2026-05-27 05:49:00.000000000","message":"so funny thing about f-strings:\n\n```\n\u003e\u003e\u003e x \u003d (\n... \"foo\"\n... \"bar\"\n... )\n\u003e\u003e\u003e x\n\u0027foobar\u0027\n\u003e\u003e\u003e y \u003d (\n... f\"{x}\"\n... \"{x}\"\n... )\n\u003e\u003e\u003e y\n\u0027foobar{x}\u0027\n```\n\npresumably you\u0027re \"escaping\" the `{{labels}}` because you are expecting the f-string to continue across line breaks - but it does not:\n\n```\n\u003e\u003e\u003e raise ValueError(\n...    f\u0027{x}\u0027\n...    \u0027{{labels}}\u0027\n... )\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\nValueError: foobar{{labels}}\n\u003e\u003e\u003e raise ValueError(\n...     f\u0027{x}\u0027\n...     f\u0027{{labels}}\u0027\n... )\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\nValueError: foobar{labels}\n```","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":87,"context_line":"            raise ValueError("},{"line_number":88,"context_line":"                f\"Invalid Prometheus format: {line!r} - \""},{"line_number":89,"context_line":"                \"expected format \u0027metric_name value\u0027 or \""},{"line_number":90,"context_line":"                \"\u0027metric_name{{labels}} value\u0027\""},{"line_number":91,"context_line":"            )"},{"line_number":92,"context_line":"        name \u003d parts[0]"},{"line_number":93,"context_line":"        labels \u003d {}"}],"source_content_type":"text/x-python","patch_set":28,"id":"1ca65171_692527fb","line":90,"in_reply_to":"4d9fda0b_1c939820","updated":"2026-06-03 16:14:27.000000000","message":"ack! just using regular strings now on!","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":98,"context_line":"    except ValueError:"},{"line_number":99,"context_line":"        raise ValueError("},{"line_number":100,"context_line":"            f\"Invalid metric value \u0027{value_str}\u0027: must be a number. \""},{"line_number":101,"context_line":"            f\"Format: metric_name value or metric_name{{labels}} value\""},{"line_number":102,"context_line":"        )"},{"line_number":103,"context_line":""},{"line_number":104,"context_line":"    return name, labels, value"}],"source_content_type":"text/x-python","patch_set":28,"id":"1fe831e6_3302eb81","line":101,"updated":"2026-05-27 05:49:00.000000000","message":"oh here we got the f-string continuation correct!","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":98,"context_line":"    except ValueError:"},{"line_number":99,"context_line":"        raise ValueError("},{"line_number":100,"context_line":"            f\"Invalid metric value \u0027{value_str}\u0027: must be a number. \""},{"line_number":101,"context_line":"            f\"Format: metric_name value or metric_name{{labels}} value\""},{"line_number":102,"context_line":"        )"},{"line_number":103,"context_line":""},{"line_number":104,"context_line":"    return name, labels, value"}],"source_content_type":"text/x-python","patch_set":28,"id":"b4375d9b_74f03e69","line":101,"in_reply_to":"1fe831e6_3302eb81","updated":"2026-06-03 16:14:27.000000000","message":"Done","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":101,"context_line":"            f\"Format: metric_name value or metric_name{{labels}} value\""},{"line_number":102,"context_line":"        )"},{"line_number":103,"context_line":""},{"line_number":104,"context_line":"    return name, labels, value"},{"line_number":105,"context_line":""},{"line_number":106,"context_line":""},{"line_number":107,"context_line":"def _build_line_parts(name, value, labels\u003dNone):"}],"source_content_type":"text/x-python","patch_set":28,"id":"36c5a466_dc4e44a9","line":104,"updated":"2026-05-27 05:49:00.000000000","message":"I asked codex if it could make this method any shorter and it decided to go with regex:\n\n```\ndiff --git a/swift/common/prom_metrics.py b/swift/common/prom_metrics.py\nindex fb5ce7415..de83d9509 100644\n--- a/swift/common/prom_metrics.py\n+++ b/swift/common/prom_metrics.py\n@@ -28,6 +28,9 @@ PROM_CONF_USER_LABEL_PREFIX \u003d \u0027prom_user_label_\u0027\n PROM_USER_LABEL_NAMESPACE \u003d \u0027user_\u0027\n USER_LABEL_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_]\")\n USER_VALUE_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_.\\-]\")\n+_METRIC_LINE_PATTERN \u003d re.compile(\n+    r\u0027^(?P\u003cname\u003e[^{\\s]+)(?:\\{(?P\u003clabels\u003e[^}]*)\\})?\\s+\u0027\n+    r\u0027(?P\u003cvalue\u003e\\S+)$\u0027)\n \n \n def parse_metrics(line):\n@@ -46,54 +49,29 @@ def parse_metrics(line):\n     if not line:\n         raise ValueError(\"Empty metric line\")\n \n-    if \u0027{\u0027 in line:\n-        try:\n-            name_end \u003d line.index(\u0027{\u0027)\n-            labels_end \u003d line.index(\u0027}\u0027)\n-        except ValueError:\n-            raise ValueError(\n-                f\"Invalid Prometheus format: {line!r} - \"\n-                \"expected format \u0027metric_name{{label\u003d\\\"value\\\"}} value\u0027\")\n-\n-        name \u003d line[:name_end].strip()\n-        labels_str \u003d line[name_end + 1:labels_end]\n-        value_str \u003d line[labels_end + 1:].strip()\n-\n-        if not name:\n-            raise ValueError(\"Metric name cannot be empty\")\n-        if labels_end \u003c name_end:\n-            raise ValueError(\"Invalid label format: \"\n-                             \"closing brace before opening brace\")\n-\n-        # Parse labels: key\u003d\"value\",key2\u003d\"value2\"\n-        labels \u003d {}\n-        if labels_str:\n-            for pair in labels_str.split(\u0027,\u0027):\n-                if \u0027\u003d\u0027 not in pair:\n-                    raise ValueError(\n-                        f\"Invalid label pair \u0027{pair}\u0027: missing \u0027\u003d\u0027\")\n-                k, v \u003d pair.split(\u0027\u003d\u0027, 1)\n-                k \u003d k.strip()\n-                v \u003d v.strip().strip(\u0027\"\u0027)\n-                if not k:\n-                    raise ValueError(\"Label key cannot be empty\")\n-                labels[k] \u003d v\n-\n-        if not value_str:\n-            raise ValueError(\"Missing metric value after labels\")\n-    else:\n-        parts \u003d line.split()\n-        if len(parts) \u003c 2:\n-            raise ValueError(\n-                f\"Invalid Prometheus format: {line!r} - \"\n-                \"expected format \u0027metric_name value\u0027 or \"\n-                \"\u0027metric_name{{labels}} value\u0027\"\n-            )\n-        name \u003d parts[0]\n-        labels \u003d {}\n-        value_str \u003d parts[1]\n+    match \u003d _METRIC_LINE_PATTERN.match(line)\n+    if not match:\n+        raise ValueError(\n+            f\"Invalid Prometheus format: {line!r} - \"\n+            \"expected format \u0027metric_name value\u0027 or \"\n+            \"\u0027metric_name{{labels}} value\u0027\"\n+        )\n+\n+    labels \u003d {}\n+    labels_str \u003d match.group(\u0027labels\u0027)\n+    if labels_str:\n+        for pair in labels_str.split(\u0027,\u0027):\n+            if \u0027\u003d\u0027 not in pair:\n+                raise ValueError(\n+                    f\"Invalid label pair \u0027{pair}\u0027: missing \u0027\u003d\u0027\")\n+            k, v \u003d pair.split(\u0027\u003d\u0027, 1)\n+            k \u003d k.strip()\n+            if not k:\n+                raise ValueError(\"Label key cannot be empty\")\n+            labels[k] \u003d v.strip().strip(\u0027\"\u0027)\n \n     try:\n+        value_str \u003d match.group(\u0027value\u0027)\n         value \u003d float(value_str)\n     except ValueError:\n         raise ValueError(\n@@ -101,7 +79,7 @@ def parse_metrics(line):\n             f\"Format: metric_name value or metric_name{{labels}} value\"\n         )\n \n-    return name, labels, value\n+    return match.group(\u0027name\u0027), labels, value\n \n \n def _build_line_parts(name, value, labels\u003dNone):\n```\n\n... which is probably more performant b/c the regex engine is going to be in c-land, but meh... would I *rather* have to maintain:\n\n```\n_METRIC_LINE_PATTERN \u003d re.compile(\n    r\u0027^(?P\u003cname\u003e[^{\\s]+)(?:\\{(?P\u003clabels\u003e[^}]*)\\})?\\s+\u0027\n    r\u0027(?P\u003cvalue\u003e\\S+)$\u0027)\n```\n\n... *I* don\u0027t think so?\n\nFWIW when I asked it to \"improve this parser *without* using regex\" it got SO MUCH WORSE - so, kudos.","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":101,"context_line":"            f\"Format: metric_name value or metric_name{{labels}} value\""},{"line_number":102,"context_line":"        )"},{"line_number":103,"context_line":""},{"line_number":104,"context_line":"    return name, labels, value"},{"line_number":105,"context_line":""},{"line_number":106,"context_line":""},{"line_number":107,"context_line":"def _build_line_parts(name, value, labels\u003dNone):"}],"source_content_type":"text/x-python","patch_set":28,"id":"e9acc75e_edd0e746","line":104,"in_reply_to":"36c5a466_dc4e44a9","updated":"2026-06-03 16:14:27.000000000","message":"yepp, regex looks perfect!","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":122,"context_line":"    if labels:"},{"line_number":123,"context_line":"        # Sort labels for consistent output"},{"line_number":124,"context_line":"        labels_str \u003d \u0027,\u0027.join("},{"line_number":125,"context_line":"            f\u0027{k}\u003d\"{v}\"\u0027 for k, v in sorted(labels.items())"},{"line_number":126,"context_line":"        )"},{"line_number":127,"context_line":"        return name + \u0027{\u0027 + labels_str + \u0027} \u0027 + str(value) + \u0027\\n\u0027"},{"line_number":128,"context_line":"    else:"}],"source_content_type":"text/x-python","patch_set":28,"id":"fdbd2bac_b0583191","line":125,"updated":"2026-05-27 05:49:00.000000000","message":"I don\u0027t want to go down this road again:\n\n949689: metrics-injection | https://review.opendev.org/c/openstack/swift/+/949689\n\nAFAIK we\u0027ve mostly settled on the idea that we should be emitting url quoted metric values:\n\nBug #2138370 “proxy-logging statsd metric label \u0027container\u0027 has inconsistently quoted value” - https://bugs.launchpad.net/swift/+bug/2138370\n\nbut in statsd client I think that\u0027s basically not enforced; and I think we should try to make prom-client accept native strings and always quote them (to prevent label injection)\n\nif a caller happens to have a quoted path in hand and doesn\u0027t want double-quoted metric values they can unquote them before they send them prom_client - but they can NEVER write un-quoted values","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"980a37deb79434391acc1b4c8cb829180051e732","unresolved":false,"context_lines":[{"line_number":122,"context_line":"    if labels:"},{"line_number":123,"context_line":"        # Sort labels for consistent output"},{"line_number":124,"context_line":"        labels_str \u003d \u0027,\u0027.join("},{"line_number":125,"context_line":"            f\u0027{k}\u003d\"{v}\"\u0027 for k, v in sorted(labels.items())"},{"line_number":126,"context_line":"        )"},{"line_number":127,"context_line":"        return name + \u0027{\u0027 + labels_str + \u0027} \u0027 + str(value) + \u0027\\n\u0027"},{"line_number":128,"context_line":"    else:"}],"source_content_type":"text/x-python","patch_set":28,"id":"0b6a576c_403993f5","line":125,"in_reply_to":"110287d5_781d1e1f","updated":"2026-07-14 22:40:50.000000000","message":"Marked as resolved.","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"60de0f1100b17a91300be1dd4aa6fcd105f0fb06","unresolved":true,"context_lines":[{"line_number":122,"context_line":"    if labels:"},{"line_number":123,"context_line":"        # Sort labels for consistent output"},{"line_number":124,"context_line":"        labels_str \u003d \u0027,\u0027.join("},{"line_number":125,"context_line":"            f\u0027{k}\u003d\"{v}\"\u0027 for k, v in sorted(labels.items())"},{"line_number":126,"context_line":"        )"},{"line_number":127,"context_line":"        return name + \u0027{\u0027 + labels_str + \u0027} \u0027 + str(value) + \u0027\\n\u0027"},{"line_number":128,"context_line":"    else:"}],"source_content_type":"text/x-python","patch_set":28,"id":"110287d5_781d1e1f","line":125,"in_reply_to":"fdbd2bac_b0583191","updated":"2026-07-08 22:45:08.000000000","message":"I also agree here.\n\nIn the Prometheus text format docs,\n\u003e label_value can be any sequence of UTF-8 characters, but the backslash (\\\\), double-quote (\"), and line feed (\\n) characters have to be escaped as \\\\\\\\, \\\\\", and \\\\n, respectively.\nhttps://prometheus.io/docs/instrumenting/exposition_formats/\n\nFor example:\n\n```python\nvalue \u003d \"\\\"\\\\\\n\"\nline \u003d _build_line_parts(\"swift_metric\", 1, {\"test\": value})\nprint(line.encode(\"unicode_escape\").decode(\"ascii\"))\n```\n\nGives:\n```text\nswift_metric{test\u003d\"\"\\\\\\n\"} 1\\n\n```\n\nBut `\"` is not escaped, which will make Prometheus freak out. We should escape `\"`, `\\\\`, and `\\n`, and add a test to show this.","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":146,"context_line":"            name, value \u003d metric_data"},{"line_number":147,"context_line":"            lines.append(_build_line_parts(name, value))"},{"line_number":148,"context_line":"        elif len(metric_data) \u003d\u003d 3:"},{"line_number":149,"context_line":"            name, value, labels \u003d metric_data"},{"line_number":150,"context_line":"            lines.append(_build_line_parts(name, value, labels))"},{"line_number":151,"context_line":"        else:"},{"line_number":152,"context_line":"            raise ValueError(f\"Invalid metric tuple: {metric_data}\")"}],"source_content_type":"text/x-python","patch_set":28,"id":"fc3e10c9_a4bbde4b","line":149,"updated":"2026-05-27 05:49:00.000000000","message":"why do we want this polymorphism?\n\nConsider this diff:\n\n```\ndiff --git a/swift/common/prom_metrics.py b/swift/common/prom_metrics.py\nindex fb5ce7415..463ef85f5 100644\n--- a/swift/common/prom_metrics.py\n+++ b/swift/common/prom_metrics.py\n@@ -140,18 +140,7 @@ def format_metrics_batch(metrics_list):\n                          (name, value, labels_dict)\n     :returns: Multi-line Prometheus-formatted string ending with newline\n     \"\"\"\n-    lines \u003d []\n-    for metric_data in metrics_list:\n-        if len(metric_data) \u003d\u003d 2:\n-            name, value \u003d metric_data\n-            lines.append(_build_line_parts(name, value))\n-        elif len(metric_data) \u003d\u003d 3:\n-            name, value, labels \u003d metric_data\n-            lines.append(_build_line_parts(name, value, labels))\n-        else:\n-            raise ValueError(f\"Invalid metric tuple: {metric_data}\")\n-\n-    return \u0027\u0027.join(lines)\n+    return \u0027\u0027.join(_build_line_parts(*item) for item in metrics_list)\n \n \n def get_prometheus_client(conf):\n```\n\nI get one test failure:\n\n```\nFAILED test/unit/common/test_prom_metrics.py::TestPrometheusParsing::test_format_metrics_batch_invalid_tuple_size - TypeError: _build_line_parts() takes from 2 to 3 positional arguments but 4 were given\n```\n\nit\u0027s expecting the `assertRaises(ValueError)` below; but I think the `TypeError` is reasonable behavior for \"garbage in\"","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":146,"context_line":"            name, value \u003d metric_data"},{"line_number":147,"context_line":"            lines.append(_build_line_parts(name, value))"},{"line_number":148,"context_line":"        elif len(metric_data) \u003d\u003d 3:"},{"line_number":149,"context_line":"            name, value, labels \u003d metric_data"},{"line_number":150,"context_line":"            lines.append(_build_line_parts(name, value, labels))"},{"line_number":151,"context_line":"        else:"},{"line_number":152,"context_line":"            raise ValueError(f\"Invalid metric tuple: {metric_data}\")"}],"source_content_type":"text/x-python","patch_set":28,"id":"54316814_878ef335","line":149,"in_reply_to":"fc3e10c9_a4bbde4b","updated":"2026-06-03 16:14:27.000000000","message":"Done","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":201,"context_line":"        self.filename \u003d conf.get(\u0027metrics_filename\u0027)"},{"line_number":202,"context_line":"        self.user_labels \u003d user_labels or {}"},{"line_number":203,"context_line":""},{"line_number":204,"context_line":"    def _update_stat_internal(self, name, value, labels\u003dNone):"},{"line_number":205,"context_line":"        self._stats[metric_key(name, **(labels or {}))] \u003d value"},{"line_number":206,"context_line":""},{"line_number":207,"context_line":"    def write_all_stats(self, filename\u003dNone):"}],"source_content_type":"text/x-python","patch_set":28,"id":"0ec2b0fc_4962a550","line":204,"updated":"2026-05-27 05:49:00.000000000","message":"this adapter feels akward; I guess the crux of the issue is `labels or {}`\n\nI love the literal for of:\n\n```\nmetrics_key(\u0027my_stats\u0027, label\u003d\u0027value\u0027)\n```\n\nbut even it has to support:\n\n```\nmetrics_key(\u0027my_stats\u0027, **{\u0027my-label\u0027: \u0027value\u0027})\n```\n\nwhat it doesn\u0027t have a great way to support is:\n\n```\nlabels \u003d None\nmetrics_key(\u0027my_stats\u0027, **(labels or {}))\n```\n\nbut everywhere `PrometheusClient` wants to use a `metrics_key` it\u0027s handling a `labels\u003dNone` kwarg\n\nwould:\n\n```\nself._stats[self._metrics_key(name, labels)] \u003d value\n```\n\nread better than:\n\n```\nself._update_stat_internal(name, value, label)\n```\n\n... I guess not *really* b/c you have to remember `self._metrics_key` instead of `metrics_key`\n\nexcept... we don\u0027t even use this helper!?\n\n```\nvagrant@vsaio-1:~/swift$ grep -R _update_stat swift/common/prom_metrics.py\n    def _update_stat_internal(self, name, value, labels\u003dNone):\n```\n\neverywhere we we have to update `self._stats` we\u0027re dealing with user_labels and have to say:\n\n```\nlabels \u003d dict(labels or {}, **self.user_labels)\nself._stats[metrics_key(name, **labels)] \u003d value\n```\n\nso maybe DRYing that out would be a *good* reason to have an `_update_stats(name, value, labels\u003dNone)` method.\n\nthe `_` prefix and the `_internal` suffix in the name are redundant; methods that start with `_` prefix are considered \"private\" (internal to the class) in python.","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":201,"context_line":"        self.filename \u003d conf.get(\u0027metrics_filename\u0027)"},{"line_number":202,"context_line":"        self.user_labels \u003d user_labels or {}"},{"line_number":203,"context_line":""},{"line_number":204,"context_line":"    def _update_stat_internal(self, name, value, labels\u003dNone):"},{"line_number":205,"context_line":"        self._stats[metric_key(name, **(labels or {}))] \u003d value"},{"line_number":206,"context_line":""},{"line_number":207,"context_line":"    def write_all_stats(self, filename\u003dNone):"}],"source_content_type":"text/x-python","patch_set":28,"id":"be29aa30_d5841af6","line":204,"in_reply_to":"0ec2b0fc_4962a550","updated":"2026-06-03 16:14:27.000000000","message":"Done","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":209,"context_line":"        if not filename:"},{"line_number":210,"context_line":"            raise RuntimeError(\u0027please supply a filename\u0027)"},{"line_number":211,"context_line":"        if not self._stats:"},{"line_number":212,"context_line":"            raise RuntimeError(\u0027no stats to write\u0027)"},{"line_number":213,"context_line":"        stats_data \u003d format_metrics_batch(["},{"line_number":214,"context_line":"            (name, value, dict(labels_frozenset))"},{"line_number":215,"context_line":"            for (name, labels_frozenset), value in self._stats.items()"}],"source_content_type":"text/x-python","patch_set":28,"id":"51be8f46_ce29ba1d","line":212,"updated":"2026-05-27 05:49:00.000000000","message":"I don\u0027t understand the reasoning for this.","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":209,"context_line":"        if not filename:"},{"line_number":210,"context_line":"            raise RuntimeError(\u0027please supply a filename\u0027)"},{"line_number":211,"context_line":"        if not self._stats:"},{"line_number":212,"context_line":"            raise RuntimeError(\u0027no stats to write\u0027)"},{"line_number":213,"context_line":"        stats_data \u003d format_metrics_batch(["},{"line_number":214,"context_line":"            (name, value, dict(labels_frozenset))"},{"line_number":215,"context_line":"            for (name, labels_frozenset), value in self._stats.items()"}],"source_content_type":"text/x-python","patch_set":28,"id":"f27f2e44_75ccc775","line":212,"in_reply_to":"51be8f46_ce29ba1d","updated":"2026-06-03 16:14:27.000000000","message":"I think that write_stats should raise an error if there is no correct file or no stats to write!","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":213,"context_line":"        stats_data \u003d format_metrics_batch(["},{"line_number":214,"context_line":"            (name, value, dict(labels_frozenset))"},{"line_number":215,"context_line":"            for (name, labels_frozenset), value in self._stats.items()"},{"line_number":216,"context_line":"        ])"},{"line_number":217,"context_line":"        tf \u003d None"},{"line_number":218,"context_line":"        try:"},{"line_number":219,"context_line":"            with tempfile.NamedTemporaryFile("}],"source_content_type":"text/x-python","patch_set":28,"id":"8c70657e_8f5588b0","line":216,"updated":"2026-05-27 05:49:00.000000000","message":"I think for the specific use case of \"how do I get my `PromethusClient` instance to render metrics to stdout/stderr\" we\u0027re going to want a public interface that will just return the formatted metrics string.\n\nHonestly I think I\u0027d git rid of format_metrics_batch:\n\n```\ndiff --git a/swift/common/prom_metrics.py b/swift/common/prom_metrics.py\nindex fb5ce7415..2f75e77b0 100644\n--- a/swift/common/prom_metrics.py\n+++ b/swift/common/prom_metrics.py\n@@ -204,16 +204,19 @@ class PrometheusClient:\n     def _update_stat_internal(self, name, value, labels\u003dNone):\n         self._stats[metric_key(name, **(labels or {}))] \u003d value\n \n+    def get_all_stats(self):\n+        return \u0027\u0027.join(\n+            _build_line_parts(name, value, dict(labels))\n+            for (name, labels), value in self._stats.items()\n+        )\n+\n     def write_all_stats(self, filename\u003dNone):\n         filename \u003d filename or self.filename\n         if not filename:\n             raise RuntimeError(\u0027please supply a filename\u0027)\n         if not self._stats:\n             raise RuntimeError(\u0027no stats to write\u0027)\n-        stats_data \u003d format_metrics_batch([\n-            (name, value, dict(labels_frozenset))\n-            for (name, labels_frozenset), value in self._stats.items()\n-        ])\n+        stats_data \u003d self.get_all_stats()\n         tf \u003d None\n         try:\n             with tempfile.NamedTemporaryFile(\n```","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":213,"context_line":"        stats_data \u003d format_metrics_batch(["},{"line_number":214,"context_line":"            (name, value, dict(labels_frozenset))"},{"line_number":215,"context_line":"            for (name, labels_frozenset), value in self._stats.items()"},{"line_number":216,"context_line":"        ])"},{"line_number":217,"context_line":"        tf \u003d None"},{"line_number":218,"context_line":"        try:"},{"line_number":219,"context_line":"            with tempfile.NamedTemporaryFile("}],"source_content_type":"text/x-python","patch_set":28,"id":"caf6f2ec_3de82d4b","line":216,"in_reply_to":"8c70657e_8f5588b0","updated":"2026-06-03 16:14:27.000000000","message":"Done","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":220,"context_line":"                    mode\u003d\u0027w\u0027, dir\u003dos.path.dirname(filename),"},{"line_number":221,"context_line":"                    delete\u003dFalse) as tf:"},{"line_number":222,"context_line":"                tf.write(stats_data)"},{"line_number":223,"context_line":"            os.rename(tf.name, filename)"},{"line_number":224,"context_line":"        finally:"},{"line_number":225,"context_line":"            if tf:"},{"line_number":226,"context_line":"                try:"}],"source_content_type":"text/x-python","patch_set":28,"id":"01f215a6_509946f2","line":223,"updated":"2026-05-27 05:49:00.000000000","message":"my review agent had some concern about using `NamedTemporaryFile` to create the file and then rename into the exposed path.  I think the idea was that the umask is going to turn out weird:\n\n```\nvagrant@vsaio-1:~$ ls -alhF test.prom \n-rw------- 1 vagrant vagrant 24 May 27 03:39 test.prom\nvagrant@vsaio-1:~$ touch test.default\nvagrant@vsaio-1:~$ ls -alhF test.default \n-rw-rw-r-- 1 vagrant vagrant 0 May 27 03:40 test.default\n```\n\nconsider the ring code:\n\n```\n    @classmethod\n    @contextlib.contextmanager\n    def open(cls, filename, *a, **kw):\n        \"\"\"\n        Open a compressed writer for ``filename``\n\n        Note that this also guarantees atomic writes using a temporary file\n\n        :returns: a context manager that provides a ``_RingGzWriter`` instance\n        \"\"\"\n        fp \u003d tempfile.NamedTemporaryFile(\n            dir\u003dos.path.dirname(filename),\n            prefix\u003dos.path.basename(filename),\n            delete\u003dFalse)\n        try:\n            with cls(fp, filename, *a, **kw) as writer:\n                yield writer\n        except BaseException:\n            fp.close()\n            os.unlink(fp.name)\n            raise\n        else:\n            fp.flush()\n            os.fsync(fp.fileno())\n            fp.close()\n            os.chmod(fp.name, 0o644)\n            os.rename(fp.name, filename)\n```\n\nN.B. the `os.chmod` (that ignores umask and just decides `0o644` is always correct)\n\nwhere as the recon dump code:\n\n```\n                with NamedTemporaryFile(dir\u003dos.path.dirname(cache_file),\n                                        delete\u003dFalse) as tf:\n                    cache_data \u003d json.dumps(cache_entry, ensure_ascii\u003dTrue,\n                                            sort_keys\u003dTrue)\n                    tf.write(cache_data.encode(\u0027ascii\u0027) + b\u0027\\n\u0027)\n                if set_owner:\n                    os.chown(tf.name, pwd.getpwnam(set_owner).pw_uid, -1)\n                renamer(tf.name, cache_file, fsync\u003dFalse)\n```\n\n... has the same \"problem\"\n\n```\nvagrant@vsaio-1:~$ ls -alhF /var/cache/swift/node1/object.recon \n-rw------- 1 vagrant vagrant 344 May 27 03:49 /var/cache/swift/node1/object.recon\n```\n\nand `write_pickle`\n\n```\n    if tmp is None:\n        tmp \u003d os.path.dirname(dest)\n    mkdirs(tmp)\n    fd, tmppath \u003d mkstemp(dir\u003dtmp, suffix\u003d\u0027.tmp\u0027)\n    with os.fdopen(fd, \u0027wb\u0027) as fo:\n        pickle.dump(obj, fo, pickle_protocol)\n        fo.flush()\n        os.fsync(fd)\n        renamer(tmppath, dest)\n```\n\n... also has the same \"problem\"\n\n```\nvagrant@vsaio-1:~$ ls -alhF /srv/node2/sdb2/objects/645/hashes.pkl\n-rw------- 1 vagrant vagrant 93 May 27 03:58 /srv/node2/sdb2/objects/645/hashes.pkl\n```\n\n*unfortunately* I think ~matters b/c our node-exporter runs as the prometheus user and if we start dumping stuff into `/opt/ss/support/metrics/` we\u0027ll actually want them to be world readable\n\n```\n[root@s8k ~]# ps -e --format user:20,cmd | grep node_exporter\nprometheus           /opt/prometheus/node_exporter --collector.processes --collector.qdisc --collector.tcpstat --collector.meminfo_numa --collector.textfile --collector.textfile.directory\u003d/opt/ss/support/metrics\n\n[root@s8k ~]# ls -alhF /opt/ss/support/metrics/\ntotal 160K\ndrwxr-xr-x 2 prometheus prometheus 4.0K May 27 04:01 ./\ndrwxr-xr-x 9 root       root       4.0K Aug 11  2025 ../\n-rw-r--r-- 1 root       root         46 Nov  8  2022 configure_version.prom\n-rw-r--r-- 1 root       root          4 May 27 03:58 corestorage_exporter.disk_firmware.json\n-rw-r--r-- 1 root       root        78K May 27 04:01 corestorage_exporter.prom\n-rw-r--r-- 1 prometheus prometheus 9.2K May 27 03:58 mcrouter_exporter.prom\n-rw-r--r-- 1 root       root        21K May 27 04:00 mcrouter_memcached_conn.prom\n-rw-rw-r-- 1 root       root        126 May 22 14:56 node_config_version_match.prom\n-rw-rw-r-- 1 root       root         90 May 27 03:59 node_connectivity.prom\n-rw-r--r-- 1 root       root          0 May 27 04:00 rss_killer.prom\n-rw-r--r-- 1 root       root        972 May 27 04:01 ss_hitch.prom\n-rw-r--r-- 1 root       root       8.1K May 27 04:00 swift_proxy_mcrouter_conn.prom\n-rw-r--r-- 1 root       root          1 May 27 04:00 swift_proxy_queues.prom\n```\n\nso... I guess slap a `os.chmod(fp.name, 0o644)` in there and find somewhere you can assert on it","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":220,"context_line":"                    mode\u003d\u0027w\u0027, dir\u003dos.path.dirname(filename),"},{"line_number":221,"context_line":"                    delete\u003dFalse) as tf:"},{"line_number":222,"context_line":"                tf.write(stats_data)"},{"line_number":223,"context_line":"            os.rename(tf.name, filename)"},{"line_number":224,"context_line":"        finally:"},{"line_number":225,"context_line":"            if tf:"},{"line_number":226,"context_line":"                try:"}],"source_content_type":"text/x-python","patch_set":28,"id":"a2e28df3_d94cbad8","line":223,"in_reply_to":"01f215a6_509946f2","updated":"2026-06-03 16:14:27.000000000","message":"Done","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":231,"context_line":"    def record(self, name, value, labels\u003dNone):"},{"line_number":232,"context_line":"        all_labels \u003d dict(self.user_labels)"},{"line_number":233,"context_line":"        if labels is not None:"},{"line_number":234,"context_line":"            all_labels.update(labels)"},{"line_number":235,"context_line":"        key \u003d metric_key(name, **all_labels)"},{"line_number":236,"context_line":"        self._stats[key] \u003d value"},{"line_number":237,"context_line":"        return key"}],"source_content_type":"text/x-python","patch_set":28,"id":"b1bcc178_8e23d33f","line":234,"updated":"2026-05-27 05:49:00.000000000","message":"I would find `dict(labels or {}, **self.user_lables)` more idiomatic:\n\n```\n\u003e\u003e\u003e labels \u003d {\u0027foo\u0027: \u0027bar\u0027}\n\u003e\u003e\u003e user_labels \u003d {\u0027baz\u0027: \u0027buz\u0027}\n\u003e\u003e\u003e dict(labels, **user_labels)\n{\u0027foo\u0027: \u0027bar\u0027, \u0027baz\u0027: \u0027buz\u0027}\n```\n\nFWIW I\u0027m not sure I have a strong opinion on the update semantics:\n\n```\n\u003e\u003e\u003e user_labels \u003d {\u0027key\u0027: \u0027default\u0027}\n\u003e\u003e\u003e labels \u003d {\u0027key\u0027: \u0027override\u0027}\n\u003e\u003e\u003e dict(labels, **user_labels)\n{\u0027key\u0027: \u0027default\u0027}\n```\n\n... I think it\u0027s mostly mute if callers stay out of the `PROM_USER_LABEL_NAMESPACE`","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":231,"context_line":"    def record(self, name, value, labels\u003dNone):"},{"line_number":232,"context_line":"        all_labels \u003d dict(self.user_labels)"},{"line_number":233,"context_line":"        if labels is not None:"},{"line_number":234,"context_line":"            all_labels.update(labels)"},{"line_number":235,"context_line":"        key \u003d metric_key(name, **all_labels)"},{"line_number":236,"context_line":"        self._stats[key] \u003d value"},{"line_number":237,"context_line":"        return key"}],"source_content_type":"text/x-python","patch_set":28,"id":"877cfb09_c449d6bc","line":234,"in_reply_to":"b1bcc178_8e23d33f","updated":"2026-06-03 16:14:27.000000000","message":"Acknowledged","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":7233,"name":"Matthew Oliver","email":"matt@oliver.net.au","username":"mattoliverau"},"change_message_id":"4a4577c4eaa5ddc710daffe26b37b38f3c602a01","unresolved":true,"context_lines":[{"line_number":63,"context_line":"        for pair in labels_str.split(\u0027,\u0027):"},{"line_number":64,"context_line":"            if \u0027\u003d\u0027 not in pair:"},{"line_number":65,"context_line":"                raise ValueError("},{"line_number":66,"context_line":"                    \"Invalid label pair \u0027{pair}\u0027: missing \u0027\u003d\u0027\")"},{"line_number":67,"context_line":"            k, v \u003d pair.split(\u0027\u003d\u0027, 1)"},{"line_number":68,"context_line":"            k \u003d k.strip()"},{"line_number":69,"context_line":"            if not k:"}],"source_content_type":"text/x-python","patch_set":30,"id":"594a2c58_492a4bfc","line":66,"updated":"2026-06-11 01:42:14.000000000","message":"I assume this is suppose to be a format string f\u0027\u0027. Because at the moment it isn\u0027t so it\u0027s not going substitute pair into the string.","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"0d657b9ffe2a15b648b2ec1582d493f4ccd80053","unresolved":false,"context_lines":[{"line_number":63,"context_line":"        for pair in labels_str.split(\u0027,\u0027):"},{"line_number":64,"context_line":"            if \u0027\u003d\u0027 not in pair:"},{"line_number":65,"context_line":"                raise ValueError("},{"line_number":66,"context_line":"                    \"Invalid label pair \u0027{pair}\u0027: missing \u0027\u003d\u0027\")"},{"line_number":67,"context_line":"            k, v \u003d pair.split(\u0027\u003d\u0027, 1)"},{"line_number":68,"context_line":"            k \u003d k.strip()"},{"line_number":69,"context_line":"            if not k:"}],"source_content_type":"text/x-python","patch_set":30,"id":"fa36d9f4_905281be","line":66,"in_reply_to":"594a2c58_492a4bfc","updated":"2026-06-15 16:13:14.000000000","message":"Acknowledged","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":7233,"name":"Matthew Oliver","email":"matt@oliver.net.au","username":"mattoliverau"},"change_message_id":"4a4577c4eaa5ddc710daffe26b37b38f3c602a01","unresolved":true,"context_lines":[{"line_number":76,"context_line":"    except ValueError:"},{"line_number":77,"context_line":"        raise ValueError("},{"line_number":78,"context_line":"            \"Invalid metric value \u0027{value_str}\u0027: must be a number. \""},{"line_number":79,"context_line":"            \"Format: metric_name value or metric_name{{labels}} value\""},{"line_number":80,"context_line":"        )"},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"    return match.group(\u0027name\u0027), labels, value"}],"source_content_type":"text/x-python","patch_set":30,"id":"9e58cfac_95c93b21","line":79,"updated":"2026-06-11 01:42:14.000000000","message":"Here too, f-string here too.","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"0d657b9ffe2a15b648b2ec1582d493f4ccd80053","unresolved":false,"context_lines":[{"line_number":76,"context_line":"    except ValueError:"},{"line_number":77,"context_line":"        raise ValueError("},{"line_number":78,"context_line":"            \"Invalid metric value \u0027{value_str}\u0027: must be a number. \""},{"line_number":79,"context_line":"            \"Format: metric_name value or metric_name{{labels}} value\""},{"line_number":80,"context_line":"        )"},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"    return match.group(\u0027name\u0027), labels, value"}],"source_content_type":"text/x-python","patch_set":30,"id":"4a1c977b_df7b1814","line":79,"in_reply_to":"9e58cfac_95c93b21","updated":"2026-06-15 16:13:14.000000000","message":"Acknowledged","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":7233,"name":"Matthew Oliver","email":"matt@oliver.net.au","username":"mattoliverau"},"change_message_id":"4a4577c4eaa5ddc710daffe26b37b38f3c602a01","unresolved":true,"context_lines":[{"line_number":133,"context_line":"        if result is not None:"},{"line_number":134,"context_line":"            raise ValueError("},{"line_number":135,"context_line":"                \"invalid character in prom user label \""},{"line_number":136,"context_line":"                \"configuration {k!r}: {result.group(0)!r}\")"},{"line_number":137,"context_line":"        result \u003d USER_VALUE_PATTERN.search(v)"},{"line_number":138,"context_line":"        if result is not None:"},{"line_number":139,"context_line":"            raise ValueError("}],"source_content_type":"text/x-python","patch_set":30,"id":"33ed8746_5ed49baf","line":136,"updated":"2026-06-11 01:42:14.000000000","message":"Here too\n\n\nAlso do we want to print out the full config key or just the user part (more of a NIT)?","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"0d657b9ffe2a15b648b2ec1582d493f4ccd80053","unresolved":false,"context_lines":[{"line_number":133,"context_line":"        if result is not None:"},{"line_number":134,"context_line":"            raise ValueError("},{"line_number":135,"context_line":"                \"invalid character in prom user label \""},{"line_number":136,"context_line":"                \"configuration {k!r}: {result.group(0)!r}\")"},{"line_number":137,"context_line":"        result \u003d USER_VALUE_PATTERN.search(v)"},{"line_number":138,"context_line":"        if result is not None:"},{"line_number":139,"context_line":"            raise ValueError("}],"source_content_type":"text/x-python","patch_set":30,"id":"bda48d2b_2a1fbef8","line":136,"in_reply_to":"33ed8746_5ed49baf","updated":"2026-06-15 16:13:14.000000000","message":"Ack\nCould print out either! Just went with all the info we had to begin!","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":7233,"name":"Matthew Oliver","email":"matt@oliver.net.au","username":"mattoliverau"},"change_message_id":"4a4577c4eaa5ddc710daffe26b37b38f3c602a01","unresolved":true,"context_lines":[{"line_number":138,"context_line":"        if result is not None:"},{"line_number":139,"context_line":"            raise ValueError("},{"line_number":140,"context_line":"                \"invalid character in configuration {k!r} \""},{"line_number":141,"context_line":"                \"value {v!r}: {result.group(0)!r}\")"},{"line_number":142,"context_line":"        conf_label \u003d PROM_USER_LABEL_NAMESPACE + conf_label"},{"line_number":143,"context_line":"        user_labels[conf_label] \u003d v"},{"line_number":144,"context_line":""}],"source_content_type":"text/x-python","patch_set":30,"id":"7b34b9db_d0c0cf0e","line":141,"updated":"2026-06-11 01:42:14.000000000","message":"And here","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"0d657b9ffe2a15b648b2ec1582d493f4ccd80053","unresolved":false,"context_lines":[{"line_number":138,"context_line":"        if result is not None:"},{"line_number":139,"context_line":"            raise ValueError("},{"line_number":140,"context_line":"                \"invalid character in configuration {k!r} \""},{"line_number":141,"context_line":"                \"value {v!r}: {result.group(0)!r}\")"},{"line_number":142,"context_line":"        conf_label \u003d PROM_USER_LABEL_NAMESPACE + conf_label"},{"line_number":143,"context_line":"        user_labels[conf_label] \u003d v"},{"line_number":144,"context_line":""}],"source_content_type":"text/x-python","patch_set":30,"id":"8cfc26c9_105e700c","line":141,"in_reply_to":"7b34b9db_d0c0cf0e","updated":"2026-06-15 16:13:14.000000000","message":"Acknowledged","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":7233,"name":"Matthew Oliver","email":"matt@oliver.net.au","username":"mattoliverau"},"change_message_id":"4a4577c4eaa5ddc710daffe26b37b38f3c602a01","unresolved":true,"context_lines":[{"line_number":180,"context_line":"                    delete\u003dFalse) as tf:"},{"line_number":181,"context_line":"                tf.write(stats_data)"},{"line_number":182,"context_line":"            os.chmod(tf.name, 0o644)"},{"line_number":183,"context_line":"            os.rename(tf.name, filename)"},{"line_number":184,"context_line":"        finally:"},{"line_number":185,"context_line":"            if tf:"},{"line_number":186,"context_line":"                try:"}],"source_content_type":"text/x-python","patch_set":30,"id":"46e1b45e_7e376fcd","line":183,"updated":"2026-06-11 01:42:14.000000000","message":"When we write all the stats out to the prom file, it\u0027s just all the `name{labels} value` lines. I think prometheus can deal with that, but should we also include any `# TYPE` statements to help scrapers? \nfor example `record` looks like it\u0027s more of a guage and add, increment, decrement seem to be counters.\n\n```\n# TYPE some_metric guage\nsome_metric{labels} 100\n...\n# TYPE other_metric counter\nother_metric{labels} 5\n```\n\nOr is the plan that they are all counters and you can either define a value with record and then increment/decrement/add to it?","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"0d657b9ffe2a15b648b2ec1582d493f4ccd80053","unresolved":false,"context_lines":[{"line_number":180,"context_line":"                    delete\u003dFalse) as tf:"},{"line_number":181,"context_line":"                tf.write(stats_data)"},{"line_number":182,"context_line":"            os.chmod(tf.name, 0o644)"},{"line_number":183,"context_line":"            os.rename(tf.name, filename)"},{"line_number":184,"context_line":"        finally:"},{"line_number":185,"context_line":"            if tf:"},{"line_number":186,"context_line":"                try:"}],"source_content_type":"text/x-python","patch_set":30,"id":"0a822c53_877c9f65","line":183,"in_reply_to":"46e1b45e_7e376fcd","updated":"2026-06-15 16:13:14.000000000","message":"they are essentially all counters! record is more of a gauge when we need to add/ record into self.stats to write later and define value!","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":7233,"name":"Matthew Oliver","email":"matt@oliver.net.au","username":"mattoliverau"},"change_message_id":"4a4577c4eaa5ddc710daffe26b37b38f3c602a01","unresolved":true,"context_lines":[{"line_number":189,"context_line":"                    pass"},{"line_number":190,"context_line":""},{"line_number":191,"context_line":"    def record(self, name, value, labels\u003dNone):"},{"line_number":192,"context_line":"        all_labels \u003d dict(labels or {}, **self.user_labels)"},{"line_number":193,"context_line":"        if labels is not None:"},{"line_number":194,"context_line":"            all_labels.update(labels)"},{"line_number":195,"context_line":"        key \u003d metric_key(name, **all_labels)"}],"source_content_type":"text/x-python","patch_set":30,"id":"86bd593e_7e6c484f","line":192,"updated":"2026-06-11 01:42:14.000000000","message":"Ahh, doesn\u0027t this mean all_labels already include the passed in labels? The if on 193 seems redundant.\n\nOr is the point that labels need to override user_labels? If it is can we just:\n```\nall_labels \u003d dict(**self.user_labels, **(labels or {}))\n```\nSo the passed in labels apply last? OR:\n\n```\nall_labels \u003d dict(self.user_labels)\nif labels:\n    all_labels.update(labels)\n```","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"0d657b9ffe2a15b648b2ec1582d493f4ccd80053","unresolved":false,"context_lines":[{"line_number":189,"context_line":"                    pass"},{"line_number":190,"context_line":""},{"line_number":191,"context_line":"    def record(self, name, value, labels\u003dNone):"},{"line_number":192,"context_line":"        all_labels \u003d dict(labels or {}, **self.user_labels)"},{"line_number":193,"context_line":"        if labels is not None:"},{"line_number":194,"context_line":"            all_labels.update(labels)"},{"line_number":195,"context_line":"        key \u003d metric_key(name, **all_labels)"}],"source_content_type":"text/x-python","patch_set":30,"id":"7db8151d_391eed20","line":192,"in_reply_to":"86bd593e_7e6c484f","updated":"2026-06-15 16:13:14.000000000","message":"Done","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":7233,"name":"Matthew Oliver","email":"matt@oliver.net.au","username":"mattoliverau"},"change_message_id":"4a4577c4eaa5ddc710daffe26b37b38f3c602a01","unresolved":true,"context_lines":[{"line_number":197,"context_line":"        return key"},{"line_number":198,"context_line":""},{"line_number":199,"context_line":"    def add(self, name, value, labels\u003dNone):"},{"line_number":200,"context_line":"        all_labels \u003d dict(labels or {}, **self.user_labels)"},{"line_number":201,"context_line":"        if labels is not None:"},{"line_number":202,"context_line":"            all_labels.update(labels)"},{"line_number":203,"context_line":"        key \u003d metric_key(name, **all_labels)"}],"source_content_type":"text/x-python","patch_set":30,"id":"95c56509_c0eceba3","line":200,"updated":"2026-06-11 01:42:14.000000000","message":"Here too.","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"0d657b9ffe2a15b648b2ec1582d493f4ccd80053","unresolved":false,"context_lines":[{"line_number":197,"context_line":"        return key"},{"line_number":198,"context_line":""},{"line_number":199,"context_line":"    def add(self, name, value, labels\u003dNone):"},{"line_number":200,"context_line":"        all_labels \u003d dict(labels or {}, **self.user_labels)"},{"line_number":201,"context_line":"        if labels is not None:"},{"line_number":202,"context_line":"            all_labels.update(labels)"},{"line_number":203,"context_line":"        key \u003d metric_key(name, **all_labels)"}],"source_content_type":"text/x-python","patch_set":30,"id":"00881995_136fef31","line":200,"in_reply_to":"95c56509_c0eceba3","updated":"2026-06-15 16:13:14.000000000","message":"Done","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":7233,"name":"Matthew Oliver","email":"matt@oliver.net.au","username":"mattoliverau"},"change_message_id":"4a4577c4eaa5ddc710daffe26b37b38f3c602a01","unresolved":true,"context_lines":[{"line_number":202,"context_line":"            all_labels.update(labels)"},{"line_number":203,"context_line":"        key \u003d metric_key(name, **all_labels)"},{"line_number":204,"context_line":"        new_value \u003d self._stats.get(key, 0) + value"},{"line_number":205,"context_line":"        self._update_stats(key, new_value)"},{"line_number":206,"context_line":"        return key"},{"line_number":207,"context_line":""},{"line_number":208,"context_line":"    def increment(self, name, labels\u003dNone):"}],"source_content_type":"text/x-python","patch_set":30,"id":"0dc99a94_06709a3c","line":205,"updated":"2026-06-11 01:42:14.000000000","message":"Do we need to think about locking, we\u0027re pulling data then updating it. What if a greentrhead or something comes inbetween the call. or is Prometheus client per worker/green thread?\n\nMaybe this is out of scope for this patch, but something we probably need to think about.","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":202,"context_line":"            all_labels.update(labels)"},{"line_number":203,"context_line":"        key \u003d metric_key(name, **all_labels)"},{"line_number":204,"context_line":"        new_value \u003d self._stats.get(key, 0) + value"},{"line_number":205,"context_line":"        self._update_stats(key, new_value)"},{"line_number":206,"context_line":"        return key"},{"line_number":207,"context_line":""},{"line_number":208,"context_line":"    def increment(self, name, labels\u003dNone):"}],"source_content_type":"text/x-python","patch_set":30,"id":"4d6b86f7_c5317ed2","line":205,"in_reply_to":"0dc99a94_06709a3c","updated":"2026-07-21 02:05:10.000000000","message":"\u003e we\u0027re pulling data then updating it\n\nthis only matters if you have pre-emptive concurrency - which we aim to at some point; but since that\u0027s currently on a feature branch I don\u0027t see a good realistic way to enforce that as a requirement onto ongoing development\n\n\u003e Prometheus client per worker/green thread\n\nwell those are two different things aren\u0027t they - there\u0027s nothing in the current change that deal with multi-process IPC - so if/when we want to add file back prometheus guages to a multi-worker client (like the reconstructor) we\u0027ll need to evolve the interface OR just let it keep using recon for IPC and only dump prom stats via the aggregation hook in the watcher.\n\nFor green threads (i.e. a bg spawn to dump/translate recon aggregations to prom) since there\u0027s no cooperative yeild (this client is all blocking) any sort of locking would just be overhead.  If (when?) we replace all cooperative concurrency when a pre-emptive concurency model we\u0027ll have to make sure all spanwed threads only ever used threadsafe objects - which the PrometheusClient as written is not yet.\n\n\u003e Maybe this is out of scope for this patch, but something we probably need to think about.\n\nthat\u0027s my take as well, I don\u0027t see anything in this proposed interface that becomes harder once we have per-emptive concurency or when we try to instrument pre-existing aggregated metrics across a multi-process workers (although we may come up with some new ideas on how to leverage this interface as temporary per-worker prometheus guage file to coordinate aggregated reporting as an alternative to recon - TBD IMHO)","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"980a37deb79434391acc1b4c8cb829180051e732","unresolved":true,"context_lines":[{"line_number":31,"context_line":"_METRIC_LINE_PATTERN \u003d re.compile("},{"line_number":32,"context_line":"    r\u0027^(?P\u003cname\u003e[^{\\s]+)(?:\\{(?P\u003clabels\u003e[^}]*)\\})?\\s+\u0027"},{"line_number":33,"context_line":"    r\u0027(?P\u003cvalue\u003e\\S+)$\u0027)"},{"line_number":34,"context_line":""},{"line_number":35,"context_line":""},{"line_number":36,"context_line":"def _escape_label_value(value):"},{"line_number":37,"context_line":"    \"\"\""}],"source_content_type":"text/x-python","patch_set":34,"id":"5a9488a4_7324bc74","line":34,"updated":"2026-07-14 22:40:50.000000000","message":"`_METRIC_LINE_PATTERN` (or maybe some other regex(s)) should validate the `metric_name` and `label_name` in `label_pair`.\n\nSo something like this should not work since spider-man has a `-`.\n\n```python\n\u003e\u003e\u003e value \u003d \u0027metric1{spider-man\u003d\"banana\"} 1\u0027\n\u003e\u003e\u003e parse_metrics(value)\n(\u0027metric1\u0027, {\u0027spider-man\u0027: \u0027banana\u0027}, 1.0)\n```\n\nSee https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels","commit_id":"b7e62281fb7e807745056fb6f58f589d19327a13"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"30d0439cea5cf0addc398aa41e6bdaba8ddae388","unresolved":false,"context_lines":[{"line_number":31,"context_line":"_METRIC_LINE_PATTERN \u003d re.compile("},{"line_number":32,"context_line":"    r\u0027^(?P\u003cname\u003e[^{\\s]+)(?:\\{(?P\u003clabels\u003e[^}]*)\\})?\\s+\u0027"},{"line_number":33,"context_line":"    r\u0027(?P\u003cvalue\u003e\\S+)$\u0027)"},{"line_number":34,"context_line":""},{"line_number":35,"context_line":""},{"line_number":36,"context_line":"def _escape_label_value(value):"},{"line_number":37,"context_line":"    \"\"\""}],"source_content_type":"text/x-python","patch_set":34,"id":"c96f6e8d_b21f8d0b","line":34,"in_reply_to":"5a9488a4_7324bc74","updated":"2026-07-15 14:50:31.000000000","message":"Done","commit_id":"b7e62281fb7e807745056fb6f58f589d19327a13"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"980a37deb79434391acc1b4c8cb829180051e732","unresolved":true,"context_lines":[{"line_number":69,"context_line":"    labels \u003d {}"},{"line_number":70,"context_line":"    labels_str \u003d match.group(\u0027labels\u0027)"},{"line_number":71,"context_line":"    if labels_str:"},{"line_number":72,"context_line":"        for pair in labels_str.split(\u0027,\u0027):"},{"line_number":73,"context_line":"            if \u0027\u003d\u0027 not in pair:"},{"line_number":74,"context_line":"                raise ValueError("},{"line_number":75,"context_line":"                    f\"Invalid label pair \u0027{pair}\u0027: missing \u0027\u003d\u0027\")"}],"source_content_type":"text/x-python","patch_set":34,"id":"3927a155_c01b243a","line":72,"range":{"start_line":72,"start_character":0,"end_line":72,"end_character":42},"updated":"2026-07-14 22:40:50.000000000","message":"This assumes that we would never have a `label_value` contains a `,`:\n```python\n\u003e\u003e\u003e from swift.common.prom_metrics import parse_metrics\n\u003e\u003e\u003e value \u003d \u0027metric1{grape\u003d\"banana\",apple\u003d\"orange,pear\"} 1\u0027\n\u003e\u003e\u003e parse_metrics(value)\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\n  File \"/vagrant/swift/swift/common/prom_metrics.py\", line 74, in parse_metrics\n    raise ValueError(\nValueError: Invalid label pair \u0027pear\"\u0027: missing \u0027\u003d\u0027\n```\n\nWhich I don\u0027t think we have metrics that have commas in the values... but it looks like container names for instance can have them:\n```shell\nswift_proxy_server_request_timing:12.3|ms|#account:AUTH_test,container:orange,pear,method:GET,resource:object,status:200\n```\n\nWe should handle commas in label values if it is possible, with a test case too!","commit_id":"b7e62281fb7e807745056fb6f58f589d19327a13"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"30d0439cea5cf0addc398aa41e6bdaba8ddae388","unresolved":false,"context_lines":[{"line_number":69,"context_line":"    labels \u003d {}"},{"line_number":70,"context_line":"    labels_str \u003d match.group(\u0027labels\u0027)"},{"line_number":71,"context_line":"    if labels_str:"},{"line_number":72,"context_line":"        for pair in labels_str.split(\u0027,\u0027):"},{"line_number":73,"context_line":"            if \u0027\u003d\u0027 not in pair:"},{"line_number":74,"context_line":"                raise ValueError("},{"line_number":75,"context_line":"                    f\"Invalid label pair \u0027{pair}\u0027: missing \u0027\u003d\u0027\")"}],"source_content_type":"text/x-python","patch_set":34,"id":"f1c3d1c0_167fe24a","line":72,"range":{"start_line":72,"start_character":0,"end_line":72,"end_character":42},"in_reply_to":"3927a155_c01b243a","updated":"2026-07-15 14:50:31.000000000","message":"Done","commit_id":"b7e62281fb7e807745056fb6f58f589d19327a13"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"7fc280942f8db115b9316a5d142e0d6564470a85","unresolved":true,"context_lines":[{"line_number":33,"context_line":"_METRIC_LINE_PATTERN \u003d re.compile("},{"line_number":34,"context_line":"    r\u0027^(?P\u003cname\u003e[^{\\s]+)(?:\\{(?P\u003clabels\u003e[^}]*)\\})?\\s+\u0027"},{"line_number":35,"context_line":"    r\u0027(?P\u003cvalue\u003e\\S+)$\u0027)"},{"line_number":36,"context_line":""},{"line_number":37,"context_line":""},{"line_number":38,"context_line":"def _escape_label_value(value):"},{"line_number":39,"context_line":"    \"\"\""}],"source_content_type":"text/x-python","patch_set":36,"id":"be8a347d_55b45558","line":36,"updated":"2026-07-15 21:05:00.000000000","message":"I should\u0027ve caught this in my prior review, I am sorry for that, BUT we also do not yet handle if the `label_value` has a `}` inside of it:\n\n```python\n\u003e\u003e\u003e from swift.common.prom_metrics import parse_metrics\n\u003e\u003e\u003e from swift.common.prom_metrics import _build_line_parts\n\u003e\u003e\u003e value \u003d \u0027swift_metric{test\u003d\"brace } value\"} 1\u0027\n\u003e\u003e\u003e line \u003d _build_line_parts(\"swift_metric\", 1, {\"test\": \"brace } value\"})\n\u003e\u003e\u003e print(line)\nswift_metric{test\u003d\"brace } value\"} 1\n\n\u003e\u003e\u003e parse_metrics(line)\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\n  File \"/home/nmartes/dev/dev-vsaio/swift/swift/common/prom_metrics.py\", line 99, in parse_metrics\n    raise ValueError(\nValueError: Invalid Prometheus format: \u0027swift_metric{test\u003d\"brace } value\"} 1\u0027 - expected format \u0027metric_name value\u0027 or \u0027metric_name{labels} value\u0027\n\n```\n\nWhich is valid in a `label_value`\nSee 997455: sq? handle \u0027}\u0027 in label_value | https://review.opendev.org/c/openstack/swift/+/997455","commit_id":"1cc6bd70b44f8332cb1565aeb5812bc2ba24de1f"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"8369f4a3d8c4c3cf3e1363bc08c3b67b97513a34","unresolved":false,"context_lines":[{"line_number":33,"context_line":"_METRIC_LINE_PATTERN \u003d re.compile("},{"line_number":34,"context_line":"    r\u0027^(?P\u003cname\u003e[^{\\s]+)(?:\\{(?P\u003clabels\u003e[^}]*)\\})?\\s+\u0027"},{"line_number":35,"context_line":"    r\u0027(?P\u003cvalue\u003e\\S+)$\u0027)"},{"line_number":36,"context_line":""},{"line_number":37,"context_line":""},{"line_number":38,"context_line":"def _escape_label_value(value):"},{"line_number":39,"context_line":"    \"\"\""}],"source_content_type":"text/x-python","patch_set":36,"id":"fbd042d1_7cd1dee9","line":36,"in_reply_to":"be8a347d_55b45558","updated":"2026-07-17 00:02:04.000000000","message":"Done","commit_id":"1cc6bd70b44f8332cb1565aeb5812bc2ba24de1f"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"7fc280942f8db115b9316a5d142e0d6564470a85","unresolved":true,"context_lines":[{"line_number":119,"context_line":"                raise ValueError(\"Label key cannot be empty\")"},{"line_number":120,"context_line":"            if not _LABEL_KEY_PATTERN.match(k):"},{"line_number":121,"context_line":"                raise ValueError(f\"Invalid Prometheus label key: {k!r}\")"},{"line_number":122,"context_line":"            labels[k] \u003d v.strip().strip(\u0027\"\u0027)"},{"line_number":123,"context_line":""},{"line_number":124,"context_line":"    try:"},{"line_number":125,"context_line":"        value_str \u003d match.group(\u0027value\u0027)"}],"source_content_type":"text/x-python","patch_set":36,"id":"83c974e0_b971b597","line":122,"range":{"start_line":122,"start_character":0,"end_line":122,"end_character":44},"updated":"2026-07-15 21:05:00.000000000","message":"There also seems to be an edge case here where the labels that are returned from `parse_metrics()` are escaped and fed into `_build_line_parts()` they become Prometheus valid. BUT if we take those Prometheus valid labels and feed them back into `parse_metrics()`, then we SHOULD take the strings that we escaped and un-escape them, like Prometheus does for reading in metric lines.\n\n```python\npython3 -c \u0027\nfrom swift.common.prom_metrics import _build_line_parts, parse_metrics\n\nlabels \u003d {\"reason\": \"read \\\"EOF\\\"\\nretry\"}\nline \u003d _build_line_parts(\"swift_metric\", 1, labels)\nparsed \u003d parse_metrics(line)\n\nprint(\"line:\", repr(line))\nprint(\"parsed:\", repr(parsed))\n\nassert parsed[1] \u003d\u003d labels\n\u0027\n\nline: \u0027swift_metric{reason\u003d\"read \\\\\"EOF\\\\\"\\\\nretry\"} 1\\n\u0027 \n# This is Prometheus valid!\n\nparsed: (\u0027swift_metric\u0027, {\u0027reason\u0027: \u0027read \\\\\"EOF\\\\\"\\\\nretry\u0027}, 1.0) \n# But as a Prometheus line parser, \n# we should be able to un-escape those strings...\n\nTraceback (most recent call last):\n  File \"\u003cstring\u003e\", line 11, in \u003cmodule\u003e\nAssertionError\n```\n\nThis means then that this only applies to labels that are ALREADY Prometheus valid... not sure if we should handle this...\n\nSee 997456: sq? handle label values that have escaped strings | https://review.opendev.org/c/openstack/swift/+/997456","commit_id":"1cc6bd70b44f8332cb1565aeb5812bc2ba24de1f"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"13432a06112212641289dce42c48bb2b2fc34abe","unresolved":false,"context_lines":[{"line_number":119,"context_line":"                raise ValueError(\"Label key cannot be empty\")"},{"line_number":120,"context_line":"            if not _LABEL_KEY_PATTERN.match(k):"},{"line_number":121,"context_line":"                raise ValueError(f\"Invalid Prometheus label key: {k!r}\")"},{"line_number":122,"context_line":"            labels[k] \u003d v.strip().strip(\u0027\"\u0027)"},{"line_number":123,"context_line":""},{"line_number":124,"context_line":"    try:"},{"line_number":125,"context_line":"        value_str \u003d match.group(\u0027value\u0027)"}],"source_content_type":"text/x-python","patch_set":36,"id":"b23f85b9_b130bfd3","line":122,"range":{"start_line":122,"start_character":0,"end_line":122,"end_character":44},"in_reply_to":"83c974e0_b971b597","updated":"2026-07-17 01:23:39.000000000","message":"Done","commit_id":"1cc6bd70b44f8332cb1565aeb5812bc2ba24de1f"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"8369f4a3d8c4c3cf3e1363bc08c3b67b97513a34","unresolved":true,"context_lines":[{"line_number":13,"context_line":"# See the License for the specific language governing permissions and"},{"line_number":14,"context_line":"# limitations under the License."},{"line_number":15,"context_line":""},{"line_number":16,"context_line":"\"\"\" Prometheous Metrics \"\"\""},{"line_number":17,"context_line":""},{"line_number":18,"context_line":"import os"},{"line_number":19,"context_line":"import re"}],"source_content_type":"text/x-python","patch_set":38,"id":"75c1c1ec_d35ce4b3","line":16,"range":{"start_line":16,"start_character":4,"end_line":16,"end_character":15},"updated":"2026-07-17 00:02:04.000000000","message":"nit: s/Prometheous/Prometheus/","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"31bad5e4588a11227b7590159bbce9c62531bb6b","unresolved":false,"context_lines":[{"line_number":13,"context_line":"# See the License for the specific language governing permissions and"},{"line_number":14,"context_line":"# limitations under the License."},{"line_number":15,"context_line":""},{"line_number":16,"context_line":"\"\"\" Prometheous Metrics \"\"\""},{"line_number":17,"context_line":""},{"line_number":18,"context_line":"import os"},{"line_number":19,"context_line":"import re"}],"source_content_type":"text/x-python","patch_set":38,"id":"e7519fb1_402c3b75","line":16,"range":{"start_line":16,"start_character":4,"end_line":16,"end_character":15},"in_reply_to":"75c1c1ec_d35ce4b3","updated":"2026-07-20 19:32:19.000000000","message":"Acknowledged","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"8369f4a3d8c4c3cf3e1363bc08c3b67b97513a34","unresolved":true,"context_lines":[{"line_number":27,"context_line":"PROM_CONF_USER_LABEL_PREFIX \u003d \u0027prom_user_label_\u0027"},{"line_number":28,"context_line":"PROM_USER_LABEL_NAMESPACE \u003d \u0027user_\u0027"},{"line_number":29,"context_line":"USER_LABEL_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_]\")"},{"line_number":30,"context_line":"USER_VALUE_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_.\\-]\")"},{"line_number":31,"context_line":"_METRIC_NAME_PATTERN \u003d re.compile(r\u0027^[a-zA-Z_:][a-zA-Z0-9_:]*$\u0027)"},{"line_number":32,"context_line":"_LABEL_KEY_PATTERN \u003d re.compile(r\u0027^[a-zA-Z_][a-zA-Z0-9_]*$\u0027)"},{"line_number":33,"context_line":"_METRIC_LINE_PATTERN \u003d re.compile("}],"source_content_type":"text/x-python","patch_set":38,"id":"5e266d4e_379d5b98","line":30,"range":{"start_line":30,"start_character":0,"end_line":30,"end_character":18},"updated":"2026-07-17 00:02:04.000000000","message":"This does not allow for value prometheus `label_value`\u0027s:\n\n```python\n\u003e\u003e\u003e from swift.common.prom_metrics import get_prometheus_client\n\u003e\u003e\u003e get_prometheus_client({\"prom_user_label_endpoint\": \"/api\"})\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\n  File \"/home/nmartes/dev/test-vsaio-pool/instances/vsaio-1/swift/swift/common/prom_metrics.py\", line 226, in get_prometheus_client\n    raise ValueError(\nValueError: invalid character in configuration \u0027prom_user_label_endpoint\u0027 value \u0027/api\u0027: \u0027/\u0027\n\u003e\u003e\u003e client \u003d get_prometheus_client({})\n\u003e\u003e\u003e client.record(\"requests_total\", 1, {\"endpoint\": \"/api\"})\n(\u0027requests_total\u0027, frozenset({(\u0027endpoint\u0027, \u0027/api\u0027)}))\n\u003e\u003e\u003e print(client.get_all_stats(), end\u003d\"\")\nrequests_total{endpoint\u003d\"/api\"} 1\n\n```","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"31bad5e4588a11227b7590159bbce9c62531bb6b","unresolved":false,"context_lines":[{"line_number":27,"context_line":"PROM_CONF_USER_LABEL_PREFIX \u003d \u0027prom_user_label_\u0027"},{"line_number":28,"context_line":"PROM_USER_LABEL_NAMESPACE \u003d \u0027user_\u0027"},{"line_number":29,"context_line":"USER_LABEL_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_]\")"},{"line_number":30,"context_line":"USER_VALUE_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_.\\-]\")"},{"line_number":31,"context_line":"_METRIC_NAME_PATTERN \u003d re.compile(r\u0027^[a-zA-Z_:][a-zA-Z0-9_:]*$\u0027)"},{"line_number":32,"context_line":"_LABEL_KEY_PATTERN \u003d re.compile(r\u0027^[a-zA-Z_][a-zA-Z0-9_]*$\u0027)"},{"line_number":33,"context_line":"_METRIC_LINE_PATTERN \u003d re.compile("}],"source_content_type":"text/x-python","patch_set":38,"id":"e3f12016_d9242471","line":30,"range":{"start_line":30,"start_character":0,"end_line":30,"end_character":18},"in_reply_to":"5e266d4e_379d5b98","updated":"2026-07-20 19:32:19.000000000","message":"Acknowledged","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"8369f4a3d8c4c3cf3e1363bc08c3b67b97513a34","unresolved":true,"context_lines":[{"line_number":100,"context_line":"    if in_quotes:"},{"line_number":101,"context_line":"        raise ValueError(\"Unterminated label value quote\")"},{"line_number":102,"context_line":""},{"line_number":103,"context_line":"    pairs.append(labels_str[start:])"},{"line_number":104,"context_line":"    return pairs"},{"line_number":105,"context_line":""},{"line_number":106,"context_line":""}],"source_content_type":"text/x-python","patch_set":38,"id":"b62d8d96_7f93e3f1","line":103,"range":{"start_line":103,"start_character":0,"end_line":103,"end_character":36},"updated":"2026-07-17 00:02:04.000000000","message":"I think I forgot to handle trailing commas...\nhttps://prometheus.io/docs/instrumenting/exposition_formats/\n\n\u003e `label_pairs \u003d label_pair { \",\" label_pair } [ \",\" ]`","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"31bad5e4588a11227b7590159bbce9c62531bb6b","unresolved":false,"context_lines":[{"line_number":100,"context_line":"    if in_quotes:"},{"line_number":101,"context_line":"        raise ValueError(\"Unterminated label value quote\")"},{"line_number":102,"context_line":""},{"line_number":103,"context_line":"    pairs.append(labels_str[start:])"},{"line_number":104,"context_line":"    return pairs"},{"line_number":105,"context_line":""},{"line_number":106,"context_line":""}],"source_content_type":"text/x-python","patch_set":38,"id":"168f1429_9fb16b02","line":103,"range":{"start_line":103,"start_character":0,"end_line":103,"end_character":36},"in_reply_to":"b62d8d96_7f93e3f1","updated":"2026-07-20 19:32:19.000000000","message":"Done","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"8369f4a3d8c4c3cf3e1363bc08c3b67b97513a34","unresolved":true,"context_lines":[{"line_number":148,"context_line":"            v \u003d v.strip()"},{"line_number":149,"context_line":"            if v.startswith(\u0027\"\u0027) and v.endswith(\u0027\"\u0027):"},{"line_number":150,"context_line":"                v \u003d _unescape_label_value(v[1:-1])"},{"line_number":151,"context_line":"            labels[k] \u003d v"},{"line_number":152,"context_line":""},{"line_number":153,"context_line":"    try:"},{"line_number":154,"context_line":"        value_str \u003d match.group(\u0027value\u0027)"}],"source_content_type":"text/x-python","patch_set":38,"id":"8377e3ab_c87aceb2","line":151,"range":{"start_line":151,"start_character":0,"end_line":151,"end_character":25},"updated":"2026-07-17 00:02:04.000000000","message":"This doesn\u0027t handle if the `label_value` is not in quotes:\n\n```python\n\u003e\u003e\u003e from swift.common.prom_metrics import parse_metrics\n\u003e\u003e\u003e parse_metrics(\u0027metric{label\u003dvalue} 1\u0027) # value should be \"value\"\n(\u0027metric\u0027, {\u0027label\u0027: \u0027value\u0027}, 1.0)\n```\n\nAnd it looks like this edge case fails too:\n```python\n\u003e\u003e\u003e parse_metrics(\u0027metric{a\u003d\"x\"}{b\u003d\"y\"} 1\u0027)\n(\u0027metric\u0027, {\u0027a\u0027: \u0027x\"}{b\u003d\"y\u0027}, 1.0) # two sets of braces\n```","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"31bad5e4588a11227b7590159bbce9c62531bb6b","unresolved":false,"context_lines":[{"line_number":148,"context_line":"            v \u003d v.strip()"},{"line_number":149,"context_line":"            if v.startswith(\u0027\"\u0027) and v.endswith(\u0027\"\u0027):"},{"line_number":150,"context_line":"                v \u003d _unescape_label_value(v[1:-1])"},{"line_number":151,"context_line":"            labels[k] \u003d v"},{"line_number":152,"context_line":""},{"line_number":153,"context_line":"    try:"},{"line_number":154,"context_line":"        value_str \u003d match.group(\u0027value\u0027)"}],"source_content_type":"text/x-python","patch_set":38,"id":"475faa4e_86e7c7c7","line":151,"range":{"start_line":151,"start_character":0,"end_line":151,"end_character":25},"in_reply_to":"8377e3ab_c87aceb2","updated":"2026-07-20 19:32:19.000000000","message":"Done","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"8369f4a3d8c4c3cf3e1363bc08c3b67b97513a34","unresolved":true,"context_lines":[{"line_number":274,"context_line":"                    pass"},{"line_number":275,"context_line":""},{"line_number":276,"context_line":"    def record(self, name, value, labels\u003dNone):"},{"line_number":277,"context_line":"        all_labels \u003d dict(**self.user_labels, **(labels or {}))"},{"line_number":278,"context_line":"        if labels is not None:"},{"line_number":279,"context_line":"            all_labels.update(labels)"},{"line_number":280,"context_line":"        key \u003d metric_key(name, **all_labels)"}],"source_content_type":"text/x-python","patch_set":38,"id":"268321c7_e0de0c19","line":277,"range":{"start_line":277,"start_character":0,"end_line":277,"end_character":63},"updated":"2026-07-17 00:02:04.000000000","message":"If we have a conflict in the user labels and the other labels, then things might not be ideal\n\n```shell\npython3 -c \u0027\nuser_labels \u003d {\"user_hostname\": \"default-host\"}\nlabels \u003d {\"user_hostname\": \"caller-host\"}\n\nprint(dict(**user_labels, **labels))\n\u0027\nTraceback (most recent call last):\n  File \"\u003cstring\u003e\", line 5, in \u003cmodule\u003e\nTypeError: dict() got multiple values for keyword argument \u0027user_hostname\u0027\n\n```","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"31bad5e4588a11227b7590159bbce9c62531bb6b","unresolved":false,"context_lines":[{"line_number":274,"context_line":"                    pass"},{"line_number":275,"context_line":""},{"line_number":276,"context_line":"    def record(self, name, value, labels\u003dNone):"},{"line_number":277,"context_line":"        all_labels \u003d dict(**self.user_labels, **(labels or {}))"},{"line_number":278,"context_line":"        if labels is not None:"},{"line_number":279,"context_line":"            all_labels.update(labels)"},{"line_number":280,"context_line":"        key \u003d metric_key(name, **all_labels)"}],"source_content_type":"text/x-python","patch_set":38,"id":"b0f0715d_b1051dfc","line":277,"range":{"start_line":277,"start_character":0,"end_line":277,"end_character":63},"in_reply_to":"268321c7_e0de0c19","updated":"2026-07-20 19:32:19.000000000","message":"Done","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"8369f4a3d8c4c3cf3e1363bc08c3b67b97513a34","unresolved":true,"context_lines":[{"line_number":277,"context_line":"        all_labels \u003d dict(**self.user_labels, **(labels or {}))"},{"line_number":278,"context_line":"        if labels is not None:"},{"line_number":279,"context_line":"            all_labels.update(labels)"},{"line_number":280,"context_line":"        key \u003d metric_key(name, **all_labels)"},{"line_number":281,"context_line":"        self._update_stats(key, value)"},{"line_number":282,"context_line":"        return key"},{"line_number":283,"context_line":""}],"source_content_type":"text/x-python","patch_set":38,"id":"09f2199d_0e52cf12","line":280,"range":{"start_line":280,"start_character":14,"end_line":280,"end_character":24},"updated":"2026-07-17 00:02:04.000000000","message":"So the string `name` cannot be used as a `label_key`, since the expansion could lead to:\n```\nmetric_key(name, name\u003d\"label_value\")\n```\n\nThis is funny to find, and I actually think of all the label keys that could be used, this is a reasonable one that someone might pick:\n\n```python\n\u003e\u003e\u003e from swift.common.prom_metrics import get_prometheus_client\n\u003e\u003e\u003e client \u003d get_prometheus_client(None)\n\u003e\u003e\u003e client.record(\u0027swift_metric\u0027, 1, {\u0027name\u0027: \u0027hello errors!\u0027})\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\n  File \"/home/nmartes/dev/test-vsaio-pool/instances/vsaio-1/swift/swift/common/prom_metrics.py\", line 280, in record\n    key \u003d metric_key(name, **all_labels)\n          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nTypeError: metric_key() got multiple values for argument \u0027name\u0027\n\u003e\u003e\u003e client.record(\u0027swift_metric\u0027, 1, {\u0027not_name\u0027: \u0027hello errors!\u0027})\n(\u0027swift_metric\u0027, frozenset({(\u0027not_name\u0027, \u0027hello errors!\u0027)}))\n```\n\nI\u0027d say we cannot use optional arguments then for `metric_key` because then if we change the needed argument of `name` to something like `foo` and then we want to pass in a `label_key` like `foo` we would run into the same issue.\n\nIt looks like in Python 3.8, there is a fix for this https://peps.python.org/pep-0570/ , but we can just switch it to\n```\ndef metric_key(name, labels)\n```\n\ninstead.","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"31bad5e4588a11227b7590159bbce9c62531bb6b","unresolved":false,"context_lines":[{"line_number":277,"context_line":"        all_labels \u003d dict(**self.user_labels, **(labels or {}))"},{"line_number":278,"context_line":"        if labels is not None:"},{"line_number":279,"context_line":"            all_labels.update(labels)"},{"line_number":280,"context_line":"        key \u003d metric_key(name, **all_labels)"},{"line_number":281,"context_line":"        self._update_stats(key, value)"},{"line_number":282,"context_line":"        return key"},{"line_number":283,"context_line":""}],"source_content_type":"text/x-python","patch_set":38,"id":"d99c5c46_bbe75654","line":280,"range":{"start_line":280,"start_character":14,"end_line":280,"end_character":24},"in_reply_to":"09f2199d_0e52cf12","updated":"2026-07-20 19:32:19.000000000","message":"Looks good!","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":39146,"name":"Nathaniel Martes","display_name":"Nate Martes","email":"nmartes@NVIDIA.com","username":"nmartes"},"change_message_id":"8369f4a3d8c4c3cf3e1363bc08c3b67b97513a34","unresolved":true,"context_lines":[{"line_number":285,"context_line":"        all_labels \u003d dict(**self.user_labels, **(labels or {}))"},{"line_number":286,"context_line":"        if labels is not None:"},{"line_number":287,"context_line":"            all_labels.update(labels)"},{"line_number":288,"context_line":"        key \u003d metric_key(name, **all_labels)"},{"line_number":289,"context_line":"        new_value \u003d self._stats.get(key, 0) + value"},{"line_number":290,"context_line":"        self._update_stats(key, new_value)"},{"line_number":291,"context_line":"        return key"}],"source_content_type":"text/x-python","patch_set":38,"id":"387ed569_8ccefe42","line":288,"range":{"start_line":288,"start_character":14,"end_line":288,"end_character":24},"updated":"2026-07-17 00:02:04.000000000","message":"Same thing with string `name` as a label_key","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"31bad5e4588a11227b7590159bbce9c62531bb6b","unresolved":false,"context_lines":[{"line_number":285,"context_line":"        all_labels \u003d dict(**self.user_labels, **(labels or {}))"},{"line_number":286,"context_line":"        if labels is not None:"},{"line_number":287,"context_line":"            all_labels.update(labels)"},{"line_number":288,"context_line":"        key \u003d metric_key(name, **all_labels)"},{"line_number":289,"context_line":"        new_value \u003d self._stats.get(key, 0) + value"},{"line_number":290,"context_line":"        self._update_stats(key, new_value)"},{"line_number":291,"context_line":"        return key"}],"source_content_type":"text/x-python","patch_set":38,"id":"590eaa0b_7b6cd0f7","line":288,"range":{"start_line":288,"start_character":14,"end_line":288,"end_character":24},"in_reply_to":"387ed569_8ccefe42","updated":"2026-07-20 19:32:19.000000000","message":"Done","commit_id":"f66f5386889e6327fec14c783d006b255fbdc2aa"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":27,"context_line":"PROM_CONF_USER_LABEL_PREFIX \u003d \u0027prom_user_label_\u0027"},{"line_number":28,"context_line":"PROM_USER_LABEL_NAMESPACE \u003d \u0027user_\u0027"},{"line_number":29,"context_line":"USER_LABEL_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_]\")"},{"line_number":30,"context_line":"_METRIC_NAME_PATTERN \u003d re.compile(r\u0027^[a-zA-Z_:][a-zA-Z0-9_:]*$\u0027)"},{"line_number":31,"context_line":"_LABEL_KEY_PATTERN \u003d re.compile(r\u0027^[a-zA-Z_][a-zA-Z0-9_]*$\u0027)"},{"line_number":32,"context_line":"_METRIC_LINE_PATTERN \u003d re.compile("},{"line_number":33,"context_line":"    r\u0027^(?P\u003cname\u003e[^{\\s]+)(?:\\{(?P\u003clabels\u003e.*)\\})?\\s+\u0027"}],"source_content_type":"text/x-python","patch_set":40,"id":"7610ad0d_abb42a03","line":30,"updated":"2026-07-21 02:05:10.000000000","message":"\u003e In Prometheus tradition, colons are reserved for recording rules / generated aggregate series,\n\u003e style-wise for producer-side metrics: usually discouraged\n\u003e you could reasonably tighten the validation to exclude :\n\nit\u0027s always easier to start with a tighter scope and only allow `:` in emitted metrics as needed; I know I\u0027ve seen SRE cook up some metrics like `cluster:primary_parts` based on some scripts we have that drop prom-style metrics - but it sounds like that\u0027s valid but conveys a special meaning we don\u0027t really have a good reason to trample on.  e.g. I\u0027m super skeptical about a metric name that STARTS with a `:`","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":28,"context_line":"PROM_USER_LABEL_NAMESPACE \u003d \u0027user_\u0027"},{"line_number":29,"context_line":"USER_LABEL_PATTERN \u003d re.compile(r\"[^0-9a-zA-Z_]\")"},{"line_number":30,"context_line":"_METRIC_NAME_PATTERN \u003d re.compile(r\u0027^[a-zA-Z_:][a-zA-Z0-9_:]*$\u0027)"},{"line_number":31,"context_line":"_LABEL_KEY_PATTERN \u003d re.compile(r\u0027^[a-zA-Z_][a-zA-Z0-9_]*$\u0027)"},{"line_number":32,"context_line":"_METRIC_LINE_PATTERN \u003d re.compile("},{"line_number":33,"context_line":"    r\u0027^(?P\u003cname\u003e[^{\\s]+)(?:\\{(?P\u003clabels\u003e.*)\\})?\\s+\u0027"},{"line_number":34,"context_line":"    r\u0027(?P\u003cvalue\u003e\\S+)$\u0027)"}],"source_content_type":"text/x-python","patch_set":40,"id":"7224aef4_9d7f5611","line":31,"updated":"2026-07-21 02:05:10.000000000","message":"oic, at least one non-numeric value followed by 0 or more other `string.ascii_letters + string.digits`","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":40,"context_line":"    double-quote and line feed become ``\\\\\\\\``, ``\\\\\"`` and ``\\\\n``."},{"line_number":41,"context_line":"    \"\"\""},{"line_number":42,"context_line":"    return str(value).replace(\u0027\\\\\u0027, \u0027\\\\\\\\\u0027).replace(\u0027\"\u0027, \u0027\\\\\"\u0027).replace("},{"line_number":43,"context_line":"        \u0027\\n\u0027, \u0027\\\\n\u0027)"},{"line_number":44,"context_line":""},{"line_number":45,"context_line":""},{"line_number":46,"context_line":"def _unescape_label_value(value):"}],"source_content_type":"text/x-python","patch_set":40,"id":"520b03f3_000219f2","line":43,"updated":"2026-07-21 02:05:10.000000000","message":"I think some \"raw-strings\" could help here:\n\n```\n\u003e\u003e\u003e r\u0027\\\\\u0027 \u003d\u003d \u0027\\\\\\\\\u0027\nTrue\n\u003e\u003e\u003e r\u0027\\\"\u0027 \u003d\u003d \u0027\\\\\"\u0027\nTrue\n\u003e\u003e\u003e r\u0027\\n\u0027 \u003d\u003d \u0027\\\\n\u0027\nTrue\n```\n\nthe only annoying one is that stupid `\u0027\\\u0027`","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":56,"context_line":"            elif char in (\u0027\\\\\u0027, \u0027\"\u0027):"},{"line_number":57,"context_line":"                chars.append(char)"},{"line_number":58,"context_line":"            else:"},{"line_number":59,"context_line":"                chars.append(\u0027\\\\\u0027)"},{"line_number":60,"context_line":"                chars.append(char)"},{"line_number":61,"context_line":"            escaped \u003d False"},{"line_number":62,"context_line":"            continue"}],"source_content_type":"text/x-python","patch_set":40,"id":"767d6617_29d21cad","line":59,"updated":"2026-07-21 02:05:10.000000000","message":"ok, it sounds like maybe these three values *explicitly* MUST be supported to escape:\n\nhttps://prometheus.io/docs/instrumenting/exposition_formats/#comments-help-text-and-type-information\n\n\u003e it is looser than Prometheus, because unknown escapes are preserved instead of rejected\n\n```\n• That’s a sensible read.\n\n  1. Yes, the current code is not spec-tight. It treats unknown escapes as literal backslash-plus-char instead of rejecting them, so it is more permissive than Prometheus.\n  2. Also yes, “reach for a standard parser” is usually the right instinct. The problem here is just that Prometheus label quoting is a tiny custom grammar that does not line up exactly with a clean stdlib primitive.\n\n  Given that, I think the most obvious implementation is probably not the current one. I’d prefer something that makes the rule explicit:\n\n  _ALLOWED_ESCAPES \u003d {\n      \u0027\\\\\u0027: \u0027\\\\\u0027,\n      \u0027\"\u0027: \u0027\"\u0027,\n      \u0027n\u0027: \u0027\\n\u0027,\n  }\n\n  then decode by scanning for backslashes and:\n\n  - translate those three\n  - raise on anything else\n\n  That is still custom code, but it is no longer “inventing a serializer in an ad hoc way”; it is directly encoding the Prometheus contract in a small table.\n\n  So I’d frame it like this:\n\n  - using stdlib would be nicer if there were a true fit\n  - there really isn’t\n  - once you know the allowed set is only three escapes, a tiny explicit table-driven decoder is probably the clearest and most maintainable option\n\n  That is also easier to justify in review than “sort of JSON, except not really”.\n```\n\nI *do* actually prefer the regex one:\n\n```\n• There is an obvious way. It just is not obviously better.\n\n  This is the straightforward re.sub version:\n\n  import re\n\n  _ALLOWED_ESCAPES \u003d {\n      \u0027\\\\\u0027: \u0027\\\\\u0027,\n      \u0027\"\u0027: \u0027\"\u0027,\n      \u0027n\u0027: \u0027\\n\u0027,\n  }\n  _ESCAPE_RE \u003d re.compile(r\u0027\\\\(.)\u0027)\n\n  def decode_escaped_string(s):\n      if s.endswith(\u0027\\\\\u0027):\n          raise ValueError(\u0027trailing backslash\u0027)\n\n      def repl(match):\n          esc \u003d match.group(1)\n          try:\n              return _ALLOWED_ESCAPES[esc]\n          except KeyError:\n              raise ValueError(f\u0027invalid escape: \\\\{esc}\u0027)\n\n      return _ESCAPE_RE.sub(repl, s)\n\n  Why people still reach for the manual loop:\n\n  - the loop maps more directly to the grammar\n  - trailing-backslash handling is part of the same control flow\n  - error behavior is easier to reason about without regex mechanics\n  - re.sub(..., repl, s) feels like a text-transformation tool, while this is really a tiny parser\n\n  So it is not that re.sub cannot do it. It can. It is just not clearly simpler than:\n\n  def decode_escaped_string(s):\n      out \u003d []\n      i \u003d 0\n      while i \u003c len(s):\n          if s[i] !\u003d \u0027\\\\\u0027:\n              out.append(s[i])\n              i +\u003d 1\n              continue\n          i +\u003d 1\n          if i \u003e\u003d len(s):\n              raise ValueError(\u0027trailing backslash\u0027)\n          if s[i] \u003d\u003d \u0027\\\\\u0027:\n              out.append(\u0027\\\\\u0027)\n          elif s[i] \u003d\u003d \u0027\"\u0027:\n              out.append(\u0027\"\u0027)\n          elif s[i] \u003d\u003d \u0027n\u0027:\n              out.append(\u0027\\n\u0027)\n          else:\n              raise ValueError(f\u0027invalid escape: \\\\{s[i]}\u0027)\n          i +\u003d 1\n      return \u0027\u0027.join(out)\n\n  If you prefer the re.sub version, I think that is defensible. I just would not claim it is more obviously correct than the loop.\n```\n\n... another thing that bugs me is how hard it is to write a literal-back-slash in python!?\n\n```\n\u003e\u003e\u003e print(r\u0027\\\\n\u0027)\n\\\\n\n\u003e\u003e\u003e print(r\u0027\\\\\u0027)\n\\\\\n\u003e\u003e\u003e print(r\u0027\\\u0027)\n  File \"\u003cstdin\u003e\", line 1\n    print(r\u0027\\\u0027)\n          ^\nSyntaxError: unterminated string literal (detected at line 1)\n\u003e\u003e\u003e print(r\u0027\\ \u0027)\n\\ \n\u003e\u003e\u003e r\u0027\\ \u0027.rstrip()\n\u0027\\\\\u0027\n```\n\nI feel like at this point I\u0027m almost read to say:\n\n```\n\u003e\u003e\u003e [c for c in string.punctuation[20:25]]\n[\u0027?\u0027, \u0027@\u0027, \u0027[\u0027, \u0027\\\\\u0027, \u0027]\u0027]\n\nLITERAL_BACKSLASH \u003d string.punctuation[23]\n```","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":65,"context_line":"        else:"},{"line_number":66,"context_line":"            chars.append(char)"},{"line_number":67,"context_line":"    if escaped:"},{"line_number":68,"context_line":"        chars.append(\u0027\\\\\u0027)"},{"line_number":69,"context_line":"    return \u0027\u0027.join(chars)"},{"line_number":70,"context_line":""},{"line_number":71,"context_line":""}],"source_content_type":"text/x-python","patch_set":40,"id":"b2e465b0_fe8c3b6e","line":68,"updated":"2026-07-21 02:05:10.000000000","message":"is anyone actually using all this escaping stuff?  Do we want to add tests to support the test helper or can we just YAGNI and make it simpler?\n\n```\n3. `swift/common/prom_metrics.py:53`\n   The patch adds parser-side normalization and tolerant parsing branches in\n   `_unescape_label_value()`, `_split_label_pairs()`, `_parse_label_value()`,\n   and `parse_metrics()` even though nothing in-tree imports that reader path\n   outside the new unit test file. The `agent-defensive-contract` worker\n   proved one of those branches is still untested by mutating the trailing\n   backslash path at lines `67-68`; the full unit file still passed under that\n   sentinel in the private worktree. This matters because it grows a future\n   wire-format contract without a named production caller or evidence that the\n   extra parser tolerance is needed. The smallest acceptable fix is to drop\n   the parser/readback half from this patch and keep the writer/client surface,\n   or land the real reader in the same change with focused tests proving each\n   parser branch against that caller contract.\n```","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":175,"context_line":"            if not _LABEL_KEY_PATTERN.match(k):"},{"line_number":176,"context_line":"                raise ValueError(f\"Invalid Prometheus label key: {k!r}\")"},{"line_number":177,"context_line":"            if k in labels:"},{"line_number":178,"context_line":"                raise ValueError(f\"Duplicate Prometheus label key: {k!r}\")"},{"line_number":179,"context_line":"            labels[k] \u003d _parse_label_value(k, v)"},{"line_number":180,"context_line":""},{"line_number":181,"context_line":"    try:"}],"source_content_type":"text/x-python","patch_set":40,"id":"ef63b01d_47fd756f","line":178,"updated":"2026-07-21 02:05:10.000000000","message":"seems reasonable","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":38368,"name":"Christian Ohanaja","display_name":"Christian Ohanaja","email":"cohanaja@nvidia.com","username":"cohanaja"},"change_message_id":"42c95aa0519324ba4571a713317cf3958cf7297d","unresolved":true,"context_lines":[{"line_number":200,"context_line":"    :returns: Prometheus-formatted metric string"},{"line_number":201,"context_line":"    \"\"\""},{"line_number":202,"context_line":""},{"line_number":203,"context_line":"    if not isinstance(name, str):"},{"line_number":204,"context_line":"        raise TypeError(f\"Metric name must be string, got {type(name)}\")"},{"line_number":205,"context_line":"    if not _METRIC_NAME_PATTERN.match(name):"},{"line_number":206,"context_line":"        raise ValueError(f\"Invalid Prometheus metric name: {name!r}\")"},{"line_number":207,"context_line":""}],"source_content_type":"text/x-python","patch_set":40,"id":"9a6bf54b_7aa0a765","line":204,"range":{"start_line":203,"start_character":0,"end_line":204,"end_character":72},"updated":"2026-07-20 23:32:03.000000000","message":"I see that we don\u0027t really validate that values are what we expect them to be like we do labels; I think something like\n\n```\nif not isinstance(value, (int, float)):\n        raise TypeError(f\"Metric value must be a number, got {type(value)}\")\n```\n\nhere as well wouldn\u0027t be too out of place.","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":244,"context_line":"            continue"},{"line_number":245,"context_line":"        conf_label \u003d k[len(PROM_CONF_USER_LABEL_PREFIX):]"},{"line_number":246,"context_line":"        result \u003d USER_LABEL_PATTERN.search(conf_label)"},{"line_number":247,"context_line":"        if result is not None:"},{"line_number":248,"context_line":"            raise ValueError("},{"line_number":249,"context_line":"                f\"invalid character in prom user label \""},{"line_number":250,"context_line":"                f\"configuration {k!r}: {result.group(0)!r}\")"}],"source_content_type":"text/x-python","patch_set":40,"id":"9795f800_5abe80a8","line":247,"updated":"2026-07-21 02:05:10.000000000","message":"super weird to have `USER_LABEL_PATTERN` defined the characters you CAN\u0027T use\n\nmaybe `INVALID_USER_LABEL_CHAR_RE` would be better - or find a way to have the constant express the valid chars?\n\n```\n  if not USER_LABEL_PATTERN.fullmatch(conf_label):\n      raise ValueError(...)\n```\n\n... depending on how important you think it is to say \"invalid char \u0027X\u0027 at pos Y\" - I don\u0027t think it\u0027s that big of deal as long as the error says:\n\n\u003e user label must be ascii letter, digit or underscore","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":271,"context_line":"            for key in labels:"},{"line_number":272,"context_line":"                if key in all_labels:"},{"line_number":273,"context_line":"                    raise ValueError("},{"line_number":274,"context_line":"                        \"Duplicate Prometheus label key: %r\" % key)"},{"line_number":275,"context_line":"            all_labels.update(labels)"},{"line_number":276,"context_line":"        return metric_key(name, all_labels)"},{"line_number":277,"context_line":""}],"source_content_type":"text/x-python","patch_set":40,"id":"cc45880d_3b279e3d","line":274,"updated":"2026-07-21 02:05:10.000000000","message":"I don\u0027t really understand this - it reads like we want to explode when a caller passes in a user-label that just so happens to conflict with a configured label\n\nHonestly I don\u0027t think it\u0027s worith it - `all_labels.update(labels or {})` FTW\n\n... but if you want to keep it the more obvious contract should be \"a caller can not pass in a label from the user label reserved namespace\" - as it\u0027s written it\u0027s confusing my review agents:\n\n```\n1. `swift/common/prom_metrics.py:228`\n   The `get_prometheus_client()` docstring says deployment-defined user labels\n   are \"automatically added to all metrics as `user_*` labels\", but actual\n   writes go through `_make_key()` at `swift/common/prom_metrics.py:268-276`,\n   which raises `ValueError` on caller-supplied collisions with those\n   generated `user_*` labels. The new tests at\n   `test/unit/common/test_prom_metrics.py:500-539` lock in that failure path.\n   This matters because a maintainer or follow-on integration can reasonably\n   read the current prose as a transparent stamp-everything contract and then\n   hit a runtime rejection instead. The smallest acceptable fix is to qualify\n   the docstring so it describes the collision rule, or to change the runtime\n   behavior so the unconditional prose becomes true.\n```","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":296,"context_line":"                    mode\u003d\u0027w\u0027, dir\u003dos.path.dirname(filename),"},{"line_number":297,"context_line":"                    delete\u003dFalse) as tf:"},{"line_number":298,"context_line":"                tf.write(stats_data)"},{"line_number":299,"context_line":"            os.chmod(tf.name, 0o644)"},{"line_number":300,"context_line":"            os.rename(tf.name, filename)"},{"line_number":301,"context_line":"        finally:"},{"line_number":302,"context_line":"            if tf:"}],"source_content_type":"text/x-python","patch_set":40,"id":"deecb11c_47ddfa68","line":299,"updated":"2026-07-21 02:05:10.000000000","message":"my review agent is worried about this:\n\n```\n2. `swift/common/prom_metrics.py:295`\n   `write_all_stats()` always forces the destination file to mode `0644`\n   through `os.chmod(tf.name, 0o644)` before renaming it into place. That\n   gives a concrete failure path for operators who intentionally keep a\n   textfile-collector output at `0640` or tighter: every refresh silently\n   widens readability again. This matters here because the same helper is\n   explicitly designed to carry deployment-defined `prom_user_label_*` data,\n   and future integrations may add more site metadata. The smallest acceptable\n   fix is to preserve an existing target mode, or otherwise respect umask or a\n   configurable mode instead of hard-coding world-readable output.\n```\n\nI think maybe just pull the \"magic number\" up to a module level constant - then we can debate if it should be 0644 or if 0640 is resonable - or try to learn if we can do something sensible relying on umask?\n\n```\nPROM_METRICS_FILE_MODE \u003d 0o644\n```","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"}],"swift/common/utils/prom_metrics.py":[{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"8fb8665fbef2e8aa06ac54c1eaf90bc2dcbf1ca7","unresolved":true,"context_lines":[{"line_number":40,"context_line":"        \"\"\""},{"line_number":41,"context_line":"        return \u0027%s{hostname\u003d\"%s\", policy\u003d\"%s\"} %d\\n\u0027 % ("},{"line_number":42,"context_line":"            metricname,"},{"line_number":43,"context_line":"            socket.gethostname(),"},{"line_number":44,"context_line":"            policy,"},{"line_number":45,"context_line":"            value,"},{"line_number":46,"context_line":"        )"}],"source_content_type":"text/x-python","patch_set":1,"id":"a174e6d8_0b5a2185","line":43,"updated":"2025-12-17 15:54:43.000000000","message":"the instance should probably cache this, IIRC the way we handle hostname for labeled statsd metrics is using a \"user label\"","commit_id":"fd1d11deca9bbfce218bbbb0e06c6b2b68f9b938"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"d28f060d253fa45bd118c68808834739ea47a8a0","unresolved":false,"context_lines":[{"line_number":40,"context_line":"        \"\"\""},{"line_number":41,"context_line":"        return \u0027%s{hostname\u003d\"%s\", policy\u003d\"%s\"} %d\\n\u0027 % ("},{"line_number":42,"context_line":"            metricname,"},{"line_number":43,"context_line":"            socket.gethostname(),"},{"line_number":44,"context_line":"            policy,"},{"line_number":45,"context_line":"            value,"},{"line_number":46,"context_line":"        )"}],"source_content_type":"text/x-python","patch_set":1,"id":"e456d271_b708fa88","line":43,"in_reply_to":"a174e6d8_0b5a2185","updated":"2026-02-12 01:17:54.000000000","message":"Acknowledged","commit_id":"fd1d11deca9bbfce218bbbb0e06c6b2b68f9b938"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"8fb8665fbef2e8aa06ac54c1eaf90bc2dcbf1ca7","unresolved":true,"context_lines":[{"line_number":41,"context_line":"        return \u0027%s{hostname\u003d\"%s\", policy\u003d\"%s\"} %d\\n\u0027 % ("},{"line_number":42,"context_line":"            metricname,"},{"line_number":43,"context_line":"            socket.gethostname(),"},{"line_number":44,"context_line":"            policy,"},{"line_number":45,"context_line":"            value,"},{"line_number":46,"context_line":"        )"},{"line_number":47,"context_line":""}],"source_content_type":"text/x-python","patch_set":1,"id":"5faf3b5a_7b006772","line":44,"updated":"2025-12-17 15:54:43.000000000","message":"at this low level policy should be a generic label - same as method, or account or any other arbitrary label.","commit_id":"fd1d11deca9bbfce218bbbb0e06c6b2b68f9b938"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"b4dbb83a9c6164c1821f5c9a05aeb315f2b653ff","unresolved":false,"context_lines":[{"line_number":41,"context_line":"        return \u0027%s{hostname\u003d\"%s\", policy\u003d\"%s\"} %d\\n\u0027 % ("},{"line_number":42,"context_line":"            metricname,"},{"line_number":43,"context_line":"            socket.gethostname(),"},{"line_number":44,"context_line":"            policy,"},{"line_number":45,"context_line":"            value,"},{"line_number":46,"context_line":"        )"},{"line_number":47,"context_line":""}],"source_content_type":"text/x-python","patch_set":1,"id":"ba62561f_42f83383","line":44,"in_reply_to":"5faf3b5a_7b006772","updated":"2026-01-05 21:46:11.000000000","message":"Acknowledged","commit_id":"fd1d11deca9bbfce218bbbb0e06c6b2b68f9b938"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"8fb8665fbef2e8aa06ac54c1eaf90bc2dcbf1ca7","unresolved":true,"context_lines":[{"line_number":54,"context_line":"        logging.info(\"\u003d\" * 50)"},{"line_number":55,"context_line":"        metricname, policy, value \u003d stats.split(\u0027 \u0027)"},{"line_number":56,"context_line":"        self._gen_metrics(metricname, policy, value)"},{"line_number":57,"context_line":"        # write in file? show graphically?"}],"source_content_type":"text/x-python","patch_set":1,"id":"ebfd0294_50444f55","line":57,"updated":"2025-12-17 15:54:43.000000000","message":"yes, the main interface of the client should be accumulate stats/counters or track a guage set and then consumers will want to periodically \"output\" or \"dump_stats\" to an output file.","commit_id":"fd1d11deca9bbfce218bbbb0e06c6b2b68f9b938"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"b4dbb83a9c6164c1821f5c9a05aeb315f2b653ff","unresolved":false,"context_lines":[{"line_number":54,"context_line":"        logging.info(\"\u003d\" * 50)"},{"line_number":55,"context_line":"        metricname, policy, value \u003d stats.split(\u0027 \u0027)"},{"line_number":56,"context_line":"        self._gen_metrics(metricname, policy, value)"},{"line_number":57,"context_line":"        # write in file? show graphically?"}],"source_content_type":"text/x-python","patch_set":1,"id":"cfb3ebf8_829ec99f","line":57,"in_reply_to":"ebfd0294_50444f55","updated":"2026-01-05 21:46:11.000000000","message":"Done","commit_id":"fd1d11deca9bbfce218bbbb0e06c6b2b68f9b938"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1f89dae221ca224f992884d4d85d0f14cb377f9f","unresolved":true,"context_lines":[{"line_number":36,"context_line":"class AbstractPrometheousClient:"},{"line_number":37,"context_line":"    def __init__(self, env, metricname, value, conf\u003dNone, logger\u003dNone):"},{"line_number":38,"context_line":"        self.metricname \u003d metricname"},{"line_number":39,"context_line":"        self.value \u003d value"},{"line_number":40,"context_line":"        self.env \u003d env"},{"line_number":41,"context_line":"        self.conf \u003d conf"},{"line_number":42,"context_line":"        self.logger \u003d logger"}],"source_content_type":"text/x-python","patch_set":5,"id":"508b2aec_44852374","line":39,"updated":"2026-01-15 20:22:16.000000000","message":"metricname and value seem like weird state for the *client* object to hold.  You could maybe *try* to make a dataclass object to represent an individual metric+labels \"key\" but I might recommend trying to see how far you can get with a stdlib datastructure like `(metricname, frozenset)` as the key as I think any custom class will need to be basically a wrapper on that anyway.\n\nIf you look at a statsd client the metric name and value are provided as arguments to the update_stats method - while the client state holds information about e.g. \"where to send the udp packets\".\n\nIf you translate the design of the statsd-client faithfully to a prometheus-client - I think the client should be initialized with \"the path/file where it writes stats\" and have an update_stats method that takes metric name and value.\n\n```\ndef update_stats(metricname, value, lables\u003dNone):\n    labels \u003d labels or {}\n    key \u003d (metricname, frozenset(labels.items())\n    self._stats[key] \u003d value\n```","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"8ea433753461363516daad2f0a75b32496d00d18","unresolved":false,"context_lines":[{"line_number":36,"context_line":"class AbstractPrometheousClient:"},{"line_number":37,"context_line":"    def __init__(self, env, metricname, value, conf\u003dNone, logger\u003dNone):"},{"line_number":38,"context_line":"        self.metricname \u003d metricname"},{"line_number":39,"context_line":"        self.value \u003d value"},{"line_number":40,"context_line":"        self.env \u003d env"},{"line_number":41,"context_line":"        self.conf \u003d conf"},{"line_number":42,"context_line":"        self.logger \u003d logger"}],"source_content_type":"text/x-python","patch_set":5,"id":"79872286_eba1ec72","line":39,"in_reply_to":"508b2aec_44852374","updated":"2026-02-06 01:20:27.000000000","message":"Done","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1f89dae221ca224f992884d4d85d0f14cb377f9f","unresolved":true,"context_lines":[{"line_number":53,"context_line":"        \"\"\""},{"line_number":54,"context_line":"        return \u0027%s %d\\n\u0027 % (metricname, value)"},{"line_number":55,"context_line":""},{"line_number":56,"context_line":"    def display_stats(self, env, stats, args):"},{"line_number":57,"context_line":"        logging.info(\"\u003d\" * 50)"},{"line_number":58,"context_line":"        metricname, policy, value \u003d stats.split(\u0027 \u0027) #?"},{"line_number":59,"context_line":"        prom_metric \u003d self._gen_metrics(metricname, value)"}],"source_content_type":"text/x-python","patch_set":5,"id":"f9238714_ddf27246","line":56,"updated":"2026-01-15 20:22:16.000000000","message":"I don\u0027t understand this signature - maybe try to a write a doc string for your methods so it\u0027s more clear what we should think this method is supposed to do?","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ec30e831ea7cb2dda7eadbfa78029491992f0527","unresolved":false,"context_lines":[{"line_number":53,"context_line":"        \"\"\""},{"line_number":54,"context_line":"        return \u0027%s %d\\n\u0027 % (metricname, value)"},{"line_number":55,"context_line":""},{"line_number":56,"context_line":"    def display_stats(self, env, stats, args):"},{"line_number":57,"context_line":"        logging.info(\"\u003d\" * 50)"},{"line_number":58,"context_line":"        metricname, policy, value \u003d stats.split(\u0027 \u0027) #?"},{"line_number":59,"context_line":"        prom_metric \u003d self._gen_metrics(metricname, value)"}],"source_content_type":"text/x-python","patch_set":5,"id":"da9380ab_af117af7","line":56,"in_reply_to":"f9238714_ddf27246","updated":"2026-01-29 16:56:40.000000000","message":"Acknowledged","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1f89dae221ca224f992884d4d85d0f14cb377f9f","unresolved":true,"context_lines":[{"line_number":57,"context_line":"        logging.info(\"\u003d\" * 50)"},{"line_number":58,"context_line":"        metricname, policy, value \u003d stats.split(\u0027 \u0027) #?"},{"line_number":59,"context_line":"        prom_metric \u003d self._gen_metrics(metricname, value)"},{"line_number":60,"context_line":"        with open(env[\u0027input\u0027], \"a\") as file:"},{"line_number":61,"context_line":"            env[\u0027input\u0027].file \u003d file"},{"line_number":62,"context_line":"            file.write(prom_metric)"},{"line_number":63,"context_line":"        return"}],"source_content_type":"text/x-python","patch_set":5,"id":"17ce4156_78b8806c","line":60,"updated":"2026-01-15 20:22:16.000000000","message":"the name of the file where the stats go is obviously one of the things the client is expected to provide as input - I think that would be more clear if you asked for it explicitly - the nesting in an \"env\" dict seems\n\n1) unnecessarily obfuscating (Flat is better than nested https://peps.python.org/pep-0020/)\n2) confusing conceptual naming overlap with swob.Request.environ\n\nI\u0027d be curious to learn more about how you came to the conceptualization for this idea...","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ec30e831ea7cb2dda7eadbfa78029491992f0527","unresolved":false,"context_lines":[{"line_number":57,"context_line":"        logging.info(\"\u003d\" * 50)"},{"line_number":58,"context_line":"        metricname, policy, value \u003d stats.split(\u0027 \u0027) #?"},{"line_number":59,"context_line":"        prom_metric \u003d self._gen_metrics(metricname, value)"},{"line_number":60,"context_line":"        with open(env[\u0027input\u0027], \"a\") as file:"},{"line_number":61,"context_line":"            env[\u0027input\u0027].file \u003d file"},{"line_number":62,"context_line":"            file.write(prom_metric)"},{"line_number":63,"context_line":"        return"}],"source_content_type":"text/x-python","patch_set":5,"id":"971a9e6e_6372eb81","line":60,"in_reply_to":"17ce4156_78b8806c","updated":"2026-01-29 16:56:40.000000000","message":"Gotcha! I just thought of who shares variables in test and swift. env was the first one to pop up since we send env with needed info! :))\n\nUsing @with_tempdir now!","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1f89dae221ca224f992884d4d85d0f14cb377f9f","unresolved":true,"context_lines":[{"line_number":58,"context_line":"        metricname, policy, value \u003d stats.split(\u0027 \u0027) #?"},{"line_number":59,"context_line":"        prom_metric \u003d self._gen_metrics(metricname, value)"},{"line_number":60,"context_line":"        with open(env[\u0027input\u0027], \"a\") as file:"},{"line_number":61,"context_line":"            env[\u0027input\u0027].file \u003d file"},{"line_number":62,"context_line":"            file.write(prom_metric)"},{"line_number":63,"context_line":"        return"},{"line_number":64,"context_line":""}],"source_content_type":"text/x-python","patch_set":5,"id":"46229c3c_689d2bf5","line":61,"updated":"2026-01-15 20:22:16.000000000","message":"wait what, what do we think this is doing exactly... is `env[\u0027input\u0027]` supposed to be the a filename as string?  a file object?  a *Path* object!?","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ec30e831ea7cb2dda7eadbfa78029491992f0527","unresolved":false,"context_lines":[{"line_number":58,"context_line":"        metricname, policy, value \u003d stats.split(\u0027 \u0027) #?"},{"line_number":59,"context_line":"        prom_metric \u003d self._gen_metrics(metricname, value)"},{"line_number":60,"context_line":"        with open(env[\u0027input\u0027], \"a\") as file:"},{"line_number":61,"context_line":"            env[\u0027input\u0027].file \u003d file"},{"line_number":62,"context_line":"            file.write(prom_metric)"},{"line_number":63,"context_line":"        return"},{"line_number":64,"context_line":""}],"source_content_type":"text/x-python","patch_set":5,"id":"3e46d379_3bcf6076","line":61,"in_reply_to":"46229c3c_689d2bf5","updated":"2026-01-29 16:56:40.000000000","message":"Done","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1f89dae221ca224f992884d4d85d0f14cb377f9f","unresolved":true,"context_lines":[{"line_number":59,"context_line":"        prom_metric \u003d self._gen_metrics(metricname, value)"},{"line_number":60,"context_line":"        with open(env[\u0027input\u0027], \"a\") as file:"},{"line_number":61,"context_line":"            env[\u0027input\u0027].file \u003d file"},{"line_number":62,"context_line":"            file.write(prom_metric)"},{"line_number":63,"context_line":"        return"},{"line_number":64,"context_line":""},{"line_number":65,"context_line":"    def _build_line_parts(metric, value, metric_type, sample_rate):"}],"source_content_type":"text/x-python","patch_set":5,"id":"285ef3a5_61c71c2f","line":62,"updated":"2026-01-15 20:22:16.000000000","message":"I think \"display\" is a weird name for a method that writes to a file - maybe better as \"write_metrics\"","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ec30e831ea7cb2dda7eadbfa78029491992f0527","unresolved":false,"context_lines":[{"line_number":59,"context_line":"        prom_metric \u003d self._gen_metrics(metricname, value)"},{"line_number":60,"context_line":"        with open(env[\u0027input\u0027], \"a\") as file:"},{"line_number":61,"context_line":"            env[\u0027input\u0027].file \u003d file"},{"line_number":62,"context_line":"            file.write(prom_metric)"},{"line_number":63,"context_line":"        return"},{"line_number":64,"context_line":""},{"line_number":65,"context_line":"    def _build_line_parts(metric, value, metric_type, sample_rate):"}],"source_content_type":"text/x-python","patch_set":5,"id":"2be2a327_ace21939","line":62,"in_reply_to":"285ef3a5_61c71c2f","updated":"2026-01-29 16:56:40.000000000","message":"Acknowledged","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":7847,"name":"Alistair Coles","email":"alistairncoles@gmail.com","username":"acoles"},"change_message_id":"94af78cac5977ac70e611aaaf52dd8cd3d8cec79","unresolved":true,"context_lines":[{"line_number":21,"context_line":"PROM_USER_LABEL_NAMESPACE \u003d \u0027user_\u0027"},{"line_number":22,"context_line":""},{"line_number":23,"context_line":""},{"line_number":24,"context_line":"def get_prometheous_client(env, metricname, labels, value, filename, conf\u003dNone, logger\u003dNone):"},{"line_number":25,"context_line":"    \"\"\""},{"line_number":26,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":27,"context_line":"    \"\"\""}],"source_content_type":"text/x-python","patch_set":7,"id":"4961e5a7_36902b61","line":24,"updated":"2026-01-30 17:34:38.000000000","message":"we have\n```\ndef get_statsd_client(conf\u003dNone, tail_prefix\u003d\u0027\u0027, logger\u003dNone)\n```\nand\n```\ndef get_swift_logger(conf, name\u003dNone, log_to_console\u003dFalse, log_route\u003dNone,\n                     fmt\u003d\"%(server)s: %(message)s\"):\n```\nwhere the first arg is the conf dict.\n\nCould be worth sticking with that pattern or \"first arg is conf\"","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"8ea433753461363516daad2f0a75b32496d00d18","unresolved":false,"context_lines":[{"line_number":21,"context_line":"PROM_USER_LABEL_NAMESPACE \u003d \u0027user_\u0027"},{"line_number":22,"context_line":""},{"line_number":23,"context_line":""},{"line_number":24,"context_line":"def get_prometheous_client(env, metricname, labels, value, filename, conf\u003dNone, logger\u003dNone):"},{"line_number":25,"context_line":"    \"\"\""},{"line_number":26,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":27,"context_line":"    \"\"\""}],"source_content_type":"text/x-python","patch_set":7,"id":"3f1f1bbd_be67cac6","line":24,"in_reply_to":"4961e5a7_36902b61","updated":"2026-02-06 01:20:27.000000000","message":"Acknowledged","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":7847,"name":"Alistair Coles","email":"alistairncoles@gmail.com","username":"acoles"},"change_message_id":"94af78cac5977ac70e611aaaf52dd8cd3d8cec79","unresolved":true,"context_lines":[{"line_number":26,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":27,"context_line":"    \"\"\""},{"line_number":28,"context_line":"    conf \u003d conf or {}"},{"line_number":29,"context_line":"    metricname \u003d metricname"},{"line_number":30,"context_line":"    labels \u003d dict(sorted(labels.items()))"},{"line_number":31,"context_line":"    labels \u003d frozenset(labels.items())"},{"line_number":32,"context_line":"    key \u003d (metricname, labels)"}],"source_content_type":"text/x-python","patch_set":7,"id":"7548df38_c2d6855d","line":29,"updated":"2026-01-30 17:34:38.000000000","message":"there\u0027s no need to reassign these variables (metricname, value, logger)","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"8ea433753461363516daad2f0a75b32496d00d18","unresolved":false,"context_lines":[{"line_number":26,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":27,"context_line":"    \"\"\""},{"line_number":28,"context_line":"    conf \u003d conf or {}"},{"line_number":29,"context_line":"    metricname \u003d metricname"},{"line_number":30,"context_line":"    labels \u003d dict(sorted(labels.items()))"},{"line_number":31,"context_line":"    labels \u003d frozenset(labels.items())"},{"line_number":32,"context_line":"    key \u003d (metricname, labels)"}],"source_content_type":"text/x-python","patch_set":7,"id":"a6defbd1_adc10455","line":29,"in_reply_to":"7548df38_c2d6855d","updated":"2026-02-06 01:20:27.000000000","message":"Acknowledged","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":7847,"name":"Alistair Coles","email":"alistairncoles@gmail.com","username":"acoles"},"change_message_id":"94af78cac5977ac70e611aaaf52dd8cd3d8cec79","unresolved":true,"context_lines":[{"line_number":38,"context_line":"    return PrometheousClient(env, key, value, filename, conf, logger)"},{"line_number":39,"context_line":""},{"line_number":40,"context_line":""},{"line_number":41,"context_line":"class AbstractPrometheousClient:"},{"line_number":42,"context_line":"    def __init__(self, env, key, value, fileneme, conf\u003dNone, logger\u003dNone):"},{"line_number":43,"context_line":"        self.key \u003d key"},{"line_number":44,"context_line":"        self.value \u003d value"}],"source_content_type":"text/x-python","patch_set":7,"id":"08a471b9_b88062ca","line":41,"updated":"2026-01-30 17:34:38.000000000","message":"I\u0027m wondering why there is an abstract superclass - do you have thoughts for what other subclasses there will be?","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"8ea433753461363516daad2f0a75b32496d00d18","unresolved":true,"context_lines":[{"line_number":38,"context_line":"    return PrometheousClient(env, key, value, filename, conf, logger)"},{"line_number":39,"context_line":""},{"line_number":40,"context_line":""},{"line_number":41,"context_line":"class AbstractPrometheousClient:"},{"line_number":42,"context_line":"    def __init__(self, env, key, value, fileneme, conf\u003dNone, logger\u003dNone):"},{"line_number":43,"context_line":"        self.key \u003d key"},{"line_number":44,"context_line":"        self.value \u003d value"}],"source_content_type":"text/x-python","patch_set":7,"id":"4acb8a76_ae7da871","line":41,"in_reply_to":"08a471b9_b88062ca","updated":"2026-02-06 01:20:27.000000000","message":"Just this one, I noticed this pattern in statsd_client! No particular reason to add an abstract class in this","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"d28f060d253fa45bd118c68808834739ea47a8a0","unresolved":false,"context_lines":[{"line_number":38,"context_line":"    return PrometheousClient(env, key, value, filename, conf, logger)"},{"line_number":39,"context_line":""},{"line_number":40,"context_line":""},{"line_number":41,"context_line":"class AbstractPrometheousClient:"},{"line_number":42,"context_line":"    def __init__(self, env, key, value, fileneme, conf\u003dNone, logger\u003dNone):"},{"line_number":43,"context_line":"        self.key \u003d key"},{"line_number":44,"context_line":"        self.value \u003d value"}],"source_content_type":"text/x-python","patch_set":7,"id":"9cfbdd77_2b1538cc","line":41,"in_reply_to":"4acb8a76_ae7da871","updated":"2026-02-12 01:17:54.000000000","message":"Done","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":7847,"name":"Alistair Coles","email":"alistairncoles@gmail.com","username":"acoles"},"change_message_id":"94af78cac5977ac70e611aaaf52dd8cd3d8cec79","unresolved":true,"context_lines":[{"line_number":80,"context_line":"            metric \u003d self._prefix + metric"},{"line_number":81,"context_line":"            line \u003d self._build_line_parts("},{"line_number":82,"context_line":"                metric, value, metric_type, adjusted_sample_rate)"},{"line_number":83,"context_line":"            return self._send_line(line)"},{"line_number":84,"context_line":""},{"line_number":85,"context_line":"    def _update_stats(self, metric, value, **kwargs):"},{"line_number":86,"context_line":"        return self._send(metric, value, \u0027c\u0027, **kwargs)"}],"source_content_type":"text/x-python","patch_set":7,"id":"ca07b125_e3fcc324","line":83,"range":{"start_line":83,"start_character":19,"end_line":83,"end_character":40},"updated":"2026-01-30 17:34:38.000000000","message":"I can\u0027t see a _send_line method...but this all looks very similar to the statsd client ... is this still WIP and needing to be further adapted for the PrometheusClient","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"8ea433753461363516daad2f0a75b32496d00d18","unresolved":false,"context_lines":[{"line_number":80,"context_line":"            metric \u003d self._prefix + metric"},{"line_number":81,"context_line":"            line \u003d self._build_line_parts("},{"line_number":82,"context_line":"                metric, value, metric_type, adjusted_sample_rate)"},{"line_number":83,"context_line":"            return self._send_line(line)"},{"line_number":84,"context_line":""},{"line_number":85,"context_line":"    def _update_stats(self, metric, value, **kwargs):"},{"line_number":86,"context_line":"        return self._send(metric, value, \u0027c\u0027, **kwargs)"}],"source_content_type":"text/x-python","patch_set":7,"id":"52be0013_62750d1a","line":83,"range":{"start_line":83,"start_character":19,"end_line":83,"end_character":40},"in_reply_to":"ca07b125_e3fcc324","updated":"2026-02-06 01:20:27.000000000","message":"It was WIP, not needed anymore","commit_id":"1395affd541c81ac1de954bf43fefab455ddd104"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"795c971ab91775835a923645c604f184ea3f9df3","unresolved":true,"context_lines":[{"line_number":33,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":34,"context_line":"    \"\"\""},{"line_number":35,"context_line":"    conf \u003d conf or {}"},{"line_number":36,"context_line":"    name, labels, value \u003d parse_metrics(metric)"},{"line_number":37,"context_line":"    _stats \u003d {}"},{"line_number":38,"context_line":"    with open(filename, \"a\"):"},{"line_number":39,"context_line":"        pass"}],"source_content_type":"text/x-python","patch_set":11,"id":"bb697203_af166bbc","line":36,"updated":"2026-02-13 20:04:07.000000000","message":"let\u0027s get rid of the \"metric\" argument entirely - when I create a new prometheus client I should just have a blank slate.","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"fe3175dffa8e10748f1a5bea4adf6536c696df08","unresolved":false,"context_lines":[{"line_number":33,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":34,"context_line":"    \"\"\""},{"line_number":35,"context_line":"    conf \u003d conf or {}"},{"line_number":36,"context_line":"    name, labels, value \u003d parse_metrics(metric)"},{"line_number":37,"context_line":"    _stats \u003d {}"},{"line_number":38,"context_line":"    with open(filename, \"a\"):"},{"line_number":39,"context_line":"        pass"}],"source_content_type":"text/x-python","patch_set":11,"id":"9f1af57e_9fe06785","line":36,"in_reply_to":"bb697203_af166bbc","updated":"2026-03-19 18:47:20.000000000","message":"Done","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"795c971ab91775835a923645c604f184ea3f9df3","unresolved":true,"context_lines":[{"line_number":72,"context_line":"        return"},{"line_number":73,"context_line":""},{"line_number":74,"context_line":"    def update_stats(self, metric, update_val):"},{"line_number":75,"context_line":"        name, labels, value \u003d parse_metrics(metric)"},{"line_number":76,"context_line":"        self._stats[str(name) + str(labels)] \u003d update_val"},{"line_number":77,"context_line":"        self.write_stats()"},{"line_number":78,"context_line":""}],"source_content_type":"text/x-python","patch_set":11,"id":"6ba02893_79d44fa1","line":75,"updated":"2026-02-13 20:04:07.000000000","message":"I think instead of parse_metrics here I think the input should look like statsd:\n\nhttps://github.com/NVIDIA/swift/blob/master/swift/common/statsd_client.py#L569C46-L569C52\n\nthe \"metric\" argument is required - it\u0027s the name of the metrics - and any \"labels\" would come in as a dict.","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"fe3175dffa8e10748f1a5bea4adf6536c696df08","unresolved":false,"context_lines":[{"line_number":72,"context_line":"        return"},{"line_number":73,"context_line":""},{"line_number":74,"context_line":"    def update_stats(self, metric, update_val):"},{"line_number":75,"context_line":"        name, labels, value \u003d parse_metrics(metric)"},{"line_number":76,"context_line":"        self._stats[str(name) + str(labels)] \u003d update_val"},{"line_number":77,"context_line":"        self.write_stats()"},{"line_number":78,"context_line":""}],"source_content_type":"text/x-python","patch_set":11,"id":"6b96a2f8_4fd8ff23","line":75,"in_reply_to":"6ba02893_79d44fa1","updated":"2026-03-19 18:47:20.000000000","message":"Acknowledged","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"795c971ab91775835a923645c604f184ea3f9df3","unresolved":true,"context_lines":[{"line_number":74,"context_line":"    def update_stats(self, metric, update_val):"},{"line_number":75,"context_line":"        name, labels, value \u003d parse_metrics(metric)"},{"line_number":76,"context_line":"        self._stats[str(name) + str(labels)] \u003d update_val"},{"line_number":77,"context_line":"        self.write_stats()"},{"line_number":78,"context_line":""},{"line_number":79,"context_line":"    def _update_stats(self, key, count):"},{"line_number":80,"context_line":"        return self.update_stats(key, self._stats[self.key] + count)"}],"source_content_type":"text/x-python","patch_set":11,"id":"4ca2ccd4_afb0e3df","line":77,"updated":"2026-02-13 20:04:07.000000000","message":"There is probably a \"batch\" optimization here where you might want to add all the metrics with multiple calls to udpate_stats/increment etc and then call \"write_stats\" once at the end (or periodically from another thread with syncronization or something)","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"969839b5ec3d5ae322ddb7888758e5a8e33dd831","unresolved":false,"context_lines":[{"line_number":74,"context_line":"    def update_stats(self, metric, update_val):"},{"line_number":75,"context_line":"        name, labels, value \u003d parse_metrics(metric)"},{"line_number":76,"context_line":"        self._stats[str(name) + str(labels)] \u003d update_val"},{"line_number":77,"context_line":"        self.write_stats()"},{"line_number":78,"context_line":""},{"line_number":79,"context_line":"    def _update_stats(self, key, count):"},{"line_number":80,"context_line":"        return self.update_stats(key, self._stats[self.key] + count)"}],"source_content_type":"text/x-python","patch_set":11,"id":"5e667e1d_7146834f","line":77,"in_reply_to":"4ca2ccd4_afb0e3df","updated":"2026-03-24 18:40:08.000000000","message":"Acknowledged","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"795c971ab91775835a923645c604f184ea3f9df3","unresolved":true,"context_lines":[{"line_number":77,"context_line":"        self.write_stats()"},{"line_number":78,"context_line":""},{"line_number":79,"context_line":"    def _update_stats(self, key, count):"},{"line_number":80,"context_line":"        return self.update_stats(key, self._stats[self.key] + count)"},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"    def increment(self, key):"},{"line_number":83,"context_line":"        return self._update_stats(key, 1)"}],"source_content_type":"text/x-python","patch_set":11,"id":"119857f6_ce67fb98","line":80,"updated":"2026-02-13 20:04:07.000000000","message":"this is not the normal order I expect with method naming....\n\nI would have expected that \"update_stats\" uses \"_update_stats\" not the other way.\n\nin statsd client we always send `self._send(metric, value, \u0027c\u0027, **kwargs)` so it\u0027s a `c` unit - i.e. a *relative* change (a counter); however statsd *does* support \u0027g\u0027 (a guage)\n\nhttps://github.com/statsd/statsd/blob/master/docs/metric_types.md#gauges\n\nwe should look at an otel client to align their method naming - having two \"update_stats\" where one is a gauge/set and the other is an relative/increment/offset/counter is going to be confusing.\n\nOtel has separate primitives for counters/gauges.\n\nTo change a counter you use the \"add\" method:\n\nhttps://opentelemetry.io/docs/specs/otel/metrics/api/#counter-creation\n\nTo change a gauge you use the \"record\" method:\n\nhttps://opentelemetry.io/docs/specs/otel/metrics/api/#gauge-creation\n\nso maybe \"_add_counter\" and \"record_[stat|gauge]\" ????","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"969839b5ec3d5ae322ddb7888758e5a8e33dd831","unresolved":false,"context_lines":[{"line_number":77,"context_line":"        self.write_stats()"},{"line_number":78,"context_line":""},{"line_number":79,"context_line":"    def _update_stats(self, key, count):"},{"line_number":80,"context_line":"        return self.update_stats(key, self._stats[self.key] + count)"},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"    def increment(self, key):"},{"line_number":83,"context_line":"        return self._update_stats(key, 1)"}],"source_content_type":"text/x-python","patch_set":11,"id":"e1debb1f_2f2e837d","line":80,"in_reply_to":"119857f6_ce67fb98","updated":"2026-03-24 18:40:08.000000000","message":"Done","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"bb10abec2f1db98ebceb473dfcf63d3600208e8f","unresolved":true,"context_lines":[{"line_number":62,"context_line":"    labels \u003d labels or {}"},{"line_number":63,"context_line":""},{"line_number":64,"context_line":"    if not isinstance(name, str):"},{"line_number":65,"context_line":"        raise ValueError(f\"Metric name must be string, got {type(name)}\")"},{"line_number":66,"context_line":""},{"line_number":67,"context_line":"    if labels and isinstance(labels, dict):"},{"line_number":68,"context_line":"        # Sort labels for consistent output"}],"source_content_type":"text/x-python","patch_set":14,"id":"469007c2_9f7ffb35","line":65,"updated":"2026-03-10 23:04:54.000000000","message":"this seems like a reasonable thing to do if the type of the argument isn\u0027t what we expect","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"fe3175dffa8e10748f1a5bea4adf6536c696df08","unresolved":false,"context_lines":[{"line_number":62,"context_line":"    labels \u003d labels or {}"},{"line_number":63,"context_line":""},{"line_number":64,"context_line":"    if not isinstance(name, str):"},{"line_number":65,"context_line":"        raise ValueError(f\"Metric name must be string, got {type(name)}\")"},{"line_number":66,"context_line":""},{"line_number":67,"context_line":"    if labels and isinstance(labels, dict):"},{"line_number":68,"context_line":"        # Sort labels for consistent output"}],"source_content_type":"text/x-python","patch_set":14,"id":"d6f386cc_295301c4","line":65,"in_reply_to":"469007c2_9f7ffb35","updated":"2026-03-19 18:47:20.000000000","message":"Done","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"bb10abec2f1db98ebceb473dfcf63d3600208e8f","unresolved":true,"context_lines":[{"line_number":69,"context_line":"        labels_str \u003d \u0027,\u0027.join("},{"line_number":70,"context_line":"            f\u0027{k}\u003d\"{v}\"\u0027 for k, v in sorted(labels.items())"},{"line_number":71,"context_line":"        )"},{"line_number":72,"context_line":"        return f\u0027{name}{{{labels_str}}} {value}\\n\u0027"},{"line_number":73,"context_line":"    else:"},{"line_number":74,"context_line":"        return f\u0027{name} {value}\\n\u0027"},{"line_number":75,"context_line":""}],"source_content_type":"text/x-python","patch_set":14,"id":"5f396d84_2ad21882","line":72,"updated":"2026-03-10 23:04:54.000000000","message":"the `{{{label_str}}}` sort of stinks IMHO - maybe use another format method to avoid conflating the literal `{}` chars?\n\n```\n\u003e\u003e\u003e \u0027%(name)s{%(label_str)s} %(value)s\u0027 % {\u0027name\u0027:\u0027foo\u0027, \u0027label_str\u0027: \u0027k1\u003dv1,k2\u003dv2\u0027, \u0027value\u0027: 3}\n\u0027foo{k1\u003dv1,k2\u003dv2} 3\u0027\n```\n\nor maybe even:\n\n```\n\u003e\u003e\u003e name + \u0027{\u0027 + label_str + \u0027} \u0027 + str(value)\n\u0027foo{k1\u003dv1,k2\u003dv2} 3\u0027\n```","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"969839b5ec3d5ae322ddb7888758e5a8e33dd831","unresolved":false,"context_lines":[{"line_number":69,"context_line":"        labels_str \u003d \u0027,\u0027.join("},{"line_number":70,"context_line":"            f\u0027{k}\u003d\"{v}\"\u0027 for k, v in sorted(labels.items())"},{"line_number":71,"context_line":"        )"},{"line_number":72,"context_line":"        return f\u0027{name}{{{labels_str}}} {value}\\n\u0027"},{"line_number":73,"context_line":"    else:"},{"line_number":74,"context_line":"        return f\u0027{name} {value}\\n\u0027"},{"line_number":75,"context_line":""}],"source_content_type":"text/x-python","patch_set":14,"id":"4e595a48_d5cda28c","line":72,"in_reply_to":"5f396d84_2ad21882","updated":"2026-03-24 18:40:08.000000000","message":"Done","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"bb10abec2f1db98ebceb473dfcf63d3600208e8f","unresolved":true,"context_lines":[{"line_number":71,"context_line":"        )"},{"line_number":72,"context_line":"        return f\u0027{name}{{{labels_str}}} {value}\\n\u0027"},{"line_number":73,"context_line":"    else:"},{"line_number":74,"context_line":"        return f\u0027{name} {value}\\n\u0027"},{"line_number":75,"context_line":""},{"line_number":76,"context_line":""},{"line_number":77,"context_line":"def format_metrics_batch(metrics_list):"}],"source_content_type":"text/x-python","patch_set":14,"id":"f730a2e0_21df483b","line":74,"updated":"2026-03-10 23:04:54.000000000","message":"if labels is a non-empty non-dict I\u0027m not sure WHAT we should do - but I don\u0027t think \"silently ignore\" it is the best answer.","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"5caa9f5a203bdca5ca37c018ea381307cbd3ba3c","unresolved":false,"context_lines":[{"line_number":71,"context_line":"        )"},{"line_number":72,"context_line":"        return f\u0027{name}{{{labels_str}}} {value}\\n\u0027"},{"line_number":73,"context_line":"    else:"},{"line_number":74,"context_line":"        return f\u0027{name} {value}\\n\u0027"},{"line_number":75,"context_line":""},{"line_number":76,"context_line":""},{"line_number":77,"context_line":"def format_metrics_batch(metrics_list):"}],"source_content_type":"text/x-python","patch_set":14,"id":"a0a171f0_2d6d6c1e","line":74,"in_reply_to":"6bf1fb02_f68d1dfb","updated":"2026-04-14 16:53:37.000000000","message":"Done","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"fe3175dffa8e10748f1a5bea4adf6536c696df08","unresolved":true,"context_lines":[{"line_number":71,"context_line":"        )"},{"line_number":72,"context_line":"        return f\u0027{name}{{{labels_str}}} {value}\\n\u0027"},{"line_number":73,"context_line":"    else:"},{"line_number":74,"context_line":"        return f\u0027{name} {value}\\n\u0027"},{"line_number":75,"context_line":""},{"line_number":76,"context_line":""},{"line_number":77,"context_line":"def format_metrics_batch(metrics_list):"}],"source_content_type":"text/x-python","patch_set":14,"id":"6bf1fb02_f68d1dfb","line":74,"in_reply_to":"f730a2e0_21df483b","updated":"2026-03-19 18:47:20.000000000","message":"As an edge case, i assumed labels can be empty! Would they never be empty?","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"bb10abec2f1db98ebceb473dfcf63d3600208e8f","unresolved":true,"context_lines":[{"line_number":96,"context_line":"    return \u0027\u0027.join(lines)"},{"line_number":97,"context_line":""},{"line_number":98,"context_line":""},{"line_number":99,"context_line":"def get_prometheus_client(conf, env, filename, logger\u003dNone):"},{"line_number":100,"context_line":"    \"\"\""},{"line_number":101,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":102,"context_line":"    \"\"\""}],"source_content_type":"text/x-python","patch_set":14,"id":"91d056a3_03bcbe50","line":99,"updated":"2026-03-10 23:04:54.000000000","message":"why is env passed in here?\n\nuse the same signature as `get_labeled_statsd_client`\n\n```\ndef get_labeled_statsd_client(conf\u003dNone, logger\u003dNone):\n```\n\nhttps://github.com/NVIDIA/swift/blob/master/swift/common/statsd_client.py#L141","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"969839b5ec3d5ae322ddb7888758e5a8e33dd831","unresolved":false,"context_lines":[{"line_number":96,"context_line":"    return \u0027\u0027.join(lines)"},{"line_number":97,"context_line":""},{"line_number":98,"context_line":""},{"line_number":99,"context_line":"def get_prometheus_client(conf, env, filename, logger\u003dNone):"},{"line_number":100,"context_line":"    \"\"\""},{"line_number":101,"context_line":"        Get an instance of PrometheousClient using config settings."},{"line_number":102,"context_line":"    \"\"\""}],"source_content_type":"text/x-python","patch_set":14,"id":"bab62a03_badd3eb2","line":99,"in_reply_to":"91d056a3_03bcbe50","updated":"2026-03-24 18:40:08.000000000","message":"Acknowledged","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"bb10abec2f1db98ebceb473dfcf63d3600208e8f","unresolved":true,"context_lines":[{"line_number":110,"context_line":"class AbstractPrometheusClient:"},{"line_number":111,"context_line":"    def __init__(self, conf, env, _stats, filename, logger\u003dNone):"},{"line_number":112,"context_line":"        self.conf \u003d conf"},{"line_number":113,"context_line":"        self.env \u003d env"},{"line_number":114,"context_line":"        self._stats \u003d {}"},{"line_number":115,"context_line":"        self.filename \u003d filename"},{"line_number":116,"context_line":"        self.logger \u003d logger"}],"source_content_type":"text/x-python","patch_set":14,"id":"c5fe1565_b3f7f735","line":113,"updated":"2026-03-10 23:04:54.000000000","message":"this is never used?  Is this supposed to be a stab at a base labels sort of configuration?","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"fe3175dffa8e10748f1a5bea4adf6536c696df08","unresolved":false,"context_lines":[{"line_number":110,"context_line":"class AbstractPrometheusClient:"},{"line_number":111,"context_line":"    def __init__(self, conf, env, _stats, filename, logger\u003dNone):"},{"line_number":112,"context_line":"        self.conf \u003d conf"},{"line_number":113,"context_line":"        self.env \u003d env"},{"line_number":114,"context_line":"        self._stats \u003d {}"},{"line_number":115,"context_line":"        self.filename \u003d filename"},{"line_number":116,"context_line":"        self.logger \u003d logger"}],"source_content_type":"text/x-python","patch_set":14,"id":"71f788f4_e47c75a5","line":113,"in_reply_to":"c5fe1565_b3f7f735","updated":"2026-03-19 18:47:20.000000000","message":"Incase this patch develops big, and there are multiple methods/ classes. Although, I agree that it\u0027s not been used as of now. If it\u0027s still unneeded in the following patch, I\u0027ll remove this","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"bb10abec2f1db98ebceb473dfcf63d3600208e8f","unresolved":true,"context_lines":[{"line_number":222,"context_line":""},{"line_number":223,"context_line":"    def increment(self, metric):"},{"line_number":224,"context_line":"        \"\"\"Increment a metric value by 1.\"\"\""},{"line_number":225,"context_line":"        name, labels, value \u003d parse_metrics(metric)"},{"line_number":226,"context_line":"        metric_key_str \u003d metric_key(name, **labels)"},{"line_number":227,"context_line":"        current_value \u003d self._stats.get(metric_key_str, 0)"},{"line_number":228,"context_line":"        return self.update_stats(metric, current_value + 1)"}],"source_content_type":"text/x-python","patch_set":14,"id":"d81bc450_13057a98","line":225,"updated":"2026-03-10 23:04:54.000000000","message":"this is not right, the method signature should look like the statsd client - the caller wants to provide a metric name w/ labels as an optional kwarg dict.\n\n```\ndef increment(self, metric, *, labels\u003dNone):\n```\n\nhttps://github.com/NVIDIA/swift/blob/master/swift/common/statsd_client.py#L581C5-L582C1","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"969839b5ec3d5ae322ddb7888758e5a8e33dd831","unresolved":false,"context_lines":[{"line_number":222,"context_line":""},{"line_number":223,"context_line":"    def increment(self, metric):"},{"line_number":224,"context_line":"        \"\"\"Increment a metric value by 1.\"\"\""},{"line_number":225,"context_line":"        name, labels, value \u003d parse_metrics(metric)"},{"line_number":226,"context_line":"        metric_key_str \u003d metric_key(name, **labels)"},{"line_number":227,"context_line":"        current_value \u003d self._stats.get(metric_key_str, 0)"},{"line_number":228,"context_line":"        return self.update_stats(metric, current_value + 1)"}],"source_content_type":"text/x-python","patch_set":14,"id":"ebd95008_d17647ed","line":225,"in_reply_to":"d81bc450_13057a98","updated":"2026-03-24 18:40:08.000000000","message":"Acknowledged","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"}],"test/unit/common/test_prom_metrics.py":[{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"b4dbb83a9c6164c1821f5c9a05aeb315f2b653ff","unresolved":false,"context_lines":[{"line_number":29,"context_line":"        # expirer_balance_score{host \u003d \"saio\", days \u003d \"-1\", date \u003d \"2025-11-05\"} 48.70"},{"line_number":30,"context_line":"        # expirer_balance_rmse{host \u003d \"saio\", days \u003d \"-1\", date \u003d \"2025-11-05\"} 1.54"},{"line_number":31,"context_line":"        # expirer_balance_total_tasks{host \u003d \"saio\", days \u003d \"-1\", date \u003d \"2025-11-05\"} 300"},{"line_number":32,"context_line":"        # expirer_balance_expected_per_container{host \u003d \"saio\", days \u003d \"-1\", date \u003d \"2025-11-05\"} 3.00"}],"source_content_type":"text/x-python","patch_set":1,"id":"1f886773_1b06477d","line":32,"in_reply_to":"41156bef_6c415986","updated":"2026-01-05 21:46:11.000000000","message":"Done","commit_id":"fd1d11deca9bbfce218bbbb0e06c6b2b68f9b938"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"8fb8665fbef2e8aa06ac54c1eaf90bc2dcbf1ca7","unresolved":true,"context_lines":[{"line_number":29,"context_line":"        # expirer_balance_score{host \u003d \"saio\", days \u003d \"-1\", date \u003d \"2025-11-05\"} 48.70"},{"line_number":30,"context_line":"        # expirer_balance_rmse{host \u003d \"saio\", days \u003d \"-1\", date \u003d \"2025-11-05\"} 1.54"},{"line_number":31,"context_line":"        # expirer_balance_total_tasks{host \u003d \"saio\", days \u003d \"-1\", date \u003d \"2025-11-05\"} 300"},{"line_number":32,"context_line":"        # expirer_balance_expected_per_container{host \u003d \"saio\", days \u003d \"-1\", date \u003d \"2025-11-05\"} 3.00"}],"source_content_type":"text/x-python","patch_set":1,"id":"41156bef_6c415986","line":32,"in_reply_to":"9ca03444_f32b72de","updated":"2025-12-17 15:54:43.000000000","message":"yes, this is perfect - a test to demonstrate how you can use the PrometheousClient to generate the kinds of metrics we need would be a great starting point.","commit_id":"fd1d11deca9bbfce218bbbb0e06c6b2b68f9b938"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1f89dae221ca224f992884d4d85d0f14cb377f9f","unresolved":true,"context_lines":[{"line_number":49,"context_line":"        }"},{"line_number":50,"context_line":"        blah \u003d get_prometheous_client(env, \u0027expirer_balance_score\u0027, 50, conf\u003dNone, logger\u003dNone)"},{"line_number":51,"context_line":"        self.assertEqual(type(blah), PrometheousClient)"},{"line_number":52,"context_line":"        self.assertEqual(blah.display_stats, read_stats(env))"}],"source_content_type":"text/x-python","patch_set":5,"id":"30b39795_b1c77a5b","line":52,"updated":"2026-01-15 20:22:16.000000000","message":"I have no idea what this is even attempting to achieve\n\n```\nAssertionError: \u003cbound method PrometheousClient.display_s[80 chars]3a0\u003e\u003e !\u003d None\n```","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ec30e831ea7cb2dda7eadbfa78029491992f0527","unresolved":false,"context_lines":[{"line_number":49,"context_line":"        }"},{"line_number":50,"context_line":"        blah \u003d get_prometheous_client(env, \u0027expirer_balance_score\u0027, 50, conf\u003dNone, logger\u003dNone)"},{"line_number":51,"context_line":"        self.assertEqual(type(blah), PrometheousClient)"},{"line_number":52,"context_line":"        self.assertEqual(blah.display_stats, read_stats(env))"}],"source_content_type":"text/x-python","patch_set":5,"id":"4f57cfce_4b1f5dd3","line":52,"in_reply_to":"30b39795_b1c77a5b","updated":"2026-01-29 16:56:40.000000000","message":"Improvised!","commit_id":"460c16e21099714aa2972623cd888a158824f13b"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"795c971ab91775835a923645c604f184ea3f9df3","unresolved":true,"context_lines":[{"line_number":80,"context_line":"                        })): 3.0}"},{"line_number":81,"context_line":"        self.assertEqual(read_metrics(filename), exp_stats)"},{"line_number":82,"context_line":""},{"line_number":83,"context_line":"        blah.update_stats(metric, 50)"},{"line_number":84,"context_line":"        exp_stats \u003d {"},{"line_number":85,"context_line":"            (\u0027object_updater\u0027,"},{"line_number":86,"context_line":"             frozenset({(\u0027metrics_name\u0027, \"\u0027sweep\u0027\"),"}],"source_content_type":"text/x-python","patch_set":11,"id":"42c9fdb7_caf91037","line":83,"updated":"2026-02-13 20:04:07.000000000","message":"this should use `metric\u003d\u0027object_updater\u0027` (really `swift_object_updater_sweep_time`) and `labels\u003d{HOST\u003d\u0027s8k...\u0027, ...}`\n\n... similar to statsd client\u0027s interface:\n\nhttps://github.com/NVIDIA/swift/blob/master/swift/common/statsd_client.py#L569\n\nN.B. sample_rate doesn\u0027t matter for an in-memory prom client that flushes to disk when the consumer calls `blah.write_stats()`","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"969839b5ec3d5ae322ddb7888758e5a8e33dd831","unresolved":false,"context_lines":[{"line_number":80,"context_line":"                        })): 3.0}"},{"line_number":81,"context_line":"        self.assertEqual(read_metrics(filename), exp_stats)"},{"line_number":82,"context_line":""},{"line_number":83,"context_line":"        blah.update_stats(metric, 50)"},{"line_number":84,"context_line":"        exp_stats \u003d {"},{"line_number":85,"context_line":"            (\u0027object_updater\u0027,"},{"line_number":86,"context_line":"             frozenset({(\u0027metrics_name\u0027, \"\u0027sweep\u0027\"),"}],"source_content_type":"text/x-python","patch_set":11,"id":"66b0af29_c3c0c01e","line":83,"in_reply_to":"42c9fdb7_caf91037","updated":"2026-03-24 18:40:08.000000000","message":"Done","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"795c971ab91775835a923645c604f184ea3f9df3","unresolved":true,"context_lines":[{"line_number":85,"context_line":"            (\u0027object_updater\u0027,"},{"line_number":86,"context_line":"             frozenset({(\u0027metrics_name\u0027, \"\u0027sweep\u0027\"),"},{"line_number":87,"context_line":"                        (\u0027HOST\u0027, \"\u0027s8k-sjc11-a25-ac-01.nsv.sjc11.nvmetal.net\u0027\")"},{"line_number":88,"context_line":"                        })): 50.0}"},{"line_number":89,"context_line":"        self.assertEqual(read_metrics(filename), exp_stats)"},{"line_number":90,"context_line":""},{"line_number":91,"context_line":"        blah.increment(metric)"}],"source_content_type":"text/x-python","patch_set":11,"id":"48744cd9_837c0c48","line":88,"updated":"2026-02-13 20:04:07.000000000","message":"use metric_key here to make the assertion a little more readable.","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"969839b5ec3d5ae322ddb7888758e5a8e33dd831","unresolved":false,"context_lines":[{"line_number":85,"context_line":"            (\u0027object_updater\u0027,"},{"line_number":86,"context_line":"             frozenset({(\u0027metrics_name\u0027, \"\u0027sweep\u0027\"),"},{"line_number":87,"context_line":"                        (\u0027HOST\u0027, \"\u0027s8k-sjc11-a25-ac-01.nsv.sjc11.nvmetal.net\u0027\")"},{"line_number":88,"context_line":"                        })): 50.0}"},{"line_number":89,"context_line":"        self.assertEqual(read_metrics(filename), exp_stats)"},{"line_number":90,"context_line":""},{"line_number":91,"context_line":"        blah.increment(metric)"}],"source_content_type":"text/x-python","patch_set":11,"id":"25bcdd7b_2708cc65","line":88,"in_reply_to":"48744cd9_837c0c48","updated":"2026-03-24 18:40:08.000000000","message":"Acknowledged","commit_id":"1bb5b822b37bb309193ae5549f05ee92fe6ac625"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"bb10abec2f1db98ebceb473dfcf63d3600208e8f","unresolved":true,"context_lines":[{"line_number":11,"context_line":"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or"},{"line_number":12,"context_line":"# implied."},{"line_number":13,"context_line":"# See the License for the specific language governing permissions and"},{"line_number":14,"context_line":"# limitations under the License."},{"line_number":15,"context_line":"import os"},{"line_number":16,"context_line":"import unittest"},{"line_number":17,"context_line":""}],"source_content_type":"text/x-python","patch_set":14,"id":"f3529c2c_85ca7c6b","line":14,"updated":"2026-03-10 23:04:54.000000000","message":"why isn\u0027t this file `test.unit.common.utils.test_prom_metrics` to match the new module `common.utils.prom_metrics`","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"969839b5ec3d5ae322ddb7888758e5a8e33dd831","unresolved":false,"context_lines":[{"line_number":11,"context_line":"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or"},{"line_number":12,"context_line":"# implied."},{"line_number":13,"context_line":"# See the License for the specific language governing permissions and"},{"line_number":14,"context_line":"# limitations under the License."},{"line_number":15,"context_line":"import os"},{"line_number":16,"context_line":"import unittest"},{"line_number":17,"context_line":""}],"source_content_type":"text/x-python","patch_set":14,"id":"830915f6_78e7050e","line":14,"in_reply_to":"f3529c2c_85ca7c6b","updated":"2026-03-24 18:40:08.000000000","message":"moved the other way, similar to how statsd is!","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"bb10abec2f1db98ebceb473dfcf63d3600208e8f","unresolved":true,"context_lines":[{"line_number":49,"context_line":"        env \u003d {"},{"line_number":50,"context_line":"            \u0027REQUEST_METHOD\u0027: \u0027GET\u0027,"},{"line_number":51,"context_line":"            \u0027HTTP_AUTHORIZATION\u0027: \u0027AWS X:Y:Z\u0027,"},{"line_number":52,"context_line":"        }"},{"line_number":53,"context_line":"        filename \u003d os.path.join(tempdir, \u0027example.txt\u0027)"},{"line_number":54,"context_line":"        # Use tuple format to write metric with proper labels"},{"line_number":55,"context_line":"        conf \u003d {}"}],"source_content_type":"text/x-python","patch_set":14,"id":"0c43b49a_48285c6f","line":52,"updated":"2026-03-10 23:04:54.000000000","message":"what on earth is this structure?","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"969839b5ec3d5ae322ddb7888758e5a8e33dd831","unresolved":false,"context_lines":[{"line_number":49,"context_line":"        env \u003d {"},{"line_number":50,"context_line":"            \u0027REQUEST_METHOD\u0027: \u0027GET\u0027,"},{"line_number":51,"context_line":"            \u0027HTTP_AUTHORIZATION\u0027: \u0027AWS X:Y:Z\u0027,"},{"line_number":52,"context_line":"        }"},{"line_number":53,"context_line":"        filename \u003d os.path.join(tempdir, \u0027example.txt\u0027)"},{"line_number":54,"context_line":"        # Use tuple format to write metric with proper labels"},{"line_number":55,"context_line":"        conf \u003d {}"}],"source_content_type":"text/x-python","patch_set":14,"id":"0045c102_58b6988d","line":52,"in_reply_to":"0c43b49a_48285c6f","updated":"2026-03-24 18:40:08.000000000","message":"Done","commit_id":"1999c38dab00890ce9f2226c398d0ceb5b0774ec"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"dc8f5366b4f25b474339ef7650d4e30727ed3508","unresolved":true,"context_lines":[{"line_number":36,"context_line":"            content \u003d f.read()"},{"line_number":37,"context_line":"            return content"},{"line_number":38,"context_line":"    except FileNotFoundError:"},{"line_number":39,"context_line":"        print(\"The file was not found.\")"},{"line_number":40,"context_line":""},{"line_number":41,"context_line":""},{"line_number":42,"context_line":"class BaseTestPrometheusMetrics(unittest.TestCase):"}],"source_content_type":"text/x-python","patch_set":21,"id":"13c54dfa_1bb1101a","line":39,"updated":"2026-04-14 19:10:20.000000000","message":"\u003e Errors should never pass silently.\n\nI wouldn\u0027t want to merge this, I would try to let the error get raised and see if tests fail - if anyone is *depending* on this behavior; maybe we can bake it into the test helper function name like `failsafe_read_stats` and throw a doc-string on for \"returns None if filename does not exist\"\n\n... also this is a pretty thin/opionated wrapper around `with open(): f.read()` - do we open files other than prom metrics?  invalid prom metrics maybe!?","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":36,"context_line":"            content \u003d f.read()"},{"line_number":37,"context_line":"            return content"},{"line_number":38,"context_line":"    except FileNotFoundError:"},{"line_number":39,"context_line":"        print(\"The file was not found.\")"},{"line_number":40,"context_line":""},{"line_number":41,"context_line":""},{"line_number":42,"context_line":"class BaseTestPrometheusMetrics(unittest.TestCase):"}],"source_content_type":"text/x-python","patch_set":21,"id":"dcec80da_688131fd","line":39,"in_reply_to":"13c54dfa_1bb1101a","updated":"2026-04-28 04:49:45.000000000","message":"done! Also raised errors if we open a wrong file or invalid prom metrics","commit_id":"81b11074c834ebbc1fe521d6f15ee70c5f468fb3"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":26,"context_line":""},{"line_number":27,"context_line":"from test.debug_logger import debug_logger"},{"line_number":28,"context_line":""},{"line_number":29,"context_line":"from test import read_metrics, metric_key"},{"line_number":30,"context_line":"from test.unit import with_tempdir"},{"line_number":31,"context_line":""},{"line_number":32,"context_line":""}],"source_content_type":"text/x-python","patch_set":22,"id":"1d533f94_25093431","line":29,"updated":"2026-04-21 20:05:35.000000000","message":"what\u0027s the difference between `read_metrics` and `parse_metrics`?\n\nI think you should get rid of 975948: add prom metrics test helpers | https://review.opendev.org/c/openstack/swift/+/975948 and collapse the implemention into `common.prom_metrics` and the tests into here.","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"7abd8df05b8081785b9541f05ff02331685d4b3f","unresolved":false,"context_lines":[{"line_number":26,"context_line":""},{"line_number":27,"context_line":"from test.debug_logger import debug_logger"},{"line_number":28,"context_line":""},{"line_number":29,"context_line":"from test import read_metrics, metric_key"},{"line_number":30,"context_line":"from test.unit import with_tempdir"},{"line_number":31,"context_line":""},{"line_number":32,"context_line":""}],"source_content_type":"text/x-python","patch_set":22,"id":"d87869fb_96a897f5","line":29,"in_reply_to":"1d533f94_25093431","updated":"2026-05-05 15:26:31.000000000","message":"Acknowledged","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":64,"context_line":"            (\u0027object_updater\u0027,"},{"line_number":65,"context_line":"             frozenset({(\u0027metrics_name\u0027, \u0027sweep\u0027),"},{"line_number":66,"context_line":"                        (\u0027HOST\u0027, \u0027s8k-sjc11-a25-ac-01.nsv.sjc11.nvmetal.net\u0027)"},{"line_number":67,"context_line":"                        })): 3.0}"},{"line_number":68,"context_line":"        self.assertEqual(read_metrics(filename), exp_stats)"},{"line_number":69,"context_line":""},{"line_number":70,"context_line":"    @with_tempdir"}],"source_content_type":"text/x-python","patch_set":22,"id":"28364b32_8c5d5b68","line":67,"updated":"2026-04-21 20:05:35.000000000","message":"personally I find the `metric_key` helper sufficiently literal and quite readable:\n\n```\ndiff --git a/test/unit/common/test_prom_metrics.py b/test/unit/common/test_prom_metrics.py\nindex a2b8cdb1d2..2179484fb0 100644\n--- a/test/unit/common/test_prom_metrics.py\n+++ b/test/unit/common/test_prom_metrics.py\n@@ -60,12 +60,10 @@ class BaseTestPrometheusMetrics(unittest.TestCase):\n         blah._write_stat(metric_data)\n         self.assertIs(type(blah), PrometheusClient)\n \n-        exp_stats \u003d {\n-            (\u0027object_updater\u0027,\n-             frozenset({(\u0027metrics_name\u0027, \u0027sweep\u0027),\n-                        (\u0027HOST\u0027, \u0027s8k-sjc11-a25-ac-01.nsv.sjc11.nvmetal.net\u0027)\n-                        })): 3.0}\n-        self.assertEqual(read_metrics(filename), exp_stats)\n+        self.assertEqual({\n+            metric_key(\u0027object_updater\u0027, metrics_name\u003d\u0027sweep\u0027,\n+                       HOST\u003d\u0027s8k-sjc11-a25-ac-01.nsv.sjc11.nvmetal.net\u0027): 3.0,\n+        }, read_metrics(filename))\n \n     @with_tempdir\n     def test_update_stats(self, tempdir):\n```","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"7abd8df05b8081785b9541f05ff02331685d4b3f","unresolved":false,"context_lines":[{"line_number":64,"context_line":"            (\u0027object_updater\u0027,"},{"line_number":65,"context_line":"             frozenset({(\u0027metrics_name\u0027, \u0027sweep\u0027),"},{"line_number":66,"context_line":"                        (\u0027HOST\u0027, \u0027s8k-sjc11-a25-ac-01.nsv.sjc11.nvmetal.net\u0027)"},{"line_number":67,"context_line":"                        })): 3.0}"},{"line_number":68,"context_line":"        self.assertEqual(read_metrics(filename), exp_stats)"},{"line_number":69,"context_line":""},{"line_number":70,"context_line":"    @with_tempdir"}],"source_content_type":"text/x-python","patch_set":22,"id":"299881ec_a23e96a0","line":67,"in_reply_to":"28364b32_8c5d5b68","updated":"2026-05-05 15:26:31.000000000","message":"Done","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":80,"context_line":"        blah._write_stat(metric_str)"},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"        # Update stats with new value"},{"line_number":83,"context_line":"        blah.update_stats(metric_str, 50)"},{"line_number":84,"context_line":"        exp_stats \u003d {"},{"line_number":85,"context_line":"            (\u0027object_updater\u0027,"},{"line_number":86,"context_line":"             frozenset({(\u0027metrics_name\u0027, \u0027sweep\u0027),"}],"source_content_type":"text/x-python","patch_set":22,"id":"89cdb76b_49b93c32","line":83,"updated":"2026-04-21 20:05:35.000000000","message":"this `update_stats` interface needs to go:\n\n```\n-\u003e blah.update_stats(metric_str, 50)\n(Pdb) !metric_str\n\u0027object_updater{HOST\u003d\"s8k-sjc11-a25-ac-01.nsv.sjc11.nvmetal.net\",metrics_name\u003d\"sweep\"} 3.0\u0027\n```\n\nsince `metric_str` already has a value I think it\u0027s a strange interface, compared to better interfaces that already exist:\n\n```\n(Pdb) blah._stats\n{(\u0027object_updater\u0027, frozenset({(\u0027metrics_name\u0027, \u0027sweep\u0027), (\u0027HOST\u0027, \u0027s8k-sjc11-a25-ac-01.nsv.sjc11.nvmetal.net\u0027)})): 3.0}\n(Pdb) blah.gauge(\u0027object_updater\u0027, 50, labels\u003d{\u0027metrics_name\u0027: \u0027sweep\u0027, \u0027HOST\u0027: \u0027s8k-sjc11-a25-ac-01.nsv.sjc11.nvmetal.net\u0027})\n\u0027object_updater{HOST\u003d\"s8k-sjc11-a25-ac-01.nsv.sjc11.nvmetal.net\",metrics_name\u003d\"sweep\"} 50\\n\u0027\n(Pdb) blah._stats\n{(\u0027object_updater\u0027, frozenset({(\u0027metrics_name\u0027, \u0027sweep\u0027), (\u0027HOST\u0027, \u0027s8k-sjc11-a25-ac-01.nsv.sjc11.nvmetal.net\u0027)})): 50}\n```","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ccfc40fce0c2d3d8e0bb87dbdc88aaf7ea4a150b","unresolved":false,"context_lines":[{"line_number":80,"context_line":"        blah._write_stat(metric_str)"},{"line_number":81,"context_line":""},{"line_number":82,"context_line":"        # Update stats with new value"},{"line_number":83,"context_line":"        blah.update_stats(metric_str, 50)"},{"line_number":84,"context_line":"        exp_stats \u003d {"},{"line_number":85,"context_line":"            (\u0027object_updater\u0027,"},{"line_number":86,"context_line":"             frozenset({(\u0027metrics_name\u0027, \u0027sweep\u0027),"}],"source_content_type":"text/x-python","patch_set":22,"id":"5be06a0d_2af29c18","line":83,"in_reply_to":"89cdb76b_49b93c32","updated":"2026-05-07 21:59:18.000000000","message":"Acknowledged","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":113,"context_line":""},{"line_number":114,"context_line":"        self.assertEqual(name, \u0027http_requests_total\u0027)"},{"line_number":115,"context_line":"        self.assertEqual(labels, {\u0027endpoint\u0027: \u0027/api\u0027, \u0027method\u0027: \u0027GET\u0027})"},{"line_number":116,"context_line":"        self.assertEqual(value, 42.0)"},{"line_number":117,"context_line":""},{"line_number":118,"context_line":"    def test_parse_metrics_without_labels(self):"},{"line_number":119,"context_line":"        \"\"\"Test parsing simple metrics without labels\"\"\""}],"source_content_type":"text/x-python","patch_set":22,"id":"48761674_6463f228","line":116,"updated":"2026-04-21 20:05:35.000000000","message":"I\u0027d encourage you to put all the tests for parsing/reading metrics into their own `TestPrometheusParsing` separate from the tests of `TestPrometheusClient`","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"ff8bbcd7be70afe80dd41554e2d66eae83fe1cc8","unresolved":false,"context_lines":[{"line_number":113,"context_line":""},{"line_number":114,"context_line":"        self.assertEqual(name, \u0027http_requests_total\u0027)"},{"line_number":115,"context_line":"        self.assertEqual(labels, {\u0027endpoint\u0027: \u0027/api\u0027, \u0027method\u0027: \u0027GET\u0027})"},{"line_number":116,"context_line":"        self.assertEqual(value, 42.0)"},{"line_number":117,"context_line":""},{"line_number":118,"context_line":"    def test_parse_metrics_without_labels(self):"},{"line_number":119,"context_line":"        \"\"\"Test parsing simple metrics without labels\"\"\""}],"source_content_type":"text/x-python","patch_set":22,"id":"cdf8805f_fc1ffeef","line":116,"in_reply_to":"48761674_6463f228","updated":"2026-04-28 04:49:45.000000000","message":"Done","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"6b828f105ebe2501d80931b112050931b95064db","unresolved":true,"context_lines":[{"line_number":198,"context_line":"        self.assertEqual("},{"line_number":199,"context_line":"            result,"},{"line_number":200,"context_line":"            \u0027room_temperature{location\u003d\"room1\",unit\u003d\"celsius\"} 22.0\\n\u0027"},{"line_number":201,"context_line":"        )"},{"line_number":202,"context_line":"        exp_stats \u003d {"},{"line_number":203,"context_line":"            metric_key(\u0027room_temperature\u0027, location\u003d\u0027room1\u0027,"},{"line_number":204,"context_line":"                       unit\u003d\u0027celsius\u0027): 22.0"}],"source_content_type":"text/x-python","patch_set":22,"id":"4adffb67_9feccfa0","line":201,"updated":"2026-04-21 20:05:35.000000000","message":"this is why I don\u0027t think it\u0027s a good idea to define the interface as returning this string - it makes it harder to change later b/c callers will come to expect it.","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"7abd8df05b8081785b9541f05ff02331685d4b3f","unresolved":false,"context_lines":[{"line_number":198,"context_line":"        self.assertEqual("},{"line_number":199,"context_line":"            result,"},{"line_number":200,"context_line":"            \u0027room_temperature{location\u003d\"room1\",unit\u003d\"celsius\"} 22.0\\n\u0027"},{"line_number":201,"context_line":"        )"},{"line_number":202,"context_line":"        exp_stats \u003d {"},{"line_number":203,"context_line":"            metric_key(\u0027room_temperature\u0027, location\u003d\u0027room1\u0027,"},{"line_number":204,"context_line":"                       unit\u003d\u0027celsius\u0027): 22.0"}],"source_content_type":"text/x-python","patch_set":22,"id":"3bf1acb2_0e664e0d","line":201,"in_reply_to":"4adffb67_9feccfa0","updated":"2026-05-05 15:26:31.000000000","message":"Done! used metric_key to get by!","commit_id":"1eb2486a2b5322c030db7f3e4129d7d3b84b59b5"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":140,"context_line":"            f.write(\u0027metric_without_value\\n\u0027)"},{"line_number":141,"context_line":""},{"line_number":142,"context_line":"        with self.assertRaises(ValueError):"},{"line_number":143,"context_line":"            read_metrics(filename)"},{"line_number":144,"context_line":""},{"line_number":145,"context_line":""},{"line_number":146,"context_line":"class TestPrometheusParsing(unittest.TestCase):"}],"source_content_type":"text/x-python","patch_set":28,"id":"4025d70d_bff8b787","line":143,"updated":"2026-05-27 05:49:00.000000000","message":"seems reasonable - any useful context in the ValueError?\n\n```\n(Pdb) read_metrics(filename)\n*** ValueError: Invalid Prometheus format: \u0027metric_without_value\u0027 - expected format \u0027metric_name value\u0027 or \u0027metric_name{{labels}} value\u0027\n```","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":140,"context_line":"            f.write(\u0027metric_without_value\\n\u0027)"},{"line_number":141,"context_line":""},{"line_number":142,"context_line":"        with self.assertRaises(ValueError):"},{"line_number":143,"context_line":"            read_metrics(filename)"},{"line_number":144,"context_line":""},{"line_number":145,"context_line":""},{"line_number":146,"context_line":"class TestPrometheusParsing(unittest.TestCase):"}],"source_content_type":"text/x-python","patch_set":28,"id":"ac977bd6_a816d1e7","line":143,"in_reply_to":"4025d70d_bff8b787","updated":"2026-06-03 16:14:27.000000000","message":"Done","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":223,"context_line":"            result,"},{"line_number":224,"context_line":"            \u0027simple_metric 42\\n\u0027"},{"line_number":225,"context_line":"            \u0027labeled_metric{label\u003d\"value\"} 100\\n\u0027"},{"line_number":226,"context_line":"            \u0027another_simple 55\\n\u0027)"},{"line_number":227,"context_line":""},{"line_number":228,"context_line":"    def test_format_metrics_batch_invalid_tuple_size(self):"},{"line_number":229,"context_line":"        with self.assertRaises(ValueError) as ctx:"}],"source_content_type":"text/x-python","patch_set":28,"id":"9c332b58_32980109","line":226,"updated":"2026-05-27 05:49:00.000000000","message":"oh good, there\u0027s not an excessive amount of tests to get rid of when you remove `format_metrics_batch`","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":223,"context_line":"            result,"},{"line_number":224,"context_line":"            \u0027simple_metric 42\\n\u0027"},{"line_number":225,"context_line":"            \u0027labeled_metric{label\u003d\"value\"} 100\\n\u0027"},{"line_number":226,"context_line":"            \u0027another_simple 55\\n\u0027)"},{"line_number":227,"context_line":""},{"line_number":228,"context_line":"    def test_format_metrics_batch_invalid_tuple_size(self):"},{"line_number":229,"context_line":"        with self.assertRaises(ValueError) as ctx:"}],"source_content_type":"text/x-python","patch_set":28,"id":"28028402_a4eff894","line":226,"in_reply_to":"9c332b58_32980109","updated":"2026-06-03 16:14:27.000000000","message":"Acknowledged","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":313,"context_line":"        client._update_stat_internal(\u0027simple_metric\u0027, 42)"},{"line_number":314,"context_line":"        client._update_stat_internal(\u0027labeled_metric\u0027, 100, {\u0027label\u0027: \u0027value\u0027})"},{"line_number":315,"context_line":"        client._update_stat_internal(\u0027string_metric\u0027, 55.0, {\u0027type\u0027: \u0027test\u0027})"},{"line_number":316,"context_line":"        client._update_stat_internal(\u0027dict_metric\u0027, 99, {\u0027key\u0027: \u0027val\u0027})"},{"line_number":317,"context_line":"        client.write_all_stats(filename\u003dfilename)"},{"line_number":318,"context_line":"        self.assertEqual(read_metrics(filename), {"},{"line_number":319,"context_line":"            metric_key(\u0027simple_metric\u0027): 42.0,"}],"source_content_type":"text/x-python","patch_set":28,"id":"ab509a03_df8db533","line":316,"updated":"2026-05-27 05:49:00.000000000","message":"as best I can tell this is the only place `_update_stat_internal` is used","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":313,"context_line":"        client._update_stat_internal(\u0027simple_metric\u0027, 42)"},{"line_number":314,"context_line":"        client._update_stat_internal(\u0027labeled_metric\u0027, 100, {\u0027label\u0027: \u0027value\u0027})"},{"line_number":315,"context_line":"        client._update_stat_internal(\u0027string_metric\u0027, 55.0, {\u0027type\u0027: \u0027test\u0027})"},{"line_number":316,"context_line":"        client._update_stat_internal(\u0027dict_metric\u0027, 99, {\u0027key\u0027: \u0027val\u0027})"},{"line_number":317,"context_line":"        client.write_all_stats(filename\u003dfilename)"},{"line_number":318,"context_line":"        self.assertEqual(read_metrics(filename), {"},{"line_number":319,"context_line":"            metric_key(\u0027simple_metric\u0027): 42.0,"}],"source_content_type":"text/x-python","patch_set":28,"id":"470c793e_059fe8c0","line":316,"in_reply_to":"ab509a03_df8db533","updated":"2026-06-03 16:14:27.000000000","message":"Noted and changed to make it useful!","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":323,"context_line":"        })"},{"line_number":324,"context_line":""},{"line_number":325,"context_line":"    @with_tempdir"},{"line_number":326,"context_line":"    def test_write_all_stats_no_stats_raises(self, tempdir):"},{"line_number":327,"context_line":"        filename \u003d os.path.join(tempdir, \u0027metrics.txt\u0027)"},{"line_number":328,"context_line":"        client \u003d get_prometheus_client({})"},{"line_number":329,"context_line":"        with self.assertRaises(RuntimeError) as ctx:"}],"source_content_type":"text/x-python","patch_set":28,"id":"18308b4e_360a446d","line":326,"updated":"2026-05-27 05:49:00.000000000","message":"this test doesn\u0027t really *justify* this behavior.\n\nWhy not:\n\n`def test_write_all_stats_no_stats_creates_empty_file`","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":323,"context_line":"        })"},{"line_number":324,"context_line":""},{"line_number":325,"context_line":"    @with_tempdir"},{"line_number":326,"context_line":"    def test_write_all_stats_no_stats_raises(self, tempdir):"},{"line_number":327,"context_line":"        filename \u003d os.path.join(tempdir, \u0027metrics.txt\u0027)"},{"line_number":328,"context_line":"        client \u003d get_prometheus_client({})"},{"line_number":329,"context_line":"        with self.assertRaises(RuntimeError) as ctx:"}],"source_content_type":"text/x-python","patch_set":28,"id":"ccf0c41d_c1b5e459","line":326,"in_reply_to":"18308b4e_360a446d","updated":"2026-06-03 16:14:27.000000000","message":"Done","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"1fab2d63aafb4b13ba01a94a960be5eb6df723a2","unresolved":true,"context_lines":[{"line_number":335,"context_line":"        client.record(\u0027cpu\u0027, 1.0)"},{"line_number":336,"context_line":"        with self.assertRaises(RuntimeError) as ctx:"},{"line_number":337,"context_line":"            client.write_all_stats()"},{"line_number":338,"context_line":"        self.assertIn(\u0027filename\u0027, str(ctx.exception))"},{"line_number":339,"context_line":""},{"line_number":340,"context_line":"    def test_user_labels_from_config(self):"},{"line_number":341,"context_line":"        client \u003d get_prometheus_client({"}],"source_content_type":"text/x-python","patch_set":28,"id":"5a66ab4d_c7929093","line":338,"updated":"2026-05-27 05:49:00.000000000","message":"I didn\u0027t find this `assertIn` particularly convincing\n\n```\n\u003e\u003e\u003e stats.write_all_stats()\nTraceback (most recent call last):\n  File \"\u003cstdin\u003e\", line 1, in \u003cmodule\u003e\n  File \"/vagrant/swift/swift/common/prom_metrics.py\", line 210, in write_all_stats\n    raise RuntimeError(\u0027please supply a filename\u0027)\nRuntimeError: please supply a filename\n```\n\nThe please is rather nice 😄\n\nperhaps \"supply a filename\" or \"filename required\" would have been more obvious error context for the assertion.","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"a31bc469cfb3297b5dc837bce59a5281e10b2251","unresolved":false,"context_lines":[{"line_number":335,"context_line":"        client.record(\u0027cpu\u0027, 1.0)"},{"line_number":336,"context_line":"        with self.assertRaises(RuntimeError) as ctx:"},{"line_number":337,"context_line":"            client.write_all_stats()"},{"line_number":338,"context_line":"        self.assertIn(\u0027filename\u0027, str(ctx.exception))"},{"line_number":339,"context_line":""},{"line_number":340,"context_line":"    def test_user_labels_from_config(self):"},{"line_number":341,"context_line":"        client \u003d get_prometheus_client({"}],"source_content_type":"text/x-python","patch_set":28,"id":"745e0b1b_ec1c67c5","line":338,"in_reply_to":"5a66ab4d_c7929093","updated":"2026-06-03 16:14:27.000000000","message":"Acknowledged","commit_id":"f1e49db86dfba5732e65403c0b82a0b42e5a214a"},{"author":{"_account_id":7233,"name":"Matthew Oliver","email":"matt@oliver.net.au","username":"mattoliverau"},"change_message_id":"4a4577c4eaa5ddc710daffe26b37b38f3c602a01","unresolved":true,"context_lines":[{"line_number":316,"context_line":"        client \u003d get_prometheus_client({})"},{"line_number":317,"context_line":"        with self.assertRaises(RuntimeError) as ctx:"},{"line_number":318,"context_line":"            client.write_all_stats(filename\u003dfilename)"},{"line_number":319,"context_line":"        self.assertIn(\u0027no stats to write\u0027, str(ctx.exception))"},{"line_number":320,"context_line":""},{"line_number":321,"context_line":"    def test_write_all_stats_no_filename_raises(self):"},{"line_number":322,"context_line":"        client \u003d get_prometheus_client({})"}],"source_content_type":"text/x-python","patch_set":30,"id":"ccac768a_9b45586d","line":319,"updated":"2026-06-11 01:42:14.000000000","message":"The test name suggests it creates an empty file, does it? if it does should we assert it exists and is empty?","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":35790,"name":"Shreeya Deshpande","email":"shreeyad@nvidia.com","username":"shreeyad"},"change_message_id":"d15ab17764d2885a256a3427290ca9ce3f9d92f5","unresolved":false,"context_lines":[{"line_number":316,"context_line":"        client \u003d get_prometheus_client({})"},{"line_number":317,"context_line":"        with self.assertRaises(RuntimeError) as ctx:"},{"line_number":318,"context_line":"            client.write_all_stats(filename\u003dfilename)"},{"line_number":319,"context_line":"        self.assertIn(\u0027no stats to write\u0027, str(ctx.exception))"},{"line_number":320,"context_line":""},{"line_number":321,"context_line":"    def test_write_all_stats_no_filename_raises(self):"},{"line_number":322,"context_line":"        client \u003d get_prometheus_client({})"}],"source_content_type":"text/x-python","patch_set":30,"id":"3c3fc577_589022e6","line":319,"in_reply_to":"ccac768a_9b45586d","updated":"2026-07-13 13:33:38.000000000","message":"Done","commit_id":"31a1f30322a2748ca38cc9639bbbfde6298fd897"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":104,"context_line":"        with open(filename, \u0027w\u0027) as f:"},{"line_number":105,"context_line":"            f.write(\u0027metric_one 1\\n\u0027)"},{"line_number":106,"context_line":"            f.write(\u0027\\n\u0027)"},{"line_number":107,"context_line":"            f.write(\u0027   \\n\u0027)"},{"line_number":108,"context_line":"            f.write(\u0027metric_two 2\\n\u0027)"},{"line_number":109,"context_line":""},{"line_number":110,"context_line":"        metrics \u003d read_metrics(filename)"}],"source_content_type":"text/x-python","patch_set":40,"id":"2ceceafb_d6237a57","line":107,"updated":"2026-07-21 02:05:10.000000000","message":"i like this test","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":105,"context_line":"            f.write(\u0027metric_one 1\\n\u0027)"},{"line_number":106,"context_line":"            f.write(\u0027\\n\u0027)"},{"line_number":107,"context_line":"            f.write(\u0027   \\n\u0027)"},{"line_number":108,"context_line":"            f.write(\u0027metric_two 2\\n\u0027)"},{"line_number":109,"context_line":""},{"line_number":110,"context_line":"        metrics \u003d read_metrics(filename)"},{"line_number":111,"context_line":""}],"source_content_type":"text/x-python","patch_set":40,"id":"0489b9ab_d24421a6","line":108,"updated":"2026-07-21 02:05:10.000000000","message":"maybe double `\\n\\n` or even ` \\r\\n` if we want to get crazy!?","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":157,"context_line":"            str(ctx.exception),"},{"line_number":158,"context_line":"            \"Invalid Prometheus format: \u0027metric_without_value\u0027 - \""},{"line_number":159,"context_line":"            \"expected format \u0027metric_name value\u0027 or \""},{"line_number":160,"context_line":"            \"\u0027metric_name{labels} value\u0027\")"},{"line_number":161,"context_line":""},{"line_number":162,"context_line":""},{"line_number":163,"context_line":"class TestPrometheusParsing(unittest.TestCase):"}],"source_content_type":"text/x-python","patch_set":40,"id":"7dc7b680_71b0553b","line":160,"updated":"2026-07-21 02:05:10.000000000","message":"seems reasonable!","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":194,"context_line":"            \u0027swift_metric{reason\u003d\"read \\\\\"EOF\\\\\"\\\\nretry \\\\\\\\ again\"} 1\u0027)"},{"line_number":195,"context_line":"        self.assertEqual(name, \u0027swift_metric\u0027)"},{"line_number":196,"context_line":"        self.assertEqual(labels, {"},{"line_number":197,"context_line":"            \u0027reason\u0027: \u0027read \"EOF\"\\nretry \\\\ again\u0027,"},{"line_number":198,"context_line":"        })"},{"line_number":199,"context_line":"        self.assertEqual(value, 1.0)"},{"line_number":200,"context_line":""}],"source_content_type":"text/x-python","patch_set":40,"id":"216e1c91_7c1aef0c","line":197,"updated":"2026-07-21 02:05:10.000000000","message":"i have no idea if this is correct parsing - obviously we can\u0027t *write* a label name with `\u0027\"\u0027` without escaping it somehow, but I don\u0027t know where the rules are.","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":245,"context_line":"        with self.assertRaises(ValueError) as ctx:"},{"line_number":246,"context_line":"            parse_metrics(\u0027metric{label\u003dvalue} 42\u0027)"},{"line_number":247,"context_line":"        self.assertIn(\u0027Invalid Prometheus label value\u0027, str(ctx.exception))"},{"line_number":248,"context_line":"        self.assertIn(\u0027must be quoted\u0027, str(ctx.exception))"},{"line_number":249,"context_line":""},{"line_number":250,"context_line":"    def test_parse_metrics_unterminated_label_value(self):"},{"line_number":251,"context_line":"        with self.assertRaises(ValueError) as ctx:"}],"source_content_type":"text/x-python","patch_set":40,"id":"066e9c60_c80b47c8","line":248,"updated":"2026-07-21 02:05:10.000000000","message":"why these parser errors are getting specific\n\nto be 100% clear tho - we don\u0027t have a in-tree use-case for *reading* the prometheus metric file format tho - currently we just want to be able to *write* metics and then correctly parse back the ones we write for verifiction?","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":260,"context_line":"    def test_parse_metrics_duplicate_label_key(self):"},{"line_number":261,"context_line":"        with self.assertRaises(ValueError) as ctx:"},{"line_number":262,"context_line":"            parse_metrics(\u0027metric{label\u003d\"old\",label\u003d\"new\"} 42\u0027)"},{"line_number":263,"context_line":"        self.assertIn(\u0027Duplicate Prometheus label key\u0027, str(ctx.exception))"},{"line_number":264,"context_line":""},{"line_number":265,"context_line":"    def test_parse_metrics_invalid_metric_name(self):"},{"line_number":266,"context_line":"        with self.assertRaises(ValueError) as ctx:"}],"source_content_type":"text/x-python","patch_set":40,"id":"bfc7da29_ba68fa51","line":263,"updated":"2026-07-21 02:05:10.000000000","message":"oic, so probably the duplicate handling has nothing to do with user-labels?\n\nit seems like parsing and writing should be different","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":323,"context_line":"        labels \u003d {\u0027location\u0027: \u0027room1\u0027, \u0027unit\u0027: \u0027celsius\u0027}"},{"line_number":324,"context_line":"        result \u003d client.record(\u0027room_temperature\u0027, 22.0, labels)"},{"line_number":325,"context_line":"        self.assertEqual(result, metric_key("},{"line_number":326,"context_line":"            \u0027room_temperature\u0027, {\u0027location\u0027: \u0027room1\u0027, \u0027unit\u0027: \u0027celsius\u0027}))"},{"line_number":327,"context_line":"        client.write_all_stats(filename\u003dfilename)"},{"line_number":328,"context_line":"        self.assertEqual(read_metrics(filename), {"},{"line_number":329,"context_line":"            metric_key(\u0027room_temperature\u0027,"}],"source_content_type":"text/x-python","patch_set":40,"id":"ac720c8f_274648ac","line":326,"updated":"2026-07-21 02:05:10.000000000","message":"for what purpose does record return the `metric_key` instead of just `None`","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":418,"context_line":"        self.assertEqual(read_metrics(filename), {"},{"line_number":419,"context_line":"            metric_key(\u0027metric1\u0027, {}): 100.0,"},{"line_number":420,"context_line":"            metric_key(\u0027metric2\u0027, {\u0027label\u0027: \u0027value\u0027}): 200.0,"},{"line_number":421,"context_line":"            metric_key(\u0027metric3\u0027, {}): 300.0,"},{"line_number":422,"context_line":"        })"},{"line_number":423,"context_line":"        self.assertEqual(os.stat(filename).st_mode \u0026 0o777, 0o644)"},{"line_number":424,"context_line":""}],"source_content_type":"text/x-python","patch_set":40,"id":"59bf2cee_b378829f","line":421,"updated":"2026-07-21 02:05:10.000000000","message":"I think this is where I would add some tests for metrics that have the same name and only differ by label:\n\n```\nhttp_request{status\u003d200} 10\nhttp_request{status\u003d503} 2\n```","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":535,"context_line":"                labels\u003d{\u0027user_service\u0027: \u0027caller-service\u0027})"},{"line_number":536,"context_line":""},{"line_number":537,"context_line":"        self.assertIn(\"Duplicate Prometheus label key: \u0027user_service\u0027\","},{"line_number":538,"context_line":"                      str(ctx.exception))"},{"line_number":539,"context_line":"        self.assertEqual({}, client._stats)"},{"line_number":540,"context_line":""},{"line_number":541,"context_line":"    def test_invalid_label_name_in_config(self):"}],"source_content_type":"text/x-python","patch_set":40,"id":"c5c0c32b_1ef05679","line":538,"updated":"2026-07-21 02:05:10.000000000","message":"see I don\u0027t think the error is that it\u0027s a *duplicate* key (that\u0027s probably what was confusing my review agent) - the error is \"callers really shouldn\u0027t be passing in labels from the user_* namespace; that should be reserved for configured user labels\"\n\n... and yet here we have a test showing it\u0027s expected behavior to raise a duplicate key error if and only if the caller interlopes into the user label namespace AND the user happen to have needed that specific part of the user label namespace\n\nI wouldn\u0027t have expected it would be worth it - but I suppose I could get behind a:\n\n```\nif any(k.startswith(USER_PREFX) for k in labels):\n    raise(\u0027stay in your lane!\u0027)\n```","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"},{"author":{"_account_id":1179,"name":"Clay Gerrard","email":"clay.gerrard@gmail.com","username":"clay-gerrard"},"change_message_id":"42577a7751a10bb8ed523443b858dd9c0348ce21","unresolved":true,"context_lines":[{"line_number":554,"context_line":"        self.assertEqual("},{"line_number":555,"context_line":"            client.get_all_stats(),"},{"line_number":556,"context_line":"            \u0027requests_total{user_endpoint\u003d\"/api\",\u0027"},{"line_number":557,"context_line":"            \u0027user_reason\u003d\"read \\\\\"EOF\\\\\" \\\\\\\\ retry\",\u0027"},{"line_number":558,"context_line":"            \u0027user_route\u003d\"a,b\"} 1\\n\u0027)"},{"line_number":559,"context_line":""},{"line_number":560,"context_line":"    def test_record_no_filename_updates_stats_only(self):"}],"source_content_type":"text/x-python","patch_set":40,"id":"56b90dba_1a186cd6","line":557,"updated":"2026-07-21 02:05:10.000000000","message":"```\n\u003e\u003e\u003e json.loads(\u0027\"read \\\\\"EOF\\\\\" \\\\\\\\ retry\"\u0027)\n\u0027read \"EOF\" \\\\ retry\u0027\n```\n\nthere is almost no way I would have attempted to write this code for fear of it being incorrect and full of bugs - or maybe the REAL fear is that someone would have to REVIEW it","commit_id":"accfef51e26f501b25ad8252adbd26d29976e1a0"}]}
