Распределенная трассировка

Отслеживание  —  самая сложная часть микросервисов. Решением для нее является распределенная трассировка, благодаря которой разработчики мониторят и визуализируют протекание запросов по сложным распределенным системам.

Что такое OpenTelemetry?

Это универсальный стандарт, которым предоставляются метрики, журналы, трассировки в распределенных приложениях и обеспечивается комплексная наблюдаемость без привязки к конкретной платформе мониторинга. OpenTelemetry также предоставляются:

  1. Автоматическое и ручное инструментирование кода.
  2. Совместимость со многими серверными системами.

Настройка приложения Spring Boot 3 с OpenTelemetry

Сконфигурируем OpenTelemetry для приложения Gradle. Ниже приведены внесенные изменения.

Структура проекта

Создадим два микросервиса:

  • Служба заказов с портом 8084: ею обрабатываются операции над заказами и вызывается служба ценообразования.
  • Служба ценообразования с портом 8083: ею предоставляется информация о ценах.
opentelemetry-spring-boot/
├── order-service/
│ ├── src/
│ └── build.gradle
├── price-service/
│ ├── src/
│ └── build.gradle
├── build.gradle
├── settings.gradle
├── docker-compose.yml
└── otel-config.yml

Основные зависимости

Вот ключевые зависимости:

dependencies {
// Основные зависимости Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'

// Основные зависимости OpenTelemetry
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
implementation 'io.opentelemetry:opentelemetry-api'
implementation 'io.opentelemetry:opentelemetry-sdk'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
implementation 'io.opentelemetry.instrumentation:opentelemetry-instrumentation-api'
implementation 'io.opentelemetry:opentelemetry-exporter-logging'
}

Подробнее о ключевых зависимостях:

  • micrometer-tracing-bridge-otel: трассировка Micrometer соединяется с OpenTelemetry.
  • opentelemetry-api: основной API-интерфейс OpenTelemetry.
  • opentelemetry-sdk: реализация API-интерфейса OpenTelemetry.
  • opentelemetry-exporter-otlp: данные телеметрии экспортируются по протоколу OTLP.

Реализация сервиса

Служба заказов:

@RestController
@RequestMapping("/orders")
public class OrderController {
private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class);
private final PriceGateway priceGateway;

@Autowired
public OrderController(PriceGateway priceGateway) {
this.priceGateway = priceGateway;
}

@GetMapping("/{id}")
public Order findById(@PathVariable Long id) {
// Логируется с контекстом трассировки
LOGGER.info("Processing order request for id: {}", id);

// Служба ценообразования вызывается через шлюз
Price price = priceGateway.getPrice(id);

// Создается и возвращается заказ
return new Order(id, 1L, ZonedDateTime.now(), price.getAmount());
}
}

Шлюз ценообразования, важен для распределенной трассировки:

@Component
public class PriceGateway {
private final RestTemplate restTemplate;
private static final Logger LOGGER = LoggerFactory.getLogger(PriceGateway.class);
private static final String BASE_URL = "http://localhost:8083";

@Autowired
public PriceGateway(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

public Price getPrice(long productId) {
LOGGER.info("Fetching price details for product: {}", productId);
String url = String.format("%s/prices/%d", BASE_URL, productId);

try {
ResponseEntity<Price> response = restTemplate.getForEntity(url, Price.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
return response.getBody();
}
throw new RuntimeException("Failed to fetch price");
} catch (Exception e) {
LOGGER.error("Error fetching price: {}", e.getMessage());
throw new RuntimeException("Price service communication failed", e);
}
}
}

Конфигурация RestTemplate, важна для распространения трассировки:

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

Служба ценообразования:

@RestController
@RequestMapping("/prices")
public class PriceController {
private final static Logger LOGGER = LoggerFactory.getLogger(PriceController.class);

@GetMapping("/{id}")
public Price findById(@PathVariable Long id) {
LOGGER.info("Retrieving price for product: {}", id);

// Моделируется продолжительность обработки
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

return new Price(id, BigDecimal.valueOf(Math.random() * 100));
}
}

Конфигурация OpenTelemetry

Свойства приложения

Файл свойств важен для корректной настройки OpenTelemetry:

# Конфигурация службы
spring.application.name=order-service
server.port=8084

# Основная конфигурация OpenTelemetry
otel.service.name=${spring.application.name}
otel.exporter.otlp.endpoint=http://localhost:4317
otel.traces.exporter=otlp
otel.metrics.exporter=otlp
otel.logs.exporter=otlp

# Конфигурация выборки
management.tracing.sampling.probability=1.0

# Шаблон логирования с идентификаторами трассировок и спанов
logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]

# Настройки инструментирования
otel.instrumentation.spring-webmvc.enabled=true
otel.instrumentation.spring-webflux.enabled=true
otel.resource.attributes=deployment.environment=development

Конфигурация коллектора OpenTelemetry:

receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318

processors:
batch:
timeout: 1s
send_batch_size: 1024
attributes:
actions:
- key: service.name
action: upsert
from_attribute: service.name

exporters:
logging:
loglevel: debug
jaeger:
endpoint: jaeger:14250
tls:
insecure: true

service:
pipelines:
traces:
receivers: [otlp]
processors: [batch, attributes]
exporters: [logging, jaeger]

Настройка Docker

Dockerfile службы:

FROM eclipse-temurin:17-jdk
WORKDIR /app
COPY build/libs/*.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java","-jar","app.jar"]

Конфигурация Docker Compose:

version: '3.8'
services:
order-service:
build:
context: ./order-service
dockerfile: Dockerfile
ports:
- "8084:8084"
environment:
- SPRING_PROFILES_ACTIVE=docker
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
networks:
- otel-network

price-service:
build:
context: ./price-service
dockerfile: Dockerfile
ports:
- "8083:8083"
environment:
- SPRING_PROFILES_ACTIVE=docker
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
networks:
- otel-network

otel-collector:
image: otel/opentelemetry-collector:0.88.0
volumes:
- ./otel-config.yml:/etc/otel-collector-config.yml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "8888:8888" # Метрики Prometheus
networks:
- otel-network

jaeger:
image: jaegertracing/all-in-one:1.47
environment:
- COLLECTOR_OTLP_ENABLED=true
ports:
- "16686:16686" # Пользовательский интерфейс
- "14250:14250" # Коллектор
networks:
- otel-network

networks:
otel-network:
driver: bridge

Изменения кода завершены, теперь создадим приложение.

Запуск и тестирование

  1. Собираем проект:
./gradlew clean build

2. Запускаем контейнеры Docker:

docker-compose up --build

3. Проверяем, запускаются ли службы:

docker ps

После этой команды видим такие результаты:

4. Затем тестируем конечную точку с помощью команды curl или postman:

curl http://localhost:8080/product/1
curl http://localhost:8081/price/1

Если вызов API прерывается, прибегаем к таким командам:

# Просматриваются логи службы продуктов
docker-compose logs -f product-service

# Просматриваются логи службы ценообразования
docker-compose logs -f price-service

# Просматриваются логи коллектора OpenTelemetry
docker-compose logs -f otel-collector

# Просматриваются логи Jaeger
docker-compose logs -f jaeger

5. Получаем доступ к пользовательскому интерфейсу Jaeger:

  • Вводим в браузере: http://localhost:16686.
  • Выбираем из выпадающего списка службу.
  • Нажимаем Find Traces и видим распределенные трассировки.

Появится такой же пользовательский интерфейс. Финальный вызов API отслеживается до того места, где был выполнен. В этом красота OpenTelemetry.

Устранение неполадок

Типичные проблемы и их решения:

  1. В Jaeger не появляются трассировки:
  • Проверяем конфигурацию конечной точки OTLP.
  • Проверяем логи коллектора: docker-compose logs otel-collector.
  • Вероятность выборки задаем 1,0.

2. Службы не «общаются»:

  • Проверяем конфигурацию сети в docker-compose.
  • Проверяем порты и URL-адреса служб.
  • Проверяем логи служб: docker-compose logs service-name.

3. Контекст трассировки не распространяется:

  • Корректно конфигурируем RestTemplate.
  • Проверяем наличие идентификаторов трассировки в шаблоне логирования.
  • Проверяем настройки инструментирования OpenTelemetry.

Рекомендации

Стратегия выборки:

  • Использовать 1,0 при разработке.
  • Для продакшена делать корректировку, исходя из трафика.

Логирование:

  • Всегда включать идентификаторы трассировки и спанов.
  • Использовать согласованные уровни ведения журнала.
  • Добавлять в логи содержательный контекст.

Принадлежность ресурсов:

  • Помечать трассировки с окружением.
  • Добавлять версию службы.
  • Включать информацию о развертывании.

Обработка ошибок:

  • Корректно распространять и логировать ошибки.
  • Включать в трассировки контекст ошибки.
  • Использовать соответствующие коды состояния HTTP.

Стратегия выборки:

  • Использовать 1,0 при разработке.
  • Для продакшена делать корректировку, исходя из трафика.

Логирование:

  • Всегда включать идентификаторы трассировки и спанов.
  • Использовать согласованные уровни ведения журнала.
  • Добавлять в логи содержательный контекст.

Принадлежность ресурсов:

  • Помечать трассировки с окружением.
  • Добавлять версию службы.
  • Включать информацию о развертывании.

Обработка ошибок:

  • Корректно распространять и логировать ошибки.
  • Включать в трассировки контекст ошибки.
  • Использовать соответствующие коды состояния HTTP.

Заключение

Распределенная трассировка  —  это передовой подход к мониторингу микросервисов и прояснению сложных потоков запросов. OpenTelemetry является универсальным решением, которым обеспечивается наблюдаемость в распределенных системах.

Интеграцией OpenTelemetry в приложение Spring Boot 3 реализуется сквозная трассировка, благодаря которой упрощаются отладка и мониторинг производительности. А такими инструментами, как Jaeger и OpenTelemetry Collector, разработчики визуализируют трассировки и эффективнее оптимизируют микросервисы. Этой настройкой обеспечивается масштабируемый и сопровождаемый подход к наблюдаемости, необходимый для современных приложений.

Код без наблюдаемости  —  все равно что навигация в темноте. С OpenTelemetry не просто пишешь код  —  создаешь систему, которую можно понять, отладить, доработать. Благодарим за внимание, и пусть ваши трассировки всегда будут суперинформативными!

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Chanuka Dinuwan: Implementing Distributed Tracing with OpenTelemetry and Spring Boot 3

Предыдущая статьяЗачем использовать RTK Query для API-вызовов в React
Следующая статья7 лучших ресурсов для iOS-разработчиков в 2025 году