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;
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户