diff --git a/ordinis-read-api/pom.xml b/ordinis-read-api/pom.xml index 37c35aa..03cbc61 100644 --- a/ordinis-read-api/pom.xml +++ b/ordinis-read-api/pom.xml @@ -49,5 +49,26 @@ org.springframework.cloud spring-cloud-starter-vault-config + + com.fasterxml.jackson.core + jackson-databind + + + io.micrometer + micrometer-core + + + io.micrometer + micrometer-tracing + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/OrdinisReadApiApplication.java b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/OrdinisReadApiApplication.java new file mode 100644 index 0000000..2e055bf --- /dev/null +++ b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/OrdinisReadApiApplication.java @@ -0,0 +1,16 @@ +package cloud.nstart.terravault.ordinis.readapi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@SpringBootApplication(scanBasePackages = "cloud.nstart.terravault.ordinis") +@EnableJpaRepositories(basePackages = "cloud.nstart.terravault.ordinis") +@EntityScan(basePackages = "cloud.nstart.terravault.ordinis") +public class OrdinisReadApiApplication { + + public static void main(String[] args) { + SpringApplication.run(OrdinisReadApiApplication.class, args); + } +} diff --git a/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/dto/FlattenedRecordResponse.java b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/dto/FlattenedRecordResponse.java new file mode 100644 index 0000000..cf57b7e --- /dev/null +++ b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/dto/FlattenedRecordResponse.java @@ -0,0 +1,42 @@ +package cloud.nstart.terravault.ordinis.readapi.dto; + +import cloud.nstart.terravault.ordinis.domain.DataScope; +import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecord; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.JsonNode; +import org.locationtech.jts.io.WKTWriter; + +import java.time.OffsetDateTime; +import java.util.UUID; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record FlattenedRecordResponse( + UUID id, + String businessKey, + JsonNode data, + String geometryWkt, + DataScope dataScope, + OffsetDateTime validFrom, + OffsetDateTime validTo, + Meta _meta) { + + public record Meta(String locale, String defaultLocale, OffsetDateTime asOf) {} + + public static FlattenedRecordResponse of( + DictionaryRecord r, + JsonNode flattenedData, + String chosenLocale, + String defaultLocale, + OffsetDateTime asOf) { + String wkt = r.getGeometry() == null ? null : new WKTWriter().write(r.getGeometry()); + return new FlattenedRecordResponse( + r.getId(), + r.getBusinessKey(), + flattenedData, + wkt, + r.getDataScope(), + r.getValidFrom(), + r.getValidTo(), + new Meta(chosenLocale, defaultLocale, asOf)); + } +} diff --git a/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/dto/PageResponse.java b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/dto/PageResponse.java new file mode 100644 index 0000000..994076d --- /dev/null +++ b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/dto/PageResponse.java @@ -0,0 +1,17 @@ +package cloud.nstart.terravault.ordinis.readapi.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record PageResponse( + List items, + int total, + int page, + int size) { + + public static PageResponse of(List items, int page, int size, int total) { + return new PageResponse<>(items, total, page, size); + } +} diff --git a/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/locale/LocaleAwareJsonFlattener.java b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/locale/LocaleAwareJsonFlattener.java new file mode 100644 index 0000000..853257c --- /dev/null +++ b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/locale/LocaleAwareJsonFlattener.java @@ -0,0 +1,82 @@ +package cloud.nstart.terravault.ordinis.readapi.locale; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.stereotype.Component; + +import java.util.Iterator; +import java.util.Map; + +/** + * Сплющивает multi-locale JSON в плоское значение по выбранной локали. + * + *

Walk проходит {@code schemaJson.properties}, для каждого property с + * {@code x-localized: true} в {@code data} заменяет object {@code {locale:text}} + * на string выбранной локали. Если выбранной нет — fallback на default. + * + *

Если {@code chosenLocale == null} — возвращает data как есть (raw multi-locale). + */ +@Component +public class LocaleAwareJsonFlattener { + + private final ObjectMapper objectMapper; + + public LocaleAwareJsonFlattener(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * @param schema schema_json справочника + * @param data текущая запись (полный multi-locale) + * @param chosenLocale выбранная локаль, или null для raw + * @param defaultLocale fallback если chosenLocale нет в data + */ + public JsonNode flatten(JsonNode schema, JsonNode data, String chosenLocale, String defaultLocale) { + if (chosenLocale == null) { + return data; + } + if (data == null || !data.isObject()) { + return data; + } + ObjectNode out = data.deepCopy(); + walkAndFlatten(schema, out, chosenLocale, defaultLocale); + return out; + } + + private void walkAndFlatten(JsonNode schema, ObjectNode data, String locale, String defaultLocale) { + if (schema == null || !schema.isObject()) return; + JsonNode properties = schema.get("properties"); + if (properties == null || !properties.isObject()) return; + + Iterator> it = properties.fields(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + String fieldName = entry.getKey(); + JsonNode fieldSchema = entry.getValue(); + + JsonNode xLocalized = fieldSchema.get("x-localized"); + if (xLocalized != null && xLocalized.isBoolean() && xLocalized.asBoolean()) { + flattenLocalizedField(data, fieldName, locale, defaultLocale); + } else if (fieldSchema.has("properties") && data.has(fieldName) && data.get(fieldName).isObject()) { + walkAndFlatten(fieldSchema, (ObjectNode) data.get(fieldName), locale, defaultLocale); + } + } + } + + private void flattenLocalizedField(ObjectNode data, String field, String locale, String defaultLocale) { + JsonNode value = data.get(field); + if (value == null || !value.isObject()) return; + + JsonNode picked = value.get(locale); + if (picked == null || picked.isNull()) { + picked = value.get(defaultLocale); + } + if (picked == null || picked.isNull()) { + // ни запрошенной, ни default нет — оставляем null + data.set(field, objectMapper.nullNode()); + } else { + data.set(field, picked); + } + } +} diff --git a/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/locale/LocaleNegotiator.java b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/locale/LocaleNegotiator.java new file mode 100644 index 0000000..10419c1 --- /dev/null +++ b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/locale/LocaleNegotiator.java @@ -0,0 +1,56 @@ +package cloud.nstart.terravault.ordinis.readapi.locale; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** + * Locale negotiation per RFC 7231: парсит Accept-Language, lookup'ает по + * supported_locales справочника, fallback на default_locale. + * + *

Особенный случай — {@code Accept-Language: *} → возвращаем null, что + * означает «отдай как есть, не сплющивай». + */ +@Component +public class LocaleNegotiator { + + /** + * @return chosen locale (BCP 47 tag like "ru-RU"), либо null если клиент + * попросил wildcard "*" (= raw multi-locale в ответе). + */ + public String negotiate(String acceptLanguage, String[] supported, String defaultLocale) { + if (supported == null || supported.length == 0) { + return defaultLocale; + } + if (acceptLanguage == null || acceptLanguage.isBlank()) { + return defaultLocale; + } + if ("*".equals(acceptLanguage.trim())) { + return null; // raw mode + } + + try { + List ranges = Locale.LanguageRange.parse(acceptLanguage); + List supportedLocales = new ArrayList<>(supported.length); + for (String s : supported) supportedLocales.add(Locale.forLanguageTag(s)); + + Locale chosen = Locale.lookup(ranges, supportedLocales); + if (chosen != null) { + return chosen.toLanguageTag(); + } + } catch (IllegalArgumentException ignore) { + // malformed Accept-Language → fall through to default + } + return defaultLocale; + } + + /** + * Поддерживаемые локали справочника как list для удобства логирования. + */ + public List supportedAsList(String[] supported) { + return supported == null ? List.of() : Arrays.asList(supported); + } +} diff --git a/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/service/RecordReadService.java b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/service/RecordReadService.java new file mode 100644 index 0000000..9f921ca --- /dev/null +++ b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/service/RecordReadService.java @@ -0,0 +1,134 @@ +package cloud.nstart.terravault.ordinis.readapi.service; + +import cloud.nstart.terravault.ordinis.domain.DataScope; +import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinition; +import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinitionRepository; +import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecord; +import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecordRepository; +import cloud.nstart.terravault.ordinis.readapi.dto.FlattenedRecordResponse; +import cloud.nstart.terravault.ordinis.readapi.locale.LocaleAwareJsonFlattener; +import cloud.nstart.terravault.ordinis.readapi.locale.LocaleNegotiator; +import com.fasterxml.jackson.databind.JsonNode; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +/** + * Read-side. Negotiate locale → load record from replica с фильтрацией по + * scope → flatten multi-locale → ответ. + * + *

Подключение через transactional read-only — Hibernate использует replica + * connection (см. application.yml для read-api). + */ +@Service +public class RecordReadService { + + private final DictionaryDefinitionRepository definitionRepository; + private final DictionaryRecordRepository recordRepository; + private final LocaleNegotiator localeNegotiator; + private final LocaleAwareJsonFlattener flattener; + private final Timer queryTimer; + + public RecordReadService( + DictionaryDefinitionRepository definitionRepository, + DictionaryRecordRepository recordRepository, + LocaleNegotiator localeNegotiator, + LocaleAwareJsonFlattener flattener, + MeterRegistry meterRegistry) { + this.definitionRepository = definitionRepository; + this.recordRepository = recordRepository; + this.localeNegotiator = localeNegotiator; + this.flattener = flattener; + this.queryTimer = Timer.builder("ordinis_read_api_query_duration_seconds") + .description("Время чтения записи через read-api с locale negotiation") + .register(meterRegistry); + } + + public record ReadResult(FlattenedRecordResponse response, String chosenLocale) {} + + @Transactional(readOnly = true) + public ReadResult readActive( + String dictionaryName, + String businessKey, + String acceptLanguage, + List allowedScopes, + OffsetDateTime asOf) { + + Timer.Sample sample = Timer.start(); + try { + DictionaryDefinition def = definitionRepository.findByName(dictionaryName) + .orElseThrow(() -> new NoSuchElementException("Dictionary not found: " + dictionaryName)); + + if (!allowedScopes.contains(def.getScope())) { + throw new ScopeAccessDeniedException( + "Scope " + def.getScope() + " not in allowed scopes " + allowedScopes); + } + + OffsetDateTime when = asOf == null ? OffsetDateTime.now() : asOf; + Optional opt = recordRepository.findActiveAt(def.getId(), businessKey, when); + if (opt.isEmpty()) { + throw new NoSuchElementException( + "No active record for business_key=" + businessKey + " at " + when); + } + DictionaryRecord record = opt.get(); + + if (!allowedScopes.contains(record.getDataScope())) { + throw new ScopeAccessDeniedException( + "Record scope " + record.getDataScope() + " not in " + allowedScopes); + } + + String chosen = localeNegotiator.negotiate( + acceptLanguage, def.getSupportedLocales(), def.getDefaultLocale()); + + JsonNode flat = flattener.flatten(def.getSchemaJson(), record.getData(), chosen, def.getDefaultLocale()); + + return new ReadResult( + FlattenedRecordResponse.of(record, flat, chosen, def.getDefaultLocale(), when), + chosen); + } finally { + sample.stop(queryTimer); + } + } + + @Transactional(readOnly = true) + public List listActive( + String dictionaryName, + String acceptLanguage, + List allowedScopes, + OffsetDateTime asOf) { + + DictionaryDefinition def = definitionRepository.findByName(dictionaryName) + .orElseThrow(() -> new NoSuchElementException("Dictionary not found: " + dictionaryName)); + + if (!allowedScopes.contains(def.getScope())) { + throw new ScopeAccessDeniedException( + "Scope " + def.getScope() + " not in allowed scopes " + allowedScopes); + } + + OffsetDateTime when = asOf == null ? OffsetDateTime.now() : asOf; + List records = recordRepository.findActiveByDictionaryAndScopes( + def.getId(), allowedScopes, when); + + String chosen = localeNegotiator.negotiate( + acceptLanguage, def.getSupportedLocales(), def.getDefaultLocale()); + + return records.stream() + .map(r -> FlattenedRecordResponse.of( + r, + flattener.flatten(def.getSchemaJson(), r.getData(), chosen, def.getDefaultLocale()), + chosen, + def.getDefaultLocale(), + when)) + .toList(); + } + + public static class ScopeAccessDeniedException extends RuntimeException { + public ScopeAccessDeniedException(String message) { super(message); } + } +} diff --git a/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/web/DictionaryReadController.java b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/web/DictionaryReadController.java new file mode 100644 index 0000000..4811107 --- /dev/null +++ b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/web/DictionaryReadController.java @@ -0,0 +1,87 @@ +package cloud.nstart.terravault.ordinis.readapi.web; + +import cloud.nstart.terravault.ordinis.domain.DataScope; +import cloud.nstart.terravault.ordinis.readapi.dto.FlattenedRecordResponse; +import cloud.nstart.terravault.ordinis.readapi.service.RecordReadService; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.OffsetDateTime; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Read API для consumers (геопортал, Альтум). Фронт публичный — здесь + * locale negotiation + scope filtering. + * + *

Scope authorisation: в v1 берём из query param {@code as_scope} (для + * dev/curl). В prod — из JWT (@nstart/auth) — сделаем через filter позже. + */ +@RestController +@RequestMapping("/api/v1/{dictionaryName}/records") +public class DictionaryReadController { + + private final RecordReadService service; + private final Counter scopeDeniedCounter; + + public DictionaryReadController(RecordReadService service, MeterRegistry meterRegistry) { + this.service = service; + this.scopeDeniedCounter = Counter.builder("ordinis_scope_access_denied_total") + .description("Попытки чтения недоступного scope (v1 — из query param)") + .register(meterRegistry); + } + + @GetMapping("/{businessKey}") + public ResponseEntity getActive( + @PathVariable String dictionaryName, + @PathVariable String businessKey, + @RequestHeader(value = HttpHeaders.ACCEPT_LANGUAGE, required = false) String acceptLanguage, + @RequestParam(value = "as_scope", defaultValue = "PUBLIC") String asScope, + @RequestParam(value = "at", required = false) OffsetDateTime at) { + + List scopes = parseScopes(asScope); + var result = service.readActive(dictionaryName, businessKey, acceptLanguage, scopes, at); + + HttpHeaders headers = new HttpHeaders(); + if (result.chosenLocale() != null) { + headers.add(HttpHeaders.CONTENT_LANGUAGE, result.chosenLocale()); + } + headers.add(HttpHeaders.VARY, HttpHeaders.ACCEPT_LANGUAGE); + return new ResponseEntity<>(result.response(), headers, 200); + } + + @GetMapping + public ResponseEntity> list( + @PathVariable String dictionaryName, + @RequestHeader(value = HttpHeaders.ACCEPT_LANGUAGE, required = false) String acceptLanguage, + @RequestParam(value = "as_scope", defaultValue = "PUBLIC") String asScope, + @RequestParam(value = "at", required = false) OffsetDateTime at) { + + List scopes = parseScopes(asScope); + var items = service.listActive(dictionaryName, acceptLanguage, scopes, at); + + HttpHeaders headers = new HttpHeaders(); + if (!items.isEmpty() && items.get(0)._meta() != null && items.get(0)._meta().locale() != null) { + headers.add(HttpHeaders.CONTENT_LANGUAGE, items.get(0)._meta().locale()); + } + headers.add(HttpHeaders.VARY, HttpHeaders.ACCEPT_LANGUAGE); + return new ResponseEntity<>(items, headers, 200); + } + + private List parseScopes(String asScope) { + return Stream.of(asScope.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(s -> DataScope.valueOf(s.toUpperCase())) + .collect(Collectors.toList()); + } +} diff --git a/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/web/ReadApiExceptionHandler.java b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/web/ReadApiExceptionHandler.java new file mode 100644 index 0000000..c811e82 --- /dev/null +++ b/ordinis-read-api/src/main/java/cloud/nstart/terravault/ordinis/readapi/web/ReadApiExceptionHandler.java @@ -0,0 +1,63 @@ +package cloud.nstart.terravault.ordinis.readapi.web; + +import cloud.nstart.terravault.ordinis.readapi.service.RecordReadService.ScopeAccessDeniedException; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.micrometer.tracing.Tracer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.OffsetDateTime; +import java.util.NoSuchElementException; + +@RestControllerAdvice +public class ReadApiExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(ReadApiExceptionHandler.class); + + private final ObjectProvider tracerProvider; + + public ReadApiExceptionHandler(ObjectProvider tracerProvider) { + this.tracerProvider = tracerProvider; + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ErrorBody(int status, String code, String message, String traceId, OffsetDateTime timestamp) {} + + @ExceptionHandler(NoSuchElementException.class) + public ResponseEntity onNotFound(NoSuchElementException e) { + return body(HttpStatus.NOT_FOUND, "not_found", e.getMessage()); + } + + @ExceptionHandler(ScopeAccessDeniedException.class) + public ResponseEntity onScope(ScopeAccessDeniedException e) { + return body(HttpStatus.FORBIDDEN, "scope_access_denied", e.getMessage()); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity onBadRequest(IllegalArgumentException e) { + return body(HttpStatus.BAD_REQUEST, "bad_request", e.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity onGeneric(Exception e) { + log.error("Unhandled exception in read-api", e); + return body(HttpStatus.INTERNAL_SERVER_ERROR, "internal_error", "Internal server error"); + } + + private ResponseEntity body(HttpStatus status, String code, String message) { + return ResponseEntity.status(status) + .body(new ErrorBody(status.value(), code, message, currentTraceId(), OffsetDateTime.now())); + } + + private String currentTraceId() { + Tracer tracer = tracerProvider.getIfAvailable(); + if (tracer == null) return null; + var span = tracer.currentSpan(); + return span == null ? null : span.context().traceId(); + } +} diff --git a/ordinis-read-api/src/main/resources/application.yml b/ordinis-read-api/src/main/resources/application.yml new file mode 100644 index 0000000..70ef23f --- /dev/null +++ b/ordinis-read-api/src/main/resources/application.yml @@ -0,0 +1,119 @@ +spring: + application: + name: ordinis-read-api + profiles: + active: ${SPRING_PROFILES_ACTIVE:dev} + +server: + port: 8081 + shutdown: graceful + +management: + endpoints: + web: + exposure: + include: health,info,prometheus,refresh,metrics,env,mappings + endpoint: + health: + probes: + enabled: true + show-details: when-authorized + group: + liveness: + include: livenessState + readiness: + include: readinessState,db + prometheus: + metrics: + export: + enabled: true + metrics: + tags: + application: ${spring.application.name} + environment: ${ENVIRONMENT:dev} + +otel: + exporter: + otlp: + endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://tempo.monitoring:4317} + resource: + attributes: + service.name: ${spring.application.name} + service.namespace: ordinis + deployment.environment: ${ENVIRONMENT:dev} + traces: + sampler: parentbased_traceidratio + sampler.arg: ${ORDINIS_TRACING_SAMPLING:0.1} + +logging: + level: + root: INFO + cloud.nstart.terravault.ordinis: DEBUG + +--- +# DEV: подключение к replica через port-forward. +# Для read-api используем `ordinis-postgres-ro` сервис (read-only replica). +# Через port-forward в local dev можно использовать тот же ordinis-postgres-rw +# (single-node cluster, replica = тот же primary), важна только конвенция. +spring: + config: + activate: + on-profile: dev + cloud: + config: + enabled: false + vault: + enabled: false + autoconfigure: + exclude: + - org.springframework.cloud.vault.config.VaultAutoConfiguration + - org.springframework.cloud.vault.config.VaultObservationAutoConfiguration + - org.springframework.cloud.vault.config.VaultReactiveAutoConfiguration + datasource: + url: jdbc:postgresql://${ORDINIS_PG_HOST:localhost}:${ORDINIS_PG_PORT:5433}/${ORDINIS_PG_DB:ordinis} + username: ${ORDINIS_PG_USER:ordinis_app} + password: ${ORDINIS_PG_PASSWORD:} + driver-class-name: org.postgresql.Driver + hikari: + read-only: true + jpa: + hibernate: + ddl-auto: none + properties: + hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect + open-in-view: false + +otel: + sdk: + disabled: true + +--- +# K8s: подключение к replica + Vault для credentials, Cloud Config для feature flags. +spring: + config: + activate: + on-profile: k8s + import: + - "configserver:${SPRING_CLOUD_CONFIG_URI:http://config-server.ordinis-dev:8888}" + - "vault://" + cloud: + config: + enabled: true + vault: + enabled: true + uri: ${VAULT_URI:http://vault.config:8200} + authentication: KUBERNETES + kubernetes: + role: ordinis-read-api + service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token + kv: + enabled: true + backend: secret + application-name: ordinis/cuod + datasource: + hikari: + read-only: true + jpa: + hibernate: + ddl-auto: none + open-in-view: false diff --git a/ordinis-read-api/src/main/resources/logback-spring.xml b/ordinis-read-api/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..3dbf800 --- /dev/null +++ b/ordinis-read-api/src/main/resources/logback-spring.xml @@ -0,0 +1,38 @@ + + + + + + + + trace_id + span_id + scope + dictionary + locale + request_id + user_id + {"service":"${appName}","environment":"${environment}"} + + + + + + %d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n + + + + + + + + + + + + + + + + +