From 12cb895d238197ebd9cf90ce13b34753c425689c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B1=9F=E6=B3=A2?= Date: Thu, 12 Dec 2024 22:13:40 +0800 Subject: [PATCH] feat: add prometheus metrics --- api/.env.example | 1 + api/configs/feature/__init__.py | 28 +++-- .../advanced_chat/generate_task_pipeline.py | 11 +- .../apps/workflow/generate_task_pipeline.py | 6 + api/core/app/task_pipeline/__init__.py | 36 ++++++ .../easy_ui_based_generate_task_pipeline.py | 53 +++++++- .../task_pipeline/workflow_cycle_manage.py | 36 ++++++ api/core/model_manager.py | 49 +++++++- .../callbacks/metrics_callback.py | 117 ++++++++++++++++++ .../model_providers/__base/ai_model.py | 1 + .../__base/large_language_model.py | 13 +- api/core/tools/tool/tool.py | 34 ++++- api/docker/entrypoint.sh | 1 + api/extensions/ext_app_metrics.py | 91 +++++++++++++- api/extensions/ext_storage.py | 35 ++++++ api/poetry.lock | 60 ++++++++- api/pyproject.toml | 1 + 17 files changed, 546 insertions(+), 27 deletions(-) create mode 100644 api/core/model_runtime/callbacks/metrics_callback.py diff --git a/api/.env.example b/api/.env.example index c2e3f33fc40be0..1e276e97b36fd3 100644 --- a/api/.env.example +++ b/api/.env.example @@ -431,3 +431,4 @@ RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5 CREATE_TIDB_SERVICE_JOB_ENABLED=false +PROMETHEUS_MULTIPROC_DIR=/tmp/prometheus_multiproc_dir diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index f1cb3efda7b3e3..6a2b652b7488e4 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -22,8 +22,9 @@ class SecurityConfig(BaseSettings): SECRET_KEY: str = Field( description="Secret key for secure session cookie signing." - "Make sure you are changing this key for your deployment with a strong key." - "Generate a strong key using `openssl rand -base64 42` or set via the `SECRET_KEY` environment variable.", + "Make sure you are changing this key for your deployment with a strong key." + "Generate a strong key using `openssl rand -base64 42` " + "or set via the `SECRET_KEY` environment variable.", default="", ) @@ -141,7 +142,7 @@ class EndpointConfig(BaseSettings): CONSOLE_API_URL: str = Field( description="Base URL for the console API," - "used for login authentication callback or notion integration callbacks", + "used for login authentication callback or notion integration callbacks", default="", ) @@ -168,8 +169,8 @@ class FileAccessConfig(BaseSettings): FILES_URL: str = Field( description="Base URL for file preview or download," - " used for frontend display and multi-model inputs" - "Url is signed and has expiration time.", + " used for frontend display and multi-model inputs" + "Url is signed and has expiration time.", validation_alias=AliasChoices("FILES_URL", "CONSOLE_API_URL"), alias_priority=1, default="", @@ -318,7 +319,7 @@ def WEB_API_CORS_ALLOW_ORIGINS(self) -> list[str]: RESPECT_XFORWARD_HEADERS_ENABLED: bool = Field( description="Enable or disable the X-Forwarded-For Proxy Fix middleware from Werkzeug" - " to respect X-* headers to redirect clients", + " to respect X-* headers to redirect clients", default=False, ) @@ -571,7 +572,7 @@ class RagEtlConfig(BaseSettings): KEYWORD_DATA_SOURCE_TYPE: str = Field( description="Data source type for keyword extraction" - " ('database' or other supported types), default to 'database'", + " ('database' or other supported types), default to 'database'", default="database", ) @@ -751,6 +752,18 @@ class LoginConfig(BaseSettings): ) +class PrometheusConfig(BaseSettings): + HISTOGRAM_BUCKETS_1MIN: list[float] = Field( + description="The buckets of Prometheus histogram under 1 minute", + default=[0.1, 0.2, 0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20, 25, 30, 40, 50, 60], + ) + + HISTOGRAM_BUCKETS_5MIN: list[float] = Field( + description="The buckets of Prometheus histogram under 5 minute", + default=[0.1, 0.2, 0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20, 25, 30, 40, 50, 60, 120, 180, 300], + ) + + class FeatureConfig( # place the configs in alphabet order AppExecutionConfig, @@ -777,6 +790,7 @@ class FeatureConfig( WorkflowConfig, WorkspaceConfig, LoginConfig, + PrometheusConfig, # hosted services config HostedServiceConfig, CeleryBeatConfig, diff --git a/api/core/app/apps/advanced_chat/generate_task_pipeline.py b/api/core/app/apps/advanced_chat/generate_task_pipeline.py index 32a23a7fdb8690..2930b3d985670a 100644 --- a/api/core/app/apps/advanced_chat/generate_task_pipeline.py +++ b/api/core/app/apps/advanced_chat/generate_task_pipeline.py @@ -381,7 +381,9 @@ def _process_stream_response( conversation_id=self._conversation.id, trace_manager=trace_manager, ) - + self._workflow_time_it( + is_success=True, graph_runtime_state=graph_runtime_state, workflow_run=workflow_run + ) yield self._workflow_finish_to_stream_response( task_id=self._application_generate_entity.task_id, workflow_run=workflow_run ) @@ -404,6 +406,9 @@ def _process_stream_response( conversation_id=None, trace_manager=trace_manager, ) + self._workflow_time_it( + is_success=False, graph_runtime_state=graph_runtime_state, workflow_run=workflow_run + ) yield self._workflow_finish_to_stream_response( task_id=self._application_generate_entity.task_id, workflow_run=workflow_run @@ -428,7 +433,9 @@ def _process_stream_response( trace_manager=trace_manager, exceptions_count=event.exceptions_count, ) - + self._workflow_time_it( + is_success=False, graph_runtime_state=graph_runtime_state, workflow_run=workflow_run + ) yield self._workflow_finish_to_stream_response( task_id=self._application_generate_entity.task_id, workflow_run=workflow_run ) diff --git a/api/core/app/apps/workflow/generate_task_pipeline.py b/api/core/app/apps/workflow/generate_task_pipeline.py index 8483fa91f80a02..4ec1b663871aec 100644 --- a/api/core/app/apps/workflow/generate_task_pipeline.py +++ b/api/core/app/apps/workflow/generate_task_pipeline.py @@ -340,6 +340,9 @@ def _process_stream_response( conversation_id=None, trace_manager=trace_manager, ) + self._workflow_time_it( + is_success=True, graph_runtime_state=graph_runtime_state, workflow_run=workflow_run + ) # save workflow app log self._save_workflow_app_log(workflow_run) @@ -364,6 +367,9 @@ def _process_stream_response( conversation_id=None, trace_manager=trace_manager, ) + self._workflow_time_it( + is_success=False, graph_runtime_state=graph_runtime_state, workflow_run=workflow_run + ) # save workflow app log self._save_workflow_app_log(workflow_run) diff --git a/api/core/app/task_pipeline/__init__.py b/api/core/app/task_pipeline/__init__.py index e69de29bb2d1d6..e4307ec31133d1 100644 --- a/api/core/app/task_pipeline/__init__.py +++ b/api/core/app/task_pipeline/__init__.py @@ -0,0 +1,36 @@ +from prometheus_client import Counter, Histogram + +from configs import dify_config + +app_request = Counter( + name="app_request", + documentation="The total count of APP requests", + labelnames=["app_id", "tenant_id", "username"], +) +app_request_failed = Counter( + name="app_request_failed", + documentation="The failed count of APP requests", + labelnames=["app_id", "tenant_id", "username"], +) +app_request_latency = Histogram( + name="app_request_latency", + documentation="The latency of APP requests", + unit="seconds", + labelnames=["app_id", "tenant_id", "username"], + buckets=dify_config.HISTOGRAM_BUCKETS_5MIN, +) +app_input_tokens = Counter( + name="app_input_tokens", + documentation="The input tokens cost by APP requests", + labelnames=["app_id", "tenant_id", "username"], +) +app_output_tokens = Counter( + name="app_output_tokens", + documentation="The output tokens cost by APP requests", + labelnames=["app_id", "tenant_id", "username"], +) +app_total_tokens = Counter( + name="app_total_tokens", + documentation="The total tokens cost by APP requests", + labelnames=["app_id", "tenant_id", "username"], +) diff --git a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py index 917649f34e769c..2c6e74ee507f40 100644 --- a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py @@ -39,6 +39,14 @@ MessageEndStreamResponse, StreamResponse, ) +from core.app.task_pipeline import ( + app_input_tokens, + app_output_tokens, + app_request, + app_request_failed, + app_request_latency, + app_total_tokens, +) from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline from core.app.task_pipeline.message_cycle_manage import MessageCycleManage from core.model_manager import ModelInstance @@ -251,6 +259,47 @@ def _wrapper_process_stream_response( if publisher: yield MessageAudioEndStreamResponse(audio="", task_id=task_id) + def _chat_time_it(self, is_success: bool) -> None: + """ + Record chat / completion / agent run metrics. + """ + app_id = self._app_config.app_id + tenant_id = self._app_config.tenant_id + username = self._conversation.from_account_name + app_request.labels( + app_id=app_id, + tenant_id=tenant_id, + username=username, + ).inc() + + if not is_success: + app_request_failed.labels( + app_id=app_id, + tenant_id=tenant_id, + username=username, + ).inc() + return + app_request_latency.labels( + app_id=app_id, + tenant_id=tenant_id, + username=username, + ).observe(self._message.provider_response_latency) + app_input_tokens.labels( + app_id=app_id, + tenant_id=tenant_id, + username=username, + ).inc(self._message.message_tokens) + app_output_tokens.labels( + app_id=app_id, + tenant_id=tenant_id, + username=username, + ).inc(self._message.answer_tokens) + app_total_tokens.labels( + app_id=app_id, + tenant_id=tenant_id, + username=username, + ).inc(self._message.message_tokens + self._message.answer_tokens) + def _process_stream_response( self, publisher: AppGeneratorTTSPublisher, trace_manager: Optional[TraceQueueManager] = None ) -> Generator[StreamResponse, None, None]: @@ -265,6 +314,7 @@ def _process_stream_response( if isinstance(event, QueueErrorEvent): err = self._handle_error(event, self._message) + self._chat_time_it(is_success=False) yield self._error_to_stream_response(err) break elif isinstance(event, QueueStopEvent | QueueMessageEndEvent): @@ -283,6 +333,7 @@ def _process_stream_response( # Save message self._save_message(trace_manager) + self._chat_time_it(is_success=True) yield self._message_end_to_stream_response() elif isinstance(event, QueueRetrieverResourcesEvent): @@ -374,7 +425,7 @@ def _save_message(self, trace_manager: Optional[TraceQueueManager] = None) -> No application_generate_entity=self._application_generate_entity, conversation=self._conversation, is_first_message=self._application_generate_entity.app_config.app_mode in {AppMode.AGENT_CHAT, AppMode.CHAT} - and self._application_generate_entity.conversation_id is None, + and self._application_generate_entity.conversation_id is None, extras=self._application_generate_entity.extras, ) diff --git a/api/core/app/task_pipeline/workflow_cycle_manage.py b/api/core/app/task_pipeline/workflow_cycle_manage.py index d78f124e3a2690..ab878e98176108 100644 --- a/api/core/app/task_pipeline/workflow_cycle_manage.py +++ b/api/core/app/task_pipeline/workflow_cycle_manage.py @@ -33,6 +33,14 @@ WorkflowStartStreamResponse, WorkflowTaskState, ) +from core.app.task_pipeline import ( + app_input_tokens, + app_output_tokens, + app_request, + app_request_failed, + app_request_latency, + app_total_tokens, +) from core.file import FILE_MODEL_IDENTITY, File from core.model_runtime.utils.encoders import jsonable_encoder from core.ops.entities.trace_entity import TraceTaskName @@ -40,6 +48,7 @@ from core.tools.tool_manager import ToolManager from core.workflow.entities.node_entities import NodeRunMetadataKey from core.workflow.enums import SystemVariableKey +from core.workflow.graph_engine import GraphRuntimeState from core.workflow.nodes import NodeType from core.workflow.nodes.tool.entities import ToolNodeData from core.workflow.workflow_entry import WorkflowEntry @@ -117,6 +126,33 @@ def _handle_workflow_run_start(self) -> WorkflowRun: return workflow_run + def _workflow_time_it( + self, is_success: bool, graph_runtime_state: GraphRuntimeState, workflow_run: WorkflowRun + ) -> None: + """ + Record advanced-chat / workflow run metrics. + """ + app_id = workflow_run.app_id + tenant_id = workflow_run.tenant_id + username = self._user.name + app_request.labels(app_id=app_id, tenant_id=tenant_id, username=username).inc() + + if not is_success: + app_request_failed.labels(app_id=app_id, tenant_id=tenant_id, username=username).inc() + return + app_request_latency.labels(app_id=app_id, tenant_id=tenant_id, username=username).observe( + workflow_run.elapsed_time + ) + app_input_tokens.labels(app_id=app_id, tenant_id=tenant_id, username=username).inc( + graph_runtime_state.llm_usage.prompt_tokens + ) + app_output_tokens.labels(app_id=app_id, tenant_id=tenant_id, username=username).inc( + graph_runtime_state.llm_usage.completion_tokens + ) + app_total_tokens.labels(app_id=app_id, tenant_id=tenant_id, username=username).inc( + graph_runtime_state.llm_usage.total_tokens + ) + def _handle_workflow_run_success( self, workflow_run: WorkflowRun, diff --git a/api/core/model_manager.py b/api/core/model_manager.py index 1986688551b601..1af82b1db0793d 100644 --- a/api/core/model_manager.py +++ b/api/core/model_manager.py @@ -2,6 +2,8 @@ from collections.abc import Callable, Generator, Iterable, Sequence from typing import IO, Any, Optional, Union, cast +from prometheus_client import Counter, Histogram + from configs import dify_config from core.entities.embedding_type import EmbeddingInputType from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle @@ -26,6 +28,25 @@ logger = logging.getLogger(__name__) +model_request_total_counter = Counter( + name="model_request_total_counter", + documentation="The total count of model requests", + labelnames=["model_type", "provider", "model", "method"], +) +model_request_failed_counter = Counter( + name="model_request_failed_counter", + documentation="The failed count of model requests", + labelnames=["model_type", "provider", "model", "method"], +) +model_request_latency = Histogram( + name="model_request_latency", + documentation="The latency of model requests. For the LLM model, it just indicate " + "the TTFT (a.k.a. Time To First Token).", + unit="seconds", + labelnames=["model_type", "provider", "model", "method"], + buckets=dify_config.HISTOGRAM_BUCKETS_1MIN, +) + class ModelInstance: """ @@ -298,6 +319,30 @@ def invoke_tts(self, content_text: str, tenant_id: str, voice: str, user: Option voice=voice, ) + def _invoke_with_timeit(self, function: Callable[..., Any], *args, **kwargs): + with model_request_latency.labels( + model_type=self.model_type_instance.model_type.value, + provider=self.provider, + model=self.model, + method=function.__name__ if hasattr(function, "__name__") else "unknown", + ).time(): + model_request_total_counter.labels( + model_type=self.model_type_instance.model_type.value, + provider=self.provider, + model=self.model, + method=function.__name__ if hasattr(function, "__name__") else "unknown", + ).inc() + try: + return function(*args, **kwargs) + except Exception as e: + model_request_failed_counter.labels( + model_type=self.model_type_instance.model_type.value, + provider=self.provider, + model=self.model, + method=function.__name__ if hasattr(function, "__name__") else "unknown", + ).inc() + raise e + def _round_robin_invoke(self, function: Callable[..., Any], *args, **kwargs): """ Round-robin invoke @@ -307,7 +352,7 @@ def _round_robin_invoke(self, function: Callable[..., Any], *args, **kwargs): :return: """ if not self.load_balancing_manager: - return function(*args, **kwargs) + return self._invoke_with_timeit(function, *args, **kwargs) last_exception = None while True: @@ -321,7 +366,7 @@ def _round_robin_invoke(self, function: Callable[..., Any], *args, **kwargs): try: if "credentials" in kwargs: del kwargs["credentials"] - return function(*args, **kwargs, credentials=lb_config.credentials) + return self._invoke_with_timeit(function, *args, **kwargs, credentials=lb_config.credentials) except InvokeRateLimitError as e: # expire in 60 seconds self.load_balancing_manager.cooldown(lb_config, expire=60) diff --git a/api/core/model_runtime/callbacks/metrics_callback.py b/api/core/model_runtime/callbacks/metrics_callback.py new file mode 100644 index 00000000000000..db71fe4002cb01 --- /dev/null +++ b/api/core/model_runtime/callbacks/metrics_callback.py @@ -0,0 +1,117 @@ +from typing import Optional + +from prometheus_client import Counter, Histogram + +from configs import dify_config +from core.model_runtime.callbacks.base_callback import Callback +from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk +from core.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool +from core.model_runtime.model_providers.__base.ai_model import AIModel + +llm_model_request_total_counter = Counter( + name="llm_model_request_total_counter", + documentation="The total count of LLM model requests", + labelnames=["model_type", "model"], +) +llm_model_request_failed_counter = Counter( + name="llm_model_request_failed_counter", + documentation="The failed count of LLM model requests", + labelnames=["model_type", "model"], +) +llm_model_request_first_chunk_latency = Histogram( + name="llm_model_request_first_chunk_latency", + documentation="The first chunk latency of LLM model requests", + unit="seconds", + labelnames=["model_type", "model"], + buckets=dify_config.HISTOGRAM_BUCKETS_1MIN, +) +llm_model_request_following_chunk_latency = Histogram( + name="llm_model_request_following_chunk_latency", + documentation="The following chunk latency of LLM model requests", + unit="seconds", + labelnames=["model_type", "model"], + buckets=Histogram.DEFAULT_BUCKETS, +) +llm_model_request_entire_latency = Histogram( + name="llm_model_request_entire_latency", + documentation="The entire latency of LLM model requests", + unit="seconds", + labelnames=["model_type", "model"], + buckets=dify_config.HISTOGRAM_BUCKETS_5MIN, +) + + +class MetricsCallback(Callback): + first_chunk: bool = True + + def on_before_invoke( + self, + llm_instance: AIModel, + model: str, + credentials: dict, + prompt_messages: list[PromptMessage], + model_parameters: dict, + tools: Optional[list[PromptMessageTool]] = None, + stop: Optional[list[str]] = None, + stream: bool = True, + user: Optional[str] = None, + ) -> None: + llm_model_request_total_counter.labels(model_type=llm_instance.model_type.value, + model=model).inc() + + def on_new_chunk( + self, + llm_instance: AIModel, + chunk: LLMResultChunk, + model: str, + credentials: dict, + prompt_messages: list[PromptMessage], + model_parameters: dict, + tools: Optional[list[PromptMessageTool]] = None, + stop: Optional[list[str]] = None, + stream: bool = True, + user: Optional[str] = None, + ): + # Skip the last one. The last one indicate the entire usage. + if chunk.delta.finish_reason is not None: + return + + if self.first_chunk: + llm_model_request_first_chunk_latency.labels(model_type=llm_instance.model_type.value, + model=model).observe(chunk.delta.usage.latency) + self.first_chunk = False + else: + llm_model_request_following_chunk_latency.labels(model_type=llm_instance.model_type.value, + model=model).observe(chunk.delta.usage.latency) + + def on_after_invoke( + self, + llm_instance: AIModel, + result: LLMResult, + model: str, + credentials: dict, + prompt_messages: list[PromptMessage], + model_parameters: dict, + tools: Optional[list[PromptMessageTool]] = None, + stop: Optional[list[str]] = None, + stream: bool = True, + user: Optional[str] = None, + ) -> None: + llm_model_request_entire_latency.labels(model_type=llm_instance.model_type.value, + model=model).observe(result.usage.latency) + + def on_invoke_error( + self, + llm_instance: AIModel, + ex: Exception, + model: str, + credentials: dict, + prompt_messages: list[PromptMessage], + model_parameters: dict, + tools: Optional[list[PromptMessageTool]] = None, + stop: Optional[list[str]] = None, + stream: bool = True, + user: Optional[str] = None, + ) -> None: + llm_model_request_failed_counter.labels(model_type=llm_instance.model_type.value, + model=model).inc() diff --git a/api/core/model_runtime/model_providers/__base/ai_model.py b/api/core/model_runtime/model_providers/__base/ai_model.py index 79a1d28ebe637e..78f01be668dbc8 100644 --- a/api/core/model_runtime/model_providers/__base/ai_model.py +++ b/api/core/model_runtime/model_providers/__base/ai_model.py @@ -31,6 +31,7 @@ class AIModel(ABC): model_type: ModelType model_schemas: Optional[list[AIModelEntity]] = None started_at: float = 0 + last_chunked_at: float = 0 # pydantic configs model_config = ConfigDict(protected_namespaces=()) diff --git a/api/core/model_runtime/model_providers/__base/large_language_model.py b/api/core/model_runtime/model_providers/__base/large_language_model.py index 8faeffa872b40f..e30ffa1d89344c 100644 --- a/api/core/model_runtime/model_providers/__base/large_language_model.py +++ b/api/core/model_runtime/model_providers/__base/large_language_model.py @@ -10,6 +10,7 @@ from configs import dify_config from core.model_runtime.callbacks.base_callback import Callback from core.model_runtime.callbacks.logging_callback import LoggingCallback +from core.model_runtime.callbacks.metrics_callback import MetricsCallback from core.model_runtime.entities.llm_entities import LLMMode, LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage from core.model_runtime.entities.message_entities import ( AssistantPromptMessage, @@ -74,8 +75,10 @@ def invoke( model_parameters = self._validate_and_filter_model_parameters(model, model_parameters, credentials) self.started_at = time.perf_counter() + self.last_chunked_at = self.started_at callbacks = callbacks or [] + callbacks.append(MetricsCallback()) if dify_config.DEBUG: callbacks.append(LoggingCallback()) @@ -429,6 +432,14 @@ def _invoke_result_generator( for chunk in result: yield chunk + if chunk.delta.usage: + usage = chunk.delta.usage + else: + chunk.delta.usage = LLMUsage.empty_usage() + now = time.perf_counter() + chunk.delta.usage.latency = now - self.last_chunked_at + self.last_chunked_at = now + self._trigger_new_chunk_callbacks( chunk=chunk, model=model, @@ -444,8 +455,6 @@ def _invoke_result_generator( prompt_message.content += chunk.delta.message.content real_model = chunk.model - if chunk.delta.usage: - usage = chunk.delta.usage if chunk.system_fingerprint: system_fingerprint = chunk.system_fingerprint diff --git a/api/core/tools/tool/tool.py b/api/core/tools/tool/tool.py index 8d4045038171a6..3ce58c34497dd2 100644 --- a/api/core/tools/tool/tool.py +++ b/api/core/tools/tool/tool.py @@ -4,9 +4,11 @@ from enum import Enum, StrEnum from typing import TYPE_CHECKING, Any, Optional, Union +from prometheus_client import Counter, Histogram from pydantic import BaseModel, ConfigDict, field_validator from pydantic_core.core_schema import ValidationInfo +from configs import dify_config from core.app.entities.app_invoke_entities import InvokeFrom from core.tools.entities.tool_entities import ( ToolDescription, @@ -21,9 +23,26 @@ ) from core.tools.tool_file_manager import ToolFileManager + if TYPE_CHECKING: from core.file.models import File +tool_request_total_counter = Counter( + name="tool_request_total_counter", documentation="The total count of tool requests", labelnames=["provider", "tool"] +) +tool_request_failed_counter = Counter( + name="tool_request_failed_counter", + documentation="The failed count of tool requests", + labelnames=["provider", "tool"], +) +tool_request_latency = Histogram( + name="tool_request_latency", + documentation="The latency of tool requests", + unit="seconds", + labelnames=["provider", "tool"], + buckets=dify_config.HISTOGRAM_BUCKETS_5MIN, +) + class Tool(BaseModel, ABC): identity: Optional[ToolIdentity] = None @@ -206,10 +225,17 @@ def invoke(self, user_id: str, tool_parameters: Mapping[str, Any]) -> list[ToolI # try parse tool parameters into the correct type tool_parameters = self._transform_tool_parameters_type(tool_parameters) - result = self._invoke( - user_id=user_id, - tool_parameters=tool_parameters, - ) + result = [] + with tool_request_latency.labels(provider=self.identity.provider, tool=self.identity.name).time(): + tool_request_total_counter.labels(provider=self.identity.provider, tool=self.identity.name).inc() + try: + result = self._invoke( + user_id=user_id, + tool_parameters=tool_parameters, + ) + except Exception as e: + tool_request_failed_counter.labels(provider=self.identity.provider, tool=self.identity.name).inc() + raise e if not isinstance(result, list): result = [result] diff --git a/api/docker/entrypoint.sh b/api/docker/entrypoint.sh index 881263171fa145..07f96a952e051e 100755 --- a/api/docker/entrypoint.sh +++ b/api/docker/entrypoint.sh @@ -26,6 +26,7 @@ if [[ "${MODE}" == "worker" ]]; then elif [[ "${MODE}" == "beat" ]]; then exec celery -A app.celery beat --loglevel ${LOG_LEVEL} else + mkdir "${PROMETHEUS_MULTIPROC_DIR}" if [[ "${DEBUG}" == "true" ]]; then exec flask run --host=${DIFY_BIND_ADDRESS:-0.0.0.0} --port=${DIFY_PORT:-5001} --debug else diff --git a/api/extensions/ext_app_metrics.py b/api/extensions/ext_app_metrics.py index de1cdfeb984e86..616b1caaf3b088 100644 --- a/api/extensions/ext_app_metrics.py +++ b/api/extensions/ext_app_metrics.py @@ -3,9 +3,61 @@ import threading from flask import Response +from prometheus_client import ( + CollectorRegistry, + make_wsgi_app, + multiprocess, +) +from prometheus_client import ( + Counter, + Gauge, +) +from sqlalchemy import text +from werkzeug.middleware.dispatcher import DispatcherMiddleware from configs import dify_config from dify_app import DifyApp +from extensions.ext_database import db +from extensions.ext_redis import redis_client +from extensions.ext_storage import storage + + +health_check_total_counter = Counter(name="health_check_total_counter", documentation="The count of health check") +redis_checked_counter = Counter( + name="redis_checked_counter", documentation="The count of Redis has been checked as health" +) +db_checked_counter = Counter(name="db_checked_counter", documentation="The count of DB has been checked as health") +storage_checked_counter = Counter( + name="storage_checked_counter", documentation="The count of storage has been checked as health" +) +redis_used_memory_bytes = Gauge( + name="redis_used_memory_bytes", documentation="The used bytes of memory in Redis", multiprocess_mode="livesum" +) +redis_total_memory_bytes = Gauge( + name="redis_total_memory_bytes", documentation="The total bytes of memory in Redis", multiprocess_mode="livesum" +) + +db_pool_total_size = Gauge( + name="db_pool_total_size", + documentation="The total size of db pool", + multiprocess_mode="livesum", +) +db_pool_checkout_size = Gauge( + name="db_pool_checkout_size", documentation="The checkout size of db pool", multiprocess_mode="livesum" +) +db_pool_overflow_size = Gauge( + name="db_pool_overflow_size", documentation="The overflow size of db pool", multiprocess_mode="livesum" +) + + +# Using multiprocess collector for registry +def _make_metrics_app(): + if os.getenv("PROMETHEUS_MULTIPROC_DIR", "") != "": + registry = CollectorRegistry() + multiprocess.MultiProcessCollector(registry) + return make_wsgi_app(registry=registry) + else: + return make_wsgi_app() def init_app(app: DifyApp): @@ -18,11 +70,32 @@ def after_request(response): @app.route("/health") def health(): - return Response( - json.dumps({"pid": os.getpid(), "status": "ok", "version": dify_config.CURRENT_VERSION}), - status=200, - content_type="application/json", - ) + try: + health_check_key = "dify.health_check" + redis_client.set(health_check_key, 1) + redis_client.get(health_check_key) + redis_checked_counter.inc() + + info = redis_client.info() + redis_used_memory_bytes.set(info["used_memory"]) + redis_total_memory_bytes.set(info["maxmemory"] if info["maxmemory"] != 0 else info["total_system_memory"]) + + db.session.execute(text("SELECT 1")) + db_checked_counter.inc() + + storage.save(health_check_key, b"test") + storage.load(health_check_key) + storage_checked_counter.inc() + + return Response( + json.dumps({"pid": os.getpid(), "status": "ok", "version": dify_config.CURRENT_VERSION}), + status=200, + content_type="application/json", + ) + finally: + # 最后才增加计数,保证在健康时,也不会出现 health_check_total_counter > *_checked_counter 的情况, + # 保证 max(*_checked_counter / health_check_total_counter, 1) == 1,以便于监控系统判断是否有异常 + health_check_total_counter.inc() @app.route("/threads") def threads(): @@ -54,7 +127,7 @@ def pool_stat(): from extensions.ext_database import db engine = db.engine - return { + stat = { "pid": os.getpid(), "pool_size": engine.pool.size(), "checked_in_connections": engine.pool.checkedin(), @@ -63,3 +136,9 @@ def pool_stat(): "connection_timeout": engine.pool.timeout(), "recycle_time": db.engine.pool._recycle, } + db_pool_total_size.set(stat["pool_size"]) + db_pool_checkout_size.set(stat["checked_out_connections"]) + db_pool_overflow_size.set(stat["overflow_connections"]) + return stat + + app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {"/metrics": _make_metrics_app()}) diff --git a/api/extensions/ext_storage.py b/api/extensions/ext_storage.py index 4b66f3801ef83f..daca2463e2c7e3 100644 --- a/api/extensions/ext_storage.py +++ b/api/extensions/ext_storage.py @@ -1,8 +1,10 @@ import logging from collections.abc import Callable, Generator, Mapping +from functools import wraps from typing import Union from flask import Flask +from prometheus_client import Counter, Histogram from configs import dify_config from configs.middleware.storage.opendal_storage_config import OpenDALScheme @@ -12,6 +14,32 @@ logger = logging.getLogger(__name__) +storage_request_latency = Histogram(name='storage_request_latency', + documentation='The latency of storage requests', + unit='seconds', + labelnames=["method", "provider"]) + +storage_request_total_counter = Counter(name='storage_request_total_counter', + documentation='The total count of storage requests', + labelnames=["method", "provider"]) + +storage_request_failed_counter = Counter(name='storage_request_failed_counter', + documentation='The failed count of storage requests', + labelnames=["method", "provider"]) + + +def timeit(func): + @wraps(func) + def decorator(*args, **kwargs): + with storage_request_latency.labels(method=func.__name__, provider=dify_config.STORAGE_TYPE).time(): + storage_request_total_counter.labels(method=func.__name__, provider=dify_config.STORAGE_TYPE).inc() + try: + return func(*args, **kwargs) + except Exception as e: + storage_request_failed_counter.labels(method=func.__name__, provider=dify_config.STORAGE_TYPE).inc() + raise e + return decorator + class Storage: def init_app(self, app: Flask): @@ -77,6 +105,7 @@ def get_storage_factory(storage_type: str) -> Callable[[], BaseStorage]: case _: raise ValueError(f"Unsupported storage type {storage_type}") + @timeit def save(self, filename, data): try: self.storage_runner.save(filename, data) @@ -84,6 +113,7 @@ def save(self, filename, data): logger.exception(f"Failed to save file {filename}") raise e + @timeit def load(self, filename: str, /, *, stream: bool = False) -> Union[bytes, Generator]: try: if stream: @@ -94,6 +124,7 @@ def load(self, filename: str, /, *, stream: bool = False) -> Union[bytes, Genera logger.exception(f"Failed to load file {filename}") raise e + @timeit def load_once(self, filename: str) -> bytes: try: return self.storage_runner.load_once(filename) @@ -101,6 +132,7 @@ def load_once(self, filename: str) -> bytes: logger.exception(f"Failed to load_once file {filename}") raise e + @timeit def load_stream(self, filename: str) -> Generator: try: return self.storage_runner.load_stream(filename) @@ -108,6 +140,7 @@ def load_stream(self, filename: str) -> Generator: logger.exception(f"Failed to load_stream file {filename}") raise e + @timeit def download(self, filename, target_filepath): try: self.storage_runner.download(filename, target_filepath) @@ -115,6 +148,7 @@ def download(self, filename, target_filepath): logger.exception(f"Failed to download file {filename}") raise e + @timeit def exists(self, filename): try: return self.storage_runner.exists(filename) @@ -122,6 +156,7 @@ def exists(self, filename): logger.exception(f"Failed to check file exists {filename}") raise e + @timeit def delete(self, filename): try: return self.storage_runner.delete(filename) diff --git a/api/poetry.lock b/api/poetry.lock index a68f17064b2c6c..251cc24d31f982 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -955,6 +955,10 @@ files = [ {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"}, {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, @@ -967,8 +971,14 @@ files = [ {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"}, {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"}, {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, @@ -979,8 +989,24 @@ files = [ {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"}, {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"}, + {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"}, + {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"}, {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, @@ -990,6 +1016,10 @@ files = [ {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"}, {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, @@ -1001,6 +1031,10 @@ files = [ {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"}, {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, @@ -1013,6 +1047,10 @@ files = [ {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"}, {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, @@ -1025,6 +1063,10 @@ files = [ {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"}, {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, @@ -1951,7 +1993,6 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -1962,7 +2003,6 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -6810,6 +6850,20 @@ files = [ [package.extras] dev = ["certifi", "pytest (>=8.1.1)"] +[[package]] +name = "prometheus-client" +version = "0.21.1" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +files = [ + {file = "prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"}, + {file = "prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb"}, +] + +[package.extras] +twisted = ["twisted"] + [[package]] name = "prompt-toolkit" version = "3.0.48" @@ -11052,4 +11106,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "1a01596d1d2bbd5240ee8432820f1c026141b16f0be3c8a392b55d1b777a520c" +content-hash = "5fd1c4d7e64f16018430c6197c989fd497cde4b1fa977e0f777e642ddfc47bc8" diff --git a/api/pyproject.toml b/api/pyproject.toml index 230ee09b988c1c..97fd79cc6a85a5 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -60,6 +60,7 @@ oci = "~2.135.1" openai = "~1.52.0" openpyxl = "~3.1.5" pandas = { version = "~2.2.2", extras = ["performance", "excel"] } +prometheus-client = ">=0.5.0,<1.0.0" psycopg2-binary = "~2.9.6" pycryptodome = "3.19.1" pydantic = "~2.9.2"