ELK 실습(서울시 지하철 대시보드)

Ted Kim·2020년 6월 13일
2

ELK

목록 보기
2/6

저는 원리를 파고 들어가는 진성 개발자보다는,
일단 hello world를 해보고 원리를 다시 공부하는 청개구리 스타일이에요
왜 중요한지를 알지 못하면 하지를 않아요..

그래서 ElasticSearch로 대시보드 만드는 예시를 찾아보던 중
지하철 대시보드를 따라하기로 했어요
Elastic에서 엔지니어로 일하고 계신 갓종민 님이 올려주신 유투브
과할 정도로 친절합니다. 사랑합니다 형님

들어가기 전에

2016년에 올리셨던거를, 2019년에 다시 올려주셨어요
그리고 코로나 사태가 터졌을 때 코로나 대시보드도 만들어주샸고요
Elk 모두 사용하고 있기 때문에

  • https://ela.st/cloud-kr (guest / 손님)
    여기에 들어가면 김종민님이 만든 대시보드를 볼 수 있어요. 언제 없어질지 모르지만 일단 뭘 구현하려는지 보고 들가는게 좋다고 생각해요

소스

유투브, Git, Blog

시작

아래와 같은 순서로 진행되고요
저처럼 기초가 없는 상태에서 시작하려는 분들께서 참고하면 좋은 내용도 정리해봣어요

  1. 서울시 데이터 포털에서 csv 데이터 다운로드
  2. npm을 이용해 전처리 seoul-metro-2018.logs
  3. logstash를 이용해 elasticsearch에 인덱싱
  4. Kibana를 이용해 대시보드

데이터 준비

  • 친구들한테 설명하는 용도로 적는거라, csv 다운로드와 전처리에 대한 설명은 제외할게요
  • 데이터 전처리하는데 몇분 걸립니다.
  • 유투브에서 빼먹은 역정보 .json
    서울시 데이터 포털에서 지하철 호선 자료를 어디서 가져오는지 안나오는데
    역별 주소 정보 라고 검색하면 나와요

데이터 삽입

logstash 7.7 사용
윈도우에서는 설정이 달라요

logstash/bin 에 들어가서 실행
$ logstash -f seoul-metro-2018.conf

윈도우에서 하다보니 상대경로가 안맞아서 그냥 conf 파일을 bin에 넣어줘요.
270만건이어서 들어가는데 시간이 좀 걸립니다. 또한 윈도우는 input의 설정이 약간 달라요. sincedb_path => "nul"

//seoul-metro-2018.conf
input {
  file {
	path => "C:/seoul-metro-2018.logs"
    # path => "/Users/kimjmin/git/elastic-demos/seoul-metro-logs/data/seoul-metro-2018.logs"
    codec => "json"
    start_position => "beginning"
    sincedb_path => "nul"
  }
}

filter {
  mutate {
    remove_field => ["host","path","@version"]
  }
}

output {
  # stdout { }

  # 환경변수 설정: 
  # $LS_HOME/config/startup.options 또는
  # $LS_HOME/bin/logstash-keystore

  elasticsearch {
    hosts => ["192.168.4.103:9200"]
    index => "seoul-metro-logs-2018"
	pipeline => "hour_and_week"
  }
}
  • 잘들어갔는지 확인해주세요. 들어가는 도중에 그림 그려도됩니다.
    GET seoul-metro-logs-2018/_count 계속 때려주면 데이터가 들어가는지 볼수 있어요.
    stack monitoring 기능을 사용해도되요

그림 그리기

visualize에서 모듈형식으로 그래프들을 여러개 만들어요
승차/하차 처럼 같은 내용의 경우는 save as new를 체크하고 저장하면 됩니다.
날짜도 처음에는 안나올텐데 2018년으로 설정해줘요
나는 처음에 last 15 minutes가 @timestamp 기준이 아니라 insert 날짜인줄 알고 조금 헤멨어요

대시보드에 집어넣기

하나씩 집어넣는게 은근 쉬우면서도 귀찮다. 속도도 약간 느려서 버벅거리기도하고. 시간 필터의 경우는 우측 상단의 날짜 설정으로 체크해야해서 매번 바꾸는게 번거로워요. 그래도 ELK 기본적인 내용을 실습했다는 뿌듯함을 느낄 수 있어 기분 좋네요

자세한거는 스터디할 때 설명 예정!

@Timestamp를 이용한 전처리

1.painless로 시간 쪼개기

이런식으로 시간/ 일/ 주 등 원하는 양식으로 전처리를 할 수 있습니다.
1. GET 방식으로 확인
2. kibana -> index_patterns -> script fields에서 추가

//hour of day
GET seoul-metro-logs-2017/_search
{
  "script_fields": {
    "hour_of_day": {
      "script":{
        "lang": "painless",
        "source":"""
def hod=doc['@timestamp'].value.getHour();
return hod;
"""
      }
    }
  },
  "query": {
    "range":{
      "@timestamp": {
        "gte": "2017-01-01",
        "lte": "2018-12-31"
      }
    }
  }
}

//day of week
GET seoul-metro-logs-2017/_search
{
  "script_fields": {
    "hour_of_day": {
      "script":{
        "lang": "painless",
        "source":"""
def dow=doc['@timestamp'].value.getDayOfWeekEnum().getValue();
return dow;
"""
      }
    }
  },
  "query": {
    "range":{
      "@timestamp": {
        "gte": "2017-01-01",
        "lte": "2018-12-31"
      }
    }
  }
}

2.pipline에 정의하기

일일이 넣는건 불편하죠. 그래서 pipline이라는 것을 사용할 수 있어요
여러 방안이 있지만 script processor이라는 것을 유투브에서 설명해주십니다.
https://www.elastic.co/guide/en/elasticsearch/painless/master/painless-ingest-processor-context.html

//파이프라인 만들기, 여기서는 ctx.이 중요!
PUT _ingest/pipeline/hour_and_week
{
  "description": "add hour_of_day and day_of_week field from @timestamp",
  "processors": [
    {
      "script":{
        "lang": "painless",
        "source":"""
def ts=ctx['@timestamp'];
def sdf=new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
def date=sdf.parse(ts);
def cal=Calendar.getInstance();
cal.setTime(date);

ctx.hour_of_day = cal.get(Calendar.HOUR_OF_DAY);

def dowNum=cal.get(Calendar.DAY_OF_WEEK);
def dowEn=["SUN","MON","TUE","Wed","Thu","Fri","Sat"][dowNum];
def dowKr=["일","월","화","수","목","금","토"][dowNum];

ctx.day_of_week = ["num":dowNum, "en":"downEn","kr":dowKr]
"""
      }
    }
  ]
}

#재색인으로 테스트!
POST _reindex
{
  "source": {
    "index": "seoul-metro-logs-2017",
    "query": {
      "range":{
        "@timestamp": {
          "gte": "2018-01-01",
          "lte": "2018-01-02"
        }
      }
    }
  },
  "dest": {
    "index": "seoul-metro-logs-test",
    "pipeline": "hour_and_week"
  }
}

실제 스터디에서 사용한 스크립트

#hello world
GET _all

DELETE _template/seoul-metro
GET _template/seoul-metro

#logstash check
# count(*), * 
GET seoul-metro-logs-*/_count
GET seoul-metro-logs-*/_search

#mapping check
GET seoul-metro-logs-*/_mapping
DELETE seoul-metro-logs-*

#data check

GET seoul-metro-logs-2018/_search
{
  "size":1,
  "query": {
    "match": {
      "station.name": "홍대입구"
    }
  }
}

GET seoul-metro-logs-2018/_search
{
  "size":1,
  "query": {
    "match": {
      "station.name": "홍대"
    }
  }
}

#nori check
# bin/elasticsearch-plugin install analysis-nori
# elasticsearch restart

GET seoul-metro-logs-2018/_analyze
{
  "text":"홍대입구",
  "analyzer": "nori"
}

