기업 요청 샘플 (수강생) - Python Dash를 활용한 대시보드

Page content

강의 홍보

개요

  • 보안 로그 파일을 업로드한 뒤, 점검 결과를 자동으로 출력해주도록 한다.
  • (수강생의 도전) 보안 로그 파일을 업로드 한 뒤, CPU 사용률이 70%가 넘으면 경고 메시지를 뛰우도록 한다.

Chapter 1. 로그데이터 분석 및 확인

  • 먼저 CPU가 들어있는 로그데이터를 확인한다.
    • 골든시스에서 제공한 Sample 데이터를 근거로 랜덤하게 데이터를 생성했다. 파일명: (cpu_test.txt)
Dorm_E_116.95#
Dorm_E_116.95#sh logg
Dorm_E_116.95#sh logging 
Syslog logging: enabled (0 messages dropped, 1 messages rate-limited, 0 flushes, 0 overruns, xml disabled, filtering disabled)

No Active Message Discriminator.

No Inactive Message Discriminator.

    Console logging: level debugging, 2178 messages logged, xml disabled,
                     filtering disabled
    Monitor logging: level debugging, 244 messages logged, xml disabled,
                     filtering disabled
    Buffer logging:  level debugging, 2178 messages logged, xml disabled,
                    filtering disabled
    Exception Logging: size (4096 bytes)
    Count and timestamp logging messages: disabled
    File logging: disabled
    Persistent logging: disabled

No active filter modules.

    Trap logging: level informational, 2176 message lines logged
        Logging Source-Interface:       VRF Name:
          
Log Buffer (100000 bytes):
17:19:23: %PM-4-ERR_DISABLE: storm-control error detected on Gi1/0/36, putting Gi1/0/36 in err-disable state
Jul  7 17:19:23: %STORM_CONTROL-3-SHUTDOWN: A packet storm was detected on Gi1/0/36. The interface has been disabled.
Jul  7 17:19:25: %LINK-3-UPDOWN: Interface GigabitEthernet1/0/36, changed state to down
Jul  7 17:19:53: %PM-4-ERR_RECOVER: Attempting to recover from storm-control err-disable state on Gi1/0/36
Switch#show processor cpu
CPU utilization for five seconds: 4%/1%; one minute: 3%; five minutes: 3%
.
.
.
  • 업무 수행 시, 위와 같은 데이터를 생성한다. 그 후에, 점검일지 파일을 작성한다.
    • 점검일지 파일 Sample /img/python/dash/dash_cpu_logdata/log_01.png log_01.png

(1) 문제점 진단

  • 통상적으로 위와 같이 명령어를 입력한 뒤 결괏값을 찾아 수기로 입력하여 작성하도록 함
  • 일일이 확인해야 하는 번거로움이 있다보니, 이를 자동으로 처리할 수 있는 툴 제작 의뢰를 받음

(2) 해결방안

  • 명령어 및 결괏값이 일정한 패턴이 있기 때문에, 문자열 매칭을 통해 전처리가 가능한 것을 확인
  • 또한, Dash 프레임워크를 통해 비교적 간단하게 대시보드를 만들 수 있음에 착안하였다.

(3) 배경지식

Chapter 2. 문자열 전처리 함수 만들기

  • 우선 Jupyter Lab에서 간단한 테스트 함수를 만들어 보았다.
  • 전체 코드는 아래와 같다.
    • 파일을 불러온 뒤 우선 각 코드 라인 중에서 CPU utilization 가 있는 텍스트만 남기고 그 외에는 모두 삭제한다.
    • 여기에서 다른 명령어의 결괏값을 확인하다면, if else 구문으로 계속적으로 확장할 수 있을 것이다.
    • 그 후에, 각 re.sub 함수를 활용하여 수치만 뽑아낸다.
    • 이를 각 데이터 프레임에 추가한 것이다.
import pandas as pd
import re

