diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..98a5b54 --- /dev/null +++ b/CLAUDE.md @@ -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' : 프론트엔드 \ No newline at end of file diff --git a/blog/urls.py b/blog/urls.py index 1579f5d..f3d0f40 100644 --- a/blog/urls.py +++ b/blog/urls.py @@ -6,7 +6,8 @@ from .views import ( PostListView, PostListCreateView, PostDetailView, CommentViewSet, TagListView, AttachmentListCreateView, AttachmentDeleteView, - TempAttachmentUploadView, TempAttachmentDeleteView + TempAttachmentUploadView, TempAttachmentDeleteView, + PresignedUrlView ) # 댓글 라우터 @@ -36,4 +37,8 @@ urlpatterns = [ TempAttachmentUploadView.as_view(), name='temp-attachment-upload'), path('attachments//', TempAttachmentDeleteView.as_view(), name='temp-attachment-delete'), + + # Presigned URL (MinIO/S3) + path('presigned//', + PresignedUrlView.as_view(), name='presigned-url'), ] diff --git a/blog/views.py b/blog/views.py index ff4bbcc..d408911 100644 --- a/blog/views.py +++ b/blog/views.py @@ -2,6 +2,9 @@ import os 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.exceptions import PermissionDenied from rest_framework.response import Response @@ -371,4 +374,40 @@ class AttachmentDeleteView(APIView): attachment.delete() logger.info(f"Attachment '{original_name}' deleted from post {post_pk} by {username}.") - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + 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 + ) \ No newline at end of file diff --git a/blog_prj/settings.py b/blog_prj/settings.py index 576d700..77ad79f 100644 --- a/blog_prj/settings.py +++ b/blog_prj/settings.py @@ -37,6 +37,11 @@ else: # 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') +# 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! DEBUG = int(os.environ.get('DEBUG', 1)) diff --git a/blog_prj/wsgi.py b/blog_prj/wsgi.py index 9915607..4943544 100644 --- a/blog_prj/wsgi.py +++ b/blog_prj/wsgi.py @@ -9,8 +9,76 @@ https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ import os -from django.core.wsgi import get_wsgi_application - +# Django 설정을 미리 불러온다 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() diff --git a/requirements.txt b/requirements.txt index 452d15c..0a96989 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,3 +35,23 @@ sqlparse==0.5.3 typing_extensions==4.13.2 uritemplate==4.1.1 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 diff --git a/version b/version index b52cc10..71ad83c 100644 --- a/version +++ b/version @@ -1 +1 @@ -v0.0.12 \ No newline at end of file +v0.0.13 \ No newline at end of file