From 590787300f8b75097962d6f900915a8507e2eb08 Mon Sep 17 00:00:00 2001 From: Sarah Hom Date: Fri, 27 Feb 2026 10:57:03 -0600 Subject: [PATCH 1/6] AE-1236: initial code --- glean_parser/go_server.py | 38 ++++++++++++++++++++++++- glean_parser/templates/go_server.jinja2 | 22 ++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/glean_parser/go_server.py b/glean_parser/go_server.py index af4b5e50..c154e56e 100644 --- a/glean_parser/go_server.py +++ b/glean_parser/go_server.py @@ -38,6 +38,7 @@ "event", "datetime", "boolean", + "labeled_boolean", "string_list", ] @@ -66,6 +67,11 @@ def generate_metric_argument_name(metric: metrics.Metric) -> str: return f"{util.Camelize(metric.category)}{util.Camelize(metric.name)}" +def generate_labeled_boolean_type_name(metric: metrics.Metric) -> str: + """Generate the Go type name for a labeled_boolean metric.""" + return f"{util.Camelize(metric.category)}{util.Camelize(metric.name)}" + + def generate_metric_type(metric_type: str) -> str: if metric_type == "quantity": return "int64" @@ -87,6 +93,22 @@ def clean_string(s: str) -> str: return s.replace("\n", " ").rstrip() +def validate_labeled_boolean(metric: metrics.Metric) -> bool: + """ + Validate that a labeled_boolean metric has predefined labels. + + Returns: + bool: True if valid, False otherwise + """ + if not hasattr(metric, 'labels') or not metric.labels: + print( + "❌ Ignoring labeled_boolean metric without predefined labels: " + + f"{metric.name}. labeled_boolean requires a 'labels' list." + ) + return False + return True + + def output_go( objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] ) -> None: @@ -110,6 +132,7 @@ def output_go( ("event_extra_name", generate_extra_name), ("metric_name", generate_metric_name), ("metric_argument_name", generate_metric_argument_name), + ("labeled_boolean_type_name", generate_labeled_boolean_type_name), ("go_metric_type", generate_metric_type), ("clean_string", clean_string), ), @@ -117,6 +140,9 @@ def output_go( # unique list of event metrics used in any ping event_metrics: List[metrics.Metric] = [] + + # unique list of labeled_boolean metrics used in any ping + labeled_boolean_metrics: List[metrics.Metric] = [] # Go through all metrics in objs and build a map of # ping->list of metric categories->list of metrics @@ -134,9 +160,16 @@ def output_go( ) continue + # Validate labeled_boolean metrics + if metric.type == "labeled_boolean" and not validate_labeled_boolean(metric): + continue + for ping in metric.send_in_pings: if metric.type == "event" and metric not in event_metrics: event_metrics.append(metric) + + if metric.type == "labeled_boolean" and metric not in labeled_boolean_metrics: + labeled_boolean_metrics.append(metric) metrics_by_type = ping_to_metrics[ping] metrics_list = metrics_by_type.setdefault(metric.type, []) @@ -156,6 +189,9 @@ def output_go( with filepath.open("w", encoding="utf-8") as fd: fd.write( template.render( - parser_version=__version__, pings=ping_to_metrics, events=event_metrics + parser_version=__version__, + pings=ping_to_metrics, + events=event_metrics, + labeled_booleans=labeled_boolean_metrics ) ) diff --git a/glean_parser/templates/go_server.jinja2 b/glean_parser/templates/go_server.jinja2 index bdcb2625..c06d5e73 100644 --- a/glean_parser/templates/go_server.jinja2 +++ b/glean_parser/templates/go_server.jinja2 @@ -220,6 +220,18 @@ func (e {{ event|event_type_name }}) gleanEvent() gleanEvent { } {% endfor %} {% endif %} +{# Generate struct types for labeled_boolean metrics #} +{% if labeled_booleans %} +{% for metric in labeled_booleans %} + +// {{ metric.description|clean_string }} +type {{ metric|labeled_boolean_type_name }} struct { + {% for label in metric.labels %} + {{ label|event_extra_name }} bool // {{ label }} + {% endfor %} +} +{% endfor %} +{% endif %} {# struct & methods for submitting pings #} {% for ping, metrics_by_type in pings.items() %} {% if metrics_by_type['event'] %} @@ -240,7 +252,11 @@ type {{ ping|ping_type_name }} struct { {% for metric_type, metrics in metrics_by_type.items() %} {% if metric_type != 'event' %} {% for metric in metrics %} + {% if metric_type == 'labeled_boolean' %} + {{ metric|metric_argument_name }} {{ metric|labeled_boolean_type_name }} // {{ metric.description|clean_string }} + {% else %} {{ metric|metric_argument_name }} {{ metric_type|go_metric_type}} // {{ metric.description|clean_string }} + {% endif %} {% endfor %} {% endif %} {% endfor %} @@ -261,6 +277,12 @@ func (g GleanEventsLogger) Record{{ ping|ping_type_name }}( {% for metric in metrics %} {% if metric_type == 'datetime' %} "{{ metric|metric_name }}": params.{{ metric|metric_argument_name }}.Format("2006-01-02T15:04:05.000Z"), + {% elif metric_type == 'labeled_boolean' %} + "{{ metric|metric_name }}": map[string]bool{ + {% for label in metric.labels %} + "{{ label }}": params.{{ metric|metric_argument_name }}.{{ label|event_extra_name }}, + {% endfor %} + }, {% else %} "{{ metric|metric_name }}": params.{{ metric|metric_argument_name }}, {% endif %} From 2fd7659973bbc303d586147ee7b6d184f45fc01b Mon Sep 17 00:00:00 2001 From: Sarah Hom Date: Mon, 2 Mar 2026 14:07:18 -0600 Subject: [PATCH 2/6] AE-1236: add tests --- glean_parser/go_server.py | 19 +++++--- .../go_server_labeled_boolean_metrics.yaml | 25 +++++++++++ tests/data/go_server_metrics_unsupported.yaml | 2 +- tests/test_go_server.py | 45 ++++++++++++++++++- 4 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 tests/data/go_server_labeled_boolean_metrics.yaml diff --git a/glean_parser/go_server.py b/glean_parser/go_server.py index c154e56e..41e5cefa 100644 --- a/glean_parser/go_server.py +++ b/glean_parser/go_server.py @@ -96,11 +96,11 @@ def clean_string(s: str) -> str: def validate_labeled_boolean(metric: metrics.Metric) -> bool: """ Validate that a labeled_boolean metric has predefined labels. - + Returns: bool: True if valid, False otherwise """ - if not hasattr(metric, 'labels') or not metric.labels: + if not hasattr(metric, "labels") or not metric.labels: print( "❌ Ignoring labeled_boolean metric without predefined labels: " + f"{metric.name}. labeled_boolean requires a 'labels' list." @@ -140,7 +140,7 @@ def output_go( # unique list of event metrics used in any ping event_metrics: List[metrics.Metric] = [] - + # unique list of labeled_boolean metrics used in any ping labeled_boolean_metrics: List[metrics.Metric] = [] @@ -161,14 +161,19 @@ def output_go( continue # Validate labeled_boolean metrics - if metric.type == "labeled_boolean" and not validate_labeled_boolean(metric): + if metric.type == "labeled_boolean" and not validate_labeled_boolean( + metric + ): continue for ping in metric.send_in_pings: if metric.type == "event" and metric not in event_metrics: event_metrics.append(metric) - - if metric.type == "labeled_boolean" and metric not in labeled_boolean_metrics: + + if ( + metric.type == "labeled_boolean" + and metric not in labeled_boolean_metrics + ): labeled_boolean_metrics.append(metric) metrics_by_type = ping_to_metrics[ping] @@ -192,6 +197,6 @@ def output_go( parser_version=__version__, pings=ping_to_metrics, events=event_metrics, - labeled_booleans=labeled_boolean_metrics + labeled_booleans=labeled_boolean_metrics, ) ) diff --git a/tests/data/go_server_labeled_boolean_metrics.yaml b/tests/data/go_server_labeled_boolean_metrics.yaml new file mode 100644 index 00000000..f4bb729a --- /dev/null +++ b/tests/data/go_server_labeled_boolean_metrics.yaml @@ -0,0 +1,25 @@ +# Any copyright is dedicated to the Public Domain. +# https://creativecommons.org/publicdomain/zero/1.0/ + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 + +telemetry: + feature_flags: + type: labeled_boolean + description: > + Feature flags for A/B testing + labels: + - feature_one + - feature_two + - feature_three + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1234567 + data_reviews: + - https://example.com/review + notification_emails: + - telemetry@example.com + lifetime: application + expires: never + send_in_pings: + - events diff --git a/tests/data/go_server_metrics_unsupported.yaml b/tests/data/go_server_metrics_unsupported.yaml index e0e475e3..e356fee5 100644 --- a/tests/data/go_server_metrics_unsupported.yaml +++ b/tests/data/go_server_metrics_unsupported.yaml @@ -9,7 +9,7 @@ metric: labeled_bool: type: labeled_boolean description: > - labeled boolean + labeled boolean without predefined labels (should be rejected) bugs: - TBD data_reviews: diff --git a/tests/test_go_server.py b/tests/test_go_server.py index 8790ff4e..a3c8f4c2 100644 --- a/tests/test_go_server.py +++ b/tests/test_go_server.py @@ -39,8 +39,6 @@ def test_parser_go_server_metrics_unsupported_type(tmp_path, capsys): captured = capsys.readouterr() assert "Ignoring unsupported metric type" in captured.out unsupported_types = [ - "boolean", - "labeled_boolean", "labeled_string", "timespan", "uuid", @@ -50,6 +48,49 @@ def test_parser_go_server_metrics_unsupported_type(tmp_path, capsys): assert t in captured.out +def test_parser_go_server_labeled_boolean_without_labels(tmp_path, capsys): + """Test that labeled_boolean without predefined labels is rejected.""" + translate.translate( + [ + ROOT / "data" / "go_server_metrics_unsupported.yaml", + ], + "go_server", + tmp_path, + ) + captured = capsys.readouterr() + assert "Ignoring labeled_boolean metric without predefined labels" in captured.out + + +def test_parser_go_server_labeled_boolean(tmp_path): + """Test that labeled_boolean metrics generate proper struct types.""" + translate.translate( + ROOT / "data" / "go_server_labeled_boolean_metrics.yaml", + "go_server", + tmp_path, + ) + + assert set(x.name for x in tmp_path.iterdir()) == set(["server_events.go"]) + + # Read generated file and verify struct is created + with (tmp_path / "server_events.go").open("r", encoding="utf-8") as fd: + content = fd.read() + + # Check that the labeled_boolean struct type was generated + assert "type TelemetryFeatureFlags struct {" in content + assert "FeatureOne bool" in content + assert "FeatureTwo bool" in content + assert "FeatureThree bool" in content + + # Check that it's used in the ping struct + assert "TelemetryFeatureFlags TelemetryFeatureFlags" in content + + # Check that serialization includes map creation + assert "map[string]bool{" in content + assert '"feature_one":' in content + assert '"feature_two":' in content + assert '"feature_three":' in content + + def test_parser_go_server_events_only(tmp_path): """Test that parser works for definitions that only use events ping""" translate.translate( From 6412f92d5538fad78f5aebd3d37238ebf46f501a Mon Sep 17 00:00:00 2001 From: Sarah Hom Date: Mon, 2 Mar 2026 14:59:38 -0600 Subject: [PATCH 3/6] AE-1236: add check for number of labeled booleans --- glean_parser/go_server.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/glean_parser/go_server.py b/glean_parser/go_server.py index 41e5cefa..fdc7374e 100644 --- a/glean_parser/go_server.py +++ b/glean_parser/go_server.py @@ -106,6 +106,15 @@ def validate_labeled_boolean(metric: metrics.Metric) -> bool: + f"{metric.name}. labeled_boolean requires a 'labels' list." ) return False + + if len(metric.labels) > 100: + print( + "❌ Ignoring labeled_boolean metric with too many labels: " + + f"{metric.name} has {len(metric.labels)} labels, " + + "but the maximum is 100." + ) + return False + return True From f6a56d7572a794eb1176752761ce5824e880d028 Mon Sep 17 00:00:00 2001 From: Sarah Hom Date: Mon, 2 Mar 2026 15:03:31 -0600 Subject: [PATCH 4/6] AE-1236: tests --- tests/test_go_server.py | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_go_server.py b/tests/test_go_server.py index a3c8f4c2..922141b8 100644 --- a/tests/test_go_server.py +++ b/tests/test_go_server.py @@ -385,6 +385,53 @@ def test_run_logging_nil_writer(tmp_path): assert logged_output == "writer not specified\n" +@pytest.mark.go_dependency +def test_run_logging_labeled_boolean(tmp_path): + glean_module_path = tmp_path / "glean" + + translate.translate( + [ + ROOT / "data" / "go_server_labeled_boolean_metrics.yaml", + ], + "go_server", + glean_module_path, + ) + + code = """ + logger.RecordEventsPing( + glean.RequestInfo{ + UserAgent: "glean-test/1.0", + IpAddress: "127.0.0.1", + }, + glean.EventsPing{ + TelemetryFeatureFlags: glean.TelemetryFeatureFlags{ + FeatureOne: true, + FeatureTwo: false, + FeatureThree: true, + }, + }, + ) + """ + + logged_output = run_logger(tmp_path, code) + logged_output = json.loads(logged_output) + fields = logged_output["Fields"] + payload_str = fields["payload"] + payload = json.loads(payload_str) + + assert "glean-server-event" == logged_output["Type"] + assert "glean.test" == fields["document_namespace"] + assert "events" == fields["document_type"] + + # Check that labeled_boolean is properly serialized as a map + labeled_boolean_metrics = payload["metrics"]["labeled_boolean"] + assert "telemetry.feature_flags" in labeled_boolean_metrics + feature_flags = labeled_boolean_metrics["telemetry.feature_flags"] + assert feature_flags["feature_one"] is True + assert feature_flags["feature_two"] is False + assert feature_flags["feature_three"] is True + + @pytest.mark.go_dependency def test_run_logging_custom_ping_with_event(tmp_path): glean_module_path = tmp_path / "glean" From 721f5de146eb26bb0d2b09a316d651dbc3ca55ec Mon Sep 17 00:00:00 2001 From: Sarah Hom Date: Mon, 2 Mar 2026 15:06:56 -0600 Subject: [PATCH 5/6] AE-1236: logged support for labeled booleans in Go --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d5f834..50ab6328 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Python server: Support optional metrics ([#828](https://github.com/mozilla/glean_parser/pull/828)) - BUGFIX: Correct event timestamp values in server language templates ([#831](https://github.com/mozilla/glean_parser/pull/831)) - Remove the `coverage` subcommand. The SDK also removed metric testing covergae ([#832](https://github.com/mozilla/glean_parser/pull/832)) +- Add support for parsing labeled booleans in Go. Does not support dynamic labels. ## 18.2.0 From c752b0a587544f10b887dd6a5d8ee759d1c4e37b Mon Sep 17 00:00:00 2001 From: Sarah Hom Date: Mon, 2 Mar 2026 16:44:58 -0600 Subject: [PATCH 6/6] AE-1236: ad comment --- glean_parser/go_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glean_parser/go_server.py b/glean_parser/go_server.py index fdc7374e..30633311 100644 --- a/glean_parser/go_server.py +++ b/glean_parser/go_server.py @@ -38,7 +38,7 @@ "event", "datetime", "boolean", - "labeled_boolean", + "labeled_boolean", # does not support dynamic labeling "string_list", ]