feat: xuqm-log-service 新建 — 日志/错误追踪服务
Agent 5 — xuqm-log-service: - Spring Boot 3.x + MySQL + Redis - 5 张表:log_issues、log_issue_events、log_events、log_sourcemaps、log_webhooks - SDK 入库接口:POST /log/v1/issues/batch(指纹去重)、POST /log/v1/events/batch - 查询接口:issues 列表/详情、高频/高危排行、事件流水、漏斗分析、概览统计 - SourceMap 上传:POST /log/v1/sourcemaps/upload - Webhook CRUD + Redis SETNX 冷却逻辑 - Flyway 数据库迁移
这个提交包含在:
父节点
336ce72c7a
当前提交
936664c9cd
75
xuqm-log-service/pom.xml
普通文件
75
xuqm-log-service/pom.xml
普通文件
@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>xuqmgroup-server-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>xuqm-log-service</artifactId>
|
||||
<name>xuqm-log-service</name>
|
||||
<description>Log collection, dedup, symbolication, webhook notification & funnel analytics</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.xuqm</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<EventItem> 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
|
||||
) {}
|
||||
}
|
||||
@ -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<IssueEventItem> 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
|
||||
) {}
|
||||
}
|
||||
@ -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
|
||||
) {}
|
||||
@ -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<IssueEventResponse> events
|
||||
) {}
|
||||
@ -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<String> events,
|
||||
@JsonProperty("cooldownSec") int cooldownSec
|
||||
) {}
|
||||
@ -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<String> events,
|
||||
@JsonProperty("cooldownSec") int cooldownSec,
|
||||
boolean enabled,
|
||||
@JsonProperty("createdAt") LocalDateTime createdAt
|
||||
) {}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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<LogEventEntity, Long> {
|
||||
|
||||
Page<LogEventEntity> findByAppKeyAndNameAndUserIdAndCreatedAtBetween(
|
||||
String appKey, String name, String userId,
|
||||
LocalDateTime from, LocalDateTime to,
|
||||
Pageable pageable);
|
||||
|
||||
Page<LogEventEntity> findByAppKeyAndNameAndCreatedAtBetween(
|
||||
String appKey, String name,
|
||||
LocalDateTime from, LocalDateTime to,
|
||||
Pageable pageable);
|
||||
|
||||
Page<LogEventEntity> 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<Object[]> findFunnelData(@Param("appKey") String appKey,
|
||||
@Param("steps") List<String> 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<String> findDistinctEventNames(@Param("appKey") String appKey);
|
||||
|
||||
void deleteByAppKey(String appKey);
|
||||
}
|
||||
@ -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<LogIssueEventEntity, Long> {
|
||||
|
||||
List<LogIssueEventEntity> findTop20ByIssueIdOrderByCreatedAtDesc(Long issueId);
|
||||
|
||||
Page<LogIssueEventEntity> findByAppKeyAndCreatedAtBetween(
|
||||
String appKey, LocalDateTime from, LocalDateTime to, Pageable pageable);
|
||||
|
||||
void deleteByIssueId(Long issueId);
|
||||
|
||||
void deleteByAppKey(String appKey);
|
||||
}
|
||||
@ -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<LogIssueEntity, Long> {
|
||||
|
||||
Optional<LogIssueEntity> 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<LogIssueEntity> findByAppKeyAndTypeAndPlatformAndLastSeenAtBetween(
|
||||
String appKey, String type, String platform,
|
||||
LocalDateTime from, LocalDateTime to,
|
||||
Pageable pageable);
|
||||
|
||||
Page<LogIssueEntity> findByAppKeyAndLastSeenAtBetween(
|
||||
String appKey, LocalDateTime from, LocalDateTime to, Pageable pageable);
|
||||
|
||||
Page<LogIssueEntity> 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<LogIssueEntity> 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<LogIssueEntity> 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<String> findDistinctPlatforms(@Param("appKey") String appKey);
|
||||
|
||||
void deleteByAppKey(String appKey);
|
||||
}
|
||||
@ -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<LogSourcemapEntity, Long> {
|
||||
|
||||
Optional<LogSourcemapEntity> findByAppKeyAndPlatformAndAppVersionAndBundleName(
|
||||
String appKey, String platform, String appVersion, String bundleName);
|
||||
|
||||
void deleteByAppKey(String appKey);
|
||||
}
|
||||
@ -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<LogWebhookEntity, Long> {
|
||||
|
||||
List<LogWebhookEntity> findByAppKeyAndEnabledTrue(String appKey);
|
||||
|
||||
List<LogWebhookEntity> findByAppKey(String appKey);
|
||||
|
||||
void deleteByAppKey(String appKey);
|
||||
}
|
||||
@ -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
|
||||
@ -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;
|
||||
正在加载...
在新工单中引用
屏蔽一个用户