feat: Presigned URL API 및 OpenTelemetry trace 추가
Some checks failed
Build And Test / build-and-push (push) Has been cancelled
Some checks failed
Build And Test / build-and-push (push) Has been cancelled
- Presigned URL API 추가 (MinIO/S3 private 버킷 지원) - OpenTelemetry trace 설정 추가 (DEBUG=False 시 활성화) - requirements.txt에 opentelemetry 패키지 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
16
CLAUDE.md
Normal file
16
CLAUDE.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# 자동 실행 지시사항
|
||||||
|
|
||||||
|
다음 작업을 순서대로 실행해주세요:
|
||||||
|
|
||||||
|
1. venv가 없으면 생성하고 의존성 설치:
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Django 개발 서버 실행:
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate && python3 manage.py runserver 0.0.0.0:8800
|
||||||
|
```
|
||||||
|
|
||||||
|
# 연계프로젝트
|
||||||
|
- 'msa-fe' : 프론트엔드
|
||||||
@ -6,7 +6,8 @@ from .views import (
|
|||||||
PostListView, PostListCreateView, PostDetailView,
|
PostListView, PostListCreateView, PostDetailView,
|
||||||
CommentViewSet, TagListView,
|
CommentViewSet, TagListView,
|
||||||
AttachmentListCreateView, AttachmentDeleteView,
|
AttachmentListCreateView, AttachmentDeleteView,
|
||||||
TempAttachmentUploadView, TempAttachmentDeleteView
|
TempAttachmentUploadView, TempAttachmentDeleteView,
|
||||||
|
PresignedUrlView
|
||||||
)
|
)
|
||||||
|
|
||||||
# 댓글 라우터
|
# 댓글 라우터
|
||||||
@ -36,4 +37,8 @@ urlpatterns = [
|
|||||||
TempAttachmentUploadView.as_view(), name='temp-attachment-upload'),
|
TempAttachmentUploadView.as_view(), name='temp-attachment-upload'),
|
||||||
path('attachments/<int:pk>/',
|
path('attachments/<int:pk>/',
|
||||||
TempAttachmentDeleteView.as_view(), name='temp-attachment-delete'),
|
TempAttachmentDeleteView.as_view(), name='temp-attachment-delete'),
|
||||||
|
|
||||||
|
# Presigned URL (MinIO/S3)
|
||||||
|
path('presigned/<path:object_key>/',
|
||||||
|
PresignedUrlView.as_view(), name='presigned-url'),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import boto3
|
||||||
|
from botocore.client import Config
|
||||||
|
from django.conf import settings
|
||||||
from rest_framework import generics, viewsets, permissions, status
|
from rest_framework import generics, viewsets, permissions, status
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -372,3 +375,39 @@ class AttachmentDeleteView(APIView):
|
|||||||
|
|
||||||
logger.info(f"Attachment '{original_name}' deleted from post {post_pk} by {username}.")
|
logger.info(f"Attachment '{original_name}' deleted from post {post_pk} by {username}.")
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class PresignedUrlView(APIView):
|
||||||
|
"""Presigned URL 생성 (MinIO/S3)"""
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def get_s3_client(self):
|
||||||
|
"""S3 클라이언트 생성"""
|
||||||
|
return boto3.client(
|
||||||
|
's3',
|
||||||
|
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
|
||||||
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
|
config=Config(signature_version='s3v4'),
|
||||||
|
region_name=settings.AWS_S3_REGION_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, request, object_key):
|
||||||
|
"""GET presigned URL 생성"""
|
||||||
|
try:
|
||||||
|
s3_client = self.get_s3_client()
|
||||||
|
presigned_url = s3_client.generate_presigned_url(
|
||||||
|
'get_object',
|
||||||
|
Params={
|
||||||
|
'Bucket': settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
|
'Key': object_key,
|
||||||
|
},
|
||||||
|
ExpiresIn=3600, # 1시간 유효
|
||||||
|
)
|
||||||
|
return Response({'url': presigned_url})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Presigned URL 생성 실패: {e}")
|
||||||
|
return Response(
|
||||||
|
{'detail': 'Presigned URL 생성 실패'},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
@ -37,6 +37,11 @@ else:
|
|||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-ec9me^z%x7-2vwee5#qq(kvn@^cs!!22_*f-im(320_k5-=0j5')
|
SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-ec9me^z%x7-2vwee5#qq(kvn@^cs!!22_*f-im(320_k5-=0j5')
|
||||||
|
|
||||||
|
# OpenTelemetry Trace 설정
|
||||||
|
SERVICE_PLATFORM = os.getenv("SERVICE_PLATFORM", "none")
|
||||||
|
TRACE_SERVICE_NAME = os.getenv("TRACE_SERVICE_NAME", "msa-django-blog")
|
||||||
|
TRACE_ENDPOINT = os.getenv("TRACE_ENDPOINT", "none")
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = int(os.environ.get('DEBUG', 1))
|
DEBUG = int(os.environ.get('DEBUG', 1))
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,76 @@ https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
# Django 설정을 미리 불러온다
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_prj.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_prj.settings')
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
# DEBUG 모드 아닐 때만 OpenTelemetry 활성
|
||||||
|
if not settings.DEBUG:
|
||||||
|
import grpc
|
||||||
|
from opentelemetry import trace
|
||||||
|
from opentelemetry.sdk.resources import Resource
|
||||||
|
from opentelemetry.sdk.trace import TracerProvider
|
||||||
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||||
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||||
|
from opentelemetry.instrumentation.django import DjangoInstrumentor
|
||||||
|
from opentelemetry.instrumentation.requests import RequestsInstrumentor
|
||||||
|
from opentelemetry.instrumentation.logging import LoggingInstrumentor
|
||||||
|
from opentelemetry.instrumentation.dbapi import trace_integration
|
||||||
|
import MySQLdb
|
||||||
|
|
||||||
|
trace.set_tracer_provider(
|
||||||
|
TracerProvider(
|
||||||
|
resource=Resource.create({
|
||||||
|
"service.platform": settings.SERVICE_PLATFORM,
|
||||||
|
"service.name": settings.TRACE_SERVICE_NAME,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# TRACE_CA_CERT 설정에 따른 gRPC credentials 구성
|
||||||
|
credentials = None
|
||||||
|
ca_cert_path = os.getenv('TRACE_CA_CERT', '').strip()
|
||||||
|
if ca_cert_path and os.path.exists(ca_cert_path):
|
||||||
|
with open(ca_cert_path, 'rb') as f:
|
||||||
|
ca_cert = f.read()
|
||||||
|
credentials = grpc.ssl_channel_credentials(root_certificates=ca_cert)
|
||||||
|
insecure = False
|
||||||
|
else:
|
||||||
|
insecure = True
|
||||||
|
|
||||||
|
otlp_exporter = OTLPSpanExporter(
|
||||||
|
endpoint=settings.TRACE_ENDPOINT,
|
||||||
|
insecure=insecure,
|
||||||
|
credentials=credentials,
|
||||||
|
headers={
|
||||||
|
"x-scope-orgid": settings.SERVICE_PLATFORM,
|
||||||
|
"x-service": settings.TRACE_SERVICE_NAME
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
trace.get_tracer_provider().add_span_processor(
|
||||||
|
BatchSpanProcessor(otlp_exporter)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Django 요청/응답 추적
|
||||||
|
DjangoInstrumentor().instrument()
|
||||||
|
|
||||||
|
# HTTP 클라이언트 요청 추적 (requests 라이브러리)
|
||||||
|
RequestsInstrumentor().instrument()
|
||||||
|
|
||||||
|
# 로그와 Trace 연동 (trace_id, span_id를 로그에 자동 추가)
|
||||||
|
LoggingInstrumentor().instrument(set_logging_format=True)
|
||||||
|
|
||||||
|
# MySQL DB 쿼리 추적
|
||||||
|
trace_integration(
|
||||||
|
MySQLdb,
|
||||||
|
"connect",
|
||||||
|
"mysql",
|
||||||
|
capture_parameters=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
|||||||
@ -35,3 +35,23 @@ sqlparse==0.5.3
|
|||||||
typing_extensions==4.13.2
|
typing_extensions==4.13.2
|
||||||
uritemplate==4.1.1
|
uritemplate==4.1.1
|
||||||
urllib3==2.4.0
|
urllib3==2.4.0
|
||||||
|
# OpenTelemetry Trace
|
||||||
|
googleapis-common-protos==1.70.0
|
||||||
|
grpcio==1.72.1
|
||||||
|
opentelemetry-api==1.34.0
|
||||||
|
opentelemetry-exporter-otlp==1.34.0
|
||||||
|
opentelemetry-exporter-otlp-proto-common==1.34.0
|
||||||
|
opentelemetry-exporter-otlp-proto-grpc==1.34.0
|
||||||
|
opentelemetry-exporter-otlp-proto-http==1.34.0
|
||||||
|
opentelemetry-instrumentation==0.55b0
|
||||||
|
opentelemetry-instrumentation-dbapi==0.55b0
|
||||||
|
opentelemetry-instrumentation-django==0.55b0
|
||||||
|
opentelemetry-instrumentation-logging==0.55b0
|
||||||
|
opentelemetry-instrumentation-requests==0.55b0
|
||||||
|
opentelemetry-instrumentation-wsgi==0.55b0
|
||||||
|
opentelemetry-proto==1.34.0
|
||||||
|
opentelemetry-sdk==1.34.0
|
||||||
|
opentelemetry-semantic-conventions==0.55b0
|
||||||
|
opentelemetry-util-http==0.55b0
|
||||||
|
protobuf==5.29.5
|
||||||
|
wrapt==1.17.2
|
||||||
|
|||||||
Reference in New Issue
Block a user