Fairy ' s

[30. March] 로그 파이프라인 / TIL #2 본문

Devops Bootcamp

[30. March] 로그 파이프라인 / TIL #2

berafairy 2023. 3. 30. 17:24

1. nginx 로그 분석 (sample.log)

10.0.210.17 - - [28/Nov/2022:11:33:28 +0900] "GET /hello HTTP/1.1" 200 615 "-" "curl/7.84.0" "-"

  • IP 주소 : 요청하는 클라이언트의 주소이다.
  • - : 클라이언트의 신원
  • - : 클라이언트와 연결된 사용자 ID
  • 날짜 : 요청이 이루어진 날짜
  • "GET /hello HTTP/1.1" : 사용된 HTTP 메서드, 요청된 URL 및 사용된 HTTP 버전
  • 200 : 요청이 성공했다는 의미
  • "-" : 알 수 없는 리퍼러
  • "curl/7.84.0" : 요청에 사용된 소프트웨어를 식별하는 사용자 에이전트 문자열
  • "-" : 알 수 없는 응답의 내용 유형
$ cat sample.log | ./parser.js

위 명령어를 통해 log 파일과 parser.js 파일을 살펴보면 제대로 작성된 프로그램인지 여부를 판단할 수 있다.

// 실행 결과
{
  "source_ip": "127.0.0.1",
  "method": "POST",
  "status_code": 404,
  "path": "/replace-me",
  "timestamp": "2022-11-28T02:33:28.000Z"
}

현재 출력되는 결과는 하드코딩된 결과물로, 제대로 나오는 값은 날짜 하나 뿐인 것을 볼 수 있다.

따라서 지금부터 진행해야 할 부분은 parser.js를 수정해서 표준 입력으로부터 들어오는 로그가 JSON의 형태로 변환되어 표준 출력으로 나오게 해야한다.

 

2. 데이터베이스 실습

데이터베이스 실습을 위해 ElephantSQL을 가입하고 인스턴스를 생성한다.

// 데이터베이스 연결 테스트에 필요한 파일들
.
├── sql                        // 테스트해볼 만한 SQL문 모음입니다.
│   ├── 1_reset.sql
│   ├── 2_describe_table.sql
│   └── 3_display_table_data.sql
└── sql-runner.js              // SQL문을 실행시켜서 결과를 콘솔에 출력합니다.

 

인스턴스를 살펴보면 URL에 나와있는 부분에 아이디와 암호, 호스트이름과 데이터베이스 이름이 나타나있다.

위의 형식대로 .env 파일에 내용을 붙여 넣는다.

HOSTNAME=arjuna.db.elephantsql.com
USERNAME=mwjzzafc
PASSWORD=SUPERSECRET
DATABASE=mwjzzafc

아래 코드 설명 부분에서 parser.js 파일을 수정한다.

이제 sql 폴더 안에 있는 쿼리 명령 파일들의 데이터를 sql-runner.js 에 표준 입력을 해본다.

## sql/1_reset.sql

DROP TABLE IF EXISTS public.nginx;

CREATE TABLE public.nginx (
	id serial4 NOT NULL,
	source_ip varchar NULL,
	"method" varchar NULL,
	status_code varchar NULL,
	"path" varchar NULL,
	"timestamp" timestamptz NULL,
	CONSTRAINT nginx_pk PRIMARY KEY (id)
);

## public.nginx라는 새 테이블을 생성한다.
## sql/2_describe_table.sql

SELECT column_name, udt_name, is_nullable 
FROM information_schema.columns 
WHERE table_name = 'nginx'

## information_schema.columns 시스템 뷰에서 nginx 테이블의 열에 대한 정보를 검색한다.
## column_name : nginx 테이블의 열 이름
## udt_name : 열의 데이터 유형 이름
## is_nullable : 컬럼이 NULL 값을 허용하는지 여부
## sql/3_display_table_data.sql

