Skip to content

Commit 821ddbc

Browse files
SONARJAVA-5689 Aggregate telemetry measures at project level (#5252)
1 parent 3c3a53b commit 821ddbc

File tree

18 files changed

+564
-54
lines changed

18 files changed

+564
-54
lines changed

its/plugin/tests/src/test/java/com/sonar/it/java/suite/JavaTutorialTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ private void executeAndAssertBuild(MavenBuild build, String projectKey) {
6969
assertThat(issuesForRule(issues, "mycompany-java:SecurityAnnotationMandatory")).hasSize(2);
7070
assertThat(issuesForRule(issues, "mycompany-java:SpringControllerRequestMappingEntity")).hasSize(1);
7171

72-
assertThat(buildResult.getLogs()).containsOnlyOnce("java.scanner_app=ScannerMaven");
72+
assertThat(buildResult.getLogs())
73+
.containsOnlyOnce("Telemetry java.language.version: 17")
74+
.containsOnlyOnce("Telemetry java.module_count: 1")
75+
.containsOnlyOnce("Telemetry java.scanner_app: ScannerMaven");
7376
}
7477

7578
private static Stream<String> issuesForRule(List<Issue> issues, String key) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 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.java.telemetry;
18+
19+
import java.math.BigInteger;
20+
import java.util.Comparator;
21+
import java.util.regex.Pattern;
22+
import javax.annotation.Nullable;
23+
24+
/**
25+
* Comparator for alphanumeric strings that compares numbers as integers and other parts as strings.
26+
* For example: "elem1,elem2,elem12" instead of "elem1,elem12,elem2".
27+
*/
28+
class AlphaNumericComparator implements Comparator<String> {
29+
30+
// Splits a string into parts where each part is either a sequence of digits or a sequence of non-digits.
31+
private static final Pattern DIGITS_SEPARATOR = Pattern.compile("(?<=\\D)(?=\\d)|(?<=\\d)(?=\\D)");
32+
33+
@Override
34+
public int compare(@Nullable String o1, @Nullable String o2) {
35+
if (o1 == null || o2 == null) {
36+
if (o1 != null) {
37+
return 1;
38+
}
39+
return o2 != null ? -1 : 0;
40+
}
41+
if (o1.equals(o2)) {
42+
return 0;
43+
}
44+
String[] arr1 = DIGITS_SEPARATOR.split(o1, -1);
45+
String[] arr2 = DIGITS_SEPARATOR.split(o2, -1);
46+
int len = Math.min(arr1.length, arr2.length);
47+
for (int i = 0; i < len; i++) {
48+
int comp = compareStringOrNumber(arr1[i], arr2[i]);
49+
if (comp != 0) {
50+
return comp;
51+
}
52+
}
53+
return Integer.compare(arr1.length, arr2.length);
54+
}
55+
56+
private static int compareStringOrNumber(String str1, String str2) {
57+
if (startWithDigit(str1) && startWithDigit(str2)) {
58+
return new BigInteger(str1).compareTo(new BigInteger(str2));
59+
} else {
60+
return str1.compareTo(str2);
61+
}
62+
}
63+
64+
private static boolean startWithDigit(String str) {
65+
return !str.isEmpty() && str.charAt(0) >= '0' && str.charAt(0) <= '9';
66+
}
67+
68+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 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.java.telemetry;
18+
19+
import java.util.EnumMap;
20+
import java.util.Map;
21+
import java.util.Set;
22+
import java.util.TreeMap;
23+
import java.util.TreeSet;
24+
25+
public class DefaultTelemetry implements Telemetry {
26+
27+
private static final AlphaNumericComparator ALPHA_NUMERIC_COMPARATOR = new AlphaNumericComparator();
28+
29+
private final Map<TelemetryKey, Set<String>> sets = new EnumMap<>(TelemetryKey.class);
30+
private final Map<TelemetryKey, Long> counters = new EnumMap<>(TelemetryKey.class);
31+
32+
@Override
33+
public void aggregateAsSortedSet(TelemetryKey key, String value) {
34+
sets.computeIfAbsent(key, k -> new TreeSet<>(ALPHA_NUMERIC_COMPARATOR)).add(value);
35+
}
36+
37+
@Override
38+
public void aggregateAsCounter(TelemetryKey key, long value) {
39+
counters.merge(key, value, Long::sum);
40+
}
41+
42+
@Override
43+
public Map<String, String> toMap() {
44+
Map<String, String> map = new TreeMap<>(ALPHA_NUMERIC_COMPARATOR);
45+
sets.forEach((key, value) -> map.put(key.key(), String.join(",", value)));
46+
counters.forEach((key, value) -> map.put(key.key(), String.valueOf(value)));
47+
return map;
48+
}
49+
50+
}

sonar-java-plugin/src/main/java/org/sonar/plugins/java/SensorTelemetry.java renamed to java-frontend/src/main/java/org/sonar/java/telemetry/NoOpTelemetry.java

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,28 @@
1414
* You should have received a copy of the Sonar Source-Available License
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
17-
package org.sonar.plugins.java;
17+
package org.sonar.java.telemetry;
1818

19-
import org.slf4j.Logger;
20-
import org.slf4j.LoggerFactory;
21-
import org.sonar.api.batch.sensor.SensorContext;
22-
import org.sonar.java.Telemetry;
23-
import org.sonar.java.TelemetryKey;
19+
import java.util.Map;
2420

2521
/**
26-
* Wraps up {@link SensorContext} to allow passing it around without exposing other APIs.
22+
* Placeholder for {@link Telemetry} that ignores all calls. Its main use is to satisfy a dependency in SonarQube for IDE, which does not send telemetry.
2723
*/
28-
public class SensorTelemetry implements Telemetry {
29-
private static final Logger LOG = LoggerFactory.getLogger(SensorTelemetry.class);
24+
public class NoOpTelemetry implements Telemetry {
3025

31-
private final SensorContext context;
26+
@Override
27+
public void aggregateAsSortedSet(TelemetryKey key, String value) {
28+
// no operation
29+
}
3230

33-
public SensorTelemetry(SensorContext context) {
34-
this.context = context;
31+
@Override
32+
public void aggregateAsCounter(TelemetryKey key, long value) {
33+
// no operation
3534
}
3635

3736
@Override
38-
public void addMetric(TelemetryKey key, String value) {
39-
LOG.debug("{}={}", key.key(), value);
40-
this.context.addTelemetryProperty(key.key(), value);
37+
public Map<String, String> toMap() {
38+
return Map.of();
4139
}
40+
4241
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 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.java.telemetry;
18+
19+
import java.util.Map;
20+
import org.sonar.api.ExtensionPoint;
21+
import org.sonar.api.scanner.ScannerSide;
22+
import org.sonarsource.api.sonarlint.SonarLintSide;
23+
24+
/**
25+
* Provides access to the APIs for reporting telemetry.
26+
*/
27+
@ScannerSide
28+
@SonarLintSide
29+
@ExtensionPoint
30+
public interface Telemetry {
31+
32+
/**
33+
* Aggregates all the given values as a sorted set for the given key.
34+
* The final map will contain the key and a comma-separated list of sorted values.
35+
*/
36+
void aggregateAsSortedSet(TelemetryKey key, String value);
37+
38+
/**
39+
* Aggregates all the given values as a sum for the given key.
40+
*/
41+
void aggregateAsCounter(TelemetryKey key, long value);
42+
43+
/**
44+
* @return convert all the different kind of key / value pairs type into a string / string map.
45+
*/
46+
Map<String, String> toMap();
47+
48+
}

java-frontend/src/main/java/org/sonar/java/TelemetryKey.java renamed to java-frontend/src/main/java/org/sonar/java/telemetry/TelemetryKey.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
* You should have received a copy of the Sonar Source-Available License
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
17-
package org.sonar.java;
17+
package org.sonar.java.telemetry;
1818

1919
/**
2020
* Telemetry keys used by the Java analyzer.
2121
*/
2222
public enum TelemetryKey {
2323
JAVA_LANGUAGE_VERSION("java.language.version"),
24-
JAVA_SCANNER_APP("java.scanner_app");
24+
JAVA_SCANNER_APP("java.scanner_app"),
25+
JAVA_MODULE_COUNT("java.module_count");
2526

2627
private final String key;
2728

java-frontend/src/main/java/org/sonar/java/Telemetry.java renamed to java-frontend/src/main/java/org/sonar/java/telemetry/package-info.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,7 @@
1414
* You should have received a copy of the Sonar Source-Available License
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
17-
package org.sonar.java;
17+
@ParametersAreNonnullByDefault
18+
package org.sonar.java.telemetry;
1819

19-
/**
20-
* Provides access to the APIs for reporting telemetry.
21-
*/
22-
public interface Telemetry {
23-
// `addMetric` will forward the call to `addTelemetryProperty`.
24-
// We chose a different name to make textual search for the real thing easier.
25-
void addMetric(TelemetryKey key, String value);
26-
}
20+
import javax.annotation.ParametersAreNonnullByDefault;

java-frontend/src/test/java/org/sonar/java/SonarComponentsTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ class SonarComponentsTest {
140140
private SensorContext context;
141141

142142
@RegisterExtension
143-
public ThreadLocalLogTester logTester = new ThreadLocalLogTester().setLevel(Level.DEBUG);
143+
public ThreadLocalLogTester logTester = new ThreadLocalLogTester();
144144

145145
@BeforeEach
146146
void setUp() {
@@ -980,6 +980,7 @@ private static Stream<Arguments> fileCanBeSkipped_only_logs_on_first_call_input(
980980
@ParameterizedTest
981981
@MethodSource("fileCanBeSkipped_only_logs_on_first_call_input")
982982
void fileCanBeSkipped_only_logs_on_the_first_call(SonarComponents sonarComponents, InputFile inputFile, String logMessage) throws IOException {
983+
logTester.setLevel(Level.INFO);
983984
assertThat(logTester.logs(Level.INFO)).isEmpty();
984985

985986
SensorContext contextMock = mock(SensorContext.class);
@@ -1087,6 +1088,7 @@ void beforeEach() {
10871088

10881089
@Test
10891090
void log_only_50_undefined_types() {
1091+
logTester.setLevel(Level.DEBUG);
10901092
String source = generateSource(26);
10911093

10921094
// artificially populated the semantic errors with 26 unknown types and 52 errors
@@ -1131,6 +1133,7 @@ void remove_info_and_warning_from_log_related_to_undefined_types() {
11311133

11321134
@Test
11331135
void log_all_undefined_types_if_less_than_threshold() {
1136+
logTester.setLevel(Level.DEBUG);
11341137
String source = generateSource(1);
11351138

11361139
// artificially populated the semantic errors with 1 unknown types and 2 errors
@@ -1155,6 +1158,7 @@ void log_all_undefined_types_if_less_than_threshold() {
11551158

11561159
@Test
11571160
void suspicious_empty_libraries_should_be_logged() {
1161+
logTester.setLevel(Level.INFO);
11581162
logUndefinedTypesWithOneMainAndOneTest();
11591163

11601164
assertThat(logTester.logs(Level.WARN))
@@ -1166,6 +1170,7 @@ void suspicious_empty_libraries_should_be_logged() {
11661170

11671171
@Test
11681172
void suspicious_empty_libraries_should_not_be_logged_in_autoscan() {
1173+
logTester.setLevel(Level.INFO);
11691174
// Enable autoscan with a property
11701175
context.setSettings(new MapSettings().setProperty(SonarComponents.SONAR_AUTOSCAN, true));
11711176

@@ -1180,6 +1185,7 @@ void suspicious_empty_libraries_should_not_be_logged_in_autoscan() {
11801185

11811186
@Test
11821187
void log_problems_with_list_of_paths_of_files_affected() {
1188+
logTester.setLevel(Level.DEBUG);
11831189
String source = generateSource(1);
11841190

11851191
// Add one test and one main file

0 commit comments

Comments
 (0)