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

org.glowroot.local.ui.InstrumentationJsonService Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2013-2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.glowroot.local.ui;

import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.annotation.Nullable;

import org.glowroot.shaded.fasterxml.jackson.core.JsonProcessingException;
import org.glowroot.shaded.fasterxml.jackson.databind.ObjectMapper;
import org.glowroot.shaded.google.common.base.Optional;
import org.glowroot.shaded.google.common.base.Splitter;
import org.glowroot.shaded.google.common.cache.CacheBuilder;
import org.glowroot.shaded.google.common.cache.CacheLoader;
import org.glowroot.shaded.google.common.cache.LoadingCache;
import org.glowroot.shaded.google.common.collect.ImmutableList;
import org.glowroot.shaded.google.common.collect.Lists;
import org.glowroot.shaded.google.common.collect.Ordering;
import org.glowroot.shaded.google.common.collect.Sets;
import org.glowroot.shaded.netty.handler.codec.http.HttpResponseStatus;
import org.immutables.value.Value;

import org.glowroot.common.ObjectMappers;
import org.glowroot.config.CaptureKind;
import org.glowroot.config.ConfigService;
import org.glowroot.config.InstrumentationConfig;
import org.glowroot.plugin.api.weaving.MethodModifier;
import org.glowroot.transaction.AdviceCache;
import org.glowroot.transaction.TransactionModule;
import org.glowroot.weaving.AnalyzedWorld;

import static org.glowroot.shaded.google.common.base.Preconditions.checkNotNull;
import static org.glowroot.shaded.objectweb.asm.Opcodes.ACC_FINAL;
import static org.glowroot.shaded.objectweb.asm.Opcodes.ACC_SYNCHRONIZED;

@JsonService
class InstrumentationJsonService {

    private static final String DUMMY_KEY = "KEY";

    private static final ObjectMapper mapper = ObjectMappers.create();
    private static final Splitter splitter = Splitter.on(' ').omitEmptyStrings();

    private final ConfigService configService;
    private final AdviceCache adviceCache;
    private final TransactionModule transactionModule;
    private final AnalyzedWorld analyzedWorld;
    private final @Nullable Instrumentation instrumentation;

    // hopefully can simplify someday https://github.com/google/guava/issues/872
    private final LoadingCache classpathCache = CacheBuilder.newBuilder()
            .softValues()
            .maximumSize(1)
            .build(new CacheLoader() {
                @Override
                public ClasspathCache load(String key) throws Exception {
                    return new ClasspathCache(analyzedWorld, instrumentation);
                }
            });

    InstrumentationJsonService(ConfigService configService, AdviceCache adviceCache,
            TransactionModule transactionModule, AnalyzedWorld analyzedWorld,
            @Nullable Instrumentation instrumentation) {
        this.configService = configService;
        this.adviceCache = adviceCache;
        this.transactionModule = transactionModule;
        this.analyzedWorld = analyzedWorld;
        this.instrumentation = instrumentation;
    }

    @GET("/backend/config/instrumentation")
    String getInstrumentationConfigs() throws Exception {
        List configs = configService.getInstrumentationConfigs();
        configs = InstrumentationConfig.ordering.immutableSortedCopy(configs);
        List dtos = Lists.newArrayList();
        for (InstrumentationConfig config : configs) {
            dtos.add(InstrumentationConfigDtoBase.fromConfig(config));
        }
        return mapper.writeValueAsString(InstrumentationListResponse.builder()
                .addAllConfigs(dtos)
                .jvmOutOfSync(adviceCache.isOutOfSync(configService.getInstrumentationConfigs()))
                .jvmRetransformClassesSupported(
                        transactionModule.isJvmRetransformClassesSupported())
                .build());
    }

    @GET("/backend/config/instrumentation/([0-9a-f]{40})")
    String getInstrumentationConfig(String version) throws JsonProcessingException {
        InstrumentationConfig config = configService.getInstrumentationConfig(version);
        if (config == null) {
            throw new JsonServiceException(HttpResponseStatus.NOT_FOUND);
        }
        List methodSignatures =
                getMethodSignatures(config.className(), config.methodName());
        return mapper.writeValueAsString(InstrumentationConfigResponse.builder()
                .config(InstrumentationConfigDtoBase.fromConfig(config))
                .addAllMethodSignatures(methodSignatures)
                .build());
    }

    // this is marked as @GET so it can be used without update rights (e.g. demo instance)
    @GET("/backend/config/preload-classpath-cache")
    void preloadClasspathCache() throws IOException {
        // HttpServer is configured with a very small thread pool to keep number of threads down
        // (currently only a single thread), so spawn a background thread to perform the preloading
        // so it doesn't block other http requests
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                getClasspathCache().updateCache();
            }
        });
        thread.setDaemon(true);
        thread.setName("Glowroot-Temporary-Thread");
        thread.start();
    }

    @GET("/backend/config/matching-class-names")
    String getMatchingClassNames(String queryString) throws Exception {
        ClassNamesRequest request = QueryStrings.decode(queryString, ClassNamesRequest.class);
        List matchingClassNames = getClasspathCache()
                .getMatchingClassNames(request.partialClassName(), request.limit());
        return mapper.writeValueAsString(matchingClassNames);
    }

    @GET("/backend/config/matching-method-names")
    String getMatchingMethodNames(String queryString) throws Exception {
        MethodNamesRequest request = QueryStrings.decode(queryString, MethodNamesRequest.class);
        List matchingMethodNames = getMatchingMethodNames(request.className(),
                request.partialMethodName(), request.limit());
        return mapper.writeValueAsString(matchingMethodNames);
    }

    @GET("/backend/config/method-signatures")
    String getMethodSignatures(String queryString) throws Exception {
        MethodSignaturesRequest request =
                QueryStrings.decode(queryString, MethodSignaturesRequest.class);
        List methodSignatures =
                getMethodSignatures(request.className(), request.methodName());
        return mapper.writeValueAsString(methodSignatures);
    }

    @POST("/backend/config/instrumentation/add")
    String addInstrumentationConfig(String content) throws Exception {
        InstrumentationConfigDto configDto =
                mapper.readValue(content, InstrumentationConfigDto.class);
        InstrumentationConfig config = configDto.toConfig();
        ImmutableList errors = config.validationErrors();
        if (!errors.isEmpty()) {
            return mapper.writeValueAsString(InstrumentationErrorResponse.builder()
                    .addAllErrors(errors)
                    .build());
        }
        String version = configService.insertInstrumentationConfig(config);
        return getInstrumentationConfig(version);
    }

    @POST("/backend/config/instrumentation/update")
    String updateInstrumentationConfig(String content) throws IOException {
        InstrumentationConfigDto configDto =
                mapper.readValue(content, InstrumentationConfigDto.class);
        InstrumentationConfig config = configDto.toConfig();
        String version = configDto.version();
        checkNotNull(version, "Missing required request property: version");
        version = configService.updateInstrumentationConfig(config, version);
        return getInstrumentationConfig(version);
    }

    @POST("/backend/config/instrumentation/remove")
    void removeInstrumentationConfig(String content) throws IOException {
        String version = mapper.readValue(content, String.class);
        checkNotNull(version);
        configService.deleteInstrumentationConfig(version);
    }

    private ClasspathCache getClasspathCache() {
        return classpathCache.getUnchecked(DUMMY_KEY);
    }

    // returns the first  matching method names, ordered alphabetically (case-insensitive)
    private ImmutableList getMatchingMethodNames(String className, String partialMethodName,
            int limit) {
        String partialMethodNameUpper = partialMethodName.toUpperCase(Locale.ENGLISH);
        Set methodNames = Sets.newHashSet();
        for (UiAnalyzedMethod analyzedMethod : getClasspathCache().getAnalyzedMethods(className)) {
            String methodName = analyzedMethod.name();
            if (methodName.equals("") || methodName.equals("")) {
                // static initializers are not supported by weaver
                // (see AdviceMatcher.isMethodNameMatch())
                // and constructors do not support @OnBefore advice at this time
                continue;
            }
            if (methodName.toUpperCase(Locale.ENGLISH).contains(partialMethodNameUpper)) {
                methodNames.add(methodName);
            }
        }
        ImmutableList sortedMethodNames =
                Ordering.from(String.CASE_INSENSITIVE_ORDER).immutableSortedCopy(methodNames);
        if (methodNames.size() > limit) {
            return sortedMethodNames.subList(0, limit);
        } else {
            return sortedMethodNames;
        }
    }

    private List getMethodSignatures(String className, String methodName) {
        if (methodName.contains("*") || methodName.contains("|")) {
            return ImmutableList.of();
        }
        List analyzedMethods = getAnalyzedMethods(className, methodName);
        List methodSignatures = Lists.newArrayList();
        for (UiAnalyzedMethod analyzedMethod : analyzedMethods) {
            MethodSignature.Builder builder = MethodSignature.builder();
            builder.name(analyzedMethod.name());
            builder.addAllParameterTypes(analyzedMethod.parameterTypes());
            builder.returnType(analyzedMethod.returnType());
            // strip final and synchronized from displayed modifiers since they have no impact on
            // the weaver's method matching
            int reducedModifiers = analyzedMethod.modifiers() & ~ACC_FINAL & ~ACC_SYNCHRONIZED;
            String modifierNames = Modifier.toString(reducedModifiers);
            for (String modifier : splitter.split(modifierNames)) {
                builder.addModifiers(modifier.toLowerCase(Locale.ENGLISH));
            }
            methodSignatures.add(builder.build());
        }
        return methodSignatures;
    }

    private List getAnalyzedMethods(String className, String methodName) {
        // use set to remove duplicate methods (e.g. same class loaded by multiple class loaders)
        Set analyzedMethods = Sets.newHashSet();
        for (UiAnalyzedMethod analyzedMethod : getClasspathCache().getAnalyzedMethods(className)) {
            if (analyzedMethod.name().equals(methodName)) {
                analyzedMethods.add(analyzedMethod);
            }
        }
        // order methods by accessibility, then by name, then by number of args
        return UiAnalyzedMethodBase.ordering.sortedCopy(analyzedMethods);
    }

    @Value.Immutable
    abstract static class ClassNamesRequestBase {
        abstract String partialClassName();
        abstract int limit();
    }

    @Value.Immutable
    abstract static class MethodNamesRequestBase {
        abstract String className();
        abstract String partialMethodName();
        abstract int limit();
    }

    @Value.Immutable
    abstract static class MethodSignaturesRequestBase {
        abstract String className();
        abstract String methodName();
    }

    @Value.Immutable
    abstract static class MethodSignatureBase {
        abstract String name();
        abstract ImmutableList parameterTypes();
        abstract String returnType();
        abstract ImmutableList modifiers();
    }

    @Value.Immutable
    abstract static class InstrumentationListResponseBase {
        abstract ImmutableList configs();
        abstract boolean jvmOutOfSync();
        abstract boolean jvmRetransformClassesSupported();
    }

    @Value.Immutable
    abstract static class InstrumentationConfigResponseBase {
        abstract InstrumentationConfigDto config();
        abstract ImmutableList methodSignatures();
    }

    @Value.Immutable
    abstract static class InstrumentationErrorResponseBase {
        abstract ImmutableList errors();
    }

    @Value.Immutable
    abstract static class InstrumentationConfigDtoBase {

        abstract String className();
        abstract Optional declaringClassName();
        abstract String methodName();
        abstract ImmutableList methodParameterTypes();
        abstract Optional methodReturnType();
        abstract ImmutableList methodModifiers();
        abstract CaptureKind captureKind();
        abstract Optional timerName();
        abstract Optional traceEntryTemplate();
        abstract @Nullable Long traceEntryStackThresholdMillis();
        abstract Optional traceEntryCaptureSelfNested();
        abstract Optional transactionType();
        abstract Optional transactionNameTemplate();
        abstract Optional transactionUserTemplate();
        abstract Map transactionCustomAttributeTemplates();
        abstract @Nullable Long transactionSlowThresholdMillis();
        abstract Optional enabledProperty();
        abstract Optional traceEntryEnabledProperty();
        abstract @Nullable String version(); // null for insert operations

        private static InstrumentationConfigDto fromConfig(InstrumentationConfig config) {
            return InstrumentationConfigDto.builder()
                    .className(config.className())
                    .declaringClassName(config.declaringClassName())
                    .methodName(config.methodName())
                    .addAllMethodParameterTypes(config.methodParameterTypes())
                    .methodReturnType(config.methodReturnType())
                    .addAllMethodModifiers(config.methodModifiers())
                    .captureKind(config.captureKind())
                    .timerName(config.timerName())
                    .traceEntryTemplate(config.traceEntryTemplate())
                    .traceEntryStackThresholdMillis(config.traceEntryStackThresholdMillis())
                    .traceEntryCaptureSelfNested(config.traceEntryCaptureSelfNested())
                    .transactionType(config.transactionType())
                    .transactionNameTemplate(config.transactionNameTemplate())
                    .transactionUserTemplate(config.transactionUserTemplate())
                    .putAllTransactionCustomAttributeTemplates(
                            config.transactionCustomAttributeTemplates())
                    .transactionSlowThresholdMillis(config.transactionSlowThresholdMillis())
                    .enabledProperty(config.enabledProperty())
                    .traceEntryEnabledProperty(config.traceEntryEnabledProperty())
                    .version(config.version())
                    .build();
        }

        InstrumentationConfig toConfig() {
            return InstrumentationConfig.builder()
                    .className(className())
                    .declaringClassName(declaringClassName().or(""))
                    .methodName(methodName())
                    .addAllMethodParameterTypes(methodParameterTypes())
                    .methodReturnType(methodReturnType().or(""))
                    .addAllMethodModifiers(methodModifiers())
                    .captureKind(captureKind())
                    .timerName(timerName().or(""))
                    .traceEntryTemplate(traceEntryTemplate().or(""))
                    .traceEntryStackThresholdMillis(traceEntryStackThresholdMillis())
                    .traceEntryCaptureSelfNested(traceEntryCaptureSelfNested().or(false))
                    .transactionType(transactionType().or(""))
                    .transactionNameTemplate(transactionNameTemplate().or(""))
                    .transactionUserTemplate(transactionUserTemplate().or(""))
                    .putAllTransactionCustomAttributeTemplates(
                            transactionCustomAttributeTemplates())
                    .transactionSlowThresholdMillis(transactionSlowThresholdMillis())
                    .enabledProperty(enabledProperty().or(""))
                    .traceEntryEnabledProperty(traceEntryEnabledProperty().or(""))
                    .build();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy