Fairy ' s
[30. March] 로그 파이프라인 / TIL #2 본문
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