Skip to main content

opik-backend

Java backend patterns for Opik. Use when working in apps/opik-backend, designing APIs, database operations, or services.

소스 정보

저장소
comet-ml/opik
최근 소스 활동
2026년 9월 17일 14:00
감지된 SKILL.md 언어
영어
스타
22,503
포크
1,846

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
6 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
opik-backend
description
Java backend patterns for Opik. Use when working in apps/opik-backend, designing APIs, database operations, or services.
# Opik Backend ## Architecture - **Layered**: Resource → Service → DAO (never skip layers) - **DI**: Guice modules, constructor injection with `@Inject` - **Databases**: MySQL (metadata, transactional) + ClickHouse (analytics, append-only) ## Naming Conventions ### Plural Names (Resources, Tests, URLs, DB Tables) - **Resource classes**: `TracesResource`, `SpansResource`, `DatasetsResource` (not `TraceResource`) - **Resource test classes**: `TracesResourceTest`, `SpansResourceTest`, `DatasetsResourceTest` (not `TraceResourceTest`) - **URL paths**: `/v1/private/traces`, `/v1/private/spans` (not `/v1/private/trace`) - **DB table names**: `traces`, `spans`, `feedback_scores` (not `trace`, `span`, `feedback_score`) ### Singular Names (DAO, Service) - **DAO classes**: `TraceDAO`, `SpanDAO`, `DatasetDAO` (not `TracesDAO`) - **Service classes**: `TraceService`, `SpanService`, `DatasetService` (not `TracesService`) ```java // ✅ GOOD @Path("/v1/private/traces") public class TracesResource { } // ✅ GOOD - DAO and Service use singular public class TraceDAO { } public class TraceService { } // ✅ GOOD - test classes match plural resource name public class TracesResourceTest { } // ❌ BAD - singular test class public class TraceResourceTest { } // ❌ BAD - singular resource/URL @Path("/v1/private/trace") public class TraceResource { } // ❌ BAD - plural DAO/Service public class TracesDAO { } public class TracesService { } ``` ## Lombok Conventions ### Records and DTOs - Always annotate records/DTOs with `@Builder(toBuilder = true)` - Use builders (not constructors) when instantiating records - For **internal records** (built programmatically, never validated by Bean Validation), use Lombok `@NonNull` on required fields — it generates a runtime null check at construction - For **request-body DTOs** validated via `@Valid` cascade (Jakarta validators like `@NotNull`/`@NotBlank`/`@Size`), use Jakarta annotations only — do **not** stack `@NonNull` on top. Bean Validation already enforces the contract at the API boundary; doubling up is redundant noise ```java // ✅ GOOD - internal record, Lombok @NonNull @Builder(toBuilder = true) record MyData(@NonNull UUID id, @NonNull String name, String description) {} MyData data = MyData.builder() .id(id) .name(name) .build(); // ✅ GOOD - request-body DTO, Jakarta validators only @Builder(toBuilder = true) public record MyRequest( @NotNull UUID id, @NotBlank String name, @NotNull @Size(min = 1, max = 1000) @Valid List<MyItem> items) {} // ❌ BAD - plain constructor (positional mistakes, less readable) new MyData(id, name, null); // ❌ BAD - @Builder without toBuilder @Builder record MyData(UUID id, String name) {} // ❌ BAD - stacking @NonNull and @NotNull on the same field public record MyRequest(@NonNull @NotNull UUID id) {} ``` ### Dependency Injection - Use `@RequiredArgsConstructor(onConstructor_ = @Inject)` instead of manual constructors ```java // ✅ GOOD @RequiredArgsConstructor(onConstructor_ = @Inject) public class MyService { private final @NonNull DependencyA depA; private final @NonNull DependencyB depB; } // ❌ BAD - boilerplate constructor public class MyService { private final DependencyA depA; @Inject public MyService(DependencyA depA) { this.depA = depA; } } ``` ### Interfaces - Don't put validation annotations (`@NonNull`) on interface method parameters - Keep interfaces free of implementation details ```java // ✅ GOOD interface MyService { void process(String workspaceId, UUID promptId); } // ❌ BAD - validation on interface interface MyService { void process(@NonNull String workspaceId, @NonNull UUID promptId); } ``` ## Critical Gotchas ### StringTemplate Memory Leak ```java // ✅ GOOD var template = TemplateUtils.newST(QUERY); // ❌ BAD - causes memory leak via STGroup singleton var template = new ST(QUERY); ``` ### List Access ```java // ✅ GOOD users.getFirst() users.getLast() // ❌ BAD users.get(0) users.get(users.size() - 1) ``` ### SQL Query Construction **Never build a query out of Java string operations.** No `+`, no `String.format` / `.formatted(...)`, no `StringBuilder`, no `MessageFormat`, no `String.join` over clauses. A query is declared once as a text block, and everything that varies goes through exactly one of two mechanisms: | What varies | Mechanism | |-------------|-----------| | A **value** — id, name, timestamp, list of ids | `:placeholder` + `.bind("placeholder", value)` | | A **fragment** — predicate, sort clause, projected column, CTE | StringTemplate `<if(x)>…<endif>`, `<else>`, `<x>` + `template.add("x", …)` | Why: interpolating values is the SQL-injection surface, and interpolating fragments hides which query a DAO actually runs — the declaration site stops being readable, and callers drift apart over time. ```java // ✅ GOOD - text block, values bound, structure via StringTemplate @SqlQuery(""" SELECT * FROM datasets WHERE workspace_id = :workspace_id <if(name)> AND name like concat('%', :name, '%') <endif> """) // ❌ BAD - string concatenation @SqlQuery("SELECT * FROM datasets " + "WHERE workspace_id = :workspace_id " + "<if(name)> AND name like concat('%', :name, '%') <endif> ") ``` A predicate that differs between callers is a **fragment**, so it belongs in the template — not in a `%s` slot the caller fills in: ```java // ❌ BAD - caller splices the predicate in private static final String TOKEN_USAGE_NAMES_TEMPLATE = """ SELECT DISTINCT name FROM ( SELECT usage FROM spans FINAL WHERE workspace_id = :workspace_id AND %s ) ... """; static String tokenUsageNames(String projectPredicate) { return TOKEN_USAGE_NAMES_TEMPLATE.formatted(projectPredicate); } // caller: tokenUsageNames("project_id IN :project_ids") // ✅ GOOD - both shapes live in the template, the caller picks one private static final String TOKEN_USAGE_NAMES = """ SELECT DISTINCT name FROM ( SELECT usage FROM spans FINAL WHERE workspace_id = :workspace_id <if(project_ids)> AND project_id IN :project_ids <endif> <if(project_id)> AND project_id = :project_id <endif> ) ... """; var template = TemplateUtils.newST(TOKEN_USAGE_NAMES); template.add("project_ids", true); ... statement.bind("project_ids", projectIds.toArray(new UUID[0])); ``` A fragment that genuinely can't be enumerated in the template — a user-chosen sort field or filter clause — must be produced by the allow-listed builders (`SortingQueryBuilder`, `FilterQueryBuilder`), never assembled from raw request strings. `.formatted(...)` stays correct for log and exception messages. The rule is about SQL text only. Some `%s` query templates predate this rule. Don't copy them and don't add new ones. ### Immutable Collections ```java // ✅ GOOD Set.of("A", "B", "C") List.of(1, 2, 3) Map.of("key", "value") // ❌ BAD Arrays.asList("A", "B", "C") ``` ## API Design - **Query parameters that accept lists**: Use plural names from the start (e.g., `exclude_category_names` not `exclude_category_name`). Starting with a singular name and later adding a plural variant results in two redundant query params on the same endpoint. Plural names are backward-compatible since they work for both single and multiple values. ## Error Handling ### Use Jakarta Exceptions ```java throw new BadRequestException("Invalid input"); throw new NotFoundException("User not found: '%s'".formatted(id)); throw new ConflictException("Already exists"); throw new InternalServerErrorException("System error", cause); ``` ### Error Response Classes - Simple: `io.dropwizard.jersey.errors.ErrorMessage` - Complex: `com.comet.opik.api.error.ErrorMessage` - **Never create new error message classes** ## Logging ### Format Convention ```java // ✅ GOOD - values in single quotes log.info("Created user: '{}'", userId); log.error("Failed for workspace: '{}'", workspaceId, exception); // ❌ BAD - no quotes log.info("Created user: {}", userId); ``` ### Value Placement Put the message first and the interpolated values at the **end** of the sentence, as a trailing `name '{}'` list. This keeps a stable literal prefix that stays greppable during a production debugging session — a message whose values are interleaved has no fixed substring to search for. Applies to `log.*` format strings and to exception messages built with `.formatted(...)`. ```java // ✅ GOOD - literal prefix first, values trailing log.warn("Alert name is required, workspaceId '{}'", workspaceId); log.debug("Webhook delivery failed, id '{}', status '{}'", eventId, status); throw new DestinationGuardException("destination has no valid host, url '%s'".formatted(url)); // ❌ BAD - values interleaved, no greppable prefix log.debug("Webhook '{}' failed with status '{}'", eventId, status); throw new DestinationGuardException("destination '%s' has no valid host".formatted(url)); ``` ### Never Log - Emails, passwords, tokens, API keys - PII, personal identifiers - Database credentials ## Reference Files - [clickhouse.md](clickhouse.md) - ClickHouse query patterns - [mysql.md](mysql.md) - TransactionTemplate patterns - [testing.md](testing.md) - PODAM, naming, assertion patterns - [migrations.md](migrations.md) - Liquibase format for MySQL/ClickHouse - [permissions.md](permissions.md) - `@RequiredPermissions` annotation guidance for endpoints
GitHub에서 보기