Skip to content

Support declarative config for spring starter #14048

Open
@zeitlinger

Description

@zeitlinger

Supersedes open-telemetry/opentelemetry-specification#4428 with a simpler approach.

Is your feature request related to a problem? Please describe.

#14014 adds support for declarative config - except for the spring starter.

The spring starter already supports yaml syntax as part of the spring configuration system.

Therefore, we either need to wait for v3 - or give users a way to opt-in to the new declarative config.

Describe the solution you'd like

The entry point into the configuration is the same, but the structure is different.
See https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/sdk-configuration/

Old format:

application.yaml:

otel:
  resource:
    attributes:
      deployment.environment: dev
      service:
        name: cart
        namespace: shop

New format:

application.yaml:

spring:
  application:
    name: demo-app
otel:
  # "file_format" serves as opt-in to the new file format 
  # it's not a clear indication that this is experimental though
  file_format: "0.4" 
  log_level: debug
  resource:
    attributes:
      - name: foo
        value: bar
  tracer_provider:
    processors:
      - simple:
          exporter:
            console:

Plan for the implementation:

  1. Read the spring config file in a rather hacky way, parsing a string.
    • It seems a bit fragile, but the alternative is to create a spring adapter for every property,
      similar to the adapter for the old config file.
    • Spring Boot is very conservative about everything, so I think it's fine to use string parsing here.
  2. Extract the otel YAML node
  3. Write the contents to a temporary file
  4. Set the otel.experimental.config.file system property to the path of the temporary file (would be great to extend the SDK autoconfig to not need a temp file and setting a system property)
  5. Initialize the OpenTelemetry SDK
  6. Delete the temporary file after the SDK is initialized

Here's the PoC code that implements this plan:

package com.grafana.demo;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

	private Pattern pattern = Pattern.compile(
            "^Config resource 'class path resource \\[(.+)]' via location 'optional:classpath:/'$"
	);

	@EventListener
	public void on(ApplicationReadyEvent event) throws IOException {
		// get the application.yaml file that was used to start the application
		ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();
		if (environment.getProperty("otel.file_format") == null) {
			System.out.println("otel.file_format is not set, skipping application.yaml extraction.");
			return;
		}

		for (PropertySource<?> propertySource : environment.getPropertySources()) {
			if (propertySource instanceof OriginTrackedMapPropertySource source) {
                String name = source.getName();
				System.out.println("Property Source: " + name);
				Matcher matcher = pattern.matcher(name);
				if (matcher.matches()) {
					String file = matcher.group(1);
					System.out.println("Found application.yaml: " + file);

                    try (InputStream resourceAsStream = event.getClass().getClassLoader().getResourceAsStream(file)) {
						// Print the contents of the application.yaml file
						if (resourceAsStream != null) {
							String content = new String(resourceAsStream.readAllBytes());
							System.out.println("Contents of " + file + ":");
							System.out.println(content);

							extractOtelConfigFile(content);
						} else {
							System.out.println("Could not find the application.yaml file in the classpath.");
						}
                    }
				}
			}
		}
	}

	private void extractOtelConfigFile(String content) throws IOException {
//		https://github.com/open-telemetry/opentelemetry-configuration/blob/c205770a956713e512eddb056570a99737e3383a/examples/kitchen-sink.yaml#L11

		// 1. read to yaml tree in jackson
		ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
		JsonNode rootNode = yamlMapper.readTree(content);

		// 2. find the "otel" node
		JsonNode otelNode = rootNode.get("otel");
		if (otelNode == null) {
			System.out.println("No 'otel' configuration found in the YAML file.");
			return;
		}

		// 3. write the "otel" node to a temp file
		File tempFile = File.createTempFile("otel-config", ".yaml");
		try (FileWriter writer = new FileWriter(tempFile)) {
			writer.write(yamlMapper.writeValueAsString(otelNode));
		}

		System.out.println("OpenTelemetry configuration extracted to: " + tempFile.getAbsolutePath());

		// Set the system property to point to the extracted config file
		String key = "otel.experimental.config.file";
		System.setProperty(key, tempFile.getAbsolutePath());
		System.out.println("Set system property " + key + "=" + tempFile.getAbsolutePath());

		AutoConfiguredOpenTelemetrySdk sdk = AutoConfiguredOpenTelemetrySdk.initialize();
		System.out.println("OpenTelemetry SDK initialized with configuration from: " + sdk.getOpenTelemetrySdk());

		// print the config file

		System.out.println("OpenTelemetry configuration file content:");
		try (InputStream inputStream = new FileInputStream(tempFile)) {
			String configContent = new String(inputStream.readAllBytes());
			System.out.println(configContent);
		} catch (IOException e) {
			System.err.println("Error reading the OpenTelemetry configuration file: " + e.getMessage());
		}

		tempFile.delete();
	}
}

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestneeds triageNew issue that requires triage

Type

No type

Projects

Status

In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions