/*
* Copyright 2011-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 javax.annotation.Nullable;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import org.immutables.value.Value;
import org.glowroot.common.repo.AgentRepository;
import org.glowroot.common.repo.ConfigRepository;
import org.glowroot.common.repo.ConfigRepository.OptimisticLockException;
import org.glowroot.common.util.ObjectMappers;
import org.glowroot.common.util.Versions;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.AdvancedConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.PluginConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.PluginProperty;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.TransactionConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.UiConfig;
import org.glowroot.wire.api.model.AgentConfigOuterClass.AgentConfig.UserRecordingConfig;
import org.glowroot.wire.api.model.Proto.OptionalInt32;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.netty.handler.codec.http.HttpResponseStatus.PRECONDITION_FAILED;
@JsonService
class ConfigJsonService {
private static final ObjectMapper mapper = ObjectMappers.create();
private final AgentRepository agentRepository;
private final ConfigRepository configRepository;
ConfigJsonService(AgentRepository agentRepository, ConfigRepository configRepository) {
this.agentRepository = agentRepository;
this.configRepository = configRepository;
}
@GET(path = "/backend/config/transaction", permission = "agent:config:view:transaction")
String getTransactionConfig(@BindAgentId String agentId) throws Exception {
TransactionConfig config = configRepository.getTransactionConfig(agentId);
return mapper.writeValueAsString(TransactionConfigDto.create(config));
}
// central supports ui config on rollups
@GET(path = "/backend/config/ui", permission = "agent:config:view:ui")
String getUiConfig(@BindAgentRollupId String agentRollupId) throws Exception {
UiConfig config = configRepository.getUiConfig(agentRollupId);
return mapper.writeValueAsString(UiConfigDto.create(config));
}
@GET(path = "/backend/config/plugins", permission = "agent:config:view:plugin")
String getPluginConfig(@BindAgentId String agentId, @BindRequest PluginConfigRequest request)
throws Exception {
Optional<String> pluginId = request.pluginId();
if (pluginId.isPresent()) {
return getPluginConfigInternal(agentId, request.pluginId().get());
} else {
List<PluginResponse> pluginResponses = Lists.newArrayList();
List<PluginConfig> pluginConfigs = configRepository.getPluginConfigs(agentId);
for (PluginConfig pluginConfig : pluginConfigs) {
pluginResponses.add(ImmutablePluginResponse.builder()
.id(pluginConfig.getId())
.name(pluginConfig.getName())
.hasConfig(pluginConfig.getPropertyCount() > 0)
.build());
}
return mapper.writeValueAsString(pluginResponses);
}
}
@GET(path = "/backend/config/user-recording", permission = "agent:config:view:userRecording")
String getUserRecordingConfig(@BindAgentId String agentId) throws Exception {
UserRecordingConfig config = configRepository.getUserRecordingConfig(agentId);
return mapper.writeValueAsString(UserRecordingConfigDto.create(config));
}
// central supports advanced config on rollups
// (maxAggregateQueriesPerType and maxAggregateServiceCallsPerType)
@GET(path = "/backend/config/advanced", permission = "agent:config:view:advanced")
String getAdvancedConfig(@BindAgentRollupId String agentRollupId) throws Exception {
AdvancedConfig config = configRepository.getAdvancedConfig(agentRollupId);
return mapper.writeValueAsString(
AdvancedConfigDto.create(config, agentRepository.isAgent(agentRollupId)));
}
@POST(path = "/backend/config/transaction", permission = "agent:config:edit:transaction")
String updateTransactionConfig(@BindAgentId String agentId,
@BindRequest TransactionConfigDto configDto) throws Exception {
try {
configRepository.updateTransactionConfig(agentId, configDto.convert(),
configDto.version());
} catch (OptimisticLockException e) {
throw new JsonServiceException(PRECONDITION_FAILED, e);
}
return getTransactionConfig(agentId);
}
// central supports ui config on rollups
@POST(path = "/backend/config/ui", permission = "agent:config:edit:ui")
String updateUiConfig(@BindAgentRollupId String agentRollupId,
@BindRequest UiConfigDto configDto) throws Exception {
try {
configRepository.updateUiConfig(agentRollupId, configDto.convert(),
configDto.version());
} catch (OptimisticLockException e) {
throw new JsonServiceException(PRECONDITION_FAILED, e);
}
return getUiConfig(agentRollupId);
}
@POST(path = "/backend/config/plugins", permission = "agent:config:edit:plugins")
String updatePluginConfig(@BindAgentId String agentId, @BindRequest PluginUpdateRequest request)
throws Exception {
List<PluginProperty> properties = Lists.newArrayList();
for (PluginPropertyDto prop : request.properties()) {
properties.add(prop.convert());
}
String pluginId = request.pluginId();
try {
configRepository.updatePluginConfig(agentId, pluginId, properties, request.version());
} catch (OptimisticLockException e) {
throw new JsonServiceException(PRECONDITION_FAILED, e);
}
return getPluginConfigInternal(agentId, pluginId);
}
@POST(path = "/backend/config/user-recording", permission = "agent:config:edit:userRecording")
String updateUserRecordingConfig(@BindAgentId String agentId,
@BindRequest UserRecordingConfigDto configDto) throws Exception {
try {
configRepository.updateUserRecordingConfig(agentId, configDto.convert(),
configDto.version());
} catch (OptimisticLockException e) {
throw new JsonServiceException(PRECONDITION_FAILED, e);
}
return getUserRecordingConfig(agentId);
}
// central supports advanced config on rollups
// (maxAggregateQueriesPerType and maxAggregateServiceCallsPerType)
@POST(path = "/backend/config/advanced", permission = "agent:config:edit:advanced")
String updateAdvancedConfig(@BindAgentRollupId String agentRollupId,
@BindRequest AdvancedConfigDto configDto) throws Exception {
try {
AdvancedConfig config = configDto.convert(agentRepository.isAgent(agentRollupId));
configRepository.updateAdvancedConfig(agentRollupId, config, configDto.version());
} catch (OptimisticLockException e) {
throw new JsonServiceException(PRECONDITION_FAILED, e);
}
return getAdvancedConfig(agentRollupId);
}
private String getPluginConfigInternal(String agentId, String pluginId) throws Exception {
PluginConfig config = configRepository.getPluginConfig(agentId, pluginId);
if (config == null) {
throw new IllegalArgumentException("Plugin id not found: " + pluginId);
}
return mapper.writeValueAsString(PluginConfigDto.create(config));
}
private static OptionalInt32 of(int value) {
return OptionalInt32.newBuilder().setValue(value).build();
}
@Value.Immutable
interface PluginConfigRequest {
Optional<String> pluginId();
}
@Value.Immutable
interface PluginResponse {
String id();
String name();
boolean hasConfig();
}
// these DTOs are only different from underlying config objects in that they contain the version
// attribute, and that they have no default attribute values
@Value.Immutable
abstract static class TransactionConfigDto {
abstract int slowThresholdMillis();
abstract int profilingIntervalMillis();
abstract boolean captureThreadStats();
abstract String version();
private TransactionConfig convert() {
return TransactionConfig.newBuilder()
.setSlowThresholdMillis(of(slowThresholdMillis()))
.setProfilingIntervalMillis(of(profilingIntervalMillis()))
.setCaptureThreadStats(captureThreadStats())
.build();
}
private static TransactionConfigDto create(TransactionConfig config) {
return ImmutableTransactionConfigDto.builder()
.slowThresholdMillis(config.getSlowThresholdMillis().getValue())
.profilingIntervalMillis(config.getProfilingIntervalMillis().getValue())
.captureThreadStats(config.getCaptureThreadStats())
.version(Versions.getVersion(config))
.build();
}
}
@Value.Immutable
abstract static class UserRecordingConfigDto {
abstract ImmutableList<String> users();
abstract @Nullable Integer profilingIntervalMillis();
abstract String version();
private UserRecordingConfig convert() {
UserRecordingConfig.Builder builder = UserRecordingConfig.newBuilder()
.addAllUser(users());
Integer profilingIntervalMillis = profilingIntervalMillis();
if (profilingIntervalMillis != null) {
builder.setProfilingIntervalMillis(
OptionalInt32.newBuilder().setValue(profilingIntervalMillis));
}
return builder.build();
}
private static UserRecordingConfigDto create(UserRecordingConfig config) {
ImmutableUserRecordingConfigDto.Builder builder =
ImmutableUserRecordingConfigDto.builder()
.users(config.getUserList());
if (config.hasProfilingIntervalMillis()) {
builder.profilingIntervalMillis(config.getProfilingIntervalMillis().getValue());
}
return builder.version(Versions.getVersion(config))
.build();
}
}
@Value.Immutable
interface PluginUpdateRequest {
String pluginId();
List<ImmutablePluginPropertyDto> properties();
String version();
}
// only used in response
@Value.Immutable
abstract static class PluginConfigDto {
abstract String name();
abstract List<ImmutablePluginPropertyDto> properties();
abstract String version();
private static PluginConfigDto create(PluginConfig config) {
ImmutablePluginConfigDto.Builder builder = ImmutablePluginConfigDto.builder()
.name(config.getName());
for (PluginProperty property : config.getPropertyList()) {
builder.addProperties(PluginPropertyDto.create(property));
}
return builder.version(Versions.getVersion(config))
.build();
}
}
// only used in response
@Value.Immutable
abstract static class PluginPropertyDto {
abstract String name();
abstract PropertyType type();
abstract @Nullable Object value();
abstract @Nullable Object defaultValue(); // only used in response
abstract @Nullable String label(); // only used in response
abstract @Nullable String checkboxLabel(); // only used in response
abstract @Nullable String description(); // only used in response
private PluginProperty convert() {
return PluginProperty.newBuilder()
.setName(name())
.setValue(getValue())
.build();
}
private PluginProperty.Value getValue() {
Object value = value();
switch (type()) {
case BOOLEAN:
checkNotNull(value);
return PluginProperty.Value.newBuilder().setBval((Boolean) value).build();
case DOUBLE:
if (value == null) {
return PluginProperty.Value.newBuilder().setDvalNull(true).build();
} else {
return PluginProperty.Value.newBuilder()
.setDval(((Number) value).doubleValue()).build();
}
case STRING:
checkNotNull(value);
return PluginProperty.Value.newBuilder().setSval((String) value).build();
default:
throw new IllegalStateException("Unexpected property type: " + type());
}
}
private static ImmutablePluginPropertyDto create(PluginProperty property) {
return ImmutablePluginPropertyDto.builder()
.name(property.getName())
.type(getPropertyType(property.getValue().getValCase()))
.value(getPropertyValue(property.getValue()))
.defaultValue(getPropertyValue(property.getValue()))
.label(property.getLabel())
.checkboxLabel(property.getCheckboxLabel())
.description(property.getDescription())
.build();
}
private static PropertyType getPropertyType(PluginProperty.Value.ValCase valCase) {
switch (valCase) {
case BVAL:
return PropertyType.BOOLEAN;
case DVAL_NULL:
case DVAL:
return PropertyType.DOUBLE;
case SVAL:
return PropertyType.STRING;
default:
throw new IllegalStateException("Unexpected property type: " + valCase);
}
}
private static @Nullable Object getPropertyValue(PluginProperty.Value value) {
PluginProperty.Value.ValCase valCase = value.getValCase();
switch (valCase) {
case BVAL:
return value.getBval();
case DVAL_NULL:
return null;
case DVAL:
return value.getDval();
case SVAL:
return value.getSval();
default:
throw new IllegalStateException("Unexpected property type: " + valCase);
}
}
}
enum PropertyType {
BOOLEAN, DOUBLE, STRING;
}
@Value.Immutable
abstract static class AdvancedConfigDto {
abstract @Nullable Boolean weavingTimer(); // null for rollup config
abstract @Nullable Integer immediatePartialStoreThresholdSeconds(); // null for rollup cfg
abstract @Nullable Integer maxAggregateTransactionsPerType(); // null for rollup config
abstract int maxAggregateQueriesPerType();
abstract int maxAggregateServiceCallsPerType();
abstract @Nullable Integer maxTraceEntriesPerTransaction(); // null for rollup config
abstract @Nullable Integer maxStackTraceSamplesPerTransaction(); // null for rollup config
abstract @Nullable Integer mbeanGaugeNotFoundDelaySeconds(); // null for rollup config
abstract String version();
private AdvancedConfig convert(boolean agent) {
if (agent) {
return AdvancedConfig.newBuilder()
.setWeavingTimer(checkNotNull(weavingTimer()))
.setImmediatePartialStoreThresholdSeconds(
of(checkNotNull(immediatePartialStoreThresholdSeconds())))
.setMaxAggregateTransactionsPerType(
of(checkNotNull(maxAggregateTransactionsPerType())))
.setMaxAggregateQueriesPerType(of(maxAggregateQueriesPerType()))
.setMaxAggregateServiceCallsPerType(of(maxAggregateServiceCallsPerType()))
.setMaxTraceEntriesPerTransaction(
of(checkNotNull(maxTraceEntriesPerTransaction())))
.setMaxStackTraceSamplesPerTransaction(
of(checkNotNull(maxStackTraceSamplesPerTransaction())))
.setMbeanGaugeNotFoundDelaySeconds(
of(checkNotNull(mbeanGaugeNotFoundDelaySeconds())))
.build();
} else {
return AdvancedConfig.newBuilder()
.setMaxAggregateQueriesPerType(of(maxAggregateQueriesPerType()))
.setMaxAggregateServiceCallsPerType(of(maxAggregateServiceCallsPerType()))
.build();
}
}
private static AdvancedConfigDto create(AdvancedConfig config, boolean agent) {
if (agent) {
return ImmutableAdvancedConfigDto.builder()
.weavingTimer(config.getWeavingTimer())
.immediatePartialStoreThresholdSeconds(
config.getImmediatePartialStoreThresholdSeconds().getValue())
.maxAggregateTransactionsPerType(
config.getMaxAggregateTransactionsPerType().getValue())
.maxAggregateQueriesPerType(
config.getMaxAggregateQueriesPerType().getValue())
.maxAggregateServiceCallsPerType(
config.getMaxAggregateServiceCallsPerType().getValue())
.maxTraceEntriesPerTransaction(
config.getMaxTraceEntriesPerTransaction().getValue())
.maxStackTraceSamplesPerTransaction(
config.getMaxStackTraceSamplesPerTransaction().getValue())
.mbeanGaugeNotFoundDelaySeconds(
config.getMbeanGaugeNotFoundDelaySeconds().getValue())
.version(Versions.getVersion(config))
.build();
} else {
return ImmutableAdvancedConfigDto.builder()
.maxAggregateQueriesPerType(
config.getMaxAggregateQueriesPerType().getValue())
.maxAggregateServiceCallsPerType(
config.getMaxAggregateServiceCallsPerType().getValue())
.version(Versions.getVersion(config))
.build();
}
}
}
@Value.Immutable
abstract static class UiConfigDto {
abstract String defaultDisplayedTransactionType();
abstract ImmutableList<Double> defaultDisplayedPercentiles();
abstract String version();
private UiConfig convert() throws Exception {
return UiConfig.newBuilder()
.setDefaultDisplayedTransactionType(defaultDisplayedTransactionType())
.addAllDefaultDisplayedPercentile(
Ordering.natural().immutableSortedCopy(defaultDisplayedPercentiles()))
.build();
}
private static UiConfigDto create(UiConfig config) {
return ImmutableUiConfigDto.builder()
.defaultDisplayedTransactionType(config.getDefaultDisplayedTransactionType())
.defaultDisplayedPercentiles(Ordering.natural()
.immutableSortedCopy(config.getDefaultDisplayedPercentileList()))
.version(Versions.getVersion(config))
.build();
}
}
}