SELECT * FROM nginx;

## nginx 테이블의 모든 레코드(행)와 열 검색한다.
## sql/4_clean_up_table.sql

TRUNCATE TABLE public.nginx;

## 데이터베이스의 공용(public) 스키마에 있는 nginx 테이블의 모든 행을 비운다.

 

SQL 데이터베이스 연결 / 실행

다음은 현재 폴더의 sql-runner.js에 sql 폴더의 1_reset.sql을 표준 입력하는 명령어를 실행한 결과이다. 

$ ./sql-runner.js < sql/1_reset.sql
## 1_rest.sql 내용을 sql-runner.js의 process.stdin.on() 메서드를 통해 표준 입력으로 수신한다.

DROP TABLE IF EXISTS public.nginx;

CREATE TABLE public.nginx (
        id serial4 NOT NULL,
        source_ip varchar NULL,
        "method" varchar NULL,
        status_code varchar NULL,
        "path" varchar NULL,
        "timestamp" timestamptz NULL,
        CONSTRAINT nginx_pk PRIMARY KEY (id)
);

undefined

데이터베이스 연결 닫는 중...
데이터베이스 연결 종료
## 실행 중인 SQL 문과 실행 결과(데이터베이스에서 nginx 테이블 생성)를 보여준다.
  • sql-runner.js 코드는 .env 파일에 지정된 환경 변수를 사용해 데이터베이스 연결을 생성하고, 1_reset.sql 파일의 내용을 읽는다.
  • 다음으로 Node.js의 pg 라이브러리를 사용해 1_reset.sql 파일의 SQL 명령어를 실행한다.
  • SQL 명령이 실행되면 console.table() 메서드를 사용해 SQL 쿼리 결과 행을 테이블 형식으로 콘솔에 표시한다.

다음은 현재 폴더의 sql-runner.js에 sql 폴더의 2_descrip_table.sql을 표준 입력하는 명령어를 실행한 결과이다.

$ ./sql-runner.js < sql/2_describe_table.sql 
## 2_describe_table.sql 내용을 sql-runner.js의 process.stdin.on() 메서드를 통해 표준 입력으로 수신한다.

SELECT column_name, udt_name, is_nullable FROM information_schema.columns WHERE table_name = 'nginx'

┌─────────┬───────────────┬───────────────┬─────────────┐
│ (index) │  column_name  │   udt_name    │ is_nullable │
├─────────┼───────────────┼───────────────┼─────────────┤
│    0    │     'id'      │    'int4'     │    'NO'     │
│    1    │  'timestamp'  │ 'timestamptz' │    'YES'    │
│    2    │  'source_ip'  │   'varchar'   │    'YES'    │
│    3    │   'method'    │   'varchar'   │    'YES'    │
│    4    │ 'status_code' │   'varchar'   │    'YES'    │
│    5    │    'path'     │   'varchar'   │    'YES'    │
└─────────┴───────────────┴───────────────┴─────────────┘

데이터베이스 연결 닫는 중...
데이터베이스 연결 종료

SQL 파일들이 각각 무슨 역할을 하는지 코드를 분석해보자.

1. sql-runner.js

#!/usr/bin/env node
// 스크립트를 실행하는 데 사용할 인터프리터를 지정하는 shebang 지정
// Node.js를 사용하여 스크립트를 실행하도록 지정

const dotenv = require('dotenv')
// .env 파일에서 process.env 개체로 환경 변수를 읽을 수 있는 dotenv 패키지를 가져온다.
const { Client } = require('pg')
// Node.js용 PostgreSQL 클라이언트 라이브러리인 pg 패키지를 가져온다.
dotenv.config()
// .env 파일의 환경 변수를 process.env 객체로 로드한다.

const { HOSTNAME, USERNAME2, PASSWORD, DATABASE } = process.env
// 구조 분해를 사용하여 환경 변수 값을 별도의 상수로 추출한다.
const client = new Client({
  host: HOSTNAME,
  user: USERNAME2,
  password: PASSWORD,
  database: DATABASE
})
// 환경 변수에서 추출한 연결 정보를 사용해, 새 PostgreSQL 클라이언트 인스턴스를 만든다.

client.connect().then(() => {
// 클라이언트 인스턴스를 사용하여 PostgreSQL 서버에 대한 연결을 시작한다.

  process.stdin.on("data", async data => {
  // 표준 입력 스트림의 데이터 이벤트에 대한 수신기를 설정하여 명령줄에서 쿼리를 받는다.
    let queryString = data.toString()
    // 데이터를 문자열로 변환하고 queryString 변수에 할당한다.

    console.log(`\n${queryString}\n`)
    // 입력 쿼리를 콘솔에 기록한다.
    try {
      const result = await client.query(queryString)
      console.table(result.rows)
    }
    catch(e) {
      console.log(e)
    }
    // 클라이언트 인스턴스의 query() 메서드를 사용해 입력 쿼리를 실행하려고 시도한다.
    // 성공하면 console.table()을 통해 결과가 콘솔에 기록되고,
    // 실패하면 오류 메시지가 콘솔에 기록된다.
    
    finally {
      console.log('\n데이터베이스 연결 닫는 중...')
      await client.end()
      console.log('데이터베이스 연결 종료')
      process.exit(1)
    }
    // 쿼리의 성공 여부에 관계없이 실행된다.
    // 클라이언트 인스턴스의 end() 메서드를 사용하여 데이터베이스 연결을 닫고,
    // 콘솔에 메시지를 기록한 다음 상태 코드 1로 프로세스를 종료한다.
  })

}).catch(err => console.log('연결 오류', err.stack))
// 서버에 대한 연결이 실패할 경우 연결 오류라는 문자열과 에러 메시지를 콘솔에 출력한다.
  • sql-runner.js 파일은 PostgreSQL 데이터베이스에 연결하고, sql 파일을 표준입력으로 받아서 실행하는 역할을 한다.

2. collector.js

#!/usr/bin/env node

const dotenv = require('dotenv')
const { Client } = require('pg')
dotenv.config()

const { HOSTNAME, USERNAME2, PASSWORD, DATABASE } = process.env
const client = new Client({
  host: HOSTNAME,
  user: USERNAME2,
  password: PASSWORD,
  database: DATABASE
})

client.connect().then(() => {

  process.stdin.on("data", async data => {
  // 비동기적으로 데이터를 읽기 위해 process.stdin에 리스너를 설정한다.
    let raw = data.toString()
    // 입력 스트림의 데이터를 문자열로 변환하고 
    let json = JSON.parse(raw)
    // 문자열을 JSON으로 구문으로 분석하고 
    let rawsplit = raw.split(" ");
    // 문자열을 배열로 띄어쓰기 하나를 두고 한 문자열씩 분할한다.

    let queryString = `
      INSERT INTO public.nginx (source_ip, method, status_code, path, timestamp)
      VALUES (
        '${json.source_ip}','${json.method}','${json.status_code}','${json.path}','${json.timestamp}'
      );
    ` 
    // PostgreSQL 데이터베이스에 데이터를 삽입하기 위한 SQL 쿼리 문자열을 생성한다.

    console.log(queryString)
    // SQL 쿼리 문자열을 콘솔에 기록한다.
    try {
      await client.query(queryString)
    }
    catch(e) {
      console.log(e)
    }
    // PostgreSQL 클라이언트의 query 메서드를 사용해 SQL 쿼리 문자열을 실행하려고 시도한다.
  })

}).catch(err => console.log('연결 오류', err.stack))
// 클라이언트가 PostgreSQL 데이터베이스에 연결하는 동안 발생하는 
// 모든 오류에 대해 오류 처리기를 설정한다.

