Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。

Expand Down
34 changes: 22 additions & 12 deletions scripts/test/observability-contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" '<artifactId>backend-observability</artifactId>'
done

Expand All @@ -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'
Expand Down
6 changes: 6 additions & 0 deletions services/admin/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions services/app/app-web/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ spring:
http:
converters:
preferred-json-mapper: jackson2
Comment thread
DavidHLP marked this conversation as resolved.
websocket:
messaging:
preferred-json-mapper: jackson2
application:
name: backend-app
servlet:
Expand Down Expand Up @@ -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:
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions services/auth/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions services/core/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@ spring:
http:
converters:
preferred-json-mapper: jackson2
websocket:
messaging:
preferred-json-mapper: jackson2
application:
name: ${CORE_APPLICATION_NAME:ulticode-core}
main:
banner-mode: off
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:
Expand Down
15 changes: 15 additions & 0 deletions services/judge-runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@
</dependencies>

<build>
<testResources>
<testResource>
<directory>src/test/resources</directory>
</testResource>
<testResource>
<directory>${project.basedir}/../../docker/sandbox/harness/contract</directory>
<targetPath>contract</targetPath>
<includes>
<include>input.schema.json</include>
<include>envelope.schema.json</include>
<include>golden/input.json</include>
<include>golden/envelope.json</include>
</includes>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,13 @@
*
* <p>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.
*
* <p>Scope (additive — no runtime change):
* <ol>
* <li>Decoding: a real-shaped envelope (golden) decodes via
* {@link CodeExecutionHelperImpl#parseDEnvelope} into the expected
* per-case {@link RunResultDTO.RunCaseResult} values.</li>
* <li>Encoding: {@link CodeExecutionHelperImpl#buildDBatchInputsJson}
* and {@link CodeExecutionHelperImpl#buildDInputsJson} emit JSON
* {@code DFormEnvelopeCodec} into the expected per-case response values.</li>
* <li>Encoding: the codec's batch and single-case methods emit JSON
* that contains every field the input schema requires.</li>
* <li>Vocabulary: every {@code status} value in the contract is
* decodable by {@link SubmissionStatusCodec#fromWire(String)}.</li>
Expand All @@ -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();
Expand Down Expand Up @@ -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}.
*/
Expand Down Expand Up @@ -266,4 +262,4 @@ private List<JudgeRunRequest.TestCase> buildTestCasesFromGoldenInput() throws Ex
}
return out;
}
}
}
1 change: 1 addition & 0 deletions services/submission/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ management:
headers:
Authorization: ${MANAGEMENT_OTLP_AUTHORIZATION:}
timeout: 5s
otlp:
metrics:
export:
enabled: ${MANAGEMENT_OTLP_METRICS_ENABLED:false}
Expand Down
Loading