Skip to content

Commit d7f764c

Browse files
committed
SONARPY-2464 Add safety around the telemetry mechanism
1 parent 8c094f8 commit d7f764c

File tree

5 files changed

+149
-38
lines changed

5 files changed

+149
-38
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
@@ -97,18 +97,18 @@ private void processNotebooksFiles(List<PythonInputFile> pythonFiles, SensorCont
9797
PythonIndexer pythonIndexer = new SonarQubePythonIndexer(pythonFiles, cacheContext, context);
9898
PythonScanner scanner = new PythonScanner(context, checks, fileLinesContextFactory, noSonarFilter, PythonParser.createIPythonParser(), pythonIndexer);
9999
scanner.execute(pythonFiles, context);
100-
sensorTelemetryStorage.updateMetric(SensorTelemetryStorage.MetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY, String.valueOf(scanner.getRecognitionErrorCount()));
100+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY, String.valueOf(scanner.getRecognitionErrorCount()));
101101
}
102102

103103
private List<PythonInputFile> parseNotebooks(List<PythonInputFile> pythonFiles, SensorContext context) {
104104
List<PythonInputFile> generatedIPythonFiles = new ArrayList<>();
105105

106-
sensorTelemetryStorage.updateMetric(SensorTelemetryStorage.MetricKey.NOTEBOOK_TOTAL_KEY, String.valueOf(pythonFiles.size()));
106+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.NOTEBOOK_TOTAL_KEY, String.valueOf(pythonFiles.size()));
107107
var numberOfExceptions = 0;
108108

109109
for (PythonInputFile inputFile : pythonFiles) {
110110
try {
111-
sensorTelemetryStorage.updateMetric(SensorTelemetryStorage.MetricKey.NOTEBOOK_PRESENT_KEY, "1");
111+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.NOTEBOOK_PRESENT_KEY, "1");
112112
var result = IpynbNotebookParser.parseNotebook(inputFile);
113113
result.ifPresent(generatedIPythonFiles::add);
114114
} catch (Exception e) {
@@ -119,7 +119,7 @@ private List<PythonInputFile> parseNotebooks(List<PythonInputFile> pythonFiles,
119119
}
120120
}
121121

122-
sensorTelemetryStorage.updateMetric(SensorTelemetryStorage.MetricKey.NOTEBOOK_EXCEPTION_KEY, String.valueOf(numberOfExceptions));
122+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.NOTEBOOK_EXCEPTION_KEY, String.valueOf(numberOfExceptions));
123123

124124
return generatedIPythonFiles;
125125
}

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

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,50 +17,47 @@
1717
package org.sonar.plugins.python;
1818

1919
import java.util.Collections;
20-
import java.util.HashMap;
20+
import java.util.EnumMap;
2121
import java.util.Map;
2222
import org.slf4j.Logger;
2323
import org.slf4j.LoggerFactory;
2424
import org.sonar.api.batch.sensor.SensorContext;
25+
import org.sonar.api.utils.Version;
2526

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

29-
private final Map<String, String> data;
30+
private final Map<TelemetryMetricKey, String> data;
3031

3132
public SensorTelemetryStorage() {
32-
data = new HashMap<>();
33+
data = new EnumMap<>(TelemetryMetricKey.class);
3334
}
3435

35-
public Map<String, String> data() {
36+
protected Map<TelemetryMetricKey, String> data() {
3637
return Collections.unmodifiableMap(data);
3738
}
3839

3940
public void send(SensorContext sensorContext) {
40-
data.forEach((k, v) -> {
41-
LOG.info("Metrics property: {}={}", k, v);
42-
sensorContext.addTelemetryProperty(k, v);
43-
});
41+
// This try/catch block should be useless, as in the worst case it should be a no-op depending on the SensorContext implementation
42+
// It exists to be extra sure for the LTA
43+
try {
44+
var apiVersion = sensorContext.runtime().getApiVersion();
45+
if (apiVersion.isGreaterThanOrEqual(Version.create(10, 9))) {
46+
data.forEach((k, v) -> {
47+
LOG.info("Collected metric: {}={}", k, v);
48+
sensorContext.addTelemetryProperty(k.key(), v);
49+
});
50+
51+
} else {
52+
LOG.info("Skipping sending metrics because the plugin API version is {}", apiVersion);
53+
}
54+
} catch (Exception e) {
55+
LOG.error("Failed to send metrics", e);
56+
}
4457
}
4558

46-
public void updateMetric(MetricKey key, String value) {
47-
data.put(key.key(), value);
59+
public void updateMetric(TelemetryMetricKey key, String value) {
60+
data.put(key, value);
4861
}
4962

50-
public enum MetricKey {
51-
NOTEBOOK_PRESENT_KEY("python.notebook.present"),
52-
NOTEBOOK_TOTAL_KEY("python.notebook.total"),
53-
NOTEBOOK_RECOGNITION_ERROR_KEY("python.notebook.recognition_error"),
54-
NOTEBOOK_EXCEPTION_KEY("python.notebook.exceptions");
55-
56-
private final String key;
57-
58-
MetricKey(String key) {
59-
this.key = key;
60-
}
61-
62-
public String key() {
63-
return key;
64-
}
65-
}
6663
}
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
@@ -225,10 +225,10 @@ void test_notebook_sensor_is_executed_on_json_file() {
225225
assertDoesNotThrow(() -> sensor.execute(context));
226226
assertThat(sensor.getSensorTelemetryStorage().data())
227227
.containsExactlyInAnyOrderEntriesOf(Map.of(
228-
SensorTelemetryStorage.MetricKey.NOTEBOOK_PRESENT_KEY.key(), "1",
229-
SensorTelemetryStorage.MetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY.key(), "0",
230-
SensorTelemetryStorage.MetricKey.NOTEBOOK_TOTAL_KEY.key(), "1",
231-
SensorTelemetryStorage.MetricKey.NOTEBOOK_EXCEPTION_KEY.key(), "0"));
228+
TelemetryMetricKey.NOTEBOOK_PRESENT_KEY, "1",
229+
TelemetryMetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY, "0",
230+
TelemetryMetricKey.NOTEBOOK_TOTAL_KEY, "1",
231+
TelemetryMetricKey.NOTEBOOK_EXCEPTION_KEY, "0"));
232232
verify(sensorTelemetryStorage, Mockito.times(1)).send(context);
233233
}
234234

@@ -252,9 +252,9 @@ void test_notebook_sensor_parse_error_on_valid_line() {
252252
assertThat(logs).contains("Unable to parse file: notebook_parse_error.ipynbParse error at line 1");
253253
assertThat(sensor.getSensorTelemetryStorage().data())
254254
.containsExactlyInAnyOrderEntriesOf(Map.of(
255-
SensorTelemetryStorage.MetricKey.NOTEBOOK_PRESENT_KEY.key(), "1",
256-
SensorTelemetryStorage.MetricKey.NOTEBOOK_TOTAL_KEY.key(), "1",
257-
SensorTelemetryStorage.MetricKey.NOTEBOOK_EXCEPTION_KEY.key(), "0",
258-
SensorTelemetryStorage.MetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY.key(), "1"));
255+
TelemetryMetricKey.NOTEBOOK_PRESENT_KEY, "1",
256+
TelemetryMetricKey.NOTEBOOK_TOTAL_KEY, "1",
257+
TelemetryMetricKey.NOTEBOOK_EXCEPTION_KEY, "0",
258+
TelemetryMetricKey.NOTEBOOK_RECOGNITION_ERROR_KEY, "1"));
259259
}
260260
}
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)