diff options
author | alain <108417558+alain-kermis-sonarsource@users.noreply.github.com> | 2022-12-19 16:48:47 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-12-19 20:02:46 +0000 |
commit | 4bdd63589597f5b32f04f02a3a32fe3e6e2927d4 (patch) | |
tree | 2baaee45d0096fa5eeda7ccc3bf29a5ab0ee99d0 | |
parent | 590d662a839a255957e72624001e18ba3338a6b5 (diff) | |
download | sonarqube-4bdd63589597f5b32f04f02a3a32fe3e6e2927d4.tar.gz sonarqube-4bdd63589597f5b32f04f02a3a32fe3e6e2927d4.zip |
SONAR-17735 Telemetry improvements
9 files changed, 147 insertions, 84 deletions
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java b/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java index c2825545c8a..0870ff9b845 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java @@ -36,10 +36,10 @@ import static java.util.Objects.requireNonNullElse; 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; @@ -52,10 +52,10 @@ public class TelemetryData { 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; @@ -74,6 +74,10 @@ public class TelemetryData { return version; } + public Long getMessageSequenceNumber() { + return messageSequenceNumber; + } + public Map<String, String> getPlugins() { return plugins; } @@ -86,10 +90,6 @@ public class TelemetryData { return Optional.ofNullable(edition); } - public Optional<String> getLicenseType() { - return Optional.ofNullable(licenseType); - } - public Long getInstallationDate() { return installationDate; } @@ -129,10 +129,10 @@ public class TelemetryData { 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; @@ -156,6 +156,11 @@ public class TelemetryData { return this; } + Builder setMessageSequenceNumber(@Nullable Long messageSequenceNumber) { + this.messageSequenceNumber = messageSequenceNumber; + return this; + } + Builder setPlugins(Map<String, String> plugins) { this.plugins = plugins; return this; @@ -171,11 +176,6 @@ public class TelemetryData { return this; } - Builder setLicenseType(@Nullable String licenseType) { - this.licenseType = licenseType; - return this; - } - Builder setInstallationDate(@Nullable Long installationDate) { this.installationDate = installationDate; return this; @@ -212,7 +212,7 @@ public class TelemetryData { } TelemetryData build() { - requireNonNullValues(serverId, version, plugins, database); + requireNonNullValues(serverId, version, plugins, database, messageSequenceNumber); return new TelemetryData(this); } @@ -224,6 +224,7 @@ public class TelemetryData { private static void requireNonNullValues(Object... values) { Arrays.stream(values).forEach(Objects::requireNonNull); } + } static class Database { diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java b/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java index de904d212e7..e22b4e934b5 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java @@ -23,15 +23,27 @@ import com.google.common.annotations.VisibleForTesting; 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"; @@ -40,8 +52,9 @@ public class TelemetryDataJsonWriter { 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()); @@ -78,6 +91,8 @@ public class TelemetryDataJsonWriter { writeProjectData(json, statistics); writeProjectStatsData(json, statistics); + extensions.forEach(e -> e.write(json)); + json.endObject(); } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java index b32a7aeb3b0..fad92279708 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java @@ -36,8 +36,10 @@ import org.apache.commons.codec.digest.DigestUtils; 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; @@ -45,6 +47,8 @@ import static java.lang.String.format; 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; @@ -53,17 +57,22 @@ public class TelemetryDataJsonWriterTest { 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() + "}"); } @@ -91,29 +100,6 @@ public class TelemetryDataJsonWriterTest { } @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); String version = randomAlphabetic(10); @@ -252,6 +238,18 @@ public class TelemetryDataJsonWriterTest { } @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() .setUsers(attachUsers()) @@ -376,6 +374,7 @@ public class TelemetryDataJsonWriterTest { return TelemetryData.builder() .setServerId("foo") .setVersion("bar") + .setMessageSequenceNumber(1L) .setPlugins(Collections.emptyMap()) .setDatabase(new TelemetryData.Database("H2", "11")); } diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/LicenseReader.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/LicenseReader.java index 80110a1a749..0efaac7f130 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/LicenseReader.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/telemetry/LicenseReader.java @@ -28,5 +28,6 @@ public interface LicenseReader { interface License { String getType(); + Boolean isValidLicense(); } } diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java index 1a5c0296c75..106e97d0fd7 100644 --- a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java @@ -50,6 +50,7 @@ public class TelemetryDaemon implements Startable { 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; @@ -125,7 +126,7 @@ public class TelemetryDaemon implements Startable { 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); @@ -134,6 +135,19 @@ public class TelemetryDaemon implements Startable { }; } + 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)) { @@ -155,7 +169,7 @@ public class TelemetryDaemon implements Startable { 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() { diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java index 21f524078b1..147d0a7c39a 100644 --- a/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java @@ -29,8 +29,6 @@ import java.util.Optional; 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; @@ -59,6 +57,7 @@ import static org.sonar.core.config.CorePropertyDefinitions.SONAR_ANALYSIS_DETEC 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 { @@ -79,14 +78,11 @@ 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; @@ -94,7 +90,6 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { this.internalProperties = internalProperties; this.configuration = configuration; this.dockerSupport = dockerSupport; - this.licenseReader = licenseReader; } private static Database loadDatabaseMetadata(DbSession dbSession) { @@ -110,12 +105,10 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { 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); @@ -140,6 +133,10 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader { .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); diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java index ba4186af014..330bcaa0614 100644 --- a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java @@ -62,6 +62,7 @@ public class TelemetryDaemonTest { 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(); @@ -153,6 +154,7 @@ public class TelemetryDaemonTest { 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(); @@ -177,6 +179,36 @@ public class TelemetryDaemonTest { 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()); diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java index 1e384cdeba6..34ceb0b210c 100644 --- a/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java @@ -55,7 +55,6 @@ import org.sonar.server.property.MapInternalProperties; 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; @@ -91,12 +90,11 @@ public class TelemetryDataLoaderImplTest { 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() { @@ -157,6 +155,7 @@ public class TelemetryDataLoaderImplTest { 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")); @@ -247,22 +246,6 @@ public class TelemetryDataLoaderImplTest { } @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)); db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW)); @@ -276,18 +259,6 @@ public class TelemetryDataLoaderImplTest { } @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); String version = randomAlphanumeric(10); @@ -317,6 +288,13 @@ public class TelemetryDataLoaderImplTest { } @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(); assertThat(data.getInstallationDate()).isNull(); diff --git a/sonar-core/src/main/java/org/sonar/core/telemetry/TelemetryExtension.java b/sonar-core/src/main/java/org/sonar/core/telemetry/TelemetryExtension.java new file mode 100644 index 00000000000..542fb53db7e --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/telemetry/TelemetryExtension.java @@ -0,0 +1,26 @@ +/* + * 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); +} |