@@ -28,10 +28,13 @@ import okhttp3.Request; | |||
import okhttp3.RequestBody; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.api.utils.log.Logger; | |||
import org.sonar.api.utils.log.Loggers; | |||
@ServerSide | |||
public class TelemetryClient { | |||
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); | |||
private static final Logger LOG = Loggers.get(TelemetryClient.class); | |||
private final OkHttpClient okHttpClient; | |||
private final TelemetryUrl serverUrl; | |||
@@ -50,6 +53,19 @@ public class TelemetryClient { | |||
} | |||
} | |||
void optOut(String json) { | |||
Request.Builder request = new Request.Builder(); | |||
request.url(serverUrl.get()); | |||
RequestBody body = RequestBody.create(JSON, json); | |||
request.delete(body); | |||
try { | |||
okHttpClient.newCall(request.build()).execute(); | |||
} catch (IOException e) { | |||
LOG.debug("Error when sending opt-out usage statistics: %s", e.getMessage()); | |||
} | |||
} | |||
private Request buildHttpRequest(String json) { | |||
Request.Builder request = new Request.Builder(); | |||
request.url(serverUrl.get()); | |||
@@ -57,4 +73,5 @@ public class TelemetryClient { | |||
request.post(body); | |||
return request.build(); | |||
} | |||
} |
@@ -30,19 +30,26 @@ import org.sonar.api.config.Configuration; | |||
import org.sonar.api.platform.Server; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.api.utils.log.Logger; | |||
import org.sonar.api.utils.log.Loggers; | |||
import org.sonar.api.utils.text.JsonWriter; | |||
import org.sonar.server.property.InternalProperties; | |||
import static org.sonar.api.utils.DateUtils.formatDate; | |||
import static org.sonar.api.utils.DateUtils.parseDate; | |||
import static org.sonar.server.telemetry.TelemetryProperties.PROP_ENABLE; | |||
import static org.sonar.server.telemetry.TelemetryProperties.PROP_URL; | |||
@ServerSide | |||
public class TelemetryDaemon implements Startable { | |||
private static final String THREAD_NAME_PREFIX = "sq-telemetry-service-"; | |||
private static final int SEVEN_DAYS = 7 * 24 * 60 * 60 * 1_000; | |||
private static final String I_PROP_LAST_PING = "sonar.telemetry.lastPing"; | |||
private static final String I_PROP_OPT_OUT = "sonar.telemetry.optOut"; | |||
private static final Logger LOG = Loggers.get(TelemetryDaemon.class); | |||
private final TelemetryClient telemetryClient; | |||
private final Configuration config; | |||
private final InternalProperties internalProperties; | |||
private final Server server; | |||
private final System2 system2; | |||
@@ -50,16 +57,38 @@ public class TelemetryDaemon implements Startable { | |||
private ScheduledExecutorService executorService; | |||
public TelemetryDaemon(TelemetryClient telemetryClient, InternalProperties internalProperties, Server server, System2 system2, Configuration config) { | |||
public TelemetryDaemon(TelemetryClient telemetryClient, Configuration config, InternalProperties internalProperties, Server server, System2 system2) { | |||
this.telemetryClient = telemetryClient; | |||
this.config = config; | |||
this.frequencyInSeconds = new TelemetryFrequency(config); | |||
this.internalProperties = internalProperties; | |||
this.server = server; | |||
this.frequencyInSeconds = new TelemetryFrequency(config); | |||
this.system2 = system2; | |||
} | |||
@Override | |||
public void start() { | |||
boolean isTelemetryActivated = config.getBoolean(PROP_ENABLE).orElseThrow(() -> new IllegalStateException(String.format("Setting '%s' must be provided.", PROP_URL))); | |||
if (!internalProperties.read(I_PROP_OPT_OUT).isPresent()) { | |||
if (!isTelemetryActivated) { | |||
StringWriter json = new StringWriter(); | |||
try (JsonWriter writer = JsonWriter.of(json)) { | |||
writer.beginObject(); | |||
writer.prop("id", server.getId()); | |||
writer.endObject(); | |||
} | |||
telemetryClient.optOut(json.toString()); | |||
internalProperties.write(I_PROP_OPT_OUT, String.valueOf(system2.now())); | |||
LOG.info("Sharing of SonarQube statistics is disabled."); | |||
} else { | |||
internalProperties.write(I_PROP_OPT_OUT, null); | |||
} | |||
} | |||
if (!isTelemetryActivated) { | |||
return; | |||
} | |||
LOG.info("Sharing of SonarQube statistics is enabled."); | |||
executorService = Executors.newSingleThreadScheduledExecutor( | |||
new ThreadFactoryBuilder() | |||
.setNameFormat(THREAD_NAME_PREFIX + "%d") |
@@ -25,6 +25,11 @@ import org.sonar.api.Property; | |||
@Properties({ | |||
@Property( | |||
key = TelemetryProperties.PROP_ENABLE, | |||
defaultValue = "true", | |||
name = "Share SonarQube statistics", | |||
global = false), | |||
@Property( | |||
key = TelemetryProperties.PROP_FREQUENCY, | |||
// 6 hours in seconds | |||
defaultValue = "21600", | |||
@@ -37,6 +42,7 @@ import org.sonar.api.Property; | |||
global = false) | |||
}) | |||
public class TelemetryProperties { | |||
static final String PROP_ENABLE = "sonar.telemetry.enable"; | |||
static final String PROP_FREQUENCY = "sonar.telemetry.frequency"; | |||
static final String PROP_URL = "sonar.telemetry.url"; | |||
} |
@@ -55,7 +55,7 @@ public class TelemetryDaemonTest { | |||
settings = new MapSettings(new PropertyDefinitions(TelemetryProperties.class)); | |||
system2.setNow(System.currentTimeMillis()); | |||
underTest = new TelemetryDaemon(client, internalProperties, server, system2, settings.asConfig()); | |||
underTest = new TelemetryDaemon(client, settings.asConfig(), internalProperties, server, system2); | |||
} | |||
@Test | |||
@@ -63,7 +63,7 @@ public class TelemetryDaemonTest { | |||
settings.setProperty("sonar.telemetry.frequency", "1"); | |||
underTest.start(); | |||
verify(client, timeout(2_000).atLeastOnce()).send(anyString()); | |||
verify(client, timeout(1_000).atLeastOnce()).send(anyString()); | |||
} | |||
@Test | |||
@@ -73,7 +73,6 @@ public class TelemetryDaemonTest { | |||
long sevenDaysAgo = now - (ONE_DAY * 7L); | |||
internalProperties.write("sonar.telemetry.lastPing", String.valueOf(sixDaysAgo)); | |||
settings.setProperty("sonar.telemetry.frequency", "1"); | |||
underTest = new TelemetryDaemon(client, internalProperties, server, system2, settings.asConfig()); | |||
underTest.start(); | |||
verify(client, timeout(1_000).never()).send(anyString()); | |||
internalProperties.write("sonar.telemetry.lastPing", String.valueOf(sevenDaysAgo)); | |||
@@ -114,7 +113,19 @@ public class TelemetryDaemonTest { | |||
underTest.start(); | |||
verify(client, timeout(2_000)).send(anyString()); | |||
verify(client, timeout(1_000).atLeastOnce()).send(anyString()); | |||
assertThat(internalProperties.read("sonar.telemetry.lastPing").get()).isEqualTo(String.valueOf(today)); | |||
} | |||
@Test | |||
public void opt_out_sent_once() { | |||
settings.setProperty("sonar.telemetry.frequency", "1"); | |||
settings.setProperty("sonar.telemetry.enable", "false"); | |||
underTest.start(); | |||
underTest.start(); | |||
verify(client, timeout(1_000).never()).send(anyString()); | |||
verify(client, timeout(1_000).times(1)).optOut(anyString()); | |||
} | |||
} |
@@ -342,6 +342,12 @@ | |||
#sonar.path.data=data | |||
#sonar.path.temp=temp | |||
# Telemetry - Share anonymous SonarQube statistics | |||
# By sharing anonymous SonarQube statistics, you help us understand how SonarQube is used so we can improve the product to work even better for you. | |||
# We don't collect source code or IP addresses. And we don't share the data with anyone else. | |||
# To see an example of the data shared: login as a global administrator, call the WS api/system/info and check the Statistics field. | |||
#sonar.telemetry.enable=true | |||
#-------------------------------------------------------------------------------------------------- | |||
# DEVELOPMENT - only for developers |
@@ -70,4 +70,25 @@ public class TelemetryTest { | |||
orchestrator.stop(); | |||
} | |||
@Test | |||
public void opt_out_of_telemetry() throws Exception { | |||
String serverId = randomAlphanumeric(40); | |||
orchestrator = Orchestrator.builderEnv() | |||
.addPlugin(xooPlugin()) | |||
.setServerProperty("sonar.telemetry.enable", "false") | |||
.setServerProperty("sonar.telemetry.url", url) | |||
.setServerProperty("sonar.telemetry.frequency", "1") | |||
.setServerProperty("sonar.core.id", serverId) | |||
.build(); | |||
orchestrator.start(); | |||
RecordedRequest request = server.takeRequest(1, TimeUnit.SECONDS); | |||
assertThat(request.getMethod()).isEqualTo("DELETE"); | |||
assertThat(request.getBody().readUtf8()).contains(serverId); | |||
assertThat(request.getHeader(HttpHeaders.USER_AGENT)).contains("SonarQube"); | |||
orchestrator.stop(); | |||
} | |||
} |