diff --git a/apiserver/plane/app/views/workspace/member.py b/apiserver/plane/app/views/workspace/member.py index ce77f70e0b..528ac40b9c 100644 --- a/apiserver/plane/app/views/workspace/member.py +++ b/apiserver/plane/app/views/workspace/member.py @@ -36,6 +36,7 @@ from plane.db.models import ( WorkspaceMember, ) from plane.utils.cache import cache_response, invalidate_cache +from plane.utils.tracer import trace_operation from .. import BaseViewSet @@ -65,6 +66,7 @@ class WorkSpaceMemberViewSet(BaseViewSet): @allow_permission( allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" ) + @trace_operation("workspace_member_list", custom_attribute="value") def list(self, request, slug): workspace_member = WorkspaceMember.objects.get( member=request.user, diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index 007d8cecff..d9164431dc 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -16,6 +16,15 @@ from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.redis import RedisIntegration from corsheaders.defaults import default_headers +# OpenTelemetry +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.sdk.resources import Resource + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Secret Key @@ -24,6 +33,25 @@ SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key()) # SECURITY WARNING: don't run with debug turned on in production! DEBUG = int(os.environ.get("DEBUG", "0")) +# Check if telemetry should be enabled +TELEMETRY_ENABLED = ( + os.environ.get("ENABLE_TELEMETRY", "True").lower() == "true" +) + +if TELEMETRY_ENABLED: + # Configure the tracer provider + service_name = os.environ.get("SERVICE_NAME", "plane-ce") + resource = Resource.create({"service.name": service_name}) + trace.set_tracer_provider(TracerProvider(resource=resource)) + # Configure the OTLP exporter + otel_endpoint = os.environ.get("OTLP_ENDPOINT", "https://ingest.plane.so") + otlp_exporter = OTLPSpanExporter(endpoint=otel_endpoint) + span_processor = BatchSpanProcessor(otlp_exporter) + trace.get_tracer_provider().add_span_processor(span_processor) +else: + # Set up a no-op tracer when telemetry is disabled + trace.set_tracer_provider(TracerProvider()) + # Allowed Hosts ALLOWED_HOSTS = ["*"] @@ -53,6 +81,7 @@ INSTALLED_APPS = [ # Middlewares MIDDLEWARE = [ + "opentelemetry.instrumentation.django.middleware.OpenTelemetryMiddleware", "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "plane.authentication.middleware.session.SessionMiddleware", @@ -64,6 +93,11 @@ MIDDLEWARE = [ "django.middleware.gzip.GZipMiddleware", "plane.middleware.api_log_middleware.APITokenLogMiddleware", ] +if TELEMETRY_ENABLED: + MIDDLEWARE.insert( + 0, + "opentelemetry.instrumentation.django.middleware.OpenTelemetryMiddleware", + ) # Rest Framework settings REST_FRAMEWORK = { diff --git a/apiserver/plane/utils/tracer.py b/apiserver/plane/utils/tracer.py new file mode 100644 index 0000000000..831472cbfd --- /dev/null +++ b/apiserver/plane/utils/tracer.py @@ -0,0 +1,26 @@ +from opentelemetry import trace +from django.conf import settings +from functools import wraps + +tracer = trace.get_tracer(__name__) + + +def trace_operation(operation_name, **attributes): + def wrapper(func): + @wraps(func) + def traced_func(*args, **kwargs): + if settings.TELEMETRY_ENABLED: + with tracer.start_as_current_span(operation_name) as span: + for key, value in attributes.items(): + span.set_attribute(key, value) + result = func(*args, **kwargs) + span.add_event( + "operation_completed", {"result": str(result)} + ) + return result + else: + return func(*args, **kwargs) + + return traced_func + + return wrapper diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index d06438593d..8b96cb9979 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -62,3 +62,8 @@ zxcvbn==4.4.28 pytz==2024.1 # jwt PyJWT==2.8.0 +# OpenTelemetry +opentelemetry-api==1.27.0 +opentelemetry-sdk==1.27.0 +opentelemetry-instrumentation-django==0.48b0 +opentelemetry-exporter-otlp==1.27.0 \ No newline at end of file