Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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 Go support for parsing the Object metric type. The oneOf parameter type is currently unsupported.

## 18.2.0

Expand Down
146 changes: 143 additions & 3 deletions glean_parser/go_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"datetime",
"boolean",
"string_list",
"object",
]


Expand Down Expand Up @@ -66,6 +67,15 @@ def generate_metric_argument_name(metric: metrics.Metric) -> str:
return f"{util.Camelize(metric.category)}{util.Camelize(metric.name)}"


def generate_object_type_name(metric: metrics.Metric) -> str:
"""Generate the Go type name for an object metric."""
return f"{util.Camelize(metric.category)}{util.Camelize(metric.name)}Object"


def clean_string(s: str) -> str:
return s.replace("\n", " ").rstrip()


def generate_metric_type(metric_type: str) -> str:
if metric_type == "quantity":
return "int64"
Expand All @@ -77,14 +87,133 @@ def generate_metric_type(metric_type: str) -> str:
return "time.Time"
elif metric_type == "string_list":
return "[]string"
# 'oneOf' is not currently supported in object structures
elif metric_type == "object":
return "object"
else:
print("❌ Unable to generate Go type from metric type: " + metric_type)
exit
return "NONE"


def clean_string(s: str) -> str:
return s.replace("\n", " ").rstrip()
def generate_parameter_type(schema: Dict[str, Any], indent: int = 0) -> str:
"""
Convert a JSON schema type definition to a Go type string.

:param schema: JSON schema definition (e.g., from metric.structure)
:param indent: Current indentation level for nested structs
:return: Go type string
"""
parameter_type = schema.get("type")

if parameter_type == "string":
return "string"
elif parameter_type == "number":
return "float64"
elif parameter_type == "boolean":
return "bool"
elif parameter_type == "array":
return generate_array_struct_definition(schema, indent)
elif parameter_type == "object":
properties = schema.get("properties", {})
if not properties:
print(
"❌ Unable to generate Go type. Object type must have 'properties' field with at least one property"
)
exit
return "NONE"

indent_str = "\t" * (indent + 1)
fields = []
for prop_name, prop_schema in properties.items():
field_type = generate_parameter_type(prop_schema, indent + 1)
field_name = util.Camelize(prop_name)
json_tag = f'`json:"{prop_name}"`'
fields.append(f"{indent_str}{field_name} {field_type} {json_tag}")

fields_str = "\n".join(fields)
close_indent = "\t" * indent
return f"struct {{\n{fields_str}\n{close_indent}}}"
else:
print(
f"❌ Unable to generate Go type. Unknown parameter type '{parameter_type}'. Supported types: string, number, boolean, array, object"
)
exit
return "NONE"


def generate_object_struct_definition(metric: metrics.Metric) -> str:
"""
Generate a complete Go struct definition for an object metric.

:param metric: The object metric
:return: Go struct definition as a string
"""
type_name = generate_object_type_name(metric)

if not hasattr(metric, "structure") or not metric.structure:
print(
f"❌ Unable to generate Go type. Object metric '{metric.category}.{metric.name}' is missing required 'structure' field"
)
exit
return "NONE"

parameter_type = metric.structure.get("type")
indent = 0

if parameter_type == "array":
array_type = generate_array_struct_definition(metric.structure, indent)
return f"type {type_name} {array_type}"
elif parameter_type == "object":
properties = metric.structure.get("properties", {})
if not properties:
print(
f"❌ Unable to generate Go type. Object metric '{metric.category}.{metric.name}' has object type but no 'properties' defined"
)
exit
return "NONE"

indent += 1
fields = []
for prop_name, prop_schema in properties.items():
field_type = generate_parameter_type(prop_schema, indent)
field_name = util.Camelize(prop_name)
json_tag = f'`json:"{prop_name}"`'
fields.append(f"\t{field_name} {field_type} {json_tag}")

fields_str = "\n".join(fields)
return f"type {type_name} struct {{\n{fields_str}\n}}"
else:
print(
f"❌ Unable to generate Go type. Object metric '{metric.category}.{metric.name}' has unexpected type '{parameter_type}'. Expected 'array' or 'object'"
)
exit
return "NONE"


def generate_array_struct_definition(schema: Dict[str, Any], indent: int) -> str:
"""
Generate Go type for an array schema.

:param schema: Array schema with 'items' field
:param indent: Current indentation level for nested structs
:return: Go array type string
"""
items_schema = schema.get("items", {})

if "oneOf" in items_schema and "type" not in items_schema:
print("❌ oneOf is currently not supported in Go struct generation")
exit
return "NONE"
elif "type" not in items_schema:
print(
"❌ Unable to generate Go type. Array items schema must have 'type' field"
)
exit
return "NONE"

