promtext-cli 0.1.2.dev97__tar.gz → 0.1.2.dev100__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/.copier-answers.yml +2 -2
  2. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/.pre-commit-config.yaml +1 -1
  3. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/PKG-INFO +1 -1
  4. promtext_cli-0.1.2.dev100/promtext_cli/main.py +12 -0
  5. promtext_cli-0.1.2.dev100/promtext_cli/promtext.py +184 -0
  6. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/pyproject.toml +24 -2
  7. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/tests/test_blackbox.py +6 -13
  8. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/tests/test_functional.py +52 -58
  9. promtext_cli-0.1.2.dev97/promtext_cli/main.py +0 -152
  10. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/.commitlintrc.yaml +0 -0
  11. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/.forgejo/workflows/commitlint.yaml +0 -0
  12. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/.forgejo/workflows/lint.yaml +0 -0
  13. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/.forgejo/workflows/publish.yaml +0 -0
  14. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/.forgejo/workflows/test.yaml +0 -0
  15. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/.gitignore +0 -0
  16. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/README.md +0 -0
  17. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/cliff.toml +0 -0
  18. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/promtext_cli/__init__.py +0 -0
  19. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/renovate.json +0 -0
  20. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/tests/.gitkeep +0 -0
  21. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev100}/uv.lock +0 -0
@@ -1,5 +1,5 @@
1
1
  # Changes here will be overwritten by Copier.
2
- _commit: 0.8.5
2
+ _commit: 0.9.0
3
3
  _src_path: https://codeberg.org/margau/copier-python-uv.git
4
4
  code_dir: promtext_cli
5
5
  container: false
@@ -14,4 +14,4 @@ project_name: promtext-cli
14
14
  pypi_publish: true
15
15
  pypi_publish_dev: true
16
16
  scripts:
17
- promtext: promtext_cli.main:promtext
17
+ promtext: promtext_cli:main.main
@@ -10,7 +10,7 @@ repos:
10
10
  stages: [commit-msg]
11
11
  args: []
12
12
  - repo: https://github.com/astral-sh/ruff-pre-commit
13
- rev: v0.15.13
13
+ rev: v0.15.14
14
14
  hooks:
15
15
  # Run the linter.
16
16
  - id: ruff-check
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: promtext-cli
3
- Version: 0.1.2.dev97
3
+ Version: 0.1.2.dev100
4
4
  Summary: Prometheus Textfile Tooling
5
5
  Project-URL: Documentation, https://codeberg.org/margau/promtext-cli/src/branch/main#readme
6
6
  Project-URL: Issues, https://codeberg.org/margau/promtext-cli/issues
