Slack integration enables real-time alerts and notifications for your Java applications, making it ideal for monitoring, error reporting, and team collaboration.
Core Concepts and Setup
Why Slack for Alerts?
- Real-time notifications
- Rich message formatting
- File uploads and interactions
- Webhook and API support
- Easy integration with existing workflows
Key Integration Methods:
- Incoming Webhooks - Simple, one-way messaging
- Slack API (chat.postMessage) - Advanced features, interactions
- Socket Mode - Real-time bi-directional communication
- Block Kit - Rich, interactive message layouts
Dependencies and Setup
Maven Dependencies
<properties>
<slack.api.version>1.29.2</slack.api.version>
<spring-boot.version>3.1.0</spring-boot.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Slack SDK -->
<dependency>
<groupId>com.slack.api</groupId>
<artifactId>slack-api-client</artifactId>
<version>${slack.api.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
</dependencies>
Slack App Configuration
- Create a Slack App at api.slack.com/apps
- Enable Incoming Webhooks and add a webhook URL
- Configure OAuth Scopes (for API method):
chat:write- Send messageschat:write.public- Send to public channelsfiles:write- Upload filesreactions:write- Add reactions
Core Implementation
1. Configuration Classes
@Configuration
@ConfigurationProperties(prefix = "slack")
@Data
public class SlackConfig {
private String webhookUrl;
private String botToken;
private String channel;
private String errorChannel;
private String username;
private String iconEmoji;
private boolean enabled = true;
public boolean isConfigured() {
return enabled && (webhookUrl != null || botToken != null);
}
}
@Configuration
@EnableConfigurationProperties(SlackConfig.class)
public class SlackClientConfig {
@Bean
@ConditionalOnProperty(name = "slack.bot-token")
public Slack slackClient() {
return Slack.getInstance();
}
@Bean
public HttpClient httpClient() {
return HttpClients.custom()
.setConnectionManager(new PoolingHttpClientConnectionManager())
.build();
}
}
2. Slack Message Models
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SlackMessage {
private String text;
private String channel;
private String username;
private String iconEmoji;
private String iconUrl;
private boolean mrkdwn = true;
private List<Attachment> attachments;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Attachment {
private String color;
private String pretext;
private String title;
private String titleLink;
private String text;
private String fallback;
private List<Field> fields;
private String footer;
private Long ts; // timestamp
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Field {
private String title;
private String value;
private boolean Short;
}
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AlertRequest {
@NotBlank
private String message;
private String channel;
private AlertLevel level;
private Map<String, String> details;
private String title;
private String traceId;
private String serviceName;
public enum AlertLevel {
INFO, WARNING, ERROR, CRITICAL
}
}
3. Webhook-Based Slack Service (Simple)
@Component
@Slf4j
public class SlackWebhookService {
private final SlackConfig slackConfig;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
private static final String[] ERROR_COLORS = {"#36a64f", "#f2c744", "#ff8000", "#ff0000"};
public SlackWebhookService(SlackConfig slackConfig, HttpClient httpClient, ObjectMapper objectMapper) {
this.slackConfig = slackConfig;
this.httpClient = httpClient;
this.objectMapper = objectMapper;
}
public void sendSimpleMessage(String message) {
if (!slackConfig.isConfigured()) {
log.warn("Slack is not configured, message not sent: {}", message);
return;
}
SlackMessage slackMessage = SlackMessage.builder()
.text(message)
.channel(slackConfig.getChannel())
.username(slackConfig.getUsername())
.iconEmoji(slackConfig.getIconEmoji())
.mrkdwn(true)
.build();
sendMessage(slackMessage);
}
public void sendAlert(AlertRequest alertRequest) {
if (!slackConfig.isConfigured()) {
log.warn("Slack is not configured, alert not sent: {}", alertRequest.getMessage());
return;
}
String channel = alertRequest.getChannel() != null ?
alertRequest.getChannel() : slackConfig.getChannel();
if (alertRequest.getLevel() == AlertLevel.ERROR ||
alertRequest.getLevel() == AlertLevel.CRITICAL) {
channel = slackConfig.getErrorChannel() != null ?
slackConfig.getErrorChannel() : channel;
}
SlackMessage.Attachment attachment = buildAttachment(alertRequest);
SlackMessage slackMessage = SlackMessage.builder()
.text(alertRequest.getTitle() != null ? alertRequest.getTitle() : "Alert")
.channel(channel)
.username(slackConfig.getUsername())
.iconEmoji(getIconForLevel(alertRequest.getLevel()))
.attachments(List.of(attachment))
.mrkdwn(true)
.build();
sendMessage(slackMessage);
}
public void sendErrorAlert(String message, Exception exception, String traceId) {
AlertRequest alertRequest = AlertRequest.builder()
.level(AlertLevel.ERROR)
.message(message)
.traceId(traceId)
.serviceName("user-service")
.details(Map.of(
"exception", exception.getClass().getSimpleName(),
"message", exception.getMessage()
))
.build();
sendAlert(alertRequest);
}
private SlackMessage.Attachment buildAttachment(AlertRequest alertRequest) {
List<SlackMessage.Attachment.Field> fields = new ArrayList<>();
// Add level field
fields.add(SlackMessage.Attachment.Field.builder()
.title("Level")
.value(alertRequest.getLevel().toString())
.Short(true)
.build());
// Add service name if available
if (alertRequest.getServiceName() != null) {
fields.add(SlackMessage.Attachment.Field.builder()
.title("Service")
.value(alertRequest.getServiceName())
.Short(true)
.build());
}
// Add trace ID if available
if (alertRequest.getTraceId() != null) {
fields.add(SlackMessage.Attachment.Field.builder()
.title("Trace ID")
.value(alertRequest.getTraceId())
.Short(false)
.build());
}
// Add custom details
if (alertRequest.getDetails() != null) {
alertRequest.getDetails().forEach((key, value) -> {
fields.add(SlackMessage.Attachment.Field.builder()
.title(key)
.value(value)
.Short(true)
.build());
});
}
return SlackMessage.Attachment.builder()
.color(getColorForLevel(alertRequest.getLevel()))
.title(alertRequest.getTitle() != null ? alertRequest.getTitle() : "Alert Details")
.text(alertRequest.getMessage())
.fields(fields)
.footer("Sent from Java Application")
.ts(System.currentTimeMillis() / 1000)
.build();
}
private void sendMessage(SlackMessage slackMessage) {
try {
String jsonPayload = objectMapper.writeValueAsString(slackMessage);
HttpPost httpPost = new HttpPost(slackConfig.getWebhookUrl());
httpPost.setHeader("Content-Type", "application/json");
httpPost.setEntity(new StringEntity(jsonPayload, StandardCharsets.UTF_8));
try (CloseableHttpResponse response = (CloseableHttpResponse) httpClient.execute(httpPost)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode < 200 || statusCode >= 300) {
log.error("Failed to send Slack message. Status: {}, Response: {}",
statusCode, EntityUtils.toString(response.getEntity()));
} else {
log.debug("Slack message sent successfully");
}
}
} catch (Exception e) {
log.error("Failed to send Slack message", e);
}
}
private String getColorForLevel(AlertRequest.AlertLevel level) {
return switch (level) {
case INFO -> ERROR_COLORS[0]; // Green
case WARNING -> ERROR_COLORS[1]; // Yellow
case ERROR -> ERROR_COLORS[2]; // Orange
case CRITICAL -> ERROR_COLORS[3]; // Red
};
}
private String getIconForLevel(AlertRequest.AlertLevel level) {
return switch (level) {
case INFO -> ":information_source:";
case WARNING -> ":warning:";
case ERROR -> ":x:";
case CRITICAL -> ":rotating_light:";
};
}
}
4. Slack API Service (Advanced)
@Component
@Slf4j
public class SlackApiService {
private final Slack slack;
private final SlackConfig slackConfig;
public SlackApiService(Slack slack, SlackConfig slackConfig) {
this.slack = slack;
this.slackConfig = slackConfig;
}
public String sendMessage(String channel, String text) {
if (!slackConfig.isConfigured()) {
log.warn("Slack API is not configured");
return null;
}
try {
ChatPostMessageResponse response = slack.methods(slackConfig.getBotToken())
.chatPostMessage(req -> req
.channel(channel)
.text(text)
.mrkdwn(true));
if (response.isOk()) {
log.debug("Message sent to Slack: {}", response.getTs());
return response.getTs();
} else {
log.error("Failed to send message to Slack: {}", response.getError());
return null;
}
} catch (IOException | SlackApiException e) {
log.error("Failed to send message via Slack API", e);
return null;
}
}
public String sendBlockMessage(String channel, List<LayoutBlock> blocks) {
try {
ChatPostMessageResponse response = slack.methods(slackConfig.getBotToken())
.chatPostMessage(req -> req
.channel(channel)
.blocks(blocks));
if (response.isOk()) {
return response.getTs();
} else {
log.error("Failed to send block message: {}", response.getError());
return null;
}
} catch (IOException | SlackApiException e) {
log.error("Failed to send block message via Slack API", e);
return null;
}
}
public void updateMessage(String channel, String timestamp, String newText) {
try {
ChatUpdateResponse response = slack.methods(slackConfig.getBotToken())
.chatUpdate(req -> req
.channel(channel)
.ts(timestamp)
.text(newText));
if (!response.isOk()) {
log.error("Failed to update message: {}", response.getError());
}
} catch (IOException | SlackApiException e) {
log.error("Failed to update message via Slack API", e);
}
}
public void addReaction(String channel, String timestamp, String emoji) {
try {
ReactionsAddResponse response = slack.methods(slackConfig.getBotToken())
.reactionsAdd(req -> req
.channel(channel)
.timestamp(timestamp)
.name(emoji));
if (!response.isOk()) {
log.error("Failed to add reaction: {}", response.getError());
}
} catch (IOException | SlackApiException e) {
log.error("Failed to add reaction via Slack API", e);
}
}
public void uploadFile(String channel, String content, String filename, String title) {
try {
FilesUploadResponse response = slack.methods(slackConfig.getBotToken())
.filesUpload(req -> req
.channels(List.of(channel))
.content(content)
.filename(filename)
.title(title));
if (!response.isOk()) {
log.error("Failed to upload file: {}", response.getError());
}
} catch (IOException | SlackApiException e) {
log.error("Failed to upload file via Slack API", e);
}
}
}
5. Block Kit Message Builder
@Component
public class SlackBlockBuilder {
public List<LayoutBlock> buildAlertBlocks(AlertRequest alertRequest) {
List<LayoutBlock> blocks = new ArrayList<>();
// Header section
blocks.add(HeaderBlock.builder()
.text(PlainTextObject.builder()
.text(getHeaderText(alertRequest.getLevel()))
.emoji(true)
.build())
.build());
// Message section
blocks.add(SectionBlock.builder()
.text(MarkdownTextObject.builder()
.text(String.format("*Message:*\n%s", alertRequest.getMessage()))
.build())
.build());
// Fields section
List<TextObject> fields = new ArrayList<>();
fields.add(MarkdownTextObject.builder()
.text(String.format("*Level:*\n%s", alertRequest.getLevel()))
.build());
if (alertRequest.getServiceName() != null) {
fields.add(MarkdownTextObject.builder()
.text(String.format("*Service:*\n%s", alertRequest.getServiceName()))
.build());
}
if (alertRequest.getTraceId() != null) {
fields.add(MarkdownTextObject.builder()
.text(String.format("*Trace ID:*\n`%s`", alertRequest.getTraceId()))
.build());
}
if (!fields.isEmpty()) {
blocks.add(SectionBlock.builder()
.fields(fields)
.build());
}
// Details section
if (alertRequest.getDetails() != null && !alertRequest.getDetails().isEmpty()) {
blocks.add(DividerBlock.builder().build());
blocks.add(SectionBlock.builder()
.text(MarkdownTextObject.builder()
.text("*Additional Details:*")
.build())
.build());
alertRequest.getDetails().forEach((key, value) -> {
blocks.add(SectionBlock.builder()
.text(MarkdownTextObject.builder()
.text(String.format("• *%s:* %s", key, value))
.build())
.build());
});
}
// Context section with timestamp
blocks.add(ContextBlock.builder()
.elements(List.of(
MarkdownTextObject.builder()
.text(String.format(":clock1: Sent at: %s",
LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)))
.build()
))
.build());
// Actions section
if (alertRequest.getTraceId() != null) {
blocks.add(ActionsBlock.builder()
.elements(List.of(
ButtonElement.builder()
.text(PlainTextObject.builder()
.text("View Logs")
.emoji(true)
.build())
.value(alertRequest.getTraceId())
.actionId("view_logs")
.url("https://logs.example.com/trace/" + alertRequest.getTraceId())
.build()
))
.build());
}
return blocks;
}
public List<LayoutBlock> buildDeploymentBlocks(String service, String version,
String environment, String status,
String commitUrl) {
return List.of(
HeaderBlock.builder()
.text(PlainTextObject.builder()
.text(String.format("%s Deployment - %s", service, environment))
.emoji(true)
.build())
.build(),
SectionBlock.builder()
.fields(List.of(
MarkdownTextObject.builder()
.text(String.format("*Service:*\n%s", service))
.build(),
MarkdownTextObject.builder()
.text(String.format("*Version:*\n%s", version))
.build(),
MarkdownTextObject.builder()
.text(String.format("*Environment:*\n%s", environment))
.build(),
MarkdownTextObject.builder()
.text(String.format("*Status:*\n%s", status))
.build()
))
.build(),
SectionBlock.builder()
.text(MarkdownTextObject.builder()
.text(String.format("Commit: <%s|View Changes>", commitUrl))
.build())
.build(),
ContextBlock.builder()
.elements(List.of(
MarkdownTextObject.builder()
.text(String.format("Deployed at: %s",
LocalDateTime.now().format(DateTimeFormatter.RFC_1123_DATE_TIME)))
.build()
))
.build()
);
}
private String getHeaderText(AlertRequest.AlertLevel level) {
return switch (level) {
case INFO -> "ℹ️ Information Alert";
case WARNING -> "⚠️ Warning Alert";
case ERROR -> "❌ Error Alert";
case CRITICAL -> "🚨 Critical Alert";
};
}
}
Spring Boot Integration
1. Alert Service
@Service
@Slf4j
public class AlertService {
private final SlackWebhookService slackWebhookService;
private final SlackApiService slackApiService;
private final SlackBlockBuilder blockBuilder;
private final SlackConfig slackConfig;
public AlertService(SlackWebhookService slackWebhookService, SlackApiService slackApiService,
SlackBlockBuilder blockBuilder, SlackConfig slackConfig) {
this.slackWebhookService = slackWebhookService;
this.slackApiService = slackApiService;
this.blockBuilder = blockBuilder;
this.slackConfig = slackConfig;
}
public void sendSimpleAlert(String message) {
slackWebhookService.sendSimpleMessage(message);
}
public void sendRichAlert(AlertRequest alertRequest) {
// Use webhook for simple alerts
slackWebhookService.sendAlert(alertRequest);
// For critical alerts, also send via API with blocks
if (alertRequest.getLevel() == AlertRequest.AlertLevel.CRITICAL) {
sendCriticalAlertWithBlocks(alertRequest);
}
}
public void sendErrorAlert(String message, Exception exception) {
sendErrorAlert(message, exception, null);
}
public void sendErrorAlert(String message, Exception exception, String traceId) {
slackWebhookService.sendErrorAlert(message, exception, traceId);
}
public void sendDeploymentAlert(String service, String version, String environment,
String status, String commitUrl) {
if (!slackConfig.isConfigured()) {
return;
}
List<LayoutBlock> blocks = blockBuilder.buildDeploymentBlocks(
service, version, environment, status, commitUrl);
String channel = slackConfig.getChannel();
slackApiService.sendBlockMessage(channel, blocks);
}
public void sendMetricAlert(String metricName, double value, double threshold,
String unit, String recommendation) {
AlertRequest alertRequest = AlertRequest.builder()
.level(value > threshold ? AlertRequest.AlertLevel.WARNING : AlertRequest.AlertLevel.INFO)
.title("Metric Alert")
.message(String.format("Metric %s is %s %s %s (threshold: %s %s)",
metricName, value, unit, value > threshold ? ">" : "<=", threshold, unit))
.details(Map.of(
"metric", metricName,
"current_value", String.valueOf(value),
"threshold", String.valueOf(threshold),
"unit", unit,
"recommendation", recommendation
))
.build();
sendRichAlert(alertRequest);
}
private void sendCriticalAlertWithBlocks(AlertRequest alertRequest) {
if (!slackConfig.isConfigured() || slackConfig.getBotToken() == null) {
return;
}
List<LayoutBlock> blocks = blockBuilder.buildAlertBlocks(alertRequest);
String channel = slackConfig.getErrorChannel() != null ?
slackConfig.getErrorChannel() : slackConfig.getChannel();
String messageTs = slackApiService.sendBlockMessage(channel, blocks);
// Add urgent reaction for critical alerts
if (messageTs != null) {
slackApiService.addReaction(channel, messageTs, "rotating_light");
slackApiService.addReaction(channel, messageTs, "sos");
}
}
}
2. REST Controller for Alerts
@RestController
@RequestMapping("/api/alerts")
@Slf4j
public class AlertController {
private final AlertService alertService;
public AlertController(AlertService alertService) {
this.alertService = alertService;
}
@PostMapping("/simple")
public ResponseEntity<Map<String, String>> sendSimpleAlert(@RequestBody Map<String, String> request) {
String message = request.get("message");
if (message == null || message.trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Message is required"));
}
alertService.sendSimpleAlert(message);
return ResponseEntity.ok(Map.of("status", "Alert sent"));
}
@PostMapping("/rich")
public ResponseEntity<Map<String, String>> sendRichAlert(@Valid @RequestBody AlertRequest alertRequest) {
alertService.sendRichAlert(alertRequest);
return ResponseEntity.ok(Map.of("status", "Rich alert sent"));
}
@PostMapping("/error")
public ResponseEntity<Map<String, String>> sendErrorAlert(@RequestBody ErrorAlertRequest request) {
try {
Exception simulatedException = new RuntimeException(request.getErrorMessage());
alertService.sendErrorAlert(request.getMessage(), simulatedException, request.getTraceId());
return ResponseEntity.ok(Map.of("status", "Error alert sent"));
} catch (Exception e) {
log.error("Failed to send error alert", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "Failed to send alert"));
}
}
@PostMapping("/deployment")
public ResponseEntity<Map<String, String>> sendDeploymentAlert(@RequestBody DeploymentRequest request) {
alertService.sendDeploymentAlert(
request.getServiceName(),
request.getVersion(),
request.getEnvironment(),
request.getStatus(),
request.getCommitUrl()
);
return ResponseEntity.ok(Map.of("status", "Deployment alert sent"));
}
@Data
public static class ErrorAlertRequest {
private String message;
private String errorMessage;
private String traceId;
}
@Data
public static class DeploymentRequest {
private String serviceName;
private String version;
private String environment;
private String status;
private String commitUrl;
}
}
3. Global Exception Handler with Slack Alerts
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
private final AlertService alertService;
private final TraceContextHolder traceContextHolder;
public GlobalExceptionHandler(AlertService alertService, TraceContextHolder traceContextHolder) {
this.alertService = alertService;
this.traceContextHolder = traceContextHolder;
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleUncaughtException(Exception ex,
HttpServletRequest request) {
String traceId = traceContextHolder.getCurrentTraceId();
String path = request.getRequestURI();
String method = request.getMethod();
log.error("Uncaught exception [TraceId: {}, Path: {}, Method: {}]",
traceId, path, method, ex);
// Send alert for unexpected errors
alertService.sendErrorAlert(
String.format("Uncaught exception in %s %s", method, path),
ex,
traceId
);
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", Instant.now());
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
body.put("error", "Internal Server Error");
body.put("message", "An unexpected error occurred");
body.put("traceId", traceId);
body.put("path", path);
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Map<String, Object>> handleBusinessException(BusinessException ex,
HttpServletRequest request) {
String traceId = traceContextHolder.getCurrentTraceId();
// Send alert for business errors (but not as critical)
if (ex.isCritical()) {
alertService.sendErrorAlert(
String.format("Business exception: %s", ex.getMessage()),
ex,
traceId
);
}
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", Instant.now());
body.put("status", HttpStatus.BAD_REQUEST.value());
body.put("error", "Business Error");
body.put("message", ex.getMessage());
body.put("traceId", traceId);
body.put("code", ex.getErrorCode());
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}
}
4. Scheduled Health Check Alerts
@Component
@Slf4j
public class HealthCheckMonitor {
private final AlertService alertService;
private final RestTemplate restTemplate;
private final SlackConfig slackConfig;
private final Map<String, ServiceStatus> serviceStatus = new ConcurrentHashMap<>();
public HealthCheckMonitor(AlertService alertService, RestTemplate restTemplate,
SlackConfig slackConfig) {
this.alertService = alertService;
this.restTemplate = restTemplate;
this.slackConfig = slackConfig;
}
@Scheduled(fixedRate = 300000) // 5 minutes
public void checkServicesHealth() {
if (!slackConfig.isEnabled()) {
return;
}
List<String> services = List.of(
"https://api.service1.com/health",
"https://api.service2.com/health",
"https://api.service3.com/health"
);
services.forEach(this::checkService);
}
private void checkService(String healthUrl) {
try {
ResponseEntity<Map> response = restTemplate.getForEntity(healthUrl, Map.class);
if (response.getStatusCode().is2xxSuccessful()) {
updateServiceStatus(healthUrl, "UP", null);
} else {
updateServiceStatus(healthUrl, "DOWN",
"HTTP " + response.getStatusCodeValue());
}
} catch (Exception e) {
updateServiceStatus(healthUrl, "DOWN", e.getMessage());
}
}
private void updateServiceStatus(String serviceUrl, String status, String error) {
String serviceName = extractServiceName(serviceUrl);
ServiceStatus previousStatus = serviceStatus.get(serviceUrl);
serviceStatus.put(serviceUrl, new ServiceStatus(status, error, Instant.now()));
// Send alert on status change to DOWN
if (previousStatus == null || "UP".equals(previousStatus.status)) {
if ("DOWN".equals(status)) {
alertService.sendRichAlert(AlertRequest.builder()
.level(AlertRequest.AlertLevel.ERROR)
.title("Service Down Alert")
.message(String.format("Service %s is down", serviceName))
.details(Map.of(
"service", serviceName,
"url", serviceUrl,
"error", error != null ? error : "Unknown error",
"timestamp", Instant.now().toString()
))
.build());
}
}
// Send recovery alert
if ("DOWN".equals(previousStatus != null ? previousStatus.status : "UP") && "UP".equals(status)) {
alertService.sendRichAlert(AlertRequest.builder()
.level(AlertRequest.AlertLevel.INFO)
.title("Service Recovered")
.message(String.format("Service %s is back online", serviceName))
.details(Map.of(
"service", serviceName,
"url", serviceUrl,
"downtime", formatDuration(previousStatus.timestamp, Instant.now())
))
.build());
}
}
private String extractServiceName(String url) {
try {
URI uri = new URI(url);
String host = uri.getHost();
return host != null ? host.split("\\.")[0] : url;
} catch (Exception e) {
return url;
}
}
private String formatDuration(Instant from, Instant to) {
Duration duration = Duration.between(from, to);
return String.format("%dh %dm %ds",
duration.toHours(),
duration.toMinutesPart(),
duration.toSecondsPart());
}
private record ServiceStatus(String status, String error, Instant timestamp) {}
}
Application Configuration
# application.yml
slack:
enabled: true
webhook-url: ${SLACK_WEBHOOK_URL:}
bot-token: ${SLACK_BOT_TOKEN:}
channel: "#alerts"
error-channel: "#critical-alerts"
username: "Java Alert Bot"
icon-emoji: ":robot_face:"
logging:
level:
com.example.slack: DEBUG
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
Testing
@SpringBootTest
@TestPropertySource(properties = {
"slack.enabled=true",
"slack.channel=#test-alerts"
})
class SlackAlertServiceTest {
@Autowired
private AlertService alertService;
@MockBean
private SlackWebhookService slackWebhookService;
@Test
void shouldSendSimpleAlert() {
// Given
String testMessage = "Test alert message";
// When
alertService.sendSimpleAlert(testMessage);
// Then
verify(slackWebhookService, times(1)).sendSimpleMessage(testMessage);
}
@Test
void shouldSendErrorAlert() {
// Given
String message = "Test error";
Exception exception = new RuntimeException("Test exception");
String traceId = "trace-123";
// When
alertService.sendErrorAlert(message, exception, traceId);
// Then
verify(slackWebhookService, times(1))
.sendErrorAlert(message, exception, traceId);
}
}
Best Practices
- Rate Limiting: Implement rate limiting to avoid hitting Slack API limits
- Error Handling: Always handle Slack API failures gracefully
- Sensitive Data: Never log or send sensitive information to Slack
- Environment-specific: Use different channels for different environments
- Alert Fatigue: Avoid sending too many non-critical alerts
- Monitoring: Monitor your Slack integration itself
@Component
public class SlackRateLimiter {
private final RateLimiter rateLimiter;
public SlackRateLimiter() {
// 20 messages per minute
this.rateLimiter = RateLimiter.create(20.0 / 60.0);
}
public boolean acquirePermission() {
return rateLimiter.tryAcquire();
}
public void waitForPermission() {
rateLimiter.acquire();
}
}
Conclusion
Slack integration for alerts in Java provides:
- Real-time notifications for critical application events
- Rich, interactive messages with Block Kit
- Centralized alert management for distributed teams
- Flexible integration options (webhooks, API, Socket Mode)
- Easy collaboration and quick response to incidents
By implementing the patterns shown above, you can create a robust alerting system that helps your team stay informed about application health, errors, and important events in real-time. The combination of webhooks for simple alerts and the Slack API for advanced features provides both simplicity and power for different use cases.