Skip to content

Commit 213927e

Browse files
Make table generation conflicts easier to debug (with colors!) (#121)
Co-authored-by: Joao Grassi <[email protected]>
1 parent 29a0e97 commit 213927e

File tree

9 files changed

+236
-7
lines changed

9 files changed

+236
-7
lines changed

semantic-conventions/Dockerfile

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ RUN apk --update add --virtual build-dependencies build-base \
77
&& pip install -U ./semconvgen-*.whl \
88
&& apk del build-dependencies \
99
&& rm *.whl
10+
ENV COLORED_DIFF true
1011
ENTRYPOINT ["gen-semconv"]

semantic-conventions/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ The image also supports customising
117117
[Whitespace Control in Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/#whitespace-control)
118118
via the additional flag `--trim-whitespace`. Providing the flag will enable both `lstrip_blocks` and `trim_blocks`.
119119

120+
### Enabling/disabling support for colored diffs in error messages
121+
The `COLORED_DIFF` environment variable is set in the `semantic-conventions` `Dockerfile`. When this environment varibale is set, errors related to reformatting tables will show a "colored diff" using standard ANSI control characters. While this should be supported natively in any modern terminal environment, you may unset this variable if issues arise. Doing so will enable a "fall back" of non-colored inline diffs showing what was "added" and what was "removed", followed by the exact tokens added/removed encased in single quotes.
122+
120123
## Version compatibility check
121124

122125
You can check compatibility between the local one specified with `--yaml-root` and sepcific OpenTelemetry semantic convention version using the following command:

semantic-conventions/src/opentelemetry/semconv/templating/markdown/__init__.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
from opentelemetry.semconv.model.utils import ID_RE
4040
from opentelemetry.semconv.templating.markdown.options import MarkdownOptions
4141

42+
from .utils import VisualDiffer
43+
4244
_REQUIREMENT_LEVEL_URL = (
4345
"https://opentelemetry.io/docs/specs/semconv/general/attribute-requirement-level/"
4446
)
@@ -392,12 +394,11 @@ def render_md(self):
392394
output = io.StringIO()
393395
self._render_single_file(content, md_filename, output)
394396
if self.options.check_only:
395-
if content != output.getvalue():
396-
sys.exit(
397-
"File "
398-
+ md_filename
399-
+ " contains a table that would be reformatted."
400-
)
397+
output_value = output.getvalue()
398+
if content != output_value:
399+
diff = VisualDiffer.visual_diff(content, output_value)
400+
err_msg = f"File {md_filename} contains a table that would be reformatted.\n{diff}"
401+
sys.exit(err_msg)
401402
else:
402403
with open(md_filename, "w", encoding="utf-8") as md_file:
403404
md_file.write(output.getvalue())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import difflib
16+
import os
17+
18+
19+
class VisualDiffer:
20+
"""Colorize differential outputs, which can be useful in development of
21+
semantic conventions
22+
"""
23+
24+
@staticmethod
25+
def colorize_text(r: int, g: int, b: int, text: str):
26+
"""
27+
Colorize text according to ANSI standards
28+
The way this works is we send out a control character,
29+
then send the color information (the r,g,b parts),
30+
then the normal text, then an escape char to end the coloring
31+
32+
## Breakdown of magic values
33+
33 (octal) == 1b (hexadecimal) == ESC control character
34+
35+
ESC[38 => This sets foreground color
36+
https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
37+
38+
ESC[38;2; => Set foreground color with 24-bit color mode
39+
https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit
40+
41+
ESC[0m => Resets foreground color (basically turn off "coloring mode")
42+
43+
{r};{g};{b};m => Sets the color mode. "m" denotes the end of the escape sequence prefix
44+
45+
For more information and colors, see
46+
https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
47+
"""
48+
escape_color_24bitmode = "\x1b[38;2"
49+
reset_color = "\x1b[0m"
50+
return f"{escape_color_24bitmode};{r};{g};{b}m{text}{reset_color}"
51+
52+
@classmethod
53+
def removed(cls, text: str) -> str:
54+
return cls.colorize_text(255, 0, 0, text)
55+
56+
@classmethod
57+
def added(cls, text: str) -> str:
58+
return cls.colorize_text(0, 255, 0, text)
59+
60+
@classmethod
61+
def visual_diff(cls, a: str, b: str):
62+
"""
63+
Prints git-like colored diff using ANSI terminal coloring.
64+
Diff is "from a to b", that is, red text is text deleted in `a`
65+
while green text is new to `b`
66+
"""
67+
if "true" != os.environ.get("COLORED_DIFF", "false").lower():
68+
return "".join(difflib.context_diff(a, b))
69+
70+
colored_diff = []
71+
diff_partitions = difflib.SequenceMatcher(None, a, b)
72+
for operation, a_start, a_end, b_start, b_end in diff_partitions.get_opcodes():
73+
if operation == "equal":
74+
colored_diff.append(a[a_start:a_end])
75+
elif operation == "insert":
76+
colored_diff.append(cls.added(b[b_start:b_end]))
77+
elif operation == "delete":
78+
colored_diff.append(cls.removed(a[a_start:a_end]))
79+
elif operation == "replace":
80+
colored_diff.append(cls.added(b[b_start:b_end]))
81+
colored_diff.append(cls.removed(a[a_start:a_end]))
82+
else:
83+
# Log.warn would be best here
84+
raise ValueError(
85+
f"Unhandled opcode from difflib in semantic conversion markdown renderer: {operation}"
86+
)
87+
return "".join(colored_diff)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
***
2+
---
3+
***************
4+
*** 145,157 ****
5+
A t r i b u t e- | --- 145,157 ----
6+
A t+ t r i b u t e | ***************
7+
*** 176,181 ****
8+
--- 176,182 ----
9+
o n + | E***************
10+
*** 243,248 ****
11+
--- 244,250 ----
12+
- - -+ - | ***************
13+
*** 280,292 ****
14+
| ` b a r- .- f- o- o ` |--- 282,294 ----
15+
| `+ f+ o+ o+ . b a r ` |***************
16+
*** 311,316 ****
17+
--- 313,319 ----
18+
s u m+ z |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# TestTable
2+
3+
**Status**: [Experimental](../../../document-status.md)
4+
5+
**type:** `foo.bar`
6+
7+
**Description:** Test case
8+
9+
<!-- semconv redis -->
10+
11+
| Attribute  | Type | Description  | Examples | Requirement Level |
12+
| --------- | ------ | ------------ | -------- | ----------------- |
13+
| `foo.bar.foo` | string | Lorem Ipsumz | no | Recommended |
14+
15+
<!-- endsemconv -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# TestTable
2+
3+
**Status**: [Experimental](../../../document-status.md)
4+
5+
**type:** `foo.bar`
6+
7+
**Description:** Test case
8+
9+
<!-- semconv redis -->
10+
11+
| Atribute | Type | Description | Examples | Requirement Level |
12+
| --------- | ------ | ----------- | -------- | ----------------- |
13+
| `bar.foo` | string | Lorem Ipsum | no | Recommended |
14+
15+
<!-- endsemconv -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# TestTable
2+
3+
**Status**: [Experimental](../../../document-status.md)
4+
5+
**type:** `foo.bar`
6+
7+
**Description:** Test case
8+
9+
<!-- semconv redis -->
10+
11+
| Attribute | Type | Description | Examples | Requirement Level |
12+
| --------- | ------ | ------------ | -------- | ----------------- |
13+
| `foo.bar` | string | Lorem Ipsumz | no | Recommended |
14+
15+
<!-- endsemconv -->

semantic-conventions/src/tests/semconv/templating/test_markdown.py

+75-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
import unittest
1818
from pathlib import Path
1919
from typing import Optional, Sequence
20+
from unittest.mock import patch
2021

2122
from opentelemetry.semconv.model.semantic_convention import SemanticConventionSet
22-
from opentelemetry.semconv.templating.markdown import MarkdownRenderer
23+
from opentelemetry.semconv.templating.markdown import MarkdownRenderer, VisualDiffer
2324
from opentelemetry.semconv.templating.markdown.options import MarkdownOptions
2425

2526

@@ -168,6 +169,79 @@ def test_attribute_templates(self):
168169
def test_sorting(self):
169170
self.check("markdown/sorting/")
170171

172+
def testVisualDiffer(self):
173+
with open(
174+
self.get_file_path("markdown/table_generation_conflict/input-1.md"),
175+
encoding="utf8",
176+
) as fin:
177+
sample_1 = fin.read()
178+
with open(
179+
self.get_file_path("markdown/table_generation_conflict/input-2.md"),
180+
encoding="utf8",
181+
) as fin:
182+
sample_2 = fin.read()
183+
with open(
184+
self.get_file_path(
185+
"markdown/table_generation_conflict/expected-no-colors.md"
186+
),
187+
encoding="utf8",
188+
) as fin:
189+
expected = fin.read()
190+
actual = VisualDiffer.visual_diff(sample_1, sample_2)
191+
with open(
192+
self.get_file_path(
193+
"markdown/table_generation_conflict/expected-no-colors.md"
194+
),
195+
"w+",
196+
encoding="utf8",
197+
) as out:
198+
out.writelines(actual)
199+
self.assertEqual(expected, actual)
200+
201+
@patch.dict(os.environ, {"COLORED_DIFF": "false"})
202+
def testVisualDifferExplicitNoColors(self):
203+
with open(
204+
self.get_file_path("markdown/table_generation_conflict/input-1.md"),
205+
encoding="utf8",
206+
) as fin:
207+
sample_1 = fin.read()
208+
with open(
209+
self.get_file_path("markdown/table_generation_conflict/input-2.md"),
210+
encoding="utf8",
211+
) as fin:
212+
sample_2 = fin.read()
213+
with open(
214+
self.get_file_path(
215+
"markdown/table_generation_conflict/expected-no-colors.md"
216+
),
217+
encoding="utf8",
218+
) as fin:
219+
expected = fin.read()
220+
actual = VisualDiffer.visual_diff(sample_1, sample_2)
221+
self.assertEqual(expected, actual)
222+
223+
@patch.dict(os.environ, {"COLORED_DIFF": "true"})
224+
def testColoredVisualDiffer(self):
225+
with open(
226+
self.get_file_path("markdown/table_generation_conflict/input-1.md"),
227+
encoding="utf8",
228+
) as fin:
229+
sample_1 = fin.read()
230+
with open(
231+
self.get_file_path("markdown/table_generation_conflict/input-2.md"),
232+
encoding="utf8",
233+
) as fin:
234+
sample_2 = fin.read()
235+
with open(
236+
self.get_file_path(
237+
"markdown/table_generation_conflict/expected-with-colors.md",
238+
),
239+
encoding="utf8",
240+
) as fin:
241+
expected = fin.read()
242+
actual = VisualDiffer.visual_diff(sample_1, sample_2)
243+
self.assertEqual(expected, actual)
244+
171245
def check(
172246
self,
173247
input_dir: str,

0 commit comments

Comments
 (0)