diff --git a/xuqm-log-service/pom.xml b/xuqm-log-service/pom.xml new file mode 100644 index 0000000..07837b5 --- /dev/null +++ b/xuqm-log-service/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + + com.xuqm + xuqmgroup-server-parent + 0.1.0-SNAPSHOT + ../pom.xml + + + xuqm-log-service + xuqm-log-service + Log collection, dedup, symbolication, webhook notification & funnel analytics + + + + com.xuqm + common + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-actuator + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.mysql + mysql-connector-j + runtime + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-mysql + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/LogServiceApplication.java b/xuqm-log-service/src/main/java/com/xuqm/log/LogServiceApplication.java new file mode 100644 index 0000000..48ce01e --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/LogServiceApplication.java @@ -0,0 +1,15 @@ +package com.xuqm.log; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableAsync; + +@SpringBootApplication +@ComponentScan(basePackages = {"com.xuqm.log", "com.xuqm.common"}) +@EnableAsync +public class LogServiceApplication { + public static void main(String[] args) { + SpringApplication.run(LogServiceApplication.class, args); + } +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/dto/EventBatchRequest.java b/xuqm-log-service/src/main/java/com/xuqm/log/dto/EventBatchRequest.java new file mode 100644 index 0000000..48f3f16 --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/dto/EventBatchRequest.java @@ -0,0 +1,21 @@ +package com.xuqm.log.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +public record EventBatchRequest( + @JsonProperty("events") @NotEmpty List events +) { + public record EventItem( + @NotBlank @JsonProperty("appKey") String appKey, + @NotBlank String name, + @JsonProperty("userId") String userId, + @JsonProperty("sessionId") String sessionId, + String properties, + String platform, + @JsonProperty("appVersion") String appVersion, + @JsonProperty("timestamp") long timestamp + ) {} +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/dto/IssueBatchRequest.java b/xuqm-log-service/src/main/java/com/xuqm/log/dto/IssueBatchRequest.java new file mode 100644 index 0000000..ba2abd2 --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/dto/IssueBatchRequest.java @@ -0,0 +1,23 @@ +package com.xuqm.log.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +public record IssueBatchRequest( + @JsonProperty("events") @NotEmpty List events +) { + public record IssueEventItem( + @NotBlank String type, + String message, + String stack, + @NotBlank String fingerprint, + @JsonProperty("timestamp") long timestamp, + @JsonProperty("userId") String userId, + @JsonProperty("sessionId") String sessionId, + @NotBlank @JsonProperty("appKey") String appKey, + String platform, + @JsonProperty("appVersion") String appVersion + ) {} +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/dto/IssueEventResponse.java b/xuqm-log-service/src/main/java/com/xuqm/log/dto/IssueEventResponse.java new file mode 100644 index 0000000..40c404e --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/dto/IssueEventResponse.java @@ -0,0 +1,19 @@ +package com.xuqm.log.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.LocalDateTime; + +public record IssueEventResponse( + Long id, + @JsonProperty("issueId") Long issueId, + @JsonProperty("appKey") String appKey, + @JsonProperty("userId") String userId, + @JsonProperty("sessionId") String sessionId, + String message, + String stack, + @JsonProperty("stackSymbolicated") String stackSymbolicated, + String metadata, + String platform, + @JsonProperty("appVersion") String appVersion, + @JsonProperty("createdAt") LocalDateTime createdAt +) {} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/dto/IssueResponse.java b/xuqm-log-service/src/main/java/com/xuqm/log/dto/IssueResponse.java new file mode 100644 index 0000000..8cecdce --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/dto/IssueResponse.java @@ -0,0 +1,20 @@ +package com.xuqm.log.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.LocalDateTime; +import java.util.List; + +public record IssueResponse( + Long id, + @JsonProperty("appKey") String appKey, + String fingerprint, + String type, + String title, + @JsonProperty("firstSeenAt") LocalDateTime firstSeenAt, + @JsonProperty("lastSeenAt") LocalDateTime lastSeenAt, + int count, + @JsonProperty("isResolved") boolean isResolved, + String platform, + @JsonProperty("appVersion") String appVersion, + List events +) {} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/dto/WebhookRequest.java b/xuqm-log-service/src/main/java/com/xuqm/log/dto/WebhookRequest.java new file mode 100644 index 0000000..ff0f893 --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/dto/WebhookRequest.java @@ -0,0 +1,13 @@ +package com.xuqm.log.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record WebhookRequest( + @NotBlank @JsonProperty("appKey") String appKey, + @NotBlank String url, + @NotNull List events, + @JsonProperty("cooldownSec") int cooldownSec +) {} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/dto/WebhookResponse.java b/xuqm-log-service/src/main/java/com/xuqm/log/dto/WebhookResponse.java new file mode 100644 index 0000000..9c9fdf4 --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/dto/WebhookResponse.java @@ -0,0 +1,15 @@ +package com.xuqm.log.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.LocalDateTime; +import java.util.List; + +public record WebhookResponse( + Long id, + @JsonProperty("appKey") String appKey, + String url, + List events, + @JsonProperty("cooldownSec") int cooldownSec, + boolean enabled, + @JsonProperty("createdAt") LocalDateTime createdAt +) {} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogEventEntity.java b/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogEventEntity.java new file mode 100644 index 0000000..f9e85f9 --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogEventEntity.java @@ -0,0 +1,64 @@ +package com.xuqm.log.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "log_events") +public class LogEventEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 64) + private String appKey; + + @Column(nullable = false, length = 256) + private String name; + + @Column(length = 128) + private String userId; + + @Column(length = 128) + private String sessionId; + + @Column(columnDefinition = "JSON") + private String properties; + + @Column(length = 16) + private String platform; + + @Column(length = 32) + private String appVersion; + + @Column(nullable = false) + private LocalDateTime createdAt; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getAppKey() { return appKey; } + public void setAppKey(String appKey) { this.appKey = appKey; } + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getUserId() { return userId; } + public void setUserId(String userId) { this.userId = userId; } + + public String getSessionId() { return sessionId; } + public void setSessionId(String sessionId) { this.sessionId = sessionId; } + + public String getProperties() { return properties; } + public void setProperties(String properties) { this.properties = properties; } + + public String getPlatform() { return platform; } + public void setPlatform(String platform) { this.platform = platform; } + + public String getAppVersion() { return appVersion; } + public void setAppVersion(String appVersion) { this.appVersion = appVersion; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogIssueEntity.java b/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogIssueEntity.java new file mode 100644 index 0000000..5e55475 --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogIssueEntity.java @@ -0,0 +1,76 @@ +package com.xuqm.log.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "log_issues") +public class LogIssueEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 64) + private String appKey; + + @Column(nullable = false, length = 64) + private String fingerprint; + + @Column(nullable = false, length = 32) + private String type; + + @Column(nullable = false, length = 500) + private String title; + + @Column(nullable = false) + private LocalDateTime firstSeenAt; + + @Column(nullable = false) + private LocalDateTime lastSeenAt; + + @Column(nullable = false) + private int count = 1; + + @Column(nullable = false) + private boolean isResolved = false; + + @Column(length = 16) + private String platform; + + @Column(length = 32) + private String appVersion; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getAppKey() { return appKey; } + public void setAppKey(String appKey) { this.appKey = appKey; } + + public String getFingerprint() { return fingerprint; } + public void setFingerprint(String fingerprint) { this.fingerprint = fingerprint; } + + public String getType() { return type; } + public void setType(String type) { this.type = type; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public LocalDateTime getFirstSeenAt() { return firstSeenAt; } + public void setFirstSeenAt(LocalDateTime firstSeenAt) { this.firstSeenAt = firstSeenAt; } + + public LocalDateTime getLastSeenAt() { return lastSeenAt; } + public void setLastSeenAt(LocalDateTime lastSeenAt) { this.lastSeenAt = lastSeenAt; } + + public int getCount() { return count; } + public void setCount(int count) { this.count = count; } + + public boolean isResolved() { return isResolved; } + public void setResolved(boolean resolved) { isResolved = resolved; } + + public String getPlatform() { return platform; } + public void setPlatform(String platform) { this.platform = platform; } + + public String getAppVersion() { return appVersion; } + public void setAppVersion(String appVersion) { this.appVersion = appVersion; } +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogIssueEventEntity.java b/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogIssueEventEntity.java new file mode 100644 index 0000000..41cbdc4 --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogIssueEventEntity.java @@ -0,0 +1,82 @@ +package com.xuqm.log.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "log_issue_events") +public class LogIssueEventEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long issueId; + + @Column(nullable = false, length = 64) + private String appKey; + + @Column(length = 128) + private String userId; + + @Column(length = 128) + private String sessionId; + + @Column(columnDefinition = "TEXT") + private String message; + + @Column(columnDefinition = "LONGTEXT") + private String stack; + + @Column(name = "stack_symbolicated", columnDefinition = "LONGTEXT") + private String stackSymbolicated; + + @Column(columnDefinition = "JSON") + private String metadata; + + @Column(length = 16) + private String platform; + + @Column(length = 32) + private String appVersion; + + @Column(nullable = false) + private LocalDateTime createdAt; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Long getIssueId() { return issueId; } + public void setIssueId(Long issueId) { this.issueId = issueId; } + + public String getAppKey() { return appKey; } + public void setAppKey(String appKey) { this.appKey = appKey; } + + public String getUserId() { return userId; } + public void setUserId(String userId) { this.userId = userId; } + + public String getSessionId() { return sessionId; } + public void setSessionId(String sessionId) { this.sessionId = sessionId; } + + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + + public String getStack() { return stack; } + public void setStack(String stack) { this.stack = stack; } + + public String getStackSymbolicated() { return stackSymbolicated; } + public void setStackSymbolicated(String stackSymbolicated) { this.stackSymbolicated = stackSymbolicated; } + + public String getMetadata() { return metadata; } + public void setMetadata(String metadata) { this.metadata = metadata; } + + public String getPlatform() { return platform; } + public void setPlatform(String platform) { this.platform = platform; } + + public String getAppVersion() { return appVersion; } + public void setAppVersion(String appVersion) { this.appVersion = appVersion; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogSourcemapEntity.java b/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogSourcemapEntity.java new file mode 100644 index 0000000..4aea61d --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogSourcemapEntity.java @@ -0,0 +1,52 @@ +package com.xuqm.log.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "log_sourcemaps") +public class LogSourcemapEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 64) + private String appKey; + + @Column(nullable = false, length = 16) + private String platform; + + @Column(nullable = false, length = 32) + private String appVersion; + + @Column(nullable = false, length = 128) + private String bundleName = "index"; + + @Column(nullable = false, length = 512) + private String storageKey; + + @Column(nullable = false) + private LocalDateTime uploadedAt; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getAppKey() { return appKey; } + public void setAppKey(String appKey) { this.appKey = appKey; } + + public String getPlatform() { return platform; } + public void setPlatform(String platform) { this.platform = platform; } + + public String getAppVersion() { return appVersion; } + public void setAppVersion(String appVersion) { this.appVersion = appVersion; } + + public String getBundleName() { return bundleName; } + public void setBundleName(String bundleName) { this.bundleName = bundleName; } + + public String getStorageKey() { return storageKey; } + public void setStorageKey(String storageKey) { this.storageKey = storageKey; } + + public LocalDateTime getUploadedAt() { return uploadedAt; } + public void setUploadedAt(LocalDateTime uploadedAt) { this.uploadedAt = uploadedAt; } +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogWebhookEntity.java b/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogWebhookEntity.java new file mode 100644 index 0000000..4c8d3a6 --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/entity/LogWebhookEntity.java @@ -0,0 +1,52 @@ +package com.xuqm.log.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "log_webhooks") +public class LogWebhookEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 64) + private String appKey; + + @Column(nullable = false, length = 1024) + private String url; + + @Column(nullable = false, columnDefinition = "JSON") + private String events; + + @Column(nullable = false) + private int cooldownSec = 3600; + + @Column(nullable = false) + private boolean enabled = true; + + @Column(nullable = false) + private LocalDateTime createdAt; + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getAppKey() { return appKey; } + public void setAppKey(String appKey) { this.appKey = appKey; } + + public String getUrl() { return url; } + public void setUrl(String url) { this.url = url; } + + public String getEvents() { return events; } + public void setEvents(String events) { this.events = events; } + + public int getCooldownSec() { return cooldownSec; } + public void setCooldownSec(int cooldownSec) { this.cooldownSec = cooldownSec; } + + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogEventRepository.java b/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogEventRepository.java new file mode 100644 index 0000000..46dc2ea --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogEventRepository.java @@ -0,0 +1,44 @@ +package com.xuqm.log.repository; + +import com.xuqm.log.entity.LogEventEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; + +public interface LogEventRepository extends JpaRepository { + + Page findByAppKeyAndNameAndUserIdAndCreatedAtBetween( + String appKey, String name, String userId, + LocalDateTime from, LocalDateTime to, + Pageable pageable); + + Page findByAppKeyAndNameAndCreatedAtBetween( + String appKey, String name, + LocalDateTime from, LocalDateTime to, + Pageable pageable); + + Page findByAppKeyAndCreatedAtBetween( + String appKey, LocalDateTime from, LocalDateTime to, Pageable pageable); + + @Query(value = "SELECT e.session_id, e.name, MIN(e.created_at) AS first_time " + + "FROM log_events e " + + "WHERE e.app_key = :appKey AND e.name IN :steps AND e.created_at BETWEEN :from AND :to " + + "GROUP BY e.session_id, e.name", + nativeQuery = true) + List findFunnelData(@Param("appKey") String appKey, + @Param("steps") List steps, + @Param("from") LocalDateTime from, + @Param("to") LocalDateTime to); + + long countByAppKeyAndNameAndCreatedAtBetween(String appKey, String name, LocalDateTime from, LocalDateTime to); + + @Query("SELECT DISTINCT e.name FROM LogEventEntity e WHERE e.appKey = :appKey") + List findDistinctEventNames(@Param("appKey") String appKey); + + void deleteByAppKey(String appKey); +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogIssueEventRepository.java b/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogIssueEventRepository.java new file mode 100644 index 0000000..d40ce56 --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogIssueEventRepository.java @@ -0,0 +1,21 @@ +package com.xuqm.log.repository; + +import com.xuqm.log.entity.LogIssueEventEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDateTime; +import java.util.List; + +public interface LogIssueEventRepository extends JpaRepository { + + List findTop20ByIssueIdOrderByCreatedAtDesc(Long issueId); + + Page findByAppKeyAndCreatedAtBetween( + String appKey, LocalDateTime from, LocalDateTime to, Pageable pageable); + + void deleteByIssueId(Long issueId); + + void deleteByAppKey(String appKey); +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogIssueRepository.java b/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogIssueRepository.java new file mode 100644 index 0000000..f21be73 --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogIssueRepository.java @@ -0,0 +1,58 @@ +package com.xuqm.log.repository; + +import com.xuqm.log.entity.LogIssueEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface LogIssueRepository extends JpaRepository { + + Optional findByAppKeyAndFingerprint(String appKey, String fingerprint); + + @Modifying + @Query("UPDATE LogIssueEntity i SET i.count = i.count + 1, i.lastSeenAt = :now WHERE i.appKey = :appKey AND i.fingerprint = :fingerprint") + int incrementCount(@Param("appKey") String appKey, @Param("fingerprint") String fingerprint, @Param("now") LocalDateTime now); + + Page findByAppKeyAndTypeAndPlatformAndLastSeenAtBetween( + String appKey, String type, String platform, + LocalDateTime from, LocalDateTime to, + Pageable pageable); + + Page findByAppKeyAndLastSeenAtBetween( + String appKey, LocalDateTime from, LocalDateTime to, Pageable pageable); + + Page findByAppKey(String appKey, Pageable pageable); + + @Query("SELECT i FROM LogIssueEntity i WHERE i.appKey = :appKey AND i.lastSeenAt BETWEEN :from AND :to ORDER BY i.count DESC") + List findTopByFrequency(@Param("appKey") String appKey, + @Param("from") LocalDateTime from, + @Param("to") LocalDateTime to, + Pageable pageable); + + @Query(value = "SELECT i.*, " + + "(i.count * COALESCE((SELECT COUNT(DISTINCT e.user_id) FROM log_issue_events e WHERE e.issue_id = i.id AND e.user_id IS NOT NULL), 1) * " + + "CASE i.type WHEN 'native_crash' THEN 10 WHEN 'js_error' THEN 5 WHEN 'api_error' THEN 3 ELSE 1 END) AS risk_score " + + "FROM log_issues i WHERE i.app_key = :appKey AND i.last_seen_at BETWEEN :from AND :to " + + "ORDER BY risk_score DESC", + nativeQuery = true) + List findTopByRisk(@Param("appKey") String appKey, + @Param("from") LocalDateTime from, + @Param("to") LocalDateTime to, + Pageable pageable); + + long countByAppKeyAndFirstSeenAtBetween(String appKey, LocalDateTime from, LocalDateTime to); + + long countByAppKeyAndFirstSeenAtAfter(String appKey, LocalDateTime since); + + @Query("SELECT DISTINCT i.platform FROM LogIssueEntity i WHERE i.appKey = :appKey") + List findDistinctPlatforms(@Param("appKey") String appKey); + + void deleteByAppKey(String appKey); +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogSourcemapRepository.java b/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogSourcemapRepository.java new file mode 100644 index 0000000..353b76a --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogSourcemapRepository.java @@ -0,0 +1,14 @@ +package com.xuqm.log.repository; + +import com.xuqm.log.entity.LogSourcemapEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface LogSourcemapRepository extends JpaRepository { + + Optional findByAppKeyAndPlatformAndAppVersionAndBundleName( + String appKey, String platform, String appVersion, String bundleName); + + void deleteByAppKey(String appKey); +} diff --git a/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogWebhookRepository.java b/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogWebhookRepository.java new file mode 100644 index 0000000..8c97971 --- /dev/null +++ b/xuqm-log-service/src/main/java/com/xuqm/log/repository/LogWebhookRepository.java @@ -0,0 +1,15 @@ +package com.xuqm.log.repository; + +import com.xuqm.log.entity.LogWebhookEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface LogWebhookRepository extends JpaRepository { + + List findByAppKeyAndEnabledTrue(String appKey); + + List findByAppKey(String appKey); + + void deleteByAppKey(String appKey); +} diff --git a/xuqm-log-service/src/main/resources/application.yml b/xuqm-log-service/src/main/resources/application.yml new file mode 100644 index 0000000..f5a2012 --- /dev/null +++ b/xuqm-log-service/src/main/resources/application.yml @@ -0,0 +1,68 @@ +server: + port: 9006 + +spring: + application: + name: xuqm-log-service + datasource: + url: jdbc:mysql://39.107.53.187:3306/xuqm_log?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true + username: xuqm + password: Xuqm@2026 + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + minimum-idle: 5 + maximum-pool-size: 20 + connection-timeout: 30000 + idle-timeout: 300000 + max-lifetime: 900000 + jpa: + hibernate: + ddl-auto: validate + show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + format_sql: true + data: + redis: + host: redisdev.xuqinmin.com + port: 6379 + password: xuqinmin1022 + database: 2 + timeout: 10s + lettuce: + pool: + min-idle: 0 + max-idle: 8 + max-active: 8 + jackson: + time-zone: UTC + serialization: + write-dates-as-timestamps: false + flyway: + enabled: true + baseline-on-migrate: true + baseline-version: 0 + locations: classpath:db/migration + table: flyway_history_log + servlet: + multipart: + max-file-size: 50MB + max-request-size: 50MB + +log-service: + sourcemap: + storage-dir: ${LOG_SOURCEMAP_DIR:/data/log-service/sourcemaps} + webhook: + connect-timeout-ms: 3000 + read-timeout-ms: 5000 + +logging: + level: + com.xuqm: DEBUG + +management: + endpoints: + web: + exposure: + include: health,info diff --git a/xuqm-log-service/src/main/resources/db/migration/V1__init.sql b/xuqm-log-service/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 0000000..bc1c0fa --- /dev/null +++ b/xuqm-log-service/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,69 @@ +CREATE TABLE log_issues ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + app_key VARCHAR(64) NOT NULL, + fingerprint CHAR(64) NOT NULL, + type VARCHAR(32) NOT NULL, + title VARCHAR(500) NOT NULL, + first_seen_at DATETIME NOT NULL, + last_seen_at DATETIME NOT NULL, + count INT NOT NULL DEFAULT 1, + is_resolved TINYINT NOT NULL DEFAULT 0, + platform VARCHAR(16), + app_version VARCHAR(32), + UNIQUE KEY uk_app_fp (app_key, fingerprint), + INDEX idx_app_last (app_key, last_seen_at), + INDEX idx_app_type (app_key, type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE log_issue_events ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + issue_id BIGINT NOT NULL, + app_key VARCHAR(64) NOT NULL, + user_id VARCHAR(128), + session_id VARCHAR(128), + message TEXT, + stack LONGTEXT, + stack_symbolicated LONGTEXT, + metadata JSON, + platform VARCHAR(16), + app_version VARCHAR(32), + created_at DATETIME NOT NULL, + INDEX idx_issue (issue_id), + INDEX idx_app_time (app_key, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE log_events ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + app_key VARCHAR(64) NOT NULL, + name VARCHAR(256) NOT NULL, + user_id VARCHAR(128), + session_id VARCHAR(128), + properties JSON, + platform VARCHAR(16), + app_version VARCHAR(32), + created_at DATETIME NOT NULL, + INDEX idx_app_name (app_key, name), + INDEX idx_app_time (app_key, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE log_sourcemaps ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + app_key VARCHAR(64) NOT NULL, + platform VARCHAR(16) NOT NULL, + app_version VARCHAR(32) NOT NULL, + bundle_name VARCHAR(128) NOT NULL DEFAULT 'index', + storage_key VARCHAR(512) NOT NULL, + uploaded_at DATETIME NOT NULL, + UNIQUE KEY uk_map (app_key, platform, app_version, bundle_name) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE log_webhooks ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + app_key VARCHAR(64) NOT NULL, + url VARCHAR(1024) NOT NULL, + events JSON NOT NULL, + cooldown_sec INT NOT NULL DEFAULT 3600, + enabled TINYINT NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL, + INDEX idx_app (app_key) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;