@@ -0,0 +1,12 @@
1
+ """Provide the Entrypoints"""
2
+
3
+ from .promtext import Promtext
4
+
5
+
6
+ def main():
7
+ """Main method invoking the core class"""
8
+ Promtext().cli_entrypoint()
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1,184 @@
1
+ """module providing the core functionality"""
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from prometheus_client import CollectorRegistry, Gauge, write_to_textfile
9
+ from prometheus_client.parser import text_string_to_metric_families
10
+
11
+
12
+ class Promtext:
13
+ """Class for the core functionality"""
14
+
15
+ def __init__(self):
16
+ self.textfile = None
17
+ self.logger = None
18
+ self.args = None
19
+ self.registry = CollectorRegistry()
20
+
21
+ self.metrics = {}
22
+
23
+ def _arguments(self):
24
+ """Load every input item from arguments & define cli"""
25
+ # required file first
26
+ parser = argparse.ArgumentParser(description="Prometheus textfile helper")
27
+ parser.add_argument(
28
+ "filename",
29
+ type=str,
30
+ help="Path to existing or new prometheus textfile, will be updated",
31
+ )
32
+
33
+ # metric name, required
34
+ parser.add_argument("metric", type=str, help="metric name (new or updated)")
35
+
36
+ # metric value as int/float, required
37
+ parser.add_argument("value", type=float, help="metric value")
38
+
39
+ # metric documentation as optional argument
40
+ parser.add_argument(
41
+ "--docs",
42
+ type=str,
43
+ help="metric documentation",
44
+ default="metric appended by promtext-cli",
45
+ )
46
+
47
+ # labels, key-value, minimum 0, repeatable
48
+ parser.add_argument(
49
+ "--label",
50
+ metavar="KEY=VALUE",
51
+ help="label key=value pairs",
52
+ action="append",
53
+ )
54
+
55
+ # log level from argparse
56
+ parser.add_argument(
57
+ "-v",
58
+ "--verbose",
59
+ action="store_const",
60
+ dest="loglevel",
61
+ const=logging.INFO,
62
+ )
63
+ self.args = parser.parse_args()
64
+
65
+ # processing: check if file is available, if yes, parse
66
+ self.textfile = Path(self.args.filename)
67
+
68
+ def parse_file(self):
69
+ """If possible, convert the input textfile to metrics in the registry"""
70
+ # check if self.args.filename exists with pathlib
71
+ if self.textfile.is_file():
72
+ for f in text_string_to_metric_families(
73
+ self.textfile.read_text(encoding="utf-8"),
74
+ ):
75
+ # per metric: iterate over samples, create metric in registry
76
+ m = False
77
+ samples = list(f.samples)
78
+ if len(samples) > 0:
79
+ # if we have samples, use the labelnames from them
80
+ labelnames = list(samples[0].labels.keys())
81
+ # metric-type specific init
82
+ if f.type == "gauge":
83
+ m = Gauge(
84
+ f.name,
85
+ f.documentation,
86
+ unit=f.unit,
87
+ labelnames=labelnames,
88
+ registry=self.registry,
89
+ )
90
+ else:
91
+ # we don't support other types yet, continue in these cases
92
+ self.logger.warning(
93
+ "unsupported metric type %s, dropping",
94
+ f.type,
95
+ )
96
+ continue
97
+ for s in samples:
98
+ if len(labelnames) > 0:
99
+ labelvalues = list(s.labels.values())
100
+ m.labels(*labelvalues).set(s.value)
101
+ else:
102
+ m.set(s.value)
103
+ self.metrics[f.name] = m
104
+ self.logger.info(
105
+ "copy gauge metric %s with labels %s from old file",
106
+ f.name,
107
+ ", ".join(labelnames),
108
+ )
109
+ else:
110
+ self.logger.warning(
111
+ "got empty metric %s from old file, dropping",
112
+ f.name,
113
+ )
114
+
115
+ def _build_metrics(self):
116
+ # add new metric from commandline
117
+ m = False
118
+
119
+ # figure out labelkey- and values
120
+ labels = {}
121
+ if self.args.label:
122
+ for lpair in self.args.label:
123
+ k, v = lpair.split("=")
124
+ labels[k] = v
125
+
126
+ labelvalues = []
127
+ # here, we use a new metric
128
+ if self.args.metric not in self.metrics:
129
+ self.logger.info("adding new metric %s", self.args.metric)
130
+ m = Gauge(
131
+ self.args.metric,
132
+ self.args.docs,
133
+ registry=self.registry,
134
+ labelnames=labels.keys(),
135
+ )
136
+ labelvalues = labels.values()
137
+ else:
138
+ m = self.metrics[self.args.metric]
139
+
140
+ # There is no way to access existing labelnames directly
141
+ # pylint: disable=W0212
142
+ old_labelnames = list(m._labelnames) # noqa: SLF001
143
+ for la in old_labelnames:
144
+ self.logger.info("processing label %s", la)
145
+ if la in labels: # labelvalues are needed in order!
146
+ labelvalues.append(labels[la])
147
+ else:
148
+ self.logger.error(
149
+ "previously known label '%s' missing, cannot update!",
150
+ la,
151
+ )
152
+ sys.exit(1)
153
+ if len(old_labelnames) != len(labels.keys()):
154
+ self.logger.error(
155
+ """labelnames for metric %s not the same, cannot update!
156
+ Old: %s, New: %s""",
157
+ self.args.metric,
158
+ old_labelnames,
159
+ list(labels.keys()),
160
+ )
161
+ sys.exit(1)
162
+ self.logger.info("updating metric %s", self.args.metric)
163
+
164
+ # actually set the value
165
+ if len(labelvalues) > 0:
166
+ m.labels(*labelvalues).set(self.args.value)
167
+ else:
168
+ m.set(self.args.value)
169
+
170
+ def output_file(self):
171
+ """Output to a textfile"""
172
+ write_to_textfile(self.textfile, self.registry)
173
+ self.logger.info("wrote to %s", self.textfile)
174
+
175
+ def cli_entrypoint(self):
176
+ """Main method called from the CLI"""
177
+ self._arguments()
178
+
179
+ logging.basicConfig(level=self.args.loglevel)
180
+ self.logger = logging.getLogger(__name__)
181
+
182
+ self.parse_file()
183
+ self._build_metrics()
184
+ self.output_file()
@@ -28,7 +28,7 @@ Source = "https://codeberg.org/margau/promtext-cli.git"
28
28
 
29
29
  # Scripts
30
30
  [project.scripts]
31
- promtext = "promtext_cli.main:promtext"
31
+ promtext = "promtext_cli:main.main"
32
32
 
33
33
  [dependency-groups]
34
34
  dev = [
@@ -43,6 +43,28 @@ dev = [
43
43
  cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true } }]
44
44
 
45
45
  [tool.coverage.run]
46
- source = ["src", "tests"]
46
+ source = ["promtext_cli", "tests"]
47
+
48
+ [tool.ruff.lint]
49
+ select = ["ALL"]
50
+ ignore = [
51
+ "D104",
52
+ "D107",
53
+ "D200",
54
+ "D203",
55
+ "D205",
56
+ "D213",
57
+ "D400",
58
+ "D401",
59
+ "D415", # TODO: Remove later
60
+ "FIX002", # TODO: Remove later
61
+ "TD002", # TODO: Remove later
62
+ "TD003", # TODO: Remove later
63
+ "ANN", # TODO: Remove later
64
+ "COM812",
65
+ ]
66
+
67
+ [tool.ruff.lint.per-file-ignores]
68
+ "tests/**.py" = ["S101", "INP001", "PLR2004"]
47
69
 
48
70
  # TODO: Dev/CI scripts for lint & co.
@@ -1,39 +1,32 @@
1
1
  # pylint: disable=R0801
2
- """This script does blackbox-like testing of the promtext cli tool"""
2
+ """Blackbox-like testing of the promtext cli tool"""
3
3
 
4
4
  import shutil
5
+
5
6
  from cli_test_helpers import shell
6
7
 
7
8
 
8
9
  def test_entrypoint():
9
- """
10
- Is entrypoint script installed? (pyproject.toml)
11
- """
10
+ """Is entrypoint script installed? (pyproject.toml)"""
12
11
  assert shutil.which("promtext")
13
12
 
14
13
 
15
14
  def test_help():
16
- """
17
- Does the help command work?
18
- """
15
+ """Does the help command work?"""
19
16
  result = shell("promtext --help")
20
17
  assert result.exit_code == 0
21
18
  assert "usage:" in result.stdout
22
19
 
23
20
 
24
21
  def test_empty():
25
- """
26
- Does the command fails with no arguments?
27
- """
22
+ """Does the command fails with no arguments?"""
28
23
  result = shell("promtext")
29
24
  assert result.exit_code == 2
30
25
  assert "usage:" in result.stderr
31
26
 
32
27
 
33
28
  def test_new_file(tmp_path):
34
- """
35
- Does the command create a new file with metrics content?
36
- """
29
+ """Does the command create a new file with metrics content?"""
37
30
  promfile = tmp_path / "new_file.prom"
38
31
  result = shell(f"promtext {promfile} test_metric 0")
39
32
  assert result.exit_code == 0
@@ -1,19 +1,17 @@
1
1
  # pylint: disable=R0801,W0511
2
- """This script does functional testing by calling the methods directly"""
2
+ """Functional testing by calling the methods directly"""
3
3
 
4
- from cli_test_helpers import ArgvContext
5
4
  import pytest
5
+ from cli_test_helpers import ArgvContext
6
6
 
7
- import promtext_cli.main
7
+ from promtext_cli.main import main as promtext_main
8
8
 
9
9
 
10
10
  def test_new_file(tmp_path):
11
- """
12
- Does the command create a new file with metrics content?
13
- """
11
+ """Does the command create a new file with metrics content?"""
14
12
  promfile = tmp_path / "new_file.prom"
15
13
  with ArgvContext("promtext", str(promfile), "test_metric", "0"):
16
- promtext_cli.main.promtext()
14
+ promtext_main()
17
15
  assert promfile.exists()
18
16
  assert promfile.read_text() == (
19
17
  "# HELP test_metric metric appended by promtext-cli"
@@ -26,14 +24,17 @@ def test_new_file(tmp_path):
26
24
 
27
25
 
28
26
  def test_labels(tmp_path):
29
- """
30
- Does the command create a new file with metrics content?
31
- """
27
+ """Does the command create a new file with metrics content?"""
32
28
  promfile = tmp_path / "label_file.prom"
33
29
  with ArgvContext(
34
- "promtext", "--label", "testlabel=testvalue", str(promfile), "test_metric", 0
30
+ "promtext",
31
+ "--label",
32
+ "testlabel=testvalue",
33
+ str(promfile),
34
+ "test_metric",
35
+ 0,
35
36
  ):
36
- promtext_cli.main.promtext()
37
+ promtext_main()
37
38
  assert promfile.exists()
38
39
  assert promfile.read_text() == (
39
40
  "# HELP test_metric metric appended by promtext-cli"
@@ -46,8 +47,7 @@ def test_labels(tmp_path):
46
47
 
47
48
 
48
49
  def test_existing_metric_append_metric(tmp_path):
49
- """
50
- Does the command append a new metric to an existing file
50
+ """Does the command append a new metric to an existing file
51
51
  with metrics content and preserve existing metrics?
52
52
  """
53
53
  promfile = tmp_path / "existing_file.prom"
@@ -57,12 +57,17 @@ def test_existing_metric_append_metric(tmp_path):
57
57
  "# TYPE existing_metric gauge"
58
58
  "\n"
59
59
  'existing_metric{testlabel="testvalue"} 0.0'
60
- "\n"
60
+ "\n",
61
61
  )
62
62
  with ArgvContext(
63
- "promtext", "--label", "testlabel=testvalue", str(promfile), "new_metric", 0
63
+ "promtext",
64
+ "--label",
65
+ "testlabel=testvalue",
66
+ str(promfile),
67
+ "new_metric",
68
+ 0,
64
69
  ):
65
- promtext_cli.main.promtext()
70
+ promtext_main()
66
71
  assert promfile.exists()
67
72
  assert promfile.read_text() == (
68
73
  "# HELP existing_metric metric appended by promtext-cli"
@@ -81,8 +86,7 @@ def test_existing_metric_append_metric(tmp_path):
81
86
 
82
87
 
83
88
  def test_existing_metric_append_labelvalue(tmp_path):
84
- """
85
- Does the command append a new metric to an existing file
89
+ """Does the command append a new metric to an existing file
86
90
  with metrics content and preserve existing metrics?
87
91
  """
88
92
  promfile = tmp_path / "existing_file.prom"
@@ -92,12 +96,17 @@ def test_existing_metric_append_labelvalue(tmp_path):
92
96
  "# TYPE existing_metric gauge"
93
97
  "\n"
94
98
  'existing_metric{testlabel="existing"} 0.0'
95
- "\n"
99
+ "\n",
96
100
  )
97
101
  with ArgvContext(
98
- "promtext", "--label", "testlabel=new", str(promfile), "existing_metric", 0
102
+ "promtext",
103
+ "--label",
104
+ "testlabel=new",
105
+ str(promfile),
106
+ "existing_metric",
107
+ 0,
99
108
  ):
100
- promtext_cli.main.promtext()
109
+ promtext_main()
101
110
  assert promfile.exists()
102
111
  assert promfile.read_text() == (
103
112
  "# HELP existing_metric metric appended by promtext-cli"
@@ -112,8 +121,7 @@ def test_existing_metric_append_labelvalue(tmp_path):
112
121
 
113
122
 
114
123
  def test_existing_metric_multilabel(tmp_path):
115
- """
116
- Does the command append a new metric to an existing file
124
+ """Does the command append a new metric to an existing file
117
125
  with metrics content and preserve existing metrics?
118
126
  """
119
127
  promfile = tmp_path / "existing_file.prom"
@@ -123,7 +131,7 @@ def test_existing_metric_multilabel(tmp_path):
123
131
  "# TYPE existing_metric gauge"
124
132
  "\n"
125
133
  'existing_metric{first="existing", second="existing"} 0.0'
126
- "\n"
134
+ "\n",
127
135
  )
128
136
  with ArgvContext(
129
137
  "promtext",
@@ -135,7 +143,7 @@ def test_existing_metric_multilabel(tmp_path):
135
143
  "existing_metric",
136
144
  "42",
137
145
  ):
138
- promtext_cli.main.promtext()
146
+ promtext_main()
139
147
  assert promfile.exists()
140
148
  assert promfile.read_text() == (
141
149
  "# HELP existing_metric metric appended by promtext-cli"
@@ -150,9 +158,7 @@ def test_existing_metric_multilabel(tmp_path):
150
158
 
151
159
 
152
160
  def test_existing_metric_overwrite(tmp_path):
153
- """
154
- Does the command overwrite a metric/labelset
155
- """
161
+ """Does the command overwrite a metric/labelset"""
156
162
  promfile = tmp_path / "existing_file.prom"
157
163
  promfile.write_text(
158
164
  "# HELP existing_metric metric appended by promtext-cli"
@@ -160,7 +166,7 @@ def test_existing_metric_overwrite(tmp_path):
160
166
  "# TYPE existing_metric gauge"
161
167
  "\n"
162
168
  'existing_metric{testlabel="existing"} 0.0'
163
- "\n"
169
+ "\n",
164
170
  )
165
171
  with ArgvContext(
166
172
  "promtext",
@@ -170,7 +176,7 @@ def test_existing_metric_overwrite(tmp_path):
170
176
  "existing_metric",
171
177
  "42",
172
178
  ):
173
- promtext_cli.main.promtext()
179
+ promtext_main()
174
180
  assert promfile.exists()
175
181
  assert promfile.read_text() == (
176
182
  "# HELP existing_metric metric appended by promtext-cli"
@@ -183,9 +189,7 @@ def test_existing_metric_overwrite(tmp_path):
183
189
 
184
190
 
185
191
  def test_existing_metric_plain(tmp_path):
186
- """
187
- Does the command overwrite a metric without a label?
188
- """
192
+ """Does the command overwrite a metric without a label?"""
189
193
  promfile = tmp_path / "existing_file.prom"
190
194
  promfile.write_text(
191
195
  "# HELP existing_metric metric appended by promtext-cli"
@@ -193,10 +197,10 @@ def test_existing_metric_plain(tmp_path):
193
197
  "# TYPE existing_metric gauge"
194
198
  "\n"
195
199
  "existing_metric 0.0"
196
- "\n"
200
+ "\n",
197
201
  )
198
202
  with ArgvContext("promtext", str(promfile), "existing_metric", "42"):
199
- promtext_cli.main.promtext()
203
+ promtext_main()
200
204
  assert promfile.exists()
201
205
  assert promfile.read_text() == (
202
206
  "# HELP existing_metric metric appended by promtext-cli"
@@ -209,9 +213,7 @@ def test_existing_metric_plain(tmp_path):
209
213
 
210
214
 
211
215
  def test_existing_metric_labeldrop(tmp_path, capsys):
212
- """
213
- missing labels are not possible, promtext should fail
214
- """
216
+ """Missing labels are not possible, promtext should fail"""
215
217
  promfile = tmp_path / "existing_file.prom"
216
218
  promfile.write_text(
217
219
  "# HELP existing_metric metric appended by promtext-cli"
@@ -219,14 +221,14 @@ def test_existing_metric_labeldrop(tmp_path, capsys):
219
221
  "# TYPE existing_metric gauge"
220
222
  "\n"
221
223
  'existing_metric{testlabel="existing"} 0.0'
222
- "\n"
224
+ "\n",
223
225
  )
224
226
 
225
227
  with (
226
228
  ArgvContext("promtext", str(promfile), "existing_metric", "42"),
227
229
  pytest.raises(SystemExit) as pytest_wrapped_e,
228
230
  ):
229
- promtext_cli.main.promtext()
231
+ promtext_main()
230
232
  assert promfile.exists()
231
233
  assert promfile.read_text() == (
232
234
  "# HELP existing_metric metric appended by promtext-cli"
@@ -240,16 +242,11 @@ def test_existing_metric_labeldrop(tmp_path, capsys):
240
242
  assert pytest_wrapped_e.type is SystemExit
241
243
  assert pytest_wrapped_e.value.code == 1
242
244
  captured = capsys.readouterr()
243
- assert (
244
- "ERROR:promtext_cli.main:previously known label 'testlabel' missing, cannot update!"
245
- in captured.err
246
- )
245
+ assert "previously known label 'testlabel' missing, cannot update!" in captured.err
247
246
 
248
247
 
249
248
  def test_existing_metric_labelchange(tmp_path, capsys):
250
- """
251
- changing labels are not possible, promtext should fail
252
- """
249
+ """Changing labels are not possible, promtext should fail"""
253
250
  promfile = tmp_path / "existing_file.prom"
254
251
  promfile.write_text(
255
252
  "# HELP existing_metric metric appended by promtext-cli"
@@ -257,7 +254,7 @@ def test_existing_metric_labelchange(tmp_path, capsys):
257
254
  "# TYPE existing_metric gauge"
258
255
  "\n"
259
256
  'existing_metric{existinglabel="existing"} 0.0'
260
- "\n"
257
+ "\n",
261
258
  )
262
259
 
263
260
  with (
@@ -271,7 +268,7 @@ def test_existing_metric_labelchange(tmp_path, capsys):
271
268
  ),
272
269
  pytest.raises(SystemExit) as pytest_wrapped_e,
273
270
  ):
274
- promtext_cli.main.promtext()
271
+ promtext_main()
275
272
  assert promfile.exists()
276
273
  assert promfile.read_text() == (
277
274
  "# HELP existing_metric metric appended by promtext-cli"
@@ -286,15 +283,12 @@ def test_existing_metric_labelchange(tmp_path, capsys):
286
283
  assert pytest_wrapped_e.value.code == 1
287
284
  captured = capsys.readouterr()
288
285
  assert (
289
- "ERROR:promtext_cli.main:previously known label 'existinglabel' missing, cannot update!"
290
- in captured.err
286
+ "previously known label 'existinglabel' missing, cannot update!" in captured.err
291
287
  )
292
288
 
293
289
 
294
290
  def test_existing_metric_labeladd(tmp_path, capsys):
295
- """
296
- adding new labels are not possible, promtext should fail
297
- """
291
+ """Adding new labels are not possible, promtext should fail"""
298
292
  promfile = tmp_path / "existing_file.prom"
299
293
  promfile.write_text(
300
294
  "# HELP existing_metric metric appended by promtext-cli"
@@ -302,7 +296,7 @@ def test_existing_metric_labeladd(tmp_path, capsys):
302
296
  "# TYPE existing_metric gauge"
303
297
  "\n"
304
298
  'existing_metric{existinglabel="existing"} 0.0'
305
- "\n"
299
+ "\n",
306
300
  )
307
301
 
308
302
  with (
@@ -318,7 +312,7 @@ def test_existing_metric_labeladd(tmp_path, capsys):
318
312
  ),
319
313
  pytest.raises(SystemExit) as pytest_wrapped_e,
320
314
  ):
321
- promtext_cli.main.promtext()
315
+ promtext_main()
322
316
  assert promfile.exists()
323
317
  assert promfile.read_text() == (
324
318
  "# HELP existing_metric metric appended by promtext-cli"
@@ -333,7 +327,7 @@ def test_existing_metric_labeladd(tmp_path, capsys):
333
327
  assert pytest_wrapped_e.value.code == 1
334
328
  captured = capsys.readouterr()
335
329
  assert (
336
- "ERROR:promtext_cli.main:labelnames for metric existing_metric not the same, cannot update"
330
+ "labelnames for metric existing_metric not the same, cannot update"
337
331
  in captured.err
338
332
  )
339
333
 
@@ -1,152 +0,0 @@
1
- """promtext_cli is providing a CLI to cleanly update prometheus textfiles from scripts"""
2
-
3
- # pylint: disable=C0116,R0914,R0912,R0915
4
- # this rules will be fixed by a object-oriented refactoring
5
-
6
- import argparse
7
- from pathlib import Path
8
- import logging
9
- import sys
10
-
11
- from prometheus_client.parser import text_string_to_metric_families
12
- from prometheus_client import CollectorRegistry, Gauge, write_to_textfile
13
-
14
-
15
- def promtext():
16
- # setup argpars
17
- # required file first
18
- parser = argparse.ArgumentParser(description="Prometheus textfile helper")
19
- parser.add_argument(
20
- "filename",
21
- type=str,
22
- help="Path to existing or new prometheus textfile, will be updated",
23
- )
24
-
25
- # metric name, required
26
- parser.add_argument("metric", type=str, help="metric name (new or updated)")
27
-
28
- # metric value as int/float, required
29
- parser.add_argument("value", type=float, help="metric value")
30
-
31
- # metric documentation as optional argument
32
- parser.add_argument(
33
- "--docs",
34
- type=str,
35
- help="metric documentation",
36
- default="metric appended by promtext-cli",
37
- )
38
-
39
- # labels, key-value, minimum 0, repeatable
40
- parser.add_argument(
41
- "--label", metavar="KEY=VALUE", help="label key=value pairs", action="append"
42
- )
43
-
44
- # log level from argparse
45
- parser.add_argument(
46
- "-v",
47
- "--verbose",
48
- action="store_const",
49
- dest="loglevel",
50
- const=logging.INFO,
51
- )
52
- args = parser.parse_args()
53
- logging.basicConfig(level=args.loglevel)
54
- logger = logging.getLogger(__name__)
55
-
56
- # processing: check if file is available, if yes, parse
57
- textfile = Path(args.filename)
58
-
59
- registry = CollectorRegistry()
60
- metrics = {}
61
-
62
- # check if args.filename exists with pathlib
63
- if textfile.is_file():
64
- for f in text_string_to_metric_families(textfile.read_text(encoding="utf-8")):
65
- # per metric: iterate over samples, create metric in registry
66
- m = False
67
- samples = []
68
- for s in f.samples:
69
- samples.append(s)
70
- if len(samples) > 0:
71
- # if we have samples, use the labelnames from them
72
- labelnames = list(samples[0].labels.keys())
73
- # metric-type specific init
74
- if f.type == "gauge":
75
- m = Gauge(
76
- f.name,
77
- f.documentation,
78
- unit=f.unit,
79
- labelnames=labelnames,
80
- registry=registry,
81
- )
82
- else:
83
- # we don't support other types yet, continue in these cases
84
- logger.warning("unsupported metric type %s, dropping", f.type)
85
- continue
86
- for s in samples:
87
- if len(labelnames) > 0:
88
- labelvalues = list(s.labels.values())
89
- m.labels(*labelvalues).set(s.value)
90
- else:
91
- m.set(s.value)
92
- metrics[f.name] = m
93
- logger.info(
94
- "copy gauge metric %s with labels %s from old file",
95
- f.name,
96
- ", ".join(labelnames),
97
- )
98
- else:
99
- logger.warning("got empty metric %s from old file, dropping", f.name)
100
-
101
- # add new metric from commandline
102
- m = False
103
-
104
- # figure out labelkey- and values
105
- labels = {}
106
- if args.label:
107
- for lpair in args.label:
108
- k, v = lpair.split("=")
109
- labels[k] = v
110
-
111
- labelvalues = []
112
- # here, we use a new metric
113
- if args.metric not in metrics:
114
- logger.info("adding new metric %s", args.metric)
115
- m = Gauge(args.metric, args.docs, registry=registry, labelnames=labels.keys())
116
- labelvalues = labels.values()
117
- else:
118
- m = metrics[args.metric]
119
-
120
- # There is no way to access existing labelnames directly
121
- # pylint: disable=W0212
122
- old_labelnames = list(m._labelnames)
123
- for la in old_labelnames:
124
- logger.info("processing label %s", la)
125
- if la in labels: # labelvalues are needed in order!
126
- labelvalues.append(labels[la])
127
- else:
128
- logger.error("previously known label '%s' missing, cannot update!", la)
129
- sys.exit(1)
130
- if len(old_labelnames) != len(labels.keys()):
131
- logger.error(
132
- "labelnames for metric %s not the same, cannot update! Old: %s, New: %s",
133
- args.metric,
134
- old_labelnames,
135
- list(labels.keys()),
136
- )
137
- sys.exit(1)
138
- logger.info("updating metric %s", args.metric)
139
-
140
- # actually set the value
141
- if len(labelvalues) > 0:
142
- m.labels(*labelvalues).set(args.value)
143
- else:
144
- m.set(args.value)
145
-
146
- # write to file
147
- write_to_textfile(args.filename, registry)
148
- logger.info("wrote to %s", args.filename)
149
-
150
-
151
- if __name__ == "__main__":
152
- promtext()