목표

 

  스카우터 슬랙 플러그인을 커스터마이징하여 일부 요청에 대한 알림 여부를 다르게 하고, 요청 설정 목록을 서버 중단 없이 갱신하는 기능을 추가한다.

 

* 스카우터에 대한 자세한 설명은 블로그 내 다른 포스트인 https://team-platform.tistory.com/14 에서 다룬다

우리는

 

  개발되어 서비스 중인 자바 프로세스 들을 스카우터를 통해 관리하고 있다. 또한 프로세스들의 장애 알림 처리는 스카우터의 빌트인 플러그인인 슬랙(scouter-plugin-server-slack) 플러그인을 통해 수행하고있다. 이를 통해 스카우터 Xlog에서 에러로 감지된 요청이나 응답이 오래 걸린 요청들은 슬랙을 통해 알림을 받고 있다.

 

근데 그래서 왜?

 

  우리가 운영하는 여러 서비스들의 다양한 요청들 중, 일부의 경우 에러 알림을 굳이 받지 않아야 하는 경우가 있었다. 또한
느린 응답을 가진 요청의 경우에도 슬랙 플러그인 설정 값인 ext_plugin_elapsed_time_threshold 값으로 글로벌하게만 관리되기 때문에, 자체 서비스 요청 응답이 해당 설정 값을 초과할 수밖에 없는 복잡한 로직을 가지거나 수 ms 이내로 빠르게 응답해야만 하는 서비스들의 경우 장애 여부를 판단하기가 곤란했다.

 

예시

Endpoint 로직 수행 시간  글로벌 설정 지연 한계값 슬랙 장애 알림 여부
GET /users:bulk 30,000 ms 5,000 ms O
GET /users/{userID} 10 ms 5,000 ms X

 위 예시에 따르면 첫번째 요청(/users:bulk)의 경우 기본 로직  수행 시간이 길어, 요청할 때마다 슬랙은 느린 응답 장애를 발송할 것이다. 또한 두 번째 요청(/users/{userID})의 경우 1초만 걸려도 장애로 판단되어야 할 경우 글로벌 설정 5초에 의해 느린 응답 알림을 받을 수 없다.


그래서 플러그인을 수정하여 요청별로 각각 지연, 또는 장애에 대한 알림이 발생되더라도 발송 자체를 무시하거나 다른 지연 설정값을 적용받을 수 있도록 하는 시스템 개선이 필요했다.

 

* 슬랙 플러그인 적용에 대한 자세한 사항은 github.com/scouter-contrib/scouter-plugin-server-alert-slack/ 에서 확인하자

 

수정한 부분

 

해당문제를 해결하기 위해선 SlackPlugin.java 파일의 @ServerPlugin(PluginConstants.PLUGIN_SERVER_XLOG) 어노테이션이 걸린 메서드를 수정해야 한다.

 

원본 SlackPlugin.java

@ServerPlugin(PluginConstants.PLUGIN_SERVER_XLOG)
    public void xlog(XLogPack pack) {

        String objType = AgentManager.getAgent(pack.objHash).objType;

        if (groupConf.getBoolean("ext_plugin_slack_xlog_enabled", objType, true)) {
            if (pack.error != 0) {
                String date = DateUtil.yyyymmdd(pack.endTime);
                String service = TextRD.getString(date, TextTypes.SERVICE, pack.service);
                AlertPack ap = new AlertPack();
                ap.level = AlertLevel.ERROR;
                ap.objHash = pack.objHash;
                ap.title = "xlog Error";
                ap.message = service + " - " + TextRD.getString(date, TextTypes.ERROR, pack.error);
                ap.time = System.currentTimeMillis();
                ap.objType = objType;
                alert(ap);
            }

            try {
                int elapsedThreshold = groupConf.getInt("ext_plugin_elapsed_time_threshold", objType, 0);

                if (elapsedThreshold != 0 && pack.elapsed > elapsedThreshold) {
                    String serviceName = TextRD.getString(DateUtil.yyyymmdd(pack.endTime), TextTypes.SERVICE,
                            pack.service);

                    AlertPack ap = new AlertPack();

                    ap.level = AlertLevel.WARN;
                    ap.objHash = pack.objHash;
                    ap.title = "Elapsed time exceed a threshold.";
                    ap.message = "[" + AgentManager.getAgentName(pack.objHash) + "] " + pack.service + "(" + serviceName
                            + ") " + "elapsed time(" + pack.elapsed + " ms) exceed a threshold.";
                    ap.time = System.currentTimeMillis();
                    ap.objType = objType;

                    alert(ap);
                }

            } catch (Exception e) {
                Logger.printStackTrace(e);
            }
        }
    }

 

