arize-phoenix 11.23.1__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-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/METADATA +61 -36
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/RECORD +212 -162
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/WHEEL +1 -1
- {arize_phoenix-11.23.1.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 +2 -1
- phoenix/auth.py +27 -2
- phoenix/config.py +1594 -81
- phoenix/db/README.md +546 -28
- phoenix/db/bulk_inserter.py +119 -116
- phoenix/db/engines.py +140 -33
- phoenix/db/facilitator.py +22 -1
- phoenix/db/helpers.py +818 -65
- phoenix/db/iam_auth.py +64 -0
- phoenix/db/insertion/dataset.py +133 -1
- phoenix/db/insertion/document_annotation.py +9 -6
- phoenix/db/insertion/evaluation.py +2 -3
- phoenix/db/insertion/helpers.py +2 -2
- phoenix/db/insertion/session_annotation.py +176 -0
- phoenix/db/insertion/span_annotation.py +3 -4
- phoenix/db/insertion/trace_annotation.py +3 -4
- phoenix/db/insertion/types.py +41 -18
- 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/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 +364 -56
- phoenix/db/pg_config.py +10 -0
- phoenix/db/types/trace_retention.py +7 -6
- phoenix/experiments/functions.py +69 -19
- phoenix/inferences/inferences.py +1 -2
- phoenix/server/api/auth.py +9 -0
- phoenix/server/api/auth_messages.py +46 -0
- phoenix/server/api/context.py +60 -0
- phoenix/server/api/dataloaders/__init__.py +36 -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/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_summary_by_experiment_repeated_run_group.py +64 -0
- phoenix/server/api/dataloaders/span_cost_summary_by_project.py +28 -14
- phoenix/server/api/dataloaders/span_costs.py +3 -9
- 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/exceptions.py +5 -1
- phoenix/server/api/helpers/playground_clients.py +263 -83
- phoenix/server/api/helpers/playground_spans.py +2 -1
- phoenix/server/api/helpers/playground_users.py +26 -0
- phoenix/server/api/helpers/prompts/conversions/google.py +103 -0
- phoenix/server/api/helpers/prompts/models.py +61 -19
- phoenix/server/api/input_types/{SpanAnnotationFilter.py → AnnotationFilter.py} +22 -14
- phoenix/server/api/input_types/ChatCompletionInput.py +3 -0
- phoenix/server/api/input_types/CreateProjectSessionAnnotationInput.py +37 -0
- phoenix/server/api/input_types/DatasetFilter.py +5 -2
- phoenix/server/api/input_types/ExperimentRunSort.py +237 -0
- phoenix/server/api/input_types/GenerativeModelInput.py +3 -0
- phoenix/server/api/input_types/ProjectSessionSort.py +158 -1
- phoenix/server/api/input_types/PromptVersionInput.py +47 -1
- phoenix/server/api/input_types/SpanSort.py +3 -2
- phoenix/server/api/input_types/UpdateAnnotationInput.py +34 -0
- phoenix/server/api/input_types/UserRoleInput.py +1 -0
- phoenix/server/api/mutations/__init__.py +8 -0
- phoenix/server/api/mutations/annotation_config_mutations.py +8 -8
- phoenix/server/api/mutations/api_key_mutations.py +15 -20
- phoenix/server/api/mutations/chat_mutations.py +106 -37
- 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 +11 -9
- phoenix/server/api/mutations/project_mutations.py +4 -4
- 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 +13 -8
- phoenix/server/api/mutations/trace_mutations.py +3 -3
- phoenix/server/api/mutations/user_mutations.py +55 -26
- phoenix/server/api/queries.py +501 -617
- phoenix/server/api/routers/__init__.py +2 -2
- phoenix/server/api/routers/auth.py +141 -87
- phoenix/server/api/routers/ldap.py +229 -0
- phoenix/server/api/routers/oauth2.py +349 -101
- phoenix/server/api/routers/v1/__init__.py +22 -4
- phoenix/server/api/routers/v1/annotation_configs.py +19 -30
- phoenix/server/api/routers/v1/annotations.py +455 -13
- phoenix/server/api/routers/v1/datasets.py +355 -68
- phoenix/server/api/routers/v1/documents.py +142 -0
- phoenix/server/api/routers/v1/evaluations.py +20 -28
- phoenix/server/api/routers/v1/experiment_evaluations.py +16 -6
- phoenix/server/api/routers/v1/experiment_runs.py +335 -59
- phoenix/server/api/routers/v1/experiments.py +475 -47
- phoenix/server/api/routers/v1/projects.py +16 -50
- phoenix/server/api/routers/v1/prompts.py +50 -39
- phoenix/server/api/routers/v1/sessions.py +108 -0
- phoenix/server/api/routers/v1/spans.py +156 -96
- phoenix/server/api/routers/v1/traces.py +51 -77
- phoenix/server/api/routers/v1/users.py +64 -24
- phoenix/server/api/routers/v1/utils.py +3 -7
- phoenix/server/api/subscriptions.py +257 -93
- 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/Dataset.py +199 -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 +215 -68
- phoenix/server/api/types/ExperimentComparison.py +3 -9
- phoenix/server/api/types/ExperimentRepeatedRunGroup.py +155 -0
- phoenix/server/api/types/ExperimentRepeatedRunGroupAnnotationSummary.py +9 -0
- phoenix/server/api/types/ExperimentRun.py +120 -70
- phoenix/server/api/types/ExperimentRunAnnotation.py +158 -39
- phoenix/server/api/types/GenerativeModel.py +95 -42
- phoenix/server/api/types/GenerativeProvider.py +1 -1
- phoenix/server/api/types/ModelInterface.py +7 -2
- phoenix/server/api/types/PlaygroundModel.py +12 -2
- phoenix/server/api/types/Project.py +218 -185
- phoenix/server/api/types/ProjectSession.py +146 -29
- 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/Span.py +130 -123
- phoenix/server/api/types/SpanAnnotation.py +189 -42
- phoenix/server/api/types/SystemApiKey.py +65 -1
- phoenix/server/api/types/Trace.py +184 -53
- phoenix/server/api/types/TraceAnnotation.py +149 -50
- phoenix/server/api/types/User.py +128 -33
- 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 +154 -36
- phoenix/server/authorization.py +5 -4
- phoenix/server/bearer_auth.py +13 -5
- phoenix/server/cost_tracking/cost_model_lookup.py +42 -14
- phoenix/server/cost_tracking/model_cost_manifest.json +1085 -194
- phoenix/server/daemons/generative_model_store.py +61 -9
- phoenix/server/daemons/span_cost_calculator.py +10 -8
- phoenix/server/dml_event.py +13 -0
- phoenix/server/email/sender.py +29 -2
- phoenix/server/grpc_server.py +9 -9
- phoenix/server/jwt_store.py +8 -6
- phoenix/server/ldap.py +1449 -0
- phoenix/server/main.py +9 -3
- phoenix/server/oauth2.py +330 -12
- phoenix/server/prometheus.py +43 -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 +51 -53
- phoenix/server/static/assets/components-BreFUQQa.js +6702 -0
- phoenix/server/static/assets/{index-BPCwGQr8.js → index-CTQoemZv.js} +42 -35
- 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-Bw30oz1A.js → vendor-recharts-V9cwpXsm.js} +7 -7
- phoenix/server/static/assets/{vendor-shiki-DZajAPeq.js → vendor-shiki-Do--csgv.js} +1 -1
- phoenix/server/static/assets/vendor-three-CmB8bl_y.js +3840 -0
- phoenix/server/templates/index.html +7 -1
- phoenix/server/thread_server.py +1 -2
- phoenix/server/utils.py +74 -0
- phoenix/session/client.py +55 -1
- phoenix/session/data_extractor.py +5 -0
- phoenix/session/evaluation.py +8 -4
- phoenix/session/session.py +44 -8
- phoenix/settings.py +2 -0
- phoenix/trace/attributes.py +80 -13
- phoenix/trace/dsl/query.py +2 -0
- phoenix/trace/projects.py +5 -0
- phoenix/utilities/template_formatters.py +1 -1
- phoenix/version.py +1 -1
- phoenix/server/api/types/Evaluation.py +0 -39
- phoenix/server/static/assets/components-D0DWAf0l.js +0 -5650
- phoenix/server/static/assets/pages-Creyamao.js +0 -8612
- phoenix/server/static/assets/vendor-CU36oj8y.js +0 -905
- phoenix/server/static/assets/vendor-CqDb5u4o.css +0 -1
- phoenix/server/static/assets/vendor-arizeai-Ctgw0e1G.js +0 -168
- phoenix/server/static/assets/vendor-codemirror-Cojjzqb9.js +0 -25
- phoenix/server/static/assets/vendor-three-BLWp5bic.js +0 -2998
- phoenix/utilities/deprecation.py +0 -31
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/entry_points.txt +0 -0
- {arize_phoenix-11.23.1.dist-info → arize_phoenix-12.28.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,21 +5,16 @@ from collections.abc import AsyncIterator
|
|
|
5
5
|
from datetime import datetime, timezone
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from secrets import token_urlsafe
|
|
8
|
-
from typing import Annotated, Any,
|
|
8
|
+
from typing import Annotated, Any, Optional, Union
|
|
9
9
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
import sqlalchemy as sa
|
|
12
12
|
from fastapi import APIRouter, Depends, Header, HTTPException, Path, Query
|
|
13
|
-
from pydantic import BaseModel, Field
|
|
13
|
+
from pydantic import BaseModel, BeforeValidator, Field
|
|
14
14
|
from sqlalchemy import exists, select, update
|
|
15
15
|
from starlette.requests import Request
|
|
16
16
|
from starlette.responses import Response, StreamingResponse
|
|
17
|
-
from starlette.status import
|
|
18
|
-
HTTP_202_ACCEPTED,
|
|
19
|
-
HTTP_400_BAD_REQUEST,
|
|
20
|
-
HTTP_404_NOT_FOUND,
|
|
21
|
-
HTTP_422_UNPROCESSABLE_ENTITY,
|
|
22
|
-
)
|
|
17
|
+
from starlette.status import HTTP_404_NOT_FOUND
|
|
23
18
|
from strawberry.relay import GlobalID
|
|
24
19
|
|
|
25
20
|
from phoenix.config import DEFAULT_PROJECT_NAME
|
|
@@ -27,13 +22,13 @@ from phoenix.datetime_utils import normalize_datetime
|
|
|
27
22
|
from phoenix.db import models
|
|
28
23
|
from phoenix.db.helpers import SupportedSQLDialect, get_ancestor_span_rowids
|
|
29
24
|
from phoenix.db.insertion.helpers import as_kv, insert_on_conflict
|
|
30
|
-
from phoenix.db.insertion.types import Precursors
|
|
31
25
|
from phoenix.server.api.routers.utils import df_to_bytes
|
|
26
|
+
from phoenix.server.api.routers.v1.annotations import SpanAnnotationData
|
|
32
27
|
from phoenix.server.api.types.node import from_global_id_with_expected_type
|
|
33
28
|
from phoenix.server.authorization import is_not_locked
|
|
34
29
|
from phoenix.server.bearer_auth import PhoenixUser
|
|
35
30
|
from phoenix.server.dml_event import SpanAnnotationInsertEvent, SpanDeleteEvent
|
|
36
|
-
from phoenix.trace.attributes import flatten
|
|
31
|
+
from phoenix.trace.attributes import flatten, unflatten
|
|
37
32
|
from phoenix.trace.dsl import SpanQuery as SpanQuery_
|
|
38
33
|
from phoenix.trace.schemas import (
|
|
39
34
|
Span as SpanForInsertion,
|
|
@@ -440,7 +435,7 @@ class SpansResponseBody(PaginatedResponseBody[Span]):
|
|
|
440
435
|
"/spans",
|
|
441
436
|
operation_id="querySpans",
|
|
442
437
|
summary="Query spans with query DSL",
|
|
443
|
-
responses=add_errors_to_responses([
|
|
438
|
+
responses=add_errors_to_responses([404, 422]),
|
|
444
439
|
include_in_schema=False,
|
|
445
440
|
)
|
|
446
441
|
async def query_spans_handler(
|
|
@@ -467,30 +462,30 @@ async def query_spans_handler(
|
|
|
467
462
|
except Exception as e:
|
|
468
463
|
raise HTTPException(
|
|
469
464
|
detail=f"Invalid query: {e}",
|
|
470
|
-
status_code=
|
|
465
|
+
status_code=422,
|
|
471
466
|
)
|
|
467
|
+
|
|
472
468
|
async with request.app.state.db() as session:
|
|
473
|
-
results = []
|
|
469
|
+
results: list[pd.DataFrame] = []
|
|
474
470
|
for query in span_queries:
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
start_time
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
end_time
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
orphan_span_as_root_span=request_body.orphan_span_as_root_span,
|
|
490
|
-
)
|
|
471
|
+
df = await session.run_sync(
|
|
472
|
+
query,
|
|
473
|
+
project_name=project_name,
|
|
474
|
+
start_time=normalize_datetime(
|
|
475
|
+
request_body.start_time,
|
|
476
|
+
timezone.utc,
|
|
477
|
+
),
|
|
478
|
+
end_time=normalize_datetime(
|
|
479
|
+
end_time,
|
|
480
|
+
timezone.utc,
|
|
481
|
+
),
|
|
482
|
+
limit=request_body.limit,
|
|
483
|
+
root_spans_only=request_body.root_spans_only,
|
|
484
|
+
orphan_span_as_root_span=request_body.orphan_span_as_root_span,
|
|
491
485
|
)
|
|
486
|
+
results.append(df)
|
|
492
487
|
if not results:
|
|
493
|
-
raise HTTPException(status_code=
|
|
488
|
+
raise HTTPException(status_code=404)
|
|
494
489
|
|
|
495
490
|
if accept == "application/json":
|
|
496
491
|
boundary_token = token_urlsafe(64)
|
|
@@ -574,7 +569,7 @@ def _to_any_value(value: Any) -> OtlpAnyValue:
|
|
|
574
569
|
summary="Search spans with simple filters (no DSL)",
|
|
575
570
|
description="Return spans within a project filtered by time range. "
|
|
576
571
|
"Supports cursor-based pagination.",
|
|
577
|
-
responses=add_errors_to_responses([
|
|
572
|
+
responses=add_errors_to_responses([404, 422]),
|
|
578
573
|
)
|
|
579
574
|
async def span_search_otlpv1(
|
|
580
575
|
request: Request,
|
|
@@ -617,7 +612,7 @@ async def span_search_otlpv1(
|
|
|
617
612
|
cursor_rowid = int(GlobalID.from_id(cursor).node_id)
|
|
618
613
|
stmt = stmt.where(models.Span.id <= cursor_rowid)
|
|
619
614
|
except Exception:
|
|
620
|
-
raise HTTPException(status_code=
|
|
615
|
+
raise HTTPException(status_code=422, detail="Invalid cursor")
|
|
621
616
|
|
|
622
617
|
stmt = stmt.limit(limit + 1)
|
|
623
618
|
|
|
@@ -711,7 +706,7 @@ async def span_search_otlpv1(
|
|
|
711
706
|
summary="List spans with simple filters (no DSL)",
|
|
712
707
|
description="Return spans within a project filtered by time range. "
|
|
713
708
|
"Supports cursor-based pagination.",
|
|
714
|
-
responses=add_errors_to_responses([
|
|
709
|
+
responses=add_errors_to_responses([404, 422]),
|
|
715
710
|
)
|
|
716
711
|
async def span_search(
|
|
717
712
|
request: Request,
|
|
@@ -751,7 +746,7 @@ async def span_search(
|
|
|
751
746
|
try:
|
|
752
747
|
cursor_rowid = int(GlobalID.from_id(cursor).node_id)
|
|
753
748
|
except Exception:
|
|
754
|
-
raise HTTPException(status_code=
|
|
749
|
+
raise HTTPException(status_code=422, detail="Invalid cursor")
|
|
755
750
|
stmt = stmt.where(models.Span.id <= cursor_rowid)
|
|
756
751
|
|
|
757
752
|
stmt = stmt.limit(limit + 1)
|
|
@@ -850,51 +845,6 @@ async def get_spans_handler(
|
|
|
850
845
|
return await query_spans_handler(request, request_body, project_name)
|
|
851
846
|
|
|
852
847
|
|
|
853
|
-
class SpanAnnotationResult(V1RoutesBaseModel):
|
|
854
|
-
label: Optional[str] = Field(default=None, description="The label assigned by the annotation")
|
|
855
|
-
score: Optional[float] = Field(default=None, description="The score assigned by the annotation")
|
|
856
|
-
explanation: Optional[str] = Field(
|
|
857
|
-
default=None, description="Explanation of the annotation result"
|
|
858
|
-
)
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
class SpanAnnotationData(V1RoutesBaseModel):
|
|
862
|
-
span_id: str = Field(description="OpenTelemetry Span ID (hex format w/o 0x prefix)")
|
|
863
|
-
name: str = Field(description="The name of the annotation")
|
|
864
|
-
annotator_kind: Literal["LLM", "CODE", "HUMAN"] = Field(
|
|
865
|
-
description="The kind of annotator used for the annotation"
|
|
866
|
-
)
|
|
867
|
-
result: Optional[SpanAnnotationResult] = Field(
|
|
868
|
-
default=None, description="The result of the annotation"
|
|
869
|
-
)
|
|
870
|
-
metadata: Optional[dict[str, Any]] = Field(
|
|
871
|
-
default=None, description="Metadata for the annotation"
|
|
872
|
-
)
|
|
873
|
-
identifier: str = Field(
|
|
874
|
-
default="",
|
|
875
|
-
description=(
|
|
876
|
-
"The identifier of the annotation. "
|
|
877
|
-
"If provided, the annotation will be updated if it already exists."
|
|
878
|
-
),
|
|
879
|
-
)
|
|
880
|
-
|
|
881
|
-
def as_precursor(self, *, user_id: Optional[int] = None) -> Precursors.SpanAnnotation:
|
|
882
|
-
return Precursors.SpanAnnotation(
|
|
883
|
-
self.span_id,
|
|
884
|
-
models.SpanAnnotation(
|
|
885
|
-
name=self.name,
|
|
886
|
-
annotator_kind=self.annotator_kind,
|
|
887
|
-
score=self.result.score if self.result else None,
|
|
888
|
-
label=self.result.label if self.result else None,
|
|
889
|
-
explanation=self.result.explanation if self.result else None,
|
|
890
|
-
metadata_=self.metadata or {},
|
|
891
|
-
identifier=self.identifier,
|
|
892
|
-
source="API",
|
|
893
|
-
user_id=user_id,
|
|
894
|
-
),
|
|
895
|
-
)
|
|
896
|
-
|
|
897
|
-
|
|
898
848
|
class AnnotateSpansRequestBody(RequestBody[list[SpanAnnotationData]]):
|
|
899
849
|
data: list[SpanAnnotationData]
|
|
900
850
|
|
|
@@ -912,9 +862,7 @@ class AnnotateSpansResponseBody(ResponseBody[list[InsertedSpanAnnotation]]):
|
|
|
912
862
|
dependencies=[Depends(is_not_locked)],
|
|
913
863
|
operation_id="annotateSpans",
|
|
914
864
|
summary="Create span annotations",
|
|
915
|
-
responses=add_errors_to_responses(
|
|
916
|
-
[{"status_code": HTTP_404_NOT_FOUND, "description": "Span not found"}]
|
|
917
|
-
),
|
|
865
|
+
responses=add_errors_to_responses([{"status_code": 404, "description": "Span not found"}]),
|
|
918
866
|
response_description="Span annotations inserted successfully",
|
|
919
867
|
include_in_schema=True,
|
|
920
868
|
)
|
|
@@ -942,15 +890,17 @@ async def annotate_spans(
|
|
|
942
890
|
)
|
|
943
891
|
precursors = [d.as_precursor(user_id=user_id) for d in filtered_span_annotations]
|
|
944
892
|
if not sync:
|
|
945
|
-
await request.state.
|
|
893
|
+
await request.state.enqueue_annotations(*precursors)
|
|
946
894
|
return AnnotateSpansResponseBody(data=[])
|
|
947
895
|
|
|
948
896
|
span_ids = {p.span_id for p in precursors}
|
|
949
897
|
async with request.app.state.db() as session:
|
|
950
898
|
existing_spans = {
|
|
951
|
-
|
|
952
|
-
async for
|
|
953
|
-
select(models.Span
|
|
899
|
+
span_id: id_
|
|
900
|
+
async for span_id, id_ in await session.stream(
|
|
901
|
+
select(models.Span.span_id, models.Span.id).filter(
|
|
902
|
+
models.Span.span_id.in_(span_ids)
|
|
903
|
+
)
|
|
954
904
|
)
|
|
955
905
|
}
|
|
956
906
|
|
|
@@ -958,7 +908,7 @@ async def annotate_spans(
|
|
|
958
908
|
if missing_span_ids:
|
|
959
909
|
raise HTTPException(
|
|
960
910
|
detail=f"Spans with IDs {', '.join(missing_span_ids)} do not exist.",
|
|
961
|
-
status_code=
|
|
911
|
+
status_code=404,
|
|
962
912
|
)
|
|
963
913
|
inserted_ids = []
|
|
964
914
|
dialect = SupportedSQLDialect(session.bind.dialect.name)
|
|
@@ -982,6 +932,100 @@ async def annotate_spans(
|
|
|
982
932
|
)
|
|
983
933
|
|
|
984
934
|
|
|
935
|
+
class SpanNoteData(V1RoutesBaseModel):
|
|
936
|
+
span_id: Annotated[str, BeforeValidator(lambda v: v.strip() if isinstance(v, str) else v)] = (
|
|
937
|
+
Field(min_length=1, description="OpenTelemetry Span ID (hex format w/o 0x prefix)")
|
|
938
|
+
)
|
|
939
|
+
note: Annotated[str, BeforeValidator(lambda v: v.strip() if isinstance(v, str) else v)] = Field(
|
|
940
|
+
min_length=1, description="The note text to add to the span"
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
class CreateSpanNoteRequestBody(RequestBody[SpanNoteData]):
|
|
945
|
+
data: SpanNoteData
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
class CreateSpanNoteResponseBody(ResponseBody[InsertedSpanAnnotation]):
|
|
949
|
+
pass
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
@router.post(
|
|
953
|
+
"/span_notes",
|
|
954
|
+
dependencies=[Depends(is_not_locked)],
|
|
955
|
+
operation_id="createSpanNote",
|
|
956
|
+
summary="Create a span note",
|
|
957
|
+
description=(
|
|
958
|
+
"Add a note annotation to a span. Notes are special annotations that allow "
|
|
959
|
+
"multiple entries per span (unlike regular annotations which are unique by name "
|
|
960
|
+
"and identifier). Each note gets a unique timestamp-based identifier."
|
|
961
|
+
),
|
|
962
|
+
responses=add_errors_to_responses([{"status_code": 404, "description": "Span not found"}]),
|
|
963
|
+
response_description="Span note created successfully",
|
|
964
|
+
status_code=200,
|
|
965
|
+
)
|
|
966
|
+
async def create_span_note(
|
|
967
|
+
request: Request,
|
|
968
|
+
request_body: CreateSpanNoteRequestBody,
|
|
969
|
+
) -> CreateSpanNoteResponseBody:
|
|
970
|
+
"""
|
|
971
|
+
Create a note annotation for a span.
|
|
972
|
+
|
|
973
|
+
Notes are a special type of annotation that:
|
|
974
|
+
- Have the fixed name "note"
|
|
975
|
+
- Use a timestamp-based identifier to allow multiple notes per span
|
|
976
|
+
- Are always created with annotator_kind="HUMAN" and source="API"
|
|
977
|
+
- Store the note text in the explanation field
|
|
978
|
+
"""
|
|
979
|
+
note_data = request_body.data
|
|
980
|
+
|
|
981
|
+
user_id: Optional[int] = None
|
|
982
|
+
if request.app.state.authentication_enabled and isinstance(request.user, PhoenixUser):
|
|
983
|
+
user_id = int(request.user.identity)
|
|
984
|
+
|
|
985
|
+
async with request.app.state.db() as session:
|
|
986
|
+
# Find the span by OpenTelemetry span_id
|
|
987
|
+
span_rowid = await session.scalar(
|
|
988
|
+
select(models.Span.id).where(models.Span.span_id == note_data.span_id)
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
if span_rowid is None:
|
|
992
|
+
raise HTTPException(
|
|
993
|
+
status_code=404,
|
|
994
|
+
detail=f"Span with ID {note_data.span_id} not found",
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
# Generate a unique identifier for the note using timestamp
|
|
998
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
999
|
+
note_identifier = f"px-span-note:{timestamp}"
|
|
1000
|
+
|
|
1001
|
+
# Create the annotation values
|
|
1002
|
+
values = {
|
|
1003
|
+
"span_rowid": span_rowid,
|
|
1004
|
+
"name": "note",
|
|
1005
|
+
"label": None,
|
|
1006
|
+
"score": None,
|
|
1007
|
+
"explanation": note_data.note,
|
|
1008
|
+
"annotator_kind": "HUMAN",
|
|
1009
|
+
"metadata_": {},
|
|
1010
|
+
"identifier": note_identifier,
|
|
1011
|
+
"source": "API",
|
|
1012
|
+
"user_id": user_id,
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
# Insert the annotation
|
|
1016
|
+
result = await session.execute(
|
|
1017
|
+
sa.insert(models.SpanAnnotation).values(**values).returning(models.SpanAnnotation.id)
|
|
1018
|
+
)
|
|
1019
|
+
annotation_id = result.scalar_one()
|
|
1020
|
+
|
|
1021
|
+
# Put event on queue after successful insert
|
|
1022
|
+
request.state.event_queue.put(SpanAnnotationInsertEvent((annotation_id,)))
|
|
1023
|
+
|
|
1024
|
+
return CreateSpanNoteResponseBody(
|
|
1025
|
+
data=InsertedSpanAnnotation(id=str(GlobalID("SpanAnnotation", str(annotation_id))))
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
|
|
985
1029
|
class CreateSpansRequestBody(RequestBody[list[Span]]):
|
|
986
1030
|
data: list[Span]
|
|
987
1031
|
|
|
@@ -1000,8 +1044,8 @@ class CreateSpansResponseBody(V1RoutesBaseModel):
|
|
|
1000
1044
|
"Submit spans to be inserted into a project. If any spans are invalid or "
|
|
1001
1045
|
"duplicates, no spans will be inserted."
|
|
1002
1046
|
),
|
|
1003
|
-
responses=add_errors_to_responses([
|
|
1004
|
-
status_code=
|
|
1047
|
+
responses=add_errors_to_responses([404, 400]),
|
|
1048
|
+
status_code=202,
|
|
1005
1049
|
)
|
|
1006
1050
|
async def create_spans(
|
|
1007
1051
|
request: Request,
|
|
@@ -1040,6 +1084,7 @@ async def create_spans(
|
|
|
1040
1084
|
# Add back the openinference.span.kind attribute since it's stored separately in the API
|
|
1041
1085
|
attributes = dict(api_span.attributes)
|
|
1042
1086
|
attributes["openinference.span.kind"] = api_span.span_kind
|
|
1087
|
+
attributes = unflatten(attributes.items())
|
|
1043
1088
|
|
|
1044
1089
|
# Create span for insertion - note we ignore the 'id' field as it's server-generated
|
|
1045
1090
|
return SpanForInsertion(
|
|
@@ -1058,8 +1103,23 @@ async def create_spans(
|
|
|
1058
1103
|
conversation=None, # Unused
|
|
1059
1104
|
)
|
|
1060
1105
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1106
|
+
try:
|
|
1107
|
+
id_ = from_global_id_with_expected_type(
|
|
1108
|
+
GlobalID.from_id(project_identifier),
|
|
1109
|
+
"Project",
|
|
1110
|
+
)
|
|
1111
|
+
except Exception:
|
|
1112
|
+
project_name = project_identifier
|
|
1113
|
+
else:
|
|
1114
|
+
stmt = select(models.Project).filter_by(id=id_)
|
|
1115
|
+
async with request.app.state.db() as session:
|
|
1116
|
+
project = await session.scalar(stmt)
|
|
1117
|
+
if project is None:
|
|
1118
|
+
raise HTTPException(
|
|
1119
|
+
status_code=HTTP_404_NOT_FOUND,
|
|
1120
|
+
detail=f"Project with ID {project_identifier} not found",
|
|
1121
|
+
)
|
|
1122
|
+
project_name = project.name
|
|
1063
1123
|
|
|
1064
1124
|
total_received = len(request_body.data)
|
|
1065
1125
|
duplicate_spans: list[dict[str, str]] = []
|
|
@@ -1087,7 +1147,7 @@ async def create_spans(
|
|
|
1087
1147
|
|
|
1088
1148
|
try:
|
|
1089
1149
|
span_for_insertion = convert_api_span_for_insertion(api_span)
|
|
1090
|
-
spans_to_queue.append((span_for_insertion,
|
|
1150
|
+
spans_to_queue.append((span_for_insertion, project_name))
|
|
1091
1151
|
except Exception as e:
|
|
1092
1152
|
invalid_spans.append(
|
|
1093
1153
|
{
|
|
@@ -1109,13 +1169,13 @@ async def create_spans(
|
|
|
1109
1169
|
"invalid_spans": invalid_spans,
|
|
1110
1170
|
}
|
|
1111
1171
|
raise HTTPException(
|
|
1112
|
-
status_code=
|
|
1172
|
+
status_code=400,
|
|
1113
1173
|
detail=json.dumps(error_detail),
|
|
1114
1174
|
)
|
|
1115
1175
|
|
|
1116
1176
|
# All spans are valid, queue them all
|
|
1117
1177
|
for span_for_insertion, project_name in spans_to_queue:
|
|
1118
|
-
await request.state.
|
|
1178
|
+
await request.state.enqueue_span(span_for_insertion, project_name)
|
|
1119
1179
|
|
|
1120
1180
|
return CreateSpansResponseBody(
|
|
1121
1181
|
total_received=total_received,
|
|
@@ -1145,7 +1205,7 @@ async def create_spans(
|
|
|
1145
1205
|
**Note**: This operation is irreversible and may create orphaned spans.
|
|
1146
1206
|
"""
|
|
1147
1207
|
),
|
|
1148
|
-
responses=add_errors_to_responses([
|
|
1208
|
+
responses=add_errors_to_responses([404]),
|
|
1149
1209
|
status_code=204, # No Content for successful deletion
|
|
1150
1210
|
)
|
|
1151
1211
|
async def delete_span(
|
|
@@ -1197,7 +1257,7 @@ async def delete_span(
|
|
|
1197
1257
|
|
|
1198
1258
|
if target_span is None:
|
|
1199
1259
|
raise HTTPException(
|
|
1200
|
-
status_code=
|
|
1260
|
+
status_code=404,
|
|
1201
1261
|
detail=error_detail,
|
|
1202
1262
|
)
|
|
1203
1263
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import gzip
|
|
2
2
|
import zlib
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Optional
|
|
4
4
|
|
|
5
5
|
from fastapi import APIRouter, BackgroundTasks, Depends, Header, HTTPException, Path, Query
|
|
6
6
|
from google.protobuf.message import DecodeError
|
|
@@ -9,48 +9,62 @@ from opentelemetry.proto.collector.trace.v1.trace_service_pb2 import (
|
|
|
9
9
|
ExportTraceServiceResponse,
|
|
10
10
|
)
|
|
11
11
|
from pydantic import Field
|
|
12
|
-
from sqlalchemy import delete,
|
|
12
|
+
from sqlalchemy import delete, select
|
|
13
13
|
from starlette.concurrency import run_in_threadpool
|
|
14
14
|
from starlette.datastructures import State
|
|
15
15
|
from starlette.requests import Request
|
|
16
16
|
from starlette.responses import Response
|
|
17
|
-
from starlette.status import (
|
|
18
|
-
HTTP_404_NOT_FOUND,
|
|
19
|
-
HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
|
20
|
-
HTTP_422_UNPROCESSABLE_ENTITY,
|
|
21
|
-
)
|
|
22
17
|
from strawberry.relay import GlobalID
|
|
23
18
|
|
|
24
19
|
from phoenix.db import models
|
|
25
|
-
from phoenix.db.
|
|
26
|
-
from phoenix.db.insertion.
|
|
20
|
+
from phoenix.db.helpers import SupportedSQLDialect
|
|
21
|
+
from phoenix.db.insertion.helpers import as_kv, insert_on_conflict
|
|
22
|
+
from phoenix.server.api.routers.v1.annotations import TraceAnnotationData
|
|
27
23
|
from phoenix.server.api.types.node import from_global_id_with_expected_type
|
|
28
24
|
from phoenix.server.authorization import is_not_locked
|
|
29
25
|
from phoenix.server.bearer_auth import PhoenixUser
|
|
30
26
|
from phoenix.server.dml_event import SpanDeleteEvent, TraceAnnotationInsertEvent
|
|
27
|
+
from phoenix.server.prometheus import SPAN_QUEUE_REJECTIONS
|
|
31
28
|
from phoenix.trace.otel import decode_otlp_span
|
|
32
29
|
from phoenix.utilities.project import get_project_name
|
|
33
30
|
|
|
34
31
|
from .models import V1RoutesBaseModel
|
|
35
|
-
from .utils import
|
|
32
|
+
from .utils import (
|
|
33
|
+
RequestBody,
|
|
34
|
+
ResponseBody,
|
|
35
|
+
add_errors_to_responses,
|
|
36
|
+
)
|
|
36
37
|
|
|
37
38
|
router = APIRouter(tags=["traces"])
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
def is_not_at_capacity(request: Request) -> None:
|
|
42
|
+
if request.app.state.span_queue_is_full():
|
|
43
|
+
SPAN_QUEUE_REJECTIONS.inc()
|
|
44
|
+
raise HTTPException(
|
|
45
|
+
detail="Server is at capacity and cannot process more requests",
|
|
46
|
+
status_code=503,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
40
50
|
@router.post(
|
|
41
51
|
"/traces",
|
|
42
|
-
dependencies=[Depends(is_not_locked)],
|
|
52
|
+
dependencies=[Depends(is_not_locked), Depends(is_not_at_capacity)],
|
|
43
53
|
operation_id="addTraces",
|
|
44
54
|
summary="Send traces",
|
|
45
55
|
responses=add_errors_to_responses(
|
|
46
56
|
[
|
|
47
57
|
{
|
|
48
|
-
"status_code":
|
|
58
|
+
"status_code": 415,
|
|
49
59
|
"description": (
|
|
50
60
|
"Unsupported content type (only `application/x-protobuf` is supported)"
|
|
51
61
|
),
|
|
52
62
|
},
|
|
53
|
-
{"status_code":
|
|
63
|
+
{"status_code": 422, "description": "Invalid request body"},
|
|
64
|
+
{
|
|
65
|
+
"status_code": 503,
|
|
66
|
+
"description": "Server is at capacity and cannot process more requests",
|
|
67
|
+
},
|
|
54
68
|
]
|
|
55
69
|
),
|
|
56
70
|
openapi_extra={
|
|
@@ -72,12 +86,12 @@ async def post_traces(
|
|
|
72
86
|
if content_type != "application/x-protobuf":
|
|
73
87
|
raise HTTPException(
|
|
74
88
|
detail=f"Unsupported content type: {content_type}",
|
|
75
|
-
status_code=
|
|
89
|
+
status_code=415,
|
|
76
90
|
)
|
|
77
91
|
if content_encoding and content_encoding not in ("gzip", "deflate"):
|
|
78
92
|
raise HTTPException(
|
|
79
93
|
detail=f"Unsupported content encoding: {content_encoding}",
|
|
80
|
-
status_code=
|
|
94
|
+
status_code=415,
|
|
81
95
|
)
|
|
82
96
|
body = await request.body()
|
|
83
97
|
if content_encoding == "gzip":
|
|
@@ -90,7 +104,7 @@ async def post_traces(
|
|
|
90
104
|
except DecodeError:
|
|
91
105
|
raise HTTPException(
|
|
92
106
|
detail="Request body is invalid ExportTraceServiceRequest",
|
|
93
|
-
status_code=
|
|
107
|
+
status_code=422,
|
|
94
108
|
)
|
|
95
109
|
background_tasks.add_task(_add_spans, req, request.state)
|
|
96
110
|
|
|
@@ -104,53 +118,8 @@ async def post_traces(
|
|
|
104
118
|
)
|
|
105
119
|
|
|
106
120
|
|
|
107
|
-
class
|
|
108
|
-
|
|
109
|
-
score: Optional[float] = Field(default=None, description="The score assigned by the annotation")
|
|
110
|
-
explanation: Optional[str] = Field(
|
|
111
|
-
default=None, description="Explanation of the annotation result"
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class TraceAnnotation(V1RoutesBaseModel):
|
|
116
|
-
trace_id: str = Field(description="OpenTelemetry Trace ID (hex format w/o 0x prefix)")
|
|
117
|
-
name: str = Field(description="The name of the annotation")
|
|
118
|
-
annotator_kind: Literal["LLM", "HUMAN"] = Field(
|
|
119
|
-
description="The kind of annotator used for the annotation"
|
|
120
|
-
)
|
|
121
|
-
result: Optional[TraceAnnotationResult] = Field(
|
|
122
|
-
default=None, description="The result of the annotation"
|
|
123
|
-
)
|
|
124
|
-
metadata: Optional[dict[str, Any]] = Field(
|
|
125
|
-
default=None, description="Metadata for the annotation"
|
|
126
|
-
)
|
|
127
|
-
identifier: str = Field(
|
|
128
|
-
default="",
|
|
129
|
-
description=(
|
|
130
|
-
"The identifier of the annotation. "
|
|
131
|
-
"If provided, the annotation will be updated if it already exists."
|
|
132
|
-
),
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
def as_precursor(self, *, user_id: Optional[int] = None) -> Precursors.TraceAnnotation:
|
|
136
|
-
return Precursors.TraceAnnotation(
|
|
137
|
-
self.trace_id,
|
|
138
|
-
models.TraceAnnotation(
|
|
139
|
-
name=self.name,
|
|
140
|
-
annotator_kind=self.annotator_kind,
|
|
141
|
-
score=self.result.score if self.result else None,
|
|
142
|
-
label=self.result.label if self.result else None,
|
|
143
|
-
explanation=self.result.explanation if self.result else None,
|
|
144
|
-
metadata_=self.metadata or {},
|
|
145
|
-
identifier=self.identifier,
|
|
146
|
-
source="APP",
|
|
147
|
-
user_id=user_id,
|
|
148
|
-
),
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
class AnnotateTracesRequestBody(RequestBody[list[TraceAnnotation]]):
|
|
153
|
-
data: list[TraceAnnotation] = Field(description="The trace annotations to be upserted")
|
|
121
|
+
class AnnotateTracesRequestBody(RequestBody[list[TraceAnnotationData]]):
|
|
122
|
+
data: list[TraceAnnotationData] = Field(description="The trace annotations to be upserted")
|
|
154
123
|
|
|
155
124
|
|
|
156
125
|
class InsertedTraceAnnotation(V1RoutesBaseModel):
|
|
@@ -166,15 +135,12 @@ class AnnotateTracesResponseBody(ResponseBody[list[InsertedTraceAnnotation]]):
|
|
|
166
135
|
dependencies=[Depends(is_not_locked)],
|
|
167
136
|
operation_id="annotateTraces",
|
|
168
137
|
summary="Create trace annotations",
|
|
169
|
-
responses=add_errors_to_responses(
|
|
170
|
-
[{"status_code": HTTP_404_NOT_FOUND, "description": "Trace not found"}]
|
|
171
|
-
),
|
|
172
|
-
include_in_schema=False,
|
|
138
|
+
responses=add_errors_to_responses([{"status_code": 404, "description": "Trace not found"}]),
|
|
173
139
|
)
|
|
174
140
|
async def annotate_traces(
|
|
175
141
|
request: Request,
|
|
176
142
|
request_body: AnnotateTracesRequestBody,
|
|
177
|
-
sync: bool = Query(default=
|
|
143
|
+
sync: bool = Query(default=False, description="If true, fulfill request synchronously."),
|
|
178
144
|
) -> AnnotateTracesResponseBody:
|
|
179
145
|
if not request_body.data:
|
|
180
146
|
return AnnotateTracesResponseBody(data=[])
|
|
@@ -185,15 +151,17 @@ async def annotate_traces(
|
|
|
185
151
|
|
|
186
152
|
precursors = [d.as_precursor(user_id=user_id) for d in request_body.data]
|
|
187
153
|
if not sync:
|
|
188
|
-
await request.state.
|
|
154
|
+
await request.state.enqueue_annotations(*precursors)
|
|
189
155
|
return AnnotateTracesResponseBody(data=[])
|
|
190
156
|
|
|
191
157
|
trace_ids = {p.trace_id for p in precursors}
|
|
192
158
|
async with request.app.state.db() as session:
|
|
193
159
|
existing_traces = {
|
|
194
|
-
|
|
195
|
-
async for
|
|
196
|
-
select(models.Trace
|
|
160
|
+
trace_id: id_
|
|
161
|
+
async for trace_id, id_ in await session.stream(
|
|
162
|
+
select(models.Trace.trace_id, models.Trace.id).filter(
|
|
163
|
+
models.Trace.trace_id.in_(trace_ids)
|
|
164
|
+
)
|
|
197
165
|
)
|
|
198
166
|
}
|
|
199
167
|
|
|
@@ -201,13 +169,19 @@ async def annotate_traces(
|
|
|
201
169
|
if missing_trace_ids:
|
|
202
170
|
raise HTTPException(
|
|
203
171
|
detail=f"Traces with IDs {', '.join(missing_trace_ids)} do not exist.",
|
|
204
|
-
status_code=
|
|
172
|
+
status_code=404,
|
|
205
173
|
)
|
|
206
174
|
inserted_ids = []
|
|
175
|
+
dialect = SupportedSQLDialect(session.bind.dialect.name)
|
|
207
176
|
for p in precursors:
|
|
208
177
|
values = dict(as_kv(p.as_insertable(existing_traces[p.trace_id]).row))
|
|
209
178
|
trace_annotation_id = await session.scalar(
|
|
210
|
-
|
|
179
|
+
insert_on_conflict(
|
|
180
|
+
values,
|
|
181
|
+
dialect=dialect,
|
|
182
|
+
table=models.TraceAnnotation,
|
|
183
|
+
unique_by=("name", "trace_rowid", "identifier"),
|
|
184
|
+
).returning(models.TraceAnnotation.id)
|
|
211
185
|
)
|
|
212
186
|
inserted_ids.append(trace_annotation_id)
|
|
213
187
|
request.state.event_queue.put(TraceAnnotationInsertEvent(tuple(inserted_ids)))
|
|
@@ -225,7 +199,7 @@ async def _add_spans(req: ExportTraceServiceRequest, state: State) -> None:
|
|
|
225
199
|
for scope_span in resource_spans.scope_spans:
|
|
226
200
|
for otlp_span in scope_span.spans:
|
|
227
201
|
span = await run_in_threadpool(decode_otlp_span, otlp_span)
|
|
228
|
-
await state.
|
|
202
|
+
await state.enqueue_span(span, project_name)
|
|
229
203
|
|
|
230
204
|
|
|
231
205
|
@router.delete(
|
|
@@ -238,7 +212,7 @@ async def _add_spans(req: ExportTraceServiceRequest, state: State) -> None:
|
|
|
238
212
|
"2. An OpenTelemetry trace_id (hex string)\n\n"
|
|
239
213
|
"This will permanently remove all spans in the trace and their associated data."
|
|
240
214
|
),
|
|
241
|
-
responses=add_errors_to_responses([
|
|
215
|
+
responses=add_errors_to_responses([404]),
|
|
242
216
|
status_code=204, # No Content for successful deletion
|
|
243
217
|
)
|
|
244
218
|
async def delete_trace(
|
|
@@ -286,7 +260,7 @@ async def delete_trace(
|
|
|
286
260
|
|
|
287
261
|
if project_id is None:
|
|
288
262
|
raise HTTPException(
|
|
289
|
-
status_code=
|
|
263
|
+
status_code=404,
|
|
290
264
|
detail=error_detail,
|
|
291
265
|
)
|
|
292
266
|
|