1. 슬랙에 가서 자신만의 워크스페이스를 만든다
2. 메세지를 보내고 싶은 채널을 만든다(optional) - 기본 채널로 써도 됨
난 로그인을 하면 로그인한 사용자의 간단한 정보를 슬랙으로 보내고 싶기때문에 채널 이름을 auth로 설정함
사용자 추가는 건너뛰기를 하였다
3. 채널 이름(# 채널이름)에서 아래를 가리키는 화살표 토글 버튼을 누른다
4. 통합 탭으로 눌러서 앱 -> 앱 추가를 누른다
5. 수신 웹훅(Incoming WebHooks)를 추가한다(보기->슬랙에 추가) [ 웹으로 열린다 ]
6. 전송 보낼 채널을 선택한다
7. 웹후크 URL을 복사한다
아래에 메세지전송, 링크추가, 사용자 지정 아이콘, 채널 재정의, curl 요청 등 어떻게 보내는지에 대해 자세히 적혀있다
8. 알림 올 이름을 설정하고, 아이콘을 변경해준다 (슬랙 봇이 아님. 슬랙 봇은 다른 개념)
이름: 로그인 알림
아이콘은 구글아이콘에서 person을 찾아서 이미지를 썼다
https://fonts.google.com/icons?icon.query=user&icon.style=Outlined&icon.set=Material+Symbols
맘에 들었으면 설정 저장을 눌러주자
9. 스프링의 yaml 파일에 슬랙 링크를 적어준다( 당연하게도 example.yaml을 제외한 *.yaml, *.yml은 깃에 올리지 말자 )
기타 다른 채널들도 웹훅을 등록할 수도 있기 때문에 deep을 2단계로 했다
10. 자바 코드 작성
나같은 경우는 @Value 어노테이션
org.springframework.beans.factory.annotation.Value;
으로 접근하고 있지만, 한줄한줄 채널이 늘어갈 때마다 String 한줄한줄 늘려줄 수 없기때문에
보통 Yaml의 설정을 클래스로 읽어와서 Getter로 접근해서 사용하게 된다
@EnableConfigurationProperties
@ConfigurationProperties
이제 Slack 메세지를 보낼 MessageUtil을 util클래스 아래에 만들자
# MessageUtil.java
알록달록한 코드 색상은 Rainbow Brackets 플러그인의 최신옵션에 Enable rainbow variables를 켜주면 된다
로직이 1개뿐이고, 1개의 책임만을 갖고 있는 메서드이기때문에 굳이 e.stackTrace()로 서버에 부하를 주고 싶지 않았기 때문에
lombok의 로거를 이용해서 에러 예외 익셉션만 나오도록 했다
단일 채널만을 사용하겠다 하면, channel 인자를 없애고, 메세지만 놓고 사용해도 되지만, 여러 채널에 보내고 싶을 경우가 있기때문에 확장성을 위해서 채널 인자도 받도록 했다 - [무조건적인 확장성을 위해 무지성 인자를 늘리는 것은 지양하자]
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.concurrent.ExecutionException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MessageUtil {
/**
* Send a message to Slack Channel
*/
public static void sendSlackMessage(String channel, String message) {
if (StringUtils.isNotBlank(channel)) {
ObjectMapper objectMapper = new ObjectMapper();
HashMap<String, String> messages = new HashMap<>();
messages.put("text", message);
try {
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest =
HttpRequest.newBuilder()
.uri(URI.create(channel))
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(messages)))
.build();
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()).get().body();
} catch (InterruptedException | ExecutionException | JsonProcessingException e) {
log.error("Slack Message Processing Exception");
}
}
}
}
이제 이 메세지유틸을 이용할 유저컨트롤러에 소스코드를 작성해주자
@ApiOperation(value = "로그인", notes = "이메일과 비밀번호로 로그인한다하고 토큰값을 받는다")
@PostMapping(value = "/users/login")
private ResponseEntity<UserResponse> login(@Valid @RequestBody LoginRequest request,
HttpServletRequest httpServletRequest) {
String email = request.getUser().getEmail();
String remoteAddress = httpServletRequest.getHeader("x-forwarded-for") != null ?
httpServletRequest.getHeader("x-forwarded-for").split(",")[0].strip():
httpServletRequest.getRemoteAddr();
String slackMessage = String.format(
"""
#id: %s,
#remoteAddress: %s
""",
email, remoteAddress);
MessageUtil.sendSlackMessage(slackAuthChannel, slackMessage);
return ResponseEntity.ok().body(userService.authenticate(request));
}
나는 로그인할때 email을 받도록 했고, email은 DB에서 Unique Key이다
그래서 DTO 정보로 들어온 이메일과, 요청 IP를 알림으로 오도록 했다
참고로 어떠한 네트워크 구성 없이 API call을 날리면 remoteAddress에 IP정보가 담기지만,
웹 서버(WAS..), 캐싱서버, Proxy(nginx), 스위치(L4/Load Balancer), CloudFront 등이 앞단에 걸려 있다면 실제 IP주소를 못 얻게 된다
X-Forwarded-For(XFF)는 HTTP Header의 일종이며, 단계를 탈 때마다 , 를 joining string으로 추가해서 IP목록에 추가하게 된다
그래서 실제 요청 IP는 XFF의 헤더가 있는지 체크를 하고, 있다면 [,]를 구분자로 0번째의 IP가 최초 요청 IP가 되는 것이다
그리고 """는 Java15에 추가된 text Block이다
궁금하면 자바도 최신버전으로 쓰세요~! 스위치문도 추가되고..ㅎ 많은것이 변했습니다
11. 테스트
성공이다!
추가로 우리가 알고있는 a:b:c:d (ex: 192.0.0.1)의 IP가 아니라 저렇게 찍히는 이유는 IPv6으로 로그가 찍히기 때문이다
그리고 IPv6의 로컬호스트(127.0.0.1)은 저 형태이다
Spring System 설정 변경으로 IPv4로 찍히게 할 수 있지만, 실 운영시에는 굳이 IPv6 -> IPv4로 강제변환을 하면서 로깅을 할 이유가 없는 것 같다(낮은 자리수로 가면서 특정 범위대역은 손실될 수 있기 때문)
그래서 이 포스팅에서는 IPv6으로 그냥 두겠다
자, 이제 우리가 바꾼 아이콘과 내용이 뜨는 것을 확인했다!
추가로 에러가 발생할 시 개발자들이나 데브옵스분들이 보는 대시보드에서 스택트레이스 등으로 볼 수 있지만, 슬랙에도 알림이 가면 더 빠른 처리가 가지 않을까?
우리가 했던 방식대로 error 채널을 빠르게 추가하고 설정해보자
다 했으면 스프링에 LogUtil.java을 추가해주자
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
public class LogUtil {
/**
* StackTrace to String
*/
public static String getStackTraceString(Throwable e) {
StringWriter errors = new StringWriter();
e.printStackTrace(new PrintWriter(errors));
return errors.toString();
}
/**
* get All Headers
*/
public static String getAllHeaders(HttpServletRequest httpServletRequest) {
var result = new StringBuilder();
var keys = httpServletRequest.getHeaderNames();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
var values = httpServletRequest.getHeaders(key);
Set<String> set = new HashSet<>();
while(values.hasMoreElements()) { set.add(values.nextElement()); }
result.append(key).append(": ").append(String.join(", ", set)).append("\n");
}
return result.toString();
}
}
Throwable객체를 String으로 변환하고, 요청헤더를 String으로 반환하는 함수를 만들었다
추가로 @ContollerAdvice 혹은 @RestControllerAdvice에 메서드를 추가하고 특정 익셉션에 적용하자
난 테스트용으로 BadRequestException에 메서드를 적용했다
그리고 stackTrace메세지는 매우 길기 때문에 예외를 캐칭하는 메서드마다 stackTrace flag옵션을 줬다
@Slf4j
@RestControllerAdvice
public class ControllerAdvice {
@Value("${slack.channels.error}")
private String slackErrorChannel;
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> badRequestException(BadRequestException e,
HttpServletRequest httpServletRequest) {
log.error("BadRequestException : {}", e.getMessage());
logDetail(e, httpServletRequest, false);
return ResponseEntity.badRequest().body(ErrorResponse.from(e.getMessage()));
}
public void logDetail(Throwable ex, HttpServletRequest httpServletRequest, boolean onStackTrace) {
String exceptionMessage = onStackTrace ? LogUtil.getStackTraceString(ex) : "";
String logMessage = String.format(
"""
#user: %s,
#remoteAddr: %s,
#requestUrl: %s,
#headers: %s,
#exception: %s""",
httpServletRequest.getRemoteUser(),
httpServletRequest.getHeader("x-forwarded-for") != null ?
httpServletRequest.getHeader("x-forwarded-for").split(",")[0].strip():
httpServletRequest.getRemoteAddr(),
httpServletRequest.getRequestURI(),
LogUtil.getAllHeaders(httpServletRequest),
exceptionMessage
);
log.error(logMessage);
MessageUtil.sendSlackMessage(slackErrorChannel, logMessage);
}
}
그리고 이제 컨트롤러에서 일부러 예외를 만들고 테스트를 해봤다
@ApiOperation(value = "로그인(리졸버)", notes = "현재 로그인된 유저의 정보를 반환한다")
@GetMapping("/user")
private ResponseEntity<UserResponse> me(@LoginUser UserToken userToken) {
if (true) {
throw new BadRequestException("일부러 에러 발생시킴");
}
return ResponseEntity.ok().body(UserResponse.of(userToken));
}
에러 채널에 에러가 찍히는 것을 볼 수 있다
어느정도 Stable한 서비스를 갖고 있다면 에러 로그를 dev채널에 합쳐도 상관이 없겠다
이제는 javascript로 슬랙에 메세지를 보내본다
원래 API key값, URL같은 거는 공개되면 안된다
노드환경이거나 다른 프론트 프레임워크를 쓰고 있다면 .dotenv, .env를 활용해도 된다
나는 그냥... 테스트하는거기 때문에 위에 그냥 명시해줬다
test.html 을 하나 만들었다
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Slack Message Test</title>
<style>
.wrapper {
width: 300px;
display: flex;
flex-direction: column;
}
input[type="text"], button {
height: 30px;
}
</style>
<script>
function sendToSlack(text) {
fetch('//슬랙 채널주소 URL', {
method: "POST",
body: '{"text": "' + text + '"}',
});
}
function onClickSendBtn() {
sendToSlack(document.getElementById('test').value)
document.getElementById('test').value = ""
}
</script>
</head>
<body>
<div class="wrapper">
<h1>슬랙 메세지 테스트</h1>
<input type="text" id="test" placeholder="텍스트를 입력해주세요"> <br/>
<button style="" type="button" onclick="onClickSendBtn()">send</button>
</div>
</body>
</html>
성공!
'Backend > Java - Spring' 카테고리의 다른 글
협업을 위한 DTO Response Custom (0) | 2022.10.23 |
---|---|
jar Build Task(Feat. thin/plain, fat/uber) + no main manifest attribute in ... (0) | 2022.09.04 |
DTO 돌려막기 멈춰!(feat. jackson annotation) (0) | 2022.05.30 |
댓글