-
서울시 지하철 대시보드1 - 전처리엔지니어링/ELK 2021. 7. 31. 15:26728x90
- reference
https://github.com/eskrug/elastic-demos
GitHub - eskrug/elastic-demos: Elastic 한국 커뮤니티에서 만든 데모 모음입니다
Elastic 한국 커뮤니티에서 만든 데모 모음입니다. Contribute to eskrug/elastic-demos development by creating an account on GitHub.
github.com
https://www.youtube.com/watch?v=ypsEZXVYLo4&list=PLhFRZgJc2afqxJx0RBKkYUxSUDJNusXPl
1. 준비
프로그램 준비
- 디렉토리 생성
> git 파일 복제 후, seoul-metro-logs 디렉토리에 진입
$ cd $ git clone https://github.com/eskrug/elastic-demos.git $ cd elastic-demos/seoul-metro-logs
- data 디렉토리 생성
$ mkdir data
공공데이터 수집
- 데이터 다운로드 (wget 안됨, 로컬에 직접 받아준다)
> 데이터가 많이 바뀌어서 반드시 첨부한 데이터로 사용바랍니다.
> 서울시 열린데이터 광장
1. 서울시 역코드로 지하철역 위치 조회
https://gaussian37.github.io/python-etc-수도권-지하철/
수도권 지하철 좌표
gaussian37's blog
gaussian37.github.io
2. 서울교통공사 지하철 역명 다국어 표기 정보
http://data.seoul.go.kr/dataList/OA-2751/F/1/datasetView.do
서울교통공사 지하철 역명 다국어 표기 정보
1~8호선 지하철 역명에 대한 한글, 한자, 영자, 중국어, 일어 표기 정보 서비스 입니다.
data.seoul.go.kr
3. 서울교통공사 연도별 일별 시간대별 역별 승하차 인원
http://data.seoul.go.kr/dataList/OA-12921/F/1/datasetView.do
서울교통공사 연도별 일별 시간대별 역별 승하차 인원
서울교통공사 연도별 일별 시간대별 역별 승하차인원 (2008년~2019년)
data.seoul.go.kr
> 2018년도 데이터 다운로드
> 1행 삭제, '구분' 열 삭제
> 인코등 EUC-KR에서 UTF-8로 변경
- 다운로드 받은 파일을 source 디렉토리에 저장
2. 전처리
파일 변환 프로그램 실행
- npm 설치
$ cd /elastic-demos/seoul-metro-logs $ apt install npm $ npm install elastic-demo@1.0.0 /root/elastic-demos/seoul-metro-logs └── csv-parse@1.3.3
#만약 npm install이 잘 안되면 다음 명령어를 통해 패키지 모두 삭제 후 다시 설치한다. $ apt autoremove npm
- 변환 프로그램 수정 (데이터도 다르고ㅡ 코드가 업데이트돼서 별도로 수정 필요)
station_meta.js
var fs = require('fs'); // var parse = require('csv-parse'); var sInfo = fs.readFileSync('source/station_info.json', 'utf8'); var sLang = fs.readFileSync('source/station_lang.json', 'utf8'); var sLocation = JSON.parse(sInfo).DATA; var sNames = JSON.parse(sLang).DATA; // console.log(sLocation.length); // console.log(sNames); // 2020-01 지하철역 주소 및 전화번호 정보 추가 //var fsAddr1to4 = fs.readFileSync('source/station_addr_1to4.json', 'utf8'); //var sAddr1to4 = JSON.parse(fsAddr1to4).DATA; //var fsAddr5to8 = fs.readFileSync('source/station_addr_5to8.json', 'utf8'); //var sAddr5to8 = JSON.parse(fsAddr5to8).DATA; //위치정보 정리 //var location_meta = new Object(); var location_meta = new Array(); for(var i = 0; i < sLocation.length; i++){ if(sLocation[i].xpoint !== "" && (sLocation[i].line_num === "1" || sLocation[i].line_num === "2" || sLocation[i].line_num === "3" || sLocation[i].line_num === "4" || sLocation[i].line_num === "5" || sLocation[i].line_num === "6" || sLocation[i].line_num === "7" || sLocation[i].line_num === "8" || sLocation[i].line_num === "I" || sLocation[i].line_num === "B") ){ //"서울" 은 "서울역" 으로 이름 변경. if(sLocation[i].station_nm === "서울"){ sLocation[i].station_nm = "서울역" } // location_meta.push({ //"line_num" : sLocation[i].line_num, //"station_code" : parseInt(sLocation[i].station_cd), "station_nm" : sLocation[i].station_nm, //"line_num" : Number(sLocation[i].line_num), "geo_x" : Number(sLocation[i].xpoint), "geo_y" : Number(sLocation[i].ypoint) }); // console.log("%j",location_meta[location_meta.length-1]); } } //console.log(location_meta); //드디어 잘 쌓임 //다국어 역 정보 정리 var language_meta = new Object(); for(var i = 0; i < sNames.length; i++){ if(sNames[i].stn_nm !== ""){ var stn_name = sNames[i].stn_nm; var stn_nm_short = ""; // 광나루\n(장신대) 같은 줄바꿈 이름 앞 이름만 따옴. if(sNames[i].stn_nm.indexOf("\n") > 0){ stn_nm_short = sNames[i].stn_nm.split("\n")[0]; } else { stn_nm_short = sNames[i].stn_nm; } // 총신대입구(이수) 같은 "(" 앞 이름만 따옴 if(sNames[i].stn_nm.indexOf("(") > 0){ stn_nm_short = sNames[i].stn_nm.split("(")[0]; } else { stn_nm_short = sNames[i].stn_nm; } language_meta[stn_nm_short] = { "stn_nm_kor" : sNames[i].stn_nm, "stn_nm_chc" : sNames[i].stn_nm_chc, "stn_nm_eng" : sNames[i].stn_nm_eng, "stn_nm_chn" : sNames[i].stn_nm_chn, "stn_nm_jpn" : sNames[i].stn_nm_jpn } } } //console.log(language_meta); //메타 병합 var stations_meta = new Object(); // console.log(location_meta.length); //629 for(var i=0; i < location_meta.length; i++){ //console.log(language_meta[location_meta[i].station_nm]); //console.log(location_meta[i].station_nm); if(location_meta[i].station_nm === "총신대입구(이수)") { location_meta[i].station_nm = "이수"; } if(language_meta[location_meta[i].station_nm]){ stations_meta[location_meta[i].station_nm] = { //"line_num" : location_meta[i].line_num, //"station_code" : location_meta[i].station_code, "stn_nm" : location_meta[i].station_nm.replace("\n",""), "stn_nm_kor" : language_meta[location_meta[i].station_nm].stn_nm_kor.replace("\n",""), "stn_nm_chc" : language_meta[location_meta[i].station_nm].stn_nm_chc.replace("\n",""), "stn_nm_eng" : language_meta[location_meta[i].station_nm].stn_nm_eng.replace("\n",""), "stn_nm_chn" : language_meta[location_meta[i].station_nm].stn_nm_chn.replace("\n",""), "stn_nm_jpn" : language_meta[location_meta[i].station_nm].stn_nm_jpn.replace("\n",""), "geo_x" : location_meta[i].geo_x, "geo_y" : location_meta[i].geo_y } } else { stations_meta[location_meta[i].station_nm] = { //"station_code" : location_meta[i].station_code, "stn_nm" : location_meta[i].station_nm.replace("\n",""), "geo_x" : location_meta[i].geo_x, "geo_y" : location_meta[i].geo_y } } } //console.log(stations_meta); //드디어 잘 쌓임 module.exports = stations_meta; //for(var i=0; i < sAddr1to4.length; i++){ // console.log(sAddr1to4[i]); // if(sAddr1to4[i].statn_nm === "신천") { sAddr1to4[i].statn_nm = sAddr1to4[i].statn_nm.replace("신천","잠실새내") } // if(sAddr1to4[i].statn_nm === "총신대입구") { sAddr1to4[i].statn_nm = sAddr1to4[i].statn_nm.replace("총신대입구","이수") } // if(stations_meta[sAddr1to4[i].statn_nm]){ // stations_meta[sAddr1to4[i].statn_nm].address = sAddr1to4[i].adres; // stations_meta[sAddr1to4[i].statn_nm].road_address = sAddr1to4[i].rdnmadr; // stations_meta[sAddr1to4[i].statn_nm].phone = sAddr1to4[i].telno; // } // console.log(sAddr1to4[i]); //} //for(var i=0; i < sAddr5to8.length; i++){ // if(sAddr5to8[i] && sAddr5to8[i].stn_nm !== "서울역" ){ // if(sAddr5to8[i].stn_nm === "역촌역"){ // sAddr5to8[i].stn_nm = "역촌"; // } else { // sAddr5to8[i].stn_nm = sAddr5to8[i].stn_nm.split("역")[0]; // } // } // if(stations_meta[sAddr5to8[i].stn_nm]){ // stations_meta[sAddr5to8[i].stn_nm].address = sAddr5to8[i].stn_addr; // stations_meta[sAddr5to8[i].stn_nm].road_address = sAddr5to8[i].stn_road_addr; // stations_meta[sAddr5to8[i].stn_nm].phone = sAddr5to8[i].stn_phone; // } // console.log(sAddr5to8[i]); //}
run.js
var fs = require('fs'); var parse = require('csv-parse'); var s_meta = require('./stations_meta'); //분석할 파일 이름 정확히 기입 var f1to4 = fs.readFileSync('source/metro_log_2018.csv', 'utf8'); parse(f1to4, {comment:"#"}, function(csv_err, csv_data){ if (csv_err) { return console.log(csv_err); } // csv 파일 형태는 아래와 같이 되어야 함. // 0 1 2 3 4 5 6 7 ... 23 24 25 // 날짜, 호선, 역번호, 역명, 구분, 05~06, 06~07, 07~08, ... 23~24, 00~01, 합계 // 2018-01-01, 1호선, 150, 서울역, 승차, 373, 318, 365, ... 781, 96, 40393 // 2줄씩 루프 돌면서 0~3 열 까지의 데이터가 동일한지 확인 for(var cd=1; cd< csv_data.length ; cd+=2){ var dataIn = csv_data[cd]; var dataOut = csv_data[cd+1]; if(dataIn[0]===dataOut[0] && dataIn[1]===dataOut[1] && dataIn[2]===dataOut[2] && dataIn[3]===dataOut[3]){ // 역명 var station_name = dataIn[3]; // 날짜 var ldateTemp = dataIn[0].split('-'); // 시간 값으로 루프 for(var h=0; h < 20; h++){ var ldate = new Date(ldateTemp[0],Number(ldateTemp[1])-1,ldateTemp[2],h); // 승차인원 var people_in = dataIn[5+h]; people_in = Number(people_in); // 하차인원 var people_out = dataOut[5+h]; people_out = Number(people_out); var line_num_lang = { "1호선" : "Line 1", "2호선" : "Line 2", "3호선" : "Line 3", "4호선" : "Line 4", "5호선" : "Line 5", "6호선" : "Line 6", "7호선" : "Line 7", "8호선" : "Line 8" } // 역명에 () 포함하는 값들 모두 치환 var stn_nm_full = station_name; if(station_name.indexOf("(") > 0){ station_name = station_name.split("(")[0]; } // if(h===0){ // console.log("station_name: "+station_name); // console.log("stn_nm_full: "+ stn_nm_full); // } var s_logs = {}; var t_st_nm = station_name; if(station_name === "총신대입구") { t_st_nm = "이수"; stn_nm_full = "총신대입구(이수)" } else { if(!s_meta[t_st_nm]){ // if(h===0){ console.log("s_meta[stn_nm_full]: %j ",s_meta[stn_nm_full]); } t_st_nm = stn_nm_full; } } // if(h===0 ){ console.log(t_st_nm); } if(s_meta[t_st_nm].stn_nm_kor){ s_logs = { "@timestamp" : ldate, "code": dataIn[2], "line_num" : dataIn[1], "line_num_en" : line_num_lang[dataIn[1]], "station": { "name" : stn_nm_full, "kr" : s_meta[t_st_nm].stn_nm_kor, "en" : s_meta[t_st_nm].stn_nm_eng, "chc" : s_meta[t_st_nm].stn_nm_chc, "ch" : s_meta[t_st_nm].stn_nm_chn, "jp" : s_meta[t_st_nm].stn_nm_jpn }, "location" : { "lat" : s_meta[t_st_nm].geo_x, "lon" : s_meta[t_st_nm].geo_y }, "people":{ "in" : people_in, "out" : people_out, "total" : people_in+people_out } } } else { s_logs = { "@timestamp" : ldate, "code": dataIn[2], "line_num" : dataIn[1], "line_num_en" : line_num_lang[dataIn[1]], "station": { "name" : stn_nm_full }, "location" : { "lat" : s_meta[t_st_nm].geo_x, "lon" : s_meta[t_st_nm].geo_y }, "people":{ "in" : people_in, "out" : people_out, "total" : people_in+people_out } } } // if( t_st_nm.indexOf("구파발") > -1 ) { console.log(s_logs); } // console.log(s_logs); //console.log(ldate.toISOString().slice(0,10).replace(/-/g,"")); // var fileName = "1to4_"+ldateTemp[0]+ldateTemp[1]+ldateTemp[2]+".log"; //var fileName = "1to4_"+ldate.toISOString().slice(0,10).replace(/-/g,"")+".log"; var logdata = JSON.stringify(s_logs)+"\n"; // data 디렉토리 아래 저장할 파일 이름. data 디렉토리 없으면 생성해야 함 fs.appendFileSync("data/seoul-metro-2018.logs", logdata); } } } });
- 변환 프로그램 실행
$ cd /elastic-demos/seoul-metro-logs $ node bin/run.js
- 만들어진 로그 데이터 확인
$ cd /elastic-demos/seoul-metro-logs/data $ ls seoul-metro-2018.logs $ wc -l seoul-metro-2018.logs 2007500 seoul-metro-2018.logs
3. Logstash 를 이용해서 Elasticsearch 로 색인
Logstash 설정
- 출력테스트
$ cd elastic-demos/seoul-metro-logs/config $ vi seoul-metro-logs.conf ## 위에서 생성한 log 데이터 위치 정확히 기입 input { file { path => "/root/elastic-demos/seoul-metro-logs/data/seoul-metro-2018.logs" codec => "json" start_position => "beginning" sincedb_path => "/dev/null" } } filter { mutate { remove_field => ["host","path","@version"] } } output { stdout { } }
- Logstash로 log파일 elasticsearch에 전송하기 위한 설정 변경
$ cd elastic-demos/seoul-metro-logs/config $ vi seoul-metro-logs.conf ## 위에서 생성한 log 데이터 위치 정확히 기입 input { file { path => "/root/elastic-demos/seoul-metro-logs/data/seoul-metro-2018.logs" codec => "json" start_position => "beginning" sincedb_path => "/dev/null" } } filter { mutate { remove_field => ["host","path","@version"] } } output { #stdout { } # 환경변수 설정: # $LS_HOME/config/startup.options 또는 # $LS_HOME/bin/logstash-keystore elasticsearch { hosts => ["localhost:9200"] index => "seoul-metro-logs-2018" } }
- 전송
$ /usr/share/logstash/bin/logstash -f /root/elastic-demos/seoul-metro-logs/config/seoul-metro-logs.conf ## 실행 결과 Using JAVA_HOME defined java: /usr/lib/jvm/java-8-openjdk-amd64 WARNING, using JAVA_HOME while Logstash distribution comes with a bundled JDK WARNING: Could not find logstash.yml which is typically located in $LS_HOME/config or /etc/logstash. You can specify the path using --path.settings. Continuing using the defaults Could not find log4j2 configuration at path /usr/share/logstash/config/log4j2.properties. Using default config which logs errors to the console [INFO ] 2021-07-31 01:06:41.175 [main] runner - Starting Logstash {"logstash.version"=>"7.13.2", "jruby.version"=>"jruby 9.2.16.0 (2.5.7) 2021-03-03 f82228dc32 OpenJDK 64-Bit Server VM 25.292-b10 on 1.8.0_292-8u292-b10-0ubuntu1~18.04-b10 +indy +jit [linux-x86_64]"} [WARN ] 2021-07-31 01:06:41.540 [LogStash::Runner] multilocal - Ignoring the 'pipelines.yml' file because modules or command line options are specified [INFO ] 2021-07-31 01:06:42.695 [Api Webserver] agent - Successfully started Logstash API endpoint {:port=>9601} [INFO ] 2021-07-31 01:06:43.398 [Converge PipelineAction::Create<main>] Reflections - Reflections took 43 ms to scan 1 urls, producing 24 keys and 48 values [WARN ] 2021-07-31 01:06:44.604 [Converge PipelineAction::Create<main>] elasticsearch - Relying on default value of `pipeline.ecs_compatibility`, which may change in a future major release of Logstash. To avoid unexpected changes when upgrading Logstash, please explicitly declare your desired ECS Compatibility mode. [INFO ] 2021-07-31 01:06:44.696 [[main]-pipeline-manager] elasticsearch - New Elasticsearch output {:class=>"LogStash::Outputs::ElasticSearch", :hosts=>["//localhost:9200"]} [INFO ] 2021-07-31 01:06:45.180 [[main]-pipeline-manager] elasticsearch - Elasticsearch pool URLs updated {:changes=>{:removed=>[], :added=>[http://localhost:9200/]}} [WARN ] 2021-07-31 01:06:45.366 [[main]-pipeline-manager] elasticsearch - Restored connection to ES instance {:url=>"http://localhost:9200/"} [INFO ] 2021-07-31 01:06:45.563 [[main]-pipeline-manager] elasticsearch - Elasticsearch version determined (7.13.2) {:es_version=>7} [WARN ] 2021-07-31 01:06:45.565 [[main]-pipeline-manager] elasticsearch - Detected a 6.x and above cluster: the `type` event field won't be used to determine the document _type {:es_version=>7} [INFO ] 2021-07-31 01:06:45.784 [Ruby-0-Thread-10: :1] elasticsearch - Using a default mapping template {:es_version=>7, :ecs_compatibility=>:disabled} [INFO ] 2021-07-31 01:06:45.859 [[main]-pipeline-manager] javapipeline - Starting pipeline {:pipeline_id=>"main", "pipeline.workers"=>8, "pipeline.batch.size"=>125, "pipeline.batch.delay"=>50, "pipeline.max_inflight"=>1000, "pipeline.sources"=>["/root/elastic-demos/seoul-metro-logs/config/seoul-metro-logs.conf"], :thread=>"#<Thread:0x52f14142 run>"} [INFO ] 2021-07-31 01:06:46.981 [[main]-pipeline-manager] javapipeline - Pipeline Java execution initialization time {"seconds"=>1.12} [INFO ] 2021-07-31 01:06:47.296 [[main]-pipeline-manager] javapipeline - Pipeline started {"pipeline.id"=>"main"} [INFO ] 2021-07-31 01:06:47.392 [Agent thread] agent - Pipelines running {:count=>1, :running_pipelines=>[:main], :non_running_pipelines=>[]} [INFO ] 2021-07-31 01:06:47.488 [[main]<file] observingtail - START, creating Discoverer, Watch with file and sincedb collections
kibana에서 데이터 확인하고 전체 데이터 전송하기 위해 ctrl + c 로 전송 끊어줌
- Kibana에서 데이터 확인
명령어 한번 씩 쳐보면서 데이터 확인
- Remapping
1) 깔끔하게 보이기 위해
2) 인데스를 최적화 하기 위해
→ text 타입으로 저장하면, 저장 인덱스만 늘어나기 때문에 용량 차지 많고, 성능 떨어짐
- mapping 전, elastic search에 nori-tokenizer 사용
→ "홍대입구"라고 치면 데이터가 출력되는데 "홍대"라고 치면 데이터가 출력되지 않음
→ 이러한 문제를 보안하기 위해 nori-tokenizer 사용
- nori-tokenizer 설치 전, elastic search 설정 변경
##elasticsearch.yml에서 discovery설정 바꿔주어야 함 $ cd /etc/elasticsearch $ vi elasticsearch.yml # --------------------------------- Discovery ---------------------------------- # # Pass an initial list of hosts to perform discovery when this node is started: # The default list of hosts is ["127.0.0.1", "[::1]"] # discovery.seed_hosts: ["127.0.0.1", "[::1]"]
#wget으로 엘라스틱 서치 설치했을 때 default 경로 $ cd /usr/share/elasticsearch/bin $ ./elasticsearch-plugin install analysis-nori warning: usage of JAVA_HOME is deprecated, use ES_JAVA_HOME Future versions of Elasticsearch will require Java 11; your Java version from [/usr/lib/jvm/java-8-openjdk-amd64/jre] does not meet this requirement. Consider switching to a distribution of Elasticsearch with a bundled JDK. If you are already using a distribution with a bundled JDK, ensure the JAVA_HOME environment variable is not set. -> Installing analysis-nori -> Downloading analysis-nori from elastic [=================================================] 100% -> Installed analysis-nori -> Please restart Elasticsearch to activate any plugins installed #서비스 재시작 $ sudo systemctl restart elasticsearch.service
- Mapping & Template 생성
→ 단, search시에는 standard 사용
→ 복합어를 붙여서 검색하기 위해 shingle 사용
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" : { "ch" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "chc" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "en" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "jp" : { "type" : "text", "fields" : { "keyword" : { "type" : "keyword", "ignore_above" : 256 } } }, "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 } } } } } } } }
- reindex
?wait_for_completion=false 를 붙여주지 않으면 timeout 오류 발생
→ Reindex API 호출은 기본적으로 백그라운드 재색인 작업이 완료되기를 기다리지만, Kibana에서 이것을 호출하는 경우 Kibana는 30s표시 에서 요청을 중단하기 때문
POST _reindex?wait_for_completion=false { "source": { "index": "seoul-metro-logs-2018" }, "dest": { "index": "seoul-metro-logs-temp" } }
처리과정 확인
GET _tasks?actions=*reindex&detailed
- 토크나이저 적용 확인
GET seoul-metro-logs-temp/_search { "query": { "match": { "station.name.nori": "홍대" } } }
- 데이터 전송 확인
GET seoul-metro-logs*/_count
728x90'엔지니어링 > ELK' 카테고리의 다른 글
ElasticSearch → Kibana 특정 유저, 경로 지정하여 설치 (0) 2021.11.19 [Grafana] No date field named @timestamp found 오류 (0) 2021.08.12