It may not be safe to bootstrap the cluster from this node. It was not the last one to leave th..

mariadb 테스트 중에 위와 같은 오류가 나서 찾아봤다. 


3대에 galera 설정해서 클러스터 구성하여 사용 중이었다. 


centos7 mariadb 10 사용 중.


다른 서버들은 모두 정상 가동 중이었고, 한 대만 문제가 발생한 상태.


netstat -na | grep 3306으로 포트를 확인하니 죽어있고 


systemctl start mariadb로 재가동이 안되어


systemctl status mariadb.service로 확인하니 위와 같은 에러가 발생한 상태.


설치된 폴더 (/var/lib/mysql)로 가서 


galera.cache, grastate.dat 파일을 삭제 후 재시작하니 정상 가동됨. 


두 파일은 시스템이 갑자기 문제가 생기면서 mariadb 프로세스가 죽으면서 미처 삭제 못한 temp 파일 인 듯.



https://yuddomack.tistory.com/entry/1React-Native-%EC%84%A4%EC%B9%98%EC%99%80-%EC%8B%A4%ED%96%89hello-world?category=754156 (이하 링크 문서) - 이 링크에 너무 잘 정리되어 있어 따라하면서 배우는 중입니다. (글쓴이님. 감사합니다. :)


https://wix.github.io/react-native-navigation/#/docs/Installing (이하 공식 문서)


아래 내용은 다음과 같은 환경에서 진행된 내용입니다. 

OS 

 macOS Mojave 10.14.1

 Android Studio 

 3.3.1

 XCode 

 10.1

 nodejs 

 10.13.0

 react 

 16.6.3

 react-native

 0.58.6

 react-native-navigation

 2.13.1



일단 링크문서의 1~5 chapter까지는 좋은 가이드가 되니 꼭 참고하시기 바랍니다.


chapter 6에서는 react native navigation을 다루는데, 최신 버전에서 변경된 내용들이 있어 별도로 정리를 해둡니다.


react-native-navigation을 설치하고 나면 low severity vulnerabilities에 대한 경고를 출력하고 npm audit fix(npm v6)를 실행하라는 안내가 나올 수 있습니다.


이를 실행하면 react-native-navigation을 사용하기 위해 필요한 몇몇 module의 버전을 조정하는 작업이 진행됩니다.


Android/iOS 세팅은 공식문서의 내용 중 수동 링크를 기준으로 정리하며 부족한 샘플 코드를 추가하였습니다. 왜 수동 링크 작업을 해야 하는지는 링크 문서에 잘 나와 있으니 참고하시기 바랍니다.


[RNN iOS 세팅]


우선 링크 문서 6장 의 내용을 참고하여 xcode에 react native navigation을 추가합니다. 공식문서보다 이 링크가 좀 더 명확히 정리되어 있습니다.


AppDelegate.m을 수정할 때 아래와 같은 에러가 발생할 수 있습니다.


 React/RCTBundleURLProvider.h file not found

 Use of undeclared identifier 'rootView'


이와 관련된 해결책은 공식 문서에 있으며, XCode에서 프로젝트를 선택 후, 메뉴 Product >> Scheme 에서 react native navigation을 선택하면 사라집니다.


링크 문서에서는 공식 문서와는 달리 RCCManager.h를 사용하도록 코드가 되어 있는데, 이전 버전의 내용이니, 아래 내용을 참고하시면 됩니다.


** 중간에 여러 코드 및 세팅을 테스트를 위해 수정하면서 Systrace.setEnable  관련된 에러가 ios 에뮬레이터에서 발생. 검색해보니 방법없으니 다시 설치하라는 내용만 있어 프로젝트 삭제 후 다시 설치. -0-a


관련 코드들은 공식 문서의 내용으로 적용하면 됩니다. 


(( AppDelegate.m ))


#import "AppDelegate.h" #import <React/RCTBundleURLProvider.h> #import <React/RCTRootView.h> #import <ReactNativeNavigation/ReactNativeNavigation.h> @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; [ReactNativeNavigation bootstrap:jsCodeLocation launchOptions:launchOptions]; return YES; } @end


XCode를 위한 세팅은 이것으로 끝입니다.


[RNN Android 세팅]


** 간간히 이런 작업들을 할 때 느끼는 거지만 아래 링크 문서의 샘플 코드에서 rootProject.projectDir 처럼, 저런 정의된 값을 어디서 찾아야 하는지 난감할 때가 많다. 기존에 android studio를 많이 다뤄 보신 분들은 쉽게 알 수도 있을 듯 한데, 처음 볼 때는 뜬금없어서.. 특히 javascript 프로젝트 할 때는 predefined keyword가 아닌 것들이 (intelligence를 지원하지 않는 ide에서) class의 public 맴버처럼 접근하는걸 볼 때마다 이게 어디에 정의되어 있는지, 올바르게 사용된건지 확인하기가 만만치 않은 듯 합니다. 자꾸 하다보니 그러려니 하긴 하지만...


아래 네 개의 gradle 파일을 수정할 때는 공식 문서를 참고하고, 자신의 android studio 버전에 따라 문서의 내용과 다를 수 있으니, 그에 맞추어 수정해야 합니다.

