Skip to content

Commit a22456e

Browse files
committed
SONARPY-2464 Add safety around the telemetry mechanism
1 parent 0862bdd commit a22456e

File tree

5 files changed

+150
-35
lines changed

5 files changed

+150
-35
lines changed

sonar-python-plugin/src/main/java/org/sonar/plugins/python/IPynbSensor.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,18 +92,18 @@ private void processNotebooksFiles(List<PythonInputFile> pythonFiles, SensorCont
9292
PythonIndexer pythonIndexer = new SonarQubePythonIndexer(pythonFiles, cacheContext, context);
9393
PythonScanner scanner = new PythonScanner(context, checks, fileLinesContextFactory, noSonarFilter, PythonParser.createIPythonParser(), pythonIndexer);
9494
scanner.execute(pythonFiles, context);
95-
sensorTelemetryStorage.updateMetric(SensorTelemetryStorage.MetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY, String.valueOf(scanner.getRecognitionErrorCount()));
95+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY, scanner.getRecognitionErrorCount());
9696
}
9797

9898
private List<PythonInputFile> parseNotebooks(List<PythonInputFile> pythonFiles, SensorContext context) {
9999
List<PythonInputFile> generatedIPythonFiles = new ArrayList<>();
100100

101-
sensorTelemetryStorage.updateMetric(SensorTelemetryStorage.MetricKey.NOTEBOOK_TOTAL_KEY, String.valueOf(pythonFiles.size()));
101+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.NOTEBOOK_TOTAL_KEY, pythonFiles.size());
102102
var numberOfExceptions = 0;
103103

104104
for (PythonInputFile inputFile : pythonFiles) {
105105
try {
106-
sensorTelemetryStorage.updateMetric(SensorTelemetryStorage.MetricKey.NOTEBOOK_PRESENT_KEY, "1");
106+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.NOTEBOOK_PRESENT_KEY, "1");
107107
var result = IpynbNotebookParser.parseNotebook(inputFile);
108108
result.ifPresent(generatedIPythonFiles::add);
109109
} catch (Exception e) {
@@ -114,7 +114,7 @@ private List<PythonInputFile> parseNotebooks(List<PythonInputFile> pythonFiles,
114114
}
115115
}
116116

117-
sensorTelemetryStorage.updateMetric(SensorTelemetryStorage.MetricKey.NOTEBOOK_EXCEPTION_KEY, String.valueOf(numberOfExceptions));
117+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.NOTEBOOK_EXCEPTION_KEY, String.valueOf(numberOfExceptions));
118118
return generatedIPythonFiles;
119119
}
120120

sonar-python-plugin/src/main/java/org/sonar/plugins/python/SensorTelemetryStorage.java

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,43 @@
1616
*/
1717
package org.sonar.plugins.python;
1818

19-
import java.util.HashMap;
19+
import java.util.EnumMap;
2020
import java.util.Map;
2121
import org.slf4j.Logger;
2222
import org.slf4j.LoggerFactory;
2323
import org.sonar.api.batch.sensor.SensorContext;
24+
import org.sonar.api.utils.Version;
2425

2526
public class SensorTelemetryStorage {
2627
private static final Logger LOG = LoggerFactory.getLogger(SensorTelemetryStorage.class);
2728

28-
private final Map<String, String> data = new HashMap<>();
29+
private final Map<TelemetryMetricKey, String> data = new EnumMap<>(TelemetryMetricKey.class);
2930

3031
public void send(SensorContext sensorContext) {
31-
data.forEach((k, v) -> {
32-
LOG.info("Metrics property: {}={}", k, v);
33-
sensorContext.addTelemetryProperty(k, v);
34-
});
32+
// This try/catch block should be useless, as in the worst case it should be a no-op depending on the SensorContext implementation
33+
// It exists to be extra sure for the LTA
34+
try {
35+
var apiVersion = sensorContext.runtime().getApiVersion();
36+
if (apiVersion.isGreaterThanOrEqual(Version.create(10, 9))) {
37+
data.forEach((k, v) -> {
38+
LOG.info("Collected metric: {}={}", k, v);
39+
sensorContext.addTelemetryProperty(k.key(), v);
40+
});
41+
42+
} else {
43+
LOG.info("Skipping sending metrics because the plugin API version is {}", apiVersion);
44+
}
45+
} catch (Exception e) {
46+
LOG.error("Failed to send metrics", e);
47+
}
3548
}
3649

37-
public void updateMetric(MetricKey key, String value) {
38-
data.put(key.key(), value);
50+
public void updateMetric(TelemetryMetricKey key, String value) {
51+
data.put(key, value);
3952
}
4053

41-
public enum MetricKey {
42-
NOTEBOOK_PRESENT_KEY("python.notebook.present"),
43-
NOTEBOOK_TOTAL_KEY("python.notebook.total"),
44-
NOTEBOOK_RECOGNITION_ERROR_KEY("python.notebook.recognition_error"),
45-
NOTEBOOK_EXCEPTION_KEY("python.notebook.exceptions");
46-
47-
private final String key;
48-
49-
MetricKey(String key) {
50-
this.key = key;
51-
}
52-
53-
public String key() {
54-
return key;
55-
}
54+
public void updateMetric(TelemetryMetricKey key, int value) {
55+
data.put(key, String.valueOf(value));
5656
}
57+
5758
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2024 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.python;
18+
19+
public enum TelemetryMetricKey {
20+
NOTEBOOK_PRESENT_KEY("python.notebook.present"),
21+
NOTEBOOK_TOTAL_KEY("python.notebook.total"),
22+
NOTEBOOK_RECOGNITION_ERROR_KEY("python.notebook.recognition_error"),
23+
NOTEBOOK_EXCEPTION_KEY("python.notebook.exceptions");
24+
25+
private final String key;
26+
27+
TelemetryMetricKey(String key) {
28+
this.key = key;
29+
}
30+
31+
public String key() {
32+
return key;
33+
}
34+
}

sonar-python-plugin/src/test/java/org/sonar/plugins/python/IPynbSensorTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,10 @@ void test_notebook_sensor_is_executed_on_json_file() {
218218
var sensor = spy(notebookSensor());
219219
var contextSpy = spy(context);
220220
assertDoesNotThrow(() -> sensor.execute(contextSpy));
221-
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(SensorTelemetryStorage.MetricKey.NOTEBOOK_PRESENT_KEY.key(), "1");
222-
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(SensorTelemetryStorage.MetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY.key(), "0");
223-
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(SensorTelemetryStorage.MetricKey.NOTEBOOK_TOTAL_KEY.key(), "1");
224-
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(SensorTelemetryStorage.MetricKey.NOTEBOOK_EXCEPTION_KEY.key(), "0");
221+
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(TelemetryMetricKey.NOTEBOOK_PRESENT_KEY.key(), "1");
222+
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(TelemetryMetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY.key(), "0");
223+
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(TelemetryMetricKey.NOTEBOOK_TOTAL_KEY.key(), "1");
224+
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(TelemetryMetricKey.NOTEBOOK_EXCEPTION_KEY.key(), "0");
225225
}
226226

227227
@Test
@@ -243,9 +243,9 @@ void test_notebook_sensor_parse_error_on_valid_line() {
243243
sensor.execute(contextSpy);
244244
var logs = String.join("", logTester.logs());
245245
assertThat(logs).contains("Unable to parse file: notebook_parse_error.ipynbParse error at line 1");
246-
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(SensorTelemetryStorage.MetricKey.NOTEBOOK_PRESENT_KEY.key(), "1");
247-
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(SensorTelemetryStorage.MetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY.key(), "1");
248-
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(SensorTelemetryStorage.MetricKey.NOTEBOOK_TOTAL_KEY.key(), "1");
249-
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(SensorTelemetryStorage.MetricKey.NOTEBOOK_EXCEPTION_KEY.key(), "0");
246+
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(TelemetryMetricKey.NOTEBOOK_PRESENT_KEY.key(), "1");
247+
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(TelemetryMetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY.key(), "1");
248+
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(TelemetryMetricKey.NOTEBOOK_TOTAL_KEY.key(), "1");
249+
verify(contextSpy, Mockito.times(1)).addTelemetryProperty(TelemetryMetricKey.NOTEBOOK_EXCEPTION_KEY.key(), "0");
250250
}
251251
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2024 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.python;
18+
19+
import java.io.File;
20+
import org.junit.jupiter.api.Assertions;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.RegisterExtension;
23+
import org.slf4j.event.Level;
24+
import org.sonar.api.SonarEdition;
25+
import org.sonar.api.SonarQubeSide;
26+
import org.sonar.api.batch.sensor.SensorContext;
27+
import org.sonar.api.batch.sensor.internal.SensorContextTester;
28+
import org.sonar.api.internal.SonarRuntimeImpl;
29+
import org.sonar.api.testfixtures.log.LogTesterJUnit5;
30+
import org.sonar.api.utils.Version;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.mockito.ArgumentMatchers.any;
34+
import static org.mockito.Mockito.doThrow;
35+
import static org.mockito.Mockito.never;
36+
import static org.mockito.Mockito.spy;
37+
import static org.mockito.Mockito.verify;
38+
39+
class SensorTelemetryStorageTest {
40+
41+
@RegisterExtension
42+
public LogTesterJUnit5 logTester = new LogTesterJUnit5().setLevel(Level.DEBUG);
43+
44+
@Test
45+
void no_send_on_incompatible_version() {
46+
var sensorContext = sensorContext(Version.create(10, 8));
47+
var storage = new SensorTelemetryStorage();
48+
storage.updateMetric(TelemetryMetricKey.NOTEBOOK_PRESENT_KEY, "1");
49+
storage.send(sensorContext);
50+
51+
verify(sensorContext, never()).addTelemetryProperty(any(), any());
52+
assertThat(logTester.logs()).contains("Skipping sending metrics because the plugin API version is 10.8");
53+
}
54+
55+
@Test
56+
void send_after_10_9() {
57+
var sensorContext = sensorContext(Version.create(10, 9));
58+
var storage = new SensorTelemetryStorage();
59+
storage.updateMetric(TelemetryMetricKey.NOTEBOOK_PRESENT_KEY, "1");
60+
storage.send(sensorContext);
61+
62+
verify(sensorContext).addTelemetryProperty(TelemetryMetricKey.NOTEBOOK_PRESENT_KEY.key(), "1");
63+
}
64+
65+
@Test
66+
void no_crash_on_exception() {
67+
var sensorContext = sensorContext(Version.create(10, 9));
68+
doThrow(new RuntimeException("Some exception")).when(sensorContext).addTelemetryProperty(any(), any());
69+
var storage = new SensorTelemetryStorage();
70+
storage.updateMetric(TelemetryMetricKey.NOTEBOOK_PRESENT_KEY, "1");
71+
Assertions.assertDoesNotThrow(() -> storage.send(sensorContext));
72+
assertThat(logTester.logs()).contains("Failed to send metrics");
73+
}
74+
75+
private SensorContext sensorContext(Version version) {
76+
var sensorContext = spy(SensorContextTester.create(new File("")));
77+
sensorContext.setRuntime(SonarRuntimeImpl.forSonarQube(version, SonarQubeSide.SERVER, SonarEdition.DEVELOPER));
78+
return sensorContext;
79+
}
80+
}

0 commit comments

Comments
 (0)