Skip to content

Commit

Permalink
Add ManagementService
Browse files Browse the repository at this point in the history
Motivation:

Central Dogma does not provide a way to dump threads or heap data.

Modifications:

- Add `ManagementConfig` to config a management service using
  `dogma.json` and `CentralDogmaBuilder`
- Register `ManagementService` based on the options specified in
  `ManagementService`

Example:

- Serve a management service at the same ports of the main server
  ```
  {
    "management": {
      "port": 0
    }
  }
  ```
- Serve a management service only at an internal port which won't be
  exposed externally.
  ```
  {
    "management": {
      "address": "127.0.0.1"
      "port": 36463
    }
  }
  ```

Result:

You can now configure a managemet service to dump the thread information
or heap data.
  • Loading branch information
ikhoon committed Aug 30, 2024
1 parent 534f6bf commit 0018bd4
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.common.ServerCacheControl;
import com.linecorp.armeria.common.SessionProtocol;
import com.linecorp.armeria.common.metric.MeterIdPrefixFunction;
import com.linecorp.armeria.common.prometheus.PrometheusMeterRegistries;
import com.linecorp.armeria.common.util.EventLoopGroups;
Expand Down Expand Up @@ -106,6 +107,7 @@
import com.linecorp.armeria.server.healthcheck.HealthCheckService;
import com.linecorp.armeria.server.healthcheck.SettableHealthChecker;
import com.linecorp.armeria.server.logging.AccessLogWriter;
import com.linecorp.armeria.server.management.ManagementService;
import com.linecorp.armeria.server.metric.MetricCollectingService;
import com.linecorp.armeria.server.prometheus.PrometheusExpositionService;
import com.linecorp.armeria.server.thrift.THttpService;
Expand Down Expand Up @@ -568,7 +570,11 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,
sb.verboseResponses(true);
cfg.ports().forEach(sb::port);

if (cfg.ports().stream().anyMatch(ServerPort::hasTls)) {
final boolean needsTls =
cfg.ports().stream().anyMatch(ServerPort::hasTls) ||
(cfg.managementConfig() != null && cfg.managementConfig().protocol().isTls());

if (needsTls) {
try {
final TlsConfig tlsConfig = cfg.tls();
if (tlsConfig != null) {
Expand Down Expand Up @@ -610,6 +616,7 @@ private Server startServer(ProjectManager pm, CommandExecutor executor,
sb.service(HEALTH_CHECK_PATH, HealthCheckService.builder()
.checkers(serverHealth)
.build());
configManagement(sb, config().managementConfig());

sb.serviceUnder("/docs/",
DocService.builder()
Expand Down Expand Up @@ -914,6 +921,36 @@ private static void configCors(ServerBuilder sb, @Nullable CorsConfig corsConfig
.newDecorator());
}

private static void configManagement(ServerBuilder sb, @Nullable ManagementConfig managementConfig) {
if (managementConfig == null) {
return;
}

// curl -L https://<address>:<port>/internal/management/jvm/threaddump
// curl -L https://<address>:<port>/internal/management/jvm/heapdump -o heapdump.hprof
final int port = managementConfig.port();
if (port == 0) {
logger.info("'management.port' is 0, using the same ports as 'ports'.");
sb.route()
.pathPrefix(managementConfig.path())
.defaultServiceName("management")
.build(ManagementService.of());
} else {
final SessionProtocol managementProtocol = managementConfig.protocol();
final String address = managementConfig.address();
if (address == null) {
sb.port(new ServerPort(port, managementProtocol));
} else {
sb.port(new ServerPort(new InetSocketAddress(address, port), managementProtocol));
}
sb.virtualHost(port)
.route()
.pathPrefix(managementConfig.path())
.defaultServiceName("management")
.build(ManagementService.of());
}
}

private static Function<? super HttpService, EncodingService> contentEncodingDecorator() {
return delegate -> EncodingService
.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ public final class CentralDogmaBuilder {
private CorsConfig corsConfig;

private final List<PluginConfig> pluginConfigs = new ArrayList<>();
@Nullable
private ManagementConfig managementConfig;

/**
* Creates a new builder with the specified data directory.
Expand Down Expand Up @@ -529,6 +531,15 @@ public CentralDogmaBuilder pluginConfigs(PluginConfig... pluginConfigs) {
return this;
}

/**
* Enables a management service with the specified {@link ManagementConfig}.
*/
public CentralDogmaBuilder management(ManagementConfig managementConfig) {
requireNonNull(managementConfig, "managementConfig");
this.managementConfig = managementConfig;
return this;
}

/**
* Returns a newly-created {@link CentralDogma} server.
*/
Expand Down Expand Up @@ -562,6 +573,6 @@ private CentralDogmaConfig buildConfig() {
maxRemovedRepositoryAgeMillis, gracefulShutdownTimeout,
webAppEnabled, webAppTitle,replicationConfig,
null, accessLogFormat, authCfg, quotaConfig,
corsConfig, pluginConfigs);
corsConfig, pluginConfigs, managementConfig);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,14 @@ public static CentralDogmaConfig load(String json) throws JsonMappingException,
private final List<PluginConfig> pluginConfigs;
private final Map<Class<?>, PluginConfig> pluginConfigMap;

@Nullable
private final ManagementConfig managementConfig;

CentralDogmaConfig(
@JsonProperty(value = "dataDir", required = true) File dataDir,
@JsonProperty(value = "ports", required = true)
@JsonDeserialize(contentUsing = ServerPortDeserializer.class)
List<ServerPort> ports,
List<ServerPort> ports,
@JsonProperty("tls") @Nullable TlsConfig tls,
@JsonProperty("trustedProxyAddresses") @Nullable List<String> trustedProxyAddresses,
@JsonProperty("clientAddressSources") @Nullable List<String> clientAddressSources,
Expand All @@ -291,7 +294,8 @@ public static CentralDogmaConfig load(String json) throws JsonMappingException,
@JsonProperty("authentication") @Nullable AuthConfig authConfig,
@JsonProperty("writeQuotaPerRepository") @Nullable QuotaConfig writeQuotaPerRepository,
@JsonProperty("cors") @Nullable CorsConfig corsConfig,
@JsonProperty("pluginConfigs") @Nullable List<PluginConfig> pluginConfigs) {
@JsonProperty("pluginConfigs") @Nullable List<PluginConfig> pluginConfigs,
@JsonProperty("management") @Nullable ManagementConfig managementConfig) {

this.dataDir = requireNonNull(dataDir, "dataDir");
this.ports = ImmutableList.copyOf(requireNonNull(ports, "ports"));
Expand Down Expand Up @@ -339,6 +343,7 @@ public static CentralDogmaConfig load(String json) throws JsonMappingException,
this.pluginConfigs = firstNonNull(pluginConfigs, ImmutableList.of());
pluginConfigMap = this.pluginConfigs.stream().collect(
toImmutableMap(PluginConfig::getClass, Function.identity()));
this.managementConfig = managementConfig;
}

/**
Expand Down Expand Up @@ -568,6 +573,15 @@ public Map<Class<?>, PluginConfig> pluginConfigMap() {
return pluginConfigMap;
}

/**
* Returns the {@link ManagementConfig}.
*/
@Nullable
@JsonProperty("management")
public ManagementConfig managementConfig() {
return managementConfig;
}

@Override
public String toString() {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright 2024 LINE Corporation
*
* LINE Corporation licenses this file to you 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:
*
* https://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 com.linecorp.centraldogma.server;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;

import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.MoreObjects;

import com.linecorp.armeria.common.SessionProtocol;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.server.management.ManagementService;

/**
* A configuration for the {@link ManagementService}.
*/
public final class ManagementConfig {
private static final String DEFAULT_PROTOCOL = "http";
private static final String DEFAULT_PATH = "/internal/management";

private final SessionProtocol protocol;
private final @Nullable String address;
private final int port;
private final String path;

/**
* Creates a new instance.
*/
@JsonCreator
public ManagementConfig(@JsonProperty("protocol") @Nullable String protocol,
@JsonProperty("address") @Nullable String address,
@JsonProperty("port") int port,
@JsonProperty("path") @Nullable String path) {
this(SessionProtocol.of(firstNonNull(protocol, DEFAULT_PROTOCOL)),
address, port, path);
}

/**
* Creates a new instance.
*/
public ManagementConfig(@Nullable SessionProtocol protocol,
@Nullable String address,
int port,
@Nullable String path) {
protocol = firstNonNull(protocol, SessionProtocol.HTTP);
checkArgument(protocol != SessionProtocol.PROXY, "protocol: %s (expected: one of %s)",
protocol, SessionProtocol.httpAndHttpsValues());
this.protocol = protocol;
this.address = address;
checkArgument(port >= 0 && port <= 65535, "%s: %s (expected: 0-65535)", "management.port", port);
this.port = port;
this.path = firstNonNull(path, DEFAULT_PATH);
}

/**
* Returns the protocol of the management service.
*/
@JsonProperty("protocol")
public SessionProtocol protocol() {
return protocol;
}

/**
* Returns the address of the management service.
*/
@JsonProperty("address")
public @Nullable String address() {
return address;
}

/**
* Returns the port of the management service.
*/
@JsonProperty("port")
public int port() {
return port;
}

/**
* Returns the path of the management service.
*/
@JsonProperty("path")
public String path() {
return path;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ManagementConfig)) {
return false;
}
final ManagementConfig that = (ManagementConfig) o;
return port == that.port &&
protocol == that.protocol &&
Objects.equals(address, that.address) &&
path.equals(that.path);
}

@Override
public int hashCode() {
return Objects.hash(protocol, address, port, path);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("protocol", protocol)
.add("address", address)
.add("port", port)
.add("path", path)
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2024 LINE Corporation
*
* LINE Corporation licenses this file to you 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:
*
* https://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 com.linecorp.centraldogma.server;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.core.JsonProcessingException;

import com.linecorp.armeria.client.BlockingWebClient;
import com.linecorp.armeria.client.ClientFactory;
import com.linecorp.armeria.client.WebClient;
import com.linecorp.armeria.common.AggregatedHttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.SessionProtocol;
import com.linecorp.armeria.internal.common.util.PortUtil;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension;

class ManagementServiceTest {

private static int tlsPort;

@RegisterExtension
static final CentralDogmaExtension noManagement = new CentralDogmaExtension();

@RegisterExtension
static final CentralDogmaExtension management = new CentralDogmaExtension() {
@Override
protected void configure(CentralDogmaBuilder builder) {
builder.management(new ManagementConfig((String) null, null, 0, null));
}
};

@RegisterExtension
static final CentralDogmaExtension managementWithFullOptions = new CentralDogmaExtension() {
@Override
protected void configure(CentralDogmaBuilder builder) {
tlsPort = PortUtil.unusedTcpPort();
builder.management(
new ManagementConfig(SessionProtocol.HTTPS, "127.0.0.1", tlsPort, "/custom/management"));
}
};

@Test
void disableManagementServiceByDefault() {
final BlockingWebClient client = noManagement.blockingHttpClient();
assertThat(client.get("/internal/management").status()).isEqualTo(HttpStatus.NOT_FOUND);
}

@Test
void managementServiceWithDefaultOption() {
final BlockingWebClient client = management.blockingHttpClient();
final AggregatedHttpResponse response = client.get("/internal/management/jvm/threaddump");
assertThat(response.status()).isEqualTo(HttpStatus.OK);
assertThat(response.contentUtf8()).contains("repository-worker-");
}

@Test
void managementServiceWithFullOptions() {
final BlockingWebClient client =
WebClient.builder("https://127.0.0.1:" + tlsPort)
.factory(ClientFactory.insecure())
.build()
.blocking();
final AggregatedHttpResponse response = client.get("/custom/management/jvm/threaddump");
assertThat(response.status()).isEqualTo(HttpStatus.OK);
assertThat(response.contentUtf8()).contains("repository-worker-");
}

@Test
void testJsonDeserialization() throws JsonProcessingException {
final String json =
'{' +
"\"protocol\":\"https\"," +
"\"address\":\"127.0.0.1\"," +
"\"port\":8443," +
"\"path\":\"/custom/management\"" +
'}';
final ManagementConfig managementConfig = Jackson.readValue(json, ManagementConfig.class);

assertThat(managementConfig.protocol()).isEqualTo(SessionProtocol.HTTPS);
assertThat(managementConfig.port()).isEqualTo(8443);
assertThat(managementConfig.address()).isEqualTo("127.0.0.1");
assertThat(managementConfig.path()).isEqualTo("/custom/management");
}
}
Loading

0 comments on commit 0018bd4

Please sign in to comment.