Bug 1651424: Report build telemetry using Glean r=firefox-build-system-reviewers,Dexter,rstewart
authorMitchell Hentges <mhentges@mozilla.com>
Wed, 09 Sep 2020 23:51:57 +0000
changeset 548164 5ecacfbe4d0d990c76f84228458d5719c2f2f79a
parent 548163 276576a8537f4e34d449af93bbdc9a5fd3b70dd1
child 548165 404660b9b7f69580843911a484ab7ea20c0cccc4
push id37774
push usernbeleuzu@mozilla.com
push dateFri, 11 Sep 2020 02:31:36 +0000
treeherdermozilla-central@f92ce84f27df [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfirefox-build-system-reviewers, Dexter, rstewart
bugs1651424
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 1651424: Report build telemetry using Glean r=firefox-build-system-reviewers,Dexter,rstewart In addition to the existing build telemetry, also gather the stats and report with Glean. This new telemetry is reported in tandem with the existing telemetry to allow testing and confidence before a full roll-out. Additionally, Glean isn't compatible with Python 2, so the new telemetry only runs on Python 3 mach commands. Differential Revision: https://phabricator.services.mozilla.com/D83572
build/docs/telemetry.rst
build/mach_bootstrap.py
python/mach/docs/index.rst
python/mach/docs/metrics.md
python/mach/docs/telemetry.rst
python/mach/mach/base.py
python/mach/mach/logging.py
python/mach/mach/main.py
python/mach/mach/registrar.py
python/mach/mach/sentry.py
python/mach/mach/telemetry.py
python/mach/mach/test/test_telemetry.py
python/mach/metrics.yaml
python/mach/pings.yaml
python/mozbuild/mozbuild/controller/building.py
python/mozbuild/mozbuild/telemetry.py
--- a/build/docs/telemetry.rst
+++ b/build/docs/telemetry.rst
@@ -348,16 +348,25 @@ time
 
 Time at which this event happened
 
 :type: ``string``
 
 :format: ``date-time``
 
 
+Glean Telemetry
+===============
+
+In addition to the existing build-specific telemetry, Mozbuild is also reporting data using
+`Glean <https://mozilla.github.io/glean/>`_ via :ref:`mach_telemetry`.
+The metrics collected are documented :ref:`here<metrics>`.
+As Python 2 is phased out, the old telemetry will be replaced by the new Glean implementation.
+
+
 Error Reporting
 ===============
 
 ``./mach`` uses `Sentry <https://sentry.io/welcome/>`_
 to automatically report errors to `our issue-tracking dashboard
 <https://sentry.prod.mozaws.net/operations/mach/>`_.
 
 Information captured
--- a/build/mach_bootstrap.py
+++ b/build/mach_bootstrap.py
@@ -1,21 +1,23 @@
 # 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, unicode_literals
+from __future__ import division, print_function, unicode_literals
 
 import errno
 import json
+import math
 import os
 import platform
 import subprocess
 import sys
 import uuid
+
 if sys.version_info[0] < 3:
     import __builtin__ as builtins
 else:
     import builtins
 
 from types import ModuleType
 
 
@@ -277,107 +279,33 @@ def bootstrap(topsrcdir, mozilla_dir=Non
                         print('Tests have been disabled by mozconfig with the flag ' +
                               '"ac_add_options --disable-tests".\n' +
                               'Remove the flag, and re-compile to enable tests.')
                         sys.exit(1)
             except BuildEnvironmentNotFoundException:
                 # likely automation environment, so do nothing.
                 pass
 
-    def should_skip_telemetry_submission(handler):
-        # The user is performing a maintenance command.
-        if handler.name in (
-                'bootstrap', 'doctor', 'mach-commands', 'vcs-setup',
-                'create-mach-environment',
-                # We call mach environment in client.mk which would cause the
-                # data submission to block the forward progress of make.
-                'environment'):
-            return True
-
-        # Never submit data when running in automation or when running tests.
-        if any(e in os.environ for e in ('MOZ_AUTOMATION', 'TASK_ID', 'MACH_TELEMETRY_NO_SUBMIT')):
-            return True
-
-        return False
-
-    def post_dispatch_handler(context, handler, instance, result,
+    def post_dispatch_handler(context, handler, instance, success,
                               start_time, end_time, depth, args):
         """Perform global operations after command dispatch.
 
 
         For now,  we will use this to handle build system telemetry.
         """
-        # Don't write telemetry data if this mach command was invoked as part of another
-        # mach command.
-        if depth != 1 or os.environ.get('MACH_MAIN_PID') != str(os.getpid()):
-            return
 
-        from mozbuild.telemetry import is_telemetry_enabled
-        if not is_telemetry_enabled(context.settings):
+        # Don't finalize telemetry data if this mach command was invoked as part of
+        # another mach command.
+        if depth != 1:
             return
 
-        from mozbuild.telemetry import gather_telemetry
-        from mozbuild.base import MozbuildObject
-        import mozpack.path as mozpath
-
-        if not isinstance(instance, MozbuildObject):
-            instance = MozbuildObject.from_environment()
-
-        try:
-            substs = instance.substs
-        except Exception:
-            substs = {}
-
-        command_attrs = getattr(context, 'command_attrs', {})
-
-        # We gather telemetry for every operation.
-        paths = {
-            instance.topsrcdir: '$topsrcdir/',
-            instance.topobjdir: '$topobjdir/',
-            mozpath.normpath(os.path.expanduser('~')): '$HOME/',
-        }
-        # This might override one of the existing entries, that's OK.
-        # We don't use a sigil here because we treat all arguments as potentially relative
-        # paths, so we'd like to get them back as they were specified.
-        paths[mozpath.normpath(os.getcwd())] = ''
-        data = gather_telemetry(command=handler.name, success=(result == 0),
-                                start_time=start_time, end_time=end_time,
-                                mach_context=context, substs=substs,
-                                command_attrs=command_attrs, paths=paths)
-        if data:
-            telemetry_dir = os.path.join(get_state_dir(), 'telemetry')
-            try:
-                os.mkdir(telemetry_dir)
-            except OSError as e:
-                if e.errno != errno.EEXIST:
-                    raise
-            outgoing_dir = os.path.join(telemetry_dir, 'outgoing')
-            try:
-                os.mkdir(outgoing_dir)
-            except OSError as e:
-                if e.errno != errno.EEXIST:
-                    raise
-
-            with open(os.path.join(outgoing_dir, str(uuid.uuid4()) + '.json'),
-                      'w') as f:
-                json.dump(data, f, sort_keys=True)
-
-        if should_skip_telemetry_submission(handler):
-            return True
-
-        state_dir = get_state_dir()
-
-        machpath = os.path.join(instance.topsrcdir, 'mach')
-        with open(os.devnull, 'wb') as devnull:
-            subprocess.Popen([sys.executable, machpath, 'python',
-                              '--no-virtualenv',
-                              os.path.join(topsrcdir, 'build',
-                                           'submit_telemetry_data.py'),
-                              state_dir],
-                             stdout=devnull, stderr=devnull)
+        _finalize_telemetry_glean(context.telemetry, handler.name == 'bootstrap',
+                                  success)
+        _finalize_telemetry_legacy(context, instance, handler, success, start_time,
+                                   end_time, topsrcdir)
 
     def populate_context(key=None):
         if key is None:
             return
         if key == 'state_dir':
             state_dir = get_state_dir()
             if state_dir == os.environ.get('MOZBUILD_STATE_PATH'):
                 if not os.path.exists(state_dir):
@@ -442,16 +370,117 @@ def bootstrap(topsrcdir, mozilla_dir=Non
             driver.load_commands_from_file(os.path.join(mozilla_dir, path))
         except mach.base.MissingFileError:
             if not repo or not repo.sparse_checkout_present():
                 raise
 
     return driver
 
 
+def _finalize_telemetry_legacy(context, instance, handler, success, start_time,
+                               end_time, topsrcdir):
+    """Record and submit legacy telemetry.
+
+    Parameterized by the raw gathered telemetry, this function handles persisting and
+    submission of the data.
+
+    This has been designated as "legacy" telemetry because modern telemetry is being
+    submitted with "Glean".
+    """
+    from mozboot.util import get_state_dir
+    from mozbuild.base import MozbuildObject
+    from mozbuild.telemetry import gather_telemetry
+    from mach.telemetry import (
+        is_telemetry_enabled,
+        is_applicable_telemetry_environment
+    )
+
+    if not (is_applicable_telemetry_environment()
+            and is_telemetry_enabled(context.settings)):
+        return
+
+    if not isinstance(instance, MozbuildObject):
+        instance = MozbuildObject.from_environment()
+
+    command_attrs = getattr(context, 'command_attrs', {})
+
+    # We gather telemetry for every operation.
+    data = gather_telemetry(command=handler.name, success=success,
+                            start_time=start_time, end_time=end_time,
+                            mach_context=context, instance=instance,
+                            command_attrs=command_attrs)
+    if data:
+        telemetry_dir = os.path.join(get_state_dir(), 'telemetry')
+        try:
+            os.mkdir(telemetry_dir)
+        except OSError as e:
+            if e.errno != errno.EEXIST:
+                raise
+        outgoing_dir = os.path.join(telemetry_dir, 'outgoing')
+        try:
+            os.mkdir(outgoing_dir)
+        except OSError as e:
+            if e.errno != errno.EEXIST:
+                raise
+
+        with open(os.path.join(outgoing_dir, str(uuid.uuid4()) + '.json'),
+                  'w') as f:
+            json.dump(data, f, sort_keys=True)
+
+    # The user is performing a maintenance command, skip the upload
+    if handler.name in ('bootstrap', 'doctor', 'mach-commands', 'vcs-setup',
+                        'create-mach-environment',
+                        # We call mach environment in client.mk which would cause the
+                        # data submission to block the forward progress of make.
+                        'environment'):
+        return False
+
+    if 'TEST_MACH_TELEMETRY_NO_SUBMIT' in os.environ:
+        # In our telemetry tests, we want telemetry to be collected for analysis, but
+        # we don't want it submitted.
+        return False
+
+    state_dir = get_state_dir()
+
+    machpath = os.path.join(instance.topsrcdir, 'mach')
+    with open(os.devnull, 'wb') as devnull:
+        subprocess.Popen([sys.executable, machpath, 'python',
+                          '--no-virtualenv',
+                          os.path.join(topsrcdir, 'build',
+                                       'submit_telemetry_data.py'),
+                          state_dir],
+                         stdout=devnull, stderr=devnull)
+
+
+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 mozbuild.telemetry import get_cpu_brand, get_psutil_stats
+
+    system_metrics = telemetry.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.
 class ImportHook(object):
     def __init__(self, original_import):
         self._original_import = original_import
--- a/python/mach/docs/index.rst
+++ b/python/mach/docs/index.rst
@@ -79,8 +79,9 @@ best fit for you.
    :maxdepth: 1
    :hidden:
 
    usage
    commands
    driver
    logging
    settings
+   telemetry
new file mode 100644
--- /dev/null
+++ b/python/mach/docs/metrics.md
@@ -0,0 +1,53 @@
+<!-- AUTOGENERATED BY glean_parser.  DO NOT EDIT. -->
+
+# Metrics
+This document enumerates the metrics collected by this project.
+This project may depend on other projects which also collect metrics.
+This means you might have to go searching through the dependency tree to get a full picture of everything collected by this project.
+
+# Pings
+
+ - [usage](#usage)
+
+
+## usage
+
+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.
+
+
+This ping includes the [client id](https://mozilla.github.io/glean/book/user/pings/index.html#the-client_info-section).
+
+**Data reviews for this ping:**
+
+- <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) |
+| --- | --- | --- | --- | --- | --- | --- |
+| 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 | |
+| mach.system.physical_cores |[counter](https://mozilla.github.io/glean/book/user/metrics/counter.html) |Number of physical CPU cores present. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
+| mozbuild.artifact |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |True if `--enable-artifact-builds`. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
+| mozbuild.ccache |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |True if `--with-ccache`. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
+| mozbuild.clobber |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |True if the build was a clobber/full build. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1526072#c15)||never | |
+| mozbuild.compiler |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |The compiler type in use (CC_TYPE), such as "clang" or "gcc". |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
+| mozbuild.debug |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |True if `--enable-debug`. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
+| mozbuild.icecream |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |True if icecream in use. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
+| mozbuild.opt |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |True if `--enable-optimize`. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
+| mozbuild.sccache |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |True if ccache in use is sccache. |[1](https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34)||never | |
+
+
+Data categories are [defined here](https://wiki.mozilla.org/Firefox/Data_Collection).
+
+<!-- AUTOGENERATED BY glean_parser.  DO NOT EDIT. -->
+
copy from python/mozboot/README.rst
copy to python/mach/docs/telemetry.rst
--- a/python/mozboot/README.rst
+++ b/python/mach/docs/telemetry.rst
@@ -1,19 +1,22 @@
-mozboot - Bootstrap your system to build Mozilla projects
-=========================================================
+.. _mach_telemetry:
 
-This package contains code used for bootstrapping a system to build
-mozilla-central.
+==============
+Mach Telemetry
+==============
 
-This code is not part of the build system per se. Instead, it is related
-to everything up to invoking the actual build system.
+`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>`.
 
-If you have a copy of the source tree, you run:
+.. toctree::
+   :maxdepth: 1
 
-    python bin/bootstrap.py
+   metrics
 
-If you don't have a copy of the source tree, you can run:
+Updating Generated Metrics Docs
+===============================
 
-    curl https://hg.mozilla.org/mozilla-central/raw-file/default/python/mozboot/bin/bootstrap.py | python -
+When ``metrics.yaml`` is changed, :ref:`the metrics document<metrics>` will need to be updated.
+Glean provides doc-generation tooling for us::
 
-The bootstrap script will download everything it needs from hg.mozilla.org
-automatically!
+    glean_parser translate -f markdown -o python/mach/docs/ python/mach/metrics.yaml python/mach/pings.yaml
--- a/python/mach/mach/base.py
+++ b/python/mach/mach/base.py
@@ -1,22 +1,26 @@
 # 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
+
 
 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, **kwargs):
+    def __init__(self, cwd=None, settings=None, log_manager=None, commands=None,
+                 telemetry=Telemetry.as_noop(), **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():
             setattr(self, k, v)
 
 
 class MachError(Exception):
     """Base class for all errors raised by mach itself."""
--- a/python/mach/mach/logging.py
+++ b/python/mach/mach/logging.py
@@ -291,16 +291,20 @@ class LoggingManager(object):
                 logger.addHandler(handler)
 
     def enable_all_structured_loggers(self, terminal=True, json=True):
         """Enable logging of all structured messages from all loggers.
 
         ``terminal`` and ``json`` determine which log handlers to operate
         on. By default, all known handlers are operated on.
         """
+
+        # Glean makes logs that we're not interested in, so we squelch them.
+        logging.getLogger('glean').setLevel(logging.CRITICAL)
+
         # Remove current handlers from all loggers so we don't double
         # register handlers.
         for logger in self.root_logger.manager.loggerDict.values():
             # Some entries might be logging.PlaceHolder.
             if not isinstance(logger, logging.Logger):
                 continue
 
             if terminal:
--- a/python/mach/mach/main.py
+++ b/python/mach/mach/main.py
@@ -29,16 +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 .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()
 
@@ -385,19 +386,20 @@ 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)
         context = CommandContext(cwd=self.cwd,
                                  settings=self.settings, log_manager=self.log_manager,
-                                 commands=Registrar)
+                                 commands=Registrar, telemetry=telemetry)
 
         if self.populate_context_handler:
             context = ContextWrapper(context, self.populate_context_handler)
 
         parser = self.get_argument_parser(context)
 
         if not len(argv):
             # We don't register the usage until here because if it is globally
@@ -422,16 +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)
 
         # 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
@@ -105,17 +105,17 @@ class MachRegistrar(object):
         end_time = time.time()
 
         result = result or 0
         assert isinstance(result, six.integer_types)
 
         if not debug_command:
             postrun = getattr(context, 'post_dispatch_handler', None)
             if postrun:
-                postrun(context, handler, instance, result,
+                postrun(context, handler, instance, not result,
                         start_time, end_time, self.command_depth, args=kwargs)
         self.command_depth -= 1
 
         return result
 
     def dispatch(self, name, context, argv=None, subcommand=None, **kwargs):
         """Dispatch/run a command.
 
--- a/python/mach/mach/sentry.py
+++ b/python/mach/mach/sentry.py
@@ -5,17 +5,17 @@
 from __future__ import absolute_import
 
 import abc
 import re
 from os.path import expanduser
 
 import sentry_sdk
 from mozboot.util import get_state_dir
-from mozbuild.telemetry import is_telemetry_enabled
+from mach.telemetry import is_telemetry_enabled
 from mozversioncontrol import get_repository_object, InvalidRepoPath
 from six import string_types
 
 # The following developers frequently modify mach code, and testing will commonly cause
 # exceptions to be thrown. We don't want these exceptions reported to Sentry.
 _DEVELOPER_BLOCKLIST = [
     'ahalberstadt@mozilla.com',
     'mhentges@mozilla.com',
new file mode 100644
--- /dev/null
+++ b/python/mach/mach/telemetry.py
@@ -0,0 +1,110 @@
+# 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
+from mock import Mock
+
+from mozboot.util import get_state_dir
+from mozbuild.base import MozbuildObject, BuildEnvironmentNotFoundException
+from mozbuild.telemetry import filter_args
+
+
+class Telemetry(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.
+
+        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()
+
+        try:
+            from glean import Glean, load_metrics, load_pings
+        except ImportError:
+            return cls.as_noop(failed_glean_import=True)
+
+        from pathlib import Path
+
+        Glean.initialize(
+            'mozilla.mach',
+            'Unknown',
+            is_telemetry_enabled(settings),
+            data_dir=Path(get_state_dir()) / 'glean',
+        )
+        from pathlib import Path
+        metrics = load_metrics(Path(__file__).parent.parent / 'metrics.yaml')
+        pings = load_pings(Path(__file__).parent.parent / 'pings.yaml')
+        return cls(metrics, pings, False)
+
+
+def report_invocation_metrics(metrics, command):
+    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
+        # we can't create a MozbuildObject instance).
+        # Without this information, we're unable to filter argv paths, so
+        # we skip submitting them to telemetry.
+        return
+    metrics.mach.argv.set(filter_args(command, sys.argv, instance))
+
+
+def is_applicable_telemetry_environment():
+    if os.environ.get('MACH_MAIN_PID') != str(os.getpid()):
+        # This is a child mach process. Since we're collecting telemetry for the parent,
+        # we don't want to collect telemetry again down here.
+        return False
+
+    if any(e in os.environ for e in ('MOZ_AUTOMATION', 'TASK_ID')):
+        return False
+
+    return True
+
+
+def is_telemetry_enabled(settings):
+    if os.environ.get('DISABLE_TELEMETRY') == '1':
+        return False
+
+    try:
+        return settings.build.telemetry
+    except (AttributeError, KeyError):
+        return False
--- a/python/mach/mach/test/test_telemetry.py
+++ b/python/mach/mach/test/test_telemetry.py
@@ -32,27 +32,34 @@ def run_mach(tmpdir):
     # Use tmpdir as the mozbuild state path, and enable telemetry in
     # a machrc there.
     if PY3:
         update_or_create_build_telemetry_config(str(tmpdir.join('machrc')))
     else:
         update_or_create_build_telemetry_config(text_type(tmpdir.join('machrc')))
     env = dict(os.environ)
     env['MOZBUILD_STATE_PATH'] = str(tmpdir)
-    env['MACH_TELEMETRY_NO_SUBMIT'] = '1'
-    # Let whatever mach command we invoke from tests believe it's the main command.
-    del env['MACH_MAIN_PID']
+    env['TEST_MACH_TELEMETRY_NO_SUBMIT'] = '1'
     mach = os.path.join(buildconfig.topsrcdir, 'mach')
 
     def run(*args, **kwargs):
+        # Let whatever mach command we invoke from tests believe it's the main command.
+        mach_main_pid = env.pop('MACH_MAIN_PID')
+        moz_automation = env.pop('MOZ_AUTOMATION', None)
+        task_id = env.pop('TASK_ID', None)
+
         # Run mach with the provided arguments
         out = subprocess.check_output([sys.executable, mach] + list(args),
                                       stderr=subprocess.STDOUT,
                                       env=env,
                                       **kwargs)
+
+        env['MACH_MAIN_PID'] = mach_main_pid
+        env['MOZ_AUTOMATION'] = moz_automation
+        env['TASK_ID'] = task_id
         # Load any telemetry data that was written
         path = tmpdir.join('telemetry', 'outgoing')
         try:
             if PY3:
                 read_mode = 'r'
             else:
                 read_mode = 'rb'
             return [json.load(f.open(read_mode)) for f in path.listdir()]
new file mode 100644
--- /dev/null
+++ b/python/mach/metrics.yaml
@@ -0,0 +1,244 @@
+# 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/.
+---
+$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",
+      "doc", or "try".
+    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
+  argv:
+    type: string_list
+    description: >
+      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>".
+    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
+  success:
+    type: boolean
+    description: True if the mach invocation succeeded.
+    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
+  duration:
+    type: timespan
+    description: How long it took for the command to complete.
+    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
+
+mach.system:
+  cpu_brand:
+    type: string
+    description: CPU brand string from CPUID.
+    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
+  logical_cores:
+    type: counter
+    description: Number of logical CPU cores present.
+    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
+  physical_cores:
+    type: counter
+    description: Number of physical CPU cores present.
+    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
+  memory:
+    type: memory_distribution
+    memory_unit: gigabyte
+    description: Amount of system memory.
+    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
+
+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
new file mode 100644
--- /dev/null
+++ b/python/mach/pings.yaml
@@ -0,0 +1,19 @@
+# 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/.
+---
+$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.
+  bugs:
+    - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053
+  data_reviews:
+    - https://bugzilla.mozilla.org/show_bug.cgi?id=1291053#c34
+  include_client_id: true
+  notification_emails:
+    - build-telemetry@mozilla.com
+    - mhentges@mozilla.com
--- a/python/mozbuild/mozbuild/controller/building.py
+++ b/python/mozbuild/mozbuild/controller/building.py
@@ -1062,24 +1062,27 @@ 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)
             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)
 
             # 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()
@@ -1105,16 +1108,31 @@ class BuildDriver(MozbuildObject):
                                            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.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'))
+            mozbuild_metrics.ccache.set(get_substs_flag('CCACHE'))
+            mozbuild_metrics.sccache.set(get_substs_flag('MOZ_USING_SCCACHE'))
+            mozbuild_metrics.icecream.set(get_substs_flag('CXX_IS_ICECREAM'))
+
             all_backends = config.substs.get('BUILD_BACKENDS', [None])
             active_backend = all_backends[0]
 
             status = None
 
             if (not config_rc and
                 any([self.backend_out_of_date(mozpath.join(self.topobjdir,
                                                            'backend.%sBackend' %
@@ -1576,16 +1594,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)
         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/telemetry.py
+++ b/python/mozbuild/mozbuild/telemetry.py
@@ -1,13 +1,13 @@
 # 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, print_function, unicode_literals
+from __future__ import division, absolute_import, print_function, unicode_literals
 
 '''
 This file contains a voluptuous schema definition for build system telemetry, and functions
 to fill an instance of that schema for a single mach invocation.
 '''
 
 import json
 import os
@@ -23,19 +23,17 @@ from voluptuous import (
     Optional,
     MultipleInvalid,
     Required,
     Schema,
 )
 from voluptuous.validators import Datetime
 
 import mozpack.path as mozpath
-from .base import (
-    BuildEnvironmentNotFoundException,
-)
+from .base import BuildEnvironmentNotFoundException
 from .configure.constants import CompilerType
 
 schema = Schema({
     Required('client_id', description='A UUID to uniquely identify a client'): Any(*string_types),
     Required('time', description='Time at which this event happened'): Datetime(),
     Required('command', description='The mach command that was invoked'): Any(*string_types),
     Required('argv', description=(
         'Full mach commandline. ' +
@@ -168,44 +166,59 @@ def get_cpu_brand():
     '''
     return {
         'Linux': cpu_brand_linux,
         'Windows': cpu_brand_windows,
         'Darwin': cpu_brand_mac,
     }.get(platform.system(), lambda: None)()
 
 
+def get_os_name():
+    return {
+        'Linux': 'linux',
+        'Windows': 'windows',
+        'Darwin': 'macos',
+    }.get(platform.system(), 'other')
+
+
+def get_psutil_stats():
+    '''Return whether psutil exists and its associated stats.
+
+    @returns (bool, int, int, int) whether psutil exists, the logical CPU count,
+        physical CPU count, and total number of bytes of memory.
+    '''
+    try:
+        import psutil
+
+        return (
+            True,
+            psutil.cpu_count(),
+            psutil.cpu_count(logical=False),
+            psutil.virtual_memory().total)
+    except ImportError:
+        return False, None, None, None
+
+
 def get_system_info():
     '''
     Gather info to fill the `system` keys in the schema.
     '''
     # Normalize OS names a bit, and bucket non-tier-1 platforms into "other".
+    has_psutil, logical_cores, physical_cores, memory_total = get_psutil_stats()
     info = {
-        'os': {
-            'Linux': 'linux',
-            'Windows': 'windows',
-            'Darwin': 'macos',
-        }.get(platform.system(), 'other')
+        'os': get_os_name(),
     }
-    try:
-        import psutil
-
-        info['logical_cores'] = psutil.cpu_count()
-        physical_cores = psutil.cpu_count(logical=False)
+    if has_psutil:
+        # `total` on Linux is gathered from /proc/meminfo's `MemTotal`, which is the
+        # total amount of physical memory minus some kernel usage, so round up to the
+        # nearest GB to get a sensible answer.
+        info['memory_gb'] = int(math.ceil(float(memory_total) / (1024 * 1024 * 1024)))
+        info['logical_cores'] = logical_cores
         if physical_cores is not None:
             info['physical_cores'] = physical_cores
-        # `total` on Linux is gathered from /proc/meminfo's `MemTotal`, which is the total
-        # amount of physical memory minus some kernel usage, so round up to the nearest GB
-        # to get a sensible answer.
-        info['memory_gb'] = int(
-            math.ceil(float(psutil.virtual_memory().total) / (1024 * 1024 * 1024)))
-    except ImportError:
-        # TODO: sort out psutil availability on Windows, or write a fallback impl for Windows.
-        # https://bugzilla.mozilla.org/show_bug.cgi?id=1481612
-        pass
     cpu_brand = get_cpu_brand()
     if cpu_brand is not None:
         info['cpu_brand'] = cpu_brand
     # TODO: drive_is_ssd, virtual_machine: https://bugzilla.mozilla.org/show_bug.cgi?id=1481613
     return info
 
 
 def get_build_opts(substs):
@@ -244,58 +257,69 @@ def get_build_attrs(attrs):
     usage = attrs.get('usage')
     if usage:
         cpu_percent = usage.get('cpu_percent')
         if cpu_percent:
             res['cpu_percent'] = int(round(cpu_percent))
     return res
 
 
-def filter_args(command, argv, paths):
+def filter_args(command, argv, instance):
     '''
     Given the full list of command-line arguments, remove anything up to and including `command`,
     and attempt to filter absolute pathnames out of any arguments after that.
+    '''
 
-    `paths` is a dict whose keys are pathnames and values are sigils that should be used to
-    replace those pathnames.
-    '''
+    # Each key is a pathname and the values are replacement sigils
+    paths = {
+        instance.topsrcdir: '$topsrcdir/',
+        instance.topobjdir: '$topobjdir/',
+        mozpath.normpath(os.path.expanduser('~')): '$HOME/',
+        # This might override one of the existing entries, that's OK.
+        # We don't use a sigil here because we treat all arguments as potentially relative
+        # paths, so we'd like to get them back as they were specified.
+        mozpath.normpath(os.getcwd()): '',
+    }
+
     args = list(argv)
     while args:
         a = args.pop(0)
         if a == command:
             break
 
     def filter_path(p):
         p = mozpath.abspath(p)
         base = mozpath.basedir(p, paths.keys())
         if base:
             return paths[base] + mozpath.relpath(p, base)
         # Best-effort.
         return '<path omitted>'
     return [filter_path(arg) for arg in args]
 
 
-def gather_telemetry(command='', success=False, start_time=None, end_time=None,
-                     mach_context=None, substs={}, paths={}, command_attrs=None):
+def gather_telemetry(command, success, start_time, end_time, mach_context,
+                     instance, command_attrs):
     '''
     Gather telemetry about the build and the user's system and pass it to the telemetry
     handler to be stored for later submission.
 
-    `paths` is a dict whose keys are pathnames and values are sigils that should be used to
-    replace those pathnames.
+    Any absolute paths on the command line will be made relative to a relevant base path
+    or replaced with a placeholder to avoid including paths from developer's machines.
+    '''
+    try:
+        substs = instance.substs
+    except BuildEnvironmentNotFoundException:
+        substs = {}
 
-    Any absolute paths on the command line will be made relative to `paths` or replaced
-    with a placeholder to avoid including paths from developer's machines.
-    '''
     data = {
         'client_id': get_client_id(mach_context.state_dir),
         # Get an rfc3339 datetime string.
         'time': datetime.utcfromtimestamp(start_time).strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
         'command': command,
-        'argv': filter_args(command, sys.argv, paths),
+        'argv': filter_args(command, sys.argv, instance),
         'success': success,
         # TODO: use a monotonic clock: https://bugzilla.mozilla.org/show_bug.cgi?id=1481624
         'duration_ms': int((end_time - start_time) * 1000),
         'build_opts': get_build_opts(substs),
         'build_attrs': get_build_attrs(command_attrs),
         'system': get_system_info(),
         # TODO: exception: https://bugzilla.mozilla.org/show_bug.cgi?id=1481617
         # TODO: file_types_changed: https://bugzilla.mozilla.org/show_bug.cgi?id=1481774
@@ -335,19 +359,8 @@ def verify_statedir(statedir):
 
     if not os.path.isdir(outgoing):
         raise Exception('{} does not exist'.format(outgoing))
 
     if not os.path.isdir(submitted):
         os.mkdir(submitted)
 
     return outgoing, submitted, telemetry_log
-
-
-def is_telemetry_enabled(settings):
-    # Don't write telemetry data for 'mach' when 'DISABLE_TELEMETRY' is set.
-    if os.environ.get('DISABLE_TELEMETRY') == '1':
-        return False
-
-    try:
-        return settings.build.telemetry
-    except (AttributeError, KeyError):
-        return False