Skip to content

Commit 1e57e0a

Browse files
authored
Merge pull request #22502 from github/tausbn/python-add-various-kinds-of-extractor-telemetry
Python: Add various kinds of extractor telemetry
2 parents 37296f8 + e3dff5e commit 1e57e0a

18 files changed

Lines changed: 374 additions & 14 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
x = 1
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
match 1:
2+
case 1:
3+
pass
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
3+
set -Eeuo pipefail # see https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
4+
5+
set -x
6+
7+
CODEQL=${CODEQL:-codeql}
8+
9+
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
10+
cd "$SCRIPTDIR"
11+
12+
rm -rf db
13+
14+
$CODEQL database create db --language python --source-root repo_dir/
15+
python3 test_parser_telemetry.py db
16+
17+
rm -rf db
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import glob
2+
import json
3+
import os
4+
import sys
5+
6+
7+
database = sys.argv[1]
8+
diagnostics = []
9+
diagnostic_dir = os.path.join(database, "diagnostic", "extractors", "python")
10+
for path in glob.glob(os.path.join(diagnostic_dir, "*.jsonl")):
11+
with open(path) as diagnostic_file:
12+
diagnostics.extend(json.loads(line) for line in diagnostic_file)
13+
parser_statistics = [
14+
diagnostic
15+
for diagnostic in diagnostics
16+
if diagnostic["source"]["id"] == "py/extractor/parser-statistics"
17+
]
18+
actual = (
19+
sum(diagnostic["attributes"]["old_parser_file_count"] for diagnostic in parser_statistics),
20+
sum(
21+
diagnostic["attributes"]["tree_sitter_parser_file_count"]
22+
for diagnostic in parser_statistics
23+
),
24+
)
25+
assert actual == (1, 1), actual

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,24 @@
161161
"telemetry": true
162162
}
163163
}
164+
{
165+
"attributes": {
166+
"extractor_flags": "default",
167+
"extractor_version": "7.1.10",
168+
"python_analysis_version": "3.12",
169+
"python_runtime_version": "3.12.3"
170+
},
171+
"markdownMessage": "Internal telemetry for the Python extractor.\n\nNo action needed.",
172+
"severity": "note",
173+
"source": {
174+
"extractorName": "python",
175+
"id": "py/extractor/summary",
176+
"name": "Python extractor telemetry"
177+
},
178+
"timestamp": "2026-09-01T13:41:33.056818Z",
179+
"visibility": {
180+
"cliSummaryTable": false,
181+
"statusPage": false,
182+
"telemetry": true
183+
}
184+
}
Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
11
import os
22
import sys
3+
import glob
4+
import json
35
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "..", "integration-tests"))
46
import diagnostics_test_utils
57

68
test_db = "db"
7-
diagnostics_test_utils.check_diagnostics(".", test_db, skip_attributes=True)
9+
diagnostics = []
10+
diagnostic_dir = os.path.join(test_db, "diagnostic", "extractors", "python")
11+
for path in glob.glob(os.path.join(diagnostic_dir, "*.jsonl")):
12+
with open(path) as diagnostic_file:
13+
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"
21+
diagnostics_test_utils.check_diagnostics(
22+
".",
23+
test_db,
24+
skip_attributes=True,
25+
replacements={
26+
r'"py/extractor/parser-statistics"': '"cli/py/extractor/parser-statistics"'
27+
},
28+
)

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/extractors/module_printer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ class ModulePrinter(object):
66

77
name = "module printer"
88

9-
def __init__(self, options, trap_folder, src_archive, renamer, logger):
9+
def __init__(self, options, trap_folder, src_archive, renamer, logger, diagnostics_writer):
1010
self.logger = logger
11-
self.py_extractor = PythonExtractor(options, trap_folder, src_archive, logger)
11+
self.py_extractor = PythonExtractor(options, trap_folder, src_archive, logger, diagnostics_writer)
1212

1313
def process(self, unit):
1414
imports = ()

python/extractor/semmle/extractors/py_extractor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def __init__(self, options, trap_folder, src_archive, logger: Logger, diagnostic
1616
self.module_extractor = extractor.Extractor.from_options(options, trap_folder, src_archive, logger, diagnostics_writer)
1717
self.finder = finder.Finder.from_options_and_env(options, logger)
1818
self.importer = imports.importer_from_options(options, self.finder, logger)
19+
self.diagnostics_writer = diagnostics_writer
1920

2021
def _get_module_and_imports(self, unit):
2122
if not isinstance(unit, util.FileExtractable):
@@ -24,7 +25,7 @@ def _get_module_and_imports(self, unit):
2425
module = self.finder.from_extractable(unit)
2526
if module is None:
2627
return None, ()
27-
py_module = module.load(self.logger)
28+
py_module = module.load(self.logger, self.diagnostics_writer)
2829
if py_module is None:
2930
return None, ()
3031
imports = set(mod.get_extractable() for mod in self.importer.get_imports(module, py_module))

python/extractor/semmle/logging.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import multiprocessing
99
import enum
1010
import datetime
11+
import platform
12+
13+
from semmle.util import VERSION, get_analysis_version
1114

1215

1316
#Use standard Semmle logging levels
@@ -355,6 +358,24 @@ def with_timestamp(self, timestamp):
355358
self.timestamp = timestamp
356359
return self
357360

361+
def extractor_telemetry_message(extractor_flags):
362+
return (DiagnosticMessage(Source("py/extractor/summary", "Python extractor telemetry"), Severity.NOTE)
363+
.markdown("Internal telemetry for the Python extractor.\n\nNo action needed.")
364+
.attribute("python_analysis_version", get_analysis_version())
365+
.attribute("python_runtime_version", platform.python_version())
366+
.attribute("extractor_version", VERSION)
367+
.attribute("extractor_flags", " ".join(extractor_flags) or "default")
368+
.telemetry()
369+
)
370+
371+
def parser_statistics_telemetry_message(old_parser_file_count, tree_sitter_parser_file_count):
372+
return (DiagnosticMessage(Source("py/extractor/parser-statistics", "Python parser statistics"), Severity.NOTE)
373+
.markdown("Internal parser telemetry for the Python extractor.\n\nNo action needed.")
374+
.attribute("old_parser_file_count", old_parser_file_count)
375+
.attribute("tree_sitter_parser_file_count", tree_sitter_parser_file_count)
376+
.telemetry()
377+
)
378+
358379
def get_stack_trace_lines():
359380
"""Creates a stack trace for inclusion into the `attributes` part of a diagnostic message.
360381
Limits the size of the stack trace to 5000 characters, so as to not make the SARIF file overly big.

0 commit comments

Comments
 (0)