Collo를 좀 더 알기쉽게 전달하기 위해 개요 및 샘플들을 제작하여 SlideShare에 공유 중입니다. 

 

이 글에서도 링크를 추가하여 계속 업데이트 하도록 하겠습니다.

 

https://www.slideshare.net/winninghabit/collo-01-kr

 

Collo -01 , kr

Collo를 소개합니다! https://github.com/blackwitch/Collo

www.slideshare.net

https://www.slideshare.net/winninghabit/collo-02-kr

불러오는 중입니다...

 

'개발 이야기 > Collo' 카테고리의 다른 글

Collo - slideshare 링크 공유  (0) 2019.11.05
Collo - 실시간 마이그레이션 툴  (0) 2019.09.30

한동안 회사에서 진행한 Data Warehouse(이하 DW) 및 통계시스템 구축이 최근 완료되었습니다. 

 

그리고 데이타를 저장한 Database(이하 DB) 종류, 저장된 로그의 파일 포멧 그리고 서버 위치도 다른 데이터들을 한 곳으로 손쉽게 모으기 위해 만들었던 천 줄 내외의 Javascript 코드를 정리해서 Collo라는 이름으로 github에 며칠 전 공개하게 되었습니다.

 

Collo의 주 목표는 실시간으로 누적되는 데이터를 해당 시스템의 부하없이 DW로 가져오는 것이었으며, 마이그레이션에 대한 모든 기능을 포함한 솔루션이 아닌 손쉽게 수정, 조작 가능한 작은 유틸리티 제작을 목표로 한 프로젝트였습니다. 그리고 데이타를 가져오는 성능 보다는 안정성에 더 중점을 두어 제작하였습니다. 그리고 유지보수를 위해서도 언제든, 누구든 분석하기 쉽게 가능한 작은 코드 수를 유지하려고 노력했습니다. 혼자 개발을 했기 때문에 개발 시간 단축, 각 저장소에 대한 안정된 연결을 유지하기 위해 공개된 npm들을 최대한 활용하였습니다.

 

제작 중간부터는 사용 가능한 저장소를 하나 둘 늘리면서 내가 다뤄보지 않은 저장소에 대해서도 지원하면 좋겠다는 생각을 했고, 이 작은 프로젝트가 같은 고민과 과제를 안고 있는 누군가에게 도움이 되었으면 했습니다. 이때부터 오픈소스로 공개를 목표로 하게 되었습니다. 게다가 관리툴의 UI도 엉망이어서 누군가의 도움을 받길 원했습니다. (첫번째 목표였을지도... -_-a)

 

이 카테고리에는 Collo에 대한 활용 방법, 예시들을 올릴 예정입니다. 

 

https://github.com/blackwitch/Collo

 

 

 

 

'개발 이야기 > Collo' 카테고리의 다른 글

Collo - slideshare 링크 공유  (0) 2019.11.05
Collo - 실시간 마이그레이션 툴  (0) 2019.09.30

(* 이 글은 NodeJS 10.15.1 을 기준으로 작성되었습니다. )

한국은 GMT +9 를 기준으로 시간을 사용하며, 데이터를 다룰때도 일반적으로는 GMT +9 기준을 사용합니다. 단일 솔루션만 다룰 때는 거의 신경쓰지 않겠지만, 이기종 혹은 여러 솔루션을 한번에 컨트롤 할 때는 상당히 거슬리는 문제가 됩니다. 특히 월드 와이드 서비스를 하고 있다면요. 대부분의 DB에서 UTC 기준으로 값을 저장하기 때문에 큰 문제는 없지만, 간혹 일부 npm에서 datetime 자료형을 다룰 때 미묘한 차이가 있습니다.

node-mysql(구분을 위해 임시로 node-를 붙였습니다)의 경우, 저장된 datetime 값을 그대로 가져오기 때문에 전혀 문제가 없는데 반해, node-mssql은 useUTC옵션을 제대로 설정하지 않으면 잘못된 날짜 정보를 가져오게 되어 문제가 발생합니다. 

예를 들면 아래와 같습니다. 
(아래 테스트는 DB : MSSQL/2008R2, MySQL/MariaDB10.4, npm package version : node-mssql/4.2.0, node-mysql/2.16.0 하에서 진행되었습니다.)

MSSQL과 MySQL의 어떤 테이블의 datetime 컬럼에 "2019-01-01 00:00:00" 라는 값을 넣어보겠습니다. 이 값은 UTC 기준으로 값이 저장됩니다. string 형태로 출력해보면 "Mon, 31 Dec 2018 15:00:00 GMT", 실제 저장된 값은 "15462684000000"입니다.

이 값을 MSSQL의 management studio, MySQL는 prompt 혹은 HeidiSQL 모두에서 현지 시간으로 보입니다. 즉 "2019-01-01 00:00:00"으로 보이게 됩니다.

이제 node-mssql/node-mysql로 값을 읽어보면 아래와 같은 결과가 나옵니다.

2019-01-01 09:00:00  (MSSQL NPM으로 읽어온 경우) 
2019-01-01 00:00:00  (MySQL NPM으로 읽어온 경우) 

node-mssql으로 얻은 결과값이 이상한 것을 보실 수 있습니다. node-mssql에는 useUTC 옵션이 있습니다. default 갑이 true로 되어 있어, DB의 datetime이 어디를 기준으로 되어 있던 무조건 UTC 기준값으로 인지합니다. 그래서 최종 출력시에는 Asia/Seoul 의 시간대인 +9시간이 되어 출력된 것이죠. 

 

connection에 사용되는 config값에 useUTC 옵션값을 추가하고 값을 false로 설정하면 현지 시간값으로 읽어옵니다. 사용에 주의 하세요. 

 


이상입니다. 좋은 하루 되세요. :)





JavaScript의 Date 객체는 아래의 특징을 가지고 있습니다. 다른 언어에서도 크게 다르지 않습니다.

