Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion app/api/challenge.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from json import JSONDecodeError
from logging import log
from app.monitoring.ctf_metrics_collector import ChallengeMetricsCollector
from flask import Blueprint, jsonify, request

from app.exceptions.api import InvalidRequest
from app.exceptions.userchallenge import UserChallengeCreationError, UserChallengeDeletionError, UserChallengeNotFoundError
from app.extensions.db.repository import UserChallengesRepository
from app.extensions.k8s.client import K8sClient
from app.monitoring.ctf_metrics_collector import challenge_metrics_collector

challenge_bp = Blueprint('challenge', __name__)

Expand All @@ -15,21 +17,33 @@ def create_challenge():
# Challenge 관련 정보 가져오기
res = request.get_json()
if not res:
raise InvalidRequest(error_msg="Request body is empty or not valid JSON")
challenge_metrics_collector.challenge_operations.labels(operation='create',result='error').inc()
raise InvalidRequest(error_msg="Request body is empty or not valid JSON")
if 'challenge_id' not in res:
challenge_metrics_collector.challenge_operations.labels(operation='create',result='error').inc()
raise InvalidRequest(error_msg="Required field 'challenge_id' is missing in request")

challenge_id = res['challenge_id']

if 'username' not in res:
challenge_metrics_collector.challenge_operations.labels(operation='create',result='error').inc()
raise InvalidRequest(error_msg="Required field 'username' is missing in request")
username = res['username']
# 챌린지 생성
client = K8sClient()
endpoint = client.create_challenge_resource(challenge_id, username)
if not endpoint:
challenge_metrics_collector.challenge_operations.labels(operation='create',result='error').inc()
raise UserChallengeCreationError(error_msg=f"Faile to create challenge {challenge_id} for user {username}")

challenge_metrics_collector.challenge_state.labels(
challenge_id=challenge_id,
username=username,
state='active'
).set(1)

challenge_metrics_collector.challenge_operations.labels(operation='create',result='success').inc()

return jsonify({'data' : {'port': endpoint}}), 200

@challenge_bp.route('/delete', methods=['POST'])
Expand All @@ -42,25 +56,39 @@ def delete_userchallenges():
res = request.get_json()
if not res:
log.error("No data provided")
challenge_metrics_collector.challenge_operations.labels(operation='delete',result='error').inc()
raise UserChallengeDeletionError(error_msg="Request body is empty or not valid JSON")

if 'challenge_id' not in res:
log.error("No challenge_id provided")
challenge_metrics_collector.challenge_operations.labels(operation='delete',result='error').inc()
raise InvalidRequest(error_msg="Required field 'challenge_id' is missing in request")
challenge_id = res['challenge_id']

if 'username' not in res:
log.error("No username provided")
challenge_metrics_collector.challenge_operations.labels(operation='delete',result='error').inc()
raise InvalidRequest(error_msg="Required field 'username' is missing in request")
username = res['username']

# 사용자 챌린지 삭제
client = K8sClient()
client.delete_userchallenge(username, challenge_id)

# Metrics
challenge_metrics_collector.challenge_state.labels(
challenge_id=challenge_id,
username=username,
state='inactive'
).set(0)
challenge_metrics_collector.challenge_operations.labels(
operation='delete',
result='success'
).inc()
return jsonify({'message' : '챌린지가 정상적으로 삭제되었습니다.'}), 200
except JSONDecodeError as e:
log.error("Invalid request format")
challenge_metrics_collector.challenge_operations.labels(operation='delete',result='error').inc()
raise InvalidRequest(error_msg=str(e)) from e

@challenge_bp.route('/status', methods=['POST'])
Expand Down
41 changes: 16 additions & 25 deletions app/factory.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import sys

from requests import Response
from app.monitoring.ctf_metrics_collector import ChallengeMetricsCollector
from app.monitoring.loki_logger import FlaskLokiLogger
from app.monitoring.system_metrics_collector import SystemMetricsCollector
from flask import Flask, g, request
import threading
from datetime import datetime
from typing import Any, Dict, Type
from prometheus_client import REGISTRY, generate_latest, CONTENT_TYPE_LATEST

from app.api.challenge import challenge_bp
from app.config import Config
Expand All @@ -22,12 +27,13 @@ def __init__(self, config_class: Type[Config] = Config):
self.app = Flask(__name__)
self.app.config.from_object(config_class)
self.logger = FlaskLokiLogger(app_name="challenge-api", loki_url=self.app.config['LOKI_URL']).logger

# 초기 설정
self._init_extensions()
self._setup_middleware()
self._register_error_handlers()
self._setup_blueprints()
self._init_metrics_collector()

def _init_extensions(self):
"""Extensions 초기화"""
Expand All @@ -40,6 +46,13 @@ def _init_extensions(self):
with self.app.app_context():
db.create_all()

def _init_metrics_collector(self):

# System 메트릭 수집기 초기화
system_collector = SystemMetricsCollector(self.app)
system_collector.start_collecting()


def _setup_middleware(self):
"""미들웨어 설정"""
@self.app.before_request
Expand Down Expand Up @@ -80,7 +93,6 @@ def _get_request_context(self) -> Dict[str, Any]:
"remote_addr": request.remote_addr,
"user_agent": request.user_agent.string,
"request_id": request.headers.get('X-Request-ID', 'unknown'),
"timestamp": datetime.utcnow().isoformat()
}
except Exception as e:
# 요청 컨텍스트 추출 실패 시 기본값
Expand All @@ -103,27 +115,17 @@ def _log_request(self, response, processing_time: float):
"request_id": context.get("request_id", "unknown"),
"status_code": str(getattr(response, 'status_code', 'unknown')),
"method": context.get("method", "UNKNOWN"),
"path": context.get("path", "/")
}

# Prepare log content
log_content = {
"processing_time_ms": round(processing_time * 1000, 2),
"remote_addr": context.get("remote_addr", ""),
"user_agent": context.get("user_agent", ""),
"method": context.get("method", ""),
"path": context.get("path", ""),
"status_code": str(getattr(response, 'status_code', 'unknown')),
"timestamp": context.get("timestamp", datetime.utcnow().isoformat())
}

# 추가 정보 안전하게 포함
try:
if request.is_json:
log_content["request_body"] = request.get_json()
except Exception as e:
log_content["request_body_error"] = str(e)


self.logger.info(
"HTTP Request",
extra={
Expand All @@ -139,30 +141,19 @@ def _log_request(self, response, processing_time: float):
def _log_error(self, error: CustomBaseException):
"""에러 로깅"""
try:
# 요청 컨텍스트 안전하게 추출
context = {
"method": getattr(request, 'method', 'UNKNOWN'),
"path": getattr(request, 'path', '/'),
"remote_addr": getattr(request, 'remote_addr', ''),
"user_agent": str(getattr(request, 'user_agent', '')),
"request_id": request.headers.get('X-Request-ID', 'unknown') if request else 'unknown'
}

# 로깅
self.logger.error(
"Application Error",
extra={
"labels": {
"error_type": str(error.error_type.value),
"request_id": context.get('request_id', 'unknown')
"request_id": request.headers.get('X-Request-ID', 'unknown') if request else 'unknown'
},
"content": {
**context,
"error_type": str(error.error_type.value),
"error_message": str(error.message),
"error_msg": str(error.error_msg or ''),
"status_code": error.status_code,
"timestamp": datetime.utcnow().isoformat()
}
}
)
Expand Down
60 changes: 60 additions & 0 deletions app/monitoring/ctf_metrics_collector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from prometheus_client import REGISTRY, Gauge, Counter, CONTENT_TYPE_LATEST

class ChallengeMetricsCollector:
def __init__(self):
# 챌린지 상태 Gauge
self.challenge_state = Gauge(
'challenge_state',
'Current state of challenges',
['challenge_id', 'username','state'] # 1: active, 0: inactive
)

# API 요청 결과 카운터
self.challenge_operations = Counter(
'challenge_operations_total',
'Challenge operation results',
['operation', 'result'] # operation: create/delete/status, result: success/error
)

# 레지스트리에 메트릭 등록
# self.register_metrics()

def register_metrics(self):
"""
Prometheus 레지스트리에 메트릭을 등록
"""
REGISTRY.register(self)

def collect(self):
"""
Prometheus Collector 인터페이스 구현 메서드
실제 메트릭을 수집하여 반환
"""
yield self.challenge_state
yield self.challenge_operations

def update_challenge_state(self, challenge_id: str, username: str, state: int):
"""
챌린지 상태 업데이트
:param challenge_id: 챌린지 ID
:param username: 사용자 이름
:param state: 상태 (1: active, 0: inactive)
"""
self.challenge_state.labels(
challenge_id=challenge_id,
username=username
).set(state)

def record_challenge_operation(self, operation: str, result: str):
"""
챌린지 작업 결과 기록
:param operation: 작업 유형 (create/delete/status)
:param result: 결과 (success/error)
"""
self.challenge_operations.labels(
operation=operation,
result=result
).inc()


challenge_metrics_collector = ChallengeMetricsCollector()
Loading