1# -*- coding: utf-8 -*-
2
3# This Source Code Form is subject to the terms of the Mozilla Public
4# License, v. 2.0. If a copy of the MPL was not distributed with this
5# file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7import cpp
8import js
9import os
10import re
11import rust
12import sys
13
14import jinja2
15
16from util import generate_metric_ids
17from glean_parser import lint, parser, util
18from mozbuild.util import FileAvoidWrite
19from pathlib import Path
20
21
22GIFFT_TYPES = {
23    "Event": ["event"],
24    "Histogram": ["timing_distribution", "memory_distribution", "custom_distribution"],
25    "Scalar": [
26        "boolean",
27        "labeled_boolean",
28        "counter",
29        "labeled_counter",
30        "string",
31        "string_list",
32        "timespan",
33        "uuid",
34        "datetime",
35        "quantity",
36    ],
37}
38
39
40def get_parser_options(moz_app_version):
41    app_version_major = moz_app_version.split(".", 1)[0]
42    return {
43        "allow_reserved": False,
44        "custom_is_expired": lambda expires: expires == "expired"
45        or expires != "never"
46        and int(expires) <= int(app_version_major),
47        "custom_validate_expires": lambda expires: expires in ("expired", "never")
48        or re.fullmatch(r"\d\d+", expires, flags=re.ASCII),
49    }
50
51
52def parse(args):
53    """
54    Parse and lint the input files,
55    then return the parsed objects for further processing.
56    """
57
58    # Unfortunately, GeneratedFile appends `flags` directly after `inputs`
59    # instead of listifying either, so we need to pull stuff from a *args.
60    yaml_array = args[:-1]
61    moz_app_version = args[-1]
62
63    input_files = [Path(x) for x in yaml_array]
64
65    # Derived heavily from glean_parser.translate.translate.
66    # Adapted to how mozbuild sends us a fd, and to expire on versions not dates.
67
68    options = get_parser_options(moz_app_version)
69
70    # Lint the yaml first, then lint the metrics.
71    if lint.lint_yaml_files(input_files, parser_config=options):
72        # Warnings are Errors
73        sys.exit(1)
74
75    all_objs = parser.parse_objects(input_files, options)
76    if util.report_validation_errors(all_objs):
77        sys.exit(1)
78
79    nits = lint.lint_metrics(all_objs.value, options)
80    if nits is not None and any(nit.check_name != "EXPIRED" for nit in nits):
81        # Treat Warnings as Errors in FOG.
82        # But don't fail the whole build on expired metrics (it blocks testing).
83        sys.exit(1)
84
85    return all_objs.value, options
86
87
88# Must be kept in sync with the length of `deps` in moz.build.
89DEPS_LEN = 15
90
91
92def main(output_fd, *args):
93    args = args[DEPS_LEN:]
94    all_objs, options = parse(args)
95    rust.output_rust(all_objs, output_fd, options)
96
97
98def cpp_metrics(output_fd, *args):
99    args = args[DEPS_LEN:]
100    all_objs, options = parse(args)
101    cpp.output_cpp(all_objs, output_fd, options)
102
103
104def js_metrics(output_fd, *args):
105    args = args[DEPS_LEN:]
106    all_objs, options = parse(args)
107    js.output_js(all_objs, output_fd, options)
108
109
110def gifft_map(output_fd, *args):
111    probe_type = args[-1]
112    args = args[DEPS_LEN:-1]
113    all_objs, options = parse(args)
114
115    # Events also need to output maps from event extra enum to strings.
116    # Sadly we need to generate code for all possible events, not just mirrored.
117    # Otherwise we won't compile.
118    if probe_type == "Event":
119        output_path = Path(os.path.dirname(output_fd.name))
120        with FileAvoidWrite(output_path / "EventExtraGIFFTMaps.cpp") as cpp_fd:
121            output_gifft_map(output_fd, probe_type, all_objs, cpp_fd)
122    else:
123        output_gifft_map(output_fd, probe_type, all_objs, None)
124
125
126def output_gifft_map(output_fd, probe_type, all_objs, cpp_fd):
127    get_metric_id = generate_metric_ids(all_objs)
128    ids_to_probes = {}
129    for category_name, objs in all_objs.items():
130        for metric in objs.values():
131            if (
132                hasattr(metric, "telemetry_mirror")
133                and metric.telemetry_mirror is not None
134            ):
135                info = (metric.telemetry_mirror, f"{category_name}.{metric.name}")
136                if metric.type in GIFFT_TYPES[probe_type]:
137                    if info in ids_to_probes.values():
138                        print(
139                            f"Telemetry mirror {metric.telemetry_mirror} already registered",
140                            file=sys.stderr,
141                        )
142                        sys.exit(1)
143                    ids_to_probes[get_metric_id(metric)] = info
144                # If we don't support a mirror for this metric type: build error.
145                elif not any(
146                    [
147                        metric.type in types_for_probe
148                        for types_for_probe in GIFFT_TYPES.values()
149                    ]
150                ):
151                    print(
152                        f"Glean metric {category_name}.{metric.name} is of type {metric.type}"
153                        " which can't be mirrored (we don't know how).",
154                        file=sys.stderr,
155                    )
156                    sys.exit(1)
157
158    env = jinja2.Environment(
159        loader=jinja2.PackageLoader("run_glean_parser", "templates"),
160        trim_blocks=True,
161        lstrip_blocks=True,
162    )
163    env.filters["snake_case"] = lambda value: value.replace(".", "_").replace("-", "_")
164    env.filters["Camelize"] = util.Camelize
165    template = env.get_template("gifft.jinja2")
166    output_fd.write(
167        template.render(
168            ids_to_probes=ids_to_probes,
169            probe_type=probe_type,
170        )
171    )
172    output_fd.write("\n")
173
174    # Events also need to output maps from event extra enum to strings.
175    # Sadly we need to generate code for all possible events, not just mirrored.
176    # Otherwise we won't compile.
177    if probe_type == "Event":
178        template = env.get_template("gifft_events.jinja2")
179        cpp_fd.write(template.render(all_objs=all_objs))
180        cpp_fd.write("\n")
181
182
183if __name__ == "__main__":
184    main(sys.stdout, *sys.argv[1:])
185