구축

  • https://github.com/deviantony/docker-elk  
  • JVM Heap이 256m으로 작게 설정되어 있어서 이를 늘려주어야 함.
  • 계정 관련 설정도 무료이므로 나와 있는 대로 진행하면 됨.
    • Initial setup 부분 참고하여 PW 생성 및 변경 해 주고, yml conf 파일에 변경된 PW 적용 한 다음, 변경된 PW로 elastic 계정 키바나 로그인 하고, Management - Users 에서 새 superuser 계정 만들어서 이를 사용하면 된다.
  • Kibana HTTPS 설정 https://www.elastic.co/guide/en/elasticsearch/reference/7.14/security-basic-setup-https.html#encrypt-kibana-browser  
    • 인증서 인증은 Let's Encrypt & Certbot 으로
      • 어차피 나 혼자 쓸거라면 인증서 인증은 귀찮게 안해도 됨. 어차피 내가 발급한 인증서고, 내가 싸인한 인증서라
      • chrome에서 "이 사이트는 보안 연결(HTTPS)이 사용되지 않았습니다" 메시지가 나오지만 packet 보면 TLS로 암호화해서 주고받고 있으므로 신경쓰지 않아도 됨.
    • 인증서는 호스트 머신에 두고 docker-compose.yml에 mount 추가. 
      • 컨테이너에 넣고 commit 하는 것 보다 mount가 나아보임.
      • 수정 후 `` docker-compose up -d``

 

 

 

log 수집 방법 및 서버 아키텍쳐

일반적인 상황에서는, Filebeat + ELK

```

// 모두 가능하지만, 첫 번째 설정이 일반적임.

file - Filebeat - Logstash - ES

file - Logstash - ES

file - Filebeat - ES

```

 

  • Filebeat는 데이터를 수집하여 전송하는 역할
    • 서버에 Filebeat 설치하고 설정해주면, 지정한 로그 파일 모니터링하고 실시간으로 변경 체크해서 이벤트 수집하고 ES로 전송 (tail -f와 유사하게 동작함)
    • 파일 내용과 offset을 전송함.
    • 어떤 file, line을 logstash로 보낼지. file & line 단위 include/exclude 설정 가능
  • Logstash는 데이터를 읽어와 가공하는 역할. 가공 후 ES로 전송.
    • 가공(파일내용 파싱)은 logstash에서 하도록 설정함.
      • 필드 단위 설정
      • e.g., logstash에서 라인을 grok로 파싱해서 필드 별로 식별하고, 불필요한 필드는 제거한 다음 ES로 전송
    • Filebeat 없이 Logstash가 직접 수집 할 수도 있다.

 

file - logstash로 바로 읽을 수 있으면, 굳이 왜 Filebeat를 쓰나?

  • filebeat는 logstash에서 파일 읽고 전송하는 기능만 있는 subset이라고 보면 되는데
  • 데이터 출처(서버)가 여러 대인 경우 각각의 서버에 logstash를 설치해 운용하기에는 낭비가 심하기 때문.
  • 따라서 각 서버에는 경랑 수집기 Filebeat만 설치하고, logstash는 소수의 장비에서 운용하기 위함

 

springboot log는 filebeat에서 지원하는 module이 없는데? : 설정 예제
One big disadvantage of traditional plain text log format is that it is hard to handle multiline string, stacktrace, formatted MDCs etc.
=> stacktrace 등 멀티라인 로그 처리를 위해서 별도 설정 넣어주어야 한다.

 

```makefile

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - d:\apps\tradingsystem\logs\*

 

  #exclude_lines: ['^DBG']

  #include_lines: ['^ERR', '^WARN']

  #exclude_files: ['.gz$']

 

  # Optional additional fields. 이를 이용해 input을 구분하는 등 활용 가능

  #fields:

  #  level: debug

  #  review: 1

 

  ### Multiline options : stacktrace 같은 멀티라인 로그 처리를 위해

 

  # The regexp Pattern that has to be matched.

  multiline.pattern: {이 부분을 logback log prefix와 동일하게 설정}

 

  # true이면 pattern과 일치하는 로그를 새 로그의 시작으로 본다.

  multiline.negate: true

 

  # pattern 이후에 다른 라인이 붙을지, 이전에 붙을지를 결정한다.

  multiline.match: after

  # 상세 설명은 https://www.elastic.co/guide/en/beats/filebeat/current/multiline-examples.html 참고

```

 

하지만 보다시피 regex pattern 설정하는 부분이 약간 까다롭다.

 

file - logstash로 바로 읽을 때 multiline 처리 : 설정 예제
  • input - beats 에서는 multiline codec을 사용하지 못한다. 즉, filebeat를 사용하는 경우 멀티라인 처리는 filebeat에서 해서 넘겨줘야 함.
  • filebeat 없이 바로 logstash가 file을 읽어 처리하는 경우, multiline 처리는 아래와 같음.

```json

input {

  file {

    path => /tmp/spring.log

    codec => multiline {

      pattern => "^(%{TIMESTAMP_ISO8601})"

      negate => true

      what => "previous"

    }

  }

}

```

 

Filebeat 없이 네트워크를 통해 logstash로 바로 쏘는 경우 : pros and cons

```

app(using network) - Logstash - ES

```

One big disadvantage of traditional plain text log format is that it is hard to handle multiline string, stacktrace, formatted MDCs etc.

 

  • Filebeat에서 멀티라인을 처리하려면, multiline.pattern을 따로 정의해주어야 해서 설정이 약간 까다로움.
    • 예제 처럼 로그 형식이 한 가지로 일정하다면 괜찮지만, 다양하다면? 정규식도 복잡해지고 수용이 안된다.
    • Keep things simple and let the application just write the logs to disk. 이기는 하지만, 설정으로 수용이 안되면 어쩔 수 없다.
  • json으로 가공하여 로깅하면 멀티라인을 신경쓰지 않아도 되므로, SpringBoot log를 ES에 적재할 때는 별도의 logback appender를 사용하여 json 형태의 [추가적인 log 파일로 로깅하거나 || 네트워크로 바로 쏘도록] 하는 경우가 있음.
    • 보통 둘 중에서는 후자를 많이 씀.
    • 전자의 단점 : txt log가 있는데 별도 json log를 또 만드는 것이 지저분하다.
    • 후자의 단점 : 이렇게 file로부터 적재하지 않고 network로 바로 쏘게 되면 elk가 다운 되어 있는 경우 그 동안의 로그가 날아간다. (큰 단점)
    • (network로 바로 쏘는 방법도 backlog queue를 쓰긴 하지만, 결국 다 차면 lost 발생한다.)
  • 이러한 단점 때문에, 보통은 로그 형식이 한 가지로 일정하므로, filebeat에 일반 line으로 로깅하고 multiline 처리하는 방법이 군더더기 없이 깔끔하여 난 이 방법을 선호함.
    • 별도의 json log 파일 생성하지 않으므로 깔끔하고,
    • elk 장애가 나더라도, 정상화 후 file에서 읽었던 곳 부터 적재해 나가면 되므로 통계 유실 가능성 낮음.
  • 그럼에도 불구하고 네트워크로 바로 쏘도록 하려면, logback 사용 중인 경우 xml에 appender 하나 추가하는 식으로 간단히 수집 가능함.

 

추가로 Kafka를 사용하는 경우

  • `` Filebeat - Logstash Shipper - Kafka - Logstash Indexer - ES`` 구성
    • 이 케이스는 트래픽이 급증하거나 로그가 과도하게 찍힐 때, ELK에 부담이 그대로 전가되는 것을 막기 위해 의미가 있음. (pub/sub 모델 이니까)
    • filebeat <> logstash 사이에도 backpressure가 있긴 함.
  • `` app - kafka - logstash - Es`` 구성
    • 서버가 너무 많아 일일히 filebeat를 설치하고 관리하기 귀찮은 경우.
    • 단순 app에서 logstash로 바로 쏘는 경우 유실 가능성 있으므로 kafka를 둔다
  • https://www.elastic.co/kr/blog/just-enough-kafka-for-the-elastic-stack-part1 
    • 공식 docs 내용이 좋다. Kafka 추가를 고민하고 있다면 꼭 읽어보는 것이 좋을 듯.

 


 

logstash, index 설정

Spring logback preset 로그 포맷에 맞춘 logstash 설정

```json

input {

  beats {

    port => 5044

  }

 

  tcp {

    port => 5000

  }

}


## Add your filters / logstash plugins configuration here

 

filter {

  grok {

    match => { "message" => "%{TIMESTAMP_ISO8601:[loginfo][date]}\s+%{LOGLEVEL:[loginfo][level]} %{POSINT:[loginfo][pid]} --- \[\s*%{DATA:[loginfo][thread]}\] %{DATA:[loginfo][class]}\s+: %{GREEDYDATA:[loginfo][message]}" }

  }

  date {

    match => ["[loginfo][date]", "yyyy-MM-dd HH:mm:ss.SSS"] 

    target => "@timestamp"    // 명시하지 않으면 기본값 @timestamp 이지만 명시해주는 편이 좋아보임

    timezone => "Asia/Seoul"  // (필수) timezone을 명시하지 않으면 시간이 이상하게 파싱될 수 있음

  }

  mutate {

    remove_field => ["host", "agent", "message"]

  }

}

 

output {

  elasticsearch {

    hosts => "elasticsearch:9200"

    user => "elastic"

    password => "yourpassword"

    ecs_compatibility => disabled

    index => 'logstash-%{+YYYY.MM}'

  }

}

```

 

grok 필드 파싱 결과

 

 

index 설계

  • (중요) ES에서 필드를 인식할 때는 타입까지 인식한다. 그래서 한 인덱스 안에서 필드 네임 a는 타입이 항상 같도록 구성해야 한다. (한 필드 네임의 타입이 다양한 경우 하단에 기술한 에러를 마주할 수 있음)
  • visualize 시, 한 index 내의 항목들을 조합해서 visualize하게 된다. 서로 다른 index 사이의 항목들을 동시에 조회해 visualize는 안되는 것으로 보임. (lens를 사용하면 가능하다)
  • 로깅용 객체를 따로 두는 것도 고려해볼만 함.

 

결론  ) 같은 객체, 같은 통계를 공유하면 같은 index로 구성한다.

 

log message에서 index || logtype을 식별하여 별도 index로 적재하는 설정

  • 기타 설정은 위와 같음
  • [loginfo][type]이 없는 경우 logstash index로 적재하며 json 파싱 없이 문자열 그대로 log_msg 필드에 매핑
  • [loginfo][type]이 있는 경우 json 파싱

```json

/* 기타 설정은 위와 같음 */

grok {

  match => { "message" => "%{TIMESTAMP_ISO8601:[loginfo][date]}\s+%{LOGLEVEL:[loginfo][level]} %{POSINT:[loginfo][pid]} --- \[\s*%{DATA:[loginfo][thread]}\] %{DATA:[loginfo][class]}\s+: (\#_\[%{DATA:[loginfo][type]}\]_\#)?%{GREEDYDATA:[@metadata][body]}" }

}

 

if ![loginfo][type] {

    mutate { 

        add_field => { "[@metadata][target_index]" => "logstash" } 

        add_field => { "log_msg" => "%{[@metadata][body]}" } 

    }

} else {

    json {

        source => "[@metadata][body]"

    }

    mutate { 

        add_field => { "[@metadata][target_index]" => "%{[loginfo][type]}" } 

    }

}

 

output {

  elasticsearch {

    index => '%{[@metadata][target_index]}-%{+YYYY.MM}'

    ...

```

```kt

fun Logger.infoEs(index: String, obj: Any) {

    val jsonStr = ES_OBJECT_MAPPER.writeValueAsString(mapOf(obj::class.simpleName to obj))

    info("#_[$index]_#$jsonStr")

}

```

 

Kibana에서 List 타입 데이터 적재 시 출력 방식

```json

[

    {

        "f1": "qwer",

        "f2": 1234,

        "f3": [1, 2, 3]

    }, 

    {

        "f1": "qwer",

        "f2": 1234,

        "f3": [1, 2, 3]

    }, 

    ...

]

```

 

  • f2와 f3는 숫자 타입이므로 visualize 에서 sum 등이 가능
  • f3같은 [[1, 2, 3], [1, 2, 3]]은 1, 2, 3, 1, 2, ... 가 된다는 점을 주의해야 함
  • ES는 완벽한 BigDecimal 처리가 없다는 것도 참고. scaled_float가 있지만 무손실은 아니다.

 

"error"=>{"type"=>"illegal_argument_exception", "reason"=>"mapper [필드명] cannot be changed from type [long] to [float]"} 발생하는 경우

  • 이게 왜 발생하는거냐면, index template에 정의되어 있지 않은 index pattern이 들어오는 경우, 해당 index의 필드 및 타입을 식별하기 위해서 ES는 기본적으로 Dynamic mapping을 사용함
  • Dynamic mapping을 사용하면 최초 요청 시 필드의 값이 곧 그 필드의 타입이 됨. (1 -> long, 0.1 -> float)
    • 참고로 해당 index가 필드를 어떻게 매핑하고 있는지는 Index Management - Mapping 메뉴에 나옴
  • 최초 요청 시 필드 값이 1이어서 해당 필드 타입은 long이 되었는데, 그 필드에 0.1이 들어오면, long은 float로 변환 불가능하기 때문에 에러가 발생함.
  • 이를 해결하려면?
    • 최초 요청 시 0.1이 들어오면서 float가 될 때 까지 인덱스를 삭제하고 계속 재시도 하는 방법이 있고,
    • index template을 정의하는 방법이 있음! (권장)
  • index template을 정의하는 것이 귀찮을 수 있는데, 오류가 나는 필드만 명시하고 나머지는 Dynamic mapping에 맡겨도 되기 때문에 전혀 문제될 것이 없음.
    • ES에서도 이를 정의해서 사용하는 것을 권장하고 있음.
    • 필드의 타입을 세세하게 지정하고 싶은 경우에도 이 기능을 사용하면 된다 ( arrays , flattened / objectnested )

 

 

'Data Store' 카테고리의 다른 글

ELK with SpringBoot  (0) 2021.09.11
카프카 컨슈머  (0) 2021.07.23
docker 안의 DB를 사용할 때 timezone 문제  (0) 2021.05.19
[Oracle] Pagination  (0) 2021.05.17
Flyway  (0) 2021.05.13
[Oracle] longest match  (0) 2021.02.16