Skip to content

Commit 8116060

Browse files
committed
Python: Add telemetry for Python analysis version
Here' `python_analysis_version` is the version of Python that we are analysing the code as. In practice, all we care about is the major version, but we might as well include the full thing (since it can be overridden on the command line). The `python_runtime_version` is the actual version of Python that ran the extractor.
1 parent 4138d65 commit 8116060

5 files changed

Lines changed: 96 additions & 2 deletions

File tree

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,23 @@
161161
"telemetry": true
162162
}
163163
}
164+
{
165+
"attributes": {
166+
"extractor_version": "7.1.10",
167+
"python_analysis_version": "3.12",
168+
"python_runtime_version": "3.12.3"
169+
},
170+
"markdownMessage": "Internal telemetry for the Python extractor.\n\nNo action needed.",
171+
"severity": "note",
172+
"source": {
173+
"extractorName": "python",
174+
"id": "py/extractor/summary",
175+
"name": "Python extractor telemetry"
176+
},
177+
"timestamp": "2026-09-01T13:41:33.056818Z",
178+
"visibility": {
179+
"cliSummaryTable": false,
180+
"statusPage": false,
181+
"telemetry": true
182+
}
183+
}

python/extractor/semmle/logging.py

Lines changed: 12 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,15 @@ def with_timestamp(self, timestamp):
355358
self.timestamp = timestamp
356359
return self
357360

361+
def extractor_telemetry_message():
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+
.telemetry()
368+
)
369+
358370
def get_stack_trace_lines():
359371
"""Creates a stack trace for inclusion into the `attributes` part of a diagnostic message.
360372
Limits the size of the stack trace to 5000 characters, so as to not make the SARIF file overly big.

python/extractor/semmle/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
#Semantic version of extractor.
1212
#Update this if any changes are made
13-
VERSION = "7.1.9"
13+
VERSION = "7.1.10"
1414

1515
PY_EXTENSIONS = ".py", ".pyw"
1616

python/extractor/semmle/worker.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from semmle.extractors import SuperExtractor, ModulePrinter, SkippedBuiltin
1111
from semmle.profiling import get_profiler
1212
from semmle.path_rename import renamer_from_options_and_env
13-
from semmle.logging import WARN, recursion_error_message, internal_error_message, Logger
13+
from semmle.logging import WARN, recursion_error_message, internal_error_message, extractor_telemetry_message, Logger
1414
from semmle.util import FileExtractable, FolderExtractable
1515

1616
class ExtractorFailure(Exception):
@@ -239,6 +239,12 @@ def _drain_queue(queue):
239239
#Emptied queue as best we can.
240240
pass
241241

242+
def _write_extractor_telemetry(diagnostics_writer, logger: Logger):
243+
try:
244+
diagnostics_writer.write(extractor_telemetry_message())
245+
except OSError as ex:
246+
logger.warning("Failed to write extractor telemetry: %s", ex)
247+
242248
class DiagnosticsWriter(object):
243249
def __init__(self, proc_id):
244250
self.proc_id = proc_id
@@ -276,6 +282,8 @@ def _extract_loop(proc_id, queue, trap_dir, archive, options, reply_queue, logge
276282
reply_queue.put(("INTERRUPT", None, None))
277283
sys.exit(2)
278284
logger.set_process_id(proc_id)
285+
if write_global_data:
286+
_write_extractor_telemetry(diagnostics_writer, logger)
279287
try:
280288
if options.trace_only:
281289
extractor = ModulePrinter(options, trap_dir, archive, renamer, logger)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import platform
2+
3+
from semmle import logging
4+
from semmle import util
5+
from semmle import worker
6+
7+
8+
def test_extractor_telemetry_message(mocker):
9+
mocker.patch("semmle.logging.get_analysis_version", return_value="3.13")
10+
11+
message = logging.extractor_telemetry_message().to_dict()
12+
message.pop("timestamp")
13+
14+
assert message == {
15+
"source": {
16+
"id": "py/extractor/summary",
17+
"name": "Python extractor telemetry",
18+
"extractorName": "python",
19+
},
20+
"severity": "note",
21+
"markdownMessage": "Internal telemetry for the Python extractor.\n\nNo action needed.",
22+
"visibility": {
23+
"statusPage": False,
24+
"cliSummaryTable": False,
25+
"telemetry": True,
26+
},
27+
"attributes": {
28+
"python_analysis_version": "3.13",
29+
"python_runtime_version": platform.python_version(),
30+
"extractor_version": util.VERSION,
31+
},
32+
}
33+
34+
35+
def test_write_extractor_telemetry(mocker):
36+
diagnostics_writer = mocker.Mock()
37+
logger = mocker.Mock()
38+
39+
worker._write_extractor_telemetry(diagnostics_writer, logger)
40+
41+
diagnostics_writer.write.assert_called_once()
42+
logger.warning.assert_not_called()
43+
44+
45+
def test_write_extractor_telemetry_handles_io_error(mocker):
46+
diagnostics_writer = mocker.Mock()
47+
diagnostics_writer.write.side_effect = OSError("write failed")
48+
logger = mocker.Mock()
49+
50+
worker._write_extractor_telemetry(diagnostics_writer, logger)
51+
52+
logger.warning.assert_called_once_with(
53+
"Failed to write extractor telemetry: %s", diagnostics_writer.write.side_effect
54+
)

0 commit comments

Comments
 (0)