arize-phoenix 8.28.1__py3-none-any.whl → 8.30.0__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.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arize-phoenix
3
- Version: 8.28.1
3
+ Version: 8.30.0
4
4
  Summary: AI Observability and Evaluation
5
5
  Project-URL: Documentation, https://docs.arize.com/phoenix/
6
6
  Project-URL: Issues, https://github.com/Arize-ai/phoenix/issues
@@ -1,12 +1,12 @@
1
1
  phoenix/__init__.py,sha256=X3eUEwd2rG8KKWWYVNNDJoqo08ihfjgHhlP29dcdNJE,5481
2
2
  phoenix/auth.py,sha256=VVMHrWN31tln3Zo4z6ofecrV4daiqJjLd8r85mqlxek,10939
3
- phoenix/config.py,sha256=NrZKYKtUFyQetptVpcgtiQwky8fx3ZOBTUz3--ln0p4,40049
3
+ phoenix/config.py,sha256=p6uh69Tk0t1orLqrWrSoQ42xt6ceJyuR2BYSBYMlme0,51117
4
4
  phoenix/datetime_utils.py,sha256=iJzNG6YJ6V7_u8B2iA7P2Z26FyxYbOPtx0dhJ7kNDHA,3398
5
5
  phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
6
6
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
7
  phoenix/services.py,sha256=ngkyKGVatX3cO2WJdo2hKdaVKP-xJCMvqthvga6kJss,5196
8
8
  phoenix/settings.py,sha256=x87BX7hWGQQZbrW_vrYqFR_izCGfO9gFc--JXUG4Tdk,754
9
- phoenix/version.py,sha256=gsPW9piraEXxZqmMeTmM62F-5-fa2a-qU5TOrQPD4Gs,23
9
+ phoenix/version.py,sha256=638ZRUG4tJUhIoPEqobGe8ch91ac1hVzQMqfk59uxtY,23
10
10
  phoenix/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  phoenix/core/embedding_dimension.py,sha256=zKGbcvwOXgLf-yrJBpQyKtd-LEOPRKHnUToyAU8Owis,87
12
12
  phoenix/core/model.py,sha256=qBFraOtmwCCnWJltKNP18DDG0mULXigytlFsa6YOz6k,4837
@@ -81,12 +81,13 @@ phoenix/pointcloud/projectors.py,sha256=TQgwc9cJDjJkin1WZyZzgl3HsYrLLiyWD7Czy4jN
81
81
  phoenix/pointcloud/umap_parameters.py,sha256=db_WEPoamuWtopZx7tQfAXPnoE0MS8FkAV0_ThjEx_Q,1735
82
82
  phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
83
  phoenix/server/app.py,sha256=1lPHzZwWlTpwzEaO6jmgO18Ib4ssBxoLO9fwbfT_ois,39053
84
+ phoenix/server/authorization.py,sha256=fofeRwuoodCUB3DQYPCuAgIyeiwopXW0tkH_KZvU0Rg,1848
84
85
  phoenix/server/bearer_auth.py,sha256=9AY0-aOSHm-B7OYB-20jFA7bETAA6S1_W1za42A8Yt8,6804
85
86
  phoenix/server/dml_event.py,sha256=MjJmVEKytq75chBOSyvYDusUnEbg1pHpIjR3pZkUaJA,2838
86
87
  phoenix/server/dml_event_handler.py,sha256=EZLXmCvx4pJrCkz29gxwKwmvmUkTtPCHw6klR-XM8qE,8258
87
- phoenix/server/grpc_server.py,sha256=SknR-iLIUqU9swiAyLhbCUREA1obOM6w49xgdK1AQDs,3839
88
+ phoenix/server/grpc_server.py,sha256=dod29zE_Zlir7NyLcdVM8GH3P8sy-9ykzfaBfVifyE4,4656
88
89
  phoenix/server/jwt_store.py,sha256=asxzY4_ZBM2FWAMstHvhvnKUP_0AA3v3xPTL2IOgNqY,16831
89
- phoenix/server/main.py,sha256=EuaVqpdrSgtszLK7ov_KkMMQbY2bDV_JTSf73olLKow,16374
90
+ phoenix/server/main.py,sha256=Twpig08FSHIwICMr-2qGNLNoCrgk6EjyHrrxiBwS_VU,18029
90
91
  phoenix/server/oauth2.py,sha256=EV4wcCwG0N7cJRcfGNURdP5rZgRVCeRDvXyle19A27Y,2064
91
92
  phoenix/server/prometheus.py,sha256=1KjvSfjSa2-BPjDybVMM_Kag316CsN-Zwt64YNr_snc,7825
92
93
  phoenix/server/rate_limiters.py,sha256=cFc73D2NaxqNZZDbwfIDw4So-fRVOJPBtqxOZ8Qky_s,7155
