arize-phoenix 4.27.0__py3-none-any.whl → 4.29.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.

Files changed (27) hide show
  1. {arize_phoenix-4.27.0.dist-info → arize_phoenix-4.29.0.dist-info}/METADATA +2 -1
  2. {arize_phoenix-4.27.0.dist-info → arize_phoenix-4.29.0.dist-info}/RECORD +26 -22
  3. phoenix/auth.py +3 -0
  4. phoenix/otel/__init__.py +22 -0
  5. phoenix/otel/otel.py +284 -0
  6. phoenix/otel/settings.py +82 -0
  7. phoenix/server/api/context.py +10 -1
  8. phoenix/server/api/dataloaders/dataset_example_revisions.py +3 -2
  9. phoenix/server/api/exceptions.py +41 -0
  10. phoenix/server/api/mutations/__init__.py +4 -2
  11. phoenix/server/api/mutations/api_key_mutations.py +29 -0
  12. phoenix/server/api/mutations/auth_mutations.py +65 -0
  13. phoenix/server/api/mutations/dataset_mutations.py +9 -8
  14. phoenix/server/api/mutations/experiment_mutations.py +2 -1
  15. phoenix/server/api/queries.py +9 -8
  16. phoenix/server/api/routers/v1/experiments.py +4 -4
  17. phoenix/server/api/schema.py +2 -0
  18. phoenix/server/app.py +0 -3
  19. phoenix/server/static/.vite/manifest.json +9 -9
  20. phoenix/server/static/assets/{components-1MfQimGx.js → components-BYH03rjA.js} +108 -100
  21. phoenix/server/static/assets/{index-B263sE2x.js → index-fqdjNpYm.js} +1 -1
  22. phoenix/server/static/assets/{pages-CqZDVx20.js → pages-DnbxgoTK.js} +266 -219
  23. phoenix/version.py +1 -1
  24. phoenix/server/api/routers/auth.py +0 -52
  25. {arize_phoenix-4.27.0.dist-info → arize_phoenix-4.29.0.dist-info}/WHEEL +0 -0
  26. {arize_phoenix-4.27.0.dist-info → arize_phoenix-4.29.0.dist-info}/licenses/IP_NOTICE +0 -0
  27. {arize_phoenix-4.27.0.dist-info → arize_phoenix-4.29.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: arize-phoenix
3
- Version: 4.27.0
3
+ Version: 4.29.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
@@ -21,6 +21,7 @@ Requires-Dist: aioitertools
21
21
  Requires-Dist: aiosqlite
22
22
  Requires-Dist: alembic<2,>=1.3.0
23
23
  Requires-Dist: arize-phoenix-evals>=0.13.1
24
+ Requires-Dist: arize-phoenix-otel>=0.4.1
24
25
  Requires-Dist: cachetools
25
26
  Requires-Dist: fastapi
26
27
  Requires-Dist: grpcio
@@ -1,12 +1,12 @@
1
1
  phoenix/__init__.py,sha256=TGNWqm2UW-l67yIRpOtmqGHVAmdoobSNqUsiTtip7uQ,1542
2
- phoenix/auth.py,sha256=N8vTFmc5BEsdX4xr6Bmh6OwBrNUQykr74LuCIkC28jA,1455
2
+ phoenix/auth.py,sha256=ugvGZlseYX9NkpWaSqb8D2kzUBlAPqT45Dx5_VUANqk,1621
3
3
  phoenix/config.py,sha256=wYA_8GSSz5rnpfIWDjeBL9ehKuTy9jqXaMZnxUqRYEU,10131
4
4
  phoenix/datetime_utils.py,sha256=yDKjwX2Vtqw9h5F_ProtP-TsXidM43uIvmJ_pOzYc9A,3405
5
5
  phoenix/exceptions.py,sha256=n2L2KKuecrdflB9MsCdAYCiSEvGJptIsfRkXMoJle7A,169
6
6
  phoenix/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
7
7
  phoenix/services.py,sha256=OyML4t2XGnlqF0JXA9_uccL8HslTABxep9Ci7MViKEU,5216
8
8
  phoenix/settings.py,sha256=cO-qgis_S27nHirTobYI9hHPfZH18R--WMmxNdsVUwc,273
9
- phoenix/version.py,sha256=3tdrXCYXhzGl0HhTFxiRhMv5mTezDVgvqXVYnKeIJeo,23
9
+ phoenix/version.py,sha256=i2svGkhQxPKAQeMjtRMY-YcR__KOdErWKzax-PEH1rY,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=km_a--PBHOuA337ClRw9xqhOHhrUT6Rl9pz_zV0JYkQ,4843
@@ -63,13 +63,16 @@ phoenix/metrics/mixins.py,sha256=moZ5hENIKzUQt2IRhWOd5EFXnoqQkVrpqEqMH7KQzyA,744
63
63
  phoenix/metrics/retrieval_metrics.py,sha256=XFQPo66h16w7-1AJ92M1VL_BUIXIWxXHGKF_QVOABZI,4384
64
64
  phoenix/metrics/timeseries.py,sha256=Cib3E0njJzi0vZpmyADvbakFQA98rIkfDaYAOmsmBz8,6277
65
65
  phoenix/metrics/wrappers.py,sha256=umZqa_5lf1wZSFe3FgzxF-qp1xbPdKD54W628GlGCUI,8392
66
+ phoenix/otel/__init__.py,sha256=YvEiD-3aGZs9agwLNCXU34ofV3G-Q-dolfsiinOJuT0,407
67
+ phoenix/otel/otel.py,sha256=6tlr7VHdVzykQ3Pu5VjvDcjUBKG6GV5fJVZCluXi_d0,11793
68
+ phoenix/otel/settings.py,sha256=Qr2-RkgLQRfLhJqtLpEkSpqns7qLjPoOvpEOTqeSohM,3026
66
69
  phoenix/pointcloud/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
67
70
  phoenix/pointcloud/clustering.py,sha256=IzcG67kJ2hPP7pcqVmKPSL_6gKRonKdOT3bCtbTOqnk,820
68
71
  phoenix/pointcloud/pointcloud.py,sha256=4zAIkKs2xOUbchpj4XDAV-iPMXrfAJ15TG6rlIYGrao,2145
69
72
  phoenix/pointcloud/projectors.py,sha256=zO_RrtDYSv2rqVOfIP2_9Cv11Dc8EmcZR94xhFcBYPU,1057
70
73
  phoenix/pointcloud/umap_parameters.py,sha256=3UQSjrysVOvq2V4KNpTMqNqNiK0BsTZnPBHWZ4fyJtQ,1708
71
74
  phoenix/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
- phoenix/server/app.py,sha256=rPzpaEpTSViIP-RLbHzYfxAf7zplKOEbFHxCb40YXFc,26954
75
+ phoenix/server/app.py,sha256=7LiZiusw_yHZBVWMk8gH4tX8yFitTJ2jdyhMWVm2DdM,26817
73
76
  phoenix/server/dml_event.py,sha256=MpjCFqljxvgb9OB5Cez9vJesb3oHb3XxXictynBfcis,2851
74
77
  phoenix/server/dml_event_handler.py,sha256=6p-PucctivelVHfO-_9zNxWZYPr_eGjDF3bKjLtc5co,8251
75
78
  phoenix/server/grpc_server.py,sha256=jllxDNkpLQxDkvej4RhTokobowbvydF-SU8gSw1MTCc,3378
@@ -79,15 +82,16 @@ phoenix/server/telemetry.py,sha256=T_2OKrxNViAeaANlNspEekg_Y5uZIFWvKAnpz8Aoqvk,2
79
82
  phoenix/server/thread_server.py,sha256=RwXQGP_QhGD7le6WB7xEygEEuwBl5Ck_Zo8xGIYGi9M,2135
80
83
  phoenix/server/types.py,sha256=S2dReLNboR2nzjRK5j3MUyUDqu6AQFD7KRwJkeKj1q4,3609
81
84
  phoenix/server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
- phoenix/server/api/context.py,sha256=N_kqhQIlRyPS6JpN-fgCZ2U4_ZbhaknH-23g5JoRU8g,3282
85
+ phoenix/server/api/context.py,sha256=WuhGT2549C5Yc7pWj2S7NaPeT4a-N-_mmz-Vg5bUkI8,3637
86
+ phoenix/server/api/exceptions.py,sha256=KdAzgwNan-wQ7THDrSoeJU2k9zWQVcH6lRiB462VsRA,990
83
87
  phoenix/server/api/interceptor.py,sha256=ykDnoC_apUd-llVli3m1CW18kNSIgjz2qZ6m5JmPDu8,1294
84
- phoenix/server/api/queries.py,sha256=GXQzz1KmRqZCOENemdgoqBlTVA_4V2AZE-g0hBcO0Qk,23839
85
- phoenix/server/api/schema.py,sha256=BcxdqO5CSGqpKd-AAJHMjFlzaK9oJA8GJuxmMfcdjn4,434
88
+ phoenix/server/api/queries.py,sha256=hUzeHOUWuBQ-kjXh13-d5LgJfkbB8XSpFaHJX4YXpC8,23875
89
+ phoenix/server/api/schema.py,sha256=4L2m6QXhaV13YPTZCEZ3hqCPQFHZOy3QnJVLRYQFzpg,548
86
90
  phoenix/server/api/utils.py,sha256=Kl47G-1A7QKTDrc75BU2QK6HupsG6MWuXxy351FOfKQ,858
87
91
  phoenix/server/api/dataloaders/__init__.py,sha256=TrOGnU_SD_vEIxOE_dm8HrD5C2ScLFQ4xQ7f8r-E76s,3064
88
92
  phoenix/server/api/dataloaders/annotation_summaries.py,sha256=Wv8AORZoGd5TJ4Y-em8iqJu87AMpZP7lWOTr-SML-x8,5560
89
93
  phoenix/server/api/dataloaders/average_experiment_run_latency.py,sha256=q091UmkXx37OBKh7L-GJ5LXHyRXfX2w4XTk1NMHtPpw,1827
90
- phoenix/server/api/dataloaders/dataset_example_revisions.py,sha256=i0g8F4akEf3kQOzAvBjO27QwXNsq-kJEM8dtzduxQgY,3720
94
+ phoenix/server/api/dataloaders/dataset_example_revisions.py,sha256=rZhJoIYUGgYhXwVBtq5u0bqtHmIQ2Sh6HNnJsSGIXis,3767
91
95
  phoenix/server/api/dataloaders/dataset_example_spans.py,sha256=-TjdyyJv2c2JiN1OXu6MMmQ-BEKlHXucEDcuObeRVsU,1416
92
96
  phoenix/server/api/dataloaders/document_evaluation_summaries.py,sha256=5XOom2KRAmCwPmtlraiZOSl3vhfaW-eiiYkmetAEalw,5616
93
97
  phoenix/server/api/dataloaders/document_evaluations.py,sha256=V6sE34jON_qFxt7eArJbktykAsty-gnBZHlEkORcj0E,1296
@@ -139,11 +143,12 @@ phoenix/server/api/input_types/TimeRange.py,sha256=yzx-gxj8mDeGLft1FzU_x1MVEgIG5
139
143
  phoenix/server/api/input_types/TraceAnnotationSort.py,sha256=BzwiUnMh2VsgQYnhDlbJ6ljHugqIS4YDUlYzvq_tl3o,365
140
144
  phoenix/server/api/input_types/UserRoleInput.py,sha256=xxhFe0ITZOgRVEJbVem_W6F1Ip_H6xDENdQqMMx-kKE,129
141
145
  phoenix/server/api/input_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
- phoenix/server/api/mutations/__init__.py,sha256=Cu4lPgUFRAGzKO528jKepwKtfre9lkLTN059S2Shmnw,977
143
- phoenix/server/api/mutations/api_key_mutations.py,sha256=cv6AT6UAL55lTC_UqMdcN-1TjWAgqqZi__S9Tw12t6I,3688
146
+ phoenix/server/api/mutations/__init__.py,sha256=JS3WRqYxNeoaLsKjODFvJnZb6CF19IFW-lfOsUq3rtM,1074
147
+ phoenix/server/api/mutations/api_key_mutations.py,sha256=5GDsP0gURgXokbajc3GJEor3AGt6BGd1IMWMg054E7U,4683
144
148
  phoenix/server/api/mutations/auth.py,sha256=8o4tTfGCPkpUauuB9ijPH84Od77UX_UrQWfmUsnujI4,524
145
- phoenix/server/api/mutations/dataset_mutations.py,sha256=0feBUW_07FEIx6uzepjxfRVhk5lAck0AkrqS1GVdoF4,27041
146
- phoenix/server/api/mutations/experiment_mutations.py,sha256=OXtLYdLA33RGy1MFctfv6ug2sODcDElhJph_J9vkIjk,3157
149
+ phoenix/server/api/mutations/auth_mutations.py,sha256=XLCxmsjyVp1riGWxUhVUOiGhIFs_ZmOfZM77vs_RLDw,2182
150
+ phoenix/server/api/mutations/dataset_mutations.py,sha256=8S6qjmraSBxA7ioNogTQPp6q27ZdvdAn6yt0Z4fmOI0,27096
151
+ phoenix/server/api/mutations/experiment_mutations.py,sha256=Z2xPrK8J117l5XWN-IvdKpykWIiVXysaUhzuwbIiLtk,3226
147
152
  phoenix/server/api/mutations/export_events_mutations.py,sha256=t_wYBxaqvBJYRoHslh3Bmoxmwlzoy0u8SsBKWIKN5hE,4028
148
153
  phoenix/server/api/mutations/project_mutations.py,sha256=MLm7I97lJ85hTuc1tq8sdYA8Ps5WKMV-bGqeeN-Ey90,2279
149
154
  phoenix/server/api/mutations/span_annotations_mutations.py,sha256=DM9gzxrMSAcxwXQ6jNaNGDVgl8oP50LZsBWRYQwLaSo,5955
@@ -153,14 +158,13 @@ phoenix/server/api/openapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
153
158
  phoenix/server/api/openapi/main.py,sha256=KNutA_7AvV_WlGX8cOkvvDujcJKQ7AD1HT6rTpCpR8A,616
154
159
  phoenix/server/api/openapi/schema.py,sha256=oVZoflWMfzOrLKMIrjr3iLnJ13rmN-t_DOe9g6KoN5s,471
155
160
  phoenix/server/api/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
156
- phoenix/server/api/routers/auth.py,sha256=dGug0NjOjW1mmIghmheAgHutG7_0-RjL-FcEReWzTHc,1806
157
161
  phoenix/server/api/routers/utils.py,sha256=M41BoH-fl37izhRuN2aX7lWm7jOC20A_3uClv9TVUUY,583
158
162
  phoenix/server/api/routers/v1/__init__.py,sha256=nb49zcOdAi3DSGuC9gUubN9Yri-o7-WFdlGak4jGuFw,1462
159
163
  phoenix/server/api/routers/v1/datasets.py,sha256=l3Hlc9AVyvX5GdT9iOXBsV-i4c_vtnCaXeSAWdNzcw8,37090
160
164
  phoenix/server/api/routers/v1/evaluations.py,sha256=FSfz9MTi8s65F07abDXlb9-y97fDZSYbqsCXpimwO7g,12628
161
165
  phoenix/server/api/routers/v1/experiment_evaluations.py,sha256=RTQnjupjmh07xowjq77ajbuAZhzIEfYxA4ZtECvGwOU,4844
162
166
  phoenix/server/api/routers/v1/experiment_runs.py,sha256=0G7GgGcZv9dzK47tsPp-p4k5O7W4F_aNRrsNuJN7mho,6393
163
- phoenix/server/api/routers/v1/experiments.py,sha256=3u275sGuYSiMyzC_obbjK3mf6aYb7SkY2c_wOg3z4xg,11751
167
+ phoenix/server/api/routers/v1/experiments.py,sha256=6Ouby3jQDZjeb_nJoD5eH8h2jxINBLrHzCPMsklzcJI,11820
164
168
  phoenix/server/api/routers/v1/pydantic_compat.py,sha256=FeK8oe2brqu-djsoqRxiKL4tw5cHmi89OHVfCFxYsAo,2890
165
169
  phoenix/server/api/routers/v1/spans.py,sha256=MAkMLrONFtItQxkHJde_Wpvz0jsgydegxVZOkZkRUsU,8781
166
170
  phoenix/server/api/routers/v1/traces.py,sha256=HJDmYKMATL40dZEJro6uQ3imbCZBzk3nUun9d21jcDs,7799
@@ -237,10 +241,10 @@ phoenix/server/static/apple-touch-icon-76x76.png,sha256=CT_xT12I0u2i0WU8JzBZBuOQ
237
241
  phoenix/server/static/apple-touch-icon.png,sha256=fOfpjqGpWYbJ0eAurKsyoZP1EAs6ZVooBJ_SGk2ZkDs,3801
238
242
  phoenix/server/static/favicon.ico,sha256=bY0vvCKRftemZfPShwZtE93DiiQdaYaozkPGwNFr6H8,34494
239
243
  phoenix/server/static/modernizr.js,sha256=mvK-XtkNqjOral-QvzoqsyOMECXIMu5BQwSVN_wcU9c,2564
240
- phoenix/server/static/.vite/manifest.json,sha256=NCacyzu0qbu92qY-iwPA7JHvzK56ZJnp_usNyMD3fZw,1929
241
- phoenix/server/static/assets/components-1MfQimGx.js,sha256=NfJgri_ChJVeYEExpGOvfr5SLFuX_bZcvjfEVGH3HWI,187209
242
- phoenix/server/static/assets/index-B263sE2x.js,sha256=PZG-hlU6oncPYh7r6tfdNm5pjuL94SFLHg4fCyr5Oe8,7515
243
- phoenix/server/static/assets/pages-CqZDVx20.js,sha256=_Lor33vSFj6dZBGvOK4fPt1vXj9pndZFpyyi5BG_8AY,467596
244
+ phoenix/server/static/.vite/manifest.json,sha256=1BcJFFm0NeLAAfZ6r47H6TP_44e5P36X2rORXCQE5e0,1929
245
+ phoenix/server/static/assets/components-BYH03rjA.js,sha256=P4bMCzZcpwxqWV4NaUU4jpmay0GZusDgXbT9t9IUyeM,189918
246
+ phoenix/server/static/assets/index-fqdjNpYm.js,sha256=SDvWOvsXmEJY3350kb2M0bRmC58xw7M3zDZ0MmdneeU,7515
247
+ phoenix/server/static/assets/pages-DnbxgoTK.js,sha256=OLwZw5mVqnO6AelWT6ndr4kDOhf_8l6ezF5buwHBB7E,502087
244
248
  phoenix/server/static/assets/vendor-DxkFTwjz.css,sha256=nZrkr0u6NNElFGvpWHk9GTHeGoibCXCli1bE7mXZGZg,1816
245
249
  phoenix/server/static/assets/vendor-aSQri0vz.js,sha256=x_07SENutKMhtJ9HgFqkQHvwsDTfPkMmzQznY3HY7Zo,1359197
246
250
  phoenix/server/static/assets/vendor-arizeai-CsdcB1NH.js,sha256=VEn7hFJXcHV_DODmeDi9pEpF_D2NQ1bZYewbPe3BhIw,304008
@@ -291,8 +295,8 @@ phoenix/utilities/logging.py,sha256=lDXd6EGaamBNcQxL4vP1au9-i_SXe0OraUDiJOcszSw,
291
295
  phoenix/utilities/project.py,sha256=8IJuMM4yUMoooPi37sictGj8Etu9rGmq6RFtc9848cQ,436
292
296
  phoenix/utilities/re.py,sha256=PDve_OLjRTM8yQQJHC8-n3HdIONi7aNils3ZKRZ5uBM,2045
293
297
  phoenix/utilities/span_store.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
294
- arize_phoenix-4.27.0.dist-info/METADATA,sha256=P6671H1bFkfBHk7FF-1pEwq0_9AoN8zR0RRwVJRqZ2w,11936
295
- arize_phoenix-4.27.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
296
- arize_phoenix-4.27.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
297
- arize_phoenix-4.27.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
298
- arize_phoenix-4.27.0.dist-info/RECORD,,
298
+ arize_phoenix-4.29.0.dist-info/METADATA,sha256=UJiskTOjTLRjXDFO41I1ioPp3KZDU2nmgqMucGUyuAI,11977
299
+ arize_phoenix-4.29.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
300
+ arize_phoenix-4.29.0.dist-info/licenses/IP_NOTICE,sha256=JBqyyCYYxGDfzQ0TtsQgjts41IJoa-hiwDrBjCb9gHM,469
301
+ arize_phoenix-4.29.0.dist-info/licenses/LICENSE,sha256=HFkW9REuMOkvKRACuwLPT0hRydHb3zNg-fdFt94td18,3794
302
+ arize_phoenix-4.29.0.dist-info/RECORD,,
phoenix/auth.py CHANGED
@@ -39,7 +39,10 @@ def validate_password_format(password: str) -> None:
39
39
  raise ValueError("Password cannot contain whitespace characters")
40
40
  if not password.isascii():
41
41
  raise ValueError("Password can contain only ASCII characters")
42
+ if not len(password) >= MIN_PASSWORD_LENGTH:
43
+ raise ValueError(f"Password must be at least {MIN_PASSWORD_LENGTH} characters long")
42
44
 
43
45
 
44
46
  EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+[.][^@\s]+\Z")
45
47
  NUM_ITERATIONS = 10_000
48
+ MIN_PASSWORD_LENGTH = 4
@@ -0,0 +1,22 @@
1
+ from opentelemetry.sdk.resources import Resource
2
+
3
+ from .otel import (
4
+ PROJECT_NAME,
5
+ BatchSpanProcessor,
6
+ GRPCSpanExporter,
7
+ HTTPSpanExporter,
8
+ SimpleSpanProcessor,
9
+ TracerProvider,
10
+ register,
11
+ )
12
+
13
+ __all__ = [
14
+ "TracerProvider",
15
+ "SimpleSpanProcessor",
16
+ "BatchSpanProcessor",
17
+ "HTTPSpanExporter",
18
+ "GRPCSpanExporter",
19
+ "Resource",
20
+ "PROJECT_NAME",
21
+ "register",
22
+ ]
phoenix/otel/otel.py ADDED
@@ -0,0 +1,284 @@
1
+ import inspect
2
+ import os
3
+ import warnings
4
+ from typing import Any, Dict, List, Optional, Tuple, Union, cast
5
+ from urllib.parse import ParseResult, urlparse
6
+
7
+ from openinference.semconv.resource import ResourceAttributes as _ResourceAttributes
8
+ from opentelemetry import trace as trace_api
9
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
10
+ OTLPSpanExporter as _GRPCSpanExporter,
11
+ )
12
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
13
+ OTLPSpanExporter as _HTTPSpanExporter,
14
+ )
15
+ from opentelemetry.sdk.resources import Resource
16
+ from opentelemetry.sdk.trace import SpanProcessor
17
+ from opentelemetry.sdk.trace import TracerProvider as _TracerProvider
18
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor as _BatchSpanProcessor
19
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor as _SimpleSpanProcessor
20
+ from opentelemetry.sdk.trace.export import SpanExporter
21
+
22
+ from .settings import get_env_client_headers, get_env_collector_endpoint, get_env_project_name
23
+
24
+ PROJECT_NAME = _ResourceAttributes.PROJECT_NAME
25
+
26
+ _DEFAULT_GRPC_PORT = 4317
27
+
28
+
29
+ def register(
30
+ *,
31
+ endpoint: Optional[str] = None,
32
+ project_name: Optional[str] = None,
33
+ batch: bool = False,
34
+ set_global_tracer: bool = True,
35
+ headers: Optional[Dict[str, str]] = None,
36
+ verbose: bool = True,
37
+ ) -> _TracerProvider:
38
+ """
39
+ Creates an OpenTelemetry TracerProvider for enabling OpenInference tracing.
40
+
41
+ For futher configuration, the `phoenix.otel` module provides drop-in replacements for
42
+ OpenTelemetry TracerProvider, SimpleSpanProcessor, BatchSpanProcessor, HTTPSpanExporter, and
43
+ GRPCSpanExporter objects with Phoenix-aware defaults. Documentation on how to configure tracing
44
+ can be found at https://opentelemetry.io/docs/specs/otel/trace/sdk/.
45
+
46
+ Args:
47
+ endpoint (str, optional): The collector endpoint to which spans will be exported. If not
48
+ provided, the `PHOENIX_OTEL_COLLECTOR_ENDPOINT` environment variable will be used. The
49
+ export protocol will be inferred from the endpoint.
50
+ project_name (str, optional): The name of the project to which spans will be associated. If
51
+ not provided, the `PHOENIX_PROJECT_NAME` environment variable will be used.
52
+ batch (bool): If True, spans will be processed using a BatchSpanprocessor. If False, spans
53
+ will be processed one at a time using a SimpleSpanProcessor.
54
+ set_global_tracer (bool): If False, the TracerProvider will not be set as the global
55
+ tracer provider. Defaults to True.
56
+ headers (dict, optional): Optional headers to include in the HTTP request to the collector.
57
+ verbose (bool): If True, configuration details will be printed to stdout.
58
+ """
59
+
60
+ project_name = project_name or get_env_project_name()
61
+ resource = Resource.create({PROJECT_NAME: project_name})
62
+ tracer_provider = TracerProvider(resource=resource, verbose=False)
63
+ span_processor: SpanProcessor
64
+ if batch:
65
+ span_processor = BatchSpanProcessor(endpoint=endpoint, headers=headers)
66
+ else:
67
+ span_processor = SimpleSpanProcessor(endpoint=endpoint, headers=headers)
68
+ tracer_provider.add_span_processor(span_processor)
69
+ tracer_provider._default_processor = True
70
+
71
+ if set_global_tracer:
72
+ trace_api.set_tracer_provider(tracer_provider)
73
+ global_provider_msg = (
74
+ "| \n"
75
+ "| `register` has set this TracerProvider as the global OpenTelemetry default.\n"
76
+ "| To disable this behavior, call `register` with `set_global_tracer=False`.\n"
77
+ )
78
+ else:
79
+ global_provider_msg = ""
80
+
81
+ details = tracer_provider._tracing_details()
82
+ if verbose:
83
+ print(f"{details}" f"{global_provider_msg}")
84
+ return tracer_provider
85
+
86
+
87
+ class TracerProvider(_TracerProvider):
88
+ def __init__(
89
+ self, *args: Any, endpoint: Optional[str] = None, verbose: bool = True, **kwargs: Any
90
+ ):
91
+ sig = inspect.signature(_TracerProvider)
92
+ bound_args = sig.bind_partial(*args, **kwargs)
93
+ bound_args.apply_defaults()
94
+ if bound_args.arguments.get("resource") is None:
95
+ bound_args.arguments["resource"] = Resource.create(
96
+ {PROJECT_NAME: get_env_project_name()}
97
+ )
98
+ super().__init__(**bound_args.arguments)
99
+
100
+ parsed_url, endpoint = _normalized_endpoint(endpoint)
101
+ self._default_processor = False
102
+
103
+ if _maybe_http_endpoint(parsed_url):
104
+ http_exporter: SpanExporter = HTTPSpanExporter(endpoint=endpoint)
105
+ self.add_span_processor(SimpleSpanProcessor(span_exporter=http_exporter))
106
+ self._default_processor = True
107
+ elif _maybe_grpc_endpoint(parsed_url):
108
+ grpc_exporter: SpanExporter = GRPCSpanExporter(endpoint=endpoint)
109
+ self.add_span_processor(SimpleSpanProcessor(span_exporter=grpc_exporter))
110
+ self._default_processor = True
111
+ if verbose:
112
+ print(self._tracing_details())
113
+
114
+ def add_span_processor(self, *args: Any, **kwargs: Any) -> None:
115
+ if self._default_processor:
116
+ self._active_span_processor.shutdown()
117
+ self._active_span_processor._span_processors = tuple() # remove default processors
118
+ self._default_processor = False
119
+ return super().add_span_processor(*args, **kwargs)
120
+
121
+ def _tracing_details(self) -> str:
122
+ project = self.resource.attributes.get(PROJECT_NAME)
123
+ processor_name: Optional[str] = None
124
+ endpoint: Optional[str] = None
125
+ transport: Optional[str] = None
126
+ headers: Optional[Union[Dict[str, str], str]] = None
127
+
128
+ if self._active_span_processor:
129
+ if processors := self._active_span_processor._span_processors:
130
+ if len(processors) == 1:
131
+ span_processor = self._active_span_processor._span_processors[0]
132
+ if exporter := getattr(span_processor, "span_exporter"):
133
+ processor_name = span_processor.__class__.__name__
134
+ endpoint = exporter._endpoint
135
+ transport = _exporter_transport(exporter)
136
+ headers = _printable_headers(exporter._headers)
137
+ else:
138
+ processor_name = "Multiple Span Processors"
139
+ endpoint = "Multiple Span Exporters"
140
+ transport = "Multiple Span Exporters"
141
+ headers = "Multiple Span Exporters"
142
+
143
+ if os.name == "nt":
144
+ details_header = "OpenTelemetry Tracing Details"
145
+ else:
146
+ details_header = "🔭 OpenTelemetry Tracing Details 🔭"
147
+
148
+ configuration_msg = (
149
+ "| Using a default SpanProcessor. `add_span_processor` will overwrite this default.\n"
150
+ )
151
+
152
+ details_msg = (
153
+ f"{details_header}\n"
154
+ f"| Phoenix Project: {project}\n"
155
+ f"| Span Processor: {processor_name}\n"
156
+ f"| Collector Endpoint: {endpoint}\n"
157
+ f"| Transport: {transport}\n"
158
+ f"| Transport Headers: {headers}\n"
159
+ "| \n"
160
+ f"{configuration_msg if self._default_processor else ''}"
161
+ )
162
+ return details_msg
163
+
164
+
165
+ class SimpleSpanProcessor(_SimpleSpanProcessor):
166
+ def __init__(
167
+ self,
168
+ span_exporter: Optional[SpanExporter] = None,
169
+ endpoint: Optional[str] = None,
170
+ headers: Optional[Dict[str, str]] = None,
171
+ ):
172
+ if span_exporter is None:
173
+ parsed_url, endpoint = _normalized_endpoint(endpoint)
174
+ if _maybe_http_endpoint(parsed_url):
175
+ span_exporter = HTTPSpanExporter(endpoint=endpoint, headers=headers)
176
+ elif _maybe_grpc_endpoint(parsed_url):
177
+ span_exporter = GRPCSpanExporter(endpoint=endpoint, headers=headers)
178
+ else:
179
+ warnings.warn("Could not infer collector endpoint protocol, defaulting to HTTP.")
180
+ span_exporter = HTTPSpanExporter(endpoint=endpoint, headers=headers)
181
+ super().__init__(span_exporter)
182
+
183
+
184
+ class BatchSpanProcessor(_BatchSpanProcessor):
185
+ def __init__(
186
+ self,
187
+ span_exporter: Optional[SpanExporter] = None,
188
+ endpoint: Optional[str] = None,
189
+ headers: Optional[Dict[str, str]] = None,
190
+ ):
191
+ if span_exporter is None:
192
+ parsed_url, endpoint = _normalized_endpoint(endpoint)
193
+ if _maybe_http_endpoint(parsed_url):
194
+ span_exporter = HTTPSpanExporter(endpoint=endpoint, headers=headers)
195
+ elif _maybe_grpc_endpoint(parsed_url):
196
+ span_exporter = GRPCSpanExporter(endpoint=endpoint, headers=headers)
197
+ else:
198
+ warnings.warn("Could not infer collector endpoint protocol, defaulting to HTTP.")
199
+ span_exporter = HTTPSpanExporter(endpoint=endpoint, headers=headers)
200
+ super().__init__(span_exporter)
201
+
202
+
203
+ class HTTPSpanExporter(_HTTPSpanExporter):
204
+ def __init__(self, *args: Any, **kwargs: Any):
205
+ sig = inspect.signature(_HTTPSpanExporter)
206
+ bound_args = sig.bind_partial(*args, **kwargs)
207
+ bound_args.apply_defaults()
208
+
209
+ if not bound_args.arguments.get("headers"):
210
+ bound_args.arguments["headers"] = get_env_client_headers()
211
+
212
+ if bound_args.arguments.get("endpoint") is None:
213
+ _, endpoint = _normalized_endpoint(None)
214
+ bound_args.arguments["endpoint"] = endpoint
215
+ super().__init__(**bound_args.arguments)
216
+
217
+
218
+ class GRPCSpanExporter(_GRPCSpanExporter):
219
+ def __init__(self, *args: Any, **kwargs: Any):
220
+ sig = inspect.signature(_GRPCSpanExporter)
221
+ bound_args = sig.bind_partial(*args, **kwargs)
222
+ bound_args.apply_defaults()
223
+
224
+ if not bound_args.arguments.get("headers"):
225
+ bound_args.arguments["headers"] = get_env_client_headers()
226
+
227
+ if bound_args.arguments.get("endpoint") is None:
228
+ _, endpoint = _normalized_endpoint(None)
229
+ bound_args.arguments["endpoint"] = endpoint
230
+ super().__init__(**bound_args.arguments)
231
+
232
+
233
+ def _maybe_http_endpoint(parsed_endpoint: ParseResult) -> bool:
234
+ if parsed_endpoint.path == "/v1/traces":
235
+ return True
236
+ return False
237
+
238
+
239
+ def _maybe_grpc_endpoint(parsed_endpoint: ParseResult) -> bool:
240
+ if not parsed_endpoint.path and parsed_endpoint.port == 4317:
241
+ return True
242
+ return False
243
+
244
+
245
+ def _exporter_transport(exporter: SpanExporter) -> str:
246
+ if isinstance(exporter, _HTTPSpanExporter):
247
+ return "HTTP"
248
+ if isinstance(exporter, _GRPCSpanExporter):
249
+ return "gRPC"
250
+ else:
251
+ return exporter.__class__.__name__
252
+
253
+
254
+ def _printable_headers(headers: Union[List[Tuple[str, str]], Dict[str, str]]) -> Dict[str, str]:
255
+ if isinstance(headers, dict):
256
+ return {key.lower(): "****" for key, _ in headers.items()}
257
+ return {key.lower(): "****" for key, _ in headers}
258
+
259
+
260
+ def _construct_http_endpoint(parsed_endpoint: ParseResult) -> ParseResult:
261
+ return parsed_endpoint._replace(path="/v1/traces")
262
+
263
+
264
+ def _construct_grpc_endpoint(parsed_endpoint: ParseResult) -> ParseResult:
265
+ return parsed_endpoint._replace(netloc=f"{parsed_endpoint.hostname}:{_DEFAULT_GRPC_PORT}")
266
+
267
+
268
+ _KNOWN_PROVIDERS = {
269
+ "app.phoenix.arize.com": _construct_http_endpoint,
270
+ }
271
+
272
+
273
+ def _normalized_endpoint(endpoint: Optional[str]) -> Tuple[ParseResult, str]:
274
+ if endpoint is None:
275
+ base_endpoint = get_env_collector_endpoint() or "http://localhost:6006"
276
+ parsed = urlparse(base_endpoint)
277
+ if parsed.hostname in _KNOWN_PROVIDERS:
278
+ parsed = _KNOWN_PROVIDERS[parsed.hostname](parsed)
279
+ else:
280
+ parsed = _construct_grpc_endpoint(parsed)
281
+ else:
282
+ parsed = urlparse(endpoint)
283
+ parsed = cast(ParseResult, parsed)
284
+ return parsed, parsed.geturl()
@@ -0,0 +1,82 @@
1
+ import os
2
+ import urllib
3
+ from logging import getLogger
4
+ from re import compile
5
+ from typing import Dict, List, Optional
6
+
7
+ _logger = getLogger(__name__)
8
+
9
+ # Environment variables specific to the subpackage
10
+ ENV_PHOENIX_COLLECTOR_ENDPOINT = "PHOENIX_COLLECTOR_ENDPOINT"
11
+ ENV_PHOENIX_PROJECT_NAME = "PHOENIX_PROJECT_NAME"
12
+ ENV_PHOENIX_CLIENT_HEADERS = "PHOENIX_CLIENT_HEADERS"
13
+
14
+
15
+ def get_env_collector_endpoint() -> Optional[str]:
16
+ return os.getenv(ENV_PHOENIX_COLLECTOR_ENDPOINT)
17
+
18
+
19
+ def get_env_project_name() -> str:
20
+ return os.getenv(ENV_PHOENIX_PROJECT_NAME, "default")
21
+
22
+
23
+ def get_env_client_headers() -> Optional[Dict[str, str]]:
24
+ if headers_str := os.getenv(ENV_PHOENIX_CLIENT_HEADERS):
25
+ return parse_env_headers(headers_str)
26
+ return None
27
+
28
+
29
+ # Optional whitespace
30
+ _OWS = r"[ \t]*"
31
+ # A key contains printable US-ASCII characters except: SP and "(),/:;<=>?@[\]{}
32
+ _KEY_FORMAT = r"[\x21\x23-\x27\x2a\x2b\x2d\x2e\x30-\x39\x41-\x5a\x5e-\x7a\x7c\x7e]+"
33
+ # A value contains a URL-encoded UTF-8 string. The encoded form can contain any
34
+ # printable US-ASCII characters (0x20-0x7f) other than SP, DEL, and ",;/
35
+ _VALUE_FORMAT = r"[\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]*"
36
+ # A key-value is key=value, with optional whitespace surrounding key and value
37
+ _KEY_VALUE_FORMAT = rf"{_OWS}{_KEY_FORMAT}{_OWS}={_OWS}{_VALUE_FORMAT}{_OWS}"
38
+
39
+ _HEADER_PATTERN = compile(_KEY_VALUE_FORMAT)
40
+ _DELIMITER_PATTERN = compile(r"[ \t]*,[ \t]*")
41
+
42
+
43
+ def parse_env_headers(s: str) -> Dict[str, str]:
44
+ """
45
+ Parse ``s``, which is a ``str`` instance containing HTTP headers encoded
46
+ for use in ENV variables per the W3C Baggage HTTP header format at
47
+ https://www.w3.org/TR/baggage/#baggage-http-header-format, except that
48
+ additional semi-colon delimited metadata is not supported.
49
+
50
+ If the headers are not urlencoded, we will log a warning and attempt to urldecode them.
51
+ """
52
+ headers: Dict[str, str] = {}
53
+ headers_list: List[str] = _DELIMITER_PATTERN.split(s)
54
+
55
+ for header in headers_list:
56
+ if not header: # empty string
57
+ continue
58
+
59
+ match = _HEADER_PATTERN.fullmatch(header.strip())
60
+ if not match:
61
+ parts = header.split("=", 1)
62
+ name, value = parts
63
+ encoded_header = f"{urllib.parse.quote(name)}={urllib.parse.quote(value)}"
64
+ match = _HEADER_PATTERN.fullmatch(encoded_header.strip())
65
+ if not match:
66
+ _logger.warning(
67
+ "Header format invalid! Header values in environment variables must be "
68
+ "URL encoded: %s",
69
+ f"{name}: ****",
70
+ )
71
+ continue
72
+ _logger.warning(
73
+ "Header values in environment variables should be URL encoded, attempting to "
74
+ "URL encode header: {name}: ****"
75
+ )
76
+
77
+ name, value = header.split("=", 1)
78
+ name = urllib.parse.unquote(name).strip().lower()
79
+ value = urllib.parse.unquote(value).strip()
80
+ headers[name] = value
81
+
82
+ return headers
@@ -2,6 +2,7 @@ from dataclasses import dataclass
2
2
  from pathlib import Path
3
3
  from typing import Any, Optional
4
4
 
5
+ from starlette.responses import Response as StarletteResponse
5
6
  from strawberry.fastapi import BaseContext
6
7
 
7
8
  from phoenix.core.model_schema import Model
@@ -77,7 +78,7 @@ class Context(BaseContext):
77
78
  secret: Optional[str] = None
78
79
 
79
80
  def get_secret(self) -> str:
80
- """A type safe way to get the application secret. Throws an error if the secret is not set.
81
+ """A type-safe way to get the application secret. Throws an error if the secret is not set.
81
82
 
82
83
  Returns:
83
84
  str: the phoenix secret
@@ -88,3 +89,11 @@ class Context(BaseContext):
88
89
  " Please set the PHOENIX_SECRET environment variable and re-deploy the application."
89
90
  )
90
91
  return self.secret
92
+
93
+ def get_response(self) -> StarletteResponse:
94
+ """
95
+ A type-safe way to get the response object. Throws an error if the response is not set.
96
+ """
97
+ if (response := self.response) is None:
98
+ raise ValueError("no response is set")
99
+ return response
@@ -10,6 +10,7 @@ from strawberry.dataloader import DataLoader
10
10
  from typing_extensions import TypeAlias
11
11
 
12
12
  from phoenix.db import models
13
+ from phoenix.server.api.exceptions import NotFound
13
14
  from phoenix.server.api.types.DatasetExampleRevision import DatasetExampleRevision
14
15
  from phoenix.server.types import DbSessionFactory
15
16
 
@@ -24,7 +25,7 @@ class DatasetExampleRevisionsDataLoader(DataLoader[Key, Result]):
24
25
  super().__init__(load_fn=self._load_fn)
25
26
  self._db = db
26
27
 
27
- async def _load_fn(self, keys: List[Key]) -> List[Union[Result, ValueError]]:
28
+ async def _load_fn(self, keys: List[Key]) -> List[Union[Result, NotFound]]:
28
29
  # sqlalchemy has limited SQLite support for VALUES, so use UNION ALL instead.
29
30
  # For details, see https://github.com/sqlalchemy/sqlalchemy/issues/7228
30
31
  keys_subquery = union(
@@ -95,4 +96,4 @@ class DatasetExampleRevisionsDataLoader(DataLoader[Key, Result]):
95
96
  ) in await session.stream(query)
96
97
  if is_valid_version
97
98
  }
98
- return [results.get(key, ValueError("Could not find revision.")) for key in keys]
99
+ return [results.get(key, NotFound("Could not find revision.")) for key in keys]
@@ -0,0 +1,41 @@
1
+ from graphql.error import GraphQLError
2
+ from strawberry.extensions import MaskErrors
3
+
4
+
5
+ class CustomGraphQLError(Exception):
6
+ """
7
+ An error that represents an expected error scenario in a GraphQL resolver.
8
+ """
9
+
10
+
11
+ class BadRequest(CustomGraphQLError):
12
+ """
13
+ An error raised due to a malformed or invalid request.
14
+ """
15
+
16
+
17
+ class NotFound(CustomGraphQLError):
18
+ """
19
+ An error raised when the requested resource is not found.
20
+ """
21
+
22
+
23
+ class Unauthorized(CustomGraphQLError):
24
+ """
25
+ An error raised when login fails or a user or other entity is not authorized
26
+ to access a resource.
27
+ """
28
+
29
+
30
+ def get_mask_errors_extension() -> MaskErrors:
31
+ return MaskErrors(
32
+ should_mask_error=_should_mask_error,
33
+ error_message="an unexpected error occurred",
34
+ )
35
+
36
+
37
+ def _should_mask_error(error: GraphQLError) -> bool:
38
+ """
39
+ Masks unexpected errors raised from GraphQL resolvers.
40
+ """
41
+ return not isinstance(error.original_error, CustomGraphQLError)
@@ -1,6 +1,7 @@
1
1
  import strawberry
2
2
 
3
3
  from phoenix.server.api.mutations.api_key_mutations import ApiKeyMutationMixin
4
+ from phoenix.server.api.mutations.auth_mutations import AuthMutationMixin
4
5
  from phoenix.server.api.mutations.dataset_mutations import DatasetMutationMixin
5
6
  from phoenix.server.api.mutations.experiment_mutations import ExperimentMutationMixin
6
7
  from phoenix.server.api.mutations.export_events_mutations import ExportEventsMutationMixin
@@ -12,13 +13,14 @@ from phoenix.server.api.mutations.user_mutations import UserMutationMixin
12
13
 
13
14
  @strawberry.type
14
15
  class Mutation(
15
- ProjectMutationMixin,
16
+ ApiKeyMutationMixin,
17
+ AuthMutationMixin,
16
18
  DatasetMutationMixin,
17
19
  ExperimentMutationMixin,
18
20
  ExportEventsMutationMixin,
21
+ ProjectMutationMixin,
19
22
  SpanAnnotationMutationMixin,
20
23
  TraceAnnotationMutationMixin,
21
- ApiKeyMutationMixin,
22
24
  UserMutationMixin,
23
25
  ):
24
26
  pass