/* * Copyright 2013-2017 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.ui; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.common.primitives.Ints; import io.netty.handler.codec.http.HttpResponseStatus; import org.immutables.value.Value; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.glowroot.common.live.LiveJvmService; import org.glowroot.common.live.LiveJvmService.AgentNotConnectedException; import org.glowroot.common.live.LiveWeavingService; import org.glowroot.common.repo.ConfigRepository; import org.glowroot.common.util.ObjectMappers; import org.glowroot.common.util.Versions; import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.InstrumentationConfig; import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.InstrumentationConfig.CaptureKind; import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.InstrumentationConfig.MethodModifier; import org.glowroot.wire.api.model.DownstreamServiceOuterClass.GlobalMeta; import org.glowroot.wire.api.model.DownstreamServiceOuterClass.MethodSignature; import org.glowroot.wire.api.model.Proto.OptionalInt32; import static com.google.common.base.Preconditions.checkNotNull; @JsonService class InstrumentationConfigJsonService { private static final Logger logger = LoggerFactory.getLogger(InstrumentationConfigJsonService.class); private static final ObjectMapper mapper = ObjectMappers.create(); private static final Ordering<InstrumentationConfig> ordering = new InstrumentationConfigOrdering(); private final ConfigRepository configRepository; private final @Nullable LiveWeavingService liveWeavingService; private final @Nullable LiveJvmService liveJvmService; InstrumentationConfigJsonService(ConfigRepository configRepository, @Nullable LiveWeavingService liveWeavingService, @Nullable LiveJvmService liveJvmService) { this.configRepository = configRepository; this.liveWeavingService = liveWeavingService; this.liveJvmService = liveJvmService; } @GET(path = "/backend/config/instrumentation", permission = "agent:config:view:instrumentation") String getInstrumentationConfig(@BindAgentId String agentId, @BindRequest InstrumentationConfigRequest request) throws Exception { Optional<String> version = request.version(); if (version.isPresent()) { return getInstrumentationConfigInternal(agentId, version.get()); } else { List<InstrumentationConfig> configs = configRepository.getInstrumentationConfigs(agentId); configs = ordering.immutableSortedCopy(configs); List<InstrumentationConfigDto> dtos = Lists.newArrayList(); for (InstrumentationConfig config : configs) { dtos.add(InstrumentationConfigDto.create(config)); } GlobalMeta globalMeta = null; if (liveWeavingService != null) { try { globalMeta = liveWeavingService.getGlobalMeta(agentId); } catch (AgentNotConnectedException e) { logger.debug(e.getMessage(), e); } } return mapper.writeValueAsString(ImmutableInstrumentationListResponse.builder() .addAllConfigs(dtos) .jvmOutOfSync(globalMeta != null && globalMeta.getJvmOutOfSync()) .jvmRetransformClassesSupported( globalMeta != null && globalMeta.getJvmRetransformClassesSupported()) .build()); } } @GET(path = "/backend/config/preload-classpath-cache", permission = "agent:config:view:instrumentation") void preloadClasspathCache(final @BindAgentId String agentId) throws Exception { if (liveWeavingService == null) { return; } // 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() { try { // TODO report checker framework issue that occurs without checkNotNull checkNotNull(liveWeavingService); liveWeavingService.preloadClasspathCache(agentId); } catch (AgentNotConnectedException e) { logger.debug(e.getMessage(), e); } catch (Exception e) { logger.error(e.getMessage(), e); } } }); thread.setDaemon(true); thread.setName("Glowroot-Temporary-Thread"); thread.start(); } @GET(path = "/backend/config/new-instrumentation-check-agent-connected", permission = "agent:config:edit:instrumentation") String checkAgentConnected(@BindAgentId String agentId) throws Exception { checkNotNull(liveJvmService); // agent:config:edit is disabled in offline viewer return Boolean.toString(liveJvmService.isAvailable(agentId)); } @GET(path = "/backend/config/matching-class-names", permission = "agent:config:edit:instrumentation") String getMatchingClassNames(@BindAgentId String agentId, @BindRequest ClassNamesRequest request) throws Exception { checkNotNull(liveWeavingService); // agent:config:edit is disabled in offline viewer return mapper.writeValueAsString(liveWeavingService.getMatchingClassNames(agentId, request.partialClassName(), request.limit())); } @GET(path = "/backend/config/matching-method-names", permission = "agent:config:edit:instrumentation") String getMatchingMethodNames(@BindAgentId String agentId, @BindRequest MethodNamesRequest request) throws Exception { checkNotNull(liveWeavingService); // agent:config:edit is disabled in offline viewer List<String> matchingMethodNames = liveWeavingService.getMatchingMethodNames(agentId, request.className(), request.partialMethodName(), request.limit()); return mapper.writeValueAsString(matchingMethodNames); } @GET(path = "/backend/config/method-signatures", permission = "agent:config:edit:instrumentation") String getMethodSignatures(@BindAgentId String agentId, @BindRequest MethodSignaturesRequest request) throws Exception { checkNotNull(liveWeavingService); // agent:config:edit is disabled in offline viewer List<MethodSignature> signatures = liveWeavingService.getMethodSignatures(agentId, request.className(), request.methodName()); List<MethodSignatureDto> methodSignatures = Lists.newArrayList(); for (MethodSignature signature : signatures) { methodSignatures.add(MethodSignatureDto.create(signature)); } return mapper.writeValueAsString(methodSignatures); } @POST(path = "/backend/config/instrumentation/add", permission = "agent:config:edit:instrumentation") String addInstrumentationConfig(@BindAgentId String agentId, @BindRequest InstrumentationConfigDto configDto) throws Exception { InstrumentationConfig config = configDto.convert(); configRepository.insertInstrumentationConfig(agentId, config); return getInstrumentationConfigInternal(agentId, Versions.getVersion(config)); } @POST(path = "/backend/config/instrumentation/update", permission = "agent:config:edit:instrumentation") String updateInstrumentationConfig(@BindAgentId String agentId, @BindRequest InstrumentationConfigDto configDto) throws Exception { InstrumentationConfig config = configDto.convert(); String version = configDto.version().get(); configRepository.updateInstrumentationConfig(agentId, config, version); return getInstrumentationConfigInternal(agentId, Versions.getVersion(config)); } @POST(path = "/backend/config/instrumentation/remove", permission = "agent:config:edit:instrumentation") void removeInstrumentationConfig(@BindAgentId String agentId, @BindRequest InstrumentationDeleteRequest request) throws Exception { configRepository.deleteInstrumentationConfigs(agentId, request.versions()); } @POST(path = "/backend/config/instrumentation/import", permission = "agent:config:edit:instrumentation") void importInstrumentationConfig(@BindAgentId String agentId, @BindRequest InstrumentationImportRequest request) throws Exception { List<InstrumentationConfig> configs = Lists.newArrayList(); for (InstrumentationConfigDto configDto : request.configs()) { configs.add(configDto.convert()); } configRepository.insertInstrumentationConfigs(agentId, configs); } @POST(path = "/backend/config/reweave", permission = "agent:config:edit:instrumentation") String reweave(@BindAgentId String agentId) throws Exception { checkNotNull(liveWeavingService); // agent:config:edit is disabled in offline viewer int count = liveWeavingService.reweave(agentId); return "{\"classes\":" + count + "}"; } private String getInstrumentationConfigInternal(String agentId, String version) throws Exception { InstrumentationConfig config = configRepository.getInstrumentationConfig(agentId, version); if (config == null) { throw new JsonServiceException(HttpResponseStatus.NOT_FOUND); } List<MethodSignature> methodSignatures = null; if (liveWeavingService != null) { try { methodSignatures = liveWeavingService.getMethodSignatures(agentId, config.getClassName(), config.getMethodName()); } catch (AgentNotConnectedException e) { logger.debug(e.getMessage(), e); } } ImmutableInstrumentationConfigResponse.Builder builder = ImmutableInstrumentationConfigResponse.builder() .agentNotConnected(methodSignatures == null) .config(InstrumentationConfigDto.create(config)); if (methodSignatures == null) { // agent not connected List<String> modifiers = Lists.newArrayList(); if (!isSignatureAll(config)) { for (MethodModifier modifier : config.getMethodModifierList()) { modifiers.add(modifier.name()); } builder.addMethodSignatures(ImmutableMethodSignatureDto.builder() .name(config.getMethodName()) .parameterTypes(config.getMethodParameterTypeList()) .returnType(config.getMethodReturnType()) .modifiers(modifiers) .build()); } } else { for (MethodSignature methodSignature : methodSignatures) { builder.addMethodSignatures(MethodSignatureDto.create(methodSignature)); } } return mapper.writeValueAsString(builder.build()); } private static boolean isSignatureAll(InstrumentationConfig config) { return config.getMethodModifierCount() == 0 && config.getMethodReturnType().isEmpty() && config.getMethodParameterTypeCount() == 1 && config.getMethodParameterType(0).equals(".."); } @Value.Immutable interface InstrumentationConfigRequest { Optional<String> version(); } @Value.Immutable interface ClassNamesRequest { String partialClassName(); int limit(); } @Value.Immutable interface MethodNamesRequest { String className(); String partialMethodName(); int limit(); } @Value.Immutable interface MethodSignaturesRequest { String className(); String methodName(); } @Value.Immutable interface InstrumentationListResponse { ImmutableList<InstrumentationConfigDto> configs(); boolean jvmOutOfSync(); boolean jvmRetransformClassesSupported(); } @Value.Immutable interface InstrumentationConfigResponse { boolean agentNotConnected(); InstrumentationConfigDto config(); ImmutableList<MethodSignatureDto> methodSignatures(); } @Value.Immutable interface InstrumentationErrorResponse { ImmutableList<String> errors(); } @Value.Immutable interface InstrumentationImportRequest { ImmutableList<ImmutableInstrumentationConfigDto> configs(); } @Value.Immutable interface InstrumentationDeleteRequest { List<String> versions(); } @Value.Immutable @JsonInclude(value = Include.ALWAYS) abstract static class InstrumentationConfigDto { abstract String className(); abstract String classAnnotation(); abstract String subTypeRestriction(); abstract String superTypeRestriction(); // pointcuts with methodDeclaringClassName are no longer supported in 0.9.16, but // included here to help with transitioning of old instrumentation config // // also included here to support glowroot central 0.9.16 or newer running agent 0.9.15 or // older @Deprecated abstract @Nullable String methodDeclaringClassName(); abstract String methodName(); abstract String methodAnnotation(); abstract ImmutableList<String> methodParameterTypes(); abstract String methodReturnType(); abstract ImmutableList<MethodModifier> methodModifiers(); abstract String nestingGroup(); abstract int order(); abstract CaptureKind captureKind(); abstract String transactionType(); abstract String transactionNameTemplate(); abstract String transactionUserTemplate(); abstract Map<String, String> transactionAttributeTemplates(); abstract @Nullable Integer transactionSlowThresholdMillis(); abstract boolean transactionOuter(); abstract String traceEntryMessageTemplate(); abstract @Nullable Integer traceEntryStackThresholdMillis(); abstract boolean traceEntryCaptureSelfNested(); abstract String timerName(); abstract String enabledProperty(); abstract String traceEntryEnabledProperty(); abstract Optional<String> version(); // absent for insert operations private InstrumentationConfig convert() { InstrumentationConfig.Builder builder = InstrumentationConfig.newBuilder() .setClassName(className()) .setClassAnnotation(classAnnotation()) .setSubTypeRestriction(subTypeRestriction()) .setSuperTypeRestriction(superTypeRestriction()) // pointcuts with methodDeclaringClassName are no longer supported in 0.9.16, // but included here to help with transitioning of old instrumentation config // // also included here to support glowroot central 0.9.16 or newer running agent // 0.9.15 or older .setMethodDeclaringClassName(Strings.nullToEmpty(methodDeclaringClassName())) .setMethodName(methodName()) .setMethodAnnotation(methodAnnotation()) .addAllMethodParameterType(methodParameterTypes()) .setMethodReturnType(methodReturnType()) .addAllMethodModifier(methodModifiers()) .setNestingGroup(nestingGroup()) .setOrder(order()) .setCaptureKind(captureKind()) .setTransactionType(transactionType()) .setTransactionNameTemplate(transactionNameTemplate()) .setTransactionUserTemplate(transactionUserTemplate()) .putAllTransactionAttributeTemplates(transactionAttributeTemplates()); Integer transactionSlowThresholdMillis = transactionSlowThresholdMillis(); if (transactionSlowThresholdMillis != null) { builder.setTransactionSlowThresholdMillis( OptionalInt32.newBuilder().setValue(transactionSlowThresholdMillis)); } builder.setTransactionOuter(transactionOuter()) .setTraceEntryMessageTemplate(traceEntryMessageTemplate()); Integer traceEntryStackThresholdMillis = traceEntryStackThresholdMillis(); if (traceEntryStackThresholdMillis != null) { builder.setTraceEntryStackThresholdMillis( OptionalInt32.newBuilder().setValue(traceEntryStackThresholdMillis)); } return builder.setTraceEntryCaptureSelfNested(traceEntryCaptureSelfNested()) .setTimerName(timerName()) .setEnabledProperty(enabledProperty()) .setTraceEntryEnabledProperty(traceEntryEnabledProperty()) .build(); } private static InstrumentationConfigDto create(InstrumentationConfig config) { @SuppressWarnings("deprecation") ImmutableInstrumentationConfigDto.Builder builder = ImmutableInstrumentationConfigDto.builder() .className(config.getClassName()) .classAnnotation(config.getClassAnnotation()) .subTypeRestriction(config.getSubTypeRestriction()) .superTypeRestriction(config.getSuperTypeRestriction()) // pointcuts with methodDeclaringClassName are no longer supported in // 0.9.16, but included here to help with transitioning of old // instrumentation config // // also included here to support glowroot central 0.9.16 or newer // running agent 0.9.15 or older .methodDeclaringClassName( Strings.emptyToNull(config.getMethodDeclaringClassName())) .methodName(config.getMethodName()) .methodAnnotation(config.getMethodAnnotation()) .addAllMethodParameterTypes(config.getMethodParameterTypeList()) .methodReturnType(config.getMethodReturnType()) .addAllMethodModifiers(config.getMethodModifierList()) .nestingGroup(config.getNestingGroup()) .order(config.getOrder()) .captureKind(config.getCaptureKind()) .transactionType(config.getTransactionType()) .transactionNameTemplate(config.getTransactionNameTemplate()) .transactionUserTemplate(config.getTransactionUserTemplate()) .putAllTransactionAttributeTemplates( config.getTransactionAttributeTemplatesMap()); if (config.hasTransactionSlowThresholdMillis()) { builder.transactionSlowThresholdMillis( config.getTransactionSlowThresholdMillis().getValue()); } builder.transactionOuter(config.getTransactionOuter()) .traceEntryMessageTemplate(config.getTraceEntryMessageTemplate()); if (config.hasTraceEntryStackThresholdMillis()) { builder.traceEntryStackThresholdMillis( config.getTraceEntryStackThresholdMillis().getValue()); } return builder.traceEntryCaptureSelfNested(config.getTraceEntryCaptureSelfNested()) .timerName(config.getTimerName()) .enabledProperty(config.getEnabledProperty()) .traceEntryEnabledProperty(config.getTraceEntryEnabledProperty()) .version(Versions.getVersion(config)) .build(); } } @Value.Immutable @JsonInclude(value = Include.ALWAYS) abstract static class MethodSignatureDto { abstract String name(); abstract ImmutableList<String> parameterTypes(); abstract String returnType(); abstract ImmutableList<String> modifiers(); private static MethodSignatureDto create(MethodSignature methodSignature) { return ImmutableMethodSignatureDto.builder() .name(methodSignature.getName()) .addAllParameterTypes(methodSignature.getParameterTypeList()) .returnType(methodSignature.getReturnType()) .modifiers(methodSignature.getModifierList()) .build(); } } @VisibleForTesting static class InstrumentationConfigOrdering extends Ordering<InstrumentationConfig> { @Override public int compare(InstrumentationConfig left, InstrumentationConfig right) { int compare = left.getClassName().compareToIgnoreCase(right.getClassName()); if (compare != 0) { return compare; } compare = left.getMethodName().compareToIgnoreCase(right.getMethodName()); if (compare != 0) { return compare; } List<String> leftParameterTypes = left.getMethodParameterTypeList(); List<String> rightParameterTypes = right.getMethodParameterTypeList(); compare = Ints.compare(leftParameterTypes.size(), rightParameterTypes.size()); if (compare != 0) { return compare; } for (int i = 0; i < leftParameterTypes.size(); i++) { compare = leftParameterTypes.get(i).compareToIgnoreCase(rightParameterTypes.get(i)); if (compare != 0) { return compare; } } return 0; } } }