process.on('SIGINT', async (sig) => {
  console.log('\n데이터베이스 연결 닫는 중...')
  await client.end()
  console.log('데이터베이스 연결 종료')
  process.exit(1)
})
// Ctrl+C(SIGINT 신호)가 입력되면, 스크립트가 종료되기 전에 데이터베이스를 닫는다.
  • Collector.js 파일은 PostgreSQL 데이터베이스에 연결하고 데이터를 삽입하는 Node.js 스크립트를 생성한다.
  • 스크립트는 JSON 형식으로 예상되는 데이터의 표준 입력 스트림을 수신하고 SQL 쿼리를 구성하여 PostgreSQL 데이터베이스의 테이블에 데이터를 삽입한다.
  • 스크립트는 구성된 SQL 쿼리를 콘솔에 기록한 후 PostgreSQL 클라이언트 라이브러리를 사용하여 실행한다.
  • 쿼리 실행 중에 오류가 발생하면 오류가 콘솔에 기록된다.

3. parser.js

#!/usr/bin/env node

const dayjs = require('dayjs')
// 날짜 및 시간 구문 분석 및 조작에 사용되는 dayjs 라이브러리를 가져온다.
const customParseFormat = require('dayjs/plugin/customParseFormat')
// 추가 날짜 및 시간 구문 분석 기능을 제공하는 dayjs용 cuntomParseFormat 플러그인을 가져온다.
dayjs.extend(customParseFormat)
// customParseFormat 플러그인으로 dayjs를 확장한다.

process.stdin.on("data", (data) => {
// 새 데이터가 수신될 때마다 트리거되는 표준 입력 스트림에 대한 수신기를 등록한다.
  let raw = data.toString()
  let rawsplit = raw.split(" ");

  let regex = /\[(.+)\]/g
  // 대괄호로 묶인 하위 문자열'(.+)\'과 일치하는 정규식을 정의한다.
  let match = regex.exec(raw)[1]
  // 수신 데이터에 정규식을 적용하여 타임스태프를 나타내는 하위 문자열을 추출한다.

  let source_ip = rawsplit[0]
  let method = rawsplit[5].substring(1)
  let status_code = rawsplit[8]
  let path = rawsplit[6]
  let timestamp = dayjs(match, 'DD/MMM/YYYY:hh:mm:ss +ZZ').toISOString()
  // dayjs를 사용해 수신 데이터에서 추출한 타임스탬프를 구문 분석하여 ISO로 변환한다.
  // ' ' 안에 들어있는 형식이 ISO 형식이다.

  let jsonString = `
  {
    "source_ip": "${source_ip}",
    "method": "${method}",
    "status_code": ${status_code},
    "path": "${path}",
    "timestamp": "${timestamp}"
  }`
  // 템플릿 리터럴을 사용해 추출된 데이터를 JSON 문자열로 포맷한다.

  process.stdout.write(jsonString)
  // JSON 문자열을 표준 출력 스트림에 쓴다.
})
  • parser.js 파일은 특정 형식의 로그 파일을 구문 분석하여 표준화된 형식으로 변환하는 것이다.
  • 로그 파일의 문자열을 표준 입력으로 사용하고, 로그 항목을 개별 필드로 구문 분석한다.
  • 타임 스탬프 필드를 사용자 지정 형식에서 ISO 형식으로 변환한다.
  • 구문 분석한 로그 항목을 JSON 문자열로 표준 출력한다.

'Devops Bootcamp' 카테고리의 다른 글

[6. Apr] TCP/IP 4계층,OSI 7계층 / TIL  (0) 2023.04.06
[5. Apr] 1st project  (0) 2023.04.05
[30. March] 데이터 파이프라인 / TIL #1  (0) 2023.03.30
[29. March] 발표 / TIL  (0) 2023.03.29
[29. March] 데이터베이스 개념 / TIL  (0) 2023.03.29
Comments