def text_cleanser(FILE_PATH):
    with open(FILE_PATH, 'r') as f:
        logLines = f.read().splitlines()
    cleaned_lines = [x for x in logLines if "CPU utilization" in x]
    clean_text = re.sub('/[0-9]%', '', cleaned_lines[0])
    print("temp:", clean_text)
    result_list = re.findall("\d+", clean_text)
    return result_list
        
FILE_PATH = "data/cpu_test.txt"
result_list = text_cleanser(FILE_PATH)
result_list
  • 위 파일을 Jupyter lab에서 실행하면 아래와 같은 결괏값이 출력될 것이다.
temp: CPU utilization for five seconds: 4%; one minute: 3%; five minutes: 3%
['4', '3', '3']
  • 출력한 결과물을 pandas 데이터 프레임에 적용한 결과는 아래와 같다.
data = pd.DataFrame({"분류":["cpu"],
                     "점검내용": ["CPU 사용률 점검 및 불필요한 프로세스 확인"],
                     "점검기준": ["MEM 임계치: 70% (MAX)"],
                     "점검방법": ["show processes cpu | in five"],
                     "5초 동안 CPU 사용률": [result_list[0]], 
                     "1분 동안 CPU 사용률": [result_list[1]], 
                     "5분 동안 CPU 사용률": [result_list[2]], 
                    })
data

log_02.png