#template check
GET _template
GET _template/seoul-metro
PUT _template/seoul-metro
{
  "order": 5,
  "index_patterns": [
    "seoul-metro-logs*"
  ],
  "settings": {
    "number_of_shards": 2,
    "analysis": {
      "analyzer": {
        "nori": {
          "tokenizer": "nori_t_discard",
          "filter": "my_shingle"
        }
      },
      "tokenizer": {
        "nori_t_discard": {
          "type": "nori_tokenizer",
          "decompound_mode": "discard"
        }
      },
      "filter": {
        "my_shingle": {
          "type": "shingle",
          "token_separator": "",
          "max_shingle_size": 3
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "@timestamp": {
        "type": "date"
      },
      "code": {
        "type": "keyword"
      },
      "line_num": {
        "type": "keyword"
      },
      "line_num_en": {
        "type": "keyword"
      },
      "location": {
        "type": "geo_point"
      },
      "people": {
        "properties": {
          "in": {
            "type": "integer"
          },
          "out": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "station": {
        "properties": {
          "kr": {
            "type": "text",
            "fields": {
              "nori": {
                "type": "text",
                "analyzer": "nori",
                "search_analyzer": "standard"
              },
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "name": {
            "type": "text",
            "fields": {
              "nori": {
                "type": "text",
                "analyzer": "nori",
                "search_analyzer": "standard"
              },
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          }
        }
      }
    }
  }
}
GET _template/seoul-metro
#logstash 재색인 후 test
GET seoul-metro-logs-2018/_search
{
  "size":1,
  "query": {
    "match": {
      "station.name.nori": "홍대 입구"
    }
  }
}

# inverted index
# 홍대: 401
# 입구: 401, 402.503

#data check
GET seoul-metro-logs-*/_mapping
GET seoul-metro-logs-*/_search?size=1
GET seoul-metro-logs-2018/_doc/GYU50HIBa3wl0dvSULv2

#pipeline
GET seoul-metro-logs-2018/_search
{
  "script_fields": {
    "hour_of_day": {
      "script":{
        "lang": "painless",
        "source":"""
def dow=doc['@timestamp'].value.getDayOfWeekEnum().getValue();
return dow;
"""
      }
    }
  },
  "query": {
    "range":{
      "@timestamp": {
        "gte": "2018-01-01",
        "lte": "2018-01-02"
      }
    }
  }
}

DELETE _ingest/pipeline/hour_and_week

PUT _ingest/pipeline/hour_and_week
{
  "description": "add hour_of_day and day_of_week field from @timestamp",
  "processors": [
    {
      "script":{
        "lang": "painless",
        "source":"""
def ts=ctx['@timestamp'];
def sdf=new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
def date=sdf.parse(ts);
def cal=Calendar.getInstance();
cal.setTime(date);

ctx.hour_of_day = cal.get(Calendar.HOUR_OF_DAY);

def dowNum=cal.get(Calendar.DAY_OF_WEEK);
def dowEn=["SUN","MON","TUE","Wed","Thu","Fri","Sat"][dowNum];
def dowKr=["일","월","화","수","목","금","토"][dowNum];

ctx.day_of_week = ["num":dowNum, "en":"downEn","kr":dowKr]
"""
      }
    }
  ]
}

#재색인으로 테스트!
POST _reindex
{
  "source": {
    "index": "seoul-metro-logs-2018",
    "query": {
      "range":{
        "@timestamp": {
          "gte": "2018-01-01",
          "lte": "2018-01-02"
        }
      }
    }
  },
  "dest": {
    "index": "seoul-metro-logs-test",
    "pipeline": "hour_and_week"
  }
}

GET seoul-metro-logs-test/_search?size=1

# logstash, pipeline 있는걸로 재색인
GET seoul-metro-logs-2018/_count
GET seoul-metro-logs-2018/_search?size=1


#inspect test
GET seoul-metro-logs-*/_search
{
  "aggs": {
    "3": {
      "terms": {
        "field": "day_of_week.num",
        "order": {
          "_key": "desc"
        },
        "size": 7
      },
      "aggs": {
        "4": {
          "terms": {
            "field": "hour_of_day",
            "order": {
              "1": "desc"
            },
            "size": 24
          },
          "aggs": {
            "1": {
              "sum": {
                "field": "people.in"
              }
            }
          }
        }
      }
    }
  },
  "size": 0,
  "stored_fields": [
    "*"
  ],
  "script_fields": {},
  "docvalue_fields": [
    {
      "field": "@timestamp",
      "format": "date_time"
    }
  ],
  "_source": {
    "excludes": []
  },
  "query": {
    "bool": {
      "must": [],
      "filter": [
        {
          "match_all": {}
        },
        {
          "range": {
            "@timestamp": {
              "gte": "2017-12-01T05:30:24.385Z",
              "lte": "2018-01-31T06:30:35.395Z",
              "format": "strict_date_optional_time"
            }
          }
        }
      ],
      "should": [],
      "must_not": []
    }
  }
}

profile
코딩하는 경영학과

0개의 댓글