[FEAT] (프로젝트 구성): 초기 프로젝트 구성
v0.1 (2025-11-13) - Flask, Gunicorn, gevent, MariaDB, Redis, Swagger, logging 기본 구성 완료;
This commit is contained in:
37
.env.example
Normal file
37
.env.example
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# 환경 설정
|
||||||
|
DEBUG=True
|
||||||
|
|
||||||
|
# 서버 설정
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=5000
|
||||||
|
|
||||||
|
# 데이터베이스 설정
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=
|
||||||
|
DB_POOL_SIZE=10
|
||||||
|
DB_MAX_OVERFLOW=20
|
||||||
|
DB_POOL_RECYCLE=3600
|
||||||
|
|
||||||
|
# JWT 설정
|
||||||
|
JWT_SECRET_KEY=
|
||||||
|
JWT_ACCESS_TOKEN_HOURS=24
|
||||||
|
JWT_REFRESH_TOKEN_DAYS=30
|
||||||
|
|
||||||
|
# CORS 설정
|
||||||
|
CORS_ORIGINS=*
|
||||||
|
|
||||||
|
# Redis 설정
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_DB=0
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
# 로깅 설정
|
||||||
|
LOG_DIR=logs
|
||||||
|
LOG_LEVEL_TEXT=DEBUG
|
||||||
|
LOG_PREFIX=
|
||||||
|
MAX_LOG_SIZE=10485760
|
||||||
|
LOG_BACKUP_COUNT=5
|
||||||
65
.gitignore
vendored
Normal file
65
.gitignore
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Uploads
|
||||||
|
uploads/
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# NuriQ
|
||||||
|
|
||||||
|
Flask 기반 실시간 퀴즈 플랫폼 백엔드 API (Gunicorn + gevent)
|
||||||
85
src/app.py
Normal file
85
src/app.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"""
|
||||||
|
Flask 애플리케이션 메인 파일
|
||||||
|
Gunicorn + gevent 구조
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
# non-blocking으로 변경
|
||||||
|
from gevent import monkey
|
||||||
|
monkey.patch_all()
|
||||||
|
|
||||||
|
# 프로젝트 루트 경로 추가
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from flask import Flask, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
from extensions import db, migrate, jwt, redis_cli, custom_logger
|
||||||
|
from blueprints import register_blueprints
|
||||||
|
|
||||||
|
import settings
|
||||||
|
import constants
|
||||||
|
|
||||||
|
def create_app(config: Optional[Dict[str, Any]] = None) -> Flask:
|
||||||
|
"""Flask 애플리케이션 팩토리"""
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
app.config.from_object(settings)
|
||||||
|
|
||||||
|
if config:
|
||||||
|
app.config.update(config)
|
||||||
|
|
||||||
|
# 로깅 설정
|
||||||
|
logger = custom_logger(f"{settings.LOG_PREFIX}main_app")
|
||||||
|
|
||||||
|
# Extensions 초기화
|
||||||
|
db.init_app(app)
|
||||||
|
migrate.init_app(app, db)
|
||||||
|
jwt.init_app(app)
|
||||||
|
|
||||||
|
# Redis 초기화
|
||||||
|
redis_connected = redis_cli.start()
|
||||||
|
if not redis_connected:
|
||||||
|
logger.warning("Redis 연결에 실패하였습니다. Redis 없이 실행됩니다.")
|
||||||
|
|
||||||
|
# CORS 등록
|
||||||
|
CORS(app, origins=settings.CORS_ORIGINS)
|
||||||
|
|
||||||
|
# 에러 핸들러 등록
|
||||||
|
|
||||||
|
# 블루프린트 등록
|
||||||
|
register_blueprints(app)
|
||||||
|
|
||||||
|
@app.route('/api/v1/health')
|
||||||
|
def health_check():
|
||||||
|
return jsonify({
|
||||||
|
'status': 'healthy',
|
||||||
|
'database': 'connected' if db.engine else 'disconnected',
|
||||||
|
'redis': 'connected' if redis_cli.is_connected() else 'disconnected'
|
||||||
|
})
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""애플리케이션 실행"""
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
# 로그 정보
|
||||||
|
|
||||||
|
app.run(
|
||||||
|
host=settings.HOST,
|
||||||
|
port=settings.PORT,
|
||||||
|
debug=settings.DEBUG,
|
||||||
|
use_reloader=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Gunicorn이 import할 app 인스턴스
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
if __name__ =='__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
14
src/blueprints/__init__.py
Normal file
14
src/blueprints/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Blueprints 패키지
|
||||||
|
Flask Blueprint 관리
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
def register_blueprints(app: Flask) -> None:
|
||||||
|
|
||||||
|
from utils.swagger_config import api_blueprint
|
||||||
|
app.register_blueprint(api_blueprint, url_prefix='/api/v1')
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['register_blueprints']
|
||||||
6
src/constants.py
Normal file
6
src/constants.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# API INFO
|
||||||
|
API_VERSION = "v0.1"
|
||||||
|
API_DATE = 2025-11-13
|
||||||
|
|
||||||
|
# BASE DATE FORMAT
|
||||||
|
DATE_FORMAT = "%Y-%m-%d %H:%M:%S %z"
|
||||||
28
src/extensions.py
Normal file
28
src/extensions.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""
|
||||||
|
모든 외부 모듈, 서비스의 초기화
|
||||||
|
"""
|
||||||
|
|
||||||
|
import settings
|
||||||
|
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from flask_jwt_extended import JWTManager
|
||||||
|
|
||||||
|
from utils.logger_manager import *
|
||||||
|
|
||||||
|
from modules.redis.redis_facade import redis_cli
|
||||||
|
|
||||||
|
logger = custom_logger(f"{settings.LOG_PREFIX})extenstions")
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
logging.info("Extensions 초기화 시작 .. ")
|
||||||
|
|
||||||
|
db = SQLAlchemy()
|
||||||
|
migrate = Migrate()
|
||||||
|
jwt = JWTManager()
|
||||||
|
|
||||||
|
logging.info("Extensions 초기화 완료 .. ")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f" Fail Start Extensions.py => {str(e)}")
|
||||||
53
src/gunicorn_config.py
Normal file
53
src/gunicorn_config.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""
|
||||||
|
Gunicorn 설정 파일
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 프로젝트 루트 경로 추가
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
import settings
|
||||||
|
|
||||||
|
# Worker 설정
|
||||||
|
workers = settings.GUNICORN_WORKERS
|
||||||
|
worker_class = settings.GUNICORN_WORKER_CLASS
|
||||||
|
worker_connections = settings.GUNICORN_WORKER_CONNECTIONS
|
||||||
|
threads = settings.GUNICORN_THREADS
|
||||||
|
|
||||||
|
# 서버 바인딩
|
||||||
|
bind = f'{settings.HOST}:{settings.PORT}'
|
||||||
|
|
||||||
|
# 타임아웃 설정
|
||||||
|
timeout = settings.GUNICORN_TIMEOUT
|
||||||
|
keepalive = settings.GUNICORN_KEEPALIVE
|
||||||
|
graceful_timeout = settings.GUNICORN_GRACEFUL_TIMEOUT
|
||||||
|
|
||||||
|
# 재시작 설정 (메모리 누수 방지)
|
||||||
|
max_requests = settings.GUNICORN_MAX_REQUESTS
|
||||||
|
max_requests_jitter = settings.GUNICORN_MAX_REQUESTS_JITTER
|
||||||
|
|
||||||
|
# 로깅 설정
|
||||||
|
accesslog = settings.GUNICORN_ACCESSLOG
|
||||||
|
errorlog = settings.GUNICORN_ERRORLOG
|
||||||
|
loglevel = settings.GUNICORN_LOGLEVEL
|
||||||
|
|
||||||
|
# 프로세스 네임
|
||||||
|
proc_name = "nuriq_server"
|
||||||
|
|
||||||
|
# hook 함수
|
||||||
|
def on_starting(server):
|
||||||
|
"""
|
||||||
|
Gunicorn 마스터 프로세스가 시작될 때 한번만 실행
|
||||||
|
워커가 생성되기 전 실행, 전역 초기화 작업
|
||||||
|
"""
|
||||||
|
server.log.info("[Gunicorn] 마스터 프로세스 시작 중 ...")
|
||||||
|
|
||||||
|
# Flask 앱 컨텍스트 내에서 초기화 작업 수행
|
||||||
|
from app import create_app
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
def post_worker_init(worker):
|
||||||
|
"""각 워커 프로세스 초기화된 후 사용"""
|
||||||
|
worker.log.info(f"Worker {worker.pid} 초기화 완료")
|
||||||
0
src/models/__init__.py
Normal file
0
src/models/__init__.py
Normal file
0
src/modules/__init__.py
Normal file
0
src/modules/__init__.py
Normal file
66
src/modules/redis/redis_facade.py
Normal file
66
src/modules/redis/redis_facade.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
Redis 클라이언트 전체 관리 모듈
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
import redis
|
||||||
|
import settings
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class RedisFacadeOpr():
|
||||||
|
"""Redis 연결 및 작업을 관리하는 Facade 클래스"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Redis 설정 초기화"""
|
||||||
|
self.redis_config = {
|
||||||
|
'host': settings.REDIS_HOST,
|
||||||
|
'port': settings.REDIS_PORT,
|
||||||
|
'password': settings.REDIS_PASSWORD,
|
||||||
|
'decode_responses': True, # auto-decode response
|
||||||
|
'db': settings.REDIS_DB
|
||||||
|
}
|
||||||
|
|
||||||
|
self.redis_client: Optional[redis.StrictRedis] = None
|
||||||
|
|
||||||
|
def start(self) -> bool:
|
||||||
|
"""Redis 연결 시작
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 연결 성공 시 True, 실패 시 False
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Redis Client 연결
|
||||||
|
self.redis_client = redis.StrictRedis(**self.redis_config)
|
||||||
|
# 연결 테스트
|
||||||
|
self.redis_client.ping()
|
||||||
|
logging.info("Redis connection established successfully")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to connect to Redis: {str(e)}")
|
||||||
|
self.redis_client = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
def reset_database(self) -> None:
|
||||||
|
"""Redis 데이터베이스 초기화"""
|
||||||
|
if self.redis_client:
|
||||||
|
self.redis_client.flushdb()
|
||||||
|
logging.info("Redis database flushed")
|
||||||
|
else:
|
||||||
|
logging.warning("Cannot reset database: Redis client not connected")
|
||||||
|
|
||||||
|
def refresh_init_database(self) -> None:
|
||||||
|
"""개별 데이터 초기화"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""Redis 연결 상태 확인"""
|
||||||
|
if not self.redis_client:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
self.redis_client.ping()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
redis_cli = RedisFacadeOpr()
|
||||||
0
src/repositories/__init__.py
Normal file
0
src/repositories/__init__.py
Normal file
35
src/requirements.txt
Normal file
35
src/requirements.txt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Flask Core
|
||||||
|
Flask
|
||||||
|
Flask-RESTful
|
||||||
|
Flask-SQLAlchemy
|
||||||
|
Flask-Migrate
|
||||||
|
Flask-CORS
|
||||||
|
Flask-JWT-Extended
|
||||||
|
flask_restx
|
||||||
|
|
||||||
|
# Database
|
||||||
|
PyMySQL
|
||||||
|
SQLAlchemy
|
||||||
|
|
||||||
|
# HTTP Client
|
||||||
|
requests
|
||||||
|
|
||||||
|
# Background Tasks
|
||||||
|
redis
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
python-dotenv
|
||||||
|
|
||||||
|
# Utilities
|
||||||
|
psutil
|
||||||
|
|
||||||
|
# Password Hashing
|
||||||
|
bcrypt
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
marshmallow
|
||||||
|
pydantic
|
||||||
|
|
||||||
|
# WSGI Server
|
||||||
|
gunicorn
|
||||||
|
gevent
|
||||||
67
src/run_server.sh
Normal file
67
src/run_server.sh
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
##############################################################
|
||||||
|
# NuriQ 서버 실행 스크립트
|
||||||
|
#
|
||||||
|
# 사용법:
|
||||||
|
# ./run_server.sh [모드]
|
||||||
|
#
|
||||||
|
# 모드:
|
||||||
|
# dev - 개발 모드 (Flask built-in server)
|
||||||
|
# prod - 운영 모드 (Gunicorn + gevent)
|
||||||
|
##############################################################
|
||||||
|
|
||||||
|
set -e #에러 발생시 스크립트 중단
|
||||||
|
|
||||||
|
# 색상 정리
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# 현재 스크립트 디렉토리로 이동
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# 모드 설정 (기본값: prod)
|
||||||
|
MODE="${1:-prod}"
|
||||||
|
|
||||||
|
echo -e "${BLUE}========================================="
|
||||||
|
echo -e "${BLUE} NuriQ Server Start${NC} "
|
||||||
|
echo -e "${BLUE}========================================="
|
||||||
|
|
||||||
|
# 가상환경 활성화 확인
|
||||||
|
if [ -z "$VIRTUAL_ENV" ]; then
|
||||||
|
echo -e "${YELLOW} Virtual environment not activated${NC}"
|
||||||
|
echo -e "${YELLOW} Consider running: source .venv/bin/activate${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 모드에 따른 처리
|
||||||
|
case "$MODE" in
|
||||||
|
dev)
|
||||||
|
echo -e "${GREEN} 개발 모드 시작 중 ...${NC}"
|
||||||
|
echo -e "${GREEN} Flask built-in server 사용 중 ...${NC}"
|
||||||
|
echo ""
|
||||||
|
export DEBUG=True
|
||||||
|
gunicorn -c ./gunicorn_config.py app:app
|
||||||
|
;;
|
||||||
|
|
||||||
|
prod)
|
||||||
|
echo -e "${GREEN} 운영 모드 시작 중 ...${NC}"
|
||||||
|
echo -e "${YELLOW} Gunicorn + gevent 사용 중 ...${NC}"
|
||||||
|
echo ""
|
||||||
|
gunicorn -c ./gunicorn_config.py \
|
||||||
|
--worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker \
|
||||||
|
app:app
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "{$RED} 알 수 없는 모드: $MODE${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "사용 가능한 모드"
|
||||||
|
echo " dev - 개발 모드 (Flask built-in server)"
|
||||||
|
echo " prod - 운영 모드 (Gunicorn + gevent)"
|
||||||
|
echo ""
|
||||||
|
echo "사용: $0 [mode]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
0
src/services/__init__.py
Normal file
0
src/services/__init__.py
Normal file
80
src/settings.py
Normal file
80
src/settings.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"""
|
||||||
|
애플리케이션 설정 모듈
|
||||||
|
환경 변수 및 전역 설정 관리
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import timedelta
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
# .env 파일 로드
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# 환경설정
|
||||||
|
DEBUG = os.getenv('DEBUG', False)
|
||||||
|
|
||||||
|
# 서버
|
||||||
|
HOST = os.getenv('HOST', '0.0.0.0')
|
||||||
|
PORT = int(os.getenv('PORT', '5000'))
|
||||||
|
|
||||||
|
# 데이터베이스 설정
|
||||||
|
DB_HOST = os.getenv('DB_HOST', 'localhost')
|
||||||
|
DB_PORT = int(os.getenv('DB_PORT', 5000))
|
||||||
|
DB_USER = os.getenv('DB_USER', 'root')
|
||||||
|
DB_PASSWORD = os.getenv('DB_PASSWORD', '')
|
||||||
|
DB_NAME = os.getenv('DB_NAME', 'test')
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4"
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
SQLALCHEMY_ECHO = DEBUG
|
||||||
|
SQLALCHEMY_POOL_SIZE = int(os.getenv('DB_POOL_SIZE', 10))
|
||||||
|
SQLALCHEMY_MAX_OVERFLOW = int(os.getenv('DB_MAX_OVERFLOW', 20))
|
||||||
|
SQLALCHEMY_POOL_RECYCLE = int(os.getenv('DB_POOL_RECYCLE', 3600))
|
||||||
|
|
||||||
|
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', 'your-secret-key-change-this-in-production')
|
||||||
|
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=int(os.getenv('JWT_ACCESS_TOKEN_HOURS', 24)))
|
||||||
|
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=int(os.getenv('JWT_REFRESH_TOKEN_DAYS', 30)))
|
||||||
|
JWT_TOKEN_LOCATION = ['headers']
|
||||||
|
JWT_HEADER_NAME = 'Authorization'
|
||||||
|
JWT_HEADER_TYPE = 'Bearer'
|
||||||
|
JWT_ENCODE_JTI = True # JWT ID 포함 (블랙리스트 추적용)
|
||||||
|
|
||||||
|
# CORS 설정
|
||||||
|
CORS_ORIGINS = os.getenv('CORS_ORIGINS', '*').split(',')
|
||||||
|
CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization']
|
||||||
|
CORS_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS']
|
||||||
|
|
||||||
|
# Redis 설정
|
||||||
|
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
|
||||||
|
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
|
||||||
|
REDIS_DB = int(os.getenv('REDIS_DB', 0))
|
||||||
|
REDIS_PASSWORD = os.getenv('REDIS_PASSWORD', None)
|
||||||
|
|
||||||
|
# 로깅 설정
|
||||||
|
LOG_DIR = os.getenv('LOG_DIR', 'logs')
|
||||||
|
LOG_PREFIX = os.getenv('LOG_PREFIX', 'default.')
|
||||||
|
LOG_LEVEL_TEXT = os.getenv('LOG_LEVEL_TEXT', 'INFO' if not DEBUG else 'DEBUG')
|
||||||
|
MAX_LOG_SIZE = int(os.getenv('MAX_LOG_SIZE', 10 * 1024 * 1024)) # 10MB
|
||||||
|
LOG_BACKUP_COUNT = int(os.getenv('LOG_BACKUP_COUNT', 5))
|
||||||
|
|
||||||
|
# Worker 설정
|
||||||
|
GUNICORN_WORKERS = int(os.getenv('GUNICORN_WORKERS', multiprocessing.cpu_count() * 2 + 1))
|
||||||
|
GUNICORN_WORKER_CLASS = os.getenv('GUNICORN_WORKER_CLASS', 'gevent') # gevent for async I/O
|
||||||
|
GUNICORN_WORKER_CONNECTIONS = int(os.getenv('GUNICORN_WORKER_CONNECTIONS', 1000))
|
||||||
|
GUNICORN_THREADS = int(os.getenv('GUNICORN_THREADS', 1)) # gevent 사용 시 무시됨
|
||||||
|
|
||||||
|
# 타임아웃 설정
|
||||||
|
GUNICORN_TIMEOUT = int(os.getenv('GUNICORN_TIMEOUT', 30)) # 요청 타임아웃 (초)
|
||||||
|
GUNICORN_KEEPALIVE = int(os.getenv('GUNICORN_KEEPALIVE', 5)) # Keep-Alive 연결 유지 시간
|
||||||
|
GUNICORN_GRACEFUL_TIMEOUT = int(os.getenv('GUNICORN_GRACEFUL_TIMEOUT', 30)) # Graceful shutdown 대기 시간
|
||||||
|
|
||||||
|
# 재시작 설정 (메모리 누수 방지)
|
||||||
|
GUNICORN_MAX_REQUESTS = int(os.getenv('GUNICORN_MAX_REQUESTS', 1000))
|
||||||
|
GUNICORN_MAX_REQUESTS_JITTER = int(os.getenv('GUNICORN_MAX_REQUESTS_JITTER', 50))
|
||||||
|
|
||||||
|
# 로깅 설정
|
||||||
|
GUNICORN_ACCESSLOG = os.getenv('GUNICORN_ACCESSLOG', '-') # '-' = stdout
|
||||||
|
GUNICORN_ERRORLOG = os.getenv('GUNICORN_ERRORLOG', '-')
|
||||||
|
GUNICORN_LOGLEVEL = os.getenv('GUNICORN_LOGLEVEL', 'info')
|
||||||
0
src/test/__init__.py
Normal file
0
src/test/__init__.py
Normal file
100
src/utils/logger_manager.py
Normal file
100
src/utils/logger_manager.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import constants
|
||||||
|
|
||||||
|
import sys, time
|
||||||
|
import builtins, logging
|
||||||
|
import settings
|
||||||
|
|
||||||
|
# base logging format
|
||||||
|
log_format = "[%(asctime)s] [%(levelname)s] (%(name)s) %(message)s"
|
||||||
|
|
||||||
|
# date-time format
|
||||||
|
date_format = constants.DATE_FORMAT
|
||||||
|
|
||||||
|
# formatter for logger
|
||||||
|
formatter = logging.Formatter(log_format, date_format)
|
||||||
|
|
||||||
|
# class for redirect STD to Logging
|
||||||
|
class StdLogRedirect:
|
||||||
|
|
||||||
|
def __init__(self, stream, formatter, level=logging.ERROR, name="stderr"):
|
||||||
|
self.stream = stream
|
||||||
|
self.formatter = formatter
|
||||||
|
self.level = level
|
||||||
|
self.name = name
|
||||||
|
self._buf = ""
|
||||||
|
|
||||||
|
def _emit_line(self, line: str):
|
||||||
|
rec = logging.LogRecord(
|
||||||
|
name=self.name,
|
||||||
|
level=self.level,
|
||||||
|
pathname="<stderr>",
|
||||||
|
lineno=0,
|
||||||
|
msg=line,
|
||||||
|
args=(),
|
||||||
|
exc_info=None,
|
||||||
|
)
|
||||||
|
sys.stdout.write(self.formatter.format(rec) + "\n")
|
||||||
|
|
||||||
|
def write(self, s):
|
||||||
|
if isinstance(s, bytes):
|
||||||
|
s = s.decode(errors="replace")
|
||||||
|
self._buf += s
|
||||||
|
while True:
|
||||||
|
idx = self._bug.find("\n")
|
||||||
|
if idx == -1:
|
||||||
|
break
|
||||||
|
line = self._buf[:idx+1:]
|
||||||
|
self._buf = self._buf[idx+1:]
|
||||||
|
if line:
|
||||||
|
self._emit_line(line)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
if self._buf:
|
||||||
|
self._emit_line(self._buf.rstrip("\r"))
|
||||||
|
self._buf = ""
|
||||||
|
self.stream.flush()
|
||||||
|
|
||||||
|
def isatty(self):
|
||||||
|
return self.stream.isatty()
|
||||||
|
|
||||||
|
# redirect STDERR to STDOUT using usual Logging format
|
||||||
|
sys.stderr = StdLogRedirect(sys.stderr, formatter, level=logging.ERROR, name="external.stderr")
|
||||||
|
|
||||||
|
# redirect print to Logging
|
||||||
|
_builtin_print = print
|
||||||
|
def print_as_log(*args, level=logging.NOTSET, name="unknown.print", **kwargs):
|
||||||
|
record = logging.LogRecord(
|
||||||
|
name=name,
|
||||||
|
level=level,
|
||||||
|
pathname=__file__,
|
||||||
|
lineno=0,
|
||||||
|
msg=" ".join(str(a) for a in args),
|
||||||
|
args=(),
|
||||||
|
exc_info=None,
|
||||||
|
)
|
||||||
|
_builtin_print(formatter.format(record), **kwargs)
|
||||||
|
builtins.print = print_as_log
|
||||||
|
|
||||||
|
# logging basic config
|
||||||
|
logging.basicConfig(
|
||||||
|
level=settings.LOG_LEVEL_TEXT,
|
||||||
|
format=log_format,
|
||||||
|
datefmt=date_format,
|
||||||
|
stream=sys.stdout
|
||||||
|
)
|
||||||
|
|
||||||
|
# create own logger class to prevent init custom loggers by other libs
|
||||||
|
class GatekeeperLogger(logging.Logger):
|
||||||
|
def assHandler(self, h):
|
||||||
|
# only 'root', '__main__' ans own loggers will be accepted
|
||||||
|
if self.name == "root" or self.name.startswith(('LOG_PREFIX'), "__main__"):
|
||||||
|
return super().addHandler(h)
|
||||||
|
|
||||||
|
logging.setLoggerClass(GatekeeperLogger)
|
||||||
|
|
||||||
|
def custom_logger(log_prefix: str):
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(log_prefix)
|
||||||
|
logger.setLevel(settings.LOG_LEVEL_TEXT)
|
||||||
|
return logger
|
||||||
34
src/utils/swagger_config.py
Normal file
34
src/utils/swagger_config.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""
|
||||||
|
Swagger 설정
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Third-Party Library Imports
|
||||||
|
from flask import Blueprint, jsonify
|
||||||
|
from flask_restx import Api
|
||||||
|
from flask_jwt_extended.exceptions import JWTExtendedException, RevokedTokenError
|
||||||
|
|
||||||
|
import constants
|
||||||
|
|
||||||
|
# Swagger API Blueprint 및 설정
|
||||||
|
api_blueprint = Blueprint('api', __name__)
|
||||||
|
|
||||||
|
api = Api(
|
||||||
|
api_blueprint,
|
||||||
|
title='NuriQ API',
|
||||||
|
version=constants.API_VERSION,
|
||||||
|
description='NuriQ REST API 문서',
|
||||||
|
doc='/docs',
|
||||||
|
authorizations={
|
||||||
|
'Bearer': {
|
||||||
|
'type': 'apiKey',
|
||||||
|
'in': 'header',
|
||||||
|
'name': 'Authorization',
|
||||||
|
'description': 'JWT 토큰 입력 (예: Bearer <token>)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
security='Bearer',
|
||||||
|
validate=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# 네임 스페이스 정의 및 API에 추가
|
||||||
|
__all__ = ['api_blueprint', 'api']
|
||||||
Reference in New Issue
Block a user