- Date 객체는 UTC, 1970년 1월 1일 0시를 기준으로 하며, 밀리세컨트로 시간값을 기록합니다. 
- 만약 입력된 값이 유효하지 않다면 NaN값이 반환됩니다.
- 월은 0부터 시작하며 11이 12월이 됩니다.
- 요일은 0부터 시작하며 0이 일요일, 6일 토요일이 됩니다. 
* 크로스 브라우징 문제가 일부 있습니다. 작성 전 이에 대한 문제를 검토해 보시기 바랍니다. (https://www.google.com/search?q=js+date+cross+browser+format) 보다 상세한 내용은 이 곳(https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Date)을 참고하세요.

아래 예제에서는 JavaScript Date 객체 사용법을 알아보겠습니다.

var date1 = new Date('2019-01-01T00:00:00.000Z'); 
var date2 = new Date('2019-01-01T00:00:00'); 

console.log( "RAW DATE = ",date1, " / ", date2);  
// 결과 (1) : RAW DATE = 2019-01-01T00:00:00.000Z / 2018-12-31T15:00:00.000Z 
console.log( "toString = ",date1.toString(), " / ", date2.toString());  
// 결과 (2) : toString = Tue Jan 01 2019 09:00:00 GMT+0900 (GMT+09:00) / Tue Jan 01 2019 00:00:00 GMT+0900 (GMT+09:00)  

 

date1에는 UTC(협정 세계시, Coordinated Universal Time)로, date2에는 현지 시간을 입력했습니다. Date는 입력된 값을 분석하여 UTC로 값을 보관합니다. 

이를 그대로 출력하면 "결과 (1)"과 같이 UTC 기준으로 출력되는 것을 확인할 수 있습니다. Seoul은 시간은 UTC 기준 +9 시간이므로 UTC로 표현하게 되면 9시간 전의 시간으로 표기됩니다. 두 값을 특정한 조건이나 Date의 함수를 사용하지 않고 출력하면 UTC 기본값으로 출력됩니다. 그래서 date1은 입력값과 동일하게, date2는 9시간 전으로 출력되는 것을 확인할 수 있습니다.  

이제 문자열로 시간을 표현하기 위해서 toString 함수를 사용하니 "결과 (2)"과 같이 date1, date2 모두 현지 시간으로 출력되는 것을 확인할 수 있습니다. date1는 UTC+9에 맞춰 입력된 시간에 +9시간이 되어 출력되고, date2는 현지 시간으로 입력한 값이 그대로 보이는 것을 확인할 수 있습니다. 

그럼 시간 데이타에서 getHours()로 시간을 알아보면 어떻게 나오게 될까요?

var date1 = new Date('2019-01-01T00:00:00.000Z'); 
var date2 = new Date('2019-01-01T00:00:00'); 

console.log( "hours = ",date1.getHours() ," / ", date2.getHours() ); 
// 결과 (3) : hours = 9 / 0 
console.log( "hours = ",date1.getUTCHours() ," / ", date2.getUTCHours() ); 
// 결과 (4) : hours = 0 / 15 

getHours 함수는 현지 시간값을 기준으로 값을 반환하며, UTC 시간값을 기준으로 값을 반환받길 원한다면 getUTCHours 함수를 사용할 수 있습니다. setHours와 setUTCHours도 마찬가지입니다.

var date1 = new Date('2019-01-01T00:00:00.000Z'); 
var date2 = new Date('2019-01-01T00:00:00'); 

date1.setHours(9); 
date2.setHours(9); 

console.log( "DATE1 = ",date1 ," / DATE2 = ", date2 ); 
// 결과 (4) : DATE1 =  2019-01-01T00:00:00.000Z  / DATE2 =  2019-01-01T00:00:00.000Z 

setHours 함수는 첫 인자를 UTC가 아닌, 현지 시간의 시간값으로 인지합니다. 그러므로 당연히 내부에서도 시간값을 설정할 때 UTC가 아닌 현재 시간을 기준으로 주어진 시간값을 설정하게 됩니다. 

date1의 경우 현지 시간은 2019-01-01 09:00:00 이었으며, setHours에서 9시로 지정되었기 때문에 수정된 것은 없습니다. 그래서 출력하면 UTC 기준으로 2019-01-01T00:00:00.000Z 입니다.

date2의 경우 현지 시간은 2019-01-01 00:00:00 이었으며, setHours에서 9시로 지정되어 2019-01-01 09:00:00로 수정되었습니다. 그래서 출력하면 UTC 기준으로 2019-01-01T00:00:00.000Z 입니다. (결과 (1)에서는 2018-12-31T15:00:00.000Z 이었습니다.)


지금까지 JavaScript에서 Date 객체에 대해서 간단히 알아보았습니다. 


자주 사용하는 기능들 모아서 정리한 코드입니다. 


필요하신 분들은 가져다 그대로 사용하시면 됩니다. 



Date.prototype.mmddyyyy = function() {

      return (this.getMonth() + 1) +

      "/" +  this.padZero(this.getDate()) +

      "/" +  this.padZero(this.getFullYear());

};

Date.prototype.yyyymmdd = function() {

      return this.getFullYear()  +

      "/" +  this.padZero((this.getMonth() + 1)) +

      "/" +  this.padZero(this.getDate());

};

Date.prototype.mmddyyyytime = function(){

      return (this.getMonth() + 1) +

      "/" +  this.padZero(this.getDate()) +

      "/" +  this.padZero(this.getFullYear()) +

      " " + this.padZero(this.getHours()) + 

      ":" + this.padZero(this.getMinutes()) + 

      ":" + this.padZero(this.getSeconds());

};


Date.prototype.yyyymmddfordb = function() {

   var yyyy = this.getFullYear();

   var mm = this.getMonth() < 9 ? "0" + (this.getMonth() + 1) : (this.getMonth() + 1); // getMonth() is zero-based

   var dd  = this.getDate() < 10 ? "0" + this.getDate() : this.getDate();

   return "".concat(yyyy).concat("-").concat(mm).concat("-").concat(dd);

};


Date.prototype.yyyymmddtime = function(){

      return this.getFullYear()  +

      "/" +  this.padZero((this.getMonth() + 1)) +

      "/" +  this.padZero(this.getDate())+

      " " + this.padZero(this.getHours()) + 

      ":" + this.padZero(this.getMinutes()) + 

      ":" + this.padZero(this.getSeconds());

};

Date.prototype.yyyymmddstarttime = function(){

      return this.getFullYear()  +

      "/" +  this.padZero((this.getMonth() + 1)) +

      "/" +  this.padZero(this.getDate())+

      " " + "00:00:00";

};

Date.prototype.yyyymmddendtime = function(){

      return this.getFullYear()  +

      "/" +  this.padZero((this.getMonth() + 1)) +

      "/" +  this.padZero(this.getDate())+

      " " + "23:59:59";

};

Date.prototype.padZero =function (n) {

      n = n + '';

      return n.length >= 2 ? n : new Array(2 - n.length + 1).join('0') + n;

}



var util_date = {

      getToday_yyyymmdd: function(){

      var today = new Date();

      return today.yyyymmdd();

      },

      get_yyyymmdd:function(_date){

            if( _date == "" || _date == null || _date == undefined ){

                  return null;

            }else{

                  var date = new Date(_date);

                  return date.yyyymmdd();

            }

      },

      getToday_yyyymmddtime:function(_date){

            if( _date == "" || _date == null || _date == undefined ){

                  return null;

            }else{

                  var date = new Date(_date);

                  return date.yyyymmddtime();

            }

      },

      get_yyyymmddtime:function(_date){

            if( _date == "" || _date == null || _date == undefined ){

                  return null;

            }else{

                  var date = new Date(_date);

                  return date.yyyymmddtime();

            }

      },

      getThisMonday: function(_date){

            var d = new Date(_date);

            var day = d.getDay(),

            diff = d.getDate() - day + (day == 0 ? -6:1); // adjust when day is sunday

            return new Date(d.setDate(diff));

      },

      getLastMonday: function(_date){

            var d = this.getThisMonday(_date);

            return this.getNextDay(d,-7);

      },

      getNextMonday: function(_date){

            var d = this.getThisMonday(_date);

            return this.getNextDay(d,7);

      },

      getNextDay: function(_date, _count){

            var d = new Date(_date);

            d.setDate(d.getDate() + _count);

            return new Date(d);

      },

      getMonday:function(d, weeksago) {

            var day = d.getDay(),

            diff = d.getDate() - day + (day == 0 ? -6:1); // adjust when day is sunday

            var monday = new Date(d.setDate(diff));

            monday.setDate( monday.getDate()- 7*weeksago);


            return monday;

      },

      getFirstday: function(d){

            var t = new Date(d);

            return new Date(t.getFullYear(), t.getMonth(), 1);

      },

      getFirstdayOfLastMonth: function(d){

            var t = this.getLastdayOfLastMonth(d,0);

            return new Date(t.getFullYear(), t.getMonth(), 1);

      },

      getLastdayOfLastMonth: function(d, monthsago){

            var t = new Date(d);

            t.setMonth( t.getMonth()-monthsago);

            t = new Date(t.getFullYear(), t.getMonth(), 0);


            return t;

      },

      getLastdayOfThisMonth:function(d){

            var t = new Date(d);

            return new Date(t.getFullYear(), t.getMonth() + 1, 0);

      },

      getFirstdayOfNextMonth: function(d){

            var t = this.getLastdayOfThisMonth(d);

            return this.getNextDay(t, 1);

      },

      getDiffDays: function(f , s){

            var date1 = new Date(f);

            var date2 = new Date(s);

            var timediff = Math.abs( date1.getTime() - date2.getTime() );

            return Math.ceil( timediff/(1000*3600*24));

      },

      bThisMonth: function ( d ){

            var today = new Date();

            var date1 = new Date(d);

            if(today.getFullYear() == date1.getFullYear() && today.getMonth() == date1.getMonth())

                  return true;


            return false;

      },

      bToday: function ( d ){

            var today = new Date();

            var date1 = new Date(d);

            if(today.getFullYear() == date1.getFullYear() && today.getMonth() == date1.getMonth() && today.getDay() == date1.getDay())

                  return true;


            return false;

      },

      bThesedays: function(d){

            var today = new Date();

            var date1 = new Date(d);


            var diff = this.getDiffDays( today, date1);

            if(diff < 5)

                  return true;


            return false;

      }

};


module.exports = util_date;


다음과 같은 순서로 작성되어 있습니다.


1. 개요

2. 준비

3. 쿼리 알아보기

4. nodejs와 통합 

5. chart로 표현하기


1. 개요


최근 회사에 Elasticsearch(이하 ES)로 통계 시스템을 구축했습니다. Mysql MyISAM 엔진으로 구축하던 것과 비교해보면 엄청나게 편리해졌네요.


Kibana로 쿼리하고 결과를 손쉽게 출력하여 담당자가 아닌 컨텐츠 개발자도 자신이 보고 싶은 결과를 바로 추가하여 볼 수 있을 정도니, 작은 개발사에는 이보다 더 좋은 솔루션이 있을까 싶네요.


그런데 ES에서 나온 다른 두 결과의 비교가 필요한 경우 불가능한 경우가 있어서 조금 아쉽더군요. 그래서 이를 직접 만들어 보기로 했습니다.


시간을 기준으로 데이터를 비교하는 경우는 timelion을 사용하면 되나, 시간 기준이 아니거나 두 결과의 상세 비교, 예를 들어 지난 주 매출과 이번 주 매출을 비교하여 등락을 정확히 표기하고 싶은 경우에는 CSV로 데이터를 export하여 엑셀 등에서 별도 그래프 작업이 필요했습니다.


그래서 ES에 쿼리를 날려 결과를 가져 온 후 회사 자체에서 사용하는 홈페이지에 있는 리포트 기능과 연동하면 좋겠다라는 생각을 하게 되었습니다. nodejs로 Elasticsearch에서 쿼리를 통해 결과를 얻어와 어딘가 저장하고, 이 결과를 기존의 결과와 비교하여 내가 원하는 그래프, 표로 출력하도록 하는 것이었죠.


만들면서 다른 것들은 큰 문제가 없었지만, elasticsearch에서 사용하는 쿼리 포멧에 대한 정보를 찾기가 어려워 조금 애를 먹었습니다. 이를 기록하여 메뉴얼로 만들어 두고, 공유하면 좋겠다는 생각이 들어 정리를 해봤습니다.


사용된 상세 솔루션, 기술 스택은 아래와 같습니다.


Elasticsearch( Saving raw data )

aws-cognito( Authentication )

aws-s3( Saving search results )

nginx/javascript/jquery/chart.js  ( Front-end interface )

Node.js/Express ( Rest API )


아래 글에서는 ELK를 이용한 시스템이 운영중인 것을 기준으로 Node.js로 Elasticsearch에 어떻게 쿼리를 하는지에 대해서 기술하였습니다


시스템은 아래와 같이 구축되었습니다.




(1) 리포트 사이트에서 정보를 요청한다. (필요하다면 cognito의 사용자 풀을 이용하여, 인증을 처리하고, 사용자별 권한을 나눌 수 있습니다.)

(2) 우선 S3에 저장된 요청 정보가 있는지 확인한다. (한번 읽어온 정보는 최대 100MB 공간을 마련해서 S3에서 다시 읽을 필요없도록 저장하고 있는다. 최대 용량 초과 시 참조하지 않는 결과부터 삭제). 원하는 결과가 있을 경우 바로 (5)번으로.

(3) S3에 저장된 결과가 없다면 ES에 직접 쿼리한다. 

(4) (3)에서 얻어온 결과를 S3에 저장하고, 메모리에도 저장해둔다.

(5) 검색된 결과를 돌려준다.



이 글에서는 (3)번에 대한 내용을 집중적으로 다루도록 하겠습니다. 다른 내용에 대해 필요한 분은 댓글로 남겨주세요.


브라우저에서 직접 쿼리를 실행할 수도 있겠지만, Caching, Security 등의 이슈가 있어 실제 쿼리를 담당하는 부분은 REST API 형태로 제작하였습니다.


2. 준비


동적 웹 페이지 작성 : jquery(https://jquery.com/)

결과물 출력 : chart.js(http://www.chartjs.org/)

AWS (인증 및 저장소) : https://aws.amazon.com/ko/sdk-for-node-js/ , https://aws.amazon.com/ko/sdk-for-browser/ (참고 사항으로 이 글에서는 다루지 않습니다.)

elasticsearch의 공식 nodejs client library는 아래 사이트들에서 확인이 가능합니다. 

github : https://github.com/elastic/elasticsearch-js

npm site: https://github.com/elastic/elasticsearch-js

공식 문서: https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html



위 라이브러리를 사용하여 아래와 같이 elasticsearch에 연결할 수 있습니다. (npm site 예제 인용)

var elasticsearch = require('elasticsearch');
var client = new elasticsearch.Client({
  host: 'localhost:9200',
  log: 'trace'
});

ping 함수를 사용하여 연결 여부를 테스트 할 수 있습니다. (npm site 예제 인용)

client.ping({
  // ping usually has a 3000ms timeout
  requestTimeout: 1000
}, function (error) {
  if (error) {
    console.trace('elasticsearch cluster is down!');
  } else {
    console.log('All is well');
  }
});



(아래 내용은 elasticsearch 6.3을 기준으로 작성되어 있습니다.)
3. 쿼리 알아보기


이제 이 라이브러리를 이용해서 우리가 원하는 결과를 어떻게 쿼리할 수 있는지 알아보겠습니다.


주로 다룰 함수는 search입니다. 다른 기능들은 https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html 를 참고하세요.


search 함수에 대한 상세 내용은 아래 링크에서 확인할 수 있습니다.

https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/api-reference.html#api-search 


쿼리를 요청하는 방법은 두 가지가 있습니다. parameter에 검색어를 넣어 요청하거나 Elasticsearch가 제공하는 Json 형태의 Query DSL(Domain Specific Language)를 사용하는 방법입니다. 여기서는 Lucene Query 방법을 간단히 확인 후 Query DSL을 이용하여 원하는 결과를 얻어오는 방법을 다뤄보겠습니다.


만약 raw data의 종류를 log_index라는 키를 이용해 구분한다고 가정하고, 1000번인 데이터만 검색하고 싶다고 가정해 보겠습니다.


nodejs에서는 search 함수를 다음과 같이 사용합니다. 


[Lucene query 문을 사용한 방법]

const response = await client.search({
  index: 'myindex',
  q: 'title:test'
});


virtualize와 Timelion 등 전반적으로 Kibana에서는 Lucene query를 사용합니다. 하지만, 상세한 필터 적용, 조건 처리를 위해서는 Query DSL이 더 유용합니다.


[Query DSL을 사용한 방법]

client.search({
index: logstash-2018.10.01,
body: {
"query": {
"match" : { "log_index" : "1000"}
}
}
});

이를 nodejs에 적용하기 전에 body의 내용을 Kibana 의 Dev Tool에서 확인할 수 있습니다.


POST logstash-2018.10.01/_search?size=0
{
index: logstash-2018.10.01,
body: {
"query": {
"match" : { "log_index" : 1000}
}
}
});

size는 출력할 결과의 수이며 default는 10입니다. 0으로 지정하면 결과의 정보만 볼 수 있습니다. 아래와 같은 결과를 확인할 수 있습니다.


{ "took": 253,           //    ms 단위로 결과를 수행한 시간 "timed_out": false,        "_shards": { "total": 1000,       //    조건에 맞는 검색된 총 개수 "successful": 1000, "skipped": 0, "failed": 0 }, "hits": { "total": 100000000,    //    검색한 총 개수 "max_score": 0, "hits": [] } }

size를 원하는 만큼 지정하면 hits에 배열 형태로 포함되어 출력됩니다.


테스트 결과를 확인하는 방법을 알아보았으니, 이제 필요한 Query DSL에 대해서 좀 더 살펴보겠습니다.


아래 document에서 좀 더 상세한 내용을 확인할 수 있습니다.


https://www.elastic.co/guide/en/elasticsearch/reference/6.3/search-request-body.html

https://www.elastic.co/guide/en/elasticsearch/reference/6.3/query-dsl.html


이 중 몇 가지 샘플을 통해서 사용법을 알아보겠습니다.


예제) 특정 시간대의 데이터만 검색하기


query:{

"bool": {

"must" : [

{"match" : {"log_index" : 1000}}

],

"must_not": [

{"match" : {"item_type" : 100}},

{"query_string": {"fields":["name"],"query" : "tester OR manager"}}

],

"filter": {

"range" : {

"@timestamp" : {

"gte" : "2018-09-01",

"lte" : "2018-09-30"

}

},

}

}

}

"9월 한달 간 로그 종류가 1000번 이고, 아이템 종류가 100번이 아니고, name 필드에 tester나 manager가 포함되지 않은 데이터를 검색"하는 예제입니다.


위 쿼리에서 볼드체로 처리된 키워드들을 살펴보겠습니다. 


bool : 다른 쿼리들에 공통적으로 일치하는 document를 검색합니다. Lucene의 BooleanQuery와 같은 역할을 합니다.

must, must_not : 일치하거나 배치되는 조건을 설정합니다.

match : 정확히 일치하는 조건을 설정합니다. 이와 유사한 키워드는 한 단어를 검색할 때 사용하는 term이 있는데, 이는 주어진 조건의 단어가 포함된 것 전체를 검색하게 됩니다.

query_string : query_string의 query는 조건 검색을 지원합니다. (링크)

filter : must와 비슷하지만, score[각주:1]를 고려하지 않습니다.

range : 검색 범위를 저장할 수 있습니다.



예제) 기간 별 count 확인하기


query:{

"bool" : {

"must" : [

{"match" : {"log_index" : 2000}}

],

"filter": {

"range" : {

"@timestamp" : {

"gte" : "2018-09-01",

"lte" : "2018-09-30"

}

},

}

},

"aggs" : {

"days" : {

"date_histogram":{

"field" : "@timestamp",

"interval" : "day

},

"aggs" : {

"type_count":{

"cardinality" : {

"field": "login"

}

}

}

}

}

}


"9월 한 달간 일별 login한 횟수를 집계"하는 예제입니다. Aggregations에 대해서는 이 곳에서 상세한 정보를 확인할 수 있습니다. 


aggs : 집계에 대한 쿼리를 정의합니다. "days"는 집계 정보에 대한 사용자 지정 이름으로, 원하는 이름을 지정할 수 있습니다.

date_histogram : 시간을 기록한 @timestamp를 기준으로 일별 지표를 집계합니다. 집계된 정보는 "aggregations"하위에 buckets에 날짜별로 doc_count에 기록되어 결과를 얻을 수 있습니다.

cardinality : 공식 문서에 'approximate count' 라는 부연 설명이 있는데, 정확한 결과를 얻기에는 리소스가 많이 필요해 HyperLogLog++ algorithm에 기반하여 결과를 도출한다고 되어 있습니다. 보다 상세한 내용은 이 곳 을 참고 바랍니다.



4. nodejs에 통합


Elasticsearch로부터 원하는 결과를 얻었다면 이제 이 결과를 분석해서 웹 사이트에 표로 출력을 해보겠습니다. 


위에서 보았던 elasticsearch 공식 nodejs client library를 초기화하는 코드입니다.


var elasticsearch = require('elasticsearch');
var client = new elasticsearch.Client({
  host: 'localhost:9200',
  log: 'trace'
});

ping 함수를 사용하거나 테스트 쿼리를 날려 잘 접속되는지 확인해 보세요. 


이제 위에서 언급했던 "특정 시간대의 데이터만 검색하기" 결과를 받아 그래프를 그려보겠습니다. 


아래 코드는 Elasticsearch에 쿼리하여 원하는 데이터를 검색하고 결과를 가공하는 REST API에서 호출될 예제 함수들입니다. 


.
.
app.route('/test/query').post(function(req, res, next){
req.accepts('application/json'); testQuery(req.body.sday, req.body.eday).then((result) => { res.json('{"result":"'+JSON.stringify(result)+'"}'); // 결과를 클라이언트에 돌려줍니다.  }).catch((err) => { res.json('{"err":"'+ err + '"}'); }); })

.
.

function testQuery(startDate, endDate){
return new Promise(resolve => {
var listRawData = {};
var count =0;
.
.
(미리 caching했던 결과물이 있다면 search전체 읽어 결과를 바로 돌려줍니다. )
.
.
client.search({
index : 'logstash-2018.10.01',
scroll: '2s',
body : {

query:{

"bool": {

"must" : [

{"match" : {"log_index" : 1000}}

],

"must_not": [

{"match" : {"item_type" : 100}},

{"query_string": {"fields":["name"],"query" : "tester OR manager"}}

],

"filter": {

"range" : {

"@timestamp" : {

"gte" : startDate,

"lte" : endDate

}

}

}

}

}

}
}, function getMoreUntilDone(err, res){

// listRawData res.hits.hits의 내용을 적절히 쌓아둡니다.

count += res.hits.hits.length; // listRawData 에는 중복된 데이터가 발생할 수 있으니 카운트를 별도로 처리
if(count != res.hits.total){
client.scroll({    //    아직 total count에 도달하지 않았다면 다음 데이터를 받아옵니다.
scrollId: res._scroll_id,
scroll:'2s'
}, getMoreUntilDone);
}else{
var result=[];

//    listRawData 의 결과를 가공하여 result에 넣어두세요.
//    이 예제에서는 day와 value, value2 형태로 데이터를 가공하여 저장했다고 가정합니다.
//    가공된 결과를 s3, 별도의 저장소 혹은 cache에 저장하여 활용할 수 있습니다.
resolve(result); // 가공된 데이터를 돌려줍니다.
}
});
});
}


5. Chart로 출력하기


이제 클라이언트측에서 받은 결과를 chart로 출력하는 과정을 살펴보겠습니다.


function drawResult(mainDiv){ // 그래프를 그릴 div를 전달받습니다.
$.post("http://localhost/test/query", {'sday':'2018-09-01', 'eday':'2018-09-30'}, function(res){
res = JSON.parse( res );
var labels = []; // 그래프에 표기할 label을 넣습니다. 이 예제에서는 날짜 데이터가 들어갑니다.
var values =[]; // 그래프에 표기할 값을 넣습니다.
var values2 =[];

res.result.forEach(function(data){
labels.push( data.day );
values.push( Number( data.value));
values2.push( Number( data.value2));
});

var div = document.createElement('div'); // 추후 삭제를 위해 sub div 하나를 추가합니다.
div.classList.add('chart-container');
var chart = document.createElement('canvas');
div.appendChild(chart);
mainDiv.appendChild(div);

//    이하는 chart.js를 이용해 그래프를 설정하는 샘플입니다.
var ctx = chart.getContext('2d');
var lineChart = new Chart(ctx, {
type: 'line',
data : {
labels : labels,
datasets : [{
label : "Sample Graph",
borderColor: 'rgb(255,99,132)',
data : values
},{
label : "Sample Graph 2",
borderColor: 'rgb(99,255,132)',
data : values2
}]
},
options: {
responsive : true
}
});
});
}

출력된 샘플 결과는 아래와 같습니다.





지금까지 Elasticsearch를 nodejs에 통합하여 결과를 우리가 원하는 곳에 출력하는 전반적인 과정을 살펴 보았습니다.


다음에 요청이 있거나 다른 기회가 있다면, 각 단계의 상세 과정을 다시 정리할 수 있는 기회가 있으면 좋겠습니다.


짧지 않은 글, 읽어 주셔서 감사합니다. 





  1. 얼마나 일치하는지를 나타내는 수치이다. 예를들어 우리가 "tester"라는 단어를 검색했다면 "tester"라고 된 document의 score 는 1일 것이고, "tester 1"이라고 된 document는 0.9 정도가 될 것이다. [본문으로]

javascript로 aws cognito 연동 할 때, app 설정에서 "클라이언트 보안키 생성" 옵션을 해제해야 한다.


https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js


"When creating the App, the generate client secret box must be unchecked because the JavaScript SDK doesn't support apps that have a client secret."


클라이언트 보안 옵션은 기본으로 체크 되어 있어서 생각없이 넘어 갔는데, 켜져 있으면  "Unable to verify secret hash for client .." 라는 에러는 보게된다.

앱 클라이언트 첫 설정할 때 주의 필요!!!



(windows 환경)


nodejs에서 80포트 열어 쓰던 어플이 있는데, 어느날 갑자기


error: listen EACCES 0:0:0:0:80 에러를 뱉으면서 안된다. --a


netstat -ano 하면 ip,port 그리고 해당 포트를 사용하는 PID를 볼 수 있다. 


찾아보니 4 ... System이 쓰고 있다고. -- ㅁ.... 모지..


이래저래 찾아보니.. 최근에 로컬에 mssql을 설치했는데.. 


SQL Server Reporting Services가 내부적으로 80포트를 쓰고 있었네 --;


서비스 중지 시키니 잘 돌아감. 


휴 ~ 



방법을 몰라서 인터넷을 찾아보니 사람마다 다 방법이 다르다. --;;


var old = new Date ( 특정일 );

var now = new Date();

var gap = new Date( 0,0,0,0,0, now- old);


이렇게 처리한 분도 있던데.. 내가 원하는 값은 아니었고.. 


시간, 분, 초, ms 차이를 알고 싶었기에.. 이거저거 뒤져보다.. 

getTime 인터페이스가 있어 사용해보니.. OK!!


var old = new Date ( 특정일  or 시간);

var now = new Date();


var gap = now.getTime() - old.getTime();

var sec_gap = gap / 1000;
var min_gap = gap / 1000 /60;

이런 식으로 처리 가능!!

관련 코드 업데이트 : 여기로.. 

 


  1. 조조나리 2016.03.10 16:46

    var now = new Data(); - > var now = new Date();
    오타입니다.


클라이언트는 브라우저, 서버는 nodejs로 가정.


업데이트 진행 과정을 확인할 수 있도록 만들었다.


브라우저에서는 아래와 같은 코드로 작업을 하면 된다. 스트림으로 보낼 파일을 지정하고, 해당 서버로 소켓 연결해서 보낸다. 'data' 메세지를 통해 현재 업데이트된 사이즈를 확인할 수 있다.

var fn =$("input[type=file]")[0].files[0]; // input 폼을 이용해서 지정된 파일 확인

var AllSize = fn.size; // 진행 사항을 체크하기 위한 파일 전체 사이즈

var UploadedSize = 0; // 현재 업로드된 용량

var socket = io.connect('localhost:8765');

var stream = ss.createStream();

ss(socket).emit('upload', stream, {path: tarpath, name: fn.name, size: fn.size});

var blobstream = ss.createBlobReadStream(fn);

blobstream.on('data',function(chunk){

UploadedSize += chunk.length;

      console.log(' updated >> ' + UploadedSize + '/' + AllSize );

});

blobstream.pipe( stream );


서버에서는 upload 메세지로 전송되는 데이타를 받아 지정된 파일로 저장한다.

(*path.basename 는 지정된 경로명을 제외하고 파일명만 추려낸다.)

var io = require('socket.io').listen(8765);

var ss = require('socket.io-stream');

var path = require('path');


io.sockets.on('connection', function(socket){

ss(socket).on('upload', function(stream,data){

var fn = path.basename(data.name);

stream.pipe(fs.createWriteStream(fn));

});

});


필요해서 만들어 봄. -0-



  1. 진이 2015.05.19 15:27

    간략한 소스만 있어서 이해가 잘 되질 안네요.

    전체적인 소스를 보여주실순 없나요~?

    • 가끔.하늘 가온아 2015.05.27 11:37 신고

      댓글을 지금에서야 봤네요.

      이 샘플은 "https://github.com/blackwitch/Coconut" 을 개발하면서 올렸던 글입니다.

      해당 프로젝트에서 파일을 전송하는 부분이 있는데 그걸 참고하시면 됩니다. ^^

      Web -> Manager -> Agent로 파일이 전달되기에서 웹에서 nodejs로 nodejs에서 다른 nodejs로 전달되는 모든 과정이 구현되어 있습니다.

      manager와 Agent의 app 하단 부분을 보시면 됩니다.

      도움 되시길 바랍니다.

+ Recent posts