feat(notification-service): add Dockerfile, actuator health and alert deduplication
Add multi-stage Dockerfile with eclipse-temurin:17 and HEALTHCHECK on /actuator/health. Expose /actuator/health endpoint via spring-boot-starter-actuator. Deduplicate alerts: only notify when a room's CO2 level changes, reset on recovery. Assisted-by: Claude:claude-sonnet-4-6
This commit is contained in:
20
notification_service/Dockerfile
Normal file
20
notification_service/Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# ── Stage 1 : build ──────────────────────────────────────────────────────────
|
||||||
|
FROM eclipse-temurin:17-jdk-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pom.xml .
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
RUN apk add --no-cache maven && mvn package -DskipTests -q
|
||||||
|
|
||||||
|
# ── Stage 2 : run ─────────────────────────────────────────────────────────────
|
||||||
|
FROM eclipse-temurin:17-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/target/notification-service-*.jar app.jar
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -37,6 +37,10 @@
|
|||||||
<artifactId>spring-dotenv</artifactId>
|
<artifactId>spring-dotenv</artifactId>
|
||||||
<version>4.0.0</version>
|
<version>4.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class AirQualityScheduler {
|
public class AirQualityScheduler {
|
||||||
@@ -28,6 +30,9 @@ public class AirQualityScheduler {
|
|||||||
private final TelegramNotificationService telegramService;
|
private final TelegramNotificationService telegramService;
|
||||||
private final NotificationProperties props;
|
private final NotificationProperties props;
|
||||||
|
|
||||||
|
// last alerted level per room — null means no alert is active for that room
|
||||||
|
private final Map<String, Co2Level> lastAlertedLevel = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public AirQualityScheduler(AirQualityService airQualityService,
|
public AirQualityScheduler(AirQualityService airQualityService,
|
||||||
TelegramNotificationService telegramService,
|
TelegramNotificationService telegramService,
|
||||||
NotificationProperties props) {
|
NotificationProperties props) {
|
||||||
@@ -36,8 +41,8 @@ public class AirQualityScheduler {
|
|||||||
this.props = props;
|
this.props = props;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(fixedDelayString = "${notification.air-quality.poll-interval-ms:60000}",
|
@Scheduled(fixedDelayString = "${notification.air-quality.poll-interval-ms:60000}",
|
||||||
initialDelayString = "5000")
|
initialDelayString = "5000")
|
||||||
public void checkAirQuality() {
|
public void checkAirQuality() {
|
||||||
boolean mockMode = props.getAirQuality().isMockMode();
|
boolean mockMode = props.getAirQuality().isMockMode();
|
||||||
log.debug("Polling air quality data… (mock={})", mockMode);
|
log.debug("Polling air quality data… (mock={})", mockMode);
|
||||||
@@ -53,9 +58,19 @@ public class AirQualityScheduler {
|
|||||||
|
|
||||||
for (SensorReading reading : readings) {
|
for (SensorReading reading : readings) {
|
||||||
Co2Level level = airQualityService.resolveLevel(reading.co2());
|
Co2Level level = airQualityService.resolveLevel(reading.co2());
|
||||||
|
|
||||||
if (airQualityService.isAlertable(level)) {
|
if (airQualityService.isAlertable(level)) {
|
||||||
log.info("Alertable reading: room={} co2={} level={}", reading.roomId(), reading.co2(), level);
|
Co2Level previous = lastAlertedLevel.get(reading.roomId());
|
||||||
telegramService.sendAlert(reading, level);
|
if (level != previous) {
|
||||||
|
log.info("Alert level changed: room={} {} -> {}", reading.roomId(), previous, level);
|
||||||
|
telegramService.sendAlert(reading, level);
|
||||||
|
lastAlertedLevel.put(reading.roomId(), level);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// room recovered — reset so next alert triggers a new notification
|
||||||
|
if (lastAlertedLevel.remove(reading.roomId()) != null) {
|
||||||
|
log.info("Room {} recovered to {}", reading.roomId(), level);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ spring:
|
|||||||
application:
|
application:
|
||||||
name: notification-service
|
name: notification-service
|
||||||
|
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: health
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: never
|
||||||
|
|
||||||
notification:
|
notification:
|
||||||
telegram:
|
telegram:
|
||||||
bot-token: ${TELEGRAM_BOT_TOKEN}
|
bot-token: ${TELEGRAM_BOT_TOKEN}
|
||||||
|
|||||||
Reference in New Issue
Block a user