diff --git a/src/main/java/com/epam/reportportal/config/JacksonConfiguration.java b/src/main/java/com/epam/reportportal/config/JacksonConfiguration.java new file mode 100644 index 0000000..8cd7255 --- /dev/null +++ b/src/main/java/com/epam/reportportal/config/JacksonConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.config; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Siarhei Hrabko + */ +@Configuration +public class JacksonConfiguration { + + /** + * @return Configured object mapper + */ + @Bean(name = "objectMapper") + public ObjectMapper objectMapper() { + ObjectMapper om = new ObjectMapper(); + om.setAnnotationIntrospector(new JacksonAnnotationIntrospector()); + om.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true); + om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + om.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + om.registerModule(new JavaTimeModule()); + return om; + } +} diff --git a/src/main/java/com/epam/reportportal/jobs/statistics/DefectUpdateStatisticsJob.java b/src/main/java/com/epam/reportportal/jobs/statistics/DefectUpdateStatisticsJob.java new file mode 100644 index 0000000..a193ed8 --- /dev/null +++ b/src/main/java/com/epam/reportportal/jobs/statistics/DefectUpdateStatisticsJob.java @@ -0,0 +1,195 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.jobs.statistics; + +import static org.springframework.http.HttpMethod.POST; + +import com.epam.reportportal.jobs.BaseJob; +import java.security.SecureRandom; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.HashSet; +import java.util.Set; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +/** + * Sends statistics about amounts of manual analyzed items to the GA4 service. + * + * @author Maksim Antonov + */ +@Service +public class DefectUpdateStatisticsJob extends BaseJob { + + private static final String GA_URL = "https://www.google-analytics.com/mp/collect?measurement_id=%s&api_secret=%s"; + private static final String DATE_BEFORE = "date_before"; + + private static final String SELECT_INSTANCE_ID_QUERY = "SELECT value FROM server_settings WHERE key = 'server.details.instance';"; + private static final String SELECT_STATISTICS_QUERY = "SELECT * FROM analytics_data WHERE type = 'DEFECT_UPDATE_STATISTICS' AND created_at >= :date_before::TIMESTAMP;"; + private static final String DELETE_STATISTICS_QUERY = "DELETE FROM analytics_data WHERE type = 'DEFECT_UPDATE_STATISTICS';"; + + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private final RestTemplate restTemplate; + + private final String measurementId; + private final String apiSecret; + + + /** + * Initializes {@link DefectUpdateStatisticsJob}. + * + * @param jdbcTemplate {@link JdbcTemplate} + */ + @Autowired + public DefectUpdateStatisticsJob(JdbcTemplate jdbcTemplate, + @Value("${rp.environment.variable.ga.measurementId}") String measurementId, + @Value("${rp.environment.variable.ga.apiSecret}") String apiSecret, + NamedParameterJdbcTemplate namedParameterJdbcTemplate) { + super(jdbcTemplate); + this.measurementId = measurementId; + this.apiSecret = apiSecret; + this.namedParameterJdbcTemplate = namedParameterJdbcTemplate; + this.restTemplate = new RestTemplate(); + } + + + /** + * Sends analyzed items statistics. + */ + @Override + @Scheduled(cron = "${rp.environment.variable.ga.cron}") + @SchedulerLock(name = "defectUpdateStatisticsJob", lockAtMostFor = "24h") + @Transactional + public void execute() { + LOGGER.info("Start sending items defect update statistics"); + if (StringUtils.isEmpty(measurementId) || StringUtils.isEmpty(apiSecret)) { + LOGGER.info( + "Both 'measurementId' and 'apiSecret' environment variables should be provided in order to run the job 'defectUpdateStatisticsJob'"); + return; + } + + var now = Instant.now(); + var dateBefore = now.minus(1, ChronoUnit.DAYS) + .atOffset(ZoneOffset.UTC) + .toLocalDateTime(); + MapSqlParameterSource queryParams = new MapSqlParameterSource(); + queryParams.addValue(DATE_BEFORE, dateBefore); + + namedParameterJdbcTemplate.query(SELECT_STATISTICS_QUERY, queryParams, rs -> { + int autoAnalyzed = 0; + int userAnalyzed = 0; + int sentToAnalyze = 0; + String version; + boolean analyzerEnabled; + Set status = new HashSet<>(); + Set autoAnalysisState = new HashSet<>(); + + do { + var metadata = new JSONObject(rs.getString("metadata")) + .getJSONObject("metadata"); + + analyzerEnabled = metadata.optBoolean("analyzerEnabled"); + if (analyzerEnabled) { + autoAnalysisState.add(metadata.getBoolean("autoAnalysisOn") ? "on" : "off"); + } + + if (metadata.optInt("userAnalyzed") > 0) { + status.add("manually"); + } else { + status.add("automatically"); + } + + userAnalyzed += metadata.optInt("userAnalyzed"); + autoAnalyzed += metadata.optInt("analyzed"); + sentToAnalyze += metadata.optInt("userAnalyzed") + metadata.optInt("sentToAnalyze"); + version = metadata.getString("version"); + + } while (rs.next()); + + var instanceId = jdbcTemplate.queryForObject(SELECT_INSTANCE_ID_QUERY, String.class); + var params = new JSONObject(); + params.put("category", "analyzer"); + params.put("instanceID", instanceId); + params.put("timestamp", now.toEpochMilli()); + params.put("version", version); + params.put("type", analyzerEnabled ? "is_analyzer" : "not_analyzer"); + if (analyzerEnabled) { + params.put("number", autoAnalyzed + "#" + userAnalyzed + "#" + sentToAnalyze); + params.put("auto_analysis", String.join("#", autoAnalysisState)); + params.put("status", String.join("#", status)); + } + + var event = new JSONObject(); + event.put("name", "analyze_analyzer"); + event.put("params", params); + + JSONArray events = new JSONArray(); + events.put(event); + + JSONObject requestBody = new JSONObject(); + requestBody.put("client_id", + now.toEpochMilli() + "." + new SecureRandom().nextInt(100_000, 999_999)); + requestBody.put("events", events); + + sendRequest(requestBody); + + }); + + LOGGER.info("Completed items defect update statistics job"); + + } + + private void sendRequest(JSONObject requestBody) { + try { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Sending statistics data: {}", requestBody); + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity request = new HttpEntity<>(requestBody.toString(), headers); + + String url = String.format(GA_URL, measurementId, apiSecret); + + var response = restTemplate.exchange(url, POST, request, String.class); + if (response.getStatusCodeValue() != 204) { + LOGGER.error("Failed to send statistics: {}", response); + } + } catch (Exception e) { + LOGGER.error("Failed to send statistics", e); + } finally { + jdbcTemplate.execute(DELETE_STATISTICS_QUERY); + } + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 588149e..26b2ddd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -39,6 +39,10 @@ rp: project: ## 1 minute cron: '0 */1 * * * *' + ga: + apiSecret: + measurementId: + cron: '0 0 */24 * * *' executor: pool: storage: