Skip to content

Commit f728505

Browse files
committed
Python: Add extractor flag telemetry
Records any non-default extractor flags (without their arguments) as a normalised string. This will enable us to determine which flags are actually used (and which ones we might therefore get rid of). When there are no flags other than the ones the autobuilder injects, we simply report the string `"default"`. That way, there's no need to remember exactly which flags are enabled by default during extraction.
1 parent 30cbb14 commit f728505

7 files changed

Lines changed: 73 additions & 9 deletions

File tree

python/extractor/cli-integration-test/writing-diagnostics/diagnostics.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@
163163
}
164164
{
165165
"attributes": {
166+
"extractor_flags": "default",
166167
"extractor_version": "7.1.10",
167168
"python_analysis_version": "3.12",
168169
"python_runtime_version": "3.12.3"

python/extractor/cli-integration-test/writing-diagnostics/test_diagnostics_output.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
for path in glob.glob(os.path.join(diagnostic_dir, "*.jsonl")):
1212
with open(path) as diagnostic_file:
1313
diagnostics.extend(json.loads(line) for line in diagnostic_file)
14+
summary = [
15+
diagnostic
16+
for diagnostic in diagnostics
17+
if diagnostic["source"]["id"] == "py/extractor/summary"
18+
]
19+
assert len(summary) == 1
20+
assert summary[0]["attributes"]["extractor_flags"] == "default"
1421
parser_statistics = [
1522
diagnostic
1623
for diagnostic in diagnostics

python/extractor/semmle/cmdline.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from optparse import OptionParser, OptionGroup, HelpFormatter
1+
from optparse import Option, OptionParser, OptionGroup, HelpFormatter
22
import shlex
33
import sys
44
import os
@@ -8,9 +8,21 @@
88
from semmle.util import VERSION
99

1010

11+
DEFAULT_AUTOBUILDER_FLAGS = {"R", "c", "v", "verbosity", "z"}
12+
13+
14+
class RecordingOption(Option):
15+
def process(self, opt, value, values, parser):
16+
flag = (self._short_opts or self._long_opts)[0].lstrip("-")
17+
if flag not in DEFAULT_AUTOBUILDER_FLAGS:
18+
parser.extractor_flags.add(flag)
19+
return Option.process(self, opt, value, values, parser)
20+
21+
1122
def make_parser():
1223
'''Parse command_line, returning options, arguments'''
13-
parser = OptionParser(add_help_option=False, version='%s' % VERSION)
24+
parser = OptionParser(option_class=RecordingOption, add_help_option=False, version='%s' % VERSION)
25+
parser.extractor_flags = set()
1426

1527
import_options = OptionGroup(parser, "Import following options",
1628
description="Note that -a -n -g and -t are included for backwards compatibility. They are ignored")
@@ -172,6 +184,7 @@ def parse(command_line):
172184
setattr(options, attr, dval)
173185
args.extend(extra_args)
174186
del options.file
187+
options.extractor_flags = sorted(parser.extractor_flags)
175188
if options.help:
176189
if options.verbose:
177190
for opt in parser._get_all_options():

python/extractor/semmle/logging.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,12 +358,13 @@ def with_timestamp(self, timestamp):
358358
self.timestamp = timestamp
359359
return self
360360

361-
def extractor_telemetry_message():
361+
def extractor_telemetry_message(extractor_flags):
362362
return (DiagnosticMessage(Source("py/extractor/summary", "Python extractor telemetry"), Severity.NOTE)
363363
.markdown("Internal telemetry for the Python extractor.\n\nNo action needed.")
364364
.attribute("python_analysis_version", get_analysis_version())
365365
.attribute("python_runtime_version", platform.python_version())
366366
.attribute("extractor_version", VERSION)
367+
.attribute("extractor_flags", " ".join(extractor_flags) or "default")
367368
.telemetry()
368369
)
369370

python/extractor/semmle/worker.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,9 @@ def _drain_queue(queue):
240240
#Emptied queue as best we can.
241241
pass
242242

243-
def _write_extractor_telemetry(diagnostics_writer, logger: Logger):
243+
def _write_extractor_telemetry(diagnostics_writer, logger: Logger, extractor_flags):
244244
try:
245-
diagnostics_writer.write(extractor_telemetry_message())
245+
diagnostics_writer.write(extractor_telemetry_message(extractor_flags))
246246
except OSError as ex:
247247
logger.warning("Failed to write extractor telemetry: %s", ex)
248248

@@ -304,7 +304,7 @@ def _extract_loop(proc_id, queue, trap_dir, archive, options, reply_queue, logge
304304
sys.exit(2)
305305
logger.set_process_id(proc_id)
306306
if write_global_data:
307-
_write_extractor_telemetry(diagnostics_writer, logger)
307+
_write_extractor_telemetry(diagnostics_writer, logger, options.extractor_flags)
308308
try:
309309
if options.trace_only:
310310
extractor = ModulePrinter(options, trap_dir, archive, renamer, logger, diagnostics_writer)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from semmle import cmdline
2+
3+
4+
def test_records_flags_without_values():
5+
options, args = cmdline.parse(
6+
[
7+
"--verbosity=3",
8+
"-zall",
9+
"-R",
10+
"/src",
11+
"-vv",
12+
"--path",
13+
"/lib",
14+
"-p",
15+
"/other-lib",
16+
"module",
17+
]
18+
)
19+
20+
assert options.extractor_flags == ["p"]
21+
assert args == ["module"]
22+
23+
24+
def test_records_flags_from_option_file(tmp_path):
25+
options_file = tmp_path / "extractor-options"
26+
options_file.write_text("--colorize --max-import-depth 2")
27+
28+
options, _ = cmdline.parse(["-f", str(options_file)])
29+
30+
assert options.extractor_flags == [
31+
"colorize",
32+
"f",
33+
"max-import-depth",
34+
]

python/extractor/tests/test_diagnostics.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
def test_extractor_telemetry_message(mocker):
1010
mocker.patch("semmle.logging.get_analysis_version", return_value="3.13")
1111

12-
message = logging.extractor_telemetry_message().to_dict()
12+
message = logging.extractor_telemetry_message(["colorize", "p"]).to_dict()
1313
message.pop("timestamp")
1414

1515
assert message == {
@@ -29,6 +29,7 @@ def test_extractor_telemetry_message(mocker):
2929
"python_analysis_version": "3.13",
3030
"python_runtime_version": platform.python_version(),
3131
"extractor_version": util.VERSION,
32+
"extractor_flags": "colorize p",
3233
},
3334
}
3435

@@ -59,17 +60,24 @@ def test_parser_statistics_telemetry_message():
5960
}
6061

6162

63+
def test_extractor_telemetry_message_includes_empty_flags():
64+
message = logging.extractor_telemetry_message([]).to_dict()
65+
66+
assert message["attributes"]["extractor_flags"] == "default"
67+
68+
6269
def test_write_extractor_telemetry(mocker):
6370
diagnostics_writer = mocker.Mock()
6471
logger = mocker.Mock()
6572

66-
worker._write_extractor_telemetry(diagnostics_writer, logger)
73+
worker._write_extractor_telemetry(diagnostics_writer, logger, ["quiet"])
6774

6875
diagnostics_writer.write.assert_called_once()
6976
assert diagnostics_writer.write.call_args.args[0].to_dict()["attributes"] == {
7077
"python_analysis_version": util.get_analysis_version(),
7178
"python_runtime_version": platform.python_version(),
7279
"extractor_version": util.VERSION,
80+
"extractor_flags": "quiet",
7381
}
7482
logger.warning.assert_not_called()
7583

@@ -79,7 +87,7 @@ def test_write_extractor_telemetry_handles_io_error(mocker):
7987
diagnostics_writer.write.side_effect = OSError("write failed")
8088
logger = mocker.Mock()
8189

82-
worker._write_extractor_telemetry(diagnostics_writer, logger)
90+
worker._write_extractor_telemetry(diagnostics_writer, logger, [])
8391

8492
logger.warning.assert_called_once_with(
8593
"Failed to write extractor telemetry: %s", diagnostics_writer.write.side_effect

0 commit comments

Comments
 (0)