Chapter 3. Dash 샘플 코드

  • 우선 전체 코드를 공유한다.
  • 먼저, [app.py](http://app.py) 코드를 공유한다.
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_table
from dash.dependencies import Input, Output, State

import pandas as pd

import base64
import re
import os

data = pd.DataFrame({"분류": ["cpu"],
                     "점검내용": ["CPU 사용률 점검 및 불필요한 프로세스 확인"],
                     "점검기준": ["MEM 임계치: 70% (MAX)"],
                     "점검방법": ["show processes cpu | in five"],
                     "5초 동안 CPU 사용률": ["측정 전"],
                     "1분 동안 CPU 사용률": ["측정 전"],
                     "5분 동안 CPU 사용률": ["측정 전"],
                     })

external_stylesheets = [
    {
        "href": "https://fonts.googleapis.com/css2?"
                "family=Lato:wght@400;700&display=swap",
        "rel": "stylesheet",
    },
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets,
                suppress_callback_exceptions=True,
                prevent_initial_callbacks=True)

app.title = "보안 점검일지 표"
server = app.server

app.layout = html.Div(
    children=[
        # title text
        html.Div(
            children=[
                html.H1(children="보안일지 점검 표 샘플", className="header_title"),
                html.P(children="!!!----!!!", className="header_description")
            ],  # children
            className='header',
        ), # html.Div

        # 파일 업로드
        html.Div(
            children=[
                html.Div(
                    children=[
                        dcc.Upload(
                            id='upload-data',
                            children=html.Div([
                                        'Drag and Drop or ',
                                    html.A('Select Files')
                            ]),
                            style={
                                'width': '100%',
                                'height': '60px',
                                'lineHeight': '60px',
                                'borderWidth': '1px',
                                'borderStyle': 'dashed',
                                'borderRadius': '5px',
                                'textAlign': 'center',
                                'margin': '10px'
                            },
                            # Allow multiple files to be uploaded
                            multiple=True
                        ),
                            html.Div(id='output-data-upload'),
                    ] # children
                ), # html.Div
            ], # children
            className='menu',
        ), # html.Div

        # Display
        html.Div(
            children=[
                html.Div(
                    children=dash_table.DataTable(
                        id = "data_id",
                        columns=[{"id":c, "name":c} for c in data.columns],
                        data = [],
                        style_cell={'textAlign': 'left',
                                    'whiteSpace': 'normal',
                                    'fontWeight': 'normal',
                                    'height': 'auto'
                                    },
                        style_header={
                            'backgroundColor': 'black',
                            'fontWeight': 'bold',
                            'color': 'white'
                        },
                        export_format="xlsx",
                    ) # children
                ) # Table
            ],
            className="wrapper",
        ), # html.Div
    ] # children
) # html.Div

UPLOAD_DIRECTORY = "/project/app_uploaded_files/"

if not os.path.exists(UPLOAD_DIRECTORY):
    os.makedirs(UPLOAD_DIRECTORY)

def text_cleanser(FILE_PATH):
    print("FILE_PATH", FILE_PATH)
    with open(FILE_PATH, 'r', encoding="utf-8") as f:
        logLines = f.read().splitlines()
    print("logLines:", logLines)
    cleaned_lines = [x for x in logLines if "CPU utilization" in x]
    clean_text = re.sub('/[0-9]%', '', cleaned_lines[0])
    print("temp:", clean_text)
    result_list = re.findall("\d+", clean_text)
    return result_list

def save_file(name, content):
    """Decode and store a file uploaded with Plotly Dash."""
    data = content.encode("utf8").split(b";base64,")[1]
    with open(os.path.join(UPLOAD_DIRECTORY, name), "wb") as fp:
        fp.write(base64.decodebytes(data))

def uploaded_files():
    """List the files in the upload directory."""
    files = []
    for filename in os.listdir(UPLOAD_DIRECTORY):
        path = os.path.join(UPLOAD_DIRECTORY, filename)
        if os.path.isfile(path):
            files.append(filename)
            return files

@app.callback(
    Output("data_id", "data"),
    [Input("upload-data", "filename"), Input("upload-data", "contents")],
)
def update_output(uploaded_filenames, uploaded_file_contents):
    """Save uploaded files and regenerate the file list."""

    if uploaded_filenames is not None and uploaded_file_contents is not None:
        for name, data in zip(uploaded_filenames, uploaded_file_contents):
            save_file(name, data)

    files = uploaded_files()
    if len(files) == 0:
       print("No files yet!")
    else:
       print("Files Okay")
    result_list = text_cleanser(UPLOAD_DIRECTORY + files[0])
    data = pd.DataFrame({"분류": ["cpu"],
                         "점검내용": ["CPU 사용률 점검 및 불필요한 프로세스 확인"],
                         "점검기준": ["MEM 임계치: 70% (MAX)"],
                         "점검방법": ["show processes cpu | in five"],
                         "5초 동안 CPU 사용률": [result_list[0]],
                         "1분 동안 CPU 사용률": [result_list[1]],
                         "5분 동안 CPU 사용률": [result_list[2]],
                         })
    print(data)
    return data.to_dict('records')

if __name__ == '__main__':
    app.run_server(debug=True)
  • 다음은 style.css 코드이다.
    • 해당 코드는 assets/style.css 형태로 저장한다.
body {
    font-family: "Lato", sans-serif;
    margin: 0;
    background-color: #F7F7F7;
}

.header {
    background-color: #222222;
    height: 288px;
    padding: 16px 0 0 0;
}

.header_title {
    color: #FFFFFF;
    font-size: 48px;
    font-weight: bold;
    text-align: center;
    margin: 0 auto;
}

.header_description {
    color: #CFCFCF;
    margin: 4px auto;
    text-align: center;
    max-width: 384px;
}

.menu {
    height: 130px;
    width: 912px;
    display: flex;
    justify-content: space-evenly;
    padding-top: 24px;
    margin: -80px auto 0 auto;
    background-color: #FFFFFF;
    box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}

.naver_news_url {
    width: 100%;
}

.menu-title {
    margin-bottom: 6px;
    font-weight: normal;
    font-style: bold;
}

.url_sample {
    font-weight: normal;
    font-style: italic;
    font-size: 80%;
    color: #079A82;
}

.card {
    margin-bottom: 24px;
    box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}

.wrapper {
    margin-right: auto;
    margin-left: auto;
    max-width: 1024px;
    padding-right: 10px;
    padding-left: 10px;
    margin-top: 32px;
}
  • 폴더 구조를 보면 아래와 같다.
PS C:\Users\1\Desktop\dash-logReport> tree /f
폴더 PATH 목록입니다.
볼륨 일련 번호는 E657-CFA3입니다.
C:.
  app.py
├─assets
      style.css

└─image
       cpu_test.txt
  • 이제 터미널에서 아래와 같이 [app.py](http://app.py) 를 실행한다.
PS C:\Users\1\Desktop\dash-logReport> python .\app.py
Dash is running on http://127.0.0.1:8050/

 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
  • 결과 화면을 확인해본다.

  • 우선 첫 화면은 아래와 같을 것이다.

    log_03.png

  • 두번째 화면은 Drag and Drop or Select Files 버튼을 클릭하여 로그 파일을 업로드 하면, 자동으로 전처리 한 결과를 반영한 것이다.

    log_04.png

  • 마지막은 Export 버튼을 클릭하여 엑셀 파일 을 다운로드 받은 뒤 결과가 동일하게 나오는지 확인한다.

log_05.png

Chapter 4. 소스코드 리뷰

  • 소스코드에 관한 간단한 리뷰를 진행한다.

(1) 라이브러리 불러오기

  • 크게 dash 와 관련된 라이브러리, 전처리(re)와 데이터프레임(pandas)과 관련된 라이브러리, 파이썬의 인코딩(base64)과 파일 경로(os)와 관련된 라이브러리로 구성이 되어 있다.
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_table
from dash.dependencies import Input, Output, State

import pandas as pd
import re

import base64
import os

(2) Dash 코드

  • Dash Web 개발 관련 코드는 아래와 같다.
  • 아래 코드에서의 핵심은 dcc.Upload 이하의 절이다. 해당 코드에서 파일을 불러온 뒤, 서버에 임시적으로 저장을 해야 한다.
    • dcc.Upload의 ID는 id=‘upload-data’임을 기억한다.
  • 그 외 나머지 코드는 그동안 배워온 코드와 유사하기에 추가 설명은 생략한다.
data = pd.DataFrame({"분류": ["cpu"],
                     "점검내용": ["CPU 사용률 점검 및 불필요한 프로세스 확인"],
                     "점검기준": ["MEM 임계치: 70% (MAX)"],
                     "점검방법": ["show processes cpu | in five"],
                     "5초 동안 CPU 사용률": ["측정 전"],
                     "1분 동안 CPU 사용률": ["측정 전"],
                     "5분 동안 CPU 사용률": ["측정 전"],
                     })

external_stylesheets = [
    {
        "href": "https://fonts.googleapis.com/css2?"
                "family=Lato:wght@400;700&display=swap",
        "rel": "stylesheet",
    },
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets,
                suppress_callback_exceptions=True,
                prevent_initial_callbacks=True)

app.title = "보안 점검일지 표"
server = app.server

app.layout = html.Div(
    children=[
        # title text
        html.Div(
            children=[
                html.H1(children="보안일지 점검 표 샘플", className="header_title"),
                html.P(children="!!!----!!!", className="header_description")
            ],  # children
            className='header',
        ), # html.Div

        # 파일 업로드
        html.Div(
            children=[
                html.Div(
                    children=[
                        dcc.Upload(
                            id='upload-data',
                            children=html.Div([
                                        'Drag and Drop or ',
                                    html.A('Select Files')
                            ]),
                            style={
                                'width': '100%',
                                'height': '60px',
                                'lineHeight': '60px',
                                'borderWidth': '1px',
                                'borderStyle': 'dashed',
                                'borderRadius': '5px',
                                'textAlign': 'center',
                                'margin': '10px'
                            },
                            # Allow multiple files to be uploaded
                            multiple=True
                        ),
                            html.Div(id='output-data-upload'),
                    ] # children
                ), # html.Div
            ], # children
            className='menu',
        ), # html.Div

        # Display
        html.Div(
            children=[
                html.Div(
                    children=dash_table.DataTable(
                        id = "data_id",
                        columns=[{"id":c, "name":c} for c in data.columns],
                        data = [],
                        style_cell={'textAlign': 'left',
                                    'whiteSpace': 'normal',
                                    'fontWeight': 'normal',
                                    'height': 'auto'
                                    },
                        style_header={
                            'backgroundColor': 'black',
                            'fontWeight': 'bold',
                            'color': 'white'
                        },
                        export_format="xlsx",
                    ) # children
                ) # Table
            ],
            className="wrapper",
        ), # html.Div
    ] # children
) # html.Div

(3) 데이터 처리 코드와 관련된 주요 설명

  • 화면이 만들어졌다면, 이제는 데이터 처리 코드를 설명하도록 한다.
  • 아래 코드는 앱 서버단에서 임시로 경로 및 폴더를 만드는 코드이다.
    • 아래 경로에 업로드 된 파일이 임시로 저장될 것이다.
UPLOAD_DIRECTORY = "/project/app_uploaded_files/"

if not os.path.exists(UPLOAD_DIRECTORY):
    os.makedirs(UPLOAD_DIRECTORY)

  • 다음 코드는 text_cleanser() 코드이다.
  • 기존 코드와 큰 차이점은 없고, 다만, 중간에 파일이 잘 불러와지는지 확인하기 위해 print() 함수를 사용했다.
def text_cleanser(FILE_PATH):
    print("FILE_PATH", FILE_PATH)
    with open(FILE_PATH, 'r', encoding="utf-8") as f:
        logLines = f.read().splitlines()
    print("logLines:", logLines)
    cleaned_lines = [x for x in logLines if "CPU utilization" in x]
    clean_text = re.sub('/[0-9]%', '', cleaned_lines[0])
    print("temp:", clean_text)
    result_list = re.findall("\d+", clean_text)
    return result_list

def save_file(name, content):
    """Decode and store a file uploaded with Plotly Dash."""
    data = content.encode("utf8").split(b";base64,")[1]
    with open(os.path.join(UPLOAD_DIRECTORY, name), "wb") as fp:
        fp.write(base64.decodebytes(data))

def uploaded_files():
    """List the files in the upload directory."""
    files = []
    for filename in os.listdir(UPLOAD_DIRECTORY):
        path = os.path.join(UPLOAD_DIRECTORY, filename)
        if os.path.isfile(path):
            files.append(filename)
            return files

  • 사전에 데이터 처리와 관련된 함수를 만들었다면, 이번에는 실제로 적용을 하도록 한다.

  • 여기에서 callback이 처리하는 전체적인 처리 순서를 그림으로 표현하면 아래와 같다.

    log_06.png

  • 위 그림과 같은 프로세스를 처리하기 위해 callback 처리 코드를 작성한다.

  • 최종적인 output의 결과는 data로 처리가 된다.

  • 또한, save file(), uploaded_files(), text_cleanser() 함수가 순차적으로 사용된 것을 확인할 수 있다.

  • 이미 text_cleanser() 내부에서 pandas 데이터프레임으로 변환하는 코드를 이미 실습했기 때문에 여기에서는 추가적인 설명은 생략한다.

@app.callback(
    Output("data_id", "data"),
    [Input("upload-data", "filename"), Input("upload-data", "contents")],
)
def update_output(uploaded_filenames, uploaded_file_contents):
    """Save uploaded files and regenerate the file list."""

    if uploaded_filenames is not None and uploaded_file_contents is not None:
        for name, data in zip(uploaded_filenames, uploaded_file_contents):
            save_file(name, data)

    files = uploaded_files()
    if len(files) == 0:
       print("No files yet!")
    else:
       print("Files Okay")
    result_list = text_cleanser(UPLOAD_DIRECTORY + files[0])
    data = pd.DataFrame({"분류": ["cpu"],
                         "점검내용": ["CPU 사용률 점검 및 불필요한 프로세스 확인"],
                         "점검기준": ["MEM 임계치: 70% (MAX)"],
                         "점검방법": ["show processes cpu | in five"],
                         "5초 동안 CPU 사용률": [result_list[0]],
                         "1분 동안 CPU 사용률": [result_list[1]],
                         "5분 동안 CPU 사용률": [result_list[2]],
                         })
    print(data)
    return data.to_dict('records')

  • 이제 마지막으로 app을 실행하는 코드를 작성한다.
if __name__ == '__main__':
    app.run_server(debug=True)

Chapter 5. 수강생들의 도전