Bug 1654074: Publish glean handle to mach commands r=firefox-build-system-reviewers,rstewart
authorMitchell Hentges <mhentges@mozilla.com>
Tue, 15 Sep 2020 21:15:20 +0000
changeset 548946 92cacd2f7294e545e970cbfbc941dd8e72c4462c
parent 548945 db9899666fa8beabb0bdde4a92688691c52d3a5f
child 548947 f420690e43612a43ff317145823492dbf29405b2
push id37790
push userbtara@mozilla.com
push dateThu, 17 Sep 2020 10:09:40 +0000
treeherdermozilla-central@5f3283738794 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfirefox-build-system-reviewers, rstewart
bugs1654074
milestone82.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1654074: Publish glean handle to mach commands r=firefox-build-system-reviewers,rstewart Allows mach commands to define their own glean metrics with the `metrics_path` @CommandProvider parameter. When `metrics_path` is defined: * A `metrics` kwarg is provided to the decorated class. This `metrics` handle is a Glean instance, so Glean documentation should be consulted for usage information. * When `mach doc telemetry` is run, metrics docs will be generated from all the registered metrics files. Note: there was some consideration between making `metrics_path` a @CommandProvider or @Command parameter. In the end, @CommandProvider seemed like a better fit because: * Metrics seem to be more associated with the entire class than a specific command/method. This is because a class represents a "domain", and that domain may have different commands that have overlapping metrics. Accordingly, all the metrics should be defined once as available to the entire class. * Currently, @Command methods only take parameters that map one-to-one with CLI arguments. It could seem inconsistent to have one exception: the metrics handle Differential Revision: https://phabricator.services.mozilla.com/D85953
build/mach_bootstrap.py
python/mach/docs/metrics.md
python/mach/docs/telemetry.rst
python/mach/mach/base.py
python/mach/mach/decorators.py
python/mach/mach/dispatcher.py
python/mach/mach/main.py
python/mach/mach/registrar.py
python/mach/mach/telemetry.py
python/mach/metrics.yaml
python/mach/pings.yaml
python/mozbuild/metrics.yaml
python/mozbuild/mozbuild/base.py
python/mozbuild/mozbuild/build_commands.py
python/mozbuild/mozbuild/controller/building.py
python/mozbuild/mozbuild/mach_commands.py
python/mozbuild/mozbuild/util.py
tools/moztreedocs/mach_commands.py
--- a/build/mach_bootstrap.py
+++ b/build/mach_bootstrap.py
@@ -452,32 +452,33 @@ def _finalize_telemetry_legacy(context, 
 
 def _finalize_telemetry_glean(telemetry, is_bootstrap, success):
     """Submit telemetry collected by Glean.
 
     Finalizes some metrics (command success state and duration, system information) and
     requests Glean to send the collected data.
     """
 
+    from mach.telemetry import MACH_METRICS_PATH
     from mozbuild.telemetry import get_cpu_brand, get_psutil_stats
 
-    system_metrics = telemetry.metrics.mach.system
+    mach_metrics = telemetry.metrics(MACH_METRICS_PATH)
+    mach_metrics.mach.duration.stop()
+    mach_metrics.mach.success.set(success)
+    system_metrics = mach_metrics.mach.system
     system_metrics.cpu_brand.set(get_cpu_brand())
 
     has_psutil, logical_cores, physical_cores, memory_total = get_psutil_stats()
     if has_psutil:
         # psutil may not be available if a successful build hasn't occurred yet.
         system_metrics.logical_cores.add(logical_cores)
         system_metrics.physical_cores.add(physical_cores)
         if memory_total is not None:
             system_metrics.memory.accumulate(int(
                 math.ceil(float(memory_total) / (1024 * 1024 * 1024))))
-
-    telemetry.metrics.mach.duration.stop()
-    telemetry.metrics.mach.success.set(success)
     telemetry.submit(is_bootstrap)
 
 
 # Hook import such that .pyc/.pyo files without a corresponding .py file in
 # the source directory are essentially ignored. See further below for details
 # and caveats.
 # Objdirs outside the source directory are ignored because in most cases, if
 # a .pyc/.pyo file exists there, a .py file will be next to it anyways.
--- a/python/mach/docs/metrics.md
+++ b/python/mach/docs/metrics.md
@@ -22,17 +22,17 @@ This ping includes the [client id](https
 - <https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34>
 
 **Bugs related to this ping:**
 
 - <https://bugzilla.mozilla.org/show_bug.cgi?id=1291053>
 
 The following metrics are added to the ping:
 
-| Name | Type | Description | Data reviews | Extras | Expiration | [Data Sensitivity](https://wiki.mozilla.org/Firefix/Data_Collection) |
+| Name | Type | Description | Data reviews | Extras | Expiration | [Data Sensitivity](https://wiki.mozilla.org/Firefox/Data_Collection) |
 | --- | --- | --- | --- | --- | --- | --- |
 | mach.argv |[string_list](https://mozilla.github.io/glean/book/user/metrics/string_list.html) |Parameters provided to mach. Absolute paths are sanitized to be relative to one of a few key base paths, such as the "$topsrcdir", "$topobjdir", or "$HOME". For example: "/home/mozilla/dev/firefox/python/mozbuild" would be replaced with "$topsrcdir/python/mozbuild". If a valid replacement base path cannot be found, the path is replaced with "<path omitted>".  |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
 | mach.command |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |The name of the mach command that was invoked, such as "build", "doc", or "try".  |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
 | mach.duration |[timespan](https://mozilla.github.io/glean/book/user/metrics/timespan.html) |How long it took for the command to complete. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
 | mach.success |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |True if the mach invocation succeeded. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
 | mach.system.cpu_brand |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |CPU brand string from CPUID. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
 | mach.system.logical_cores |[counter](https://mozilla.github.io/glean/book/user/metrics/counter.html) |Number of logical CPU cores present. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
 | mach.system.memory |[memory_distribution](https://mozilla.github.io/glean/book/user/metrics/memory_distribution.html) |Amount of system memory. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
--- a/python/mach/docs/telemetry.rst
+++ b/python/mach/docs/telemetry.rst
@@ -1,22 +1,39 @@
 .. _mach_telemetry:
 
 ==============
 Mach Telemetry
 ==============
 
 `Glean <https://mozilla.github.io/glean/>`_ is used to collect telemetry, and uses the metrics
-defined in ``/python/mach/metrics.yaml``.
-Associated generated documentation can be found in :ref:`the metrics document<metrics>`.
+defined in the ``metrics.yaml`` files in-tree.
+These files are all documented in a single :ref:`generated file here<metrics>`.
 
 .. toctree::
    :maxdepth: 1
 
    metrics
 
+Adding Metrics to a new Command
+===============================
+
+If you would like to submit telemetry metrics from your mach ``@Command``, you should take two steps:
+
+#. Parameterize your class's ``@CommandProvider`` annotation with ``metrics_path``.
+#. Use the ``self.metrics`` handle provided by ``MachCommandBase``
+
+For example::
+
+    METRICS_PATH = os.path.abspath(os.path.join(__file__, '..', '..', 'metrics.yaml'))
+
+    @CommandProvider(metrics_path=METRICS_PATH)
+    class CustomCommand(MachCommandBase):
+        @Command('custom-command')
+        def custom_command(self):
+            self.metrics.custom.foo.set('bar')
+
 Updating Generated Metrics Docs
 ===============================
 
-When ``metrics.yaml`` is changed, :ref:`the metrics document<metrics>` will need to be updated.
-Glean provides doc-generation tooling for us::
+When a ``metrics.yaml`` is added/changed/removed, :ref:`the metrics document<metrics>` will need to be updated::
 
-    glean_parser translate -f markdown -o python/mach/docs/ python/mach/metrics.yaml python/mach/pings.yaml
+    ./mach doc mach-telemetry
--- a/python/mach/mach/base.py
+++ b/python/mach/mach/base.py
@@ -1,21 +1,21 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, unicode_literals
 
-from mach.telemetry import Telemetry
+from mach.telemetry import NoopTelemetry
 
 
 class CommandContext(object):
     """Holds run-time state so it can easily be passed to command providers."""
     def __init__(self, cwd=None, settings=None, log_manager=None, commands=None,
-                 telemetry=Telemetry.as_noop(), **kwargs):
+                 telemetry=NoopTelemetry(False), **kwargs):
         self.cwd = cwd
         self.settings = settings
         self.log_manager = log_manager
         self.commands = commands
         self.telemetry = telemetry
         self.command_attrs = {}
 
         for k, v in kwargs.items():
--- a/python/mach/mach/decorators.py
+++ b/python/mach/mach/decorators.py
@@ -34,16 +34,20 @@ class _MachCommand(object):
 
         # Describes how dispatch is performed.
 
         # The Python class providing the command. This is the class type not
         # an instance of the class. Mach will instantiate a new instance of
         # the class if the command is executed.
         'cls',
 
+        # The path to the `metrics.yaml` file that describes data that telemetry will
+        # gather for this command. This path is optional.
+        'metrics_path',
+
         # The name of the method providing the command. In other words, this
         # is the str name of the attribute on the class type corresponding to
         # the name of the function.
         'method',
 
         # Dict of string to _MachCommand defining sub-commands for this
         # command.
         'subcommand_handlers',
@@ -67,20 +71,27 @@ class _MachCommand(object):
         self.virtualenv_name = virtualenv_name
         self.order = order
         if ok_if_tests_disabled and category != 'testing':
             raise ValueError('ok_if_tests_disabled should only be set for '
                              '`testing` mach commands')
         self.ok_if_tests_disabled = ok_if_tests_disabled
 
         self.cls = None
+        self.metrics_path = None
         self.method = None
         self.subcommand_handlers = {}
         self.decl_order = None
 
+    def create_instance(self, context, virtualenv_name):
+        metrics = None
+        if self.metrics_path:
+            metrics = context.telemetry.metrics(self.metrics_path)
+        return self.cls(context, virtualenv_name=virtualenv_name, metrics=metrics)
+
     @property
     def parser(self):
         # Creating CLI parsers at command dispatch time can be expensive. Make
         # it possible to lazy load them by using functions.
         if callable(self._parser):
             self._parser = self._parser()
 
         return self._parser
@@ -95,100 +106,107 @@ class _MachCommand(object):
 
         for a in self.__slots__:
             if not getattr(self, a):
                 setattr(self, a, getattr(other, a))
 
         return self
 
 
-def CommandProvider(cls):
-    """Class decorator to denote that it provides subcommands for Mach.
-
-    When this decorator is present, mach looks for commands being defined by
-    methods inside the class.
-    """
+def CommandProvider(_cls=None, metrics_path=None):
+    def finalize(cls):
+        if not issubclass(cls, MachCommandBase):
+            raise MachError(
+                'Mach command provider class %s must be a subclass of '
+                'mozbuild.base.MachComandBase' % cls.__name__)
 
-    # The implementation of this decorator relies on the parse-time behavior of
-    # decorators. When the module is imported, the method decorators (like
-    # @Command and @CommandArgument) are called *before* this class decorator.
-    # The side-effect of the method decorators is to store specifically-named
-    # attributes on the function types. We just scan over all functions in the
-    # class looking for the side-effects of the method decorators.
-
-    if not issubclass(cls, MachCommandBase):
-        raise MachError(
-            'Mach command provider class %s must be a subclass of '
-            'mozbuild.base.MachComandBase' % cls.__name__)
-
-    seen_commands = set()
+        seen_commands = set()
 
-    # We scan __dict__ because we only care about the classes' own attributes,
-    # not inherited ones. If we did inherited attributes, we could potentially
-    # define commands multiple times. We also sort keys so commands defined in
-    # the same class are grouped in a sane order.
-    command_methods = sorted([
-        (name, value._mach_command)
-        for name, value in cls.__dict__.items()
-        if hasattr(value, '_mach_command')
-    ])
-
-    for method, command in command_methods:
-        # Ignore subcommands for now: we handle them later.
-        if command.subcommand:
-            continue
+        # We scan __dict__ because we only care about the classes' own attributes,
+        # not inherited ones. If we did inherited attributes, we could potentially
+        # define commands multiple times. We also sort keys so commands defined in
+        # the same class are grouped in a sane order.
+        command_methods = sorted([
+            (name, value._mach_command)
+            for name, value in cls.__dict__.items()
+            if hasattr(value, '_mach_command')
+        ])
 
-        seen_commands.add(command.name)
+        for method, command in command_methods:
+            # Ignore subcommands for now: we handle them later.
+            if command.subcommand:
+                continue
 
-        if not command.conditions and Registrar.require_conditions:
-            continue
+            seen_commands.add(command.name)
 
-        msg = 'Mach command \'%s\' implemented incorrectly. ' + \
-              'Conditions argument must take a list ' + \
-              'of functions. Found %s instead.'
+            if not command.conditions and Registrar.require_conditions:
+                continue
 
-        if not isinstance(command.conditions, collections.Iterable):
-            msg = msg % (command.name, type(command.conditions))
-            raise MachError(msg)
+            msg = 'Mach command \'%s\' implemented incorrectly. ' + \
+                  'Conditions argument must take a list ' + \
+                  'of functions. Found %s instead.'
 
-        for c in command.conditions:
-            if not hasattr(c, '__call__'):
-                msg = msg % (command.name, type(c))
+            if not isinstance(command.conditions, collections.Iterable):
+                msg = msg % (command.name, type(command.conditions))
                 raise MachError(msg)
 
-        command.cls = cls
-        command.method = method
+            for c in command.conditions:
+                if not hasattr(c, '__call__'):
+                    msg = msg % (command.name, type(c))
+                    raise MachError(msg)
 
-        Registrar.register_command_handler(command)
+            command.cls = cls
+            command.metrics_path = metrics_path
+            command.method = method
+
+            Registrar.register_command_handler(command)
 
-    # Now do another pass to get sub-commands. We do this in two passes so
-    # we can check the parent command existence without having to hold
-    # state and reconcile after traversal.
-    for method, command in command_methods:
-        # It is a regular command.
-        if not command.subcommand:
-            continue
+        # Now do another pass to get sub-commands. We do this in two passes so
+        # we can check the parent command existence without having to hold
+        # state and reconcile after traversal.
+        for method, command in command_methods:
+            # It is a regular command.
+            if not command.subcommand:
+                continue
+
+            if command.name not in seen_commands:
+                raise MachError('Command referenced by sub-command does not '
+                                'exist: %s' % command.name)
+
+            if command.name not in Registrar.command_handlers:
+                continue
 
-        if command.name not in seen_commands:
-            raise MachError('Command referenced by sub-command does not '
-                            'exist: %s' % command.name)
+            command.cls = cls
+            command.metrics_path = metrics_path
+            command.method = method
+            parent = Registrar.command_handlers[command.name]
 
-        if command.name not in Registrar.command_handlers:
-            continue
+            if command.subcommand in parent.subcommand_handlers:
+                raise MachError('sub-command already defined: %s' % command.subcommand)
+
+            parent.subcommand_handlers[command.subcommand] = command
+
+        return cls
 
-        command.cls = cls
-        command.method = method
-        parent = Registrar.command_handlers[command.name]
-
-        if command.subcommand in parent.subcommand_handlers:
-            raise MachError('sub-command already defined: %s' % command.subcommand)
-
-        parent.subcommand_handlers[command.subcommand] = command
-
-    return cls
+    if _cls:
+        # The CommandProvider was used without parameters, e.g.:
+        #
+        # @CommandProvider
+        # class Example:
+        #     ...
+        # Invoke finalize() immediately
+        return finalize(_cls)
+    else:
+        # The CommandProvider was used with parameters, e.g.:
+        #
+        # @CommandProvider(metrics_path='...')
+        # class Example:
+        #     ...
+        # Return a callback which will be parameterized with the decorated class
+        return finalize
 
 
 class Command(object):
     """Decorator for functions or methods that provide a mach command.
 
     The decorator accepts arguments that define basic attributes of the
     command. The following arguments are recognized:
 
--- a/python/mach/mach/dispatcher.py
+++ b/python/mach/mach/dispatcher.py
@@ -253,17 +253,18 @@ class CommandAction(argparse.Action):
 
             for command in sorted(r.commands_by_category[category]):
                 handler = r.command_handlers[command]
 
                 # Instantiate a handler class to see if it should be filtered
                 # out for the current context or not. Condition functions can be
                 # applied to the command's decorator.
                 if handler.conditions:
-                    instance = handler.cls(self._context, handler.virtualenv_name)
+                    instance = handler.create_instance(self._context,
+                                                       handler.virtualenv_name)
 
                     is_filtered = False
                     for c in handler.conditions:
                         if not c(instance):
                             is_filtered = True
                             break
                     if is_filtered:
                         description = handler.description
--- a/python/mach/mach/main.py
+++ b/python/mach/mach/main.py
@@ -29,17 +29,17 @@ from .base import (
     UnrecognizedArgumentError,
     FailedCommandError,
 )
 from .config import ConfigSettings
 from .dispatcher import CommandAction
 from .logging import LoggingManager
 from .registrar import Registrar
 from .sentry import register_sentry, NoopErrorReporter
-from .telemetry import report_invocation_metrics, Telemetry
+from .telemetry import report_invocation_metrics, create_telemetry_from_environment
 from .util import setenv, UserError
 
 SUGGEST_MACH_BUSTED_TEMPLATE = r'''
 You can invoke |./mach busted| to check if this issue is already on file. If it
 isn't, please use |./mach busted file %s| to report it. If |./mach busted| is
 misbehaving, you can also inspect the dependencies of bug 1543241.
 '''.lstrip()
 
@@ -386,17 +386,17 @@ To see more help for a specific command,
             os.environ.clear()
             os.environ.update(orig_env)
 
             sys.stdin = orig_stdin
             sys.stdout = orig_stdout
             sys.stderr = orig_stderr
 
     def _run(self, argv, sentry):
-        telemetry = Telemetry.from_environment(self.settings)
+        telemetry = create_telemetry_from_environment(self.settings)
         context = CommandContext(cwd=self.cwd,
                                  settings=self.settings, log_manager=self.log_manager,
                                  commands=Registrar, telemetry=telemetry)
 
         if self.populate_context_handler:
             context = ContextWrapper(context, self.populate_context_handler)
 
         parser = self.get_argument_parser(context)
@@ -424,17 +424,17 @@ To see more help for a specific command,
             print(UNRECOGNIZED_ARGUMENT_ERROR % (e.command,
                                                  ' '.join(e.arguments)))
             return 1
 
         if not hasattr(args, 'mach_handler'):
             raise MachError('ArgumentParser result missing mach handler info.')
 
         handler = getattr(args, 'mach_handler')
-        report_invocation_metrics(context.telemetry.metrics, handler.name)
+        report_invocation_metrics(context.telemetry, handler.name)
 
         # Add JSON logging to a file if requested.
         if args.logfile:
             self.log_manager.add_json_handler(args.logfile)
 
         # Up the logging level if requested.
         log_level = logging.INFO
         if args.verbose:
--- a/python/mach/mach/registrar.py
+++ b/python/mach/mach/registrar.py
@@ -57,27 +57,25 @@ class MachRegistrar(object):
             part = ['  %s' % getattr(c, '__name__', c)]
             if c.__doc__ is not None:
                 part.append(c.__doc__)
             msg.append(' - '.join(part))
         return INVALID_COMMAND_CONTEXT % (name, '\n'.join(msg))
 
     @classmethod
     def _instance(_, handler, context, **kwargs):
-        cls = handler.cls
-
         if context is None:
             raise ValueError('Expected a non-None context.')
 
         prerun = getattr(context, 'pre_dispatch_handler', None)
         if prerun:
             prerun(context, handler, args=kwargs)
 
         context.handler = handler
-        return cls(context, handler.virtualenv_name)
+        return handler.create_instance(context, handler.virtualenv_name)
 
     @classmethod
     def _fail_conditions(_, handler, instance):
         fail_conditions = []
         if handler.conditions:
             for c in handler.conditions:
                 if not c(instance):
                     fail_conditions.append(c)
--- a/python/mach/mach/telemetry.py
+++ b/python/mach/mach/telemetry.py
@@ -1,83 +1,109 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import print_function, absolute_import
 
 import os
 import sys
+
+import six
 from mock import Mock
 
-from mozboot.util import get_state_dir
+from mozboot.util import get_state_dir, get_mach_virtualenv_binary
 from mozbuild.base import MozbuildObject, BuildEnvironmentNotFoundException
 from mozbuild.telemetry import filter_args
+import mozpack.path
+
+MACH_METRICS_PATH = os.path.abspath(os.path.join(__file__, '..', '..', 'metrics.yaml'))
 
 
-class Telemetry(object):
+class NoopTelemetry(object):
+    def __init__(self, failed_glean_import):
+        self._failed_glean_import = failed_glean_import
+
+    def metrics(self, metrics_path):
+        return Mock()
+
+    def submit(self, is_bootstrap):
+        if self._failed_glean_import and not is_bootstrap:
+            print("Glean could not be found, so telemetry will not be reported. "
+                  "You may need to run |mach bootstrap|.", file=sys.stderr)
+
+
+class GleanTelemetry(object):
     """Records and sends Telemetry using Glean.
 
     Metrics are defined in python/mozbuild/metrics.yaml.
     Pings are defined in python/mozbuild/pings.yaml.
 
     The "metrics" and "pings" properties may be replaced with no-op implementations if
     Glean isn't available. This allows consumers to report telemetry without having
     to guard against incompatible environments.
     """
-    def __init__(self, metrics, pings, failed_glean_import):
-        self.metrics = metrics
-        self._pings = pings
-        self._failed_glean_import = failed_glean_import
-
-    def submit(self, is_bootstrap):
-        self._pings.usage.submit()
-
-        if self._failed_glean_import and not is_bootstrap:
-            print("Glean could not be found, so telemetry will not be reported. "
-                  "You may need to run |mach bootstrap|.", file=sys.stderr)
-
-    @classmethod
-    def as_noop(cls, failed_glean_import=False):
-        return cls(Mock(), Mock(), failed_glean_import)
-
-    @classmethod
-    def from_environment(cls, settings):
-        """Creates and configures a Telemetry instance based on system details.
+    def __init__(self, ):
+        self._metrics_cache = {}
 
-        If telemetry isn't enabled, the current interpreter isn't Python 3, or Glean
-        can't be imported, then a "mock" telemetry instance is returned that doesn't
-        set or record any data. This allows consumers to optimistically set metrics
-        data without needing to specifically handle the case where the current system
-        doesn't support it.
-        """
-        # Glean is not compatible with Python 2
-        if not (sys.version_info >= (3, 0) and is_applicable_telemetry_environment()):
-            return cls.as_noop()
+    def metrics(self, metrics_path):
+        if metrics_path not in self._metrics_cache:
+            from glean import load_metrics
+            metrics = load_metrics(metrics_path)
+            self._metrics_cache[metrics_path] = metrics
 
-        try:
-            from glean import Glean, load_metrics, load_pings
-        except ImportError:
-            return cls.as_noop(failed_glean_import=True)
-
-        from pathlib import Path
+        return self._metrics_cache[metrics_path]
 
-        Glean.initialize(
-            'mozilla.mach',
-            'Unknown',
-            is_telemetry_enabled(settings),
-            data_dir=Path(get_state_dir()) / 'glean',
-        )
+    def submit(self, _):
         from pathlib import Path
-        metrics = load_metrics(Path(__file__).parent.parent / 'metrics.yaml')
+        from glean import load_pings
         pings = load_pings(Path(__file__).parent.parent / 'pings.yaml')
-        return cls(metrics, pings, False)
+        pings.usage.submit()
 
 
-def report_invocation_metrics(metrics, command):
+def create_telemetry_from_environment(settings):
+    """Creates and a Telemetry instance based on system details.
+
+    If telemetry isn't enabled, the current interpreter isn't Python 3, or Glean
+    can't be imported, then a "mock" telemetry instance is returned that doesn't
+    set or record any data. This allows consumers to optimistically set telemetry
+    data without needing to specifically handle the case where the current system
+    doesn't support it.
+    """
+
+    is_mach_virtualenv = (mozpack.path.normpath(sys.executable) ==
+                          mozpack.path.normpath(get_mach_virtualenv_binary(py2=six.PY2)))
+
+    if not (is_applicable_telemetry_environment()
+            # Glean is not compatible with Python 2
+            and sys.version_info >= (3, 0)
+            # If not using the mach virtualenv (e.g.: bootstrap uses native python)
+            # then we can't guarantee that the glean package that we import is a
+            # compatible version. Therefore, don't use glean.
+            and is_mach_virtualenv):
+        return NoopTelemetry(False)
+
+    try:
+        from glean import Glean
+    except ImportError:
+        return NoopTelemetry(True)
+
+    from pathlib import Path
+
+    Glean.initialize(
+        'mozilla.mach',
+        'Unknown',
+        is_telemetry_enabled(settings),
+        data_dir=Path(get_state_dir()) / 'glean',
+    )
+    return GleanTelemetry()
+
+
+def report_invocation_metrics(telemetry, command):
+    metrics = telemetry.metrics(MACH_METRICS_PATH)
     metrics.mach.command.set(command)
     metrics.mach.duration.start()
 
     try:
         instance = MozbuildObject.from_environment()
     except BuildEnvironmentNotFoundException:
         # Mach may be invoked with the state dir as the current working
         # directory, in which case we're not able to find the topsrcdir (so
--- a/python/mach/metrics.yaml
+++ b/python/mach/metrics.yaml
@@ -1,11 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# If this file is changed, update the generated docs:
+# https://firefox-source-docs.mozilla.org/mach/telemetry.html#updating-generated-metrics-docs
 ---
 $schema: moz://mozilla.org/schemas/glean/metrics/1-0-0
 
 mach:
   command:
     type: string
     description: >
       The name of the mach command that was invoked, such as "build",
@@ -123,122 +126,8 @@ mach.system:
     data_reviews:
       - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
     notification_emails:
       - build-telemetry@mozilla.com
       - mhentges@mozilla.com
     expires: never
     send_in_pings:
       - usage
-
-mozbuild:
-  compiler:
-    type: string
-    description: The compiler type in use (CC_TYPE), such as "clang" or "gcc".
-    lifetime: application
-    bugs:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
-    data_reviews:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
-    notification_emails:
-      - build-telemetry@mozilla.com
-      - mhentges@mozilla.com
-    expires: never
-    send_in_pings:
-      - usage
-  artifact:
-    type: boolean
-    description: True if `--enable-artifact-builds`.
-    lifetime: application
-    bugs:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
-    data_reviews:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
-    notification_emails:
-      - build-telemetry@mozilla.com
-      - mhentges@mozilla.com
-    expires: never
-    send_in_pings:
-      - usage
-  debug:
-    type: boolean
-    description: True if `--enable-debug`.
-    lifetime: application
-    bugs:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
-    data_reviews:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
-    notification_emails:
-      - build-telemetry@mozilla.com
-      - mhentges@mozilla.com
-    expires: never
-    send_in_pings:
-      - usage
-  opt:
-    type: boolean
-    description: True if `--enable-optimize`.
-    lifetime: application
-    bugs:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
-    data_reviews:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
-    notification_emails:
-      - build-telemetry@mozilla.com
-      - mhentges@mozilla.com
-    expires: never
-    send_in_pings:
-      - usage
-  ccache:
-    type: boolean
-    description: True if `--with-ccache`.
-    lifetime: application
-    bugs:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
-    data_reviews:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
-    notification_emails:
-      - build-telemetry@mozilla.com
-      - mhentges@mozilla.com
-    expires: never
-    send_in_pings:
-      - usage
-  sccache:
-    type: boolean
-    description: True if ccache in use is sccache.
-    lifetime: application
-    bugs:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
-    data_reviews:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
-    notification_emails:
-      - build-telemetry@mozilla.com
-      - mhentges@mozilla.com
-    expires: never
-    send_in_pings:
-      - usage
-  icecream:
-    type: boolean
-    description: True if icecream in use.
-    lifetime: application
-    bugs:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
-    data_reviews:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
-    notification_emails:
-      - build-telemetry@mozilla.com
-      - mhentges@mozilla.com
-    expires: never
-    send_in_pings:
-      - usage
-  clobber:
-    type: boolean
-    description: True if the build was a clobber/full build.
-    lifetime: application
-    bugs:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1526072
-    data_reviews:
-      - https://bugzilla.mozilla.org/show_bug.cgi?id=1526072#c15
-    notification_emails:
-      - build-telemetry@mozilla.com
-      - mhentges@mozilla.com
-    expires: never
-    send_in_pings:
-      - usage
--- a/python/mach/pings.yaml
+++ b/python/mach/pings.yaml
@@ -1,11 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# If this file is changed, update the generated docs:
+# https://firefox-source-docs.mozilla.org/mach/telemetry.html#updating-generated-metrics-docs
 ---
 $schema: moz://mozilla.org/schemas/glean/pings/1-0-0
 
 usage:
   description: >
     Sent when the mach invocation is completed (regardless of result).
     Contains information about the mach invocation that was made, its result,
     and some details about the current environment and hardware.
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/metrics.yaml
@@ -0,0 +1,122 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# If this file is changed, update the generated docs:
+# https://firefox-source-docs.mozilla.org/mach/telemetry.html#updating-generated-metrics-docs
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0
+
+mozbuild:
+  compiler:
+    type: string
+    description: The compiler type in use (CC_TYPE), such as "clang" or "gcc".
+    lifetime: application
+    bugs:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
+    data_reviews:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
+    notification_emails:
+      - build-telemetry@mozilla.com
+      - mhentges@mozilla.com
+    expires: never
+    send_in_pings:
+      - usage
+  artifact:
+    type: boolean
+    description: True if `--enable-artifact-builds`.
+    lifetime: application
+    bugs:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
+    data_reviews:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
+    notification_emails:
+      - build-telemetry@mozilla.com
+      - mhentges@mozilla.com
+    expires: never
+    send_in_pings:
+      - usage
+  debug:
+    type: boolean
+    description: True if `--enable-debug`.
+    lifetime: application
+    bugs:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
+    data_reviews:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
+    notification_emails:
+      - build-telemetry@mozilla.com
+      - mhentges@mozilla.com
+    expires: never
+    send_in_pings:
+      - usage
+  opt:
+    type: boolean
+    description: True if `--enable-optimize`.
+    lifetime: application
+    bugs:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
+    data_reviews:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
+    notification_emails:
+      - build-telemetry@mozilla.com
+      - mhentges@mozilla.com
+    expires: never
+    send_in_pings:
+      - usage
+  ccache:
+    type: boolean
+    description: True if `--with-ccache`.
+    lifetime: application
+    bugs:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
+    data_reviews:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
+    notification_emails:
+      - build-telemetry@mozilla.com
+      - mhentges@mozilla.com
+    expires: never
+    send_in_pings:
+      - usage
+  sccache:
+    type: boolean
+    description: True if ccache in use is sccache.
+    lifetime: application
+    bugs:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
+    data_reviews:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
+    notification_emails:
+      - build-telemetry@mozilla.com
+      - mhentges@mozilla.com
+    expires: never
+    send_in_pings:
+      - usage
+  icecream:
+    type: boolean
+    description: True if icecream in use.
+    lifetime: application
+    bugs:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
+    data_reviews:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
+    notification_emails:
+      - build-telemetry@mozilla.com
+      - mhentges@mozilla.com
+    expires: never
+    send_in_pings:
+      - usage
+  clobber:
+    type: boolean
+    description: True if the build was a clobber/full build.
+    lifetime: application
+    bugs:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1526072
+    data_reviews:
+      - https://bugzilla.mozilla.org/show_bug.cgi?id=1526072#c15
+    notification_emails:
+      - build-telemetry@mozilla.com
+      - mhentges@mozilla.com
+    expires: never
+    send_in_pings:
+      - usage
--- a/python/mozbuild/mozbuild/base.py
+++ b/python/mozbuild/mozbuild/base.py
@@ -853,17 +853,17 @@ class MozbuildObject(ProcessExecutionMix
 
 class MachCommandBase(MozbuildObject):
     """Base class for mach command providers that wish to be MozbuildObjects.
 
     This provides a level of indirection so MozbuildObject can be refactored
     without having to change everything that inherits from it.
     """
 
-    def __init__(self, context, virtualenv_name=None):
+    def __init__(self, context, virtualenv_name=None, metrics=None):
         # Attempt to discover topobjdir through environment detection, as it is
         # more reliable than mozconfig when cwd is inside an objdir.
         topsrcdir = context.topdir
         topobjdir = None
         detect_virtualenv_mozinfo = True
         if hasattr(context, 'detect_virtualenv_mozinfo'):
             detect_virtualenv_mozinfo = getattr(context,
                                                 'detect_virtualenv_mozinfo')
@@ -900,16 +900,17 @@ class MachCommandBase(MozbuildObject):
             sys.exit(1)
 
         MozbuildObject.__init__(
             self, topsrcdir, context.settings,
             context.log_manager, topobjdir=topobjdir,
             virtualenv_name=virtualenv_name)
 
         self._mach_context = context
+        self.metrics = metrics
 
         # Incur mozconfig processing so we have unified error handling for
         # errors. Otherwise, the exceptions could bubble back to mach's error
         # handler.
         try:
             self.mozconfig
 
         except MozconfigFindException as e:
--- a/python/mozbuild/mozbuild/build_commands.py
+++ b/python/mozbuild/mozbuild/build_commands.py
@@ -10,32 +10,32 @@ import subprocess
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
 from mozbuild.base import MachCommandBase
-from mozbuild.util import ensure_subprocess_env
+from mozbuild.util import ensure_subprocess_env, MOZBUILD_METRICS_PATH
 from mozbuild.mozconfig import MozconfigLoader
 import mozpack.path as mozpath
 
 from mozbuild.backend import (
     backends,
 )
 
 BUILD_WHAT_HELP = '''
 What to build. Can be a top-level make target or a relative directory. If
 multiple options are provided, they will be built serially. BUILDING ONLY PARTS
 OF THE TREE CAN RESULT IN BAD TREE STATE. USE AT YOUR OWN RISK.
 '''.strip()
 
 
-@CommandProvider
+@CommandProvider(metrics_path=MOZBUILD_METRICS_PATH)
 class Build(MachCommandBase):
     """Interface to build the tree."""
 
     @Command('build', category='build', description='Build the tree.')
     @CommandArgument('--jobs', '-j', default='0', metavar='jobs', type=int,
                      help='Number of concurrent jobs to run. Default is the number of CPUs.')
     @CommandArgument('-C', '--directory', default=None,
                      help='Change to a subdirectory of the build directory first.')
@@ -117,16 +117,17 @@ class Build(MachCommandBase):
             subprocess.check_call(pgo_cmd, cwd=instr.topobjdir,
                                   env=ensure_subprocess_env(pgo_env))
 
             # Set the default build to MOZ_PROFILE_USE
             append_env = {'MOZ_PROFILE_USE': '1'}
 
         driver = self._spawn(BuildDriver)
         return driver.build(
+            self.metrics,
             what=what,
             jobs=jobs,
             directory=directory,
             verbose=verbose,
             keep_going=keep_going,
             mach_context=self._mach_context,
             append_env=append_env)
 
@@ -138,16 +139,17 @@ class Build(MachCommandBase):
         from mozbuild.controller.building import (
             BuildDriver,
         )
 
         self.log_manager.enable_all_structured_loggers()
         driver = self._spawn(BuildDriver)
 
         return driver.configure(
+            self.metrics,
             options=options,
             buildstatus_messages=buildstatus_messages,
             line_handler=line_handler)
 
     @Command('resource-usage', category='post-build',
              description='Show information about system resource usage for a build.')
     @CommandArgument('--address', default='localhost',
                      help='Address the HTTP server should listen on.')
--- a/python/mozbuild/mozbuild/controller/building.py
+++ b/python/mozbuild/mozbuild/controller/building.py
@@ -1026,25 +1026,27 @@ class CCacheStats(object):
             return '%.1f Kbytes' % (float(v) / CCacheStats.KiB)
 
 
 class BuildDriver(MozbuildObject):
     """Provides a high-level API for build actions."""
 
     def __init__(self, *args, **kwargs):
         MozbuildObject.__init__(self, *args, **kwargs)
+        self.metrics = None
         self.mach_context = None
 
-    def build(self, what=None, jobs=0, directory=None, verbose=False,
+    def build(self, metrics, what=None, jobs=0, directory=None, verbose=False,
               keep_going=False, mach_context=None, append_env=None):
         """Invoke the build backend.
 
         ``what`` defines the thing to build. If not defined, the default
         target is used.
         """
+        self.metrics = metrics
         self.mach_context = mach_context
         warnings_path = self._get_state_filename('warnings.json')
         monitor = self._spawn(BuildMonitor)
         monitor.init(warnings_path)
         ccache_start = monitor.ccache_stats()
         footer = BuildProgressFooter(self.log_manager.terminal, monitor)
 
         # Disable indexing in objdir because it is not necessary and can slow
@@ -1062,27 +1064,26 @@ class BuildDriver(MozbuildObject):
             if directory is not None:
                 directory = mozpath.normsep(directory)
                 if directory.startswith('/'):
                     directory = directory[1:]
 
             monitor.start_resource_recording()
 
             self.mach_context.command_attrs['clobber'] = False
-            self.mach_context.telemetry.metrics.mozbuild.clobber.set(False)
+            self.metrics.mozbuild.clobber.set(False)
             config = None
             try:
                 config = self.config_environment
             except Exception:
                 # If we don't already have a config environment this is either
                 # a fresh objdir or $OBJDIR/config.status has been removed for
                 # some reason, which indicates a clobber of sorts.
                 self.mach_context.command_attrs['clobber'] = True
-                mozbuild_telemetry = self.mach_context.telemetry.metrics.mozbuild
-                mozbuild_telemetry.clobber.set(True)
+                self.metrics.mozbuild.clobber.set(True)
 
             # Record whether a clobber was requested so we can print
             # a special message later if the build fails.
             clobber_requested = False
 
             # Write out any changes to the current mozconfig in case
             # they should invalidate configure.
             self._write_mozconfig_json()
@@ -1099,28 +1100,29 @@ class BuildDriver(MozbuildObject):
                                                         mozpath.join(self.topobjdir,
                                                                      'config_status_deps.in')):
                 if previous_backend and 'Make' not in previous_backend:
                     clobber_requested = self._clobber_configure()
 
                 if config is None:
                     print(" Config object not found by mach.")
 
-                config_rc = self.configure(buildstatus_messages=True,
+                config_rc = self.configure(metrics,
+                                           buildstatus_messages=True,
                                            line_handler=output.on_line,
                                            append_env=append_env)
 
                 if config_rc != 0:
                     return config_rc
 
                 config = self.reload_config_environment()
 
             # Collect glean metrics
             substs = config.substs
-            mozbuild_metrics = self.mach_context.telemetry.metrics.mozbuild
+            mozbuild_metrics = metrics.mozbuild
             mozbuild_metrics.compiler.set(substs.get('CC_TYPE', None))
 
             def get_substs_flag(name):
                 return bool(substs.get(name, None))
 
             mozbuild_metrics.artifact.set(get_substs_flag('MOZ_ARTIFACT_BUILDS'))
             mozbuild_metrics.debug.set(get_substs_flag('MOZ_DEBUG'))
             mozbuild_metrics.opt.set(get_substs_flag('MOZ_OPTIMIZE'))
@@ -1383,20 +1385,21 @@ class BuildDriver(MozbuildObject):
                     )
             except Exception:
                 # Ignore Exceptions in case we can't find config.status (such
                 # as when doing OSX Universal builds)
                 pass
 
         return status
 
-    def configure(self, options=None, buildstatus_messages=False,
+    def configure(self, metrics, options=None, buildstatus_messages=False,
                   line_handler=None, append_env=None):
         # Disable indexing in objdir because it is not necessary and can slow
         # down builds.
+        self.metrics = metrics
         mkdir(self.topobjdir, not_indexed=True)
         self._write_mozconfig_json()
 
         def on_line(line):
             self.log(logging.INFO, 'build_output', {'line': line}, '{line}')
 
         line_handler = line_handler or on_line
 
@@ -1594,17 +1597,17 @@ class BuildDriver(MozbuildObject):
         clobber_output.seek(0)
         for line in clobber_output.readlines():
             self.log(logging.WARNING, 'clobber',
                      {'msg': line.rstrip()}, '{msg}')
 
         clobber_required, clobber_performed, clobber_message = res
         if self.mach_context is not None and clobber_performed:
             self.mach_context.command_attrs['clobber'] = True
-            self.mach_context.telemetry.metrics.mozbuild.clobber.set(True)
+            self.metrics.mozbuild.clobber.set(True)
         if not clobber_required or clobber_performed:
             if clobber_performed and env.get('TINDERBOX_OUTPUT'):
                 self.log(logging.WARNING, 'clobber',
                          {'msg': 'TinderboxPrint: auto clobber'}, '{msg}')
         else:
             for line in clobber_message.splitlines():
                 self.log(logging.WARNING, 'clobber',
                          {'msg': line.rstrip()}, '{msg}')
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -29,16 +29,17 @@ from mach.decorators import (
 
 from mozbuild.base import (
     BinaryNotFoundException,
     BuildEnvironmentNotFoundException,
     MachCommandBase,
     MachCommandConditions as conditions,
     MozbuildObject,
 )
+from mozbuild.util import MOZBUILD_METRICS_PATH
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 EXCESSIVE_SWAP_MESSAGE = '''
 ===================
 PERFORMANCE WARNING
 
 Your machine experienced a lot of swap activity during the build. This is
@@ -169,17 +170,17 @@ class Doctor(MachCommandBase):
                      help='Attempt to fix found problems.')
     def doctor(self, fix=None):
         self.activate_virtualenv()
         from mozbuild.doctor import Doctor
         doctor = Doctor(self.topsrcdir, self.topobjdir, fix)
         return doctor.check_all()
 
 
-@CommandProvider
+@CommandProvider(metrics_path=MOZBUILD_METRICS_PATH)
 class Clobber(MachCommandBase):
     NO_AUTO_LOG = True
     CLOBBER_CHOICES = set(['objdir', 'python', 'gradle'])
 
     @Command('clobber', category='build',
              description='Clobber the tree (delete the object directory).')
     @CommandArgument('what', default=['objdir', 'python'], nargs='*',
                      help='Target to clobber, must be one of {{{}}} (default '
--- a/python/mozbuild/mozbuild/util.py
+++ b/python/mozbuild/mozbuild/util.py
@@ -24,16 +24,19 @@ import sys
 import time
 from collections import (
     OrderedDict,
 )
 from io import (BytesIO, StringIO)
 
 import six
 
+MOZBUILD_METRICS_PATH = os.path.abspath(
+    os.path.join(__file__, '..', '..', 'metrics.yaml'))
+
 if sys.platform == 'win32':
     _kernel32 = ctypes.windll.kernel32
     _FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x2000
     system_encoding = 'mbcs'
 else:
     system_encoding = 'utf-8'
 
 
--- a/tools/moztreedocs/mach_commands.py
+++ b/tools/moztreedocs/mach_commands.py
@@ -13,21 +13,23 @@ import subprocess
 import sys
 import time
 import yaml
 import uuid
 
 from functools import partial
 from pprint import pprint
 
+from mach.registrar import Registrar
 from mozbuild.base import MachCommandBase
 from mach.decorators import (
     Command,
     CommandArgument,
     CommandProvider,
+    SubCommand,
 )
 
 here = os.path.abspath(os.path.dirname(__file__))
 topsrcdir = os.path.abspath(os.path.dirname(os.path.dirname(here)))
 DOC_ROOT = os.path.join(topsrcdir, "docs")
 BASE_LINK = "http://gecko-docs.mozilla.org-l1.s3-website.us-west-2.amazonaws.com/"
 JSDOC_NOT_FOUND = """\
 JSDoc==3.5.5 is required to build the docs but was not found on your system.
@@ -394,16 +396,40 @@ class Documentation(MachCommandBase):
         print("Redirects currently staged")
         pprint(all_redirects, indent=1)
 
         s3_set_redirects(all_redirects)
 
         unique_link = BASE_LINK + unique_id + "/index.html"
         print("Uploaded documentation can be accessed here " + unique_link)
 
+    @SubCommand(
+        "doc",
+        "mach-telemetry",
+        description="Generate documentation from Glean metrics.yaml files",
+    )
+    def generate_telemetry_docs(self):
+        args = [
+            "glean_parser",
+            "translate",
+            "-f",
+            "markdown",
+            "-o",
+            os.path.join(topsrcdir, "python/mach/docs/"),
+            os.path.join(topsrcdir, "python/mach/pings.yaml"),
+            os.path.join(topsrcdir, "python/mach/metrics.yaml"),
+        ]
+        metrics_paths = [
+            handler.metrics_path
+            for handler in Registrar.command_handlers.values()
+            if handler.metrics_path is not None
+        ]
+        args.extend([os.path.join(self.topsrcdir, path) for path in set(metrics_paths)])
+        subprocess.check_output(args)
+
     def check_jsdoc(self):
         try:
             from mozfile import which
 
             exe_name = which("jsdoc")
             if not exe_name:
                 return 1
             out = subprocess.check_output([exe_name, "--version"])