item_type = generate_parameter_type(items_schema, indent)
return f"[]{item_type}"


def output_go(
Expand Down Expand Up @@ -112,12 +241,17 @@ def output_go(
("metric_argument_name", generate_metric_argument_name),
("go_metric_type", generate_metric_type),
("clean_string", clean_string),
("object_type_name", generate_object_type_name),
("object_struct_definition", generate_object_struct_definition),
),
)

# unique list of event metrics used in any ping
event_metrics: List[metrics.Metric] = []

# unique list of object metrics used in any ping
object_metrics: List[metrics.Metric] = []

# Go through all metrics in objs and build a map of
# ping->list of metric categories->list of metrics
# for easier processing in the template.
Expand All @@ -138,6 +272,9 @@ def output_go(
if metric.type == "event" and metric not in event_metrics:
event_metrics.append(metric)

if metric.type == "object" and metric not in object_metrics:
object_metrics.append(metric)

metrics_by_type = ping_to_metrics[ping]
metrics_list = metrics_by_type.setdefault(metric.type, [])
metrics_list.append(metric)
Expand All @@ -156,6 +293,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,
objects=object_metrics,
)
)
5 changes: 5 additions & 0 deletions glean_parser/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,11 @@ def __init__(self, *args, **kwargs):
self._generate_structure = self.validate_structure(structure)
super().__init__(*args, **kwargs)

@property
def structure(self):
"""Return the validated structure for this object metric."""
return self._generate_structure

ALLOWED_TOPLEVEL = {"type", "properties", "items", "description", "oneOf"}
ALLOWED_TYPES = ["object", "array", "number", "string", "boolean"]
ALLOWED_SUBTYPES = ["number", "string", "boolean"]
Expand Down
11 changes: 11 additions & 0 deletions glean_parser/templates/go_server.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,13 @@ func (e {{ event|event_type_name }}) gleanEvent() gleanEvent {
}
{% endfor %}
{% endif %}
{# if any ping has an object metric, create type definitions for them #}
{% if objects %}
{% for object in objects %}

{{ object|object_struct_definition }}
{% endfor %}
{% endif %}
{# struct & methods for submitting pings #}
{% for ping, metrics_by_type in pings.items() %}
{% if metrics_by_type['event'] %}
Expand All @@ -240,7 +247,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 == 'object' %}
{{ metric|metric_argument_name }} {{ metric|object_type_name }} // {{ metric.description|clean_string }}
{% else %}
{{ metric|metric_argument_name }} {{ metric_type|go_metric_type}} // {{ metric.description|clean_string }}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
Expand Down
116 changes: 116 additions & 0 deletions tests/data/go_server_objects_metrics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# 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

metric:
name:
type: string
description: |
Test string metric
lifetime: application
send_in_pings:
- server-telemetry-objects
notification_emails:
- CHANGE-ME@example.com
bugs:
- TBD
data_reviews:
- TBD
expires: never

test:
simple_object:
type: object
description: Simple object with basic types
send_in_pings:
- server-telemetry-objects
notification_emails:
- CHANGE-ME@example.com
bugs:
- TBD
data_reviews:
- TBD
expires: never
structure:
type: object
properties:
name:
type: string
count:
type: number
enabled:
type: boolean

number_array:
type: object
description: Array of numbers
send_in_pings:
- server-telemetry-objects
notification_emails:
- CHANGE-ME@example.com
bugs:
- TBD
data_reviews:
- TBD
expires: never
structure:
type: array
items:
type: number

nested_object:
type: object
description: Object with nested structures
send_in_pings:
- server-telemetry-objects
notification_emails:
- CHANGE-ME@example.com
bugs:
- TBD
data_reviews:
- TBD
expires: never
structure:
type: object
properties:
user_id:
type: string
metadata:
type: object
properties:
version:
type: number
active:
type: boolean
tags:
type: array
items:
type: string

complex_array:
type: object
description: Array of objects
send_in_pings:
- server-telemetry-objects
notification_emails:
- CHANGE-ME@example.com
bugs:
- TBD
data_reviews:
- TBD
expires: never
structure:
type: array
items:
type: object
properties:
id:
type: number
name:
type: string
data:
type: array
items:
type: number
17 changes: 17 additions & 0 deletions tests/data/go_server_objects_pings.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Any copyright is dedicated to the Public Domain.
# https://creativecommons.org/publicdomain/zero/1.0/

---
$schema: moz://mozilla.org/schemas/glean/pings/2-0-0

server-telemetry-objects:
description: |
Backend ping for testing object metrics
include_client_id: false
send_if_empty: false
bugs:
- TBD
data_reviews:
- TBD
notification_emails:
- CHANGE-ME@example.com
Loading