diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 79c34b8120..b0581b02d8 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -324,7 +324,7 @@ backup、restore-drill、prune 使用同一 fenced database lease(`admin:owner 可选的 `docker/docker-compose.observability.yml` 提供 loopback-only、digest-pinned 的 OpenTelemetry Collector、Prometheus、Alertmanager、Grafana、Tempo 和 Loki。HTTP Owner 暴露 metrics;无 HTTP 的 Judge/Search worker 通过 Micrometer OTLP 输出。日志携带 trace/span 关联,Grafana 可从 Loki 跳转 Tempo。 -Boot 4 tracing binds OTLP export settings under `management.opentelemetry.tracing.export.otlp`; the existing `MANAGEMENT_OTLP_TRACING_ENDPOINT` and `MANAGEMENT_OTLP_AUTHORIZATION` environment inputs remain unchanged. The shared observability module enables the tracing integration and validates the effective endpoint/header pair before export: a configured authorization header requires HTTPS. OTLP metrics configuration and the opt-in Collector overlays remain separate from tracing. +Boot 4 tracing binds OTLP export settings under `management.opentelemetry.tracing.export.otlp`; the existing `MANAGEMENT_OTLP_TRACING_ENDPOINT` and `MANAGEMENT_OTLP_AUTHORIZATION` environment inputs remain unchanged. The shared observability module enables the tracing integration and validates the effective endpoint/header pair before export: a configured authorization header requires HTTPS. Every service, including Core, binds OTLP metrics under `management.otlp.metrics.export` and defaults `MANAGEMENT_OTLP_METRICS_ENABLED` to false, preserving the opt-in Collector overlay. App and Core also select Jackson 2 independently for WebSocket messaging with `spring.websocket.messaging.preferred-json-mapper=jackson2`, keeping local and Redis-broadcast JSON contracts aligned. 生产 telemetry receiver、存储、保留周期、通知 webhook、阈值调优和真实流量 SLO 由外部运维平台负责;仓库 overlay 默认不启动,也不公开 management endpoint 或 secret。 diff --git a/scripts/test/observability-contract.sh b/scripts/test/observability-contract.sh index 20c80dc7ae..2439577d67 100755 --- a/scripts/test/observability-contract.sh +++ b/scripts/test/observability-contract.sh @@ -69,7 +69,7 @@ contains services/admin/src/main/resources/application.yml 'MANAGEMENT_OTLP_AUTH for pom in \ services/auth/pom.xml services/admin/pom.xml services/app/app-web/pom.xml \ services/submission/pom.xml services/notification/pom.xml \ - services/search/pom.xml services/judge/pom.xml; do + services/search/pom.xml services/judge/pom.xml services/core/pom.xml; do contains "$pom" 'backend-observability' done @@ -93,18 +93,28 @@ for service in services: raise SystemExit(f"{path}: sampling default must be between 0 and 1") if "Authorization: ${MANAGEMENT_OTLP_AUTHORIZATION:}" not in text: raise SystemExit(f"{path}: OTLP authorization header mapping is missing") - if service in ("notification", "search", "judge"): - metrics = re.search( - r"^ otlp:\n metrics:\n export:\n" - r" enabled: \$\{MANAGEMENT_OTLP_METRICS_ENABLED:false\}\n" - r" url: \$\{MANAGEMENT_OTLP_METRICS_ENDPOINT:http://localhost:4318/v1/metrics\}\n" - r" step: \$\{MANAGEMENT_OTLP_METRICS_STEP:30s\}$", - text, re.MULTILINE, - ) - if metrics is None: - raise SystemExit(f"{path}: OTLP metrics must retain management.otlp.metrics.export bindings") +for service in (*services, "core"): + path = root / "services" / service / "src/main/resources/application.yml" + text = path.read_text(encoding="utf-8") + metrics = re.search( + r"^ otlp:\n metrics:\n export:\n" + r" enabled: \$\{MANAGEMENT_OTLP_METRICS_ENABLED:false\}\n" + r" url: \$\{MANAGEMENT_OTLP_METRICS_ENDPOINT:http://localhost:4318/v1/metrics\}\n" + r" step: \$\{MANAGEMENT_OTLP_METRICS_STEP:30s\}$", + text, re.MULTILINE, + ) + if metrics is None: + raise SystemExit(f"{path}: OTLP metrics must retain management.otlp.metrics.export bindings") +for service in ("app/app-web", "core"): + path = root / "services" / service / "src/main/resources/application.yml" + text = path.read_text(encoding="utf-8") + if not re.search( + r"^ websocket:\n messaging:\n preferred-json-mapper: jackson2$", + text, re.MULTILINE, + ): + raise SystemExit(f"{path}: WebSocket messaging must preserve Jackson 2 wire compatibility") PY -printf 'owner OTLP sampling and authorization bindings: PASS\n' +printf 'owner OTLP sampling, authorization, opt-in metrics and WebSocket mapper bindings: PASS\n' contains .github/services-matrix.json '"artifact": "backend-search-exec.jar"' contains scripts/runbooks/observability-release-annotation.sh '/api/annotations' contains scripts/runbooks/observability-release-annotation.sh 'image-reference-policy.sh' diff --git a/services/admin/src/main/resources/application.yml b/services/admin/src/main/resources/application.yml index 9c254b6304..48d93d624b 100644 --- a/services/admin/src/main/resources/application.yml +++ b/services/admin/src/main/resources/application.yml @@ -123,6 +123,12 @@ management: tracing: sampling: probability: ${MANAGEMENT_TRACING_SAMPLING_PROBABILITY:1.0} + otlp: + metrics: + export: + enabled: ${MANAGEMENT_OTLP_METRICS_ENABLED:false} + url: ${MANAGEMENT_OTLP_METRICS_ENDPOINT:http://localhost:4318/v1/metrics} + step: ${MANAGEMENT_OTLP_METRICS_STEP:30s} opentelemetry: tracing: export: diff --git a/services/app/app-web/src/main/resources/application.yml b/services/app/app-web/src/main/resources/application.yml index 01ed5e5940..e8de31c778 100644 --- a/services/app/app-web/src/main/resources/application.yml +++ b/services/app/app-web/src/main/resources/application.yml @@ -6,6 +6,9 @@ spring: http: converters: preferred-json-mapper: jackson2 + websocket: + messaging: + preferred-json-mapper: jackson2 application: name: backend-app servlet: @@ -156,6 +159,12 @@ management: tracing: sampling: probability: ${MANAGEMENT_TRACING_SAMPLING_PROBABILITY:1.0} + otlp: + metrics: + export: + enabled: ${MANAGEMENT_OTLP_METRICS_ENABLED:false} + url: ${MANAGEMENT_OTLP_METRICS_ENDPOINT:http://localhost:4318/v1/metrics} + step: ${MANAGEMENT_OTLP_METRICS_STEP:30s} opentelemetry: tracing: export: diff --git a/services/app/app-web/src/test/java/com/ulticode/modules/contest/projection/DefaultContestProjectionTest.java b/services/app/app-web/src/test/java/com/ulticode/modules/contest/projection/DefaultContestProjectionTest.java deleted file mode 100644 index a14c18cc54..0000000000 --- a/services/app/app-web/src/test/java/com/ulticode/modules/contest/projection/DefaultContestProjectionTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ulticode.modules.contest.projection; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -@Disabled("P7 migration: port signature changes; re-enable after P7-RELOCATE-INFRA-001") -@DisplayName("Default contest projection (disabled)") -class DefaultContestProjectionTest { - @Test - @DisplayName("placeholder") - void placeholder() {} -} diff --git a/services/app/app-web/src/test/java/com/ulticode/modules/contest/service/impl/ContestServiceImplTest.java b/services/app/app-web/src/test/java/com/ulticode/modules/contest/service/impl/ContestServiceImplTest.java index c8b0c25b63..8d44f197f0 100644 --- a/services/app/app-web/src/test/java/com/ulticode/modules/contest/service/impl/ContestServiceImplTest.java +++ b/services/app/app-web/src/test/java/com/ulticode/modules/contest/service/impl/ContestServiceImplTest.java @@ -49,10 +49,8 @@ import com.ulticode.common.auth.CurrentUserProvider; /** - * Write-side unit tests for {@link ContestServiceImpl}. The read-cluster cases - * (findUpcoming / findRunning / getContestProblemSubmissions) moved to - * {@link com.ulticode.modules.contest.projection.DefaultContestProjectionTest} - * when the read paths were lifted into {@link ContestProjection}. What remains + * Write-side unit tests for {@link ContestServiceImpl}. Read paths belong to + * {@link ContestProjection}. What remains * here is the contest state machine: createContest field/slug/end-time * computation (its return value is shaped by a mocked projection.toVO echo) and * the submitContestProblem guard matrix. diff --git a/services/auth/src/main/resources/application.yml b/services/auth/src/main/resources/application.yml index 0de22d93f6..1e81b744d1 100644 --- a/services/auth/src/main/resources/application.yml +++ b/services/auth/src/main/resources/application.yml @@ -191,6 +191,12 @@ management: tracing: sampling: probability: ${MANAGEMENT_TRACING_SAMPLING_PROBABILITY:1.0} + otlp: + metrics: + export: + enabled: ${MANAGEMENT_OTLP_METRICS_ENABLED:false} + url: ${MANAGEMENT_OTLP_METRICS_ENDPOINT:http://localhost:4318/v1/metrics} + step: ${MANAGEMENT_OTLP_METRICS_STEP:30s} opentelemetry: tracing: export: diff --git a/services/core/src/main/resources/application.yml b/services/core/src/main/resources/application.yml index 6e1160edc3..3a054f8cd9 100644 --- a/services/core/src/main/resources/application.yml +++ b/services/core/src/main/resources/application.yml @@ -2,6 +2,9 @@ spring: http: converters: preferred-json-mapper: jackson2 + websocket: + messaging: + preferred-json-mapper: jackson2 application: name: ${CORE_APPLICATION_NAME:ulticode-core} main: @@ -9,6 +12,14 @@ spring: lifecycle: timeout-per-shutdown-phase: ${CORE_SHUTDOWN_TIMEOUT:60s} +management: + otlp: + metrics: + export: + enabled: ${MANAGEMENT_OTLP_METRICS_ENABLED:false} + url: ${MANAGEMENT_OTLP_METRICS_ENDPOINT:http://localhost:4318/v1/metrics} + step: ${MANAGEMENT_OTLP_METRICS_STEP:30s} + dubbo: enabled: false registry: diff --git a/services/judge-runtime/pom.xml b/services/judge-runtime/pom.xml index 6cfdcd9e73..309b0b41d1 100644 --- a/services/judge-runtime/pom.xml +++ b/services/judge-runtime/pom.xml @@ -81,6 +81,21 @@ + + + src/test/resources + + + ${project.basedir}/../../docker/sandbox/harness/contract + contract + + input.schema.json + envelope.schema.json + golden/input.json + golden/envelope.json + + + org.apache.maven.plugins diff --git a/services/judge-runtime/src/test/java/com/ulticode/modules/queue/pipeline/source/ConfiguredJudgingCaseSourceTest.java b/services/judge-runtime/src/test/java/com/ulticode/modules/queue/pipeline/source/ConfiguredJudgingCaseSourceTest.java index 294b80f3ef..c5426227c1 100644 --- a/services/judge-runtime/src/test/java/com/ulticode/modules/queue/pipeline/source/ConfiguredJudgingCaseSourceTest.java +++ b/services/judge-runtime/src/test/java/com/ulticode/modules/queue/pipeline/source/ConfiguredJudgingCaseSourceTest.java @@ -1,13 +1,67 @@ package com.ulticode.modules.queue.pipeline.source; -import org.junit.jupiter.api.Disabled; +import com.ulticode.modules.queue.port.JudgingCase; +import com.ulticode.modules.queue.port.adapter.ProblemExampleJudgingCaseSource; +import com.ulticode.modules.queue.port.adapter.TestCaseJudgingCaseSource; +import com.ulticode.modules.submission.port.JudgeConfigPort; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; -@Disabled("P7 migration: port signature changes; re-enable after P7-RELOCATE-INFRA-001") -@DisplayName("Configured judging case source (disabled)") +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("Configured judging case source") class ConfiguredJudgingCaseSourceTest { + @Mock private TestCaseJudgingCaseSource canonicalSource; + @Mock private ProblemExampleJudgingCaseSource legacySource; + @Mock private JudgeConfigPort configuration; + @InjectMocks private ConfiguredJudgingCaseSource source; + @Test - @DisplayName("placeholder") - void placeholder() {} + void selectsCanonicalCasesWhenEnabled() { + var cases = List.of(new JudgingCase("hidden", "hidden", "42", List.of(), true, false)); + when(configuration.isUseTestCases()).thenReturn(true); + when(canonicalSource.loadCases(42L)).thenReturn(cases); + + assertThat(source.loadCases(42L)).isSameAs(cases); + verifyNoInteractions(legacySource); + } + + @Test + void selectsLegacyCasesWhenDisabled() { + var cases = List.of(new JudgingCase("example", "example", "42", List.of(), false, true)); + when(configuration.isUseTestCases()).thenReturn(false); + when(legacySource.loadCases(42L)).thenReturn(cases); + + assertThat(source.loadCases(42L)).isSameAs(cases); + verifyNoInteractions(canonicalSource); + } + + @Test + void doesNotFallBackWhenCanonicalCasesAreEmpty() { + when(configuration.isUseTestCases()).thenReturn(true); + when(canonicalSource.loadCases(42L)).thenReturn(List.of()); + + assertThat(source.loadCases(42L)).isEmpty(); + verifyNoInteractions(legacySource); + } + + @Test + void propagatesCanonicalFailureWithoutFallingBack() { + var failure = new IllegalStateException("canonical source unavailable"); + when(configuration.isUseTestCases()).thenReturn(true); + when(canonicalSource.loadCases(42L)).thenThrow(failure); + + assertThatThrownBy(() -> source.loadCases(42L)).isSameAs(failure); + verifyNoInteractions(legacySource); + } } diff --git a/services/judge-runtime/src/test/java/com/ulticode/modules/submission/contract/DFormEnvelopeContractTest.java b/services/judge-runtime/src/test/java/com/ulticode/modules/submission/contract/DFormEnvelopeContractTest.java index d6c42372fb..2498bfc2e6 100644 --- a/services/judge-runtime/src/test/java/com/ulticode/modules/submission/contract/DFormEnvelopeContractTest.java +++ b/services/judge-runtime/src/test/java/com/ulticode/modules/submission/contract/DFormEnvelopeContractTest.java @@ -26,16 +26,13 @@ * *

Authoritative source: {@code docker/sandbox/harness/contract/CONTRACT.md}. * The schemas and golden files live next to that spec; they are also - * mirrored into {@code src/test/resources/contract/} so the JVM test lane - * can read them off the classpath without needing the harness source tree. + * copied into the test classpath by Maven from that canonical directory. * *

Scope (additive — no runtime change): *

    *
  1. Decoding: a real-shaped envelope (golden) decodes via - * {@link CodeExecutionHelperImpl#parseDEnvelope} into the expected - * per-case {@link RunResultDTO.RunCaseResult} values.
  2. - *
  3. Encoding: {@link CodeExecutionHelperImpl#buildDBatchInputsJson} - * and {@link CodeExecutionHelperImpl#buildDInputsJson} emit JSON + * {@code DFormEnvelopeCodec} into the expected per-case response values.
  4. + *
  5. Encoding: the codec's batch and single-case methods emit JSON * that contains every field the input schema requires.
  6. *
  7. Vocabulary: every {@code status} value in the contract is * decodable by {@link SubmissionStatusCodec#fromWire(String)}.
  8. @@ -45,7 +42,6 @@ * judge path. It runs in the regular unit-test lane. */ @DisplayName("D-form envelope contract parity (backend ↔ harness)") -@org.junit.jupiter.api.Disabled("Test fixtures not relocated; re-enable after resource migration") class DFormEnvelopeContractTest { private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -238,7 +234,7 @@ private JsonNode parseOrFail(String json) { } /** - * Synthesise a {@link RunSubmissionDTO.RunTestCase} list shaped after + * Synthesise a {@link JudgeRunRequest.TestCase} list shaped after * the golden input.json. Uses {@code UuidGenerator.newId() == "test-uuid"} * via the test-only generator wired into {@code helper}. */ @@ -266,4 +262,4 @@ private List buildTestCasesFromGoldenInput() throws Ex } return out; } -} \ No newline at end of file +} diff --git a/services/submission/src/main/resources/application.yml b/services/submission/src/main/resources/application.yml index 8432d91bf8..a9ee7864a6 100644 --- a/services/submission/src/main/resources/application.yml +++ b/services/submission/src/main/resources/application.yml @@ -86,6 +86,7 @@ management: headers: Authorization: ${MANAGEMENT_OTLP_AUTHORIZATION:} timeout: 5s + otlp: metrics: export: enabled: ${MANAGEMENT_OTLP_METRICS_ENABLED:false}