티스토리 뷰

반응형

회사에서 Elasticsearch 도입을 진행한 경험이있다.

당시, 해당 기술을 도입하면서 특정 블로그에서 Elasticsearch와 인기검색어를 함께 결합시켰던 블로그를 본적이있기에 호기심에 개발을 시작해보았다.

 

참고로, elasticsearch와 nginx는 모두 설치가 되어있는 상태입니다.

 

우선 생각하고 있는 흐름은 아래와 같다.

1. nginx에 http://localhost:7776의 경우 vivid.co 도메인 설정한다.
2. http://vivid.co/api/search/song/info?searchWord=(여자)아이들다음과 같은 요청을 보낸다.
이 때, nginx에 해당 URL로 요청했을 경우, Log를 쌓임을 확인한다.
3. Logstash에서는 nginx에 쌓인 로그를 확인하고 Elasticsearch Index를 생성해준다.
4. grouping QueryDSL을 만들어 준 뒤, 화면에 뿌려준다.

 

1. nginx에 http://localhost:7776의 경우 vivid.co 도메인 설정한다.

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;
    
    #1. 로그포맷 지정 및 추후 logstash에서 해당 로그 형식을 가지고와서 분석하여 index에 데이터를 쌓는다.
    log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"';

    access_log /opt/homebrew/var/log/nginx/access.log;
    error_log /opt/homebrew/var/log/nginx/error.log;

    server {
        listen 80;
        server_name vivid.co; # 2. server 도메인
        location /api/search/song/info { # 2. 요청 URL 시, 아래 search.log로 Log 등록
             access_log /opt/homebrew/var/log/nginx/search.log main; 

              proxy_set_header X-Real-IP            $remote_addr;
              proxy_set_header X-Forwarded-Host     $host;
              proxy_set_header X-Forwarded-Server   $host;
              proxy_set_header X-Forwarded-For      $proxy_add_x_forwarded_for;
              proxy_set_header Host                 $http_host;
              proxy_set_header X-SSL-Server         true;
              proxy_pass   http://localhost:7776/api/search/song/info; # 3. BackEnd로 proxy
        }

        location / {
              proxy_set_header X-Real-IP            $remote_addr;
              proxy_set_header X-Forwarded-Host     $host;
              proxy_set_header X-Forwarded-Server   $host;
              proxy_set_header X-Forwarded-For      $proxy_add_x_forwarded_for;
              proxy_set_header Host                 $http_host;
              proxy_set_header X-SSL-Server         true;
              proxy_pass   http://localhost:7776;
        }
    }
}

위 내용을 간단하게 살펴보자면,

1. vivid.co 로 들어왔으면서, /api/search/song/info 으로 URL요청하였을 경우, 

/opt/homebrew/var/log/nginx/search.log 해당경로에 Log를 main 방식으로 쌓는다.

추후 logstash에서 인기검색어 요청로그를 분석하여 index데이터를 쌓는데 사용된다.

log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"';

 

참고로 필자는 nuxt.config.js를 사용하기에 설정파일을 아래와같이 변경한다,

아래 내용의 경우 /api로 시작하는 요청일 경우 localhost:7777로 프록시 진행한다는 의미이다.

아래 내용은 참고만 부탁드린다.

server: {
    port: 7776,
  },

  axios: {
    proxy: true,
  },

  proxy: {
    "/api": {
      target: "http://localhost:7777/",
      pathRewrite: {
        "^/api": "", // 요청에 /api가 있을경우 공백으로 Replace하여 요청
      },
    },
  },

 

그리고 nginx를 재시작한다.

필자의 경우 hombrew를 통해 설치하였기에 명령어는 아래와 같다.

Q. nginx 위치 확인
brew info nginx

Q.ngnix 시작/종료/상태
brew services start nginx
brew services stop nginx
brew services status nginx

 

2. 요청 URL을 입력한 후, nginx에 데이터가 쌓인것을 확인해보자

nginx에 지정한 search.log 경로에 아래와 같은 데이터가 쌓임을 확인할 수 있다.

"GET /api/search/song/info?searchWord=(%EC%97%AC%EC%9E%90)%EC%95%84%EC%9D%B4%EB%93%A4%20Allergy HTTP/1.1" 
200 0 "http://vivid.co/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"

 

3. Logstash에서는 nginx에 쌓인 로그를 활용하여 Elasticsearch Index를 생성해준다.

첫번째로, 로그conf 파일을 생성한다.

경로: ~/logstash-8.2.2/config/conf.d/search-log.conf

 

두번째, 아래와 같은 소스를 적용시킨다.

아래 로직은 input, filter, output 총 3가지로 구분할 수 있다.

 

input의 경우, nginx log가 담긴 search.log 파일을 읽어온다.

이때, `sincedb_path` 를 지정해줌으로써 logstash가 어느부분까지 읽어들었는지 파악가능하다.

또한 `start_position`을 `end`로 지정해줌으로써, 어떤부분까지 읽었고 그 마지막부분부터 다시 읽도록 중복데이터가 쌓이지 않는다.

