[Spring] Slack API νμ©νμ¬ μλ¬ λͺ¨λν°λ§νκΈ°
μλΉμ€λ₯Ό λ°°ν¬ν ν, νλ‘μ νΈλ₯Ό μ μ§ κ΄λ¦¬νκΈ° μν΄μ μλ¬ λμμ νμμ μ λλ€. μλ¬λ₯Ό λͺ¨λν°λ§νκΈ° μν μλ¨μ λ€μνμ§λ§, μ΄λ² κΈμμλ Spring Boot μμ μ¬λμ νμ©νμ¬ μλ¬λ₯Ό νμΈν μ μλ λ°©λ²μ μκ°νκ³ μ ν©λλ€.
ν΄λΉ κΈμμ μκ°ν λ°©λ²μ λ°λΌμ€μλ©΄ λ€μκ³Ό κ°μ΄ μ¬λμΌλ‘ μλ¬λ₯Ό νμΈνμ€ μ μμ΅λλ€! π
π― 1. Slack μ°λμ μν ν ν° λ°κΈ
- λ¨Όμ Slack λ΄μ μμ±νκΈ° μν΄ https://api.slack.com/apps/ μ μ μν©λλ€.
- Create New App ν΄λ¦ -> From scratch ν΄λ¦
- App Name μ μμ λ‘κ² μ€μ νκ³ μ¬λ μλ¦Όμ λ°μ μν¬μ€νμ΄μ€λ₯Ό μ€μ ν©λλ€.
- Permissions ν΄λ¦
- Bot Token Scopes μμ Add an Oauth Scope λ₯Ό ν΄λ¦ν΄ λ€μκ³Ό κ°μ΄ 3κ°μ§ κΆνμ λΆμ¬ν©λλ€.
- OAuth Tokens for Your Workspace μμ Install to Workspace λ₯Ό μ ννμ¬ μν¬μ€νμ΄μ€μ λ±λ‘μ΄ μλ£λλ©΄ λ€μκ³Ό κ°μ΄ ν ν°μ΄ λ°κΈλ©λλ€. ν΄λΉ ν ν°μ Spring μ ν리μΌμ΄μ yml μ€μ νμΌμ μ¬μ©λκΈ° λλ¬Έμ 볡μ¬ν΄λ‘λλ€.
π― 2. build.gradle μ Slack API μμ‘΄μ± μΆκ°
dependencies {
// slack
implementation "com.slack.api:slack-api-client:1.25.1"
}
Slack API λ₯Ό νμ©νκΈ° μν΄ μμ κ°μ΄ μμ‘΄μ±μ μΆκ°ν΄μ€λλ€.
π― 3. application.yml μ ν ν°, μ±λλͺ λ±λ‘
slack:
token: {λ°κΈ λ°μ ν ν°κ°}
channel:
monitor: '#server-error-dev'
λ€μκ³Ό κ°μ΄ token
μλ λ°κΈ λ°μ ν ν°κ°μ μ
λ ₯ν΄μ£Όκ³
slack.channel.monitor
μλ ‘#{μ€μ ν μν¬μ€νμ΄μ€μμ μλ¬ μλ¦Όμ λ°μ μ±λλͺ
}’
μ μ
λ ₯ν΄μ€λλ€.
π― 4. SlackService, SlackServiceUtils ꡬν
SlackService
@Slf4j
@Service
public class SlackService {
@Value(value = "${spring.profiles.active}")
String profile;
@Value(value = "${slack.token}")
String token;
@Value(value = "${slack.channel.monitor}")
String channelProductError;
private static final String LOCAL = "local";
private static final String PROD_ERROR_MESSAGE_TITLE = "π€― *500 μλ¬ λ°μ*";
private static final String ATTACHMENTS_ERROR_COLOR = "#eb4034";
public void sendSlackMessageProductError(Exception exception) {
if (!profile.equals(LOCAL)) {
try {
List<LayoutBlock> layoutBlocks = SlackServiceUtils.createProdErrorMessage(exception);
List<Attachment> attachments = SlackServiceUtils.createAttachments(ATTACHMENTS_ERROR_COLOR,
layoutBlocks);
Slack.getInstance().methods(token).chatPostMessage(request ->
request.channel(channelProductError)
.attachments(attachments)
.text(PROD_ERROR_MESSAGE_TITLE));
} catch (SlackApiException | IOException e) {
log.error(e.getMessage(), e);
}
}
}
}
SlackServiceUtils
public class SlackServiceUtils {
private static final String ERROR_MESSAGE = "*Error Message:*\n";
private static final String ERROR_STACK = "*Error Stack:*\n";
private static final String FILTER_STRING = "blossom";
public static List<Attachment> createAttachments(String color, List<LayoutBlock> data) {
List<Attachment> attachments = new ArrayList<>();
Attachment attachment = new Attachment();
attachment.setColor(color);
attachment.setBlocks(data);
attachments.add(attachment);
return attachments;
}
public static List<LayoutBlock> createProdErrorMessage(Exception exception) {
StackTraceElement[] stacks = exception.getStackTrace();
List<LayoutBlock> layoutBlockList = new ArrayList<>();
List<TextObject> sectionInFields = new ArrayList<>();
sectionInFields.add(markdownText(ERROR_MESSAGE + exception.getMessage()));
sectionInFields.add(markdownText(ERROR_STACK + exception));
layoutBlockList.add(section(section -> section.fields(sectionInFields)));
layoutBlockList.add(divider());
layoutBlockList.add(section(section -> section.text(markdownText(filterErrorStack(stacks)))));
return layoutBlockList;
}
private static String filterErrorStack(StackTraceElement[] stacks) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("```");
for (StackTraceElement stack : stacks) {
if (stack.toString().contains(FILTER_STRING)) {
stringBuilder.append(stack).append("\n");
}
}
stringBuilder.append("```");
return stringBuilder.toString();
}
}
μ΄μ μμ κ°μ΄ μ¬λ μλ¦Όμ μ μ‘νκΈ° μν μλΉμ€ λ‘μ§μ ꡬνν΄μ€λλ€.
SlackService μμλ Slack μ μλ¦Ό μ μ‘μ μμ²νλ Request λ₯Ό μμ±ν΄μ 보λ΄λ μν μ λ΄λΉνκ³ ,
SlackServiceUtils μμλ Slack μ νμλ μλ¦Όμ ννλ₯Ό λ§λλ μν μ λ΄λΉν©λλ€.
μλ¦Όμ λ€λ₯Έ ννλ‘ μ»€μ€ν νκΈ° μν΄μλ νλ¨μ 곡μλ¬Έμλ₯Ό μ°Έκ³ ν΄μ μ§νν΄λ³΄μλ©΄ λ©λλ€.
λν μλ¨ μ½λμ κ²½μ° λ°°ν¬ νκ²½μΈ dev, prod νκ²½μμλ§ μ¬λ μλ¦Όμ΄ λμνλλ‘ κ΅¬νλμ΄μμ΅λλ€.
local νκ²½μμλ Slack μλ¦Όμ΄ μμ²λμ§ μκΈ° λλ¬Έμ ν μ€νΈ νμ€ λ ν΄λΉ λΆλΆμ μ£Όμ μ²λ¦¬νκ³ μ§νν΄μ£ΌμΈμ.
Reference: Secondary message attachments
Another way to attach content to messages is the old attachments system. We prefer Block Kit now.
api.slack.com
π― 5. ControllerAdvice μμ SlackService νΈμΆ
ControllerAdvice
@Slf4j
@RequiredArgsConstructor
@RestControllerAdvice
public class ControllerAdvice {
private final SlackService slackService;
/**
* 500 Internal Server Error
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
protected ApiResponse<Object> handleException(final Exception exception) {
log.error(exception.getMessage(), exception);
slackService.sendSlackMessageProductError(exception);
return ApiResponse.error(INTERNAL_SERVER_EXCEPTION);
}
}
μ΄μ μλ²κ° μμνμ§ λͺ»ν μλ¬μ λν΄ Slack μλ¦Όμ μ μ‘νκΈ° μν΄ μμ κ°μ΄ 500λ²λ μλ¬κ° λ°μνλ©΄ SlackService λ₯Ό νΈμΆν©λλ€.
μ€μ μλΉμ€ λ‘μ§μμ ν μ€νΈνκΈ° μν΄ Runtime Exception μ λ°μμμΌλ³΄λ©΄ μμ κ°μ΄ μλ¬ μλ¦Όμ νμΈν μ μμ΅λλ€.