You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TelemetryDaemon.java 6.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-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 GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.telemetry;
  21. import com.google.common.util.concurrent.ThreadFactoryBuilder;
  22. import java.io.IOException;
  23. import java.io.StringWriter;
  24. import java.util.Optional;
  25. import java.util.concurrent.Executors;
  26. import java.util.concurrent.ScheduledExecutorService;
  27. import java.util.concurrent.ThreadFactory;
  28. import java.util.concurrent.TimeUnit;
  29. import org.sonar.api.config.Configuration;
  30. import org.sonar.api.server.ServerSide;
  31. import org.sonar.api.utils.System2;
  32. import org.slf4j.Logger;
  33. import org.slf4j.LoggerFactory;
  34. import org.sonar.api.utils.text.JsonWriter;
  35. import org.sonar.server.property.InternalProperties;
  36. import org.sonar.server.util.AbstractStoppableScheduledExecutorServiceImpl;
  37. import org.sonar.server.util.GlobalLockManager;
  38. import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_ENABLE;
  39. import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_FREQUENCY_IN_SECONDS;
  40. import static org.sonar.process.ProcessProperties.Property.SONAR_TELEMETRY_URL;
  41. @ServerSide
  42. public class TelemetryDaemon extends AbstractStoppableScheduledExecutorServiceImpl<ScheduledExecutorService> {
  43. private static final String THREAD_NAME_PREFIX = "sq-telemetry-service-";
  44. private static final int ONE_DAY = 24 * 60 * 60 * 1_000;
  45. private static final String I_PROP_LAST_PING = "telemetry.lastPing";
  46. private static final String I_PROP_OPT_OUT = "telemetry.optOut";
  47. private static final String LOCK_NAME = "TelemetryStat";
  48. private static final Logger LOG = LoggerFactory.getLogger(TelemetryDaemon.class);
  49. private static final String LOCK_DELAY_SEC = "sonar.telemetry.lock.delay";
  50. static final String I_PROP_MESSAGE_SEQUENCE = "telemetry.messageSeq";
  51. private final TelemetryDataLoader dataLoader;
  52. private final TelemetryDataJsonWriter dataJsonWriter;
  53. private final TelemetryClient telemetryClient;
  54. private final GlobalLockManager lockManager;
  55. private final Configuration config;
  56. private final InternalProperties internalProperties;
  57. private final System2 system2;
  58. public TelemetryDaemon(TelemetryDataLoader dataLoader, TelemetryDataJsonWriter dataJsonWriter, TelemetryClient telemetryClient, Configuration config,
  59. InternalProperties internalProperties, GlobalLockManager lockManager, System2 system2) {
  60. super(Executors.newSingleThreadScheduledExecutor(newThreadFactory()));
  61. this.dataLoader = dataLoader;
  62. this.dataJsonWriter = dataJsonWriter;
  63. this.telemetryClient = telemetryClient;
  64. this.config = config;
  65. this.internalProperties = internalProperties;
  66. this.lockManager = lockManager;
  67. this.system2 = system2;
  68. }
  69. @Override
  70. public void start() {
  71. boolean isTelemetryActivated = config.getBoolean(SONAR_TELEMETRY_ENABLE.getKey())
  72. .orElseThrow(() -> new IllegalStateException(String.format("Setting '%s' must be provided.", SONAR_TELEMETRY_URL.getKey())));
  73. boolean hasOptOut = internalProperties.read(I_PROP_OPT_OUT).isPresent();
  74. if (!isTelemetryActivated && !hasOptOut) {
  75. optOut();
  76. internalProperties.write(I_PROP_OPT_OUT, String.valueOf(system2.now()));
  77. LOG.info("Sharing of SonarQube statistics is disabled.");
  78. }
  79. if (isTelemetryActivated && hasOptOut) {
  80. internalProperties.write(I_PROP_OPT_OUT, null);
  81. }
  82. if (!isTelemetryActivated) {
  83. return;
  84. }
  85. LOG.info("Sharing of SonarQube statistics is enabled.");
  86. int frequencyInSeconds = frequency();
  87. scheduleWithFixedDelay(telemetryCommand(), frequencyInSeconds, frequencyInSeconds, TimeUnit.SECONDS);
  88. }
  89. private static ThreadFactory newThreadFactory() {
  90. return new ThreadFactoryBuilder()
  91. .setNameFormat(THREAD_NAME_PREFIX + "%d")
  92. .setPriority(Thread.MIN_PRIORITY)
  93. .build();
  94. }
  95. private Runnable telemetryCommand() {
  96. return () -> {
  97. try {
  98. if (!lockManager.tryLock(LOCK_NAME, lockDuration())) {
  99. return;
  100. }
  101. long now = system2.now();
  102. if (shouldUploadStatistics(now)) {
  103. uploadStatistics();
  104. updateTelemetryProps(now);
  105. }
  106. } catch (Exception e) {
  107. LOG.debug("Error while checking SonarQube statistics: {}", e.getMessage(), e);
  108. }
  109. // do not check at start up to exclude test instance which are not up for a long time
  110. };
  111. }
  112. private void updateTelemetryProps(long now) {
  113. internalProperties.write(I_PROP_LAST_PING, String.valueOf(now));
  114. Optional<String> currentSequence = internalProperties.read(I_PROP_MESSAGE_SEQUENCE);
  115. if (currentSequence.isEmpty()) {
  116. internalProperties.write(I_PROP_MESSAGE_SEQUENCE, String.valueOf(1));
  117. return;
  118. }
  119. long current = Long.parseLong(currentSequence.get());
  120. internalProperties.write(I_PROP_MESSAGE_SEQUENCE, String.valueOf(current + 1));
  121. }
  122. private void optOut() {
  123. StringWriter json = new StringWriter();
  124. try (JsonWriter writer = JsonWriter.of(json)) {
  125. writer.beginObject();
  126. writer.prop("id", dataLoader.loadServerId());
  127. writer.endObject();
  128. }
  129. telemetryClient.optOut(json.toString());
  130. }
  131. private void uploadStatistics() throws IOException {
  132. TelemetryData statistics = dataLoader.load();
  133. StringWriter jsonString = new StringWriter();
  134. try (JsonWriter json = JsonWriter.of(jsonString)) {
  135. dataJsonWriter.writeTelemetryData(json, statistics);
  136. }
  137. telemetryClient.upload(jsonString.toString());
  138. dataLoader.reset();
  139. }
  140. private boolean shouldUploadStatistics(long now) {
  141. Optional<Long> lastPing = internalProperties.read(I_PROP_LAST_PING).map(Long::valueOf);
  142. return lastPing.isEmpty() || now - lastPing.get() >= ONE_DAY;
  143. }
  144. private int frequency() {
  145. return config.getInt(SONAR_TELEMETRY_FREQUENCY_IN_SECONDS.getKey())
  146. .orElseThrow(() -> new IllegalStateException(String.format("Setting '%s' must be provided.", SONAR_TELEMETRY_FREQUENCY_IN_SECONDS)));
  147. }
  148. private int lockDuration() {
  149. return config.getInt(LOCK_DELAY_SEC).orElse(60);
  150. }
  151. }