input {
  file {
    path => ["/opt/homebrew/var/log/nginx/search.log"]
    sincedb_path => "/Users/geumbit/sideproject/ELK/logstash-8.2.2/sincedb/sincedb.txt"
    start_position => "end"
  }
}

 

 

아래 내용은 nginx에 쌓인 로그를 %{NGINX_ACCESS} 포멧으로 읽어들이고,  요청데이터와 parameter를 매칭시켜 사용자가 입력한 데이터를 가지고온다. 

filter {
  grok {
    patterns_dir => "/usr/share/logstash/patterns"
    match => { "message" => "%{NGINX_ACCESS}" }
    add_field => [ "received_at", "%{@timestamp}" ]
  }

  grok {
    match => { "request" => ["%{URIPATH:uri_path}"] }
  }

  grok {
    match => { "request" => ["%{URIPARAM:uri_param}"] }
  }

  kv { #키-값 형식 데이터 구문 분석 추출
    source => "request"
    field_split => "&?" # & 혹은 ? 로 구분
    value_split => "="
    target => "params"
  }

  urldecode {
    charset => "UTF-8"
    field => "params"
  }
}

 

위 추출된 데이터의 형식데로 elasticsearch에 데이터를 등록한다.

output {
  elasticsearch {
    hosts => ["https://localhost:9200"]
    index => "nginx-search.log-%{+YYYY.MM}"
    document_type => "_doc" # document type 명
    document_id => "%{@timestamp}"
    user => "elastic"
    password => "**********"
    ssl => true
    ssl_certificate_verification => false
  }
}

위 작업이 모두 완료되었다면, 로그스태시를 재시작한다.

 

4. grouping QueryDSL을 만들어 준 뒤, 화면에 뿌려준다.

해당 키워드 요청을 날려본다면 아래와 같이 index가 생성됨을 확인할 수 있다.

{
  "nginx-search.log-2023.05" : {
    "mappings" : {
      "properties" : {
        "@timestamp" : {
          "type" : "date"
        },
        /////////// 생략 /////////////
        "params" : {
          "properties" : {
            "searchWord" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword",
                  "ignore_above" : 256
                }
              }
            }
          }
        },
        "received_at" : {
          "type" : "date"
        },
			/////// 생략 ///////////
      }
    }
  }
}

 

위 index를 고려하여 아래 조건으로 QueryDSL을 적용하고싶다고 가정한다.

30일 이전부터 지금까지를 기준
인기검색어 10개
GET nginx-*/_search
{
  "query": {
    "range": {
      "@timestamp": {
        "gte": "now-30d",
        "lte": "now"
      }
    }
  }, 
  "size": 0,
  "aggs": {
    "params_searchWord": {
      "terms": {
        "field": "params.searchWord.keyword",
        "size": 10,
        "order": {
          "_count": "desc"
        }
      }
    }
  }
}

 

위 내용을 java로 만들어보면 아래와 같다.

public List<PopularSearchWordVo> getPopularSearchWords() throws Exception {
    RangeQuery dateRange = getRangeQuery("received_at", LocalDateTime.now().minusDays(30).toString(), LocalDateTime.now().toString());

    SearchRequest searchRequest = new SearchRequest.Builder()
        .index("nginx-*")
        .query(q -> q
            .range(dateRange))
        .size(0)
        .aggregations("params_searchWord", a -> a
            .terms(
                t -> t.field("params.searchWord.keyword")
                .size(10)
            ))
        .build();

    SearchResponse<HashMap> searchResponse = esClient.search(searchRequest, HashMap.class);
    AggregateVariant agg = searchResponse.aggregations().get("params_searchWord")._get();
    List<StringTermsBucket> buckets = ((StringTermsAggregate) agg).buckets().array();

    List<PopularSearchWordVo> populars = new ArrayList<>();
    for (StringTermsBucket bucket : buckets) {
        populars.add(PopularSearchWordVo.builder()
            .docCount(bucket.docCount())
            .key(bucket.key()).build());
    }
    return populars;
}

private RangeQuery getRangeQuery(String fieldRange, String gte, String lte) {
    RangeQuery dateRange = RangeQuery.of(r -> r
            .field(fieldRange)
            .gte(JsonData.of(gte))
            .lte(JsonData.of(lte))
    );
    return dateRange;
}

 

응답

[
	0: {key: "(여자)아이들 Allergy", docCount: 2}
	1: {key: "장혜진", docCount: 2}
	2: {key: "(여자)아이들", docCount: 1}
	3: {key: "10CM", docCount: 1}
	4: {key: "BIGBANG (빅뱅)", docCount: 1}
	5: {key: "LE SSERAFIM (르세라핌)", docCount: 1}
	6: {key: "가슴은 알죠", docCount: 1}
	7: {key: "고백", docCount: 1}
	8: {key: "비투비", docCount: 1}
	9: {key: "세븐틴 (SEVENTEEN)", docCount: 1}
]

 

필자의 경우 화면은 아래와 같이 뿌려주었다..

 

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함