(( android/settings.gradle ))

(( android/gradle/wrapper/gradle-wrapper.properties ))

(( android/build.gradle ))

(( android/app/build.gradle ))


아래 두 코드를 공식 문서를 참고하여 수정 합니다.


(( MainActivity.java ))

(( MainApplication.java ))


그 다음으로 React Navtive 의 버전을 타겟팅하는 설정이 android/app/build.gradle에 추가해야 합니다. 별도 항목으로 제외된 것을 보면 중요한 항목인 듯 하네요. package.json 파일을 확인하니 제가 사용하는 버전은 0.58.6인 상태입니다.


저는 missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5"로 설정을 추가 했습니다. 


공식 문서의 내용 1~8가지 따라하면서 주의할 점은 app/build.gradle의 dependencies를 추가할 때 특정 버전 대신 "${rootProject.ext.supportLibVersion}"을 설정하세요. 버전이 맞지 않아 빌드 실패 하는 경우가 종종 있었습니다. 

(https://github.com/wix/react-native-navigation/issues/3419 를 참고하세요.)







mariadb와 galera를 사용하여 클러스터 구성하는데 두 번째 node부터 실행이 안되서 찾아보니 포트가 다 안열린 것!


http://galeracluster.com/documentation-webpages/firewallsettings.html


3306 : mysql client connections 을 위한 포트

4567 : UDP와 TCP 둘 다 사용하는 Galera Cluster  복제 트래픽, 다중 복제를 위한 포트

4568 : Incremental State Transfer(변경된 상태 전송) 를 위한 포트

4444 : State Snapshot Transfer(전체 상태 전송) 를 위한 포트



Donor (제공자) : 원래 데이터를 가진 기준 노드

Joiner (가입자) : Donor로부터 데이터를 제공받는 노드


Donor에 Joiner가 최조로 접속하면 SST를 통해 전체 복제가 진행된다. 이후 IST를 통해 변경된 데이터들이 전송된다.


기존적으로 rsync를 사용하는데 속도는 빠르지만 Donor의 blocking 시간이 길어 병목이 발생할 수 있다. 

그 외 mysqldump(이름의 뉘앙스처럼.. 엄청 느림!!)와 xtrabackup(속도가 빠르지는 않지만 Donor의 blocking 시간을 줄여준다.)이 있다.



빌드 에러가 다음과 같은 경우...

 

/**


CommandInvokationFailure: Gradle build failed. 

...

...


stderr[


FAILURE: Build failed with an exception.


* What went wrong:

A problem occurred configuring root project 'gradleOut'.

> Failed to install the following Android SDK packages as some licences have not been accepted.

     build-tools;28.0.2 Android SDK Build-Tools 28.0.2

  To build this project, accept the SDK license agreements and install the missing components using the Android Studio SDK Manager.

  Alternatively, to transfer the license agreements from one workstation to another, see http://d.android.com/r/studio-ui/export-licenses.html

  

  Using Android SDK: somewhere... 


* Try:

Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.


* Get more help at https://help.gradle.org


BUILD FAILED in 1s

]

stdout[

WARNING: The specified Android SDK Build Tools version (28.0.0) is ignored, as it is below the minimum supported version (28.0.2) for Android Gradle Plugin 3.2.0.

Android SDK Build Tools 28.0.2 will be used.

To suppress this warning, remove "buildToolsVersion '28.0.0'" from your build.gradle file, as each version of the Android Gradle Plugin now has a default version of the build tools.

...

...

Warning: License for package Android SDK Build-Tools 28.0.2 not accepted.

]

exit code: 1

...

...



**/


볼드체로 된 부분을 참고.


이런 에러가 나면 google play licensing library가 잘 설정되어 있는지 확인해야 한다. 


지원하는 안드로이드 버전에 따라 rev 2 이전의 라이브러리가 필요하기도 하다. 


나도 이번에 1  버전이 설치가 되어 있지 않아 계속 위와 같은 에러가 발생. 


라이브러리 1을 재설치 후 정상 빌드가 되는 것을 확인했다.





며칠 전 데이타 손실이 발생하여 내용을 공유하기 위해 기록합니다. 


일단 구축한 시스템은 사내에 ES로 시스템을 구축해두고, 각 컨텐츠 별로 REST API 서버  + Redis로 중간 저장소를 만들어 둔 상태입니다. 그리고 사내의 세팅된 Logstash에서 방화벽이 열리는 시간에 컨텐츠 별 중간 저장소로 접속해 그동안 쌓여있는 로그를 가져오도록 되어 있습니다. 


이렇게 세팅된 이유는 Node 별 초기/유지 비용을 줄이고 (사내에서 PC로 시스템을 구축해 둠. 서버와 비슷한 성능의 시스템을 저렴하게 구축할 수 있으며, IDC의 상면비 등을 절감할 수 있기 때문입니다. 문제 발생 시 접근성도 용이합니다.), 운영자들의 접근 속도도 높이기 위함입니다. ES외에도 리포트 사이트를 구축하여 Kibana로는 결과를 만들 수 없는 리포트를 원하는 대로 만들 수 있도록 구축해 두었습니다. 


문제는 주말이 지난 후에 발생했습니다. 


월요일 오전이면 주말 동안 가져오지 않고 쌓여 있던 데이타들을 한번에 가져오게 됩니다. 평소에 별 이상이 없었는데, 이날은 Master Node 하나가 다운되면서 발생했습니다. 가끔 다운되는 경우가 있었기에 (과도한 양의 쿼리를 하는 등으로 메모리 초과되면서 다운되는 경우가 종종 있었습니다.), 이날도 그대로 node를 기동해 두었죠. 


그런데 운영자들로부터 몇몇 데이타들이 검색되지 않는다고 보고를 받게 됩니다. 어차피 지표를 보기 위한 데이타이기에 손실이 일부 있다고 큰 문제는 되지 않지만, 이 시스템을 통해 매출 리포트를 자동화 시켜두어, 수동 매출 리포트와 결과가 틀리다는걸 바로 듣게 되었습니다. 


살펴본 결과, 실제 documents의 수가 평소 주말보다 30~50% 정도 적은 것을 확인했습니다. Facebook elasticsearch user group에 문의한 결과 아래와 같은 원인을 알게 되었습니다. 


- node를 재가동하면 shard initialize를 진행하는데, GET _cat/indices?v 로 인덱스들의 상태를 살펴보면 green이 아닌 red로 표기되는 것들이 있습니다. 혹은 ElasticHQ로 보면 unassigned Shards, Initializing Shards에 표기된 숫자들을 볼 수 있습니다. 


- 초기화가 진행되는 동안 데이터가 들어오면 bulk rejected가 되는데, 이 때 bulk queue에 보관하고 있다 작업이 완료되면 처리되도록 되어 있습니다.


- 그런데, 입력이 과다할 경우 누락이 발생되기도 한다고 합니다. 


제가 겪었던 월요일 오전의 그 사건이 딱 이런 경우였습니다. 


그래서 이를 방지하기 위해 조치를 하고, 혹시 몰라 밀려오는 데이터들을 일시적으로 redis에 expire 1주일 설정을 해두고서 마무리를 해 두었습니다. 


끝!!!

Invalid aggregation order path [something]. Buckets can only be sorted on a sub-aggregator path that is built out of zero or more single-bucket aggregations within the path and a final single-bucket or a metrics aggregation at the path end.



아래와 같은 샘플을 만들었었다.



"query" : {

   ... 검색 조건...

}.

"aggs" : {

"term_key" : {

"terms": {

"field" : "field_first",

"order" : {

"something" : "desc"

}

}

"aggs": {

"something" : {

"terms":{

"field":"field_second"

},

"aggs" : {

"sumofgold" : {

"sum": {

"field":"gold"

}

}

}

}

}

}

}


something과 sumofgold 둘 다 정렬을 하고 싶었는데 그건 안된다. path의 마지막에 오는 metrics aggregation 혹은 단일 버킷으로만 정렬 가능하다.


결과 받아서 코드로 정렬 시켜 해결함.


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


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



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;


[about setting command line tools ]


react-native run-ios 로 실행을 시도했을 때 command line tools 없다는 에러가 발생하는데, 


xcode-select --install로 설치를 해도 반응이 없다. 


단순히 설치만 해서는 안되고 xcode preferences에 설정을 해야 한다. 


XCode > Preferences > Locations 화면에서 Command Line Tools 이 비어 있다면 이를 설정하면 된다.

--------------------------------------------------------------------------------------------------------

when it occurs an error there is no command line tools you run "react-native run-ios" command


in my case, it doesn't work even I installed the tool like this "xcode-select --install"


the reason is ... you have to set an information in XCode > Preferences > Locations.





[8081 port problem]


sudo lsof -i :8081 로 해당 프로세스 찾아서 kill.

--------------------------------------------------------------------------------------------------------

Find a process using the port 8081 like this " sudo lsof -i : 8081" and kill it.



[Build failed]

Entry, ":CFBundleIdentifier", Does Not Exist


signing 관련 이슈. 생성한 프로젝트의 xcodeproj 로 xcode 실행 후 General > Signing 설정. 이후 빌드하면 관련 에러없이 애뮬레이터로 실행됨.--------------------------------------------------------------------------------------------------------

The reason is about signing. You have to set a signing information in XCode project > General/Signing on XCode project that you made.



[error : npm update check failed]


sudo chown -R $USER:$(id -gn $USER) /Users/YOUR_USERNAME_HERE/.config





Cognito 유저가 비공개 S3로 접근하려고 할 때... 


https://docs.aws.amazon.com/ko_kr/IAM/latest/UserGuide/reference_policies_examples_s3_cognito-bucket.html

=> 이 문서 중 sample rule script에서 Principal 관련된 코드가 빠져 있다. 실제 이 룰을 적용하려면 principal 에러가 발생한다. 문서 내용에는 언급되어 있지 않았다.



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


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 정도가 될 것이다. [본문으로]

+ Recent posts