해당 메서드는 Xlog의 요청이 기록 됐을 때 진입되는 부분인데 이곳을 보면 에러에 대한 알림을 보내는 영역, 느린 응답에 대한 알림을 보내는 영역이 있는 걸 알 수 있다. 간단하게 슬랙 알림을 무시할 필요가 있는 응답을 추가해보자.

 

수정 SlackPlugin.java

@ServerPlugin(PluginConstants.PLUGIN_SERVER_XLOG)
    public void xlog(XLogPack pack) {

        String objType = AgentManager.getAgent(pack.objHash).objType;

        if (groupConf.getBoolean("ext_plugin_slack_xlog_enabled", objType, true)) {
            String serviceName = TextRD.getString(DateUtil.yyyymmdd(pack.endTime), TextTypes.SERVICE, pack.service);
            String agentName = AgentManager.getAgentName(pack.objHash);
            if (pack.error != 0) {
                if ("localhost".equals(agentName)) {
                    // 특정 Agent는 에러 무시
                } else if ("/test".equals(serviceName)) {
                    // /test 요청은 에러 무시
                } else {
                    // 그 외에는 에러 발송
                    String date = DateUtil.yyyymmdd(pack.endTime);
                    String service = TextRD.getString(date, TextTypes.SERVICE, pack.service);
                    AlertPack ap = new AlertPack();
                    ap.level = AlertLevel.ERROR;
                    ap.objHash = pack.objHash;
                    ap.title = "xlog Error";
                    ap.message = service + " - " + TextRD.getString(date, TextTypes.ERROR, pack.error);
                    ap.time = System.currentTimeMillis();
                    ap.objType = objType;
                    alert(ap);
                }
            }

            try {
                int elapsedThreshold = groupConf.getInt("ext_plugin_elapsed_time_threshold", objType, 0);
				boolean slowError = true;
                if (elapsedThreshold != 0 && pack.elapsed < 1000
                        && "/users".equals(serviceName)) {
                        // 1초만 지나도 에러인 응답 체크
                	slowError = false;
                } else if (elapsedThreshold != 0 && pack.elapsed > elapsedThreshold) {
                    if (pack.elapsed < 60000 && "/users:bulk".equals(serviceName)
                    	&& "/gateway-service".equals(agentName)) {
                        // /users:bulk 요청은 60초가 넘어야 에러 발송
                        slowError = false;
                    }
                    ... more
                }
                
                if( slowError ) {
                  AlertPack ap = new AlertPack();

                  ap.level = AlertLevel.WARN;
                  ap.objHash = pack.objHash;
                  ap.title = "Elapsed time exceed a threshold.";
                  ap.message = "[" + agentName + "] " + pack.service + "(" + serviceName + ") " + "elapsed time("
                                  + pack.elapsed + " ms) exceed a threshold.";
                  ap.time = System.currentTimeMillis();
                  ap.objType = objType;

                  alert(ap);
                }

            } catch (Exception e) {
                Logger.printStackTrace(e);
            }
        }
    }

위 처럼 수정 할 경우 /test 요청은 에러가 발생해도 무시되고, /users 는 1초만 지연되더라도, /users:bulk는 60초가 넘어야만 느린 응답 알림이 발생될 것이다.

 

 

 

와 됐다!..

 

됐나..?

 

진짜 문제는..

 

Slack-Plugin은 빌트인 플러그인인 만큼 수정할 경우 스카우터 서버를 재기동해야 하는 단점이 있었다. 또한 하드코딩으로 관리되는 장애 알림 무시 대상 목록의 경우 처음에는 몇 개 되지 않아 관리에 불편함이 없었지만, 지속적으로 개발됨에 따라 점점 개수가 늘어나고 수정 또한 빈번하게 발생되어 관리를 할 필요성이 생겨 다음과 같은 관점에서 변경을 고민해야 했다.

  • 알림 무시 대상의 목록을 수정하면 서버를 재기동하지 않더라도 바로 적용이 되어야 한다

  • 기존 하드코딩으로 구현되었던 조건문을 수용할 수 있도록 간단한 로직이 허용되어야 한다.

  • 알림 무시 대상 목록을 수정할 때 서버 소스의 변경은 최소화해야 한다.

