[OPIK-2856] [BE] Implement UUIDv7 time-based filtering for traces#3921
[OPIK-2856] [BE] Implement UUIDv7 time-based filtering for traces#3921thiagohora merged 11 commits intomainfrom
Conversation
- Add InstantToUUIDMapper to convert Instant timestamps to UUIDv7 bounds - Add InstantParamConverter to parse ISO-8601 and epoch millisecond time parameters - Update TracesResource to accept from_time and to_time parameters on /traces and /traces/stats endpoints - Update TraceDAO to apply UUID-based time filtering using BETWEEN clause on id column - Update TraceSearchCriteria to include uuidFromTime and uuidToTime fields - Add comprehensive integration tests for time filtering with boundary conditions - All tests passing: 10/10 time filtering tests + validation tests
There was a problem hiding this comment.
Pull Request Overview
This PR implements UUIDv7 time-based filtering for the /v1/private/traces and /v1/private/traces/stats endpoints, enabling efficient querying of traces within specific time ranges. The implementation leverages the time-encoded nature of UUIDv7 identifiers and includes comprehensive test coverage.
Key Changes:
- New time-based filtering capability using UUIDv7 boundaries for improved query performance
- Automatic conversion of ISO-8601 and epoch millisecond timestamps via JAX-RS parameter converter
- Time range validation to ensure both parameters are provided together and in the correct order
Reviewed Changes
Copilot reviewed 52 out of 54 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
MySQLContainerUtils.java |
Updated MySQL container type to use newer testcontainers API |
InstantParamConverter.java |
New converter for automatic Instant parsing from query parameters |
InstantToUUIDMapperTest.java |
Comprehensive unit tests for UUIDv7 boundary generation |
TraceSearchCriteria.java |
Added UUID time boundary fields for filtering |
ValidationUtils.java |
New validation for time range parameters |
OpikApplication.java |
Registered InstantParamConverter with Jersey |
TraceResourceClient.java |
Added support for time query parameters in test client |
TracesResourceTest.java |
Removed large nested test class that was moved elsewhere |
TraceAssertions.java |
Moved EXCLUDE_FUNCTIONS map from test class to utility |
| Multiple test files | Updated MySQL container imports to use new package |
…d type safety - Fix InstantParamConverter to catch specific DateTimeParseException instead of generic Exception - Add debug logging when falling back to epoch milliseconds parsing - Refactor anonymous ParamConverter class to named InstantConverter inner class for clarity - Suppress unchecked cast with @SuppressWarnings annotation - Fix MySQLContainerUtils return type to use MySQLContainer<?> for type safety
a5d9353 to
de89734
Compare
- Update tests to reflect that toUpperBound uses next millisecond (+1ms) for inclusive BETWEEN queries - Remove outdated assertions expecting same timestamp in both bounds - Verify upper bound is lexicographically greater than lower bound - All 13 tests now passing
Backend Tests Results5 320 tests 5 313 ✅ 51m 36s ⏱️ Results for commit bdd2249. ♻️ This comment has been updated with latest results. |
andrescrz
left a comment
There was a problem hiding this comment.
Let me review this carefully before moving forward, we have some overlap between our custom convertOtelIdToUUIDv7 and the library one getTimeOrderedEpoch.
I'll do it in a sec.
andrescrz
left a comment
There was a problem hiding this comment.
The general idea LGTM.
Before moving forward, let's double check the choice of library to perform the conversion of a timestamp towards a UUID v7.
We have some duplications here and I'd rather rely on a stable library implementation.
| */ | ||
| @UtilityClass | ||
| @Slf4j | ||
| public class InstantToUUIDMapper { |
There was a problem hiding this comment.
For this, I'd rather have a single source of truth in IdGenerator. See getTimeOrderedEpoch method which relies on our library com.fasterxml.uuid.java-uuid-generator, specifically on method
Initially, I'd rather rely on a reliable library implementation, than re-inventing the wheel ourselves. That way we don't need to maintain the implementation ourselves.
I propose the following:
- Double check that
IdGenerator.getTimeOrderedEpoch(based onTimeBasedEpochGenerator.construct. - If all good, use it here.
- Optionally substitute our own implementation from
OpenTelemetryMapper.convertOtelIdToUUIDv7.
Let me know how this sounds.
There was a problem hiding this comment.
Hi @andrescrz,
Thanks for your comment. On why not use getTimeOrderedEpoch:
While IdGenerator.getTimeOrderedEpoch() also creates UUIDs from timestamps using the java-uuid-generator library, it uses random bits for the lower 80 bits of the UUID. For time-range queries, we need:
- Lower bound: UUID with ALL ZEROS for random bits (lexicographically smallest UUID at this timestamp)
- Upper bound: UUID at (timestamp+1ms) with ALL ZEROS (first UUID AFTER the end time)
This ensures that BETWEEN queries correctly include all traces within the specified time range. IdGenerator's random bits would make the bounds non-deterministic, breaking BETWEEN semantics.
There was a problem hiding this comment.
I remember discussing this with @BorisTkachenko and I raised the same concerns, but we came to the conclusion that getTimeOrderedEpoch works fine in practice. Some reasons:
- From the UUID v7 RFC, the sub-millisecs 12 bits are optional and depend on the implementation.
- Our implementation uses monotonic values instead, so granularly is actually by millisecs.
- As you use start and end interval, just with the semantics and/or increasing/decreasing 1 millisecs, you would get correct results if this was an issue (it isn't).
- You already do +1 millisec.
With all this in mind, I still recommend going with getTimeOrderedEpoch. Some queries based on getTimeOrderedEpoch are already in place and working for months, without bug reports so far.
But not concern if you prefer to move forward. This would be just a small piece of tech debt to centralise and remove duplications in the future (if we ever do it).
There was a problem hiding this comment.
Let me double check
andrescrz
left a comment
There was a problem hiding this comment.
Left some reasoning on why I still recommend the other approach, but not a blocker. It's just a small piece of tech debt in my view. You can move forward.
| */ | ||
| @UtilityClass | ||
| @Slf4j | ||
| public class InstantToUUIDMapper { |
There was a problem hiding this comment.
I remember discussing this with @BorisTkachenko and I raised the same concerns, but we came to the conclusion that getTimeOrderedEpoch works fine in practice. Some reasons:
- From the UUID v7 RFC, the sub-millisecs 12 bits are optional and depend on the implementation.
- Our implementation uses monotonic values instead, so granularly is actually by millisecs.
- As you use start and end interval, just with the semantics and/or increasing/decreasing 1 millisecs, you would get correct results if this was an issue (it isn't).
- You already do +1 millisec.
With all this in mind, I still recommend going with getTimeOrderedEpoch. Some queries based on getTimeOrderedEpoch are already in place and working for months, without bug reports so far.
But not concern if you prefer to move forward. This would be just a small piece of tech debt to centralise and remove duplications in the future (if we ever do it).
0a17ed6 to
4a0842a
Compare
- Simplified InstantToUUIDMapper to use IdGenerator.getTimeOrderedEpoch() instead of convertOtelIdToUUIDv7 - Per UUIDv7 RFC, sub-millisecond 12 bits are optional with millisecond granularity - Start/end interval semantics with ±1ms ensures correct BETWEEN query results - This approach has been battle-tested for months without issues per reviewer recommendation - Converted InstantToUUIDMapper to @singleton service for proper DI integration - Updated TracesResource to inject InstantToUUIDMapper dependency - Updated tests to properly mock IdGenerator dependency
| * @param timestamp the instant in time | ||
| * @return the lower bound UUIDv7 (min UUID at this timestamp) | ||
| */ | ||
| public UUID toLowerBound(Instant timestamp) { |
There was a problem hiding this comment.
Minor: probably this whole class is no longer needed, as it adds verbosity with little benefit. You could move these to IdGenerator and IdGeneratorImpl as overloaded methods and encapsulate the logic there.
) * [NA] [BE] Upgrade MySQL container from Testcontainers * Fix imports order * [OPIK-2856] [BE] Implement UUIDv7 time-based filtering for traces - Add InstantToUUIDMapper to convert Instant timestamps to UUIDv7 bounds - Add InstantParamConverter to parse ISO-8601 and epoch millisecond time parameters - Update TracesResource to accept from_time and to_time parameters on /traces and /traces/stats endpoints - Update TraceDAO to apply UUID-based time filtering using BETWEEN clause on id column - Update TraceSearchCriteria to include uuidFromTime and uuidToTime fields - Add comprehensive integration tests for time filtering with boundary conditions - All tests passing: 10/10 time filtering tests + validation tests * [OPIK-2856] Address PR review comments: improve exception handling and type safety - Fix InstantParamConverter to catch specific DateTimeParseException instead of generic Exception - Add debug logging when falling back to epoch milliseconds parsing - Refactor anonymous ParamConverter class to named InstantConverter inner class for clarity - Suppress unchecked cast with @SuppressWarnings annotation - Fix MySQLContainerUtils return type to use MySQLContainer<?> for type safety * [OPIK-2856] Fix InstantToUUIDMapperTest to match implementation - Update tests to reflect that toUpperBound uses next millisecond (+1ms) for inclusive BETWEEN queries - Remove outdated assertions expecting same timestamp in both bounds - Verify upper bound is lexicographically greater than lower bound - All 13 tests now passing * Remove setup duplicated code * Revision 2: Address PR review comments - LOW priority fixes - #7: Add INSTANCE singleton pattern for InstantConverter - #8: Use StringUtils.isEmpty() for null-safe empty check - Note: #2 and #3 already addressed in previous commit * Revision 3: Use IdGenerator.getTimeOrderedEpoch() for UUID bounds - Simplified InstantToUUIDMapper to use IdGenerator.getTimeOrderedEpoch() instead of convertOtelIdToUUIDv7 - Per UUIDv7 RFC, sub-millisecond 12 bits are optional with millisecond granularity - Start/end interval semantics with ±1ms ensures correct BETWEEN query results - This approach has been battle-tested for months without issues per reviewer recommendation - Converted InstantToUUIDMapper to @singleton service for proper DI integration - Updated TracesResource to inject InstantToUUIDMapper dependency - Updated tests to properly mock IdGenerator dependency * Fix tests
Details
This PR implements UUIDv7 time-based filtering for the
/v1/private/tracesand/v1/private/traces/statsAPI endpoints. The feature enables efficient querying of traces within specific time ranges by leveraging the time-encoded nature of UUIDv7 identifiers.Implementation
New Components:
InstantToUUIDMapper: Utility class to convertInstanttimestamps to UUIDv7 lower and upper bounds for database queriesInstantParamConverter: JAX-RSParamConverterProviderto automatically parse ISO-8601 and epoch millisecond time parametersModified Components:
TracesResource: Addedfrom_timeandto_timeparameters to both/tracesand/traces/statsendpoints with validationTraceDAO: Added UUID-based time filtering using SQLBETWEENclause on theidcolumnTraceSearchCriteria: AddeduuidFromTimeanduuidToTimefields to support time filteringValidationUtils: AddedvalidateTimeRangeParametersmethod to enforce time range validation rulesTest Coverage:
How It Works
from_timeandto_timequery parameters in ISO-8601 or epoch millisecond formatInstantParamConverterautomatically converts these toInstantobjectsTracesResourcevalidates the time range and convertsInstanttimestamps to UUIDv7 boundsTraceDAOapplies the UUID-based filter using SQLBETWEENclauseTesting
Change checklist
Issues
Testing
Scenarios covered:
from_timeparameter returns 400to_timeparameter returns 400from_timeafterto_timereturns 400How to verify:
Documentation
NA