arize-phoenix 10.0.4__py3-none-any.whl → 12.28.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +124 -72
- arize_phoenix-12.28.1.dist-info/RECORD +499 -0
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/IP_NOTICE +1 -1
- phoenix/__generated__/__init__.py +0 -0
- phoenix/__generated__/classification_evaluator_configs/__init__.py +20 -0
- phoenix/__generated__/classification_evaluator_configs/_document_relevance_classification_evaluator_config.py +17 -0
- phoenix/__generated__/classification_evaluator_configs/_hallucination_classification_evaluator_config.py +17 -0
- phoenix/__generated__/classification_evaluator_configs/_models.py +18 -0
- phoenix/__generated__/classification_evaluator_configs/_tool_selection_classification_evaluator_config.py +17 -0
- phoenix/__init__.py +5 -4
- phoenix/auth.py +39 -2
- phoenix/config.py +1763 -91
- phoenix/datetime_utils.py +120 -2
- phoenix/db/README.md +595 -25
- phoenix/db/bulk_inserter.py +145 -103
- phoenix/db/engines.py +140 -33
- phoenix/db/enums.py +3 -12
- phoenix/db/facilitator.py +302 -35
- phoenix/db/helpers.py +1000 -65
- phoenix/db/iam_auth.py +64 -0
- phoenix/db/insertion/dataset.py +135 -2
- phoenix/db/insertion/document_annotation.py +9 -6
- phoenix/db/insertion/evaluation.py +2 -3
- phoenix/db/insertion/helpers.py +17 -2
- phoenix/db/insertion/session_annotation.py +176 -0
- phoenix/db/insertion/span.py +15 -11
- phoenix/db/insertion/span_annotation.py +3 -4
- phoenix/db/insertion/trace_annotation.py +3 -4
- phoenix/db/insertion/types.py +50 -20
- phoenix/db/migrations/versions/01a8342c9cdf_add_user_id_on_datasets.py +40 -0
- phoenix/db/migrations/versions/0df286449799_add_session_annotations_table.py +105 -0
- phoenix/db/migrations/versions/272b66ff50f8_drop_single_indices.py +119 -0
- phoenix/db/migrations/versions/58228d933c91_dataset_labels.py +67 -0
- phoenix/db/migrations/versions/699f655af132_experiment_tags.py +57 -0
- phoenix/db/migrations/versions/735d3d93c33e_add_composite_indices.py +41 -0
- phoenix/db/migrations/versions/a20694b15f82_cost.py +196 -0
- phoenix/db/migrations/versions/ab513d89518b_add_user_id_on_dataset_versions.py +40 -0
- phoenix/db/migrations/versions/d0690a79ea51_users_on_experiments.py +40 -0
- phoenix/db/migrations/versions/deb2c81c0bb2_dataset_splits.py +139 -0
- phoenix/db/migrations/versions/e76cbd66ffc3_add_experiments_dataset_examples.py +87 -0
- phoenix/db/models.py +669 -56
- phoenix/db/pg_config.py +10 -0
- phoenix/db/types/model_provider.py +4 -0
- phoenix/db/types/token_price_customization.py +29 -0
- phoenix/db/types/trace_retention.py +23 -15
- phoenix/experiments/evaluators/utils.py +3 -3
- phoenix/experiments/functions.py +160 -52
- phoenix/experiments/tracing.py +2 -2
- phoenix/experiments/types.py +1 -1
- phoenix/inferences/inferences.py +1 -2
- phoenix/server/api/auth.py +38 -7
- phoenix/server/api/auth_messages.py +46 -0
- phoenix/server/api/context.py +100 -4
- phoenix/server/api/dataloaders/__init__.py +79 -5
- phoenix/server/api/dataloaders/annotation_configs_by_project.py +31 -0
- phoenix/server/api/dataloaders/annotation_summaries.py +60 -8
- phoenix/server/api/dataloaders/average_experiment_repeated_run_group_latency.py +50 -0
- phoenix/server/api/dataloaders/average_experiment_run_latency.py +17 -24
- phoenix/server/api/dataloaders/cache/two_tier_cache.py +1 -2
- phoenix/server/api/dataloaders/dataset_dataset_splits.py +52 -0
- phoenix/server/api/dataloaders/dataset_example_revisions.py +0 -1
- phoenix/server/api/dataloaders/dataset_example_splits.py +40 -0
- phoenix/server/api/dataloaders/dataset_examples_and_versions_by_experiment_run.py +47 -0
- phoenix/server/api/dataloaders/dataset_labels.py +36 -0
- phoenix/server/api/dataloaders/document_evaluation_summaries.py +2 -2
- phoenix/server/api/dataloaders/document_evaluations.py +6 -9
- phoenix/server/api/dataloaders/experiment_annotation_summaries.py +88 -34
- phoenix/server/api/dataloaders/experiment_dataset_splits.py +43 -0
- phoenix/server/api/dataloaders/experiment_error_rates.py +21 -28
- phoenix/server/api/dataloaders/experiment_repeated_run_group_annotation_summaries.py +77 -0
- phoenix/server/api/dataloaders/experiment_repeated_run_groups.py +57 -0
- phoenix/server/api/dataloaders/experiment_runs_by_experiment_and_example.py +44 -0
- phoenix/server/api/dataloaders/last_used_times_by_generative_model_id.py +35 -0
- phoenix/server/api/dataloaders/latency_ms_quantile.py +40 -8
- phoenix/server/api/dataloaders/record_counts.py +37 -10
- phoenix/server/api/dataloaders/session_annotations_by_session.py +29 -0
- phoenix/server/api/dataloaders/span_cost_by_span.py +24 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_generative_model.py +56 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_project_session.py +57 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_span.py +43 -0
- phoenix/server/api/dataloaders/span_cost_detail_summary_entries_by_trace.py +56 -0
- phoenix/server/api/dataloaders/span_cost_details_by_span_cost.py +27 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment.py +57 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment_repeated_run_group.py +64 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_experiment_run.py +58 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_generative_model.py +55 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_project.py +152 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_project_session.py +56 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_trace.py +55 -0
- phoenix/server/api/dataloaders/span_costs.py +29 -0
- phoenix/server/api/dataloaders/table_fields.py +2 -2
- phoenix/server/api/dataloaders/token_prices_by_model.py +30 -0
- phoenix/server/api/dataloaders/trace_annotations_by_trace.py +27 -0
- phoenix/server/api/dataloaders/types.py +29 -0
- phoenix/server/api/exceptions.py +11 -1
- phoenix/server/api/helpers/dataset_helpers.py +5 -1
- phoenix/server/api/helpers/playground_clients.py +1243 -292
- phoenix/server/api/helpers/playground_registry.py +2 -2
- phoenix/server/api/helpers/playground_spans.py +8 -4
- phoenix/server/api/helpers/playground_users.py +26 -0
- phoenix/server/api/helpers/prompts/conversions/aws.py +83 -0
- phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
- phoenix/server/api/helpers/prompts/models.py +205 -22
- phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
- phoenix/server/api/input_types/ChatCompletionInput.py +6 -2
- phoenix/server/api/input_types/CreateProjectInput.py +27 -0
- phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
- phoenix/server/api/input_types/DatasetFilter.py +17 -0
- phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
- phoenix/server/api/input_types/GenerativeCredentialInput.py +9 -0
- phoenix/server/api/input_types/GenerativeModelInput.py +5 -0
- phoenix/server/api/input_types/ProjectSessionSort.py +161 -1
- phoenix/server/api/input_types/PromptFilter.py +14 -0
- phoenix/server/api/input_types/PromptVersionInput.py +52 -1
- phoenix/server/api/input_types/SpanSort.py +44 -7
- phoenix/server/api/input_types/TimeBinConfig.py +23 -0
- phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
- phoenix/server/api/input_types/UserRoleInput.py +1 -0
- phoenix/server/api/mutations/__init__.py +10 -0
- phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
- phoenix/server/api/mutations/api_key_mutations.py +19 -23
- phoenix/server/api/mutations/chat_mutations.py +154 -47
- phoenix/server/api/mutations/dataset_label_mutations.py +243 -0
- phoenix/server/api/mutations/dataset_mutations.py +21 -16
- phoenix/server/api/mutations/dataset_split_mutations.py +351 -0
- phoenix/server/api/mutations/experiment_mutations.py +2 -2
- phoenix/server/api/mutations/export_events_mutations.py +3 -3
- phoenix/server/api/mutations/model_mutations.py +210 -0
- phoenix/server/api/mutations/project_mutations.py +49 -10
- phoenix/server/api/mutations/project_session_annotations_mutations.py +158 -0
- phoenix/server/api/mutations/project_trace_retention_policy_mutations.py +8 -4
- phoenix/server/api/mutations/prompt_label_mutations.py +74 -65
- phoenix/server/api/mutations/prompt_mutations.py +65 -129
- phoenix/server/api/mutations/prompt_version_tag_mutations.py +11 -8
- phoenix/server/api/mutations/span_annotations_mutations.py +15 -10
- phoenix/server/api/mutations/trace_annotations_mutations.py +14 -10
- phoenix/server/api/mutations/trace_mutations.py +47 -3
- phoenix/server/api/mutations/user_mutations.py +66 -41
- phoenix/server/api/queries.py +768 -293
- phoenix/server/api/routers/__init__.py +2 -2
- phoenix/server/api/routers/auth.py +154 -88
- phoenix/server/api/routers/ldap.py +229 -0
- phoenix/server/api/routers/oauth2.py +369 -106
- phoenix/server/api/routers/v1/__init__.py +24 -4
- phoenix/server/api/routers/v1/annotation_configs.py +23 -31
- phoenix/server/api/routers/v1/annotations.py +481 -17
- phoenix/server/api/routers/v1/datasets.py +395 -81
- phoenix/server/api/routers/v1/documents.py +142 -0
- phoenix/server/api/routers/v1/evaluations.py +24 -31
- phoenix/server/api/routers/v1/experiment_evaluations.py +19 -8
- phoenix/server/api/routers/v1/experiment_runs.py +337 -59
- phoenix/server/api/routers/v1/experiments.py +479 -48
- phoenix/server/api/routers/v1/models.py +7 -0
- phoenix/server/api/routers/v1/projects.py +18 -49
- phoenix/server/api/routers/v1/prompts.py +54 -40
- phoenix/server/api/routers/v1/sessions.py +108 -0
- phoenix/server/api/routers/v1/spans.py +1091 -81
- phoenix/server/api/routers/v1/traces.py +132 -78
- phoenix/server/api/routers/v1/users.py +389 -0
- phoenix/server/api/routers/v1/utils.py +3 -7
- phoenix/server/api/subscriptions.py +305 -88
- phoenix/server/api/types/Annotation.py +90 -23
- phoenix/server/api/types/ApiKey.py +13 -17
- phoenix/server/api/types/AuthMethod.py +1 -0
- phoenix/server/api/types/ChatCompletionSubscriptionPayload.py +1 -0
- phoenix/server/api/types/CostBreakdown.py +12 -0
- phoenix/server/api/types/Dataset.py +226 -72
- phoenix/server/api/types/DatasetExample.py +88 -18
- phoenix/server/api/types/DatasetExperimentAnnotationSummary.py +10 -0
- phoenix/server/api/types/DatasetLabel.py +57 -0
- phoenix/server/api/types/DatasetSplit.py +98 -0
- phoenix/server/api/types/DatasetVersion.py +49 -4
- phoenix/server/api/types/DocumentAnnotation.py +212 -0
- phoenix/server/api/types/Experiment.py +264 -59
- phoenix/server/api/types/ExperimentComparison.py +5 -10
- phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
- phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
- phoenix/server/api/types/ExperimentRun.py +169 -65
- phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
- phoenix/server/api/types/GenerativeModel.py +245 -3
- phoenix/server/api/types/GenerativeProvider.py +70 -11
- phoenix/server/api/types/{Model.py → InferenceModel.py} +1 -1
- phoenix/server/api/types/ModelInterface.py +16 -0
- phoenix/server/api/types/PlaygroundModel.py +20 -0
- phoenix/server/api/types/Project.py +1278 -216
- phoenix/server/api/types/ProjectSession.py +188 -28
- phoenix/server/api/types/ProjectSessionAnnotation.py +187 -0
- phoenix/server/api/types/ProjectTraceRetentionPolicy.py +1 -1
- phoenix/server/api/types/Prompt.py +119 -39
- phoenix/server/api/types/PromptLabel.py +42 -25
- phoenix/server/api/types/PromptVersion.py +11 -8
- phoenix/server/api/types/PromptVersionTag.py +65 -25
- phoenix/server/api/types/ServerStatus.py +6 -0
- phoenix/server/api/types/Span.py +167 -123
- phoenix/server/api/types/SpanAnnotation.py +189 -42
- phoenix/server/api/types/SpanCostDetailSummaryEntry.py +10 -0
- phoenix/server/api/types/SpanCostSummary.py +10 -0
- phoenix/server/api/types/SystemApiKey.py +65 -1
- phoenix/server/api/types/TokenPrice.py +16 -0
- phoenix/server/api/types/TokenUsage.py +3 -3
- phoenix/server/api/types/Trace.py +223 -51
- phoenix/server/api/types/TraceAnnotation.py +149 -50
- phoenix/server/api/types/User.py +137 -32
- phoenix/server/api/types/UserApiKey.py +73 -26
- phoenix/server/api/types/node.py +10 -0
- phoenix/server/api/types/pagination.py +11 -2
- phoenix/server/app.py +290 -45
- phoenix/server/authorization.py +38 -3
- phoenix/server/bearer_auth.py +34 -24
- phoenix/server/cost_tracking/cost_details_calculator.py +196 -0
- phoenix/server/cost_tracking/cost_model_lookup.py +179 -0
- phoenix/server/cost_tracking/helpers.py +68 -0
- phoenix/server/cost_tracking/model_cost_manifest.json +3657 -830
- phoenix/server/cost_tracking/regex_specificity.py +397 -0
- phoenix/server/cost_tracking/token_cost_calculator.py +57 -0
- phoenix/server/daemons/__init__.py +0 -0
- phoenix/server/daemons/db_disk_usage_monitor.py +214 -0
- phoenix/server/daemons/generative_model_store.py +103 -0
- phoenix/server/daemons/span_cost_calculator.py +99 -0
- phoenix/server/dml_event.py +17 -0
- phoenix/server/dml_event_handler.py +5 -0
- phoenix/server/email/sender.py +56 -3
- phoenix/server/email/templates/db_disk_usage_notification.html +19 -0
- phoenix/server/email/types.py +11 -0
- phoenix/server/experiments/__init__.py +0 -0
- phoenix/server/experiments/utils.py +14 -0
- phoenix/server/grpc_server.py +11 -11
- phoenix/server/jwt_store.py +17 -15
- phoenix/server/ldap.py +1449 -0
- phoenix/server/main.py +26 -10
- phoenix/server/oauth2.py +330 -12
- phoenix/server/prometheus.py +66 -6
- phoenix/server/rate_limiters.py +4 -9
- phoenix/server/retention.py +33 -20
- phoenix/server/session_filters.py +49 -0
- phoenix/server/static/.vite/manifest.json +55 -51
- phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
- phoenix/server/static/assets/{index-E0M82BdE.js → index-CTQoemZv.js} +140 -56
- phoenix/server/static/assets/pages-DBE5iYM3.js +9524 -0
- phoenix/server/static/assets/vendor-BGzfc4EU.css +1 -0
- phoenix/server/static/assets/vendor-DCE4v-Ot.js +920 -0
- phoenix/server/static/assets/vendor-codemirror-D5f205eT.js +25 -0
- phoenix/server/static/assets/vendor-recharts-V9cwpXsm.js +37 -0
- phoenix/server/static/assets/vendor-shiki-Do--csgv.js +5 -0
- phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
- phoenix/server/templates/index.html +40 -6
- phoenix/server/thread_server.py +1 -2
- phoenix/server/types.py +14 -4
- phoenix/server/utils.py +74 -0
- phoenix/session/client.py +56 -3
- phoenix/session/data_extractor.py +5 -0
- phoenix/session/evaluation.py +14 -5
- phoenix/session/session.py +45 -9
- phoenix/settings.py +5 -0
- phoenix/trace/attributes.py +80 -13
- phoenix/trace/dsl/helpers.py +90 -1
- phoenix/trace/dsl/query.py +8 -6
- phoenix/trace/projects.py +5 -0
- phoenix/utilities/template_formatters.py +1 -1
- phoenix/version.py +1 -1
- arize_phoenix-10.0.4.dist-info/RECORD +0 -405
- phoenix/server/api/types/Evaluation.py +0 -39
- phoenix/server/cost_tracking/cost_lookup.py +0 -255
- phoenix/server/static/assets/components-DULKeDfL.js +0 -4365
- phoenix/server/static/assets/pages-Cl0A-0U2.js +0 -7430
- phoenix/server/static/assets/vendor-WIZid84E.css +0 -1
- phoenix/server/static/assets/vendor-arizeai-Dy-0mSNw.js +0 -649
- phoenix/server/static/assets/vendor-codemirror-DBtifKNr.js +0 -33
- phoenix/server/static/assets/vendor-oB4u9zuV.js +0 -905
- phoenix/server/static/assets/vendor-recharts-D-T4KPz2.js +0 -59
- phoenix/server/static/assets/vendor-shiki-BMn4O_9F.js +0 -5
- phoenix/server/static/assets/vendor-three-C5WAXd5r.js +0 -2998
- phoenix/utilities/deprecation.py +0 -31
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-10.0.4.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/LICENSE +0 -0
phoenix/server/main.py
CHANGED
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from ssl import CERT_REQUIRED
|
|
8
8
|
from threading import Thread
|
|
9
9
|
from time import sleep, time
|
|
10
|
-
from typing import Optional
|
|
10
|
+
from typing import Awaitable, Callable, Optional
|
|
11
11
|
from urllib.parse import urljoin
|
|
12
12
|
|
|
13
13
|
from jinja2 import BaseLoader, Environment
|
|
@@ -25,16 +25,19 @@ from phoenix.config import (
|
|
|
25
25
|
get_env_db_logging_level,
|
|
26
26
|
get_env_disable_migrations,
|
|
27
27
|
get_env_enable_prometheus,
|
|
28
|
+
get_env_fullstory_org,
|
|
28
29
|
get_env_grpc_port,
|
|
29
30
|
get_env_host,
|
|
30
31
|
get_env_host_root_path,
|
|
31
32
|
get_env_log_migrations,
|
|
32
33
|
get_env_logging_level,
|
|
33
34
|
get_env_logging_mode,
|
|
35
|
+
get_env_management_url,
|
|
34
36
|
get_env_oauth2_settings,
|
|
35
37
|
get_env_password_reset_token_expiry,
|
|
36
38
|
get_env_port,
|
|
37
39
|
get_env_refresh_token_expiry,
|
|
40
|
+
get_env_scarf_sh_pixel_id,
|
|
38
41
|
get_env_smtp_hostname,
|
|
39
42
|
get_env_smtp_mail_from,
|
|
40
43
|
get_env_smtp_password,
|
|
@@ -90,15 +93,15 @@ _WELCOME_MESSAGE = Environment(loader=BaseLoader()).from_string("""
|
|
|
90
93
|
██║ ██║ ██║╚██████╔╝███████╗██║ ╚████║██║██╔╝ ██╗
|
|
91
94
|
╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝ v{{ version }}
|
|
92
95
|
|
|
96
|
+
| ⭐️⭐️⭐️ Support Open Source ⭐️⭐️⭐️
|
|
97
|
+
| ⭐️⭐️⭐️ Star on GitHub! ⭐️⭐️⭐️
|
|
98
|
+
| https://github.com/Arize-ai/phoenix
|
|
93
99
|
|
|
|
94
100
|
| 🌎 Join our Community 🌎
|
|
95
101
|
| https://arize-ai.slack.com/join/shared_invite/zt-2w57bhem8-hq24MB6u7yE_ZF_ilOYSBw#/shared-invite/email
|
|
96
102
|
|
|
|
97
|
-
| ⭐️ Leave us a Star ⭐️
|
|
98
|
-
| https://github.com/Arize-ai/phoenix
|
|
99
|
-
|
|
|
100
103
|
| 📚 Documentation 📚
|
|
101
|
-
| https://
|
|
104
|
+
| https://arize.com/docs/phoenix
|
|
102
105
|
|
|
|
103
106
|
| 🚀 Phoenix Server 🚀
|
|
104
107
|
| Phoenix UI: {{ ui_path }}
|
|
@@ -369,13 +372,17 @@ def main() -> None:
|
|
|
369
372
|
start_prometheus()
|
|
370
373
|
|
|
371
374
|
engine = create_engine_and_run_migrations(db_connection_str)
|
|
372
|
-
|
|
375
|
+
shutdown_callbacks: list[Callable[[], None | Awaitable[None]]] = []
|
|
376
|
+
shutdown_callbacks.extend(instrument_engine_if_enabled(engine))
|
|
377
|
+
# Ensure engine is disposed on shutdown to properly close database connections
|
|
378
|
+
shutdown_callbacks.append(engine.dispose)
|
|
373
379
|
factory = DbSessionFactory(db=_db(engine), dialect=engine.dialect.name)
|
|
374
380
|
corpus_model = (
|
|
375
381
|
None if corpus_inferences is None else create_model_from_inferences(corpus_inferences)
|
|
376
382
|
)
|
|
377
383
|
|
|
378
384
|
allowed_origins = get_env_allowed_origins()
|
|
385
|
+
management_url = get_env_management_url()
|
|
379
386
|
|
|
380
387
|
# Get TLS configuration
|
|
381
388
|
tls_enabled_for_http = get_env_tls_enabled_for_http()
|
|
@@ -386,12 +393,15 @@ def main() -> None:
|
|
|
386
393
|
# Print information about the server
|
|
387
394
|
http_scheme = "https" if tls_enabled_for_http else "http"
|
|
388
395
|
grpc_scheme = "https" if tls_enabled_for_grpc else "http"
|
|
396
|
+
# Use localhost for display when host is the loopback address to make URLs clickable
|
|
397
|
+
display_host = "localhost" if host in ("0.0.0.0", "::") else host
|
|
389
398
|
root_path = urljoin(f"{http_scheme}://{host}:{port}", host_root_path)
|
|
399
|
+
display_root_path = urljoin(f"{http_scheme}://{display_host}:{port}", host_root_path)
|
|
390
400
|
msg = _WELCOME_MESSAGE.render(
|
|
391
401
|
version=phoenix_version,
|
|
392
|
-
ui_path=
|
|
393
|
-
grpc_path=f"{grpc_scheme}://{
|
|
394
|
-
http_path=urljoin(
|
|
402
|
+
ui_path=display_root_path,
|
|
403
|
+
grpc_path=f"{grpc_scheme}://{display_host}:{get_env_grpc_port()}",
|
|
404
|
+
http_path=urljoin(display_root_path, "v1/traces"),
|
|
395
405
|
storage=get_printable_db_url(db_connection_str),
|
|
396
406
|
schema=get_env_database_schema(),
|
|
397
407
|
auth_enabled=auth_settings.enable_auth,
|
|
@@ -442,7 +452,7 @@ def main() -> None:
|
|
|
442
452
|
initial_spans=fixture_spans,
|
|
443
453
|
initial_evaluations=fixture_evals,
|
|
444
454
|
startup_callbacks=[lambda: print(msg)],
|
|
445
|
-
shutdown_callbacks=
|
|
455
|
+
shutdown_callbacks=shutdown_callbacks,
|
|
446
456
|
secret=auth_settings.phoenix_secret,
|
|
447
457
|
password_reset_token_expiry=get_env_password_reset_token_expiry(),
|
|
448
458
|
access_token_expiry=get_env_access_token_expiry(),
|
|
@@ -450,7 +460,9 @@ def main() -> None:
|
|
|
450
460
|
scaffolder_config=scaffolder_config,
|
|
451
461
|
email_sender=email_sender,
|
|
452
462
|
oauth2_client_configs=get_env_oauth2_settings(),
|
|
463
|
+
ldap_config=auth_settings.ldap_config,
|
|
453
464
|
allowed_origins=allowed_origins,
|
|
465
|
+
management_url=management_url,
|
|
454
466
|
)
|
|
455
467
|
|
|
456
468
|
# Configure server with TLS if enabled
|
|
@@ -459,6 +471,7 @@ def main() -> None:
|
|
|
459
471
|
host=host, # type: ignore[arg-type]
|
|
460
472
|
port=port,
|
|
461
473
|
root_path=host_root_path,
|
|
474
|
+
log_level=Settings.logging_level,
|
|
462
475
|
)
|
|
463
476
|
|
|
464
477
|
if tls_enabled_for_http:
|
|
@@ -483,11 +496,14 @@ def main() -> None:
|
|
|
483
496
|
|
|
484
497
|
|
|
485
498
|
def initialize_settings() -> None:
|
|
499
|
+
"""Initialize the settings from environment variables."""
|
|
486
500
|
Settings.logging_mode = get_env_logging_mode()
|
|
487
501
|
Settings.logging_level = get_env_logging_level()
|
|
488
502
|
Settings.db_logging_level = get_env_db_logging_level()
|
|
489
503
|
Settings.log_migrations = get_env_log_migrations()
|
|
490
504
|
Settings.disable_migrations = get_env_disable_migrations()
|
|
505
|
+
Settings.fullstory_org = get_env_fullstory_org()
|
|
506
|
+
Settings.scarf_sh_pixel_id = get_env_scarf_sh_pixel_id()
|
|
491
507
|
|
|
492
508
|
|
|
493
509
|
if __name__ == "__main__":
|
phoenix/server/oauth2.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
from
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Iterable, Mapping
|
|
3
|
+
from typing import Any, Iterator, Optional, get_args
|
|
3
4
|
|
|
5
|
+
import jmespath
|
|
4
6
|
from authlib.integrations.base_client import BaseApp
|
|
5
7
|
from authlib.integrations.base_client.async_app import AsyncOAuth2Mixin
|
|
6
8
|
from authlib.integrations.base_client.async_openid import AsyncOpenIDMixin
|
|
7
9
|
from authlib.integrations.httpx_client import AsyncOAuth2Client as AsyncHttpxOAuth2Client
|
|
8
10
|
|
|
9
|
-
from phoenix.config import OAuth2ClientConfig
|
|
11
|
+
from phoenix.config import AssignableUserRoleName, OAuth2ClientConfig
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
class OAuth2Client(AsyncOAuth2Mixin, AsyncOpenIDMixin, BaseApp): # type:ignore[misc]
|
|
@@ -25,13 +29,78 @@ class OAuth2Client(AsyncOAuth2Mixin, AsyncOpenIDMixin, BaseApp): # type:ignore[
|
|
|
25
29
|
display_name: str,
|
|
26
30
|
allow_sign_up: bool,
|
|
27
31
|
auto_login: bool,
|
|
32
|
+
use_pkce: bool = False,
|
|
33
|
+
groups_attribute_path: Optional[str] = None,
|
|
34
|
+
allowed_groups: Optional[list[str]] = None,
|
|
35
|
+
role_attribute_path: Optional[str] = None,
|
|
36
|
+
role_mapping: Optional[Mapping[str, AssignableUserRoleName]] = None,
|
|
37
|
+
role_attribute_strict: bool = False,
|
|
28
38
|
**kwargs: Any,
|
|
29
39
|
) -> None:
|
|
30
40
|
self._display_name = display_name
|
|
31
41
|
self._allow_sign_up = allow_sign_up
|
|
32
42
|
self._auto_login = auto_login
|
|
43
|
+
self._use_pkce = use_pkce
|
|
44
|
+
|
|
45
|
+
self._groups_attribute_path = (
|
|
46
|
+
groups_attribute_path.strip()
|
|
47
|
+
if groups_attribute_path and groups_attribute_path.strip()
|
|
48
|
+
else None
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if allowed_groups:
|
|
52
|
+
self._allowed_groups = {g for g in allowed_groups if g.strip()}
|
|
53
|
+
else:
|
|
54
|
+
self._allowed_groups = set()
|
|
55
|
+
|
|
56
|
+
if self._allowed_groups and not self._groups_attribute_path:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
"groups_attribute_path must be specified when allowed_groups is configured. "
|
|
59
|
+
"Group-based access control requires both parameters to be set."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if self._groups_attribute_path and not self._allowed_groups:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
"allowed_groups must be specified when groups_attribute_path is configured. "
|
|
65
|
+
"Group-based access control requires both parameters to be set. "
|
|
66
|
+
"If you don't need group-based access control, remove groups_attribute_path."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
self._compiled_groups_path = self._compile_jmespath_expression(
|
|
70
|
+
self._groups_attribute_path, "GROUPS_ATTRIBUTE_PATH"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Role mapping configuration
|
|
74
|
+
self._role_attribute_path = (
|
|
75
|
+
role_attribute_path.strip()
|
|
76
|
+
if role_attribute_path and role_attribute_path.strip()
|
|
77
|
+
else None
|
|
78
|
+
)
|
|
79
|
+
self._role_mapping = role_mapping
|
|
80
|
+
self._role_attribute_strict = role_attribute_strict
|
|
81
|
+
self._compiled_role_path = self._compile_jmespath_expression(
|
|
82
|
+
self._role_attribute_path, "ROLE_ATTRIBUTE_PATH"
|
|
83
|
+
)
|
|
84
|
+
|
|
33
85
|
super().__init__(framework=None, *args, **kwargs)
|
|
34
|
-
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _compile_jmespath_expression(
|
|
89
|
+
path: Optional[str], attribute_name: str
|
|
90
|
+
) -> Optional[jmespath.parser.ParsedResult]:
|
|
91
|
+
"""Validate and compile JMESPath expression at startup for fail-fast behavior."""
|
|
92
|
+
if not path:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
return jmespath.compile(path)
|
|
97
|
+
except (jmespath.exceptions.JMESPathError, jmespath.exceptions.ParseError) as e:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Invalid JMESPath expression in {attribute_name}: '{path}'. Error: {e}. "
|
|
100
|
+
"Hint: Claim keys with special characters (colons, dots, slashes, hyphens) "
|
|
101
|
+
"must be enclosed in double quotes. "
|
|
102
|
+
"Examples: '\"cognito:groups\"', '\"https://myapp.com/groups\"'"
|
|
103
|
+
) from e
|
|
35
104
|
|
|
36
105
|
@property
|
|
37
106
|
def allow_sign_up(self) -> bool:
|
|
@@ -45,6 +114,240 @@ class OAuth2Client(AsyncOAuth2Mixin, AsyncOpenIDMixin, BaseApp): # type:ignore[
|
|
|
45
114
|
def display_name(self) -> str:
|
|
46
115
|
return self._display_name
|
|
47
116
|
|
|
117
|
+
@property
|
|
118
|
+
def use_pkce(self) -> bool:
|
|
119
|
+
return self._use_pkce
|
|
120
|
+
|
|
121
|
+
def has_sufficient_claims(self, claims: dict[str, Any]) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
Check if the ID token contains all application-required claims.
|
|
124
|
+
|
|
125
|
+
OIDC Core §2 mandates that ID tokens contain authentication claims (iss, sub, aud,
|
|
126
|
+
exp, iat), but user profile claims (email, name, groups, roles) are optional and may
|
|
127
|
+
only be available via UserInfo endpoint (§5.4, §5.5). This method determines if we
|
|
128
|
+
need to call UserInfo.
|
|
129
|
+
|
|
130
|
+
Application-required claims:
|
|
131
|
+
- email: Required for user identification and account creation
|
|
132
|
+
- groups: Required if group-based access control is configured
|
|
133
|
+
- roles: Required if role mapping is configured
|
|
134
|
+
|
|
135
|
+
If any required claim is missing, returns False to trigger UserInfo endpoint call.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
claims: Claims from ID token (OIDC Core §3.1.3.3)
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if all application-required claims are present (UserInfo not needed)
|
|
142
|
+
False if additional claims must be fetched from UserInfo endpoint
|
|
143
|
+
"""
|
|
144
|
+
# Check for email claim (required by application)
|
|
145
|
+
email = claims.get("email")
|
|
146
|
+
if not email or not isinstance(email, str) or not email.strip():
|
|
147
|
+
# Email missing or invalid, need UserInfo
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
# Check for group claims if group-based access control is configured
|
|
151
|
+
if self._compiled_groups_path:
|
|
152
|
+
groups = self._extract_groups_from_claims(claims)
|
|
153
|
+
if len(groups) == 0:
|
|
154
|
+
# Groups required but not present, need UserInfo
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
# Check for role claims if role mapping is configured
|
|
158
|
+
if self._compiled_role_path:
|
|
159
|
+
# Check if role claim EXISTS (not whether it maps successfully)
|
|
160
|
+
# Optimization: If the claim exists but doesn't map, UserInfo won't help
|
|
161
|
+
result = self._compiled_role_path.search(claims)
|
|
162
|
+
role_value = self._normalize_to_single_string(result)
|
|
163
|
+
if not role_value:
|
|
164
|
+
# Role claim missing - UserInfo might have a mappable role
|
|
165
|
+
# (could upgrade from default VIEWER to ADMIN/MEMBER)
|
|
166
|
+
return False
|
|
167
|
+
# Role exists - UserInfo won't help even if role doesn't map
|
|
168
|
+
# (UserInfo will have the same unmappable role)
|
|
169
|
+
|
|
170
|
+
# All required claims present
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
def validate_access(self, user_claims: dict[str, Any]) -> None:
|
|
174
|
+
"""
|
|
175
|
+
Validate that the user has access based on configured claim-based access control.
|
|
176
|
+
|
|
177
|
+
Currently supports group-based access control. In the future, this may be extended
|
|
178
|
+
to support organization-based or other claim-based authorization mechanisms.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
user_claims: Claims from the OIDC ID token (OIDC Core §3.1.3.3) or userinfo
|
|
182
|
+
endpoint (OIDC Core §5.3). Custom claims for groups/roles are extracted
|
|
183
|
+
per OIDC Core §5.1.2 (Additional Claims).
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
PermissionError: If user doesn't meet the access requirements
|
|
187
|
+
"""
|
|
188
|
+
if not self._allowed_groups or not self._groups_attribute_path:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
user_groups = self._extract_groups_from_claims(user_claims)
|
|
192
|
+
|
|
193
|
+
if not any(group in self._allowed_groups for group in user_groups):
|
|
194
|
+
raise PermissionError(
|
|
195
|
+
"Access denied. Your account does not belong to any authorized groups."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def _extract_groups_from_claims(self, claims: dict[str, Any]) -> list[str]:
|
|
199
|
+
"""Extract group values from claims using the configured JMESPath expression."""
|
|
200
|
+
if not self._compiled_groups_path:
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
result = self._compiled_groups_path.search(claims)
|
|
204
|
+
return self._normalize_to_string_list(result)
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _normalize_to_string_list(value: Any) -> list[str]:
|
|
208
|
+
"""
|
|
209
|
+
Normalize a JMESPath result to a list of strings.
|
|
210
|
+
|
|
211
|
+
Handles common OIDC claim formats: single values, lists, and scalar types.
|
|
212
|
+
Non-scalar items (dicts, nested lists) are silently skipped.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
value: Result from JMESPath query
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
List of string values, or empty list if value cannot be normalized
|
|
219
|
+
"""
|
|
220
|
+
if value is None:
|
|
221
|
+
return []
|
|
222
|
+
|
|
223
|
+
if isinstance(value, str):
|
|
224
|
+
return [value]
|
|
225
|
+
|
|
226
|
+
if isinstance(value, (int, float, bool)):
|
|
227
|
+
return [str(value)]
|
|
228
|
+
|
|
229
|
+
if isinstance(value, list):
|
|
230
|
+
return [
|
|
231
|
+
str(item) if isinstance(item, (int, float, bool)) else item
|
|
232
|
+
for item in value
|
|
233
|
+
if isinstance(item, (str, int, float, bool))
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
def extract_and_map_role(self, user_claims: dict[str, Any]) -> Optional[AssignableUserRoleName]:
|
|
239
|
+
"""
|
|
240
|
+
Extract and map user role from OIDC claims.
|
|
241
|
+
|
|
242
|
+
This method extracts the role claim using the configured JMESPath expression,
|
|
243
|
+
optionally applies role mapping to translate IDP role values to Phoenix roles,
|
|
244
|
+
and handles missing/invalid roles based on the strict mode setting.
|
|
245
|
+
|
|
246
|
+
Role Mapping Flow:
|
|
247
|
+
1. Extract role claim using ROLE_ATTRIBUTE_PATH (JMESPath)
|
|
248
|
+
- Supports simple paths: "role", "user.org.role"
|
|
249
|
+
- Supports conditional logic: "contains(groups[*], 'admin') && 'ADMIN' || 'VIEWER'"
|
|
250
|
+
2. Apply ROLE_MAPPING (if configured) to translate IDP role → Phoenix role
|
|
251
|
+
- If ROLE_MAPPING not set, use extracted value directly if valid (ADMIN/MEMBER/VIEWER)
|
|
252
|
+
- This allows JMESPath expressions to return Phoenix roles directly
|
|
253
|
+
3. Validate Phoenix role (ADMIN, MEMBER, VIEWER - SYSTEM excluded for OAuth)
|
|
254
|
+
4. Handle missing/invalid roles:
|
|
255
|
+
- strict=True: Raise PermissionError (deny access)
|
|
256
|
+
- strict=False: Return "VIEWER" (default, least privilege)
|
|
257
|
+
|
|
258
|
+
IMPORTANT: Backward Compatibility
|
|
259
|
+
- If ROLE_ATTRIBUTE_PATH is NOT configured, returns None
|
|
260
|
+
- This preserves existing users' roles (no unwanted downgrades)
|
|
261
|
+
- Caller should only apply "VIEWER" default for NEW users
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
user_claims: Claims from the OIDC ID token or userinfo endpoint
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Phoenix role name (ADMIN, MEMBER, or VIEWER), or None if role attribute
|
|
268
|
+
path is not configured (to preserve existing user roles)
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
PermissionError: If strict mode is enabled and role cannot be determined
|
|
272
|
+
"""
|
|
273
|
+
# If no role mapping configured, return None to preserve existing user roles
|
|
274
|
+
if not self._compiled_role_path:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
# Extract role from claims
|
|
278
|
+
result = self._compiled_role_path.search(user_claims)
|
|
279
|
+
role_value = self._normalize_to_single_string(result)
|
|
280
|
+
|
|
281
|
+
# If role claim is missing or empty
|
|
282
|
+
if not role_value:
|
|
283
|
+
if self._role_attribute_strict:
|
|
284
|
+
raise PermissionError(
|
|
285
|
+
f"Access denied: Role claim not found in user claims. "
|
|
286
|
+
f"Role attribute path '{self._role_attribute_path}' is configured with "
|
|
287
|
+
f"strict mode enabled."
|
|
288
|
+
)
|
|
289
|
+
return "VIEWER" # Non-strict: default to least privilege
|
|
290
|
+
|
|
291
|
+
# Apply role mapping if configured
|
|
292
|
+
if self._role_mapping:
|
|
293
|
+
mapped_role = self._role_mapping.get(role_value)
|
|
294
|
+
if not mapped_role:
|
|
295
|
+
# Role value doesn't match any mapping
|
|
296
|
+
if self._role_attribute_strict:
|
|
297
|
+
raise PermissionError(
|
|
298
|
+
f"Access denied: Role '{role_value}' is not mapped to a Phoenix role. "
|
|
299
|
+
f"Role mapping is configured with strict mode enabled."
|
|
300
|
+
)
|
|
301
|
+
return "VIEWER" # Non-strict: default to least privilege
|
|
302
|
+
return mapped_role
|
|
303
|
+
|
|
304
|
+
# No role mapping configured, but role path exists
|
|
305
|
+
# Try to use the raw role value directly if it's a valid Phoenix role
|
|
306
|
+
# Note: SYSTEM is excluded from valid roles for OIDC (validated at config parsing)
|
|
307
|
+
role_upper = role_value.upper()
|
|
308
|
+
if role_upper in get_args(AssignableUserRoleName):
|
|
309
|
+
return role_upper # type: ignore[return-value]
|
|
310
|
+
|
|
311
|
+
# Role value is not a valid Phoenix role
|
|
312
|
+
if self._role_attribute_strict:
|
|
313
|
+
raise PermissionError(
|
|
314
|
+
f"Access denied: Role '{role_value}' is not a valid Phoenix role "
|
|
315
|
+
f"(expected ADMIN, MEMBER, or VIEWER). Strict mode is enabled."
|
|
316
|
+
)
|
|
317
|
+
return "VIEWER" # Non-strict: default to least privilege
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def _normalize_to_single_string(value: Any) -> Optional[str]:
|
|
321
|
+
"""
|
|
322
|
+
Normalize a JMESPath result to a single string value.
|
|
323
|
+
|
|
324
|
+
Handles common OIDC claim formats for single-value fields like role.
|
|
325
|
+
If the result is a list, takes the first element.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
value: Result from JMESPath query
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
String value or None if value cannot be normalized
|
|
332
|
+
"""
|
|
333
|
+
if value is None:
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
if isinstance(value, str):
|
|
337
|
+
return value.strip() or None
|
|
338
|
+
|
|
339
|
+
if isinstance(value, (int, float, bool)):
|
|
340
|
+
return str(value)
|
|
341
|
+
|
|
342
|
+
if isinstance(value, list) and len(value) > 0:
|
|
343
|
+
first = value[0]
|
|
344
|
+
if isinstance(first, str):
|
|
345
|
+
return first.strip() or None
|
|
346
|
+
if isinstance(first, (int, float, bool)):
|
|
347
|
+
return str(first)
|
|
348
|
+
|
|
349
|
+
return None
|
|
350
|
+
|
|
48
351
|
|
|
49
352
|
class OAuth2Clients:
|
|
50
353
|
def __init__(self) -> None:
|
|
@@ -67,26 +370,41 @@ class OAuth2Clients:
|
|
|
67
370
|
def add_client(self, config: OAuth2ClientConfig) -> None:
|
|
68
371
|
if (idp_name := config.idp_name) in self._clients:
|
|
69
372
|
raise ValueError(f"oauth client already registered: {idp_name}")
|
|
373
|
+
# RFC 6749 §3.3: scope parameter (space-delimited list of scopes)
|
|
374
|
+
client_kwargs = {"scope": config.scopes}
|
|
375
|
+
|
|
376
|
+
if config.token_endpoint_auth_method:
|
|
377
|
+
# OIDC Core §9: Client authentication method at token endpoint
|
|
378
|
+
client_kwargs["token_endpoint_auth_method"] = config.token_endpoint_auth_method
|
|
379
|
+
if config.use_pkce:
|
|
380
|
+
# Always use S256 for PKCE (RFC 7636 §4.2: SHA-256 code challenge method)
|
|
381
|
+
client_kwargs["code_challenge_method"] = "S256"
|
|
382
|
+
|
|
70
383
|
client = OAuth2Client(
|
|
71
384
|
name=config.idp_name,
|
|
72
|
-
client_id=config.client_id,
|
|
73
|
-
client_secret=config.client_secret,
|
|
74
|
-
server_metadata_url=config.oidc_config_url,
|
|
75
|
-
client_kwargs=
|
|
385
|
+
client_id=config.client_id, # RFC 6749 §2.2
|
|
386
|
+
client_secret=config.client_secret, # RFC 6749 §2.3.1
|
|
387
|
+
server_metadata_url=config.oidc_config_url, # OIDC Discovery §4
|
|
388
|
+
client_kwargs=client_kwargs,
|
|
76
389
|
display_name=config.idp_display_name,
|
|
77
390
|
allow_sign_up=config.allow_sign_up,
|
|
78
391
|
auto_login=config.auto_login,
|
|
392
|
+
use_pkce=config.use_pkce,
|
|
393
|
+
groups_attribute_path=config.groups_attribute_path,
|
|
394
|
+
allowed_groups=config.allowed_groups,
|
|
395
|
+
role_attribute_path=config.role_attribute_path,
|
|
396
|
+
role_mapping=config.role_mapping,
|
|
397
|
+
role_attribute_strict=config.role_attribute_strict,
|
|
79
398
|
)
|
|
399
|
+
|
|
80
400
|
if config.auto_login:
|
|
81
401
|
if self._auto_login_client:
|
|
82
402
|
raise ValueError("only one auto-login client is allowed")
|
|
83
403
|
self._auto_login_client = client
|
|
84
404
|
self._clients[config.idp_name] = client
|
|
85
405
|
|
|
86
|
-
def get_client(self, idp_name: str) -> OAuth2Client:
|
|
87
|
-
|
|
88
|
-
raise ValueError(f"unknown or unregistered OAuth2 client: {idp_name}")
|
|
89
|
-
return client
|
|
406
|
+
def get_client(self, idp_name: str) -> Optional[OAuth2Client]:
|
|
407
|
+
return self._clients.get(idp_name)
|
|
90
408
|
|
|
91
409
|
@classmethod
|
|
92
410
|
def from_configs(cls, configs: Iterable[OAuth2ClientConfig]) -> "OAuth2Clients":
|
phoenix/server/prometheus.py
CHANGED
|
@@ -9,6 +9,7 @@ import psutil
|
|
|
9
9
|
from prometheus_client import (
|
|
10
10
|
Counter,
|
|
11
11
|
Gauge,
|
|
12
|
+
Histogram,
|
|
12
13
|
Summary,
|
|
13
14
|
start_http_server,
|
|
14
15
|
)
|
|
@@ -36,14 +37,19 @@ CPU_METRIC = Gauge(
|
|
|
36
37
|
name="cpu_usage_percent",
|
|
37
38
|
documentation="CPU usage percent",
|
|
38
39
|
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
BULK_LOADER_SPAN_INSERTION_TIME = Histogram(
|
|
41
|
+
namespace="phoenix",
|
|
42
|
+
name="bulk_loader_span_insertion_time_seconds",
|
|
43
|
+
documentation="Histogram of span database insertion time (seconds)",
|
|
44
|
+
buckets=[0.5, 1.0, 2.0, 5.0, 10.0, 30.0, 60.0, 180.0], # 500ms to 3min
|
|
42
45
|
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
|
|
47
|
+
BULK_LOADER_SPAN_EXCEPTIONS = Counter(
|
|
48
|
+
namespace="phoenix",
|
|
49
|
+
name="bulk_loader_span_exceptions_total",
|
|
50
|
+
documentation="Total count of span insertion exceptions",
|
|
46
51
|
)
|
|
52
|
+
|
|
47
53
|
BULK_LOADER_EVALUATION_INSERTIONS = Counter(
|
|
48
54
|
name="bulk_loader_evaluation_insertions_total",
|
|
49
55
|
documentation="Total count of bulk loader evaluation insertions",
|
|
@@ -73,6 +79,59 @@ JWT_STORE_API_KEYS_ACTIVE = Gauge(
|
|
|
73
79
|
documentation="Current number of API keys in the JWT store",
|
|
74
80
|
)
|
|
75
81
|
|
|
82
|
+
DB_DISK_USAGE_BYTES = Gauge(
|
|
83
|
+
name="database_disk_usage_bytes",
|
|
84
|
+
documentation="Current database disk usage in bytes",
|
|
85
|
+
)
|
|
86
|
+
DB_DISK_USAGE_RATIO = Gauge(
|
|
87
|
+
name="database_disk_usage_ratio",
|
|
88
|
+
documentation="Current database disk usage as ratio of allocated capacity (0-1)",
|
|
89
|
+
)
|
|
90
|
+
DB_INSERTIONS_BLOCKED = Gauge(
|
|
91
|
+
name="database_insertions_blocked",
|
|
92
|
+
documentation="Whether database insertions are currently blocked due to disk usage "
|
|
93
|
+
"(1 = blocked, 0 = not blocked)",
|
|
94
|
+
)
|
|
95
|
+
DB_DISK_USAGE_WARNING_EMAILS_SENT = Counter(
|
|
96
|
+
name="database_disk_usage_warning_emails_sent_total",
|
|
97
|
+
documentation="Total count of database disk usage warning emails sent",
|
|
98
|
+
)
|
|
99
|
+
DB_DISK_USAGE_WARNING_EMAIL_ERRORS = Counter(
|
|
100
|
+
name="database_disk_usage_warning_email_errors_total",
|
|
101
|
+
documentation="Total count of database disk usage warning email send errors",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
SPAN_QUEUE_REJECTIONS = Counter(
|
|
105
|
+
namespace="phoenix",
|
|
106
|
+
name="span_queue_rejections_total",
|
|
107
|
+
documentation="Total count of requests rejected due to span queue being full",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
SPAN_QUEUE_SIZE = Gauge(
|
|
111
|
+
namespace="phoenix",
|
|
112
|
+
name="span_queue_size",
|
|
113
|
+
documentation="Current number of spans in the processing queue",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
BULK_LOADER_LAST_ACTIVITY = Gauge(
|
|
117
|
+
namespace="phoenix",
|
|
118
|
+
name="bulk_loader_last_activity_timestamp_seconds",
|
|
119
|
+
documentation="Unix timestamp when bulk loader last processed items",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
RETENTION_SWEEPER_LAST_RUN = Gauge(
|
|
123
|
+
namespace="phoenix",
|
|
124
|
+
name="retention_sweeper_last_run_seconds",
|
|
125
|
+
documentation="Unix timestamp (seconds since epoch) of the last retention sweeper run",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
RETENTION_POLICY_EXECUTIONS = Counter(
|
|
129
|
+
namespace="phoenix",
|
|
130
|
+
name="retention_policy_executions_total",
|
|
131
|
+
documentation="Total number of retention policy executions",
|
|
132
|
+
labelnames=["status"],
|
|
133
|
+
)
|
|
134
|
+
|
|
76
135
|
|
|
77
136
|
class PrometheusMiddleware(BaseHTTPMiddleware):
|
|
78
137
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
@@ -210,6 +269,7 @@ def estimate_cpu_usage_percent() -> Optional[float]:
|
|
|
210
269
|
except Exception:
|
|
211
270
|
pass
|
|
212
271
|
return psutil.cpu_percent(interval=None)
|
|
272
|
+
return None
|
|
213
273
|
|
|
214
274
|
|
|
215
275
|
@lru_cache(maxsize=1)
|
phoenix/server/rate_limiters.py
CHANGED
|
@@ -3,12 +3,7 @@ import time
|
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from collections.abc import Callable, Coroutine
|
|
5
5
|
from functools import partial
|
|
6
|
-
from typing import
|
|
7
|
-
Any,
|
|
8
|
-
Optional,
|
|
9
|
-
Pattern, # import from re module when we drop support for 3.8
|
|
10
|
-
Union,
|
|
11
|
-
)
|
|
6
|
+
from typing import Any, Iterable
|
|
12
7
|
|
|
13
8
|
from fastapi import HTTPException, Request
|
|
14
9
|
|
|
@@ -129,7 +124,7 @@ class ServerRateLimiter:
|
|
|
129
124
|
def _fetch_token_bucket(self, key: str, request_time: float) -> TokenBucket:
|
|
130
125
|
current_partition_index = self._current_partition_index(request_time)
|
|
131
126
|
active_indices = self._active_partition_indices(current_partition_index)
|
|
132
|
-
bucket:
|
|
127
|
+
bucket: TokenBucket | None = None
|
|
133
128
|
for ii in active_indices:
|
|
134
129
|
partition = self.cache_partitions[ii]
|
|
135
130
|
if key in partition:
|
|
@@ -153,7 +148,7 @@ class ServerRateLimiter:
|
|
|
153
148
|
|
|
154
149
|
|
|
155
150
|
def fastapi_ip_rate_limiter(
|
|
156
|
-
rate_limiter: ServerRateLimiter, paths:
|
|
151
|
+
rate_limiter: ServerRateLimiter, paths: Iterable[str | re.Pattern[str]] | None = None
|
|
157
152
|
) -> Callable[[Request], Coroutine[Any, Any, Request]]:
|
|
158
153
|
async def dependency(request: Request) -> Request:
|
|
159
154
|
if paths is None or any(path_match(request.url.path, path) for path in paths):
|
|
@@ -182,7 +177,7 @@ def fastapi_route_rate_limiter(
|
|
|
182
177
|
return dependency
|
|
183
178
|
|
|
184
179
|
|
|
185
|
-
def path_match(path: str, match_pattern:
|
|
180
|
+
def path_match(path: str, match_pattern: str | re.Pattern[str]) -> bool:
|
|
186
181
|
if isinstance(match_pattern, re.Pattern):
|
|
187
182
|
return bool(match_pattern.match(path))
|
|
188
183
|
return path == match_pattern
|