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

fish.payara.opentracing.OpenTelemetryService Maven / Gradle / Ivy

There is a newer version: 7.2024.1.Alpha1
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 *    Copyright (c) [2018-2023] Payara Foundation and/or its affiliates. All rights reserved.
 *
 *     The contents of this file are subject to the terms of either the GNU
 *     General Public License Version 2 only ("GPL") or the Common Development
 *     and Distribution License("CDDL") (collectively, the "License").  You
 *     may not use this file except in compliance with the License.  You can
 *     obtain a copy of the License at
 *     https://github.com/payara/Payara/blob/master/LICENSE.txt
 *     See the License for the specific
 *     language governing permissions and limitations under the License.
 *
 *     When distributing the software, include this License Header Notice in each
 *     file and include the License file at glassfish/legal/LICENSE.txt.
 *
 *     GPL Classpath Exception:
 *     The Payara Foundation designates this particular file as subject to the "Classpath"
 *     exception as provided by the Payara Foundation in the GPL Version 2 section of the License
 *     file that accompanied this code.
 *
 *     Modifications:
 *     If applicable, add the following below the License Header, with the fields
 *     enclosed by brackets [] replaced by your own identifying information:
 *     "Portions Copyright [year] [name of copyright owner]"
 *
 *     Contributor(s):
 *     If you wish your version of this file to be governed by only the CDDL or
 *     only the GPL Version 2, indicate your decision by adding "[Contributor]
 *     elects to include this software in this distribution under the [CDDL or GPL
 *     Version 2] license."  If you don't indicate a single choice of license, a
 *     recipient has the option to distribute your version of this file under
 *     either the CDDL, the GPL Version 2 or to extend the choice of license to
 *     its licensees as provided above.  However, if you add GPL Version 2 code
 *     and therefore, elected the GPL Version 2 license, then the option applies
 *     only if the new code is made subject to such option by the copyright
 *     holder.
 */
package fish.payara.opentracing;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;

import fish.payara.nucleus.requesttracing.RequestTracingService;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.TracerProvider;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.inject.Inject;
import org.glassfish.api.event.EventListener;
import org.glassfish.api.event.Events;
import org.glassfish.api.invocation.ComponentInvocation;
import org.glassfish.api.invocation.InvocationManager;
import org.glassfish.hk2.api.ServiceHandle;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.internal.data.ApplicationInfo;
import org.glassfish.internal.data.ApplicationRegistry;
import org.glassfish.internal.deployment.Deployment;
import org.jvnet.hk2.annotations.Service;

/**
 * Manages per-application OpenTelemetry SDK instances as well as export to
 * Payara Request Tracing Service.
 */
@Service(name = "opentelemetry-service")
public class OpenTelemetryService implements EventListener {

    public static final String INSTRUMENTATION_SCOPE_NAME = "payara";

    // The tracer instances
    private static final Map appTelemetries = new ConcurrentHashMap<>();

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

    @Inject
    Events events;

    @Inject
    ServiceLocator locator;

    @Inject
    InvocationManager invocationManager;

    @Inject
    ApplicationRegistry applicationRegistry;

    public Tracer getCurrentTracer() {
        String appName = initializeCurrentApplication();
        return getTracer(appName).orElseThrow(() -> currentAppNotInitializedException(appName));
    }

    public void initializeCurrentApplication(Map otelProps) {
        initializeApplication(currentApplication(), otelProps);
    }

    /**
     * Required when application registers CDI-based components.
     */
    public void shutdownCurrentApplication() {
        shutdown(currentApplication());
    }

    public OpenTelemetry getCurrentSdk() {
        var appName = initializeCurrentApplication();
        return getSdk(appName).orElseThrow(() -> currentAppNotInitializedException(appName));
    }

    @PostConstruct
    void postConstruct() {
        if (events != null) {
            events.register(this);
        } else {
            logger.log(Level.WARNING, "OpenTelemetry service not registered to Payara Events: "
                    + "The Tracer for an application won't be removed upon undeployment");
        }
        OpenTelemetry openTelemetry = GlobalOpenTelemetry.get();
        if(openTelemetry == null) {
            GlobalOpenTelemetry.set(new GlobalTelemetry());
        }
    }

    @PreDestroy
    void stopAll() {
        appTelemetries.values().forEach(OpenTelemetryAppInfo::shutdown);
    }

    @Override
    public void event(Event event) {
        // Listen for application unloaded events (happens during undeployment), so that we remove the tracer instance
        // registered to that application (if there is one)
        if (event.is(Deployment.APPLICATION_UNLOADED)) {
            ApplicationInfo info = (ApplicationInfo) event.hook();
            shutdown(info.getName());
        }
    }

    private void shutdown(String appName) {
        var appInfo = appTelemetries.remove(appName);
        if (appInfo != null) {
            appInfo.shutdown();
        }
    }

    private String initializeCurrentApplication() {
        var appName = currentApplication();
        if (appName == null) {
            throw new IllegalStateException("No application code is executing. Cannot determine current application scope");
        }
        // if no application-level initialization took place, initialize with defaults
        ensureAppInitialized(appName, null);
        return appName;
    }

    /**
     * Return existing tracer for a given application.
     * 

Because Telemetry SDKs are configurable per app, the tracer need to be explicitly created for an application

* * @param applicationName * @return */ private Optional getTracer(String applicationName) { return get(applicationName, OpenTelemetryAppInfo::tracer); } private static IllegalStateException currentAppNotInitializedException(String appName) { return new IllegalStateException("Application " + appName + " should have initialized, but it didn't"); } private String currentApplication() { final ComponentInvocation invocation = invocationManager.getCurrentInvocation(); if (invocation == null) { // In CDI context we might have app environment var appEnv = invocationManager.peekAppEnvironment(); if (appEnv != null) { return appEnv.getName(); } return null; } String appName = invocation.getAppName(); if (appName == null) { appName = invocation.getModuleName(); if (appName == null) { appName = invocation.getComponentId(); // If we've found a component name, check if there's an application registered with the same name if (appName != null) { // If it's not directly in the registry, it's possible due to how the componentId is constructed if (applicationRegistry.get(appName) == null) { String[] componentIds = appName.split("_/"); // The application name should be the first component appName = componentIds[0]; } } } } return appName; } /** * Initialize OpenTelemtry components for an application if they do not exist yet. * * @param appName * @param properties */ void ensureAppInitialized(String appName, Map properties) { appTelemetries.computeIfAbsent(appName, (k) -> new OpenTelemetryAppInfo(createSdk(appName, properties))); } private Optional get(String applicationName, Function getter) { if (applicationName == null) { return Optional.empty(); } OpenTelemetryAppInfo appInfo = appTelemetries.get(applicationName); if (appInfo == null) { return Optional.empty(); } return Optional.ofNullable(getter.apply(appInfo)); } /** * Set up SDK to be used by application. The SDK instance will be configured using auto-configuration using contents of provided * Map, system properties and environment variables (in this precedence) if *
    *
  • Request tracing is enabled, additionally passing spans onto Payara Reuest Tracing; or
  • *
  • Config properties contain {@code otel.sdk.disabled=false}; or
  • *
  • System properties contain that property, or environment contains key {@code OTEL_SDK_DISABLED=false}
  • *
* Otherwise a Noop instance is configured. *

* SDK instances are local to the app, global tracing is not installed. * * @param applicationName * @param configProperties * @return * @link * Autoconfigure documentation */ private OpenTelemetrySdk createSdk(String applicationName, Map configProperties) { if (isOtelEnabled(configProperties) || isPayaraTracingEnabled()) { var props = new HashMap<>(configProperties != null ? configProperties : Map.of()); addDefault(props, "otel.service.name", applicationName); addDefault(props, "otel.metrics.exporter", "none"); try { return AutoConfiguredOpenTelemetrySdk.builder() .setServiceClassLoader(Thread.currentThread().getContextClassLoader()) .disableShutdownHook() .addTracerProviderCustomizer((builder, config) -> { if (isPayaraTracingEnabled()) { return builder.addSpanProcessor(new PayaraRequestTracingProcessor(locator.getService(RequestTracingService.class))); } else { return builder; } }) .addPropertiesSupplier(() -> props) .build().getOpenTelemetrySdk(); } catch (ConfigurationException ce) { logger.log(Level.SEVERE, "Failed to configure OpenTelemetry for " + applicationName + " using classlaoder " + Thread.currentThread().getContextClassLoader() +" will revert to no-op", ce); // Do not prevent application from working when things go awry in telemetry config return OpenTelemetrySdk.builder().build(); } } else { // noop return OpenTelemetrySdk.builder().build(); } } private boolean isOtelEnabled(Map configProperties) { boolean result = configProperties != null && !configProperties.isEmpty(); if (!result) { result = "false".equalsIgnoreCase(System.getProperty("otel.sdk.disabled", "true")); } if (!result) { result = "false".equalsIgnoreCase(System.getenv("OTEL_SDK_DISABLED")); } return result; } /** * Very carefully ask whether Payara Request Tracing service is enabled. * The method might be invoked way sooner than it is appropriate time for request tracing to start, * or in environments where it is not supported (App Client). * * @return True if the Request Tracing Service is enabled */ private boolean isPayaraTracingEnabled() { ServiceHandle handle = locator.getServiceHandle(RequestTracingService.class); return handle != null && handle.isActive() && handle.getService().isRequestTracingEnabled(); } private void addDefault(Map props, String key, String value) { if (props.containsKey(key)) { return; } if (System.getProperty(key) != null) { return; } if (System.getenv(key.toUpperCase().replace('.', '_')) != null) { return; } props.put(key, value); } /** * Return Meter builder for given application * * @param applicationName * @return */ private Optional getMeter(String applicationName) { return get(applicationName, OpenTelemetryAppInfo::meter); } /** * Return logger for given application. * * @param applicationName * @return */ private Optional getLogger(String applicationName) { return get(applicationName, OpenTelemetryAppInfo::logger); } /** * Create new OpenTelemetry components for application. Shutdown previous ones if such already existed. * * @param applicationName * @param configProperties * @return */ private OpenTelemetrySdk initializeApplication(String applicationName, Map configProperties) { OpenTelemetrySdk sdk = createSdk(applicationName, configProperties); OpenTelemetryAppInfo previous = appTelemetries.put(applicationName, new OpenTelemetryAppInfo(sdk)); if (previous != null) { previous.shutdown(); } return sdk; } Optional getSdk(String applicationName) { return get(applicationName, OpenTelemetryAppInfo::sdk); } public Optional getSdkDependency(String applicationName, Runnable shutdownListener) { return get(applicationName, appInfo -> { appInfo.addShutdownListener(shutdownListener); return appInfo.sdk(); }); } public boolean isEnabled() { String application = currentApplication(); return application != null && appTelemetries.containsKey(application) || isPayaraTracingEnabled(); } static class OpenTelemetryAppInfo { private final OpenTelemetrySdk sdk; private Tracer tracer; private Meter meter; private io.opentelemetry.api.logs.Logger logger; private List shutdownListeners; OpenTelemetryAppInfo(OpenTelemetrySdk sdk) { this.sdk = sdk; } Tracer tracer() { if (this.tracer == null) { this.tracer = sdk.getTracerProvider().get(INSTRUMENTATION_SCOPE_NAME); } return this.tracer; } Meter meter() { if (this.meter == null) { this.meter = sdk.getMeterProvider().get(INSTRUMENTATION_SCOPE_NAME); } return this.meter; } io.opentelemetry.api.logs.Logger logger() { if (this.logger == null) { this.logger = sdk.getSdkLoggerProvider().get(INSTRUMENTATION_SCOPE_NAME); } return this.logger; } OpenTelemetrySdk sdk() { return sdk; } synchronized void addShutdownListener(Runnable listener) { if (shutdownListeners == null) { // we don't expect many listeners shutdownListeners = new ArrayList<>(2); } shutdownListeners.add(listener); } synchronized void shutdown() { if (shutdownListeners != null) { shutdownListeners.forEach(Runnable::run); } // we need to shut it down properly. SDK providers offer both async shutdown meter as well as implement // Closeable, where shutdown is invoked in sync fashion. Let's be optimistic and start with async shutdown SdkTracerProvider tracerProvider = this.sdk.getSdkTracerProvider(); if (tracerProvider != null) { tracerProvider.shutdown(); } SdkMeterProvider meterProvider = this.sdk.getSdkMeterProvider(); if (meterProvider != null) { meterProvider.shutdown(); } SdkLoggerProvider logProvider = this.sdk.getSdkLoggerProvider(); if (logProvider != null) { logProvider.shutdown(); } } } class GlobalTelemetry implements OpenTelemetry { @Override public TracerProvider getTracerProvider() { return get(currentApplication(), appInfo -> appInfo.sdk().getTracerProvider()).orElse(TracerProvider.noop()); } @Override public ContextPropagators getPropagators() { return get(currentApplication(), appInfo -> appInfo.sdk().getPropagators()).orElse(ContextPropagators.noop()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy