All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder Maven / Gradle / Ivy

The newest version!
/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.sdk.autoconfigure;

import static java.util.Objects.requireNonNull;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.incubator.events.GlobalEventLoggerProvider;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.OpenTelemetrySdkBuilder;
import io.opentelemetry.sdk.autoconfigure.internal.ComponentLoader;
import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper;
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.autoconfigure.spi.internal.AutoConfigureListener;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.StructuredConfigProperties;
import io.opentelemetry.sdk.logs.LogRecordProcessor;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
import io.opentelemetry.sdk.logs.internal.SdkEventLoggerProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
import io.opentelemetry.sdk.metrics.export.MetricExporter;
import io.opentelemetry.sdk.metrics.export.MetricReader;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;

/**
 * A builder for configuring auto-configuration of the OpenTelemetry SDK. Notably, auto-configured
 * components can be customized, for example by delegating to them from a wrapper that tweaks
 * behavior such as filtering out telemetry attributes.
 *
 * @since 1.28.0
 */
public final class AutoConfiguredOpenTelemetrySdkBuilder implements AutoConfigurationCustomizer {

  private static final Logger logger =
      Logger.getLogger(AutoConfiguredOpenTelemetrySdkBuilder.class.getName());

  @Nullable private ConfigProperties config;

  private BiFunction
      tracerProviderCustomizer = (a, unused) -> a;
  private BiFunction
      propagatorCustomizer = (a, unused) -> a;
  private BiFunction
      spanExporterCustomizer = (a, unused) -> a;

  private BiFunction
      spanProcessorCustomizer = (a, unused) -> a;
  private BiFunction samplerCustomizer =
      (a, unused) -> a;

  private BiFunction
      meterProviderCustomizer = (a, unused) -> a;
  private BiFunction
      metricExporterCustomizer = (a, unused) -> a;
  private BiFunction
      metricReaderCustomizer = (a, unused) -> a;

  private BiFunction
      loggerProviderCustomizer = (a, unused) -> a;
  private BiFunction
      logRecordExporterCustomizer = (a, unused) -> a;
  private BiFunction
      logRecordProcessorCustomizer = (a, unused) -> a;

  private BiFunction resourceCustomizer =
      (a, unused) -> a;

  private Supplier> propertiesSupplier = Collections::emptyMap;

  private final List>> propertiesCustomizers =
      new ArrayList<>();

  private Function configPropertiesCustomizer =
      Function.identity();

  private ComponentLoader componentLoader =
      SpiHelper.serviceComponentLoader(AutoConfiguredOpenTelemetrySdk.class.getClassLoader());

  private boolean registerShutdownHook = true;

  private boolean setResultAsGlobal = false;

  private boolean customized;

  AutoConfiguredOpenTelemetrySdkBuilder() {}

  /**
   * Sets the {@link ConfigProperties} to use when resolving properties for auto-configuration.
   * {@link #addPropertiesSupplier(Supplier)} and {@link #addPropertiesCustomizer(Function)} will
   * have no effect if this method is used.
   */
  AutoConfiguredOpenTelemetrySdkBuilder setConfig(ConfigProperties config) {
    requireNonNull(config, "config");
    this.config = config;
    return this;
  }

  /**
   * Adds a {@link BiFunction} to invoke the with the {@link SdkTracerProviderBuilder} to allow
   * customization. The return value of the {@link BiFunction} will replace the passed-in argument.
   *
   * 

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addTracerProviderCustomizer( BiFunction tracerProviderCustomizer) { requireNonNull(tracerProviderCustomizer, "tracerProviderCustomizer"); this.tracerProviderCustomizer = mergeCustomizer(this.tracerProviderCustomizer, tracerProviderCustomizer); return this; } /** * Adds a {@link BiFunction} to invoke with the default autoconfigured {@link TextMapPropagator} * to allow customization. The return value of the {@link BiFunction} will replace the passed-in * argument. * *

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addPropagatorCustomizer( BiFunction propagatorCustomizer) { requireNonNull(propagatorCustomizer, "propagatorCustomizer"); this.propagatorCustomizer = mergeCustomizer(this.propagatorCustomizer, propagatorCustomizer); return this; } /** * Adds a {@link BiFunction} to invoke with the default autoconfigured {@link Resource} to allow * customization. The return value of the {@link BiFunction} will replace the passed-in argument. * *

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addResourceCustomizer( BiFunction resourceCustomizer) { requireNonNull(resourceCustomizer, "resourceCustomizer"); this.resourceCustomizer = mergeCustomizer(this.resourceCustomizer, resourceCustomizer); return this; } /** * Adds a {@link BiFunction} to invoke with the default autoconfigured {@link Sampler} to allow * customization. The return value of the {@link BiFunction} will replace the passed-in argument. * *

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addSamplerCustomizer( BiFunction samplerCustomizer) { requireNonNull(samplerCustomizer, "samplerCustomizer"); this.samplerCustomizer = mergeCustomizer(this.samplerCustomizer, samplerCustomizer); return this; } /** * Adds a {@link BiFunction} to invoke with the default autoconfigured {@link SpanExporter} to * allow customization. The return value of the {@link BiFunction} will replace the passed-in * argument. * *

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addSpanExporterCustomizer( BiFunction spanExporterCustomizer) { requireNonNull(spanExporterCustomizer, "spanExporterCustomizer"); this.spanExporterCustomizer = mergeCustomizer(this.spanExporterCustomizer, spanExporterCustomizer); return this; } /** * Adds a {@link BiFunction} to invoke for all autoconfigured {@link * io.opentelemetry.sdk.trace.SpanProcessor}. The return value of the {@link BiFunction} will * replace the passed-in argument. In contrast to {@link #addSpanExporterCustomizer(BiFunction)} * this allows modifications to happen before batching occurs. As a result, it is possible to * efficiently filter spans, add artificial spans or delay spans for enhancing them with external, * delayed data. * *

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addSpanProcessorCustomizer( BiFunction spanProcessorCustomizer) { requireNonNull(spanProcessorCustomizer, "spanProcessorCustomizer"); this.spanProcessorCustomizer = mergeCustomizer(this.spanProcessorCustomizer, spanProcessorCustomizer); return this; } /** * Adds a {@link Supplier} of a map of property names and values to use as defaults for the {@link * ConfigProperties} used during auto-configuration. The order of precedence of properties is * system properties > environment variables > the suppliers registered with this method. * *

Multiple calls will cause properties to be merged in order, with later ones overwriting * duplicate keys in earlier ones. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addPropertiesSupplier( Supplier> propertiesSupplier) { requireNonNull(propertiesSupplier, "propertiesSupplier"); this.propertiesSupplier = mergeProperties(this.propertiesSupplier, propertiesSupplier); return this; } /** * Adds a {@link Function} to invoke the with the {@link ConfigProperties} to allow customization. * The return value of the {@link Function} will be merged into the {@link ConfigProperties} * before it is used for auto-configuration, overwriting the properties that are already there. * *

Multiple calls will cause properties to be merged in order, with later ones overwriting * duplicate keys in earlier ones. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addPropertiesCustomizer( Function> propertiesCustomizer) { requireNonNull(propertiesCustomizer, "propertiesCustomizer"); this.propertiesCustomizers.add(propertiesCustomizer); return this; } /** * Adds a {@link Function} to invoke the with the {@link ConfigProperties} to allow customization. * *

The argument to the function is the {@link ConfigProperties}, with the {@link * #addPropertiesCustomizer(Function)} already applied. * *

The return value of the {@link Function} replace the {@link ConfigProperties} to be used. */ AutoConfiguredOpenTelemetrySdkBuilder setConfigPropertiesCustomizer( Function configPropertiesCustomizer) { requireNonNull(configPropertiesCustomizer, "configPropertiesCustomizer"); this.configPropertiesCustomizer = configPropertiesCustomizer; return this; } /** * Adds a {@link BiFunction} to invoke the with the {@link SdkMeterProviderBuilder} to allow * customization. The return value of the {@link BiFunction} will replace the passed-in argument. * *

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addMeterProviderCustomizer( BiFunction meterProviderCustomizer) { requireNonNull(meterProviderCustomizer, "meterProviderCustomizer"); this.meterProviderCustomizer = mergeCustomizer(this.meterProviderCustomizer, meterProviderCustomizer); return this; } /** * Adds a {@link BiFunction} to invoke with the default autoconfigured {@link SpanExporter} to * allow customization. The return value of the {@link BiFunction} will replace the passed-in * argument. * *

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addMetricExporterCustomizer( BiFunction metricExporterCustomizer) { requireNonNull(metricExporterCustomizer, "metricExporterCustomizer"); this.metricExporterCustomizer = mergeCustomizer(this.metricExporterCustomizer, metricExporterCustomizer); return this; } /** * Adds a {@link BiFunction} to invoke with the autoconfigured {@link MetricReader} to allow * customization. The return value of the {@link BiFunction} will replace the passed-in argument. * *

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addMetricReaderCustomizer( BiFunction readerCustomizer) { requireNonNull(readerCustomizer, "readerCustomizer"); this.metricReaderCustomizer = mergeCustomizer(this.metricReaderCustomizer, readerCustomizer); return this; } /** * Adds a {@link BiFunction} to invoke the with the {@link SdkLoggerProviderBuilder} to allow * customization. The return value of the {@link BiFunction} will replace the passed-in argument. * *

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addLoggerProviderCustomizer( BiFunction loggerProviderCustomizer) { requireNonNull(loggerProviderCustomizer, "loggerProviderCustomizer"); this.loggerProviderCustomizer = mergeCustomizer(this.loggerProviderCustomizer, loggerProviderCustomizer); return this; } /** * Adds a {@link BiFunction} to invoke with the default autoconfigured {@link LogRecordExporter} * to allow customization. The return value of the {@link BiFunction} will replace the passed-in * argument. * *

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addLogRecordExporterCustomizer( BiFunction logRecordExporterCustomizer) { requireNonNull(logRecordExporterCustomizer, "logRecordExporterCustomizer"); this.logRecordExporterCustomizer = mergeCustomizer(this.logRecordExporterCustomizer, logRecordExporterCustomizer); return this; } /** * Adds a {@link BiFunction} to invoke for all autoconfigured {@link * io.opentelemetry.sdk.logs.LogRecordProcessor}s. The return value of the {@link BiFunction} will * replace the passed-in argument. In contrast to {@link * #addLogRecordExporterCustomizer(BiFunction)} (BiFunction)} this allows modifications to happen * before batching occurs. As a result, it is possible to efficiently filter logs, add artificial * logs or delay logs for enhancing them with external, delayed data. * *

Multiple calls will execute the customizers in order. */ @Override public AutoConfiguredOpenTelemetrySdkBuilder addLogRecordProcessorCustomizer( BiFunction logRecordProcessorCustomizer) { requireNonNull(logRecordProcessorCustomizer, "logRecordProcessorCustomizer"); this.logRecordProcessorCustomizer = mergeCustomizer(this.logRecordProcessorCustomizer, logRecordProcessorCustomizer); return this; } /** * Disable the registration of a shutdown hook to shut down the SDK when appropriate. By default, * the shutdown hook is registered. * *

Skipping the registration of the shutdown hook may cause unexpected behavior. This * configuration is for SDK consumers that require control over the SDK lifecycle. In this case, * alternatives must be provided by the SDK consumer to shut down the SDK. */ public AutoConfiguredOpenTelemetrySdkBuilder disableShutdownHook() { this.registerShutdownHook = false; return this; } /** * Sets whether the configured {@link OpenTelemetrySdk} should be set as the application's * {@linkplain io.opentelemetry.api.GlobalOpenTelemetry global} instance. * *

By default, {@link GlobalOpenTelemetry} is not set. */ public AutoConfiguredOpenTelemetrySdkBuilder setResultAsGlobal() { this.setResultAsGlobal = true; return this; } /** Sets the {@link ClassLoader} to be used to load SPI implementations. */ public AutoConfiguredOpenTelemetrySdkBuilder setServiceClassLoader( ClassLoader serviceClassLoader) { requireNonNull(serviceClassLoader, "serviceClassLoader"); this.componentLoader = SpiHelper.serviceComponentLoader(serviceClassLoader); return this; } /** Sets the {@link ComponentLoader} to be used to load SPI implementations. */ AutoConfiguredOpenTelemetrySdkBuilder setComponentLoader(ComponentLoader componentLoader) { requireNonNull(componentLoader, "componentLoader"); this.componentLoader = componentLoader; return this; } /** * Returns a new {@link AutoConfiguredOpenTelemetrySdk} holding components auto-configured using * the settings of this {@link AutoConfiguredOpenTelemetrySdkBuilder}. */ public AutoConfiguredOpenTelemetrySdk build() { SpiHelper spiHelper = SpiHelper.create(componentLoader); if (!customized) { customized = true; mergeSdkTracerProviderConfigurer(); for (AutoConfigurationCustomizerProvider customizer : spiHelper.loadOrdered(AutoConfigurationCustomizerProvider.class)) { customizer.customize(this); } } ConfigProperties config = getConfig(); AutoConfiguredOpenTelemetrySdk fromFileConfiguration = maybeConfigureFromFile(config, componentLoader); if (fromFileConfiguration != null) { maybeRegisterShutdownHook(fromFileConfiguration.getOpenTelemetrySdk()); maybeSetAsGlobal(fromFileConfiguration.getOpenTelemetrySdk()); return fromFileConfiguration; } Resource resource = ResourceConfiguration.configureResource(config, spiHelper, resourceCustomizer); // Track any closeable resources created throughout configuration. If an exception short // circuits configuration, partially configured components will be closed. List closeables = new ArrayList<>(); try { OpenTelemetrySdk openTelemetrySdk = OpenTelemetrySdk.builder().build(); boolean sdkEnabled = !config.getBoolean("otel.sdk.disabled", false); if (sdkEnabled) { SdkMeterProviderBuilder meterProviderBuilder = SdkMeterProvider.builder(); meterProviderBuilder.setResource(resource); MeterProviderConfiguration.configureMeterProvider( meterProviderBuilder, config, spiHelper, metricReaderCustomizer, metricExporterCustomizer, closeables); meterProviderBuilder = meterProviderCustomizer.apply(meterProviderBuilder, config); SdkMeterProvider meterProvider = meterProviderBuilder.build(); closeables.add(meterProvider); SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder(); tracerProviderBuilder.setResource(resource); TracerProviderConfiguration.configureTracerProvider( tracerProviderBuilder, config, spiHelper, meterProvider, spanExporterCustomizer, spanProcessorCustomizer, samplerCustomizer, closeables); tracerProviderBuilder = tracerProviderCustomizer.apply(tracerProviderBuilder, config); SdkTracerProvider tracerProvider = tracerProviderBuilder.build(); closeables.add(tracerProvider); SdkLoggerProviderBuilder loggerProviderBuilder = SdkLoggerProvider.builder(); loggerProviderBuilder.setResource(resource); LoggerProviderConfiguration.configureLoggerProvider( loggerProviderBuilder, config, spiHelper, meterProvider, logRecordExporterCustomizer, logRecordProcessorCustomizer, closeables); loggerProviderBuilder = loggerProviderCustomizer.apply(loggerProviderBuilder, config); SdkLoggerProvider loggerProvider = loggerProviderBuilder.build(); closeables.add(loggerProvider); ContextPropagators propagators = PropagatorConfiguration.configurePropagators(config, spiHelper, propagatorCustomizer); OpenTelemetrySdkBuilder sdkBuilder = OpenTelemetrySdk.builder() .setTracerProvider(tracerProvider) .setLoggerProvider(loggerProvider) .setMeterProvider(meterProvider) .setPropagators(propagators); openTelemetrySdk = sdkBuilder.build(); } maybeRegisterShutdownHook(openTelemetrySdk); maybeSetAsGlobal(openTelemetrySdk); callAutoConfigureListeners(spiHelper, openTelemetrySdk); return AutoConfiguredOpenTelemetrySdk.create(openTelemetrySdk, resource, config, null); } catch (RuntimeException e) { logger.info( "Error encountered during autoconfiguration. Closing partially configured components."); for (Closeable closeable : closeables) { try { logger.fine("Closing " + closeable.getClass().getName()); closeable.close(); } catch (IOException ex) { logger.warning( "Error closing " + closeable.getClass().getName() + ": " + ex.getMessage()); } } if (e instanceof ConfigurationException) { throw e; } throw new ConfigurationException("Unexpected configuration error", e); } } @Nullable private static AutoConfiguredOpenTelemetrySdk maybeConfigureFromFile( ConfigProperties config, ComponentLoader componentLoader) { String otelConfigFile = config.getString("otel.config.file"); if (otelConfigFile != null && !otelConfigFile.isEmpty()) { logger.warning( "otel.config.file was set, but has been replaced with otel.experimental.config.file"); } String configurationFile = config.getString("otel.experimental.config.file"); if (configurationFile == null || configurationFile.isEmpty()) { return null; } logger.fine("Autoconfiguring from configuration file: " + configurationFile); try (FileInputStream fis = new FileInputStream(configurationFile)) { Class configurationFactory = Class.forName("io.opentelemetry.sdk.extension.incubator.fileconfig.FileConfiguration"); Method parse = configurationFactory.getMethod("parse", InputStream.class); Object model = parse.invoke(null, fis); Class openTelemetryConfiguration = Class.forName( "io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel"); Method create = configurationFactory.getMethod( "create", openTelemetryConfiguration, ComponentLoader.class); OpenTelemetrySdk sdk = (OpenTelemetrySdk) create.invoke(null, model, componentLoader); Method toConfigProperties = configurationFactory.getMethod("toConfigProperties", openTelemetryConfiguration); StructuredConfigProperties structuredConfigProperties = (StructuredConfigProperties) toConfigProperties.invoke(null, model); // Note: can't access declarative configuration resource without reflection so setting a dummy // resource return AutoConfiguredOpenTelemetrySdk.create( sdk, Resource.getDefault(), null, structuredConfigProperties); } catch (FileNotFoundException e) { throw new ConfigurationException("Configuration file not found", e); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { throw new ConfigurationException( "Error configuring from file. Is opentelemetry-sdk-extension-incubator on the classpath?", e); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof ConfigurationException) { throw (ConfigurationException) cause; } throw new ConfigurationException("Unexpected error configuring from file", e); } catch (IOException e) { // IOException (other than FileNotFoundException which is caught above) is only thrown // above by FileInputStream.close() throw new ConfigurationException("Error closing file", e); } } private void maybeRegisterShutdownHook(OpenTelemetrySdk openTelemetrySdk) { if (!registerShutdownHook) { return; } Runtime.getRuntime().addShutdownHook(shutdownHook(openTelemetrySdk)); } private void maybeSetAsGlobal(OpenTelemetrySdk openTelemetrySdk) { if (!setResultAsGlobal) { return; } GlobalOpenTelemetry.set(openTelemetrySdk); GlobalEventLoggerProvider.set( SdkEventLoggerProvider.create(openTelemetrySdk.getSdkLoggerProvider())); logger.log( Level.FINE, "Global OpenTelemetry set to {0} by autoconfiguration", openTelemetrySdk); } // Visible for testing void callAutoConfigureListeners(SpiHelper spiHelper, OpenTelemetrySdk openTelemetrySdk) { for (AutoConfigureListener listener : spiHelper.getListeners()) { try { listener.afterAutoConfigure(openTelemetrySdk); } catch (Throwable throwable) { logger.log( Level.WARNING, "Error invoking listener " + listener.getClass().getName(), throwable); } } } @SuppressWarnings("deprecation") // Support deprecated SdkTracerProviderConfigurer private void mergeSdkTracerProviderConfigurer() { for (io.opentelemetry.sdk.autoconfigure.spi.traces.SdkTracerProviderConfigurer configurer : componentLoader.load( io.opentelemetry.sdk.autoconfigure.spi.traces.SdkTracerProviderConfigurer.class)) { addTracerProviderCustomizer( (builder, config) -> { configurer.configure(builder, config); return builder; }); } } private ConfigProperties getConfig() { ConfigProperties config = this.config; if (config == null) { config = computeConfigProperties(); } return config; } private ConfigProperties computeConfigProperties() { DefaultConfigProperties properties = DefaultConfigProperties.create(propertiesSupplier.get()); for (Function> customizer : propertiesCustomizers) { Map overrides = customizer.apply(properties); properties = properties.withOverrides(overrides); } return configPropertiesCustomizer.apply(properties); } // Visible for testing Thread shutdownHook(OpenTelemetrySdk sdk) { return new Thread(sdk::close); } private static BiFunction mergeCustomizer( BiFunction first, BiFunction second) { return (I configured, ConfigProperties config) -> { O1 firstResult = first.apply(configured, config); return second.apply(firstResult, config); }; } private static Supplier> mergeProperties( Supplier> first, Supplier> second) { return () -> { Map merged = new HashMap<>(); merged.putAll(first.get()); merged.putAll(second.get()); return merged; }; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy