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.

TelemetryDataLoaderImpl.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 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.annotations.VisibleForTesting;
  22. import java.sql.DatabaseMetaData;
  23. import java.sql.SQLException;
  24. import java.util.ArrayList;
  25. import java.util.Collection;
  26. import java.util.Collections;
  27. import java.util.HashMap;
  28. import java.util.List;
  29. import java.util.Map;
  30. import java.util.Optional;
  31. import java.util.Set;
  32. import java.util.function.Function;
  33. import java.util.stream.Collectors;
  34. import javax.inject.Inject;
  35. import org.sonar.api.config.Configuration;
  36. import org.sonar.api.platform.Server;
  37. import org.sonar.api.server.ServerSide;
  38. import org.sonar.core.platform.PlatformEditionProvider;
  39. import org.sonar.core.platform.PluginInfo;
  40. import org.sonar.core.platform.PluginRepository;
  41. import org.sonar.core.util.stream.MoreCollectors;
  42. import org.sonar.db.DbClient;
  43. import org.sonar.db.DbSession;
  44. import org.sonar.db.alm.setting.ALM;
  45. import org.sonar.db.alm.setting.ProjectAlmKeyAndProject;
  46. import org.sonar.db.component.AnalysisPropertyValuePerProject;
  47. import org.sonar.db.component.PrBranchAnalyzedLanguageCountByProjectDto;
  48. import org.sonar.db.measure.LiveMeasureDto;
  49. import org.sonar.db.measure.ProjectMeasureDto;
  50. import org.sonar.db.metric.MetricDto;
  51. import org.sonar.db.qualitygate.ProjectQgateAssociationDto;
  52. import org.sonar.db.qualitygate.QualityGateDto;
  53. import org.sonar.server.platform.DockerSupport;
  54. import org.sonar.server.property.InternalProperties;
  55. import org.sonar.server.qualitygate.QualityGateCaycChecker;
  56. import org.sonar.server.qualitygate.QualityGateFinder;
  57. import org.sonar.server.telemetry.TelemetryData.Database;
  58. import static java.util.Arrays.asList;
  59. import static java.util.Optional.ofNullable;
  60. import static java.util.stream.Collectors.groupingBy;
  61. import static java.util.stream.Collectors.toMap;
  62. import static org.sonar.api.internal.apachecommons.lang.StringUtils.startsWithIgnoreCase;
  63. import static org.sonar.api.measures.CoreMetrics.BUGS_KEY;
  64. import static org.sonar.api.measures.CoreMetrics.DEVELOPMENT_COST_KEY;
  65. import static org.sonar.api.measures.CoreMetrics.NCLOC_LANGUAGE_DISTRIBUTION_KEY;
  66. import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_KEY;
  67. import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT_KEY;
  68. import static org.sonar.api.measures.CoreMetrics.VULNERABILITIES_KEY;
  69. import static org.sonar.core.config.CorePropertyDefinitions.SONAR_ANALYSIS_DETECTEDCI;
  70. import static org.sonar.core.config.CorePropertyDefinitions.SONAR_ANALYSIS_DETECTEDSCM;
  71. import static org.sonar.core.platform.EditionProvider.Edition.COMMUNITY;
  72. import static org.sonar.core.platform.EditionProvider.Edition.DATACENTER;
  73. import static org.sonar.core.platform.EditionProvider.Edition.ENTERPRISE;
  74. import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_CPP_KEY;
  75. import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_C_KEY;
  76. import static org.sonar.server.telemetry.TelemetryDaemon.I_PROP_MESSAGE_SEQUENCE;
  77. @ServerSide
  78. public class TelemetryDataLoaderImpl implements TelemetryDataLoader {
  79. @VisibleForTesting
  80. static final String SCIM_PROPERTY_ENABLED = "sonar.scim.enabled";
  81. private static final String UNDETECTED = "undetected";
  82. private static final Map<String, String> LANGUAGES_BY_SECURITY_JSON_PROPERTY_MAP = Map.of(
  83. "sonar.security.config.javasecurity", "java",
  84. "sonar.security.config.phpsecurity", "php",
  85. "sonar.security.config.pythonsecurity", "python",
  86. "sonar.security.config.roslyn.sonaranalyzer.security.cs", "csharp");
  87. private final Server server;
  88. private final DbClient dbClient;
  89. private final PluginRepository pluginRepository;
  90. private final PlatformEditionProvider editionProvider;
  91. private final Configuration configuration;
  92. private final InternalProperties internalProperties;
  93. private final DockerSupport dockerSupport;
  94. private final QualityGateCaycChecker qualityGateCaycChecker;
  95. private final QualityGateFinder qualityGateFinder;
  96. @Inject
  97. public TelemetryDataLoaderImpl(Server server, DbClient dbClient, PluginRepository pluginRepository,
  98. PlatformEditionProvider editionProvider, InternalProperties internalProperties, Configuration configuration,
  99. DockerSupport dockerSupport, QualityGateCaycChecker qualityGateCaycChecker, QualityGateFinder qualityGateFinder) {
  100. this.server = server;
  101. this.dbClient = dbClient;
  102. this.pluginRepository = pluginRepository;
  103. this.editionProvider = editionProvider;
  104. this.internalProperties = internalProperties;
  105. this.configuration = configuration;
  106. this.dockerSupport = dockerSupport;
  107. this.qualityGateCaycChecker = qualityGateCaycChecker;
  108. this.qualityGateFinder = qualityGateFinder;
  109. }
  110. private static Database loadDatabaseMetadata(DbSession dbSession) {
  111. try {
  112. DatabaseMetaData metadata = dbSession.getConnection().getMetaData();
  113. return new Database(metadata.getDatabaseProductName(), metadata.getDatabaseProductVersion());
  114. } catch (SQLException e) {
  115. throw new IllegalStateException("Fail to get DB metadata", e);
  116. }
  117. }
  118. @Override
  119. public TelemetryData load() {
  120. TelemetryData.Builder data = TelemetryData.builder();
  121. data.setMessageSequenceNumber(retrieveCurrentMessageSequenceNumber() + 1);
  122. data.setServerId(server.getId());
  123. data.setVersion(server.getVersion());
  124. data.setEdition(editionProvider.get().orElse(null));
  125. Function<PluginInfo, String> getVersion = plugin -> plugin.getVersion() == null ? "undefined" : plugin.getVersion().getName();
  126. Map<String, String> plugins = pluginRepository.getPluginInfos().stream().collect(MoreCollectors.uniqueIndex(PluginInfo::getKey,
  127. getVersion));
  128. data.setPlugins(plugins);
  129. try (DbSession dbSession = dbClient.openSession(false)) {
  130. data.setDatabase(loadDatabaseMetadata(dbSession));
  131. String defaultQualityGateUuid = qualityGateFinder.getDefault(dbSession).getUuid();
  132. data.setDefaultQualityGate(defaultQualityGateUuid);
  133. resolveUnanalyzedLanguageCode(data, dbSession);
  134. resolveProjectStatistics(data, dbSession, defaultQualityGateUuid);
  135. resolveProjects(data, dbSession);
  136. resolveQualityGates(data, dbSession);
  137. resolveUsers(data, dbSession);
  138. }
  139. setSecurityCustomConfigIfPresent(data);
  140. Optional<String> installationDateProperty = internalProperties.read(InternalProperties.INSTALLATION_DATE);
  141. installationDateProperty.ifPresent(s -> data.setInstallationDate(Long.valueOf(s)));
  142. Optional<String> installationVersionProperty = internalProperties.read(InternalProperties.INSTALLATION_VERSION);
  143. return data
  144. .setInstallationVersion(installationVersionProperty.orElse(null))
  145. .setInDocker(dockerSupport.isRunningInDocker())
  146. .setIsScimEnabled(isScimEnabled())
  147. .build();
  148. }
  149. private void resolveUnanalyzedLanguageCode(TelemetryData.Builder data, DbSession dbSession) {
  150. long numberOfUnanalyzedCMeasures = dbClient.liveMeasureDao().countProjectsHavingMeasure(dbSession, UNANALYZED_C_KEY);
  151. long numberOfUnanalyzedCppMeasures = dbClient.liveMeasureDao().countProjectsHavingMeasure(dbSession, UNANALYZED_CPP_KEY);
  152. editionProvider.get()
  153. .filter(edition -> edition.equals(COMMUNITY))
  154. .ifPresent(edition -> {
  155. data.setHasUnanalyzedC(numberOfUnanalyzedCMeasures > 0);
  156. data.setHasUnanalyzedCpp(numberOfUnanalyzedCppMeasures > 0);
  157. });
  158. }
  159. private Long retrieveCurrentMessageSequenceNumber() {
  160. return internalProperties.read(I_PROP_MESSAGE_SEQUENCE).map(Long::parseLong).orElse(0L);
  161. }
  162. private void resolveProjectStatistics(TelemetryData.Builder data, DbSession dbSession, String defaultQualityGateUuid) {
  163. List<String> projectUuids = dbClient.projectDao().selectAllProjectUuids(dbSession);
  164. Map<String, String> scmByProject = getAnalysisPropertyByProject(dbSession, SONAR_ANALYSIS_DETECTEDSCM);
  165. Map<String, String> ciByProject = getAnalysisPropertyByProject(dbSession, SONAR_ANALYSIS_DETECTEDCI);
  166. Map<String, ProjectAlmKeyAndProject> almAndUrlByProject = getAlmAndUrlByProject(dbSession);
  167. Map<String, PrBranchAnalyzedLanguageCountByProjectDto> prAndBranchCountByProject =
  168. dbClient.branchDao().countPrBranchAnalyzedLanguageByProjectUuid(dbSession)
  169. .stream().collect(toMap(PrBranchAnalyzedLanguageCountByProjectDto::getProjectUuid, Function.identity()));
  170. Map<String, String> qgatesByProject = getProjectQgatesMap(dbSession);
  171. Map<String, Map<String, Number>> metricsByProject =
  172. getProjectMetricsByMetricKeys(dbSession, TECHNICAL_DEBT_KEY, DEVELOPMENT_COST_KEY, SECURITY_HOTSPOTS_KEY, VULNERABILITIES_KEY,
  173. BUGS_KEY);
  174. List<TelemetryData.ProjectStatistics> projectStatistics = new ArrayList<>();
  175. for (String projectUuid : projectUuids) {
  176. Map<String, Number> metrics = metricsByProject.getOrDefault(projectUuid, Collections.emptyMap());
  177. Optional<PrBranchAnalyzedLanguageCountByProjectDto> counts = ofNullable(prAndBranchCountByProject.get(projectUuid));
  178. TelemetryData.ProjectStatistics stats = new TelemetryData.ProjectStatistics.Builder()
  179. .setProjectUuid(projectUuid)
  180. .setBranchCount(counts.map(PrBranchAnalyzedLanguageCountByProjectDto::getBranch).orElse(0L))
  181. .setPRCount(counts.map(PrBranchAnalyzedLanguageCountByProjectDto::getPullRequest).orElse(0L))
  182. .setQG(qgatesByProject.getOrDefault(projectUuid, defaultQualityGateUuid))
  183. .setScm(Optional.ofNullable(scmByProject.get(projectUuid)).orElse(UNDETECTED))
  184. .setCi(Optional.ofNullable(ciByProject.get(projectUuid)).orElse(UNDETECTED))
  185. .setDevops(resolveDevopsPlatform(almAndUrlByProject, projectUuid))
  186. .setBugs(metrics.getOrDefault("bugs", null))
  187. .setDevelopmentCost(metrics.getOrDefault("development_cost", null))
  188. .setVulnerabilities(metrics.getOrDefault("vulnerabilities", null))
  189. .setSecurityHotspots(metrics.getOrDefault("security_hotspots", null))
  190. .setTechnicalDebt(metrics.getOrDefault("sqale_index", null))
  191. .build();
  192. projectStatistics.add(stats);
  193. }
  194. data.setProjectStatistics(projectStatistics);
  195. }
  196. private static String resolveDevopsPlatform(Map<String, ProjectAlmKeyAndProject> almAndUrlByProject, String projectUuid) {
  197. if (almAndUrlByProject.containsKey(projectUuid)) {
  198. ProjectAlmKeyAndProject projectAlmKeyAndProject = almAndUrlByProject.get(projectUuid);
  199. return getAlmName(projectAlmKeyAndProject.getAlmId(), projectAlmKeyAndProject.getUrl());
  200. }
  201. return UNDETECTED;
  202. }
  203. private void resolveProjects(TelemetryData.Builder data, DbSession dbSession) {
  204. List<ProjectMeasureDto> measures = dbClient.measureDao().selectLastMeasureForAllProjects(dbSession, NCLOC_LANGUAGE_DISTRIBUTION_KEY);
  205. List<TelemetryData.Project> projects = new ArrayList<>();
  206. for (ProjectMeasureDto measure : measures) {
  207. for (String measureTextValue : measure.getTextValue().split(";")) {
  208. String[] languageAndLoc = measureTextValue.split("=");
  209. String language = languageAndLoc[0];
  210. Long loc = Long.parseLong(languageAndLoc[1]);
  211. projects.add(new TelemetryData.Project(measure.getProjectUuid(), measure.getLastAnalysis(), language, loc));
  212. }
  213. }
  214. data.setProjects(projects);
  215. }
  216. private void resolveQualityGates(TelemetryData.Builder data, DbSession dbSession) {
  217. List<TelemetryData.QualityGate> qualityGates = new ArrayList<>();
  218. Collection<QualityGateDto> qualityGateDtos = dbClient.qualityGateDao().selectAll(dbSession);
  219. for (QualityGateDto qualityGateDto : qualityGateDtos) {
  220. qualityGates.add(
  221. new TelemetryData.QualityGate(qualityGateDto.getUuid(), qualityGateCaycChecker.checkCaycCompliant(dbSession,
  222. qualityGateDto.getUuid()).toString())
  223. );
  224. }
  225. data.setQualityGates(qualityGates);
  226. }
  227. private void resolveUsers(TelemetryData.Builder data, DbSession dbSession) {
  228. data.setUsers(dbClient.userDao().selectUsersForTelemetry(dbSession));
  229. }
  230. private void setSecurityCustomConfigIfPresent(TelemetryData.Builder data) {
  231. editionProvider.get()
  232. .filter(edition -> asList(ENTERPRISE, DATACENTER).contains(edition))
  233. .ifPresent(edition -> data.setCustomSecurityConfigs(getCustomerSecurityConfigurations()));
  234. }
  235. private Map<String, String> getAnalysisPropertyByProject(DbSession dbSession, String analysisPropertyKey) {
  236. return dbClient.analysisPropertiesDao()
  237. .selectAnalysisPropertyValueInLastAnalysisPerProject(dbSession, analysisPropertyKey)
  238. .stream()
  239. .collect(toMap(AnalysisPropertyValuePerProject::getProjectUuid, AnalysisPropertyValuePerProject::getPropertyValue));
  240. }
  241. private Map<String, ProjectAlmKeyAndProject> getAlmAndUrlByProject(DbSession dbSession) {
  242. List<ProjectAlmKeyAndProject> projectAlmKeyAndProjects = dbClient.projectAlmSettingDao().selectAlmTypeAndUrlByProject(dbSession);
  243. return projectAlmKeyAndProjects.stream().collect(toMap(ProjectAlmKeyAndProject::getProjectUuid, Function.identity()));
  244. }
  245. private static String getAlmName(String alm, String url) {
  246. if (checkIfCloudAlm(alm, ALM.GITHUB.getId(), url, "https://api.github.com")) {
  247. return "github_cloud";
  248. }
  249. if (checkIfCloudAlm(alm, ALM.GITLAB.getId(), url, "https://gitlab.com/api/v4")) {
  250. return "gitlab_cloud";
  251. }
  252. if (checkIfCloudAlm(alm, ALM.AZURE_DEVOPS.getId(), url, "https://dev.azure.com")) {
  253. return "azure_devops_cloud";
  254. }
  255. if (ALM.BITBUCKET_CLOUD.getId().equals(alm)) {
  256. return alm;
  257. }
  258. return alm + "_server";
  259. }
  260. private Map<String, String> getProjectQgatesMap(DbSession dbSession) {
  261. return dbClient.projectQgateAssociationDao().selectAll(dbSession)
  262. .stream()
  263. .collect(toMap(ProjectQgateAssociationDto::getUuid, p -> Optional.ofNullable(p.getGateUuid()).orElse("")));
  264. }
  265. private Map<String, Map<String, Number>> getProjectMetricsByMetricKeys(DbSession dbSession, String... metricKeys) {
  266. Map<String, String> metricNamesByUuid = dbClient.metricDao().selectByKeys(dbSession, asList(metricKeys))
  267. .stream()
  268. .collect(toMap(MetricDto::getUuid, MetricDto::getKey));
  269. // metrics can be empty for un-analyzed projects
  270. if (metricNamesByUuid.isEmpty()) {
  271. return Collections.emptyMap();
  272. }
  273. return dbClient.liveMeasureDao().selectForProjectsByMetricUuids(dbSession, metricNamesByUuid.keySet())
  274. .stream()
  275. .collect(groupingBy(LiveMeasureDto::getProjectUuid,
  276. toMap(lmDto -> metricNamesByUuid.get(lmDto.getMetricUuid()),
  277. lmDto -> Optional.ofNullable(lmDto.getValue()).orElseGet(() -> Double.valueOf(lmDto.getTextValue())),
  278. (oldValue, newValue) -> newValue, HashMap::new)));
  279. }
  280. private static boolean checkIfCloudAlm(String almRaw, String alm, String url, String cloudUrl) {
  281. return alm.equals(almRaw) && startsWithIgnoreCase(url, cloudUrl);
  282. }
  283. @Override
  284. public String loadServerId() {
  285. return server.getId();
  286. }
  287. private Set<String> getCustomerSecurityConfigurations() {
  288. return LANGUAGES_BY_SECURITY_JSON_PROPERTY_MAP.keySet().stream()
  289. .filter(this::isPropertyPresentInConfiguration)
  290. .map(LANGUAGES_BY_SECURITY_JSON_PROPERTY_MAP::get)
  291. .collect(Collectors.toSet());
  292. }
  293. private boolean isPropertyPresentInConfiguration(String property) {
  294. return configuration.get(property).isPresent();
  295. }
  296. private boolean isScimEnabled() {
  297. return this.configuration.getBoolean(SCIM_PROPERTY_ENABLED).orElse(false);
  298. }
  299. }