diff --git a/app/__init__.py b/app/__init__.py index 114d856d1f..8673ea33d2 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -135,6 +135,8 @@ from app.notify_client.upload_api_client import upload_api_client # noqa from app.notify_client.user_api_client import user_api_client # noqa from app.notify_session import NotifyAdminSessionInterface +from app.otel.metrics import otel_metrics +from app.otel.traces import otel_traces from app.s3_client.logo_client import logo_client from app.template_previews import template_preview_client # noqa from app.url_converters import ( @@ -179,6 +181,9 @@ def create_app(application): init_app(application) + otel_metrics.init_app(application) + otel_traces.init_app(application) + if "extensions" not in application.jinja_options: application.jinja_options["extensions"] = [] diff --git a/app/config.py b/app/config.py index b2b1b38b2a..b6cf19b4e8 100644 --- a/app/config.py +++ b/app/config.py @@ -9,6 +9,10 @@ class Config: DANGEROUS_SALT = os.environ.get("DANGEROUS_SALT") ZENDESK_API_KEY = os.environ.get("ZENDESK_API_KEY") + OTEL_EXPORT_TYPE = os.getenv("OTEL_EXPORT_TYPE", "otlp") + OTEL_COLLECTOR_ENDPOINT = os.getenv("OTEL_COLLECTOR_ENDPOINT", "localhost:4317") + OTEL_INSTRUMENTATIONS = os.getenv("OTEL_INSTRUMENTATIONS", "wsgi,celery,flask,redis,sqlalchemy,requests") + # if we're not on cloudfoundry, we can get to this app from localhost. but on cloudfoundry its different ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", "http://localhost:6012") @@ -118,6 +122,7 @@ class Development(Config): S3_BUCKET_REPORT_REQUESTS_DOWNLOAD = "development-report-requests-download" LOGO_CDN_DOMAIN = "static-logos.notify.tools" + OTEL_EXPORT_TYPE = os.getenv("OTEL_EXPORT_TYPE", "none") ADMIN_CLIENT_SECRET = "dev-notify-secret-key" DANGEROUS_SALT = "dev-notify-salt" @@ -155,6 +160,7 @@ class Test(Development): ASSET_DOMAIN = "static.example.com" ASSET_PATH = "https://static.example.com/" + OTEL_EXPORT_TYPE = os.getenv("OTEL_EXPORT_TYPE", "none") class CloudFoundryConfig(Config): diff --git a/app/main/views/templates.py b/app/main/views/templates.py index fcb2fc74ae..15f63b6d09 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -22,6 +22,9 @@ from notifications_utils.pdf import pdf_page_count from notifications_utils.s3 import s3download from notifications_utils.template import Template +from opentelemetry import trace +from opentelemetry.baggage import set_baggage +from opentelemetry.context import attach, detach from pypdf.errors import PdfReadError from requests import RequestException @@ -719,69 +722,80 @@ def abort_for_unauthorised_bilingual_letters_or_invalid_options(language: str | def edit_service_template(service_id, template_id, language=None): template = current_service.get_template_with_user_permission_or_403(template_id, current_user) - if template.template_type not in current_service.available_template_types: - return redirect( - url_for( - ".action_blocked", - service_id=service_id, - notification_type=template.template_type, - return_to="view_template", - template_id=template.id, - ) - ) + ctx = set_baggage("template_id", str(template_id)) + token = attach(ctx) - abort_for_unauthorised_bilingual_letters_or_invalid_options(language, template) + with trace.get_tracer(__name__).start_as_current_span("edit_service_template") as span: + try: + if template.template_type not in current_service.available_template_types: + return redirect( + url_for( + ".action_blocked", + service_id=service_id, + notification_type=template.template_type, + return_to="view_template", + template_id=template.id, + ) + ) - form = get_template_form(template.template_type, language=language)(**template._template) + abort_for_unauthorised_bilingual_letters_or_invalid_options(language, template) - if form.validate_on_submit(): - new_template = get_template( - template._template | form.new_template_data, - current_service, - ) - template_change = template.compare_to(new_template) + form = get_template_form(template.template_type, language=language)(**template._template) - if template_change.placeholders_added and not request.form.get("confirm") and current_service.api_keys: - return render_template( - "views/templates/breaking-change.html", - template_change=template_change, - new_template=new_template, - form=form, - ) - try: - service_api_client.update_service_template( - service_id=service_id, - template_id=template_id, - **form.new_template_data, - ) - except HTTPError as e: - if e.status_code == 400: - if "content" in e.message and any("character count greater than" in x for x in e.message["content"]): - form.template_content.errors.extend(e.message["content"]) - elif "content" in e.message and any(x == QR_CODE_TOO_LONG for x in e.message["content"]): - form.template_content.errors.append( - "Cannot create a usable QR code - the link you entered is too long" + if form.validate_on_submit(): + new_template = get_template( + template._template | form.new_template_data, + current_service, + ) + template_change = template.compare_to(new_template) + + span.add_event("This is an example span event") + + if template_change.placeholders_added and not request.form.get("confirm") and current_service.api_keys: + return render_template( + "views/templates/breaking-change.html", + template_change=template_change, + new_template=new_template, + form=form, ) + try: + service_api_client.update_service_template( + service_id=service_id, + template_id=template_id, + **form.new_template_data, + ) + except HTTPError as e: + if e.status_code == 400: + if "content" in e.message and any( + "character count greater than" in x for x in e.message["content"] + ): + form.template_content.errors.extend(e.message["content"]) + elif "content" in e.message and any(x == QR_CODE_TOO_LONG for x in e.message["content"]): + form.template_content.errors.append( + "Cannot create a usable QR code - the link you entered is too long" + ) + else: + raise e + else: + raise e else: - raise e - else: - raise e - else: - editing_english_content_in_bilingual_letter = ( - template.template_type == "letter" and template.welsh_page_count and language != "welsh" - ) - return redirect( - url_for( - "main.view_template", - service_id=service_id, - template_id=template_id, - **( - {"_anchor": "first-page-of-english-in-bilingual-letter"} - if editing_english_content_in_bilingual_letter - else {} - ), - ) - ) + editing_english_content_in_bilingual_letter = ( + template.template_type == "letter" and template.welsh_page_count and language != "welsh" + ) + return redirect( + url_for( + "main.view_template", + service_id=service_id, + template_id=template_id, + **( + {"_anchor": "first-page-of-english-in-bilingual-letter"} + if editing_english_content_in_bilingual_letter + else {} + ), + ) + ) + finally: + detach(token) return render_template( f"views/edit-{template.template_type}-template.html", diff --git a/app/otel/decorators.py b/app/otel/decorators.py new file mode 100644 index 0000000000..dd10ca09ae --- /dev/null +++ b/app/otel/decorators.py @@ -0,0 +1,70 @@ +import functools +import time + +from app.otel.metrics import otel_metrics + + +def otel(counter_name=None, histogram_name=None, attributes=None): + if attributes is None: + attributes = {} + + def time_function(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + start_time = time.monotonic() + c_name = counter_name or func.__name__ + h_name = histogram_name or f"{func.__name__}_time" + + # Create counter if it doesn't exist + if not hasattr(otel_metrics, c_name): + setattr( + otel_metrics, + c_name, + otel_metrics.meter.create_counter(c_name, description=f"Calls to the {func.__name__} task"), + ) + counter = getattr(otel_metrics, c_name) + + # Create histogram if it doesn't exist + if not hasattr(otel_metrics, h_name): + setattr( + otel_metrics, + h_name, + otel_metrics.meter.create_histogram( + h_name, + description=f"time taken to execute {func.__name__} function", + explicit_bucket_boundaries_advisory=getattr(otel_metrics, "default_histogram_bucket", None), + ), + ) + histogram = getattr(otel_metrics, h_name) + + try: + result = func(*args, **kwargs) + elapsed_time = time.monotonic() - start_time + + counter.add( + amount=1, + attributes={**attributes, "function_name": func.__name__, "status": "success"}, + ) + + histogram.record( + amount=elapsed_time, + attributes={**attributes, "function_name": func.__name__, "status": "success"}, + ) + + except Exception as e: + elapsed_time = time.monotonic() - start_time + counter.add( + amount=1, + attributes={**attributes, "function_name": func.__name__, "status": "error"}, + ) + histogram.record( + amount=elapsed_time, + attributes={**attributes, "function_name": func.__name__, "status": "error"}, + ) + raise e + else: + return result + + return wrapper + + return time_function diff --git a/app/otel/metrics.py b/app/otel/metrics.py new file mode 100644 index 0000000000..5da6eadaea --- /dev/null +++ b/app/otel/metrics.py @@ -0,0 +1,69 @@ +from opentelemetry import metrics +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, + PeriodicExportingMetricReader, +) +from opentelemetry.sdk.resources import Resource + + +class Metrics: + def __init__(self): + self.meter = None + self.default_histogram_bucket = [ + 0.005, + 0.01, + 0.025, + 0.05, + 0.075, + 0.1, + 0.25, + 0.5, + 0.75, + 1.0, + 2.5, + 5.0, + 7.5, + 10.0, + float("inf"), + ] + + def init_app(self, app): + export_mode = app.config.get("OTEL_EXPORT_TYPE", "none").lower() + metric_readers = [] + + if export_mode == "console": + app.logger.info("OpenTelemetry metrics will be exported to console") + metric_readers.append(PeriodicExportingMetricReader(ConsoleMetricExporter())) + elif export_mode == "otlp": + endpoint = app.config.get("OTEL_COLLECTOR_ENDPOINT", "localhost:4317") + app.logger.info("OpenTelemetry metrics will be exported to OTLP collector at %s", endpoint) + otlp_exporter = OTLPMetricExporter(endpoint=endpoint, insecure=True) + # Metrics will be exported every 60 seconds with a 30 seconds timeout by default. + # The following environments variables can be used to change this: + # OTEL_METRIC_EXPORT_INTERVAL + # OTEL_METRIC_EXPORT_TIMEOUT + metric_readers.append(PeriodicExportingMetricReader(otlp_exporter)) + + resource = Resource.create({"service.name": "notifications-api"}) + provider = MeterProvider(metric_readers=metric_readers, resource=resource) + metrics.set_meter_provider(provider) + self.meter = metrics.get_meter(__name__) + + self.create_counters() + self.create_histograms() + self.create_gauges() + + def create_counters(self): + pass + + def create_histograms(self): + pass + + def create_gauges(self): + pass + + +# Initialize the metrics instance singleton +otel_metrics = Metrics() diff --git a/app/otel/traces.py b/app/otel/traces.py new file mode 100644 index 0000000000..c53ac7b9ee --- /dev/null +++ b/app/otel/traces.py @@ -0,0 +1,74 @@ +import os + +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.processor.baggage import ALLOW_ALL_BAGGAGE_KEYS, BaggageSpanProcessor +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter +from opentelemetry.trace import ( + Span, + get_tracer_provider, + set_tracer_provider, +) + + +class Traces: + def __init__(self): + self.tracer = None + + def init_app(self, app): + export_mode = app.config.get("OTEL_EXPORT_TYPE", "none").lower() + resource = Resource.create({"service.name": os.getenv("NOTIFY_APP_NAME", app.config.get("NOTIFY_APP_NAME"))}) + set_tracer_provider(TracerProvider(resource=resource)) + get_tracer_provider().get_tracer(app.config.get("NOTIFY_APP_NAME")) + + span_processor = None + + if export_mode == "console": + span_processor = BatchSpanProcessor(ConsoleSpanExporter()) + elif export_mode == "otlp": + endpoint = app.config.get("OTEL_COLLECTOR_ENDPOINT", "localhost:4317") + span_processor = BatchSpanProcessor( + OTLPSpanExporter( + endpoint=endpoint, + insecure=True, + ) + ) + + if span_processor: + # Instead of adding all baggage to attributes, we could do something like + # regex_predicate = lambda baggage_key: baggage_key.startswith("^key.+") + # tracer_provider.add_span_processor(BaggageSpanProcessor(regex_predicate)) + get_tracer_provider().add_span_processor(BaggageSpanProcessor(ALLOW_ALL_BAGGAGE_KEYS)) + get_tracer_provider().add_span_processor(span_processor) + + # not sure I like the instumentation here as it adds both traces and metrics + + self.instrument_app(app) + + def instrument_app(self, app): + instrumentation = app.config.get("OTEL_INSTRUMENTATIONS", "").lower().split(",") + + if "flask" in instrumentation: + from opentelemetry.instrumentation.flask import FlaskInstrumentor + + FlaskInstrumentor().instrument_app(app) + if "redis" in instrumentation: + from opentelemetry.instrumentation.redis import RedisInstrumentor + + def redis_response_hook(span, *args, **kwargs): + if span: + span.update_name(f"redis/{span.name}") + + RedisInstrumentor().instrument(response_hook=redis_response_hook) + if "requests" in instrumentation: + from opentelemetry.instrumentation.requests import RequestsInstrumentor + + def requests_response_hook(span, *args, **kwargs): + if span: + span.update_name(f"requests/{span.name}") + + RequestsInstrumentor().instrument(response_hook=requests_response_hook) + + +otel_traces = Traces() diff --git a/requirements.in b/requirements.in index 2c423ef003..9fd37e95af 100644 --- a/requirements.in +++ b/requirements.in @@ -26,3 +26,13 @@ prometheus-client==0.15.0 git+https://github.com/alphagov/gds_metrics_python.git@6f1840a57b6fb1ee40b7e84f2f18ec229de8aa72 sentry-sdk[flask]==1.45.1 + +opentelemetry-distro==0.54b1 +opentelemetry-exporter-otlp==1.33.1 +opentelemetry-instrumentation-celery==0.54b1 +opentelemetry-instrumentation-flask==0.54b1 +opentelemetry-instrumentation-requests==0.54b1 +opentelemetry-instrumentation-redis==0.54b1 +opentelemetry-instrumentation-sqlalchemy==0.54b1 +opentelemetry-instrumentation-wsgi==0.54b1 +opentelemetry-processor-baggage==0.54b1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8d07d5e5fa..791f73aa14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,12 @@ click==8.1.3 # via flask cryptography==44.0.1 # via fido2 +deprecated==1.2.18 + # via + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-semantic-conventions dnspython==2.6.1 # via eventlet docopt==0.6.2 @@ -57,18 +63,26 @@ flask-wtf==1.2.1 # via -r requirements.in gds-metrics @ git+https://github.com/alphagov/gds_metrics_python.git@6f1840a57b6fb1ee40b7e84f2f18ec229de8aa72 # via -r requirements.in +googleapis-common-protos==1.70.0 + # via + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http govuk-bank-holidays==0.15 # via notifications-utils govuk-frontend-jinja==3.6.0 # via -r requirements.in greenlet==3.2.2 # via eventlet +grpcio==1.73.0 + # via opentelemetry-exporter-otlp-proto-grpc gunicorn==23.0.0 # via notifications-utils humanize==4.4.0 # via -r requirements.in idna==3.7 # via requests +importlib-metadata==8.6.1 + # via opentelemetry-api itsdangerous==2.2.0 # via # flask @@ -106,10 +120,92 @@ notifications-utils @ git+https://github.com/alphagov/notifications-utils.git@a9 # via -r requirements.in openpyxl==3.1.5 # via pyexcel-xlsx +opentelemetry-api==1.33.1 + # via + # opentelemetry-distro + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-instrumentation + # opentelemetry-instrumentation-celery + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-redis + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-sqlalchemy + # opentelemetry-instrumentation-wsgi + # opentelemetry-processor-baggage + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-distro==0.54b1 + # via -r requirements.in +opentelemetry-exporter-otlp==1.33.1 + # via -r requirements.in +opentelemetry-exporter-otlp-proto-common==1.33.1 + # via + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-grpc==1.33.1 + # via opentelemetry-exporter-otlp +opentelemetry-exporter-otlp-proto-http==1.33.1 + # via opentelemetry-exporter-otlp +opentelemetry-instrumentation==0.54b1 + # via + # opentelemetry-distro + # opentelemetry-instrumentation-celery + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-redis + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-sqlalchemy + # opentelemetry-instrumentation-wsgi +opentelemetry-instrumentation-celery==0.54b1 + # via -r requirements.in +opentelemetry-instrumentation-flask==0.54b1 + # via -r requirements.in +opentelemetry-instrumentation-redis==0.54b1 + # via -r requirements.in +opentelemetry-instrumentation-requests==0.54b1 + # via -r requirements.in +opentelemetry-instrumentation-sqlalchemy==0.54b1 + # via -r requirements.in +opentelemetry-instrumentation-wsgi==0.54b1 + # via + # -r requirements.in + # opentelemetry-instrumentation-flask +opentelemetry-processor-baggage==0.54b1 + # via -r requirements.in +opentelemetry-proto==1.33.1 + # via + # opentelemetry-exporter-otlp-proto-common + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-sdk==1.33.1 + # via + # opentelemetry-distro + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-processor-baggage +opentelemetry-semantic-conventions==0.54b1 + # via + # opentelemetry-instrumentation + # opentelemetry-instrumentation-celery + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-redis + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-sqlalchemy + # opentelemetry-instrumentation-wsgi + # opentelemetry-sdk +opentelemetry-util-http==0.54b1 + # via + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-wsgi ordered-set==4.1.0 # via notifications-utils packaging==23.1 - # via gunicorn + # via + # gunicorn + # opentelemetry-instrumentation + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-sqlalchemy phonenumbers==8.13.50 # via notifications-utils pillow==11.2.1 @@ -118,6 +214,10 @@ prometheus-client==0.15.0 # via # -r requirements.in # gds-metrics +protobuf==5.29.5 + # via + # googleapis-common-protos + # opentelemetry-proto pycparser==2.21 # via cffi pyexcel==0.7.1 @@ -156,6 +256,7 @@ requests==2.32.3 # govuk-bank-holidays # notifications-python-client # notifications-utils + # opentelemetry-exporter-otlp-proto-http s3transfer==0.10.1 # via boto3 segno==1.6.1 @@ -170,6 +271,8 @@ statsd==4.0.1 # via notifications-utils texttable==1.6.4 # via pyexcel +typing-extensions==4.14.0 + # via opentelemetry-sdk urllib3==1.26.19 # via # botocore @@ -179,9 +282,18 @@ werkzeug==3.1.3 # via # flask # flask-login +wrapt==1.17.2 + # via + # deprecated + # opentelemetry-instrumentation + # opentelemetry-instrumentation-redis + # opentelemetry-instrumentation-sqlalchemy + # opentelemetry-processor-baggage wtforms==3.1.0 # via flask-wtf xlrd==2.0.1 # via pyexcel-xls xlwt==1.3.0 # via pyexcel-xls +zipp==3.23.0 + # via importlib-metadata diff --git a/requirements_for_test.txt b/requirements_for_test.txt index 1447e42616..43a53e5e2d 100644 --- a/requirements_for_test.txt +++ b/requirements_for_test.txt @@ -56,6 +56,13 @@ cryptography==44.0.1 # -r requirements.txt # fido2 # moto +deprecated==1.2.18 + # via + # -r requirements.txt + # opentelemetry-api + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-semantic-conventions dnspython==2.6.1 # via # -r requirements.txt @@ -96,6 +103,11 @@ freezegun==1.5.1 # via -r requirements_for_test_common.in gds-metrics @ git+https://github.com/alphagov/gds_metrics_python.git@6f1840a57b6fb1ee40b7e84f2f18ec229de8aa72 # via -r requirements.txt +googleapis-common-protos==1.70.0 + # via + # -r requirements.txt + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http govuk-bank-holidays==0.15 # via # -r requirements.txt @@ -106,6 +118,10 @@ greenlet==3.2.2 # via # -r requirements.txt # eventlet +grpcio==1.73.0 + # via + # -r requirements.txt + # opentelemetry-exporter-otlp-proto-grpc gunicorn==23.0.0 # via # -r requirements.txt @@ -118,6 +134,10 @@ idna==3.7 # via # -r requirements.txt # requests +importlib-metadata==8.6.1 + # via + # -r requirements.txt + # opentelemetry-api iniconfig==2.0.0 # via pytest itsdangerous==2.2.0 @@ -169,6 +189,95 @@ openpyxl==3.1.5 # via # -r requirements.txt # pyexcel-xlsx +opentelemetry-api==1.33.1 + # via + # -r requirements.txt + # opentelemetry-distro + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-instrumentation + # opentelemetry-instrumentation-celery + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-redis + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-sqlalchemy + # opentelemetry-instrumentation-wsgi + # opentelemetry-processor-baggage + # opentelemetry-sdk + # opentelemetry-semantic-conventions +opentelemetry-distro==0.54b1 + # via -r requirements.txt +opentelemetry-exporter-otlp==1.33.1 + # via -r requirements.txt +opentelemetry-exporter-otlp-proto-common==1.33.1 + # via + # -r requirements.txt + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-grpc==1.33.1 + # via + # -r requirements.txt + # opentelemetry-exporter-otlp +opentelemetry-exporter-otlp-proto-http==1.33.1 + # via + # -r requirements.txt + # opentelemetry-exporter-otlp +opentelemetry-instrumentation==0.54b1 + # via + # -r requirements.txt + # opentelemetry-distro + # opentelemetry-instrumentation-celery + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-redis + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-sqlalchemy + # opentelemetry-instrumentation-wsgi +opentelemetry-instrumentation-celery==0.54b1 + # via -r requirements.txt +opentelemetry-instrumentation-flask==0.54b1 + # via -r requirements.txt +opentelemetry-instrumentation-redis==0.54b1 + # via -r requirements.txt +opentelemetry-instrumentation-requests==0.54b1 + # via -r requirements.txt +opentelemetry-instrumentation-sqlalchemy==0.54b1 + # via -r requirements.txt +opentelemetry-instrumentation-wsgi==0.54b1 + # via + # -r requirements.txt + # opentelemetry-instrumentation-flask +opentelemetry-processor-baggage==0.54b1 + # via -r requirements.txt +opentelemetry-proto==1.33.1 + # via + # -r requirements.txt + # opentelemetry-exporter-otlp-proto-common + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http +opentelemetry-sdk==1.33.1 + # via + # -r requirements.txt + # opentelemetry-distro + # opentelemetry-exporter-otlp-proto-grpc + # opentelemetry-exporter-otlp-proto-http + # opentelemetry-processor-baggage +opentelemetry-semantic-conventions==0.54b1 + # via + # -r requirements.txt + # opentelemetry-instrumentation + # opentelemetry-instrumentation-celery + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-redis + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-sqlalchemy + # opentelemetry-instrumentation-wsgi + # opentelemetry-sdk +opentelemetry-util-http==0.54b1 + # via + # -r requirements.txt + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-requests + # opentelemetry-instrumentation-wsgi ordered-set==4.1.0 # via # -r requirements.txt @@ -177,6 +286,9 @@ packaging==23.1 # via # -r requirements.txt # gunicorn + # opentelemetry-instrumentation + # opentelemetry-instrumentation-flask + # opentelemetry-instrumentation-sqlalchemy # pytest phonenumbers==8.13.50 # via @@ -190,6 +302,11 @@ prometheus-client==0.15.0 # via # -r requirements.txt # gds-metrics +protobuf==5.29.5 + # via + # -r requirements.txt + # googleapis-common-protos + # opentelemetry-proto pycparser==2.21 # via # -r requirements.txt @@ -266,6 +383,7 @@ requests==2.32.3 # moto # notifications-python-client # notifications-utils + # opentelemetry-exporter-otlp-proto-http # requests-mock # responses requests-mock==1.12.1 @@ -303,6 +421,10 @@ texttable==1.6.4 # via # -r requirements.txt # pyexcel +typing-extensions==4.14.0 + # via + # -r requirements.txt + # opentelemetry-sdk urllib3==1.26.19 # via # -r requirements.txt @@ -318,6 +440,14 @@ werkzeug==3.1.3 # flask # flask-login # moto +wrapt==1.17.2 + # via + # -r requirements.txt + # deprecated + # opentelemetry-instrumentation + # opentelemetry-instrumentation-redis + # opentelemetry-instrumentation-sqlalchemy + # opentelemetry-processor-baggage wtforms==3.1.0 # via # -r requirements.txt @@ -332,3 +462,7 @@ xlwt==1.3.0 # pyexcel-xls xmltodict==0.14.2 # via moto +zipp==3.23.0 + # via + # -r requirements.txt + # importlib-metadata