From a012ea6cb8a7586bd47f673c0be6487fc7c45497 Mon Sep 17 00:00:00 2001 From: Thiago dos Santos Hora Date: Wed, 18 Dec 2024 10:16:41 +0100 Subject: [PATCH] [OPIK-446] Add duration to trace and span (#883) * [OPIK-446] Add durartion to trace_span * Fix calc * Fix pr review * OPIK-446 remove spans calculated duration * OPIK-446 remove traces calculated duration * OPIK-446 changed DurationUtils to be a test utility * OPIK-446 changed query builder to use the materialized column --------- Co-authored-by: Ido Berkovich --- .../main/java/com/comet/opik/api/Span.java | 4 +- .../main/java/com/comet/opik/api/Trace.java | 4 +- .../java/com/comet/opik/api/filter/Field.java | 1 + .../com/comet/opik/api/filter/SpanField.java | 2 +- .../com/comet/opik/api/filter/TraceField.java | 2 +- .../api/resources/v1/priv/SpansResource.java | 2 +- .../api/resources/v1/priv/TracesResource.java | 2 +- .../java/com/comet/opik/domain/SpanDAO.java | 13 +- .../com/comet/opik/domain/SpanMapper.java | 1 + .../java/com/comet/opik/domain/TraceDAO.java | 21 +- .../domain/filter/FilterQueryBuilder.java | 5 + .../000009_add_duration_columns.sql | 19 + .../api/resources/utils/DurationUtils.java | 23 ++ .../resources/v1/priv/SpansResourceTest.java | 377 ++++++++++++++---- .../resources/v1/priv/TracesResourceTest.java | 364 ++++++++++++++--- 15 files changed, 670 insertions(+), 170 deletions(-) create mode 100644 apps/opik-backend/src/main/resources/liquibase/db-app-analytics/migrations/000009_add_duration_columns.sql create mode 100644 apps/opik-backend/src/test/java/com/comet/opik/api/resources/utils/DurationUtils.java diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/Span.java b/apps/opik-backend/src/main/java/com/comet/opik/api/Span.java index 3b4350a1d0..2ebc3b1cd9 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/Span.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/Span.java @@ -52,7 +52,9 @@ public record Span( @JsonView({ Span.View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) List feedbackScores, @JsonView({ - Span.View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) BigDecimal totalEstimatedCost){ + Span.View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) BigDecimal totalEstimatedCost, + @JsonView({ + Span.View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY, description = "Duration in milliseconds as a decimal number to support sub-millisecond precision") Double duration){ public record SpanPage( @JsonView(Span.View.Public.class) int page, diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/Trace.java b/apps/opik-backend/src/main/java/com/comet/opik/api/Trace.java index 05ea345e7c..b5f04a696a 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/Trace.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/Trace.java @@ -46,7 +46,9 @@ public record Trace( @JsonView({ Trace.View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) List feedbackScores, @JsonView({ - Trace.View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) BigDecimal totalEstimatedCost){ + Trace.View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY) BigDecimal totalEstimatedCost, + @JsonView({ + Trace.View.Public.class}) @Schema(accessMode = Schema.AccessMode.READ_ONLY, description = "Duration in milliseconds as a decimal number to support sub-millisecond precision") Double duration){ public record TracePage( @JsonView(Trace.View.Public.class) int page, diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/filter/Field.java b/apps/opik-backend/src/main/java/com/comet/opik/api/filter/Field.java index 18dfd78fdb..1c38cd2e97 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/filter/Field.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/filter/Field.java @@ -20,6 +20,7 @@ public interface Field { String USAGE_PROMPT_TOKENS_QUERY_PARAM = "usage.prompt_tokens"; String USAGE_TOTAL_TOKEN_QUERY_PARAMS = "usage.total_tokens"; String FEEDBACK_SCORES_QUERY_PARAM = "feedback_scores"; + String DURATION_QUERY_PARAM = "duration"; @JsonValue String getQueryParamField(); diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/filter/SpanField.java b/apps/opik-backend/src/main/java/com/comet/opik/api/filter/SpanField.java index fa15059ada..a1bd03ce54 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/filter/SpanField.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/filter/SpanField.java @@ -21,7 +21,7 @@ public enum SpanField implements Field { USAGE_PROMPT_TOKENS(USAGE_PROMPT_TOKENS_QUERY_PARAM, FieldType.NUMBER), USAGE_TOTAL_TOKENS(USAGE_TOTAL_TOKEN_QUERY_PARAMS, FieldType.NUMBER), FEEDBACK_SCORES(FEEDBACK_SCORES_QUERY_PARAM, FieldType.FEEDBACK_SCORES_NUMBER), - ; + DURATION(DURATION_QUERY_PARAM, FieldType.NUMBER); private final String queryParamField; private final FieldType type; diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/filter/TraceField.java b/apps/opik-backend/src/main/java/com/comet/opik/api/filter/TraceField.java index 9966088fc6..94457316c4 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/filter/TraceField.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/filter/TraceField.java @@ -19,7 +19,7 @@ public enum TraceField implements Field { USAGE_PROMPT_TOKENS(USAGE_PROMPT_TOKENS_QUERY_PARAM, FieldType.NUMBER), USAGE_TOTAL_TOKENS(USAGE_TOTAL_TOKEN_QUERY_PARAMS, FieldType.NUMBER), FEEDBACK_SCORES(FEEDBACK_SCORES_QUERY_PARAM, FieldType.FEEDBACK_SCORES_NUMBER), - ; + DURATION(DURATION_QUERY_PARAM, FieldType.NUMBER); private final String queryParamField; private final FieldType type; diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/SpansResource.java b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/SpansResource.java index f789f2f0e1..7c9ab3a947 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/SpansResource.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/SpansResource.java @@ -79,7 +79,7 @@ public class SpansResource { @Operation(operationId = "getSpansByProject", summary = "Get spans by project_name or project_id and optionally by trace_id and/or type", description = "Get spans by project_name or project_id and optionally by trace_id and/or type", responses = { @ApiResponse(responseCode = "200", description = "Spans resource", content = @Content(schema = @Schema(implementation = SpanPage.class)))}) @JsonView(Span.View.Public.class) - public Response getByProjectId( + public Response getSpansByProject( @QueryParam("page") @Min(1) @DefaultValue("1") int page, @QueryParam("size") @Min(1) @DefaultValue("10") int size, @QueryParam("project_name") String projectName, diff --git a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/TracesResource.java b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/TracesResource.java index 9b50b4e053..5150619935 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/TracesResource.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/api/resources/v1/priv/TracesResource.java @@ -78,7 +78,7 @@ public class TracesResource { @Operation(operationId = "getTracesByProject", summary = "Get traces by project_name or project_id", description = "Get traces by project_name or project_id", responses = { @ApiResponse(responseCode = "200", description = "Trace resource", content = @Content(schema = @Schema(implementation = TracePage.class)))}) @JsonView(Trace.View.Public.class) - public Response getByProjectId( + public Response getTracesByProject( @QueryParam("page") @Min(1) @DefaultValue("1") int page, @QueryParam("size") @Min(1) @DefaultValue("10") int size, @QueryParam("project_name") String projectName, diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanDAO.java index 4ce83b2a69..2cea7a059f 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanDAO.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanDAO.java @@ -478,7 +478,8 @@ LEFT JOIN ( private static final String SELECT_BY_ID = """ SELECT - * + *, + duration_millis FROM spans WHERE id = :id @@ -511,7 +512,8 @@ LEFT JOIN ( created_at, last_updated_at, created_by, - last_updated_by + last_updated_by, + duration_millis FROM spans WHERE project_id = :project_id AND workspace_id = :workspace_id @@ -607,7 +609,7 @@ AND id in ( SELECT project_id as project_id, count(DISTINCT span_id) as span_count, - arrayMap(v -> if(isNaN(v), 0, toDecimal64(v / 1000.0, 9)), quantiles(0.5, 0.9, 0.99)(duration)) AS duration, + arrayMap(v -> toDecimal64(if(isNaN(v), 0, v), 9), quantiles(0.5, 0.9, 0.99)(duration)) AS duration, sum(input_count) as input, sum(output_count) as output, sum(metadata_count) as metadata, @@ -621,7 +623,7 @@ AND id in ( s.workspace_id as workspace_id, s.project_id as project_id, s.id as span_id, - s.duration as duration, + s.duration_millis as duration, s.input_count as input_count, s.output_count as output_count, s.metadata_count as metadata_count, @@ -634,7 +636,7 @@ AND id in ( workspace_id, project_id, id, - if(end_time IS NOT NULL, date_diff('microsecond', start_time, end_time), null) as duration, + duration_millis, if(length(input) > 0, 1, 0) as input_count, if(length(output) > 0, 1, 0) as output_count, if(length(metadata) > 0, 1, 0) as metadata_count, @@ -1094,6 +1096,7 @@ private Publisher mapToDto(Result result) { .lastUpdatedAt(row.get("last_updated_at", Instant.class)) .createdBy(row.get("created_by", String.class)) .lastUpdatedBy(row.get("last_updated_by", String.class)) + .duration(row.get("duration_millis", Double.class)) .build(); }); } diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanMapper.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanMapper.java index a766adb76c..c71fd01ab1 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanMapper.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/SpanMapper.java @@ -26,5 +26,6 @@ public interface SpanMapper { void updateSpanModelBuilder(@MappingTarget SpanModel.SpanModelBuilder spanModelBuilder, SpanUpdate spanUpdate); @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + @Mapping(target = "duration", ignore = true) void updateSpanBuilder(@MappingTarget Span.SpanBuilder spanBuilder, SpanUpdate spanUpdate); } diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceDAO.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceDAO.java index 41557b8b08..66b69bad40 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceDAO.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/TraceDAO.java @@ -267,11 +267,13 @@ INSERT INTO traces ( private static final String SELECT_BY_ID = """ SELECT t.*, + t.duration_millis, sumMap(s.usage) as usage, sum(s.total_estimated_cost) as total_estimated_cost FROM ( SELECT - * + *, + duration_millis FROM traces WHERE workspace_id = :workspace_id AND id = :id @@ -290,7 +292,8 @@ LEFT JOIN ( LIMIT 1 BY id ) AS s ON t.id = s.trace_id GROUP BY - t.* + t.*, + duration_millis ORDER BY t.id DESC ; """; @@ -298,6 +301,7 @@ LEFT JOIN ( private static final String SELECT_BY_PROJECT_ID = """ SELECT t.*, + t.duration_millis, sumMap(s.usage) as usage, sum(s.total_estimated_cost) as total_estimated_cost FROM ( @@ -316,7 +320,8 @@ LEFT JOIN ( created_at, last_updated_at, created_by, - last_updated_by + last_updated_by, + duration_millis FROM traces WHERE project_id = :project_id AND workspace_id = :workspace_id @@ -354,7 +359,8 @@ LEFT JOIN ( LIMIT 1 BY id ) AS s ON t.id = s.trace_id GROUP BY - t.* + t.*, + t.duration_millis HAVING @@ -587,7 +593,7 @@ LEFT JOIN ( SELECT project_id as project_id, count(DISTINCT trace_id) as trace_count, - arrayMap(v -> if(isNaN(v), 0, toDecimal64(v / 1000.0, 9)), quantiles(0.5, 0.9, 0.99)(duration)) AS duration, + arrayMap(v -> toDecimal64(if(isNaN(v), 0, v), 9), quantiles(0.5, 0.9, 0.99)(duration)) AS duration, sum(input_count) as input, sum(output_count) as output, sum(metadata_count) as metadata, @@ -601,7 +607,7 @@ LEFT JOIN ( t.workspace_id as workspace_id, t.project_id as project_id, t.id as trace_id, - t.duration as duration, + t.duration_millis as duration, t.input_count as input_count, t.output_count as output_count, t.metadata_count as metadata_count, @@ -614,7 +620,7 @@ LEFT JOIN ( workspace_id, project_id, id, - if(end_time IS NOT NULL, date_diff('microsecond', start_time, end_time), null) as duration, + duration_millis, if(length(input) > 0, 1, 0) as input_count, if(length(output) > 0, 1, 0) as output_count, if(length(metadata) > 0, 1, 0) as metadata_count, @@ -934,6 +940,7 @@ private Publisher mapToDto(Result result) { .lastUpdatedAt(row.get("last_updated_at", Instant.class)) .createdBy(row.get("created_by", String.class)) .lastUpdatedBy(row.get("last_updated_by", String.class)) + .duration(row.get("duration_millis", Double.class)) .build()); } diff --git a/apps/opik-backend/src/main/java/com/comet/opik/domain/filter/FilterQueryBuilder.java b/apps/opik-backend/src/main/java/com/comet/opik/domain/filter/FilterQueryBuilder.java index 2fac76f528..8edad0657d 100644 --- a/apps/opik-backend/src/main/java/com/comet/opik/domain/filter/FilterQueryBuilder.java +++ b/apps/opik-backend/src/main/java/com/comet/opik/domain/filter/FilterQueryBuilder.java @@ -42,6 +42,7 @@ public class FilterQueryBuilder { private static final String USAGE_PROMPT_TOKENS_ANALYTICS_DB = "usage['prompt_tokens']"; private static final String USAGE_TOTAL_TOKENS_ANALYTICS_DB = "usage['total_tokens']"; private static final String VALUE_ANALYTICS_DB = "value"; + private static final String DURATION_ANALYTICS_DB = "duration_millis"; private static final Map> ANALYTICS_DB_OPERATOR_MAP = new EnumMap<>(Map.of( Operator.CONTAINS, new EnumMap<>(Map.of( @@ -112,6 +113,7 @@ public class FilterQueryBuilder { .put(TraceField.USAGE_PROMPT_TOKENS, USAGE_PROMPT_TOKENS_ANALYTICS_DB) .put(TraceField.USAGE_TOTAL_TOKENS, USAGE_TOTAL_TOKENS_ANALYTICS_DB) .put(TraceField.FEEDBACK_SCORES, VALUE_ANALYTICS_DB) + .put(TraceField.DURATION, DURATION_ANALYTICS_DB) .build()); private static final Map SPAN_FIELDS_MAP = new EnumMap<>( @@ -131,6 +133,7 @@ public class FilterQueryBuilder { .put(SpanField.USAGE_PROMPT_TOKENS, USAGE_PROMPT_TOKENS_ANALYTICS_DB) .put(SpanField.USAGE_TOTAL_TOKENS, USAGE_TOTAL_TOKENS_ANALYTICS_DB) .put(SpanField.FEEDBACK_SCORES, VALUE_ANALYTICS_DB) + .put(SpanField.DURATION, DURATION_ANALYTICS_DB) .build()); private static final Map EXPERIMENTS_COMPARISON_FIELDS_MAP = new EnumMap<>( @@ -149,6 +152,7 @@ public class FilterQueryBuilder { .add(TraceField.OUTPUT) .add(TraceField.METADATA) .add(TraceField.TAGS) + .add(TraceField.DURATION) .build()), FilterStrategy.TRACE_AGGREGATION, EnumSet.copyOf(ImmutableSet.builder() .add(TraceField.USAGE_COMPLETION_TOKENS) @@ -171,6 +175,7 @@ public class FilterQueryBuilder { .add(SpanField.USAGE_COMPLETION_TOKENS) .add(SpanField.USAGE_PROMPT_TOKENS) .add(SpanField.USAGE_TOTAL_TOKENS) + .add(SpanField.DURATION) .build()), FilterStrategy.FEEDBACK_SCORES, ImmutableSet.builder() .add(TraceField.FEEDBACK_SCORES) diff --git a/apps/opik-backend/src/main/resources/liquibase/db-app-analytics/migrations/000009_add_duration_columns.sql b/apps/opik-backend/src/main/resources/liquibase/db-app-analytics/migrations/000009_add_duration_columns.sql new file mode 100644 index 0000000000..bae646985f --- /dev/null +++ b/apps/opik-backend/src/main/resources/liquibase/db-app-analytics/migrations/000009_add_duration_columns.sql @@ -0,0 +1,19 @@ +--liquibase formatted sql +--changeset idoberko2:add_duration_columns + +ALTER TABLE ${ANALYTICS_DB_DATABASE_NAME}.spans + ADD COLUMN IF NOT EXISTS duration_millis Nullable(Float64) MATERIALIZED + if(end_time IS NOT NULL AND start_time IS NOT NULL + AND notEquals(start_time, toDateTime64('1970-01-01 00:00:00.000', 9)), + (dateDiff('microsecond', start_time, end_time) / 1000.0), + NULL); + +ALTER TABLE ${ANALYTICS_DB_DATABASE_NAME}.traces + ADD COLUMN IF NOT EXISTS duration_millis Nullable(Float64) MATERIALIZED + if(end_time IS NOT NULL AND start_time IS NOT NULL + AND notEquals(start_time, toDateTime64('1970-01-01 00:00:00.000', 9)), + (dateDiff('microsecond', start_time, end_time) / 1000.0), + NULL); + +--rollback ALTER TABLE ${ANALYTICS_DB_DATABASE_NAME}.spans DROP COLUMN IF EXISTS duration_millis; +--rollback ALTER TABLE ${ANALYTICS_DB_DATABASE_NAME}.traces DROP COLUMN IF EXISTS duration_millis; \ No newline at end of file diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/utils/DurationUtils.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/utils/DurationUtils.java new file mode 100644 index 0000000000..11c3d9fcb9 --- /dev/null +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/utils/DurationUtils.java @@ -0,0 +1,23 @@ +package com.comet.opik.api.resources.utils; + +import lombok.NonNull; +import lombok.experimental.UtilityClass; + +import java.time.Duration; +import java.time.Instant; + +@UtilityClass +public class DurationUtils { + + public static final Double TIME_UNIT = 1_000.0; + + public static Double getDurationInMillisWithSubMilliPrecision(@NonNull Instant startTime, Instant endTime) { + if (endTime == null) { + return null; + } + + long micros = Duration.between(startTime, endTime).toNanos() / TIME_UNIT.longValue(); + return micros / TIME_UNIT; + } + +} diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java index 7a97b1b54e..6cb42d1055 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/SpansResourceTest.java @@ -22,6 +22,7 @@ import com.comet.opik.api.resources.utils.AuthTestUtils; import com.comet.opik.api.resources.utils.ClickHouseContainerUtils; import com.comet.opik.api.resources.utils.ClientSupportUtils; +import com.comet.opik.api.resources.utils.DurationUtils; import com.comet.opik.api.resources.utils.MigrationUtils; import com.comet.opik.api.resources.utils.MySQLContainerUtils; import com.comet.opik.api.resources.utils.RedisContainerUtils; @@ -38,7 +39,6 @@ import com.comet.opik.infrastructure.auth.RequestContext; import com.comet.opik.podam.PodamFactoryUtils; import com.comet.opik.utils.JsonUtils; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -82,7 +82,9 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.sql.SQLException; +import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -92,6 +94,7 @@ import java.util.UUID; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -114,6 +117,7 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; import static org.junit.jupiter.params.provider.Arguments.arguments; @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -121,7 +125,7 @@ class SpansResourceTest { public static final String URL_TEMPLATE = "%s/v1/private/spans"; public static final String[] IGNORED_FIELDS = {"projectId", "projectName", "createdAt", - "lastUpdatedAt", "feedbackScores", "createdBy", "lastUpdatedBy", "totalEstimatedCost"}; + "lastUpdatedAt", "feedbackScores", "createdBy", "lastUpdatedBy", "totalEstimatedCost", "duration"}; public static final String[] IGNORED_FIELDS_SCORES = {"createdAt", "lastUpdatedAt", "createdBy", "lastUpdatedBy"}; public static final String API_KEY = UUID.randomUUID().toString(); @@ -1029,7 +1033,7 @@ void createAndGetByProjectIdAndTraceIdAndType() { } @Test - void getByProjectName__whenFilterIdAndNameEqual__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterIdAndNameEqual__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1069,7 +1073,7 @@ void getByProjectName__whenFilterIdAndNameEqual__thenReturnSpansFiltered() { @ParameterizedTest @MethodSource - void getByProjectName__whenFilterByCorrespondingField__thenReturnSpansFiltered(SpanField filterField, + void getSpansByProject__whenFilterByCorrespondingField__thenReturnSpansFiltered(SpanField filterField, Operator filterOperator, String filterValue) { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); @@ -1115,7 +1119,7 @@ void getByProjectName__whenFilterByCorrespondingField__thenReturnSpansFiltered(S @ParameterizedTest @MethodSource("equalAndNotEqualFilters") - void getByProjectName__whenFilterTotalEstimatedCostEqual_NotEqual__thenReturnSpansFiltered(Operator operator, + void getSpansByProject__whenFilterTotalEstimatedCostEqual_NotEqual__thenReturnSpansFiltered(Operator operator, Function, List> getUnexpectedSpans, Function, List> getExpectedSpans) { String workspaceName = UUID.randomUUID().toString(); @@ -1152,7 +1156,7 @@ void getByProjectName__whenFilterTotalEstimatedCostEqual_NotEqual__thenReturnSpa apiKey); } - static Stream getByProjectName__whenFilterByCorrespondingField__thenReturnSpansFiltered() { + static Stream getSpansByProject__whenFilterByCorrespondingField__thenReturnSpansFiltered() { return Stream.of( Arguments.of(SpanField.TOTAL_ESTIMATED_COST, Operator.GREATER_THAN, "0"), Arguments.of(SpanField.MODEL, Operator.EQUAL, "gpt-3.5-turbo-1106"), @@ -1161,7 +1165,7 @@ static Stream getByProjectName__whenFilterByCorrespondingField__thenR @ParameterizedTest @MethodSource("equalAndNotEqualFilters") - void getByProjectName__whenFilterNameEqual_NotEqual__thenReturnSpansFiltered(Operator operator, + void getSpansByProject__whenFilterNameEqual_NotEqual__thenReturnSpansFiltered(Operator operator, Function, List> getExpectedSpans, Function, List> getUnexpectedSpans) { String workspaceName = UUID.randomUUID().toString(); @@ -1203,7 +1207,7 @@ private Stream equalAndNotEqualFilters() { } @Test - void getByProjectName__whenFilterNameStartsWith__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterNameStartsWith__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1236,7 +1240,7 @@ void getByProjectName__whenFilterNameStartsWith__thenReturnSpansFiltered() { } @Test - void getByProjectName__whenFilterNameEndsWith__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterNameEndsWith__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1269,7 +1273,7 @@ void getByProjectName__whenFilterNameEndsWith__thenReturnSpansFiltered() { } @Test - void getByProjectName__whenFilterNameContains__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterNameContains__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1302,7 +1306,7 @@ void getByProjectName__whenFilterNameContains__thenReturnSpansFiltered() { } @Test - void getByProjectName__whenFilterNameNotContains__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterNameNotContains__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1341,7 +1345,7 @@ void getByProjectName__whenFilterNameNotContains__thenReturnSpansFiltered() { @ParameterizedTest @MethodSource("equalAndNotEqualFilters") - void getByProjectName__whenFilterStartTimeEqual_NotEqual__thenReturnSpansFiltered(Operator operator, + void getSpansByProject__whenFilterStartTimeEqual_NotEqual__thenReturnSpansFiltered(Operator operator, Function, List> getExpectedSpans, Function, List> getUnexpectedSpans) { String workspaceName = UUID.randomUUID().toString(); @@ -1373,7 +1377,7 @@ void getByProjectName__whenFilterStartTimeEqual_NotEqual__thenReturnSpansFiltere } @Test - void getByProjectName__whenFilterStartTimeGreaterThan__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterStartTimeGreaterThan__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1410,7 +1414,7 @@ void getByProjectName__whenFilterStartTimeGreaterThan__thenReturnSpansFiltered() } @Test - void getByProjectName__whenFilterStartTimeGreaterThanEqual__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterStartTimeGreaterThanEqual__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1447,7 +1451,7 @@ void getByProjectName__whenFilterStartTimeGreaterThanEqual__thenReturnSpansFilte } @Test - void getByProjectName__whenFilterStartTimeLessThan__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterStartTimeLessThan__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1484,7 +1488,7 @@ void getByProjectName__whenFilterStartTimeLessThan__thenReturnSpansFiltered() { } @Test - void getByProjectName__whenFilterStartTimeLessThanEqual__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterStartTimeLessThanEqual__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1521,7 +1525,7 @@ void getByProjectName__whenFilterStartTimeLessThanEqual__thenReturnSpansFiltered } @Test - void getByProjectName__whenFilterEndTimeEqual__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterEndTimeEqual__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1554,7 +1558,7 @@ void getByProjectName__whenFilterEndTimeEqual__thenReturnSpansFiltered() { } @Test - void getByProjectName__whenFilterInputEqual__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterInputEqual__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1587,7 +1591,7 @@ void getByProjectName__whenFilterInputEqual__thenReturnSpansFiltered() { } @Test - void getByProjectName__whenFilterOutputEqual__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterOutputEqual__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1621,7 +1625,7 @@ void getByProjectName__whenFilterOutputEqual__thenReturnSpansFiltered() { @ParameterizedTest @MethodSource("equalAndNotEqualFilters") - void getByProjectName__whenFilterMetadataEqualString__thenReturnSpansFiltered(Operator operator, + void getSpansByProject__whenFilterMetadataEqualString__thenReturnSpansFiltered(Operator operator, Function, List> getExpectedSpans, Function, List> getUnexpectedSpans) { String workspaceName = UUID.randomUUID().toString(); @@ -1660,7 +1664,7 @@ void getByProjectName__whenFilterMetadataEqualString__thenReturnSpansFiltered(Op } @Test - void getByProjectName__whenFilterMetadataEqualNumber__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataEqualNumber__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1700,7 +1704,7 @@ void getByProjectName__whenFilterMetadataEqualNumber__thenReturnSpansFiltered() } @Test - void getByProjectName__whenFilterMetadataEqualBoolean__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataEqualBoolean__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1741,7 +1745,7 @@ void getByProjectName__whenFilterMetadataEqualBoolean__thenReturnSpansFiltered() } @Test - void getByProjectName__whenFilterMetadataEqualNull__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataEqualNull__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1781,7 +1785,7 @@ void getByProjectName__whenFilterMetadataEqualNull__thenReturnSpansFiltered() { } @Test - void getByProjectName__whenFilterMetadataContainsString__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataContainsString__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1821,7 +1825,7 @@ void getByProjectName__whenFilterMetadataContainsString__thenReturnSpansFiltered } @Test - void getByProjectName__whenFilterMetadataContainsNumber__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataContainsNumber__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1861,7 +1865,7 @@ void getByProjectName__whenFilterMetadataContainsNumber__thenReturnSpansFiltered } @Test - void getByProjectName__whenFilterMetadataContainsBoolean__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataContainsBoolean__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1902,7 +1906,7 @@ void getByProjectName__whenFilterMetadataContainsBoolean__thenReturnSpansFiltere } @Test - void getByProjectName__whenFilterMetadataContainsNull__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataContainsNull__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1942,7 +1946,7 @@ void getByProjectName__whenFilterMetadataContainsNull__thenReturnSpansFiltered() } @Test - void getByProjectName__whenFilterMetadataGreaterThanNumber__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataGreaterThanNumber__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -1982,7 +1986,7 @@ void getByProjectName__whenFilterMetadataGreaterThanNumber__thenReturnSpansFilte } @Test - void getByProjectName__whenFilterMetadataGreaterThanString__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataGreaterThanString__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2019,7 +2023,7 @@ void getByProjectName__whenFilterMetadataGreaterThanString__thenReturnSpansFilte } @Test - void getByProjectName__whenFilterMetadataGreaterThanBoolean__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataGreaterThanBoolean__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2056,7 +2060,7 @@ void getByProjectName__whenFilterMetadataGreaterThanBoolean__thenReturnSpansFilt } @Test - void getByProjectName__whenFilterMetadataGreaterThanNull__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataGreaterThanNull__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2093,7 +2097,7 @@ void getByProjectName__whenFilterMetadataGreaterThanNull__thenReturnSpansFiltere } @Test - void getByProjectName__whenFilterMetadataLessThanNumber__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataLessThanNumber__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2133,7 +2137,7 @@ void getByProjectName__whenFilterMetadataLessThanNumber__thenReturnSpansFiltered } @Test - void getByProjectName__whenFilterMetadataLessThanString__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataLessThanString__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2170,7 +2174,7 @@ void getByProjectName__whenFilterMetadataLessThanString__thenReturnSpansFiltered } @Test - void getByProjectName__whenFilterMetadataLessThanBoolean__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataLessThanBoolean__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2207,7 +2211,7 @@ void getByProjectName__whenFilterMetadataLessThanBoolean__thenReturnSpansFiltere } @Test - void getByProjectName__whenFilterMetadataLessThanNull__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterMetadataLessThanNull__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2244,7 +2248,7 @@ void getByProjectName__whenFilterMetadataLessThanNull__thenReturnSpansFiltered() } @Test - void getByProjectName__whenFilterTagsContains__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterTagsContains__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2280,7 +2284,7 @@ void getByProjectName__whenFilterTagsContains__thenReturnSpansFiltered() { getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans, unexpectedSpans, apiKey); } - static Stream getByProjectName__whenFilterUsage__thenReturnSpansFiltered() { + static Stream getSpansByProject__whenFilterUsage__thenReturnSpansFiltered() { return Stream.of( arguments("completion_tokens", SpanField.USAGE_COMPLETION_TOKENS), arguments("prompt_tokens", SpanField.USAGE_PROMPT_TOKENS), @@ -2288,8 +2292,8 @@ static Stream getByProjectName__whenFilterUsage__thenReturnSpansFilte } @ParameterizedTest - @MethodSource("getByProjectName__whenFilterUsage__thenReturnSpansFiltered") - void getByProjectName__whenFilterUsageEqual__thenReturnSpansFiltered(String usageKey, Field field) { + @MethodSource("getSpansByProject__whenFilterUsage__thenReturnSpansFiltered") + void getSpansByProject__whenFilterUsageEqual__thenReturnSpansFiltered(String usageKey, Field field) { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2337,8 +2341,8 @@ void getByProjectName__whenFilterUsageEqual__thenReturnSpansFiltered(String usag } @ParameterizedTest - @MethodSource("getByProjectName__whenFilterUsage__thenReturnSpansFiltered") - void getByProjectName__whenFilterUsageGreaterThan__thenReturnSpansFiltered(String usageKey, Field field) { + @MethodSource("getSpansByProject__whenFilterUsage__thenReturnSpansFiltered") + void getSpansByProject__whenFilterUsageGreaterThan__thenReturnSpansFiltered(String usageKey, Field field) { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2376,8 +2380,8 @@ void getByProjectName__whenFilterUsageGreaterThan__thenReturnSpansFiltered(Strin } @ParameterizedTest - @MethodSource("getByProjectName__whenFilterUsage__thenReturnSpansFiltered") - void getByProjectName__whenFilterUsageGreaterThanEqual__thenReturnSpansFiltered(String usageKey, Field field) { + @MethodSource("getSpansByProject__whenFilterUsage__thenReturnSpansFiltered") + void getSpansByProject__whenFilterUsageGreaterThanEqual__thenReturnSpansFiltered(String usageKey, Field field) { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2415,8 +2419,8 @@ void getByProjectName__whenFilterUsageGreaterThanEqual__thenReturnSpansFiltered( } @ParameterizedTest - @MethodSource("getByProjectName__whenFilterUsage__thenReturnSpansFiltered") - void getByProjectName__whenFilterUsageLessThan__thenReturnSpansFiltered(String usageKey, Field field) { + @MethodSource("getSpansByProject__whenFilterUsage__thenReturnSpansFiltered") + void getSpansByProject__whenFilterUsageLessThan__thenReturnSpansFiltered(String usageKey, Field field) { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2454,8 +2458,8 @@ void getByProjectName__whenFilterUsageLessThan__thenReturnSpansFiltered(String u } @ParameterizedTest - @MethodSource("getByProjectName__whenFilterUsage__thenReturnSpansFiltered") - void getByProjectName__whenFilterUsageLessThanEqual__thenReturnSpansFiltered(String usageKey, Field field) { + @MethodSource("getSpansByProject__whenFilterUsage__thenReturnSpansFiltered") + void getSpansByProject__whenFilterUsageLessThanEqual__thenReturnSpansFiltered(String usageKey, Field field) { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2494,7 +2498,7 @@ void getByProjectName__whenFilterUsageLessThanEqual__thenReturnSpansFiltered(Str @ParameterizedTest @MethodSource - void getByProjectName__whenFilterFeedbackScoresEqual_NotEqual__thenReturnSpansFiltered(Operator operator, + void getSpansByProject__whenFilterFeedbackScoresEqual_NotEqual__thenReturnSpansFiltered(Operator operator, Function, List> getExpectedSpans, Function, List> getUnexpectedSpans) { @@ -2550,7 +2554,7 @@ void getByProjectName__whenFilterFeedbackScoresEqual_NotEqual__thenReturnSpansFi apiKey); } - private Stream getByProjectName__whenFilterFeedbackScoresEqual_NotEqual__thenReturnSpansFiltered() { + private Stream getSpansByProject__whenFilterFeedbackScoresEqual_NotEqual__thenReturnSpansFiltered() { return Stream.of( Arguments.of(Operator.EQUAL, (Function, List>) spans -> List.of(spans.getFirst()), @@ -2561,7 +2565,7 @@ private Stream getByProjectName__whenFilterFeedbackScoresEqual_NotEqu } @Test - void getByProjectName__whenFilterFeedbackScoresGreaterThan__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterFeedbackScoresGreaterThan__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2618,7 +2622,7 @@ void getByProjectName__whenFilterFeedbackScoresGreaterThan__thenReturnSpansFilte } @Test - void getByProjectName__whenFilterFeedbackScoresGreaterThanEqual__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterFeedbackScoresGreaterThanEqual__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2667,7 +2671,7 @@ void getByProjectName__whenFilterFeedbackScoresGreaterThanEqual__thenReturnSpans } @Test - void getByProjectName__whenFilterFeedbackScoresLessThan__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterFeedbackScoresLessThan__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2717,7 +2721,7 @@ void getByProjectName__whenFilterFeedbackScoresLessThan__thenReturnSpansFiltered } @Test - void getByProjectName__whenFilterFeedbackScoresLessThanEqual__thenReturnSpansFiltered() { + void getSpansByProject__whenFilterFeedbackScoresLessThanEqual__thenReturnSpansFiltered() { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -2766,7 +2770,80 @@ void getByProjectName__whenFilterFeedbackScoresLessThanEqual__thenReturnSpansFil getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans, unexpectedSpans, apiKey); } - static Stream getByProjectName__whenFilterInvalidOperatorForFieldType__thenReturn400() { + Stream getSpansByProject__whenFilterByDuration__thenReturnSpansFiltered() { + return Stream.of( + arguments(Operator.EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.GREATER_THAN, + Duration.ofMillis(8L).toNanos() / 1000, 7.0), + arguments(Operator.GREATER_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.GREATER_THAN_EQUAL, + Duration.ofMillis(1L).plusNanos(1000).toNanos() / 1000, 1.0), + arguments(Operator.LESS_THAN, + Duration.ofMillis(1L).plusNanos(1).toNanos() / 1000, 2.0), + arguments(Operator.LESS_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.LESS_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 2.0)); + } + + @ParameterizedTest + @MethodSource + void getSpansByProject__whenFilterByDuration__thenReturnSpansFiltered(Operator operator, long end, + double duration) { + String workspaceName = UUID.randomUUID().toString(); + String workspaceId = UUID.randomUUID().toString(); + String apiKey = UUID.randomUUID().toString(); + + mockTargetWorkspace(apiKey, workspaceName, workspaceId); + + var projectName = generator.generate().toString(); + var spans = PodamFactoryUtils.manufacturePojoList(podamFactory, Span.class) + .stream() + .map(span -> { + Instant now = Instant.now(); + return span.toBuilder() + .projectId(null) + .projectName(projectName) + .feedbackScores(null) + .startTime(now) + .endTime(Set.of(Operator.LESS_THAN, Operator.LESS_THAN_EQUAL).contains(operator) + ? Instant.now().plusSeconds(2) + : now.plusNanos(1000)) + .build(); + }) + .collect(Collectors.toCollection(ArrayList::new)); + + var start = Instant.now().truncatedTo(ChronoUnit.MILLIS); + spans.set(0, spans.getFirst().toBuilder() + .startTime(start) + .endTime(start.plus(end, ChronoUnit.MICROS)) + .build()); + + spans.forEach(expectedSpan -> createAndAssert(expectedSpan, apiKey, workspaceName)); + + var expectedSpans = List.of(spans.getFirst()); + + var unexpectedSpans = PodamFactoryUtils.manufacturePojoList(podamFactory, Span.class).stream() + .map(span -> span.toBuilder() + .projectId(null) + .build()) + .toList(); + + unexpectedSpans.forEach(expectedSpan -> createAndAssert(expectedSpan, apiKey, workspaceName)); + + var filters = List.of( + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(operator) + .value(String.valueOf(duration)) + .build()); + + getAndAssertPage(workspaceName, projectName, filters, spans, expectedSpans, unexpectedSpans, apiKey); + } + + static Stream getSpansByProject__whenFilterInvalidOperatorForFieldType__thenReturn400() { return Stream.of( SpanFilter.builder() .field(SpanField.START_TIME) @@ -3032,14 +3109,34 @@ static Stream getByProjectName__whenFilterInvalidOperatorForFieldType__t .field(SpanField.TAGS) .operator(Operator.LESS_THAN_EQUAL) .value(RandomStringUtils.randomAlphanumeric(10)) + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.CONTAINS) + .value("1") + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.ENDS_WITH) + .value("1") + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.NOT_CONTAINS) + .value("1") + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.STARTS_WITH) + .value("1") .build()); } @ParameterizedTest @MethodSource - void getByProjectName__whenFilterInvalidOperatorForFieldType__thenReturn400(Filter filter) { + void getSpansByProject__whenFilterInvalidOperatorForFieldType__thenReturn400(Filter filter) { var expectedError = new io.dropwizard.jersey.errors.ErrorMessage( - 400, + HttpStatus.SC_BAD_REQUEST, "Invalid operator '%s' for field '%s' of type '%s'".formatted( filter.operator().getQueryParamOperator(), filter.field().getQueryParamField(), @@ -3054,13 +3151,13 @@ void getByProjectName__whenFilterInvalidOperatorForFieldType__thenReturn400(Filt .header(WORKSPACE_HEADER, TEST_WORKSPACE) .get(); - assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(400); + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST); var actualError = actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class); assertThat(actualError).isEqualTo(expectedError); } - static Stream getByProjectName__whenFilterInvalidValueOrKeyForFieldType__thenReturn400() { + static Stream getSpansByProject__whenFilterInvalidValueOrKeyForFieldType__thenReturn400() { return Stream.of( SpanFilter.builder() .field(SpanField.ID) @@ -3135,12 +3232,22 @@ static Stream getByProjectName__whenFilterInvalidValueOrKeyForFieldType_ .operator(Operator.EQUAL) .value("") .key("hallucination") + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.EQUAL) + .value("") + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.EQUAL) + .value(RandomStringUtils.randomAlphanumeric(5)) .build()); } @ParameterizedTest @MethodSource - void getByProjectName__whenFilterInvalidValueOrKeyForFieldType__thenReturn400(Filter filter) { + void getSpansByProject__whenFilterInvalidValueOrKeyForFieldType__thenReturn400(Filter filter) { String workspaceName = UUID.randomUUID().toString(); String workspaceId = UUID.randomUUID().toString(); String apiKey = UUID.randomUUID().toString(); @@ -3148,7 +3255,7 @@ void getByProjectName__whenFilterInvalidValueOrKeyForFieldType__thenReturn400(Fi mockTargetWorkspace(apiKey, workspaceName, workspaceId); var expectedError = new io.dropwizard.jersey.errors.ErrorMessage( - 400, + HttpStatus.SC_BAD_REQUEST, "Invalid value '%s' or key '%s' for field '%s' of type '%s'".formatted( filter.value(), filter.key(), @@ -3165,7 +3272,7 @@ void getByProjectName__whenFilterInvalidValueOrKeyForFieldType__thenReturn400(Fi .header(WORKSPACE_HEADER, workspaceName) .get(); - assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(400); + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST); var actualError = actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class); assertThat(actualError).isEqualTo(expectedError); @@ -3279,6 +3386,13 @@ private void assertIgnoredFields(List actualSpans, List expectedSpan .withIgnoredFields(IGNORED_FIELDS_SCORES) .build()) .isEqualTo(expectedFeedbackScores); + var expected = DurationUtils.getDurationInMillisWithSubMilliPrecision( + expectedSpan.startTime(), expectedSpan.endTime()); + if (actualSpan.duration() == null || expected == null) { + assertThat(actualSpan.duration()).isEqualTo(expected); + } else { + assertThat(actualSpan.duration()).isEqualTo(expected, within(0.001)); + } if (actualSpan.feedbackScores() != null) { actualSpan.feedbackScores().forEach(feedbackScore -> { @@ -3292,29 +3406,7 @@ private void assertIgnoredFields(List actualSpans, List expectedSpan } private UUID createAndAssert(Span expectedSpan, String apiKey, String workspaceName) { - try (var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) - .request() - .header(HttpHeaders.AUTHORIZATION, apiKey) - .header(WORKSPACE_HEADER, workspaceName) - .post(Entity.json(expectedSpan))) { - - var actualHeaderString = actualResponse.getHeaderString("Location"); - assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(201); - assertThat(actualResponse.hasEntity()).isFalse(); - - UUID expectedSpanId; - if (expectedSpan.id() != null) { - expectedSpanId = expectedSpan.id(); - } else { - expectedSpanId = TestUtils.getIdFromLocation(actualResponse.getLocation()); - } - - assertThat(actualHeaderString).isEqualTo(URL_TEMPLATE.formatted(baseURI) - .concat("/") - .concat(expectedSpanId.toString())); - - return expectedSpanId; - } + return spanResourceClient.createSpan(expectedSpan, apiKey, workspaceName); } private void createAndAssert(UUID entityId, FeedbackScore score, String workspaceName, String apiKey) { @@ -3472,7 +3564,7 @@ void createWhenTryingToCreateSpanTwice() { } @Test - void testDeserializationErrorOnSpanCreate() throws JsonProcessingException { + void testDeserializationErrorOnSpanCreate() { var projectName = RandomStringUtils.randomAlphanumeric(10); var traceId = generator.generate(); @@ -3722,6 +3814,13 @@ private Span getAndAssert(Span expectedSpan, String apiKey, String workspaceName assertThat(actualSpan.lastUpdatedAt()).isAfter(expectedSpan.lastUpdatedAt()); assertThat(actualSpan.createdBy()).isEqualTo(USER); assertThat(actualSpan.lastUpdatedBy()).isEqualTo(USER); + var expected = DurationUtils.getDurationInMillisWithSubMilliPrecision( + expectedSpan.startTime(), expectedSpan.endTime()); + if (actualSpan.duration() == null || expected == null) { + assertThat(actualSpan.duration()).isEqualTo(expected); + } else { + assertThat(actualSpan.duration()).isEqualTo(expected, within(0.001)); + } return actualSpan; } } @@ -7146,6 +7245,80 @@ void getSpanStats__whenFilterFeedbackScoresLessThanEqual__thenReturnSpansFiltere getStatsAndAssert(projectName, null, filters, null, null, apiKey, workspaceName, projectStatItems); } + Stream getSpanStats__whenFilterByDuration__thenReturnSpansFiltered() { + return Stream.of( + arguments(Operator.EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.GREATER_THAN, + Duration.ofMillis(8L).toNanos() / 1000, 7.0), + arguments(Operator.GREATER_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.GREATER_THAN_EQUAL, + Duration.ofMillis(1L).plusNanos(1000).toNanos() / 1000, 1.0), + arguments(Operator.LESS_THAN, + Duration.ofMillis(1L).plusNanos(1).toNanos() / 1000, 2.0), + arguments(Operator.LESS_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.LESS_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 2.0)); + } + + @ParameterizedTest + @MethodSource + void getSpanStats__whenFilterByDuration__thenReturnSpansFiltered(Operator operator, long end, double duration) { + String workspaceName = UUID.randomUUID().toString(); + String workspaceId = UUID.randomUUID().toString(); + String apiKey = UUID.randomUUID().toString(); + + mockTargetWorkspace(apiKey, workspaceName, workspaceId); + + var projectName = generator.generate().toString(); + var spans = PodamFactoryUtils.manufacturePojoList(podamFactory, Span.class) + .stream() + .map(span -> { + Instant now = Instant.now(); + return span.toBuilder() + .projectId(null) + .projectName(projectName) + .feedbackScores(null) + .startTime(now) + .endTime(Set.of(Operator.LESS_THAN, Operator.LESS_THAN_EQUAL).contains(operator) + ? Instant.now().plusSeconds(2) + : now.plusNanos(1000)) + .build(); + }) + .collect(Collectors.toCollection(ArrayList::new)); + + var start = Instant.now().truncatedTo(ChronoUnit.MILLIS); + spans.set(0, spans.getFirst().toBuilder() + .startTime(start) + .endTime(start.plus(end, ChronoUnit.MICROS)) + .build()); + + spans.forEach(expectedSpan -> createAndAssert(expectedSpan, apiKey, workspaceName)); + + var expectedSpans = List.of(spans.getFirst()); + + var unexpectedSpans = PodamFactoryUtils.manufacturePojoList(podamFactory, Span.class).stream() + .map(span -> span.toBuilder() + .projectId(null) + .build()) + .toList(); + + unexpectedSpans.forEach(expectedSpan -> createAndAssert(expectedSpan, apiKey, workspaceName)); + + var filters = List.of( + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(operator) + .value(String.valueOf(duration)) + .build()); + + List> stats = getProjectSpanStatItems(expectedSpans); + + getStatsAndAssert(projectName, null, filters, null, null, apiKey, workspaceName, stats); + } + static Stream getSpanStats__whenFilterInvalidOperatorForFieldType__thenReturn400() { return Stream.of( SpanFilter.builder() @@ -7412,6 +7585,26 @@ static Stream getSpanStats__whenFilterInvalidOperatorForFieldType__thenR .field(SpanField.TAGS) .operator(Operator.LESS_THAN_EQUAL) .value(RandomStringUtils.randomAlphanumeric(10)) + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.NOT_CONTAINS) + .value("1") + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.CONTAINS) + .value("1") + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.ENDS_WITH) + .value("1") + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.STARTS_WITH) + .value("1") .build()); } @@ -7419,7 +7612,7 @@ static Stream getSpanStats__whenFilterInvalidOperatorForFieldType__thenR @MethodSource void getSpanStats__whenFilterInvalidOperatorForFieldType__thenReturn400(Filter filter) { var expectedError = new io.dropwizard.jersey.errors.ErrorMessage( - 400, + HttpStatus.SC_BAD_REQUEST, "Invalid operator '%s' for field '%s' of type '%s'".formatted( filter.operator().getQueryParamOperator(), filter.field().getQueryParamField(), @@ -7516,6 +7709,16 @@ static Stream getSpanStats__whenFilterInvalidValueOrKeyForFieldType__the .operator(Operator.EQUAL) .value("") .key("hallucination") + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.EQUAL) + .value("") + .build(), + SpanFilter.builder() + .field(SpanField.DURATION) + .operator(Operator.EQUAL) + .value(RandomStringUtils.randomAlphanumeric(5)) .build()); } @@ -7529,7 +7732,7 @@ void getSpanStats__whenFilterInvalidValueOrKeyForFieldType__thenReturn400(Filter mockTargetWorkspace(apiKey, workspaceName, workspaceId); var expectedError = new io.dropwizard.jersey.errors.ErrorMessage( - 400, + HttpStatus.SC_BAD_REQUEST, "Invalid value '%s' or key '%s' for field '%s' of type '%s'".formatted( filter.value(), filter.key(), diff --git a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/TracesResourceTest.java b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/TracesResourceTest.java index 5d7e410a84..3c57167082 100644 --- a/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/TracesResourceTest.java +++ b/apps/opik-backend/src/test/java/com/comet/opik/api/resources/v1/priv/TracesResourceTest.java @@ -24,6 +24,7 @@ import com.comet.opik.api.resources.utils.AuthTestUtils; import com.comet.opik.api.resources.utils.ClickHouseContainerUtils; import com.comet.opik.api.resources.utils.ClientSupportUtils; +import com.comet.opik.api.resources.utils.DurationUtils; import com.comet.opik.api.resources.utils.MigrationUtils; import com.comet.opik.api.resources.utils.MySQLContainerUtils; import com.comet.opik.api.resources.utils.RedisContainerUtils; @@ -80,7 +81,9 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.sql.SQLException; +import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.AbstractMap; import java.util.ArrayList; import java.util.List; @@ -107,6 +110,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; import static org.junit.jupiter.params.provider.Arguments.arguments; @DisplayName("Traces Resource Test") @@ -116,7 +120,7 @@ class TracesResourceTest { public static final String URL_TEMPLATE = "%s/v1/private/traces"; private static final String URL_TEMPLATE_SPANS = "%s/v1/private/spans"; private static final String[] IGNORED_FIELDS_TRACES = {"projectId", "projectName", "createdAt", - "lastUpdatedAt", "feedbackScores", "createdBy", "lastUpdatedBy", "totalEstimatedCost"}; + "lastUpdatedAt", "feedbackScores", "createdBy", "lastUpdatedBy", "totalEstimatedCost", "duration"}; private static final String[] IGNORED_FIELDS_SPANS = SpansResourceTest.IGNORED_FIELDS; private static final String[] IGNORED_FIELDS_SCORES = {"createdAt", "lastUpdatedAt", "createdBy", "lastUpdatedBy"}; @@ -763,7 +767,7 @@ class FindTraces { @Test @DisplayName("when project name and project id are null, then return bad request") - void getByProjectName__whenProjectNameAndIdAreNull__thenReturnBadRequest() { + void getTracesByProject__whenProjectNameAndIdAreNull__thenReturnBadRequest() { var actualResponse = client.target(URL_TEMPLATE.formatted(baseURI)) .request() @@ -890,7 +894,7 @@ void findWithImageTruncation(JsonNode original, JsonNode expected, boolean trunc @Test @DisplayName("when project name is not empty, then return traces by project name") - void getByProjectName__whenProjectNameIsNotEmpty__thenReturnTracesByProjectName() { + void getTracesByProject__whenProjectNameIsNotEmpty__thenReturnTracesByProjectName() { var projectName = UUID.randomUUID().toString(); var workspaceName = RandomStringUtils.randomAlphanumeric(10); @@ -932,7 +936,7 @@ void getByProjectName__whenProjectNameIsNotEmpty__thenReturnTracesByProjectName( @Test @DisplayName("when project id is not empty, then return traces by project id") - void getByProjectName__whenProjectIdIsNotEmpty__thenReturnTracesByProjectId() { + void getTracesByProject__whenProjectIdIsNotEmpty__thenReturnTracesByProjectId() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var projectName = UUID.randomUUID().toString(); @@ -975,7 +979,7 @@ void getByProjectName__whenProjectIdIsNotEmpty__thenReturnTracesByProjectId() { @Test @DisplayName("when filtering by workspace name, then return traces filtered") - void getByProjectName__whenFilterWorkspaceName__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterWorkspaceName__thenReturnTracesFiltered() { var workspaceName1 = UUID.randomUUID().toString(); var workspaceName2 = UUID.randomUUID().toString(); @@ -1034,7 +1038,7 @@ private Stream equalAndNotEqualFilters() { @ParameterizedTest @MethodSource("equalAndNotEqualFilters") - void getByProjectName__whenFilterIdAndNameEqual__thenReturnTracesFiltered(Operator operator, + void getTracesByProject__whenFilterIdAndNameEqual__thenReturnTracesFiltered(Operator operator, Function, List> getExpectedTraces, Function, List> getUnexpectedTraces) { var workspaceName = RandomStringUtils.randomAlphanumeric(10); @@ -1073,7 +1077,7 @@ void getByProjectName__whenFilterIdAndNameEqual__thenReturnTracesFiltered(Operat } @Test - void getByProjectName__whenFilterNameEqual__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterNameEqual__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1106,7 +1110,7 @@ void getByProjectName__whenFilterNameEqual__thenReturnTracesFiltered() { } @Test - void getByProjectName__whenFilterNameStartsWith__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterNameStartsWith__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1139,7 +1143,7 @@ void getByProjectName__whenFilterNameStartsWith__thenReturnTracesFiltered() { } @Test - void getByProjectName__whenFilterNameEndsWith__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterNameEndsWith__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1172,7 +1176,7 @@ void getByProjectName__whenFilterNameEndsWith__thenReturnTracesFiltered() { } @Test - void getByProjectName__whenFilterNameContains__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterNameContains__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1205,7 +1209,7 @@ void getByProjectName__whenFilterNameContains__thenReturnTracesFiltered() { } @Test - void getByProjectName__whenFilterNameNotContains__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterNameNotContains__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1244,7 +1248,7 @@ void getByProjectName__whenFilterNameNotContains__thenReturnTracesFiltered() { @ParameterizedTest @MethodSource("equalAndNotEqualFilters") - void getByProjectName__whenFilterStartTimeEqual__thenReturnTracesFiltered(Operator operator, + void getTracesByProject__whenFilterStartTimeEqual__thenReturnTracesFiltered(Operator operator, Function, List> getExpectedTraces, Function, List> getUnexpectedTraces) { var workspaceName = RandomStringUtils.randomAlphanumeric(10); @@ -1277,7 +1281,7 @@ void getByProjectName__whenFilterStartTimeEqual__thenReturnTracesFiltered(Operat } @Test - void getByProjectName__whenFilterStartTimeGreaterThan__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterStartTimeGreaterThan__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1314,7 +1318,7 @@ void getByProjectName__whenFilterStartTimeGreaterThan__thenReturnTracesFiltered( } @Test - void getByProjectName__whenFilterStartTimeGreaterThanEqual__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterStartTimeGreaterThanEqual__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1351,7 +1355,7 @@ void getByProjectName__whenFilterStartTimeGreaterThanEqual__thenReturnTracesFilt } @Test - void getByProjectName__whenFilterStartTimeLessThan__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterStartTimeLessThan__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1388,7 +1392,7 @@ void getByProjectName__whenFilterStartTimeLessThan__thenReturnTracesFiltered() { } @Test - void getByProjectName__whenFilterStartTimeLessThanEqual__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterStartTimeLessThanEqual__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1425,7 +1429,7 @@ void getByProjectName__whenFilterStartTimeLessThanEqual__thenReturnTracesFiltere } @Test - void getByProjectName__whenFilterEndTimeEqual__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterEndTimeEqual__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1458,7 +1462,7 @@ void getByProjectName__whenFilterEndTimeEqual__thenReturnTracesFiltered() { } @Test - void getByProjectName__whenFilterInputEqual__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterInputEqual__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1491,7 +1495,7 @@ void getByProjectName__whenFilterInputEqual__thenReturnTracesFiltered() { } @Test - void getByProjectName__whenFilterOutputEqual__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterOutputEqual__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1524,7 +1528,7 @@ void getByProjectName__whenFilterOutputEqual__thenReturnTracesFiltered() { } @Test - void getByProjectName__whenFilterTotalEstimatedCostGreaterThen__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterTotalEstimatedCostGreaterThen__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1571,7 +1575,7 @@ void getByProjectName__whenFilterTotalEstimatedCostGreaterThen__thenReturnTraces @ParameterizedTest @MethodSource("equalAndNotEqualFilters") - void getByProjectName__whenFilterTotalEstimatedCostEqual_NotEqual__thenReturnTracesFiltered(Operator operator, + void getTracesByProject__whenFilterTotalEstimatedCostEqual_NotEqual__thenReturnTracesFiltered(Operator operator, Function, List> getUnexpectedTraces, Function, List> getExpectedTraces) { var workspaceName = RandomStringUtils.randomAlphanumeric(10); @@ -1622,7 +1626,7 @@ void getByProjectName__whenFilterTotalEstimatedCostEqual_NotEqual__thenReturnTra @ParameterizedTest @MethodSource("equalAndNotEqualFilters") - void getByProjectName__whenFilterMetadataEqualString__thenReturnTracesFiltered(Operator operator, + void getTracesByProject__whenFilterMetadataEqualString__thenReturnTracesFiltered(Operator operator, Function, List> getExpectedTraces, Function, List> getUnexpectedTraces) { var workspaceName = RandomStringUtils.randomAlphanumeric(10); @@ -1662,7 +1666,7 @@ void getByProjectName__whenFilterMetadataEqualString__thenReturnTracesFiltered(O } @Test - void getByProjectName__whenFilterMetadataEqualNumber__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataEqualNumber__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1701,7 +1705,7 @@ void getByProjectName__whenFilterMetadataEqualNumber__thenReturnTracesFiltered() } @Test - void getByProjectName__whenFilterMetadataEqualBoolean__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataEqualBoolean__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1742,7 +1746,7 @@ void getByProjectName__whenFilterMetadataEqualBoolean__thenReturnTracesFiltered( } @Test - void getByProjectName__whenFilterMetadataEqualNull__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataEqualNull__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1782,7 +1786,7 @@ void getByProjectName__whenFilterMetadataEqualNull__thenReturnTracesFiltered() { } @Test - void getByProjectName__whenFilterMetadataContainsString__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataContainsString__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1822,7 +1826,7 @@ void getByProjectName__whenFilterMetadataContainsString__thenReturnTracesFiltere } @Test - void getByProjectName__whenFilterMetadataContainsNumber__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataContainsNumber__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1862,7 +1866,7 @@ void getByProjectName__whenFilterMetadataContainsNumber__thenReturnTracesFiltere } @Test - void getByProjectName__whenFilterMetadataContainsBoolean__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataContainsBoolean__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1903,7 +1907,7 @@ void getByProjectName__whenFilterMetadataContainsBoolean__thenReturnTracesFilter } @Test - void getByProjectName__whenFilterMetadataContainsNull__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataContainsNull__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1943,7 +1947,7 @@ void getByProjectName__whenFilterMetadataContainsNull__thenReturnTracesFiltered( } @Test - void getByProjectName__whenFilterMetadataGreaterThanNumber__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataGreaterThanNumber__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -1983,7 +1987,7 @@ void getByProjectName__whenFilterMetadataGreaterThanNumber__thenReturnTracesFilt } @Test - void getByProjectName__whenFilterMetadataGreaterThanString__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataGreaterThanString__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2019,7 +2023,7 @@ void getByProjectName__whenFilterMetadataGreaterThanString__thenReturnTracesFilt } @Test - void getByProjectName__whenFilterMetadataGreaterThanBoolean__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataGreaterThanBoolean__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2055,7 +2059,7 @@ void getByProjectName__whenFilterMetadataGreaterThanBoolean__thenReturnTracesFil } @Test - void getByProjectName__whenFilterMetadataGreaterThanNull__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataGreaterThanNull__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2091,7 +2095,7 @@ void getByProjectName__whenFilterMetadataGreaterThanNull__thenReturnTracesFilter } @Test - void getByProjectName__whenFilterMetadataLessThanNumber__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataLessThanNumber__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2131,7 +2135,7 @@ void getByProjectName__whenFilterMetadataLessThanNumber__thenReturnTracesFiltere } @Test - void getByProjectName__whenFilterMetadataLessThanString__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataLessThanString__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2167,7 +2171,7 @@ void getByProjectName__whenFilterMetadataLessThanString__thenReturnTracesFiltere } @Test - void getByProjectName__whenFilterMetadataLessThanBoolean__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataLessThanBoolean__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2203,7 +2207,7 @@ void getByProjectName__whenFilterMetadataLessThanBoolean__thenReturnTracesFilter } @Test - void getByProjectName__whenFilterMetadataLessThanNull__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterMetadataLessThanNull__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2239,7 +2243,7 @@ void getByProjectName__whenFilterMetadataLessThanNull__thenReturnTracesFiltered( } @Test - void getByProjectName__whenFilterTagsContains__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterTagsContains__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2275,7 +2279,7 @@ void getByProjectName__whenFilterTagsContains__thenReturnTracesFiltered() { getAndAssertPage(workspaceName, projectName, filters, traces, expectedTraces, unexpectedTraces, apiKey); } - static Stream getByProjectName__whenFilterUsage__thenReturnTracesFiltered() { + static Stream getTracesByProject__whenFilterUsage__thenReturnTracesFiltered() { return Stream.of( arguments("completion_tokens", TraceField.USAGE_COMPLETION_TOKENS), arguments("prompt_tokens", TraceField.USAGE_PROMPT_TOKENS), @@ -2283,8 +2287,8 @@ static Stream getByProjectName__whenFilterUsage__thenReturnTracesFilt } @ParameterizedTest - @MethodSource("getByProjectName__whenFilterUsage__thenReturnTracesFiltered") - void getByProjectName__whenFilterUsageEqual__thenReturnTracesFiltered(String usageKey, Field field) { + @MethodSource("getTracesByProject__whenFilterUsage__thenReturnTracesFiltered") + void getTracesByProject__whenFilterUsageEqual__thenReturnTracesFiltered(String usageKey, Field field) { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2332,8 +2336,8 @@ void getByProjectName__whenFilterUsageEqual__thenReturnTracesFiltered(String usa } @ParameterizedTest - @MethodSource("getByProjectName__whenFilterUsage__thenReturnTracesFiltered") - void getByProjectName__whenFilterUsageGreaterThan__thenReturnTracesFiltered(String usageKey, Field field) { + @MethodSource("getTracesByProject__whenFilterUsage__thenReturnTracesFiltered") + void getTracesByProject__whenFilterUsageGreaterThan__thenReturnTracesFiltered(String usageKey, Field field) { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2379,8 +2383,9 @@ void getByProjectName__whenFilterUsageGreaterThan__thenReturnTracesFiltered(Stri } @ParameterizedTest - @MethodSource("getByProjectName__whenFilterUsage__thenReturnTracesFiltered") - void getByProjectName__whenFilterUsageGreaterThanEqual__thenReturnTracesFiltered(String usageKey, Field field) { + @MethodSource("getTracesByProject__whenFilterUsage__thenReturnTracesFiltered") + void getTracesByProject__whenFilterUsageGreaterThanEqual__thenReturnTracesFiltered(String usageKey, + Field field) { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2426,8 +2431,8 @@ void getByProjectName__whenFilterUsageGreaterThanEqual__thenReturnTracesFiltered } @ParameterizedTest - @MethodSource("getByProjectName__whenFilterUsage__thenReturnTracesFiltered") - void getByProjectName__whenFilterUsageLessThan__thenReturnTracesFiltered(String usageKey, Field field) { + @MethodSource("getTracesByProject__whenFilterUsage__thenReturnTracesFiltered") + void getTracesByProject__whenFilterUsageLessThan__thenReturnTracesFiltered(String usageKey, Field field) { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2473,8 +2478,8 @@ void getByProjectName__whenFilterUsageLessThan__thenReturnTracesFiltered(String } @ParameterizedTest - @MethodSource("getByProjectName__whenFilterUsage__thenReturnTracesFiltered") - void getByProjectName__whenFilterUsageLessThanEqual__thenReturnTracesFiltered(String usageKey, Field field) { + @MethodSource("getTracesByProject__whenFilterUsage__thenReturnTracesFiltered") + void getTracesByProject__whenFilterUsageLessThanEqual__thenReturnTracesFiltered(String usageKey, Field field) { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2521,7 +2526,7 @@ void getByProjectName__whenFilterUsageLessThanEqual__thenReturnTracesFiltered(St @ParameterizedTest @MethodSource - void getByProjectName__whenFilterFeedbackScoresEqual__thenReturnTracesFiltered(Operator operator, + void getTracesByProject__whenFilterFeedbackScoresEqual__thenReturnTracesFiltered(Operator operator, Function, List> getExpectedTraces, Function, List> getUnexpectedTraces) { var workspaceName = RandomStringUtils.randomAlphanumeric(10); @@ -2571,7 +2576,7 @@ void getByProjectName__whenFilterFeedbackScoresEqual__thenReturnTracesFiltered(O apiKey); } - private Stream getByProjectName__whenFilterFeedbackScoresEqual__thenReturnTracesFiltered() { + private Stream getTracesByProject__whenFilterFeedbackScoresEqual__thenReturnTracesFiltered() { return Stream.of( Arguments.of(Operator.EQUAL, (Function, List>) traces -> List.of(traces.getFirst()), @@ -2582,7 +2587,7 @@ private Stream getByProjectName__whenFilterFeedbackScoresEqual__thenR } @Test - void getByProjectName__whenFilterFeedbackScoresGreaterThan__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterFeedbackScoresGreaterThan__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2634,7 +2639,7 @@ void getByProjectName__whenFilterFeedbackScoresGreaterThan__thenReturnTracesFilt } @Test - void getByProjectName__whenFilterFeedbackScoresGreaterThanEqual__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterFeedbackScoresGreaterThanEqual__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2681,7 +2686,7 @@ void getByProjectName__whenFilterFeedbackScoresGreaterThanEqual__thenReturnTrace } @Test - void getByProjectName__whenFilterFeedbackScoresLessThan__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterFeedbackScoresLessThan__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2729,7 +2734,7 @@ void getByProjectName__whenFilterFeedbackScoresLessThan__thenReturnTracesFiltere } @Test - void getByProjectName__whenFilterFeedbackScoresLessThanEqual__thenReturnTracesFiltered() { + void getTracesByProject__whenFilterFeedbackScoresLessThanEqual__thenReturnTracesFiltered() { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -2775,7 +2780,81 @@ void getByProjectName__whenFilterFeedbackScoresLessThanEqual__thenReturnTracesFi getAndAssertPage(workspaceName, projectName, filters, traces, expectedTraces, unexpectedTraces, apiKey); } - static Stream getByProjectName__whenFilterInvalidOperatorForFieldType__thenReturn400() { + Stream getTracesByProject__whenFilterByDuration__thenReturnTracesFiltered() { + return Stream.of( + arguments(Operator.EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.GREATER_THAN, + Duration.ofMillis(8L).toNanos() / 1000, 7.0), + arguments(Operator.GREATER_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.GREATER_THAN_EQUAL, + Duration.ofMillis(1L).plusNanos(1000).toNanos() / 1000, 1.0), + arguments(Operator.LESS_THAN, + Duration.ofMillis(1L).plusNanos(1).toNanos() / 1000, 2.0), + arguments(Operator.LESS_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.LESS_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 2.0)); + } + + @ParameterizedTest + @MethodSource + void getTracesByProject__whenFilterByDuration__thenReturnTracesFiltered(Operator operator, long end, + double duration) { + String workspaceName = UUID.randomUUID().toString(); + String workspaceId = UUID.randomUUID().toString(); + String apiKey = UUID.randomUUID().toString(); + + mockTargetWorkspace(apiKey, workspaceName, workspaceId); + + var projectName = generator.generate().toString(); + var traces = PodamFactoryUtils.manufacturePojoList(factory, Trace.class) + .stream() + .map(trace -> { + Instant now = Instant.now(); + return trace.toBuilder() + .projectId(null) + .usage(null) + .projectName(projectName) + .feedbackScores(null) + .startTime(now) + .endTime(Set.of(Operator.LESS_THAN, Operator.LESS_THAN_EQUAL).contains(operator) + ? Instant.now().plusSeconds(2) + : now.plusNanos(1000)) + .build(); + }) + .collect(Collectors.toCollection(ArrayList::new)); + + var start = Instant.now().truncatedTo(ChronoUnit.MILLIS); + traces.set(0, traces.getFirst().toBuilder() + .startTime(start) + .endTime(start.plus(end, ChronoUnit.MICROS)) + .build()); + + traces.forEach(expectedTrace -> create(expectedTrace, apiKey, workspaceName)); + + var expectedTraces = List.of(traces.getFirst()); + + var unexpectedTraces = PodamFactoryUtils.manufacturePojoList(factory, Trace.class).stream() + .map(span -> span.toBuilder() + .projectId(null) + .build()) + .toList(); + + unexpectedTraces.forEach(expectedTrace -> create(expectedTrace, apiKey, workspaceName)); + + var filters = List.of( + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(operator) + .value(String.valueOf(duration)) + .build()); + + getAndAssertPage(workspaceName, projectName, filters, traces, expectedTraces, unexpectedTraces, apiKey); + } + + static Stream getTracesByProject__whenFilterInvalidOperatorForFieldType__thenReturn400() { return Stream.of( TraceFilter.builder() .field(TraceField.START_TIME) @@ -2981,15 +3060,35 @@ static Stream getByProjectName__whenFilterInvalidOperatorForFieldType__t .field(TraceField.TAGS) .operator(Operator.LESS_THAN_EQUAL) .value(RandomStringUtils.randomAlphanumeric(10)) + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.ENDS_WITH) + .value("1") + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.STARTS_WITH) + .value("1") + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.CONTAINS) + .value("1") + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.NOT_CONTAINS) + .value("1") .build()); } @ParameterizedTest @MethodSource - void getByProjectName__whenFilterInvalidOperatorForFieldType__thenReturn400(Filter filter) { + void getTracesByProject__whenFilterInvalidOperatorForFieldType__thenReturn400(Filter filter) { var expectedError = new io.dropwizard.jersey.errors.ErrorMessage( - 400, + HttpStatus.SC_BAD_REQUEST, "Invalid operator '%s' for field '%s' of type '%s'".formatted( filter.operator().getQueryParamOperator(), filter.field().getQueryParamField(), @@ -3004,13 +3103,13 @@ void getByProjectName__whenFilterInvalidOperatorForFieldType__thenReturn400(Filt .header(WORKSPACE_HEADER, TEST_WORKSPACE) .get(); - assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(400); + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST); var actualError = actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class); assertThat(actualError).isEqualTo(expectedError); } - static Stream getByProjectName__whenFilterInvalidValueOrKeyForFieldType__thenReturn400() { + static Stream getTracesByProject__whenFilterInvalidValueOrKeyForFieldType__thenReturn400() { return Stream.of( TraceFilter.builder() .field(TraceField.ID) @@ -3070,12 +3169,22 @@ static Stream getByProjectName__whenFilterInvalidValueOrKeyForFieldType_ .operator(Operator.EQUAL) .value("") .key("hallucination") + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.EQUAL) + .value("") + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.EQUAL) + .value(RandomStringUtils.randomAlphanumeric(5)) .build()); } @ParameterizedTest @MethodSource - void getByProjectName__whenFilterInvalidValueOrKeyForFieldType__thenReturn400(Filter filter) { + void getTracesByProject__whenFilterInvalidValueOrKeyForFieldType__thenReturn400(Filter filter) { var workspaceName = RandomStringUtils.randomAlphanumeric(10); var workspaceId = UUID.randomUUID().toString(); var apiKey = UUID.randomUUID().toString(); @@ -3100,7 +3209,7 @@ void getByProjectName__whenFilterInvalidValueOrKeyForFieldType__thenReturn400(Fi .header(WORKSPACE_HEADER, workspaceName) .get(); - assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(400); + assertThat(actualResponse.getStatusInfo().getStatusCode()).isEqualTo(HttpStatus.SC_BAD_REQUEST); var actualError = actualResponse.readEntity(io.dropwizard.jersey.errors.ErrorMessage.class); assertThat(actualError).isEqualTo(expectedError); @@ -3136,7 +3245,7 @@ private void getAndAssertPage(int page, int size, String projectName, List actualSpans, List expecte assertThat(actualSpan.projectName()).isNull(); assertThat(actualSpan.createdAt()).isAfter(expectedSpan.createdAt()); assertThat(actualSpan.lastUpdatedAt()).isAfter(expectedSpan.lastUpdatedAt()); + var expected = DurationUtils.getDurationInMillisWithSubMilliPrecision( + expectedSpan.startTime(), expectedSpan.endTime()); + + if (actualSpan.duration() == null || expected == null) { + assertThat(actualSpan.duration()).isEqualTo(expected); + } else { + assertThat(actualSpan.duration()).isEqualTo(expected, within(0.001)); + } + assertThat(actualSpan.feedbackScores()) .usingRecursiveComparison() .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) @@ -6611,6 +6738,83 @@ void getTraceStats__whenFilterMetadataLessThanNull__thenReturnTracesFiltered() { getStatsAndAssert(projectName, null, filters, apiKey, workspaceName, expectedStats); } + Stream getTraceStats__whenFilterByDuration__thenReturnTracesFiltered() { + return Stream.of( + arguments(Operator.EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.GREATER_THAN, + Duration.ofMillis(8L).toNanos() / 1000, 7.0), + arguments(Operator.GREATER_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.GREATER_THAN_EQUAL, + Duration.ofMillis(1L).plusNanos(1000).toNanos() / 1000, 1.0), + arguments(Operator.LESS_THAN, + Duration.ofMillis(1L).plusNanos(1).toNanos() / 1000, 2.0), + arguments(Operator.LESS_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 1.0), + arguments(Operator.LESS_THAN_EQUAL, + Duration.ofMillis(1L).toNanos() / 1000, 2.0)); + } + + @ParameterizedTest + @MethodSource + void getTraceStats__whenFilterByDuration__thenReturnTracesFiltered(Operator operator, long end, + double duration) { + String workspaceName = UUID.randomUUID().toString(); + String workspaceId = UUID.randomUUID().toString(); + String apiKey = UUID.randomUUID().toString(); + + mockTargetWorkspace(apiKey, workspaceName, workspaceId); + + var projectName = generator.generate().toString(); + var traces = PodamFactoryUtils.manufacturePojoList(factory, Trace.class) + .stream() + .map(trace -> { + Instant now = Instant.now(); + return trace.toBuilder() + .projectId(null) + .usage(null) + .projectName(projectName) + .feedbackScores(null) + .totalEstimatedCost(BigDecimal.ZERO) + .startTime(now) + .endTime(Set.of(Operator.LESS_THAN, Operator.LESS_THAN_EQUAL).contains(operator) + ? Instant.now().plusSeconds(2) + : now.plusNanos(1000)) + .build(); + }) + .collect(Collectors.toCollection(ArrayList::new)); + + var start = Instant.now().truncatedTo(ChronoUnit.MILLIS); + traces.set(0, traces.getFirst().toBuilder() + .startTime(start) + .endTime(start.plus(end, ChronoUnit.MICROS)) + .build()); + + traces.forEach(expectedTrace -> create(expectedTrace, apiKey, workspaceName)); + + var expectedTraces = List.of(traces.getFirst()); + + var unexpectedTraces = PodamFactoryUtils.manufacturePojoList(factory, Trace.class).stream() + .map(span -> span.toBuilder() + .projectId(null) + .build()) + .toList(); + + unexpectedTraces.forEach(expectedTrace -> create(expectedTrace, apiKey, workspaceName)); + + var filters = List.of( + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(operator) + .value(String.valueOf(duration)) + .build()); + + var expectedStats = getProjectTraceStatItems(expectedTraces); + + getStatsAndAssert(projectName, null, filters, apiKey, workspaceName, expectedStats); + } + private void getStatsAndAssert(String projectName, UUID projectId, List filters, String apiKey, String workspaceName, List> expectedStats) { WebTarget webTarget = client.target(URL_TEMPLATE.formatted(baseURI)) @@ -7406,6 +7610,26 @@ static Stream getTraceStats__whenFilterInvalidOperatorForFieldType__then .field(TraceField.TAGS) .operator(Operator.LESS_THAN_EQUAL) .value(RandomStringUtils.randomAlphanumeric(10)) + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.ENDS_WITH) + .value("1") + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.STARTS_WITH) + .value("1") + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.CONTAINS) + .value("1") + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.NOT_CONTAINS) + .value("1") .build()); } @@ -7414,7 +7638,7 @@ static Stream getTraceStats__whenFilterInvalidOperatorForFieldType__then void getTraceStats__whenFilterInvalidOperatorForFieldType__thenReturn400(Filter filter) { var expectedError = new io.dropwizard.jersey.errors.ErrorMessage( - 400, + HttpStatus.SC_BAD_REQUEST, "Invalid operator '%s' for field '%s' of type '%s'".formatted( filter.operator().getQueryParamOperator(), filter.field().getQueryParamField(), @@ -7497,6 +7721,16 @@ static Stream getTraceStats__whenFilterInvalidValueOrKeyForFieldType__th .operator(Operator.EQUAL) .value("") .key("hallucination") + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.EQUAL) + .value("") + .build(), + TraceFilter.builder() + .field(TraceField.DURATION) + .operator(Operator.EQUAL) + .value(RandomStringUtils.randomAlphanumeric(5)) .build()); }