바꾸자

 

 데이터베이스로 알림 무시 대상 목록을 따로 관리하는 방법도 있겠지만, 따로 데이터베이스를 구성해야 하고 jdbc도 직접 구현하거나 다른 라이브러리를 첨부해야 하기에 조금 더 쉬운 방법으로 접근했다. 

먼저 슬랙 플러그인 소스 중 SlackPlugin.java에서 스카우터 관련 설정 파일을 읽어 들이는 방식과 같은 방식으로 파일 기반으로 알림 무시 대상 목록을 관리하고자 했다. scouter 패키지에 속해있는 Configure.java 파일을 보면 3초마다 지정된 경로에 있는 설정 파일을 주기적으로 체크하면서 수정사항이 있을 경우 서버 메모리 Properties 객체에 패치하는 형태로 구성되어있다.

 

이와 같은 파일 갱신 프로세스를 알림 무시 대상 목록에도 적용하기로 했다.

 

SkipConditionalConfigure.java

package scouter.plugin.server.alert.slack.skipconfig;

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import scouter.util.FileUtil;
import scouter.util.StringUtil;
import scouter.util.ThreadUtil;

public class SkipConditionalConfigure extends Thread {

    private static SkipConditionalConfigure instance = null;
    public final static String CONDITIONAL_FILE_LOC = "./conditional/";
    private List<SkipConditional> errorSkipConditional = new ArrayList<SkipConditional>();
    private List<SkipConditional> slowSkipConditional = new ArrayList<SkipConditional>();

    public List<SkipConditional> getErrorSkipConditional() {
        return errorSkipConditional;
    }

    public List<SkipConditional> getSlowSkipConditional() {
        return slowSkipConditional;
    }

    public final static synchronized SkipConditionalConfigure getInstance() {
        if (instance == null) {
            instance = new SkipConditionalConfigure();
            instance.setDaemon(true);
            instance.setName(ThreadUtil.getName(instance));
            instance.start();
        }
        return instance;
    }

    private SkipConditionalConfigure() {
        reload(false);
    }

    private boolean isModify = false;

    public boolean isModified() {
        return this.isModify;
    }

    private long last_load_time = -1;
    public Properties property = new Properties();

    private boolean running = true;

    public void run() {
        while (running) {
            reload(false);
            ThreadUtil.sleep(3000);
        }
    }

    private File conditionalFile;

    public File getConditionalFile() throws Exception {
        if (conditionalFile != null) {
            return conditionalFile;
        }
        String s = System.getProperty("scouter.conditional", CONDITIONAL_FILE_LOC + "conditional.conf");
        conditionalFile = new File(s.trim());
        return conditionalFile;
    }

    long last_check = 0;

    public synchronized boolean reload(boolean force) {
        long now = System.currentTimeMillis();
        if (force == false && now < last_check + 3000) {
            return false;
        }
        last_check = now;

        File file = null;
        try {
            file = getConditionalFile();
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        isModify = !(file.lastModified() == last_load_time);
        if (!isModify) {
            return false;
        } else {
            System.out.println("File Modified");
        }

        last_load_time = file.lastModified();

        Properties temp = new Properties();
        if (file.canRead()) {
            FileInputStream in = null;
            try {
                in = new FileInputStream(file);
                temp.load(in);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                FileUtil.close(in);
            }
        }
        property = replaceSysProp(temp);

        setErrorSkipConditionals();
        setSlowSkipConditionals();
        return true;
    }

    public Properties replaceSysProp(Properties temp) {
        Properties p = new Properties();

        Map<Object, Object> args = new HashMap<Object, Object>();
        args.putAll(System.getenv());
        args.putAll(System.getProperties());

        p.putAll(args);

        Iterator<Object> itr = temp.keySet().iterator();
        while (itr.hasNext()) {
            String key = (String) itr.next();
            String value = (String) temp.get(key);
            p.put(key, new scouter.util.ParamText(StringUtil.trim(value)).getText(args));
        }

        return p;
    }

    private void setErrorSkipConditionals() {
        errorSkipConditional.clear();
        int lineNo = 1;
        while (true) {
            try {
                String originStr = getValue("error_alert_skip_conditional_" + lineNo);
                if (originStr == null || "".equals(originStr)) {
                    break;
                }
                SkipConditional conditional = parseSkipConditional(originStr);
                if (conditional == null) {
                    System.out.println("fail skip conditional parse.");
                    break;
                }
                errorSkipConditional.add(conditional);
                lineNo++;
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }
    private SkipConditional parseSkipConditional(String originStr) {
        String[] p1 = originStr.split("\\::");
        if (p1.length == 0) {
            System.out.println("[p1] split length error");
            return null;
        }
        SkipConditional conditional = new SkipConditional();
        String p2;
        if (p1.length > 1) {
            conditional.setElapsedTime(Long.parseLong(p1[0]));
            p2 = p1[1];
        } else {
            conditional.setElapsedTime(0L);
            p2 = p1[0];
        }
        String[] p3 = { p2 };
        if (p2.contains("&&")) {
            p3 = p2.split("\\&\\&");
        } 

        if (p3 == null || p3.length == 0) {
            System.out.println("[p3] split length error");
            return null;
        }
        String[] p4;
        String value = null;
        TargetType targetType = null;
        CompareType compareType = null;

        List<ValueConditional> valueConditionals = new ArrayList<ValueConditional>();
        for (int i = 0; i < p3.length; i++) {
            String vc = StringUtil.trim(p3[i]);
            p4 = vc.split("\\,");
            value = p4[0];
            targetType = TargetType.valueOf(p4[1]);
            compareType = CompareType.valueOf(p4[2]);
            valueConditionals.add(new ValueConditional(value, targetType, compareType));
        }
        conditional.setValueConditionals(valueConditionals);

        return conditional;
    }

    private void setSlowSkipConditionals() {
        slowSkipConditional.clear();
        int no = 1;
        while (true) {
            try {
                String originStr = getValue("slow_alert_skip_conditional_" + no);
                if (originStr == null || "".equals(originStr)) {
                    break;
                }
                SkipConditional conditional = parseSkipConditional(originStr);
                if (conditional == null) {
                    System.out.println("fail skip conditional parse.");
                    break;
                }
                slowSkipConditional.add(conditional);
                no++;
            } catch (Exception e) {
                e.printStackTrace();
                break;
            }
        }
    }

    public List<String> getValueList(String key) {
        String str = StringUtil.trim(property.getProperty(key));
        if (str == null) {
            return Collections.emptyList();
        } else {
            return Arrays.asList(str.split("\\,"));
        }
    }

    public String getValue(String key) {
        return StringUtil.trim(property.getProperty(key));
    }

    public String getValue(String key, String def) {
        return StringUtil.trim(property.getProperty(key, def));
    }
}

 구성은 Configure.class파일과 거의 유사하고, 파일을 갱신하는 핵심 부분의 로직만 옮겨왔다. 추가로 두 개의 알림을 관리하는 목록 클래스로서 errorSkipConditional, slowSkipConditional를 생성, 갱신하는 로직이 추가되어있다.

굳이 로직의 동작을 설명하자면 매 3초마다 지정된 경로(/conditional)의 지정된 파일(conditional.conf)을 읽어 들여, 해당 파일의 수정 시간과 저장된 수정 시간을 비교하여 변경됐을 경우 기존 목록 클래스를 초기화하고 다시 읽어 들인 파일로부터 파싱 과정을 거쳐 목록 클래스를 갱신하는 형태로 구성되어있다.

 

 읽어 들이는 설정 파일의 경우 기존 하드코딩된 로직을 수용할 수 있도록 조건들을 포함해서 구성했다.

UrlPattern 방식을 사용하거나, 일치(equals) 체크 -> 포함(contains) 체크 순으로 검증하는 방식도 생각해 봤지만 기존 하드코딩되어있던 조건의 경우 일치하거나 포함하는 조건으로만 되어있었기 때문에 우선적으로 기존 코드와 맞게 일치, 포함 여부를 명세하는 조건 방식으로 구성하였다.

 

/conditional/conditional.conf

# 에러 알림 무시 목록
# {value},{AGENT|SERVICE},{MATCH|CONTAINS} {&& (optionals)} {other conditional (optionals)}
error_alert_skip_conditional_1=/test,AGENT,MATCH
error_alert_skip_conditional_2=/actuator,AGENT,MATCH
error_alert_skip_conditional_3=/error,AGENT,MATCH

# 느린 응답 알림 무시 목록
# {skipElapsedTime}::{value},{AGENT|SERVICE},{MATCH|CONTAINS} {&&(optionals)} {other conditional (optionals)}
slow_alert_skip_conditional_1=100::/board/1,SERVICE,CONTAINS && /gateway-service,AGENT,MATCH
slow_alert_skip_conditional_2=10000::/users:all,SERVICE,MATCH
slow_alert_skip_conditional_3=30000::/users:bulk,SERVICE,MATCH

  설정 파일의 경우 일반적인 오류와, 느린 응답에 대한 값들로 구성되어있다. 기본적으로 Xlog에 기록되는 객체 값에서 Service, Agent 텍스트 값을 비교하여 알림 여부를 결정하게 된다. 해당 구성 파일로 가능한 것은 특정 지연 시간 설정 이내의, SERVICE 또는 AGENT의 값이 일치(MATCH)하거나, 값을 포함(CONTAINS)할 경우 알림을 무시한다. 고 이해하면 된다.

  기존 Configure.class를 그대로 모방하다 보니, Properties의 구성 형태처럼 Key / Value 형태로 관리해야만 해서 설정 파일의 목록 구성이 순차적으로 이루어져야 하는 아쉬운 점이 있었다. 또한 PathVariable 형태의 값의 경우 RegexPattern기반으로 검사해야 하는 불편함에 제외하고 값이 포함되는 것으로 체크하도록 하였다. 포함으로 체크할 경우 다른 문제가 있는데, 이는 검사 순서를 변경하거나 Pattern 기반으로 변경하긴 해야 한다.

 

설정된 파일이 주기적으로 SkipConditional라고 만들어놓은 객체의 리스트로 갱신되고, 기존 If, else로 처리했던 SlackPlugin.java 에서 다음과 같이 비교하도록 처리된다.

 

SkipConditional.java, ValueConditaional.java, Enum

public class SkipConditional {
    private List<ValueConditional> valueConditionals;
    private long elapsedTime;

    public List<ValueConditional> getValueConditionals() {
        return valueConditionals;
    }

    public void setValueConditionals(List<ValueConditional> valueConditionals) {
        this.valueConditionals = valueConditionals;
    }

    public long getElapsedTime() {
        return elapsedTime;
    }

    public void setElapsedTime(long elapsedTime) {
        this.elapsedTime = elapsedTime;
    }

}

public class ValueConditional {
    private String value;
    private TargetType targetType;
    private CompareType compareType;

    public ValueConditional(String value, TargetType targetType, CompareType compareType) {
        this.value = value;
        this.targetType = targetType;
        this.compareType = compareType;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public TargetType getTargetType() {
        return targetType;
    }

    public void setTargetType(TargetType targetType) {
        this.targetType = targetType;
    }

    public CompareType getCompareType() {
        return compareType;
    }

    public void setCompareType(CompareType compareType) {
        this.compareType = compareType;
    }

}

public enum TargetType {
    SERVICE, AGENT
}

public enum CompareType {
    MATCH, CONTAINS
}
한 개의 SkipConditional.javaconditaional.conf 파일의 한 row와 일치하도록 구성되어있다.

 

SlackPlugin.java의 검사 로직 일부

  // 최상단에Configure와 같은 방식으로 초기화 해준다.
    final SkipConditionalConfigure skipConf = SkipConditionalConfigure.getInstance();
    
    // 알림 무시 검사 로직
    private boolean skipCheck(String agentName, String serviceName, int elapsed) {
        for (SkipConditional con : skipConf.getSlowSkipConditional()) {
            boolean condition = valueConditionalCheck(con, agentName, serviceName);
            long conpareElapsed = con.getElapsedTime();

            if (conpareElapsed > 0) {
                condition = condition && elapsed < conpareElapsed;
            }
            if (condition)
                return condition;
        }
        return false;
    }

    private boolean valueConditionalCheck(SkipConditional con, String agentName, String serviceName) {
        Boolean isSkip = null;
        for (ValueConditional vc : con.getValueConditionals()) {
            String value = null;
            String compareValue = vc.getValue();
            switch (vc.getTargetType()) {
            case AGENT:
                value = agentName;
                break;
            case SERVICE:
                value = serviceName;
                break;
            default:
                return false;
            }
            switch (vc.getCompareType()) {
            case MATCH:
                if (isSkip == null) {
                    isSkip = value.equals(compareValue);
                } else {
                    isSkip = isSkip && value.equals(compareValue);
                }
                break;
            case CONTAINS:
                if (isSkip == null) {
                    isSkip = value.contains(compareValue);
                } else {
                    isSkip = isSkip && value.contains(compareValue);
                }
                break;
            }
        }
        return isSkip;
    }
알림 무시 조건을 분석해서 알림 무시 여부를 리턴해주는 간단한 로직이다.

설정 파일의 구성이나 비교 로직은 입맛에 맞게 상황에 맞게 변경해서 쓰면 좋을 것 같다.

 


스카우터 플러그인 수정 작업을 처음 맡아 어려움이 예상됐지만 보면 바로 알 정도로 플러그인이 간편하게 구성되어 있어 수정에도 큰 어려움이 없었다. 현재는 파일 기반 설정 방식으로 조건이 포함되어 설정 파일 내용이 약간은 난해한 구성인데, 추후 SQLite 같은 small database 형태로 변경해서 다양한 조건들을 직관적으로 구성하여 쉽게 처리할 수 있게 하는 것도 좋을 것 같은 생각이 들었다.

Posted by 에스엠에스

댓글을 달아 주세요

  1. 스카우 2020.10.08 14:35  댓글주소  수정/삭제  댓글쓰기

    정말 좋은 글 잘보았습니다.^^
    해당 기능을 slack이 아닌 로깅파일로 남기려면 어떻게 해야할까요?
    개발자가 아니다보니 말씀하신 내용 이클립스에서 에러없이 컴파일한것만 해도
    벅찬 상태라서요 ㅠ ( 사실 맞게한지도 잘 모르겠습니다 )

    • 에스엠에스 2020.11.20 19:01 신고  댓글주소  수정/삭제

      스카우터 Built-in Server plugin을 직접 작성해서 해결 가능할 걸로 보입니다. 관련한 플러그인 작성 예시 블로그 링크를 공유해 드릴께요. https://m.blog.naver.com/PostView.nhn?blogId=occidere&logNo=221071278283&proxyReferer=https:%2F%2Fwww.google.com%2F


스카우터에서는 성능 모니터링 중 생긴 특정 상황에 대해 얼럿 기능이 들어가 있다.
스카우터 클라이언트의 얼럿항목에서 확인 및 조회가 가능하다.
하지만 항상 보고 있을수는 없는 상황에서, 얼럿을 스카우터 외부에서 받기를 원한다면 어떻게 해야 할까.

스카우터의 커스텀 플러그인으로 해결이 가능하다.
이런 상황에 맞추어 사용자가 직접 플러그인을 개발해서 넣을수 있도록 내부 API를 가지고 있고, 
이를 활용할 수 있는 커스텀 플러그인 구조를 가지고 있다.

우선 스카우터의 플러그인에 대해 알아보도록 하겠다.



스카우터의 플러그인

스카우터의 서버 플러그인은 크게 2가지 종류이다.
스크립트 형식으로 된 스크립팅 플러그인과 java의 jar 파일 형식으로 된 빌트인 플러그인이다.

스크립팅 플러그인은 파일에 java 문법으로 이루어진 스크립트를 넣어서 적용할 수 있다.
빌트인 플러그인의 경우 jar 파일로 이루어진 자바 프로젝트이다.
정해진 형식에 맞게 프로젝트를 만들고 내부 로직은 자유롭게 개발해서 사용할 수 있다.

이번 글에서는 2가지 방식 중 빌트인 플러그인 방식에 대해서 알아보려고 한다.
2가지 방식의 스카우터 플러그인 외에 에이전트에 적용하는 플러그인도 있다. 더 자세한 정보는 아래 링크에서 확인 가능하다.

처음 접하는 입장에서 플러그인을 처음부터 만드는것은 쉽지 않다.
그래서 스카우터를 사용하는 능력자들께서 손수 만드신 플러그인들을 공유하고 있다.
https://github.com/scouter-project 이곳에서 scouter-plugin 으로 시작하는 프로젝트 들이다.

확인해보고 필요한 것이 있다면 학습해보는 것도 좋을것이다.



빌트인 플러그인 적용방법

  1. 플러그인 프로젝트를 jar 파일로 만든다(빌드 방법은 아래에서 다룬다.)
  2. ./server/lib 폴더 아래에 jar 파일을 넣어준다.
  3. 스카우터 수집기 서버를 재기동 한다. (./server.startup.sh)



얼럿 플러그인

얼럿을 외부로 보내기 위한 플러그인은 매체에 따라 구분된다. email, slack, line, telegram, teamup(?) 등이 있다.
매체가 다른 플러그인이지만, 대부분 로직은 비슷하고 발송 단계의 로직만 다르기 때문에 원하는 발송 매체가 있다면 커스터마이징도 가능하다.

이번 글에서는 다양한 발송 매체 중에 슬랙을 이용하는 플러그인을 가지고 설명하려고 한다.
해당 플러그인의 깃헙 주소는 아래와 같다.

이 얼럿 플러그인으로 받을수 있는 알림은 아래와 같다.
  • CPU of Agent (warning / fatal)
  • Memory of Agent (warning / fatal)
  • Disk of Agent (warning / fatal)
  • connected new Agent
  • disconnected Agent
  • reconnect Agent



얼럿 플러그인 적용하기


1.  플러그인 빌드

적용하기 위해서는 얼럿 플러그인 프로젝트의 jar 파일이 필요하다.
클론을 받고, jar 파일로 빌드하자(macos 기준..)
git clone https://github.com/scouter-project/scouter-plugin-server-alert-slack.git
cd scouter-plugin-server-alert-slack/
mvn package

문제없이 빌드가 완료되었다면,
target 디렉토리에 scouter-plugin-server-alert-slack-1.0.1-SNAPSHOT.jar 라는 파일이 생성되어 있을 것이다.
이 파일을 스카우터 수집기 서버 디렉토리 아래 lib 폴더에 복사한다. (스카우터를 설치한 디렉토리에서 ./server/lib 이다)


2. 플러그인 설정

스카우터 수집기 서버의 설정 파일을 수정해야 한다.
별도의 변경이 없다면 경로는 아래와 같다.
./server/conf/scouter.conf
얼럿 플러그인을 사용하기 위해 필요한 설정값은 아래와 같다.
내용을 참고해서 환경에 맞게 내용을 추가해 준다.

ext_plugin_slack_send_alert : 발송기능을 사용할지 여부. true/false
ext_plugin_slack_debug : 메시지를 로깅 할지 여부. true/false
ext_plugin_slack_level : 로깅 레벨. (0=info, 1=warn, 2=error, 3=fatal)
ext_plugin_slack_webhook_url : 슬랙 웹훅 url
ext_plugin_slack_channel : 채널명 (ex. #test1) 혹은 사용자명(ex. @user_id)
ext_plugin_slack_botName : 알림을 보낼 봇 이름
ext_plugin_slack_icon_emoji : 봇 아이콘
ext_plugin_slack_xlog_enabled : xlog 얼럿 활성화 여부 true/false
ext_plugin_elapsed_time_threshold : 응답시간의 임계치. 이 값 보다 큰 응답시간에 반응한다
ext_plugin_gc_time_threshold : gc 시간 임계치. 이 값보다 큰 gc 시간이 걸리면 반응한다
ext_plugin_thread_count_threshold : 쓰레드 갯수 임계치. 쓰레드 갯수가 임계치 보다 커지면 반응한다.

적용해 사용하고 있는 설정은 아래와 같다.
# External Interface (Slack)
ext_plugin_slack_send_alert=true
ext_plugin_slack_debug=true
ext_plugin_slack_level=0
ext_plugin_slack_webhook_url=http://mydonain.com/hooks/1234CCSReKgN23wiw/dtCGozaNPAA41234yiCnhnpcNQCA7BSrkjqmFmsy3nMuKk95
ext_plugin_slack_channel=#alert_channel
ext_plugin_slack_botName=scouter
ext_plugin_slack_icon_emoji=:computer:
ext_plugin_slack_icon_url=https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQnQFLCYQ-rg_iJFXBzazZjUqMXTHPmTQ-AVU_JymsxleUHI1Oe
ext_plugin_slack_xlog_enabled=true
ext_plugin_elapsed_time_threshold=3000
ext_plugin_gc_time_threshold=5000
ext_plugin_thread_count_threshold=300

3. 적용하기

서버를 재기동 하면 적용된다.
./server/startup.sh



플러그인 커스터마이징

얼럿 플러그인을 사용하면서, 기능상 변경이 필요한 상황이 종종 찾아왔다.

예를들어,
스카우터에서 모니터링 하고 있는 서비스 중, 일부 서비스의 요청은 기본 응답시간이 1분이 넘어갈 정도로 오래 걸리게 만들어져 있었다.
응답시간 임계치 보다 크다보니 이 요청이 호출 될 때마다 응답시간 초과로 얼럿이 오게 되었다.
스카우터 전체 응답시간 임계치를 늘리면 기존에 10ms 이내로 처리되는 요청들의 응답이 느려질 때 얼럿을 받지 못하게 되어서 문제가 되었다.

이런식으로 얼럿 조건이 특수한 상황에 맞게 설정 되어야 할 때, 소스를 직접 수정하여 해결 할 수 있다.

대부분의 로직은 SlackPlugin.java 에 있다.

이 클래스의 메소드는 아래와 같은 용도를 가진다.

  • alert : 얼럿 발송
  • object : 에이전트 연결/연결끊김/재연결 얼럿 처리
  • xlog : xlog 에 에러로 표시된 건에 대한 얼럿 처리
  • counter : gc 타임 임계치 값에 대한 얼럿 처리
나의 경우 xlog 에서 응답시간 임계 초과 때 오는 얼럿을 서비스 마다 따로 설정하고 싶었다.
xlog 메소드 쪽에  분기를 추가해서 처리했다. 로직을 만들 때 사용한 내용은 아래와 같다.

  • elapsedThreshold 변수에는 아까 설정한 ext_plugin_elapsed_time_threshold 값이 들어있다. 기본 3초이다.
  • XlogPack 객체로 이루어진 pack 변수에는 xlog의 각 요청을 정보가 들어있다.
  • pack 변수에는 http 요청에 대한 엔드포인트 URL 정보와  지연된 응답시간 정보가 있다.

기존 코드의 264라인 근처에 수정을 하였다.
아래는 특정 엔드포인트에 대해서만 지연 응답시간 기준을 60초로 설정하는  분기를 추가한 코드이다.
try {
      int elapsedThreshold = conf.getInt("ext_plugin_elapsed_time_threshold", 0);
      if (elapsedThreshold != 0 && pack.elapsed > elapsedThreshold) {
         String serviceName = TextRD.getString(DateUtil.yyyymmdd(pack.endTime), TextTypes.SERVICE, pack.service);
         if (
                 AgentManager.getAgentName(pack.objHash).equals("/192.168.123.111/endpoint1")
                 || AgentManager.getAgentName(pack.objHash).equals("/192.168.123.111/endpoint2")
               && pack.elapsed < 60000) {
           //얼럿을 보내지 않음.
         } else {
            AlertPack ap = new AlertPack();
            ap.level = AlertLevel.WARN;
            ap.objHash = pack.objHash;
            ap.title = "Elapsed time exceed a threshold.";
            ap.message = "[" + AgentManager.getAgentName(pack.objHash) + "] "
                  + pack.service + "(" + serviceName + ") "
                  + "elapsed time(" + pack.elapsed + " ms) exceed a threshold.";
            ap.time = System.currentTimeMillis();
            ap.objType = AgentManager.getAgent(pack.objHash).objType;
            alert(ap);
         }
      }

수정을 마치게 되면 위에 설명한 방식으로 빌드를 다시 하고,
스카우터 수집기 서버 lib 폴더에 jar 파일을 덮어쓴 뒤 재기동 하면 바로 적용이 된다.

스카우터에서는 플러그인 개발을 위해 내부 API를 잘 정리하여 공유하고 있다.

자신만의 로직이 필요한 상황이라면 내부 API 문서를 참고하여 기존의 플러그인에 수정을 해서 필요에 맞게 사용할 수 있다.

혹은 새로운 플러그인을 새로 만들 수 있을것이다. 결과가 좋다면 커뮤니티에 공유해서 좋은 피드백도 받을 수도 있지 않을까 한다. 


Posted by panpid

댓글을 달아 주세요