Ручное внедрение OpenTelemetry в Java-приложение: различия между версиями
(Новая страница: «В этом пошаговом руководстве показано, как добавить возможность наблюдения в ваше '''Java-п...») |
(нет различий)
|
Текущая версия на 15:40, 9 октября 2025
В этом пошаговом руководстве показано, как добавить возможность наблюдения в ваше Java-приложение с помощью библиотек ручного инструментария и инструментов, предоставляемых OpenTelemetry Java.
Получите данные для доступа к Ключ-АСТРОМ
Определение базового URL API
Подробную информацию о том, как собрать базовый URL-адрес конечной точки OTLP, см. в разделе Экспорт с помощью OTLP .
URL-адрес должен заканчиваться на /api/v2/otlp.
Получение токена доступа API
Токен доступа для сбора трассировок, логов и метрик можно создать в разделе Токены доступа .
Экспорт с помощью OTLP содержит более подробную информацию о формате и необходимых областях доступа.
Инструментируйте свое приложение
С автоматической настройкой SDK
1. Добавьте текущие версии следующих пакетов в конфигурацию вашего пакета (например, Maven, Gradle).
2. Настройте следующие переменные среды, чтобы задать предпочтение временности deltaи определить параметры экспорта, заменив [URL] и [TOKEN] значениями для базового URL-адреса и токена доступа.
| OTEL_EXPORTER_OTLP_ENDPOINT=[URL]
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Api-Token [TOKEN]" OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf OTEL_RESOURCE_ATTRIBUTES="service.name=java-quickstart,service.version=1.0.1" OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta |
3. Добавьте следующие операторы импорта в класс запуска, который инициирует загрузку вашего приложения.
| import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.instrumentation.log4j.appender.v2_17.OpenTelemetryAppender; |
4. Добавьте метод initOpenTelemetry в класс запуска и вызовите его как можно раньше при запуске приложения. Это инициализирует OpenTelemetry для бэкенда Ключ-АСТРОМ и создаст поставщиков трассировщиков и счётчиков по умолчанию.
| private static void initOpenTelemetry() {
OpenTelemetrySdk sdk = AutoConfiguredOpenTelemetrySdk.builder().addResourceCustomizer((resource, properties) -> { Resource dtMetadata = Resource.empty(); for (String name : new String[]{"dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties", "/var/lib/astromkey/enrichment/dt_metadata.properties"}) { try { Properties props = new Properties(); props.load(name.startsWith("/var") ? new FileInputStream(name) : new FileInputStream(Files.readAllLines(Paths.get(name)).get(0))); dtMetadata = dtMetadata.merge(Resource.create(props.entrySet().stream() .collect(Attributes::builder, (b, e) -> b.put(e.getKey().toString(), e.getValue().toString()), (b1, b2) -> b1.putAll(b2.build())) .build()) ); } catch (IOException e) { } } return resource.merge(dtMetadata); }).build().getOpenTelemetrySdk(); OpenTelemetryAppender.install(sdk); } |
Обогащение данных Ключ-АСТРОМ
Операции чтения файлов, анализирующие файлы dt_metadata в примере кода, пытаются прочитать файлы данных ЕдиногоАгента, чтобы обогатить запрос OTLP и гарантировать, что вся соответствующая информация о топологии доступна в Ключ-АСТРОМ.
Без автонастройки SDK
1. Добавьте текущие версии следующих пакетов в конфигурацию вашего пакета (например, Maven, Gradle).
2. Добавьте текущую версию opentelemetry-log4j-appender-2.17 в качестве библиотеки времени выполнения в конфигурацию вашего пакета (область для Maven runtime, для Gradle runtimeOnly).
3. Добавьте следующие операторы импорта в класс запуска, который инициирует загрузку вашего приложения.
| import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; import io.opentelemetry.instrumentation.log4j.appender.v2_17.OpenTelemetryAppender; |
4. При приеме данных с использованием OTLP добавьте в класс запуска два поля для URL-адреса Ключ-АСТРОМ и токена доступа.
| private static final String DT_API_URL = ""; // TODO: Provide your SaaS/Managed URL here
private static final String DT_API_TOKEN = ""; // TODO: Provide the OpenTelemetry-scoped access token here |
5. Настройте имя службы с помощью переменной среды OTEL_SERVICE_NAME.
| OTEL_SERVICE_NAME=java-quickstart |
6. Добавьте метод initOpenTelemetry в класс инициализатора и вызовите его как можно раньше при запуске приложения. Это инициализирует OpenTelemetry для бэкенда Ключ-АСТРОМ и создаст поставщиков трассировщиков и счётчиков по умолчанию.
| private static void initOpenTelemetry()
{ // ===== GENERAL SETUP ===== // Read service name from the environment variable OTEL_SERVICE_NAME, if present Resource serviceName = Optional.ofNullable(System.getenv("OTEL_SERVICE_NAME")) .map(n -> Attributes.of(AttributeKey.stringKey("service.name"), n)) .map(Resource::create) .orElseGet(Resource::empty); // Parse the environment variable OTEL_RESOURCE_ATTRIBUTES into key-value pairs Resource envResourceAttributes = Resource.create(Stream.of(Optional.ofNullable(System.getenv("OTEL_RESOURCE_ATTRIBUTES")).orElse("").split(",")) .filter(pair -> pair != null && pair.length() > 0 && pair.contains("=")) .map(pair -> pair.split("=")) .filter(pair -> pair.length == 2) .collect(Attributes::builder, (b, p) -> b.put(p[0], p[1]), (b1, b2) -> b1.putAll(b2.build())) .build() ); // Define enrichment files String[] files = new String[] { "dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties", "/var/lib/astromkey/enrichment/dt_metadata.properties", "/var/lib/astromkey/enrichment/dt_host_metadata.properties" }; // Read host information from OneAgent files to enrich telemetry Resource dtMetadata = Resource.empty(); for (String name : files) { try { Properties props = new Properties(); props.load(name.startsWith("/var") ? new FileInputStream(name) : new FileInputStream(Files.readAllLines(Paths.get(name)).get(0))); dtMetadata = dtMetadata.merge(Resource.create( props.entrySet().stream() .collect(Attributes::builder, (b, e) -> b.put(e.getKey().toString(), e.getValue().toString()), (b1, b2) -> b1.putAll(b2.build())) .build() )); } catch (IOException e) {} } // ===== TRACING SETUP ===== // Configure span exporter with the astromkey URL and the API token SpanExporter exporter = OtlpHttpSpanExporter.builder() .setEndpoint(DT_API_URL + "/v1/traces") .addHeader("Authorization", "Api-Token " + DT_API_TOKEN) .build(); // Set up tracer provider with a batch processor and the span exporter SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() .setResource(Resource.getDefault().merge(envResourceAttributes).merge(serviceName).merge(dtMetadata)) .setSampler(Sampler.alwaysOn()) .addSpanProcessor(BatchSpanProcessor.builder(exporter).build()) .build(); // ===== METRIC SETUP ===== // Configure metric exporter with the astromkey URL and the API token OtlpHttpMetricExporter metricExporter = OtlpHttpMetricExporter.builder() .setEndpoint(DT_API_URL + "/v1/metrics") .addHeader("Authorization", "Api-Token " + DT_API_TOKEN) .setAggregationTemporalitySelector(AggregationTemporalitySelector.deltaPreferred()) .build(); // Set up meter provider with a periodic reader and the metric exporter SdkMeterProvider meterProvider = SdkMeterProvider.builder() .setResource(Resource.getDefault().merge(envResourceAttributes).merge(serviceName).merge(dtMetadata)) .registerMetricReader(PeriodicMetricReader.builder(metricExporter).build()) .build(); // ===== LOG SETUP ===== // Configure log exporter with the astromkey URL and the API token OtlpHttpLogRecordExporter logExporter = OtlpHttpLogRecordExporter.builder() .setEndpoint(DT_API_URL + "/v1/logs") .addHeader("Authorization", "Api-Token " + DT_API_TOKEN) .build(); // Set up log provider with the log exporter SdkLoggerProvider sdkLoggerProvider = SdkLoggerProvider.builder() .setResource(Resource.getDefault().merge(envResourceAttributes).merge(serviceName).merge(dtMetadata)) .addLogRecordProcessor(BatchLogRecordProcessor.builder(logExporter).build()) .build(); // ===== INITIALIZATION ===== // Initialize OpenTelemetry with the tracer and meter providers OpenTelemetrySdk sdk = OpenTelemetrySdk.builder() .setTracerProvider(sdkTracerProvider) .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) .setMeterProvider(meterProvider) .setLoggerProvider(sdkLoggerProvider) .buildAndRegisterGlobal(); // Runtime.getRuntime().addShutdownHook(new Thread(sdkTracerProvider::close)); OpenTelemetryAppender.install(sdk); } |
Обогащение данных Ключ-АСТРОМ
Операции чтения файлов, анализирующие файлы dt_metadata в примере кода, пытаются прочитать файлы данных ЕдиногоАгента, чтобы обогатить запрос OTLP и гарантировать, что вся соответствующая информация о топологии доступна в Ключ-АСТРОМ.
Добаление телеметрических сигналов вручную (необязательно)
Создание интервалов
1. Для создания новых интервалов нам сначала нужен объект трассировки.
| Tracer tracer = GlobalOpenTelemetry
.getTracerProvider() .tracerBuilder("my-tracer") //TODO Replace with the name of your tracer .build(); |
2. Теперь с помощью tracer, мы можем использовать конструктор интервалов для создания и запуска новых интервалов.
| // Obtain and name new span from tracer
Span span = tracer.spanBuilder("Call to /myendpoint") .setSpanKind(SpanKind.CLIENT) .startSpan(); // Set demo span attributes using semantic naming span.setAttribute("http.method", "GET"); span.setAttribute("net.protocol.version", "1.1"); // Set the span as current span and parent for future child spans try (Scope scope = span.makeCurrent()) { // TODO your code goes here } finally { // Completing the span span.end(); } |
В приведенном выше коде мы:
- Создали новый диапазон и назвали его «Call to /myendpoint».
- Добавили два атрибута, следуя семантическому соглашению об именовании, специфичные для действия этого диапазона: информацию о методе HTTP и версии.
- Использовали метод span
makeCurrent(), чтобы отметить его как активный span и родительский для будущих span (пока span не завершится) - Вызвали метод span
end()для завершения span (в блокеfinally, чтобы гарантировать вызов метода)
Сборка метрик
1. Для создания новых инструментов измерения нам сначала нужен объект счетчика.
| Meter meter = GlobalOpenTelemetry
.getMeterProvider() .meterBuilder("my-meter") //TODO Replace with the name of your meter .build(); |
2. С помощью meter мы теперь можем создавать отдельные инструменты, например, счетчик.
| LongCounter counter = meter.counterBuilder("request_counter")
.setDescription("The number of requests we received") .setUnit() .build(); |
3. Теперь мы можем вызвать метод add() для записи новых значений counter с помощью нашего счетчика и сохранения дополнительных атрибутов (например, action.type).
| Attributes attrs = Attributes.of(stringKey("action.type"), "create");
counter.add(1, attrs); |
Вы также можете создать асинхронный датчик, для которого потребуется функция обратного вызова, которая будет вызываться OpenTelemetry при сборе данных.
В следующем примере при каждом вызове записывается доступная память, а также атрибут количества активных пользователей, полученный из вымышленного метода getUserCount().
| meter.gaugeBuilder("free_memory")
.setDescription("Available memory in bytes") .setUnit("bytes") .buildWithCallback(measurement -> { measurement.record( Runtime.getRuntime().freeMemory(), Attributes.of(stringKey("user_count"), getUserCount()) ); }); |
Подключение логов
Сначала вам необходимо настроить файл конфигурации Log4j 2 log4j.xml, чтобы включить в него приложение OpenTelemetry.
| <?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" packages="io.opentelemetry.instrumentation.log4j.appender.v2_17"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} trace_id: %X{trace_id} span_id: %X{span_id} trace_flags: %X{trace_flags} - %msg%n"/> </Console> <OpenTelemetry name="OpenTelemetryAppender"/> </Appenders> <Loggers> <Root> <AppenderRef ref="OpenTelemetryAppender" level="All"/> <AppenderRef ref="Console" level="All"/> </Root> </Loggers> </Configuration> |
В этой конфигурации мы добавили новую запись <OpenTelemetry> в раздел <Appenders>, а также запись <AppenderRef> в раздел <Loggers>.
С помощью вызова GlobalLoggerProvider, который мы ранее выполнили в разделе Настройка, этот аппендер настраивается для бэкэнда Ключ-АСТРОМ.
Обеспечение распространения контекста
Распространение контекста особенно важно, когда задействованы сетевые вызовы (например, REST).
Если вы используете автоматическое инструментирование и ваши сетевые библиотеки также им охватываются, то это будет автоматически реализовано библиотеками инструментирования. В противном случае ваш код должен это учитывать.
Извлечение контекста при получении запроса
В следующем примере мы предполагаем, что получили сетевой вызов через com.sun.net.httpserver.HttpExchange и определяем экземпляр TextMapGetter для извлечения контекстной информации из HTTP-заголовков. Затем мы передаем этот экземпляр в extract(), возвращая объект контекста, что позволяет нам продолжить предыдущую трассировку с нашими интервалами.
| //The getter will be used for incoming requests
TextMapGetter<HttpExchange> getter = new TextMapGetter<>() { @Override public String get(HttpExchange carrier, String key) { if (carrier.getRequestHeaders().containsKey(key)) { return carrier.getRequestHeaders().get(key).get(0); } return null; } @Override public Iterable<String> keys(HttpExchange carrier) { return carrier.getRequestHeaders().keySet(); } }; @Override public void handle(HttpExchange httpExchange) { //Extract the SpanContext and other elements from the request Context extractedContext = GlobalOpenTelemetry.getPropagators().getTextMapPropagator() .extract(Context.current(), httpExchange, getter); try (Scope scope = extractedContext.makeCurrent()) { //This will automatically propagate context by creating child spans within the extracted context Span serverSpan = tracer.spanBuilder("my-server-span") //TODO Replace with the name of your span .setSpanKind(SpanKind.SERVER) //TODO Set the kind of your span .startSpan(); serverSpan.setAttribute(SemanticAttributes.HTTP_METHOD, "GET"); //TODO Add attributes serverSpan.end(); } } |
Внедрение контекста при отправке запросов
В следующем примере мы отправляем REST-запрос к другой службе и предоставляем наш существующий контекст как часть HTTP-заголовков нашего запроса.
Для этого мы определяем экземпляр TextMapSetter, который добавляет соответствующую информацию с помощью setRequestProperty(). После создания экземпляра REST-объекта мы передаем его вместе с контекстом и экземпляром сеттера в inject(), который добавит необходимые заголовки к запросу.
| //The setter will be used for outgoing requests
TextMapSetter<HttpURLConnection> setter = (carrier, key, value) -> { assert carrier != null; // Insert the context as Header carrier.setRequestProperty(key, value); }; URL url = new URL("<URL>"); //TODO Replace with the URL of the service to be called Span outGoing = tracer.spanBuilder("my-client-span") //TODO Replace with the name of your span .setSpanKind(SpanKind.CLIENT) //TODO Set the kind of your span .startSpan(); try (Scope scope = outGoing.makeCurrent()) { outGoing.setAttribute(SemanticAttributes.HTTP_METHOD, "GET"); //TODO Add attributes HttpURLConnection transportLayer = (HttpURLConnection) url.openConnection(); // Inject the request with the *current* Context, which contains our current span GlobalOpenTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), transportLayer, setter); // Make outgoing call } finally { outGoing.end(); } |
Настройка сбора данных в соответствии с требованиями конфиденциальности (необязательно)
Хотя Ключ-АСТРОМ автоматически собирает все атрибуты OpenTelemetry, в веб-интерфейсе Ключ-АСТРОМ сохраняются и отображаются только значения атрибутов, указанные в списке разрешенных. Это предотвращает случайное сохранение персональных данных, позволяя вам соблюдать требования к конфиденциальности и контролировать объем хранимых данных мониторинга.
Чтобы просматривать пользовательские атрибуты, необходимо сначала разрешить их использование в веб-интерфейсе Ключ-АСТРОМ.
Проверка загрузки данных в Ключ-АСТРОМ
После завершения инструментирования вашего приложения выполните несколько тестовых действий для создания и отправки демонстрационных трассировок, метрик и логов, а также проверьте, что они были правильно загружены в Ключ-АСТРОМ.
Чтобы сделать это для трассировок, перейдите в раздел Трассировки и выберите вкладку Распределенные трассировки. Если вы используете ЕдиныйАгент, выберите PurePaths .
Для просмотра метрик и логов перейдите в раздел Метрики или Журналы или Журналы и события.