public class TelemetryData {
private final String serverId;
private final String version;
+ private final Long messageSequenceNumber;
private final Map<String, String> plugins;
private final Database database;
private final EditionProvider.Edition edition;
- private final String licenseType;
private final Long installationDate;
private final String installationVersion;
private final boolean inDocker;
private TelemetryData(Builder builder) {
serverId = builder.serverId;
version = builder.version;
+ messageSequenceNumber = builder.messageSequenceNumber;
plugins = builder.plugins;
database = builder.database;
edition = builder.edition;
- licenseType = builder.licenseType;
installationDate = builder.installationDate;
installationVersion = builder.installationVersion;
inDocker = builder.inDocker;
return version;
}
+ public Long getMessageSequenceNumber() {
+ return messageSequenceNumber;
+ }
+
public Map<String, String> getPlugins() {
return plugins;
}
return Optional.ofNullable(edition);
}
- public Optional<String> getLicenseType() {
- return Optional.ofNullable(licenseType);
- }
-
public Long getInstallationDate() {
return installationDate;
}
static class Builder {
private String serverId;
private String version;
+ private Long messageSequenceNumber;
private Map<String, String> plugins;
private Database database;
private Edition edition;
- private String licenseType;
private Long installationDate;
private String installationVersion;
private boolean inDocker = false;
return this;
}
+ Builder setMessageSequenceNumber(@Nullable Long messageSequenceNumber) {
+ this.messageSequenceNumber = messageSequenceNumber;
+ return this;
+ }
+
Builder setPlugins(Map<String, String> plugins) {
this.plugins = plugins;
return this;
return this;
}
- Builder setLicenseType(@Nullable String licenseType) {
- this.licenseType = licenseType;
- return this;
- }
-
Builder setInstallationDate(@Nullable Long installationDate) {
this.installationDate = installationDate;
return this;
}
TelemetryData build() {
- requireNonNullValues(serverId, version, plugins, database);
+ requireNonNullValues(serverId, version, plugins, database, messageSequenceNumber);
return new TelemetryData(this);
}
private static void requireNonNullValues(Object... values) {
Arrays.stream(values).forEach(Objects::requireNonNull);
}
+
}
static class Database {
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
+import java.util.List;
import java.util.Locale;
import org.apache.commons.codec.digest.DigestUtils;
import org.jetbrains.annotations.NotNull;
+import org.sonar.api.utils.System2;
import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.core.telemetry.TelemetryExtension;
import static org.sonar.api.utils.DateUtils.DATETIME_FORMAT;
public class TelemetryDataJsonWriter {
+ private final List<TelemetryExtension> extensions;
+
+ private final System2 system2;
+
+ public TelemetryDataJsonWriter(List<TelemetryExtension> extensions, System2 system2) {
+ this.extensions = extensions;
+ this.system2 = system2;
+ }
+
@VisibleForTesting
static final String SCIM_PROPERTY = "scim";
private static final String LANGUAGE_PROPERTY = "language";
json.beginObject();
json.prop("id", statistics.getServerId());
json.prop("version", statistics.getVersion());
+ json.prop("messageSequenceNumber", statistics.getMessageSequenceNumber());
+ json.prop("localTimestamp", toUtc(system2.now()));
statistics.getEdition().ifPresent(e -> json.prop("edition", e.name().toLowerCase(Locale.ENGLISH)));
- statistics.getLicenseType().ifPresent(e -> json.prop("licenseType", e));
json.name("database");
json.beginObject();
json.prop("name", statistics.getDatabase().getName());
writeProjectData(json, statistics);
writeProjectStatsData(json, statistics);
+ extensions.forEach(e -> e.write(json));
+
json.endObject();
}
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.sonar.api.utils.System2;
import org.sonar.api.utils.text.JsonWriter;
import org.sonar.core.platform.EditionProvider;
+import org.sonar.core.telemetry.TelemetryExtension;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.user.UserTelemetryDto;
import static java.util.stream.Collectors.joining;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import static org.sonar.server.telemetry.TelemetryDataJsonWriter.SCIM_PROPERTY;
import static org.sonar.test.JsonAssert.assertJson;
private final Random random = new Random();
- private final TelemetryDataJsonWriter underTest = new TelemetryDataJsonWriter();
+ private final TelemetryExtension extension = mock(TelemetryExtension.class);
+
+ private final System2 system2 = mock(System2.class);
+
+ private final TelemetryDataJsonWriter underTest = new TelemetryDataJsonWriter(List.of(extension), system2);
@Test
- public void write_server_id_and_version() {
+ public void write_server_id_version_and_sequence() {
TelemetryData data = telemetryBuilder().build();
String json = writeTelemetryData(data);
assertJson(json).isSimilarTo("{" +
" \"id\": \"" + data.getServerId() + "\"," +
- " \"version\": \"" + data.getVersion() + "\"" +
+ " \"version\": \"" + data.getVersion() + "\"," +
+ " \"messageSequenceNumber\": " + data.getMessageSequenceNumber() +
"}");
}
"}");
}
- @Test
- public void does_not_write_license_type_if_null() {
- TelemetryData data = telemetryBuilder().build();
-
- String json = writeTelemetryData(data);
-
- assertThat(json).doesNotContain("licenseType");
- }
-
- @Test
- public void writes_licenseType_if_non_null() {
- String expected = randomAlphabetic(12);
- TelemetryData data = telemetryBuilder()
- .setLicenseType(expected)
- .build();
-
- String json = writeTelemetryData(data);
-
- assertJson(json).isSimilarTo("{" +
- " \"licenseType\": \"" + expected + "\"" +
- "}");
- }
-
@Test
public void writes_database() {
String name = randomAlphabetic(12);
"}");
}
+ @Test
+ public void writes_local_timestamp() {
+ when(system2.now()).thenReturn(1000L);
+
+ TelemetryData data = telemetryBuilder().build();
+ String json = writeTelemetryData(data);
+
+ assertJson(json).isSimilarTo("{" +
+ " \"localTimestamp\": \"1970-01-01T00:00:01+0000\"" +
+ "}");
+ }
+
@Test
public void writes_all_users_with_anonymous_md5_uuids() {
TelemetryData data = telemetryBuilder()
return TelemetryData.builder()
.setServerId("foo")
.setVersion("bar")
+ .setMessageSequenceNumber(1L)
.setPlugins(Collections.emptyMap())
.setDatabase(new TelemetryData.Database("H2", "11"));
}
interface License {
String getType();
+ Boolean isValidLicense();
}
}
private static final String LOCK_NAME = "TelemetryStat";
private static final Logger LOG = Loggers.get(TelemetryDaemon.class);
private static final String LOCK_DELAY_SEC = "sonar.telemetry.lock.delay";
+ static final String I_PROP_MESSAGE_SEQUENCE = "telemetry.messageSeq";
private final TelemetryDataLoader dataLoader;
private final TelemetryDataJsonWriter dataJsonWriter;
long now = system2.now();
if (shouldUploadStatistics(now)) {
uploadStatistics();
- internalProperties.write(I_PROP_LAST_PING, String.valueOf(now));
+ updateTelemetryProps(now);
}
} catch (Exception e) {
LOG.debug("Error while checking SonarQube statistics: {}", e);
};
}
+ private void updateTelemetryProps(long now) {
+ internalProperties.write(I_PROP_LAST_PING, String.valueOf(now));
+
+ Optional<String> currentSequence = internalProperties.read(I_PROP_MESSAGE_SEQUENCE);
+ if (currentSequence.isEmpty()) {
+ internalProperties.write(I_PROP_MESSAGE_SEQUENCE, String.valueOf(1));
+ return;
+ }
+
+ long current = Long.parseLong(currentSequence.get());
+ internalProperties.write(I_PROP_MESSAGE_SEQUENCE, String.valueOf(current + 1));
+ }
+
private void optOut() {
StringWriter json = new StringWriter();
try (JsonWriter writer = JsonWriter.of(json)) {
private boolean shouldUploadStatistics(long now) {
Optional<Long> lastPing = internalProperties.read(I_PROP_LAST_PING).map(Long::valueOf);
- return !lastPing.isPresent() || now - lastPing.get() >= ONE_DAY;
+ return lastPing.isEmpty() || now - lastPing.get() >= ONE_DAY;
}
private int frequency() {
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
import javax.inject.Inject;
import org.sonar.api.config.Configuration;
import org.sonar.api.platform.Server;
import static org.sonar.core.platform.EditionProvider.Edition.COMMUNITY;
import static org.sonar.core.platform.EditionProvider.Edition.DATACENTER;
import static org.sonar.core.platform.EditionProvider.Edition.ENTERPRISE;
+import static org.sonar.server.telemetry.TelemetryDaemon.I_PROP_MESSAGE_SEQUENCE;
@ServerSide
public class TelemetryDataLoaderImpl implements TelemetryDataLoader {
private final Configuration configuration;
private final InternalProperties internalProperties;
private final DockerSupport dockerSupport;
- @CheckForNull
- private final LicenseReader licenseReader;
-
@Inject
public TelemetryDataLoaderImpl(Server server, DbClient dbClient, PluginRepository pluginRepository,
PlatformEditionProvider editionProvider, InternalProperties internalProperties, Configuration configuration,
- DockerSupport dockerSupport, @Nullable LicenseReader licenseReader) {
+ DockerSupport dockerSupport) {
this.server = server;
this.dbClient = dbClient;
this.pluginRepository = pluginRepository;
this.internalProperties = internalProperties;
this.configuration = configuration;
this.dockerSupport = dockerSupport;
- this.licenseReader = licenseReader;
}
private static Database loadDatabaseMetadata(DbSession dbSession) {
public TelemetryData load() {
TelemetryData.Builder data = TelemetryData.builder();
+ data.setMessageSequenceNumber(retrieveCurrentMessageSequenceNumber() + 1);
data.setServerId(server.getId());
data.setVersion(server.getVersion());
data.setEdition(editionProvider.get().orElse(null));
- ofNullable(licenseReader)
- .flatMap(reader -> licenseReader.read())
- .ifPresent(license -> data.setLicenseType(license.getType()));
Function<PluginInfo, String> getVersion = plugin -> plugin.getVersion() == null ? "undefined" : plugin.getVersion().getName();
Map<String, String> plugins = pluginRepository.getPluginInfos().stream().collect(MoreCollectors.uniqueIndex(PluginInfo::getKey, getVersion));
data.setPlugins(plugins);
.build();
}
+ private Long retrieveCurrentMessageSequenceNumber() {
+ return internalProperties.read(I_PROP_MESSAGE_SEQUENCE).map(Long::parseLong).orElse(0L);
+ }
+
private void resolveProjectStatistics(TelemetryData.Builder data, DbSession dbSession) {
List<String> projectUuids = dbClient.projectDao().selectAllProjectUuids(dbSession);
Map<String, String> scmByProject = getAnalysisPropertyByProject(dbSession, SONAR_ANALYSIS_DETECTEDSCM);
private static final TelemetryData SOME_TELEMETRY_DATA = TelemetryData.builder()
.setServerId("foo")
.setVersion("bar")
+ .setMessageSequenceNumber(1L)
.setPlugins(Collections.emptyMap())
.setDatabase(new TelemetryData.Database("H2", "11"))
.build();
long oneDayAgo = today - ONE_DAY - ONE_HOUR;
internalProperties.write("telemetry.lastPing", String.valueOf(oneDayAgo));
reset(internalProperties);
+ when(dataLoader.load()).thenReturn(SOME_TELEMETRY_DATA);
mockDataJsonWriterDoingSomething();
underTest.start();
assertThat(logger.logs(LoggerLevel.INFO)).contains("Sharing of SonarQube statistics is disabled.");
}
+ @Test
+ public void write_sequence_as_one_if_not_previously_present() {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ mockDataJsonWriterDoingSomething();
+
+ underTest.start();
+
+ verify(internalProperties, timeout(4_000)).write("telemetry.messageSeq", "1");
+ }
+
+ @Test
+ public void write_sequence_correctly_incremented() {
+ initTelemetrySettingsToDefaultValues();
+ when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
+ settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
+ internalProperties.write("telemetry.messageSeq", "10");
+ mockDataJsonWriterDoingSomething();
+
+ underTest.start();
+
+ verify(internalProperties, timeout(4_000)).write("telemetry.messageSeq", "10");
+
+ // force another ping
+ internalProperties.write("telemetry.lastPing", String.valueOf(system2.now() - ONE_DAY));
+
+ verify(internalProperties, timeout(4_000)).write("telemetry.messageSeq", "11");
+ }
+
private void initTelemetrySettingsToDefaultValues() {
settings.setProperty(SONAR_TELEMETRY_ENABLE.getKey(), SONAR_TELEMETRY_ENABLE.getDefaultValue());
settings.setProperty(SONAR_TELEMETRY_URL.getKey(), SONAR_TELEMETRY_URL.getDefaultValue());
import org.sonar.updatecenter.common.Version;
import static java.util.Arrays.asList;
-import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
private final PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
private final DockerSupport dockerSupport = mock(DockerSupport.class);
private final InternalProperties internalProperties = spy(new MapInternalProperties());
- private final LicenseReader licenseReader = mock(LicenseReader.class);
private final TelemetryDataLoader communityUnderTest = new TelemetryDataLoaderImpl(server, db.getDbClient(), pluginRepository, editionProvider,
- internalProperties, configuration, dockerSupport, null);
+ internalProperties, configuration, dockerSupport);
private final TelemetryDataLoader commercialUnderTest = new TelemetryDataLoaderImpl(server, db.getDbClient(), pluginRepository, editionProvider,
- internalProperties, configuration, dockerSupport, licenseReader);
+ internalProperties, configuration, dockerSupport);
@Test
public void send_telemetry_data() {
assertThat(data.getServerId()).isEqualTo(serverId);
assertThat(data.getVersion()).isEqualTo(version);
assertThat(data.getEdition()).contains(DEVELOPER);
+ assertThat(data.getMessageSequenceNumber()).isOne();
assertDatabaseMetadata(data.getDatabase());
assertThat(data.getPlugins()).containsOnly(
entry("java", "4.12.0.11033"), entry("scmgit", "1.2"), entry("other", "undefined"));
tuple(2L, 0L, "undetected", "undetected"));
}
- @Test
- public void data_contains_no_license_type_on_community_edition() {
- TelemetryData data = communityUnderTest.load();
-
- assertThat(data.getLicenseType()).isEmpty();
- }
-
- @Test
- public void data_contains_no_license_type_on_commercial_edition_if_no_license() {
- when(licenseReader.read()).thenReturn(Optional.empty());
-
- TelemetryData data = commercialUnderTest.load();
-
- assertThat(data.getLicenseType()).isEmpty();
- }
-
@Test
public void data_contains_weekly_count_sonarlint_users() {
db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW - 100_000L));
.hasSize(4);
}
- @Test
- public void data_has_license_type_on_commercial_edition_if_no_license() {
- String licenseType = randomAlphabetic(12);
- LicenseReader.License license = mock(LicenseReader.License.class);
- when(license.getType()).thenReturn(licenseType);
- when(licenseReader.read()).thenReturn(Optional.of(license));
-
- TelemetryData data = commercialUnderTest.load();
-
- assertThat(data.getLicenseType()).contains(licenseType);
- }
-
@Test
public void send_server_id_and_version() {
String id = randomAlphanumeric(40);
assertThat(data.getInstallationVersion()).isEqualTo(installationVersion);
}
+ @Test
+ public void send_correct_sequence_number() {
+ internalProperties.write(TelemetryDaemon.I_PROP_MESSAGE_SEQUENCE, "10");
+ TelemetryData data = communityUnderTest.load();
+ assertThat(data.getMessageSequenceNumber()).isEqualTo(11L);
+ }
+
@Test
public void do_not_send_server_installation_details_if_missing_property() {
TelemetryData data = communityUnderTest.load();
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.core.telemetry;
+
+import org.sonar.api.utils.text.JsonWriter;
+
+public interface TelemetryExtension {
+ void write(JsonWriter json);
+}