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:
khalil-bot
2026-05-28 15:35:52 +02:00
parent 40fde77833
commit 1621465e95
4 changed files with 52 additions and 4 deletions

View 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"]

View File

@@ -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>

View File

@@ -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);
}
} }
} }
} }

View File

@@ -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}