Skip to content

Commit fa2675b

Browse files
author
lmj
committed
Introduce alerting and task log scaffolding
1 parent e120049 commit fa2675b

23 files changed

Lines changed: 1091 additions & 0 deletions
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.dbsyncer.metadata.controller;
2+
3+
import com.dbsyncer.metadata.entity.AlertEvent;
4+
import com.dbsyncer.metadata.repository.AlertEventRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.PathVariable;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
import org.springframework.web.bind.annotation.RequestParam;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
import java.util.List;
15+
import java.util.UUID;
16+
17+
/**
18+
* REST controller for querying alert events (history).
19+
*/
20+
@RestController
21+
@RequestMapping("/api/v1/tasks/{taskId}/alerts")
22+
@RequiredArgsConstructor
23+
@Slf4j
24+
public class AlertEventController {
25+
26+
private final AlertEventRepository alertEventRepository;
27+
28+
@GetMapping
29+
public ResponseEntity<List<AlertEvent>> getTaskAlerts(@PathVariable UUID taskId,
30+
@RequestParam(name = "limit", defaultValue = "100") int limit) {
31+
List<AlertEvent> events = alertEventRepository.findByTaskIdOrderByCreatedAtDesc(taskId);
32+
if (events.size() > limit) {
33+
events = events.subList(0, limit);
34+
}
35+
return ResponseEntity.ok(events);
36+
}
37+
}
38+
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.dbsyncer.metadata.controller;
2+
3+
import com.dbsyncer.metadata.dto.AlertRuleRequest;
4+
import com.dbsyncer.metadata.dto.AlertRuleResponse;
5+
import com.dbsyncer.metadata.entity.AlertRule;
6+
import com.dbsyncer.metadata.repository.AlertRuleRepository;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.bind.annotation.*;
11+
12+
import java.net.URI;
13+
import java.util.List;
14+
import java.util.UUID;
15+
import java.util.stream.Collectors;
16+
17+
/**
18+
* REST controller for managing alert rules.
19+
*/
20+
@RestController
21+
@RequestMapping("/api/v1/alerts/rules")
22+
@RequiredArgsConstructor
23+
@Slf4j
24+
public class AlertRuleController {
25+
26+
private final AlertRuleRepository alertRuleRepository;
27+
28+
@GetMapping
29+
public ResponseEntity<List<AlertRuleResponse>> getRules() {
30+
List<AlertRuleResponse> body = alertRuleRepository.findAll().stream()
31+
.map(AlertRuleResponse::fromEntity)
32+
.collect(Collectors.toList());
33+
return ResponseEntity.ok(body);
34+
}
35+
36+
@PostMapping
37+
public ResponseEntity<AlertRuleResponse> createRule(@RequestBody AlertRuleRequest request) {
38+
AlertRule rule = AlertRule.builder()
39+
.name(request.getName())
40+
.enabled(request.getEnabled() != null ? request.getEnabled() : Boolean.TRUE)
41+
.onTaskFailure(request.getOnTaskFailure() != null ? request.getOnTaskFailure() : Boolean.TRUE)
42+
.minSeverity(request.getMinSeverity() != null ? request.getMinSeverity() : com.dbsyncer.metadata.entity.AlertSeverity.ERROR)
43+
.emailRecipients(request.getEmailRecipients())
44+
.webhookUrl(request.getWebhookUrl())
45+
.description(request.getDescription())
46+
.build();
47+
rule = alertRuleRepository.save(rule);
48+
return ResponseEntity
49+
.created(URI.create("/api/v1/alerts/rules/" + rule.getId()))
50+
.body(AlertRuleResponse.fromEntity(rule));
51+
}
52+
53+
@PutMapping("/{id}")
54+
public ResponseEntity<AlertRuleResponse> updateRule(@PathVariable UUID id,
55+
@RequestBody AlertRuleRequest request) {
56+
AlertRule rule = alertRuleRepository.findById(id)
57+
.orElseThrow(() -> new IllegalArgumentException("Alert rule not found: " + id));
58+
59+
if (request.getName() != null) {
60+
rule.setName(request.getName());
61+
}
62+
if (request.getEnabled() != null) {
63+
rule.setEnabled(request.getEnabled());
64+
}
65+
if (request.getOnTaskFailure() != null) {
66+
rule.setOnTaskFailure(request.getOnTaskFailure());
67+
}
68+
if (request.getMinSeverity() != null) {
69+
rule.setMinSeverity(request.getMinSeverity());
70+
}
71+
if (request.getEmailRecipients() != null) {
72+
rule.setEmailRecipients(request.getEmailRecipients());
73+
}
74+
if (request.getWebhookUrl() != null) {
75+
rule.setWebhookUrl(request.getWebhookUrl());
76+
}
77+
if (request.getDescription() != null) {
78+
rule.setDescription(request.getDescription());
79+
}
80+
81+
rule = alertRuleRepository.save(rule);
82+
return ResponseEntity.ok(AlertRuleResponse.fromEntity(rule));
83+
}
84+
85+
@DeleteMapping("/{id}")
86+
public ResponseEntity<Void> deleteRule(@PathVariable UUID id) {
87+
if (alertRuleRepository.existsById(id)) {
88+
alertRuleRepository.deleteById(id);
89+
}
90+
return ResponseEntity.noContent().build();
91+
}
92+
}
93+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.dbsyncer.metadata.controller;
2+
3+
import com.dbsyncer.metadata.dto.TaskLogResponse;
4+
import com.dbsyncer.metadata.entity.TaskLog;
5+
import com.dbsyncer.metadata.repository.TaskLogRepository;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.GetMapping;
10+
import org.springframework.web.bind.annotation.PathVariable;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RequestParam;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
import java.util.List;
16+
import java.util.UUID;
17+
import java.util.stream.Collectors;
18+
19+
/**
20+
* REST controller for querying task execution logs.
21+
*/
22+
@RestController
23+
@RequestMapping("/api/v1/tasks/{taskId}/logs")
24+
@RequiredArgsConstructor
25+
@Slf4j
26+
public class TaskLogController {
27+
28+
private final TaskLogRepository taskLogRepository;
29+
30+
@GetMapping
31+
public ResponseEntity<List<TaskLogResponse>> getTaskLogs(
32+
@PathVariable UUID taskId,
33+
@RequestParam(name = "level", required = false) String level,
34+
@RequestParam(name = "limit", required = false, defaultValue = "100") int limit) {
35+
log.debug("REST request to get logs for task {} with level={} and limit={}", taskId, level, limit);
36+
37+
List<TaskLog> logs;
38+
if (level != null && !level.isBlank()) {
39+
logs = taskLogRepository.findByTaskIdAndLogLevelOrderByLoggedAtDesc(taskId, level.toUpperCase());
40+
} else {
41+
logs = taskLogRepository.findByTaskIdOrderByLoggedAtDesc(taskId);
42+
}
43+
44+
List<TaskLogResponse> body = logs.stream()
45+
.limit(limit)
46+
.map(TaskLogResponse::fromEntity)
47+
.collect(Collectors.toList());
48+
49+
return ResponseEntity.ok(body);
50+
}
51+
}
52+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.dbsyncer.metadata.dto;
2+
3+
import com.dbsyncer.metadata.entity.AlertSeverity;
4+
import lombok.Data;
5+
6+
/**
7+
* DTO for creating/updating alert rules.
8+
*/
9+
@Data
10+
public class AlertRuleRequest {
11+
12+
private String name;
13+
private Boolean enabled;
14+
private Boolean onTaskFailure;
15+
private AlertSeverity minSeverity;
16+
private String emailRecipients;
17+
private String webhookUrl;
18+
private String description;
19+
}
20+
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.dbsyncer.metadata.dto;
2+
3+
import com.dbsyncer.metadata.entity.AlertRule;
4+
import com.dbsyncer.metadata.entity.AlertSeverity;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Data;
8+
import lombok.NoArgsConstructor;
9+
10+
import java.time.OffsetDateTime;
11+
import java.util.UUID;
12+
13+
@Data
14+
@Builder
15+
@NoArgsConstructor
16+
@AllArgsConstructor
17+
public class AlertRuleResponse {
18+
19+
private UUID id;
20+
private String name;
21+
private Boolean enabled;
22+
private Boolean onTaskFailure;
23+
private AlertSeverity minSeverity;
24+
private String emailRecipients;
25+
private String webhookUrl;
26+
private String description;
27+
private OffsetDateTime createdAt;
28+
private OffsetDateTime updatedAt;
29+
30+
public static AlertRuleResponse fromEntity(AlertRule rule) {
31+
return AlertRuleResponse.builder()
32+
.id(rule.getId())
33+
.name(rule.getName())
34+
.enabled(rule.getEnabled())
35+
.onTaskFailure(rule.getOnTaskFailure())
36+
.minSeverity(rule.getMinSeverity())
37+
.emailRecipients(rule.getEmailRecipients())
38+
.webhookUrl(rule.getWebhookUrl())
39+
.description(rule.getDescription())
40+
.createdAt(rule.getCreatedAt())
41+
.updatedAt(rule.getUpdatedAt())
42+
.build();
43+
}
44+
}
45+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.dbsyncer.metadata.dto;
2+
3+
import com.dbsyncer.metadata.entity.TaskLog;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.time.OffsetDateTime;
10+
import java.util.Map;
11+
12+
/**
13+
* DTO for task execution log entries.
14+
*/
15+
@Data
16+
@Builder
17+
@NoArgsConstructor
18+
@AllArgsConstructor
19+
public class TaskLogResponse {
20+
21+
private Long id;
22+
private String logLevel;
23+
private String message;
24+
private Map<String, Object> context;
25+
private String sourceComponent;
26+
private OffsetDateTime loggedAt;
27+
28+
public static TaskLogResponse fromEntity(TaskLog log) {
29+
return TaskLogResponse.builder()
30+
.id(log.getId())
31+
.logLevel(log.getLogLevel())
32+
.message(log.getMessage())
33+
.context(log.getContext())
34+
.sourceComponent(log.getSourceComponent())
35+
.loggedAt(log.getLoggedAt())
36+
.build();
37+
}
38+
}
39+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.dbsyncer.metadata.entity;
2+
3+
/**
4+
* Channel for delivering alerts.
5+
*/
6+
public enum AlertChannel {
7+
EMAIL,
8+
WEBHOOK
9+
}
10+
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.dbsyncer.metadata.entity;
2+
3+
import io.hypersistence.utils.hibernate.type.json.JsonBinaryType;
4+
import jakarta.persistence.*;
5+
import lombok.*;
6+
import org.hibernate.annotations.Type;
7+
8+
import java.time.OffsetDateTime;
9+
import java.util.HashMap;
10+
import java.util.Map;
11+
import java.util.UUID;
12+
13+
/**
14+
* Entity representing an alert event (history).
15+
*/
16+
@Entity
17+
@Table(name = "alert_events")
18+
@Getter
19+
@Setter
20+
@NoArgsConstructor
21+
@AllArgsConstructor
22+
@Builder
23+
public class AlertEvent {
24+
25+
@Id
26+
@GeneratedValue(strategy = GenerationType.UUID)
27+
private UUID id;
28+
29+
@ManyToOne(fetch = FetchType.LAZY)
30+
@JoinColumn(name = "task_id")
31+
private MigrationTask task;
32+
33+
@ManyToOne(fetch = FetchType.LAZY)
34+
@JoinColumn(name = "rule_id")
35+
private AlertRule rule;
36+
37+
@Enumerated(EnumType.STRING)
38+
@Column(name = "severity", nullable = false, columnDefinition = "alert_severity")
39+
private AlertSeverity severity;
40+
41+
@Enumerated(EnumType.STRING)
42+
@Column(name = "channel", nullable = false, columnDefinition = "alert_channel")
43+
private AlertChannel channel;
44+
45+
@Column(name = "message", nullable = false, columnDefinition = "TEXT")
46+
private String message;
47+
48+
@Type(JsonBinaryType.class)
49+
@Column(name = "payload", columnDefinition = "jsonb")
50+
@Builder.Default
51+
private Map<String, Object> payload = new HashMap<>();
52+
53+
@Column(name = "status", nullable = false, length = 20)
54+
private String status;
55+
56+
@Column(name = "error_message")
57+
private String errorMessage;
58+
59+
@Column(name = "created_at", nullable = false)
60+
private OffsetDateTime createdAt;
61+
62+
@Column(name = "sent_at")
63+
private OffsetDateTime sentAt;
64+
65+
@PrePersist
66+
protected void onCreate() {
67+
createdAt = OffsetDateTime.now();
68+
}
69+
}
70+

0 commit comments

Comments
 (0)