@@ -146,7 +147,7 @@ phoenix/server/api/helpers/dataset_helpers.py,sha256=DoMBTg-qXTnC_K4Evx1WKpCCYgR
146
147
  phoenix/server/api/helpers/experiment_run_filters.py,sha256=DOnVwrmn39eAkk2mwuZP8kIcAnR5jrOgllEwWSjsw94,29893
147
148
  phoenix/server/api/helpers/playground_clients.py,sha256=tHwW-HbnoxU-EXXVvmY_1lbYh3Mv7QKw-e9KnREc7m4,41814
148
149
  phoenix/server/api/helpers/playground_registry.py,sha256=CPLMziFB2wmr-dfbx7VbzO2f8YIG_k5RftzvGXYGQ1w,2570
149
- phoenix/server/api/helpers/playground_spans.py,sha256=PjGNDc7cpqn5lmRM6TO_J1eVRGlgsNdQ8IT--5JVz0o,16881
150
+ phoenix/server/api/helpers/playground_spans.py,sha256=ObAhvV_yNwEQDkjzgU5G73wfIisc8q4cpB0OFH5cd24,16974
150
151
  phoenix/server/api/helpers/prompts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
151
152
  phoenix/server/api/helpers/prompts/models.py,sha256=zwWBA5GQvEHT4xyZwP-_kr1rJZ7SHnguATsQwsFw5aw,19519
152
153
  phoenix/server/api/helpers/prompts/conversions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -318,10 +319,10 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
318
319
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
319
320
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
320
321
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
321
- phoenix/server/static/.vite/manifest.json,sha256=6QTURnF280ofNocGEl8gB6sl5MV89eY-ud75tIEL_9s,2165
322
- phoenix/server/static/assets/components-DepxScNF.js,sha256=carDtPUQBqqHy_r-R915p_M-9nTJg-AL2Wbc2dAK3GQ,456419
323
- phoenix/server/static/assets/index-DUj7q3Q4.js,sha256=c5CoeDmjEu__Js2Je0WtUH-_94J2_IOARtC8tF37Kf4,60460
324
- phoenix/server/static/assets/pages-CJUakDKi.js,sha256=7z0F3KAcyUcdXrMrBorbSmOYJDlG7olc3hrATdEfnJA,864692
322
+ phoenix/server/static/.vite/manifest.json,sha256=LT4zk-ACfPJOdvtfdMat3RKSRZf8dq9HnPMdrnLmLsU,2165
323
+ phoenix/server/static/assets/components-BypB0fGT.js,sha256=AYnKnOwh7u_OwervE0AmQ7myFDgYIiwCyjrfoL8OAJg,456419
324
+ phoenix/server/static/assets/index-Ccebc6-h.js,sha256=lHdYZrNRbUI4kVqQcji9pKK9ofB_y0v8D7f73NJX7gI,60460
325
+ phoenix/server/static/assets/pages-DGdjQlsy.js,sha256=AaD6DdUBqgt7rAHyo-aiewoNp5CWNKFQ5NkeJL0Cdnc,864937
325
326
  phoenix/server/static/assets/vendor-BfhM_F1u.js,sha256=S90L1KRZOw_NX6C9FENfLs6bSuEzf7zVDAqjZAZJZgE,2514280
326
327
  phoenix/server/static/assets/vendor-Cg6lcjUC.css,sha256=nZrkr0u6NNElFGvpWHk9GTHeGoibCXCli1bE7mXZGZg,1816
327
328
  phoenix/server/static/assets/vendor-arizeai-CxXYQNUl.js,sha256=V0umBNhH2psKhzwNKvLylYrqVfHu4I6NDSqejmR2OIU,193248
@@ -332,7 +333,7 @@ phoenix/server/static/assets/vendor-three-C5WAXd5r.js,sha256=ELkg06u70N7h8oFmvqd
332
333
  phoenix/server/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
333
334
  phoenix/server/templates/index.html,sha256=e8_jdi7Eo19SK7DI_gglkTW094D17E0VAegoMmmmvIc,4330
334
335
  phoenix/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
335
- phoenix/session/client.py,sha256=nXSn2Zmf9wTxgSe11Y9kKSLClkG8pNEAJgfEmy4mPm8,35234
336
+ phoenix/session/client.py,sha256=51UCIZ_k4Wd4b8f4Yi888MOie6nhF8E_5in41eBTUVU,35234
336
337
  phoenix/session/data_extractor.py,sha256=Y0RzYFaNy9fQj8PEIeQ76TBZ90_E1FW7bXu3K5x0EZY,2782
337
338
  phoenix/session/evaluation.py,sha256=Q3fOMNELvqkk-b6a6PKc8pDJdsNQ0ZbTpseUSA2NKqs,5300
338
339
  phoenix/session/session.py,sha256=twE8tld8knShmTpZp6R80CUt2jau3RmfdfZAQCLSTKU,27621
@@ -368,9 +369,9 @@ phoenix/utilities/project.py,sha256=auVpARXkDb-JgeX5f2aStyFIkeKvGwN9l7qrFeJMVxI,
368
369
  phoenix/utilities/re.py,sha256=6YyUWIkv0zc2SigsxfOWIHzdpjKA_TZo2iqKq7zJKvw,2081
369
370
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
370
371
  phoenix/utilities/template_formatters.py,sha256=gh9PJD6WEGw7TEYXfSst1UR4pWWwmjxMLrDVQ_CkpkQ,2779
371
- arize_phoenix-8.28.1.dist-info/METADATA,sha256=piSQBdxjMSPCEXL3HRfFaSsgivBPxTHLu4yeSq-FxB8,24478
372
- arize_phoenix-8.28.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
373
- arize_phoenix-8.28.1.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
374
- arize_phoenix-8.28.1.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
375
- arize_phoenix-8.28.1.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
376
- arize_phoenix-8.28.1.dist-info/RECORD,,
372
+ arize_phoenix-8.30.0.dist-info/METADATA,sha256=5Q2XR0ZPn6hBnWkTNeQ5Ajdr4paRK-agq2gBwWccXHA,24478
373
+ arize_phoenix-8.30.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
374
+ arize_phoenix-8.30.0.dist-info/entry_points.txt,sha256=Pgpn8Upxx9P8z8joPXZWl2LlnAlGc3gcQoVchb06X1Q,94
375
+ arize_phoenix-8.30.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
376
+ arize_phoenix-8.30.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
377
+ arize_phoenix-8.30.0.dist-info/RECORD,,
phoenix/config.py CHANGED
@@ -2,7 +2,7 @@ import logging
2
2
  import os
3
3
  import re
4
4
  import tempfile
5
- from dataclasses import dataclass
5
+ from dataclasses import dataclass, field
6
6
  from datetime import timedelta
7
7
  from enum import Enum
8
8
  from importlib.metadata import version
@@ -240,6 +240,268 @@ ENV_PHOENIX_FASTAPI_MIDDLEWARE_PATHS = "PHOENIX_FASTAPI_MIDDLEWARE_PATHS"
240
240
  ENV_PHOENIX_GQL_EXTENSION_PATHS = "PHOENIX_GQL_EXTENSION_PATHS"
241
241
  ENV_PHOENIX_GRPC_INTERCEPTOR_PATHS = "PHOENIX_GRPC_INTERCEPTOR_PATHS"
242
242
 
243
+ ENV_PHOENIX_TLS_ENABLED = "PHOENIX_TLS_ENABLED"
244
+ """
245
+ Whether to enable TLS for Phoenix HTTP and gRPC servers.
246
+ """
247
+ ENV_PHOENIX_TLS_ENABLED_FOR_HTTP = "PHOENIX_TLS_ENABLED_FOR_HTTP"
248
+ """
249
+ Whether to enable TLS for Phoenix HTTP server. Overrides PHOENIX_TLS_ENABLED.
250
+ """
251
+ ENV_PHOENIX_TLS_ENABLED_FOR_GRPC = "PHOENIX_TLS_ENABLED_FOR_GRPC"
252
+ """
253
+ Whether to enable TLS for Phoenix gRPC server. Overrides PHOENIX_TLS_ENABLED.
254
+ """
255
+ ENV_PHOENIX_TLS_CERT_FILE = "PHOENIX_TLS_CERT_FILE"
256
+ """
257
+ Path to the TLS certificate file for HTTPS connections.
258
+ When set, Phoenix will use HTTPS instead of HTTP for all connections.
259
+ """
260
+ ENV_PHOENIX_TLS_KEY_FILE = "PHOENIX_TLS_KEY_FILE"
261
+ """
262
+ Path to the TLS private key file for HTTPS connections.
263
+ Required when PHOENIX_TLS_CERT_FILE is set.
264
+ """
265
+ ENV_PHOENIX_TLS_KEY_FILE_PASSWORD = "PHOENIX_TLS_KEY_FILE_PASSWORD"
266
+ """
267
+ Password for the TLS private key file if it's encrypted.
268
+ Only needed if the private key file requires a password.
269
+ """
270
+ ENV_PHOENIX_TLS_CA_FILE = "PHOENIX_TLS_CA_FILE"
271
+ """
272
+ Path to the Certificate Authority (CA) file for client certificate verification.
273
+ Used when PHOENIX_TLS_VERIFY_CLIENT is set to true.
274
+ """
275
+ ENV_PHOENIX_TLS_VERIFY_CLIENT = "PHOENIX_TLS_VERIFY_CLIENT"
276
+ """
277
+ Whether to verify client certificates for mutual TLS (mTLS) authentication.
278
+ When set to true, clients must provide valid certificates signed by the CA specified in
279
+ PHOENIX_TLS_CA_FILE.
280
+ """
281
+
282
+
283
+ @dataclass(frozen=True)
284
+ class TLSConfig:
285
+ """Configuration for TLS (Transport Layer Security) connections.
286
+
287
+ This class manages TLS certificates and private keys for secure connections.
288
+ It handles reading certificate and key files, and decrypting private keys
289
+ if they are password-protected.
290
+
291
+ Attributes:
292
+ cert_file: Path to the TLS certificate file
293
+ key_file: Path to the TLS private key file
294
+ key_file_password: Optional password for decrypting the private key
295
+ _cert_data: Cached certificate data (internal use)
296
+ _key_data: Cached decrypted key data (internal use)
297
+ _decrypted_key_data: Cached decrypted key data (internal use)
298
+ """
299
+
300
+ cert_file: Path
301
+ key_file: Path
302
+ key_file_password: Optional[str]
303
+ _cert_data: bytes = field(default=b"", init=False, repr=False)
304
+ _key_data: bytes = field(default=b"", init=False, repr=False)
305
+ _decrypted_key_data: Optional[bytes] = field(default=None, init=False, repr=False)
306
+
307
+ @property
308
+ def cert_data(self) -> bytes:
309
+ """Get the certificate data, reading from file if not cached.
310
+
311
+ Returns:
312
+ bytes: The certificate data in PEM format
313
+ """
314
+ if not self._cert_data:
315
+ with open(self.cert_file, "rb") as f:
316
+ object.__setattr__(self, "_cert_data", f.read())
317
+ return self._cert_data
318
+
319
+ @property
320
+ def key_data(self) -> bytes:
321
+ """Get the decrypted key data, reading from file if not cached.
322
+
323
+ This property reads the private key file and decrypts it if a password
324
+ is provided. The decrypted key is cached for subsequent accesses.
325
+
326
+ Returns:
327
+ bytes: The decrypted private key data in PEM format
328
+
329
+ Raises:
330
+ ValueError: If the cryptography library is not installed or if
331
+ decryption fails
332
+ """
333
+ if not self._key_data:
334
+ self._read_and_cache_key_data()
335
+ return self._key_data
336
+
337
+ def _read_and_cache_key_data(self) -> None:
338
+ """Read and decrypt the private key file, then cache the result.
339
+
340
+ This method reads the private key file, decrypts it if a password
341
+ is provided, and stores the decrypted key in the _key_data attribute.
342
+
343
+ Raises:
344
+ ValueError: If the cryptography library is not installed or if
345
+ decryption fails
346
+ """
347
+ try:
348
+ from cryptography.hazmat.backends import default_backend
349
+ from cryptography.hazmat.primitives.serialization import (
350
+ Encoding,
351
+ NoEncryption,
352
+ PrivateFormat,
353
+ load_pem_private_key,
354
+ )
355
+ except ImportError:
356
+ raise ValueError(
357
+ "The cryptography library is needed to read private keys for "
358
+ "TLS configuration. Please install it with: pip install cryptography"
359
+ )
360
+
361
+ # First read the key file
362
+ with open(self.key_file, "rb") as f:
363
+ key_data = f.read()
364
+
365
+ try:
366
+ # Convert password to bytes if it exists
367
+ password_bytes = self.key_file_password.encode() if self.key_file_password else None
368
+
369
+ # Load the key (decrypting if password is provided)
370
+ private_key = load_pem_private_key(
371
+ key_data,
372
+ password=password_bytes,
373
+ backend=default_backend(),
374
+ )
375
+
376
+ # Convert to PEM format without encryption
377
+ decrypted_pem = private_key.private_bytes(
378
+ encoding=Encoding.PEM,
379
+ format=PrivateFormat.PKCS8,
380
+ encryption_algorithm=NoEncryption(),
381
+ )
382
+ except Exception as e:
383
+ raise ValueError(f"Failed to decrypt private key: {e}")
384
+ object.__setattr__(self, "_key_data", decrypted_pem)
385
+
386
+
387
+ @dataclass(frozen=True)
388
+ class TLSConfigVerifyClient(TLSConfig):
389
+ """TLS configuration with client verification enabled."""
390
+
391
+ ca_file: Path
392
+ _ca_data: bytes = field(default=b"", init=False, repr=False)
393
+
394
+ @property
395
+ def ca_data(self) -> bytes:
396
+ """Get the CA certificate data, reading from file if not cached."""
397
+ if not self._ca_data:
398
+ with open(self.ca_file, "rb") as f:
399
+ object.__setattr__(self, "_ca_data", f.read())
400
+ return self._ca_data
401
+
402
+
403
+ def get_env_tls_enabled_for_http() -> bool:
404
+ """
405
+ Gets whether TLS is enabled for the HTTP server.
406
+
407
+ This function checks both PHOENIX_TLS_ENABLED_FOR_HTTP and PHOENIX_TLS_ENABLED environment variables.
408
+ If PHOENIX_TLS_ENABLED_FOR_HTTP is set, it takes precedence over PHOENIX_TLS_ENABLED.
409
+
410
+ Returns:
411
+ bool: True if TLS is enabled for HTTP server, False otherwise. Defaults to False if neither
412
+ environment variable is set.
413
+ """ # noqa: E501
414
+ return _bool_val(ENV_PHOENIX_TLS_ENABLED_FOR_HTTP, _bool_val(ENV_PHOENIX_TLS_ENABLED, False))
415
+
416
+
417
+ def get_env_tls_enabled_for_grpc() -> bool:
418
+ """
419
+ Gets whether TLS is enabled for the gRPC server.
420
+
421
+ This function checks both PHOENIX_TLS_ENABLED_FOR_GRPC and PHOENIX_TLS_ENABLED environment variables.
422
+ If PHOENIX_TLS_ENABLED_FOR_GRPC is set, it takes precedence over PHOENIX_TLS_ENABLED.
423
+
424
+ Returns:
425
+ bool: True if TLS is enabled for gRPC server, False otherwise. Defaults to False if neither
426
+ environment variable is set.
427
+ """ # noqa: E501
428
+ return _bool_val(ENV_PHOENIX_TLS_ENABLED_FOR_GRPC, _bool_val(ENV_PHOENIX_TLS_ENABLED, False))
429
+
430
+
431
+ def get_env_tls_verify_client() -> bool:
432
+ """
433
+ Gets the value of the PHOENIX_TLS_VERIFY_CLIENT environment variable.
434
+
435
+ Returns:
436
+ bool: True if client certificate verification is enabled, False otherwise. Defaults to False
437
+ if the environment variable is not set.
438
+ """ # noqa: E501
439
+ return _bool_val(ENV_PHOENIX_TLS_VERIFY_CLIENT, False)
440
+
441
+
442
+ def get_env_tls_config() -> Optional[TLSConfig]:
443
+ """
444
+ Retrieves and validates TLS configuration from environment variables.
445
+
446
+ Returns:
447
+ Optional[TLSConfig]: A configuration object containing TLS settings, or None if TLS is disabled.
448
+ If client verification is enabled, returns TLSConfigVerifyClient instead.
449
+
450
+ The function reads the following environment variables:
451
+ - PHOENIX_TLS_ENABLED: Whether TLS is enabled (defaults to False)
452
+ - PHOENIX_TLS_CERT_FILE: Path to the TLS certificate file
453
+ - PHOENIX_TLS_KEY_FILE: Path to the TLS private key file
454
+ - PHOENIX_TLS_KEY_FILE_PASSWORD: Password for the TLS private key file
455
+ - PHOENIX_TLS_CA_FILE: Path to the Certificate Authority file (required for client verification)
456
+ - PHOENIX_TLS_VERIFY_CLIENT: Whether to verify client certificates
457
+
458
+ Raises:
459
+ ValueError: If required files are missing or don't exist when TLS is enabled
460
+ """ # noqa: E501
461
+ # Check if TLS is enabled
462
+ if not (get_env_tls_enabled_for_http() or get_env_tls_enabled_for_grpc()):
463
+ return None
464
+
465
+ # Get certificate file path if specified
466
+ if not (cert_file_str := getenv(ENV_PHOENIX_TLS_CERT_FILE)):
467
+ raise ValueError("PHOENIX_TLS_CERT_FILE must be set when PHOENIX_TLS_ENABLED is true")
468
+ cert_file = Path(cert_file_str)
469
+
470
+ # Get private key file path if specified
471
+ if not (key_file_str := getenv(ENV_PHOENIX_TLS_KEY_FILE)):
472
+ raise ValueError("PHOENIX_TLS_KEY_FILE must be set when PHOENIX_TLS_ENABLED is true")
473
+ key_file = Path(key_file_str)
474
+
475
+ # Get private key password if specified
476
+ key_file_password = getenv(ENV_PHOENIX_TLS_KEY_FILE_PASSWORD)
477
+
478
+ # Validate certificate and key files
479
+ _validate_file_exists_and_is_readable(cert_file, "certificate")
480
+ _validate_file_exists_and_is_readable(key_file, "key")
481
+
482
+ # If client verification is enabled, validate CA file and return TLSConfigVerifyClient
483
+ if get_env_tls_verify_client():
484
+ if not (ca_file_str := getenv(ENV_PHOENIX_TLS_CA_FILE)):
485
+ raise ValueError(
486
+ "PHOENIX_TLS_CA_FILE must be set when PHOENIX_TLS_VERIFY_CLIENT is true"
487
+ )
488
+
489
+ ca_file = Path(ca_file_str)
490
+ _validate_file_exists_and_is_readable(ca_file, "CA")
491
+
492
+ return TLSConfigVerifyClient(
493
+ cert_file=cert_file,
494
+ key_file=key_file,
495
+ key_file_password=key_file_password,
496
+ ca_file=ca_file,
497
+ )
498
+
499
+ return TLSConfig(
500
+ cert_file=cert_file,
501
+ key_file=key_file,
502
+ key_file_password=key_file_password,
503
+ )
504
+
243
505
 
244
506
  def server_instrumentation_is_enabled() -> bool:
245
507
  return bool(
@@ -956,7 +1218,8 @@ def get_env_root_url() -> URL:
956
1218
  host = get_env_host()
957
1219
  if host == "0.0.0.0":
958
1220
  host = "127.0.0.1"
959
- return URL(urljoin(f"http://{host}:{get_env_port()}", get_env_host_root_path()))
1221
+ scheme = "https" if get_env_tls_enabled_for_http() else "http"
1222
+ return URL(urljoin(f"{scheme}://{host}:{get_env_port()}", get_env_host_root_path()))
960
1223
 
961
1224
 
962
1225
  def get_base_url() -> str:
@@ -964,7 +1227,8 @@ def get_base_url() -> str:
964
1227
  host = get_env_host()
965
1228
  if host == "0.0.0.0":
966
1229
  host = "127.0.0.1"
967
- base_url = get_env_collector_endpoint() or f"http://{host}:{get_env_port()}"
1230
+ scheme = "https" if get_env_tls_enabled_for_http() else "http"
1231
+ base_url = get_env_collector_endpoint() or f"{scheme}://{host}:{get_env_port()}"
968
1232
  return base_url if base_url.endswith("/") else base_url + "/"
969
1233
 
970
1234
 
@@ -1159,3 +1423,30 @@ When the PHOENIX_ADMIN_SECRET is used as a bearer token in API requests, the
1159
1423
  request is authenticated as the system user with the user_id set to this
1160
1424
  SYSTEM_USER_ID value (only if this variable is not None).
1161
1425
  """
1426
+
1427
+
1428
+ def _validate_file_exists_and_is_readable(
1429
+ file_path: Path,
1430
+ description: str,
1431
+ check_non_empty: bool = True,
1432
+ ) -> None:
1433
+ """
1434
+ Validate that a file exists, is readable, and optionally has non-zero size.
1435
+
1436
+ Args:
1437
+ file_path: Path to the file to validate
1438
+ description: Description of the file for error messages (e.g., "certificate", "key", "CA")
1439
+ check_non_empty: Whether to check if the file has non-zero size. Defaults to True.
1440
+
1441
+ Raises:
1442
+ ValueError: If the path is not a file, isn't readable, or has zero size (if check_non_empty is True)
1443
+ """ # noqa: E501
1444
+ if not file_path.is_file():
1445
+ raise ValueError(f"{description} path is not a file: {file_path}")
1446
+ if check_non_empty and file_path.stat().st_size == 0:
1447
+ raise ValueError(f"{description} file is empty: {file_path}")
1448
+ try:
1449
+ with open(file_path, "rb") as f:
1450
+ f.read(1) # Read just one byte to verify readability
1451
+ except Exception as e:
1452
+ raise ValueError(f"{description} file is not readable: {e}")
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import logging
2
3
  from collections import defaultdict
3
4
  from collections.abc import Mapping
4
5
  from dataclasses import asdict
@@ -54,6 +55,8 @@ ChatCompletionMessage: TypeAlias = tuple[
54
55
  ]
55
56
  ToolCallID: TypeAlias = str
56
57
 
58
+ logger = logging.getLogger(__name__)
59
+
57
60
 
58
61
  class streaming_llm_span:
59
62
  """
@@ -117,6 +120,7 @@ class streaming_llm_span:
117
120
  exception_stacktrace=format_exc(),
118
121
  )
119
122
  )
123
+ logger.exception(exc_value)
120
124
  if self._text_chunks or self._tool_call_chunks:
121
125
  self._attributes.update(
122
126
  chain(
@@ -0,0 +1,53 @@
1
+ """
2
+ Authorization dependencies for FastAPI routes.
3
+
4
+ Usage:
5
+ Use the provided dependencies (e.g., require_admin) with FastAPI's Depends to restrict access to
6
+ certain routes.
7
+
8
+ These dependencies will raise HTTP 403 if the user is not authorized.
9
+
10
+ Example:
11
+
12
+ from fastapi import APIRouter, Depends
13
+ from phoenix.server.authorization import require_admin
14
+
15
+ router = APIRouter()
16
+
17
+ @router.post("/dangerous-thing", dependencies=[Depends(require_admin)])
18
+ async def dangerous_thing(...):
19
+ ...
20
+
21
+ The require_admin dependency allows only admin or system users to access the route.
22
+ It expects authentication to be enabled and the request.user to be set by the authentication.
23
+ """
24
+
25
+ from fastapi import HTTPException, Request
26
+ from fastapi import status as fastapi_status
27
+
28
+ from phoenix.server.bearer_auth import PhoenixUser
29
+
30
+
31
+ def require_admin(request: Request) -> None:
32
+ """
33
+ FastAPI dependency to restrict access to admin or system users only.
34
+
35
+ Usage:
36
+ Add as a dependency to any route that should only be accessible by admin or system users:
37
+
38
+ @router.post("/dangerous-thing", dependencies=[Depends(require_admin)])
39
+ async def dangerous_thing(...):
40
+ ...
41
+
42
+ Behavior:
43
+ - Allows access if the authenticated user is an admin or a system user.
44
+ - Raises HTTP 403 Forbidden if the user is not authorized.
45
+ - Expects authentication to be enabled and request.user to be set by the authentication.
46
+ """
47
+ user = getattr(request, "user", None)
48
+ # System users have all privileges
49
+ if not (isinstance(user, PhoenixUser) and user.is_admin):
50
+ raise HTTPException(
51
+ status_code=fastapi_status.HTTP_403_FORBIDDEN,
52
+ detail="Only admin or system users can perform this action.",
53
+ )
@@ -14,7 +14,12 @@ from opentelemetry.proto.collector.trace.v1.trace_service_pb2_grpc import (
14
14
  from typing_extensions import TypeAlias
15
15
 
16
16
  from phoenix.auth import CanReadToken
17
- from phoenix.config import get_env_grpc_port
17
+ from phoenix.config import (
18
+ TLSConfigVerifyClient,
19
+ get_env_grpc_port,
20
+ get_env_tls_config,
21
+ get_env_tls_enabled_for_grpc,
22
+ )
18
23
  from phoenix.server.bearer_auth import ApiKeyInterceptor
19
24
  from phoenix.trace.otel import decode_otlp_span
20
25
  from phoenix.trace.schemas import Span
@@ -86,7 +91,21 @@ class GrpcServer:
86
91
  options=(("grpc.so_reuseport", 0),),
87
92
  interceptors=interceptors,
88
93
  )
89
- server.add_insecure_port(f"[::]:{get_env_grpc_port()}")
94
+ if get_env_tls_enabled_for_grpc():
95
+ assert (tls_config := get_env_tls_config())
96
+ private_key_certificate_chain_pairs = [(tls_config.key_data, tls_config.cert_data)]
97
+ server_credentials = (
98
+ grpc.ssl_server_credentials(
99
+ private_key_certificate_chain_pairs,
100
+ root_certificates=tls_config.ca_data,
101
+ require_client_auth=True,
102
+ )
103
+ if isinstance(tls_config, TLSConfigVerifyClient)
104
+ else grpc.ssl_server_credentials(private_key_certificate_chain_pairs)
105
+ )
106
+ server.add_secure_port(f"[::]:{get_env_grpc_port()}", server_credentials)
107
+ else:
108
+ server.add_insecure_port(f"[::]:{get_env_grpc_port()}")
90
109
  add_TraceServiceServicer_to_server(Servicer(self._callback), server) # type: ignore[no-untyped-call,unused-ignore]
91
110
  await server.start()
92
111
  self._server = server
phoenix/server/main.py CHANGED
@@ -4,6 +4,7 @@ import os
4
4
  import sys
5
5
  from argparse import SUPPRESS, ArgumentParser
6
6
  from pathlib import Path
7
+ from ssl import CERT_REQUIRED
7
8
  from threading import Thread
8
9
  from time import sleep, time
9
10
  from typing import Optional
@@ -15,6 +16,7 @@ from uvicorn import Config, Server
15
16
  import phoenix.trace.v1 as pb
16
17
  from phoenix.config import (
17
18
  EXPORT_DIR,
19
+ TLSConfigVerifyClient,
18
20
  get_env_access_token_expiry,
19
21
  get_env_allowed_origins,
20
22
  get_env_auth_settings,
@@ -39,6 +41,9 @@ from phoenix.config import (
39
41
  get_env_smtp_port,
40
42
  get_env_smtp_username,
41
43
  get_env_smtp_validate_certs,
44
+ get_env_tls_config,
45
+ get_env_tls_enabled_for_grpc,
46
+ get_env_tls_enabled_for_http,
42
47
  get_pids_path,
43
48
  )
44
49
  from phoenix.core.model_schema_adapter import create_model_from_inferences
@@ -98,15 +103,27 @@ _WELCOME_MESSAGE = Environment(loader=BaseLoader()).from_string("""
98
103
  | 🚀 Phoenix Server 🚀
99
104
  | Phoenix UI: {{ ui_path }}
100
105
  | Authentication: {{ auth_enabled }}
101
- | Websockets: {{ websockets_enabled }}
102
- {%- if allowed_origins %}\n| Allowed Origins: {{ allowed_origins }}{% endif %}
106
+ {%- if auth_enabled_for_http or auth_enabled_for_grpc %}
107
+ {%- if tls_enabled_for_http %}
108
+ | TLS: Enabled for HTTP
109
+ {%- endif %}
110
+ {%- if tls_enabled_for_grpc %}
111
+ | TLS: Enabled for gRPC
112
+ {%- endif %}
113
+ {%- if tls_verify_client %}
114
+ | TLS Client Verification: Enabled
115
+ {%- endif %}
116
+ {%- endif %}
117
+ {%- if allowed_origins %}
118
+ | Allowed Origins: {{ allowed_origins }}
119
+ {%- endif %}
103
120
  | Log traces:
104
121
  | - gRPC: {{ grpc_path }}
105
122
  | - HTTP: {{ http_path }}
106
123
  | Storage: {{ storage }}
107
- {% if schema -%}
124
+ {%- if schema %}
108
125
  | - Schema: {{ schema }}
109
- {% endif -%}
126
+ {%- endif %}
110
127
  """)
111
128
 
112
129
 
@@ -356,16 +373,27 @@ def main() -> None:
356
373
 
357
374
  allowed_origins = get_env_allowed_origins()
358
375
 
376
+ # Get TLS configuration
377
+ tls_enabled_for_http = get_env_tls_enabled_for_http()
378
+ tls_enabled_for_grpc = get_env_tls_enabled_for_grpc()
379
+ tls_config = get_env_tls_config()
380
+ tls_verify_client = tls_config is not None and isinstance(tls_config, TLSConfigVerifyClient)
381
+
359
382
  # Print information about the server
360
- root_path = urljoin(f"http://{host}:{port}", host_root_path)
383
+ http_scheme = "https" if tls_enabled_for_http else "http"
384
+ grpc_scheme = "https" if tls_enabled_for_grpc else "http"
385
+ root_path = urljoin(f"{http_scheme}://{host}:{port}", host_root_path)
361
386
  msg = _WELCOME_MESSAGE.render(
362
387
  version=phoenix_version,
363
388
  ui_path=root_path,
364
- grpc_path=f"http://{host}:{get_env_grpc_port()}",
389
+ grpc_path=f"{grpc_scheme}://{host}:{get_env_grpc_port()}",
365
390
  http_path=urljoin(root_path, "v1/traces"),
366
391
  storage=get_printable_db_url(db_connection_str),
367
392
  schema=get_env_database_schema(),
368
393
  auth_enabled=authentication_enabled,
394
+ tls_enabled_for_http=tls_enabled_for_http,
395
+ tls_enabled_for_grpc=tls_enabled_for_grpc,
396
+ tls_verify_client=tls_verify_client,
369
397
  allowed_origins=allowed_origins,
370
398
  )
371
399
  if sys.platform.startswith("win"):
@@ -417,7 +445,28 @@ def main() -> None:
417
445
  oauth2_client_configs=get_env_oauth2_settings(),
418
446
  allowed_origins=allowed_origins,
419
447
  )
420
- server = Server(config=Config(app, host=host, port=port, root_path=host_root_path)) # type: ignore
448
+
449
+ # Configure server with TLS if enabled
450
+ server_config = Config(
451
+ app=app,
452
+ host=host, # type: ignore[arg-type]
453
+ port=port,
454
+ root_path=host_root_path,
455
+ )
456
+
457
+ if tls_enabled_for_http:
458
+ assert tls_config
459
+ # Configure SSL context with certificate and key
460
+ server_config.ssl_keyfile = str(tls_config.key_file)
461
+ server_config.ssl_keyfile_password = tls_config.key_file_password
462
+ server_config.ssl_certfile = str(tls_config.cert_file)
463
+
464
+ # If CA file is provided and client verification is enabled
465
+ if isinstance(tls_config, TLSConfigVerifyClient):
466
+ server_config.ssl_ca_certs = str(tls_config.ca_file)
467
+ server_config.ssl_cert_reqs = CERT_REQUIRED
468
+
469
+ server = Server(config=server_config)
421
470
  Thread(target=_write_pid_file_when_ready, args=(server,), daemon=True).start()
422
471
 
423
472
  try: