-
Notifications
You must be signed in to change notification settings - Fork 898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add /info URL to provide information about server application #5626
base: main
Are you sure you want to change the base?
Changes from all commits
b6aca60
6934dec
527d850
82c35d5
448c7fe
63f109e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,100 @@ | ||||||||||
/* | ||||||||||
* 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.armeria.server.management; | ||||||||||
|
||||||||||
import java.util.Objects; | ||||||||||
|
||||||||||
import com.fasterxml.jackson.annotation.JsonProperty; | ||||||||||
import com.google.common.base.MoreObjects; | ||||||||||
|
||||||||||
import com.linecorp.armeria.common.annotation.Nullable; | ||||||||||
|
||||||||||
/** | ||||||||||
* A class that represents application information, which can be configured through | ||||||||||
* {@link ManagementService#of(AppInfo)}. | ||||||||||
*/ | ||||||||||
public final class AppInfo { | ||||||||||
@Nullable final String version; | ||||||||||
@Nullable final String name; | ||||||||||
@Nullable final String description; | ||||||||||
|
||||||||||
/** | ||||||||||
* Creates a new {@link AppInfo} that holds information about an application. | ||||||||||
* @param version A version of an application e.g. "1.0.0" | ||||||||||
* @param name A name of an application | ||||||||||
* @param description A description of application | ||||||||||
*/ | ||||||||||
public AppInfo(@Nullable String version, @Nullable String name, @Nullable String description) { | ||||||||||
Comment on lines
+35
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||
this.version = version; | ||||||||||
this.name = name; | ||||||||||
this.description = description; | ||||||||||
} | ||||||||||
ikhoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
|
||||||||||
/** | ||||||||||
* Returns the artifact version of the deployed application, such as {@code "1.0.0"}. | ||||||||||
*/ | ||||||||||
@JsonProperty | ||||||||||
public String getVersion() { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||
return version; | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Returns the name of the deployed application. | ||||||||||
*/ | ||||||||||
@JsonProperty | ||||||||||
public String getName() { | ||||||||||
return name; | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Returns the description of the deployed application. | ||||||||||
*/ | ||||||||||
@JsonProperty | ||||||||||
public String getDescription() { | ||||||||||
return description; | ||||||||||
} | ||||||||||
|
||||||||||
@Override | ||||||||||
public String toString() { | ||||||||||
return MoreObjects.toStringHelper(this) | ||||||||||
.add("version", version) | ||||||||||
.add("name", name) | ||||||||||
Comment on lines
+74
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
.add("description", description) | ||||||||||
.toString(); | ||||||||||
} | ||||||||||
Comment on lines
+71
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you move this method to the bottom of the class definition, for consistency with our existing code? |
||||||||||
|
||||||||||
@Override | ||||||||||
public boolean equals(Object o) { | ||||||||||
if (this == o) { | ||||||||||
return true; | ||||||||||
} | ||||||||||
|
||||||||||
if (o == null || getClass() != o.getClass()) { | ||||||||||
return false; | ||||||||||
} | ||||||||||
|
||||||||||
final AppInfo appInfo = (AppInfo) o; | ||||||||||
return Objects.equals(version, appInfo.version) && | ||||||||||
Objects.equals(name, appInfo.name) && | ||||||||||
Objects.equals(description, appInfo.description); | ||||||||||
} | ||||||||||
|
||||||||||
@Override | ||||||||||
public int hashCode() { | ||||||||||
return Objects.hash(version, name, description); | ||||||||||
} | ||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,101 @@ | ||||||
/* | ||||||
* 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.armeria.server.management; | ||||||
|
||||||
import static java.util.Objects.requireNonNull; | ||||||
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException; | ||||||
import com.google.common.collect.ImmutableMap; | ||||||
|
||||||
import com.linecorp.armeria.common.AggregatedHttpResponse; | ||||||
import com.linecorp.armeria.common.HttpRequest; | ||||||
import com.linecorp.armeria.common.HttpResponse; | ||||||
import com.linecorp.armeria.common.HttpStatus; | ||||||
import com.linecorp.armeria.common.MediaType; | ||||||
import com.linecorp.armeria.common.annotation.Nullable; | ||||||
import com.linecorp.armeria.common.util.Version; | ||||||
import com.linecorp.armeria.internal.common.JacksonUtil; | ||||||
import com.linecorp.armeria.server.HttpService; | ||||||
import com.linecorp.armeria.server.Server; | ||||||
import com.linecorp.armeria.server.ServiceRequestContext; | ||||||
|
||||||
/** | ||||||
* An {@link HttpService} that provides additional information about configured server. | ||||||
* This functionality is can be used by binding {@link ManagementService} | ||||||
* It provides information about not only deployed application information, which can be specified by user, | ||||||
* but also one about Armeria artifact itself. | ||||||
*/ | ||||||
public enum AppInfoService implements HttpService { | ||||||
|
||||||
INSTANCE; | ||||||
|
||||||
private static final Version armeriaVersionInfo = Version.get("armeria", Server.class.getClassLoader()); | ||||||
|
||||||
@Nullable | ||||||
private AggregatedHttpResponse infoAggregatedResponse; | ||||||
|
||||||
void setAppInfo(@Nullable AppInfo appInfo) { | ||||||
final byte[] data; | ||||||
|
||||||
try { | ||||||
if (appInfo == null) { | ||||||
data = JacksonUtil.writeValueAsBytes(buildArmeriaInfoMap()); | ||||||
} else { | ||||||
data = JacksonUtil.writeValueAsBytes(buildInfoMap(appInfo)); | ||||||
} | ||||||
} catch (JsonProcessingException e) { | ||||||
throw new IllegalArgumentException(e.toString(), e); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's probably no point in doing
Suggested change
|
||||||
} | ||||||
|
||||||
infoAggregatedResponse = AggregatedHttpResponse.of(HttpStatus.OK, MediaType.JSON, data); | ||||||
} | ||||||
|
||||||
@Override | ||||||
public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception { | ||||||
assert infoAggregatedResponse != null; | ||||||
return infoAggregatedResponse.toHttpResponse(); | ||||||
} | ||||||
|
||||||
private static ImmutableMap<Object, Object> buildInfoMap(AppInfo appInfo) { | ||||||
requireNonNull(appInfo, "appInfo"); | ||||||
return ImmutableMap.builder() | ||||||
.putAll(buildArmeriaInfoMap()) | ||||||
.putAll(buildAppInfoMap(appInfo)) | ||||||
.build(); | ||||||
} | ||||||
|
||||||
private static ImmutableMap<String, ImmutableMap<String, String>> buildArmeriaInfoMap() { | ||||||
return ImmutableMap.of( | ||||||
"armeria", ImmutableMap.of( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question) Couldn't we just return the entire version?
Suggested change
|
||||||
"version", armeriaVersionInfo.artifactVersion(), | ||||||
"commit", armeriaVersionInfo.longCommitHash(), | ||||||
"repositoryStatus", armeriaVersionInfo.repositoryStatus() | ||||||
) | ||||||
); | ||||||
} | ||||||
|
||||||
private static ImmutableMap<String, ImmutableMap<String, String>> buildAppInfoMap(AppInfo appInfo) { | ||||||
requireNonNull(appInfo, "appInfo"); | ||||||
return ImmutableMap.of( | ||||||
"app", ImmutableMap.of( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto, can't we just serialize the entire
Suggested change
|
||||||
"version", appInfo.version, | ||||||
"name", appInfo.name, | ||||||
"description", appInfo.description | ||||||
) | ||||||
); | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,8 @@ | |
|
||
package com.linecorp.armeria.server.management; | ||
|
||
import static java.util.Objects.requireNonNull; | ||
|
||
import com.linecorp.armeria.common.HttpHeaderNames; | ||
import com.linecorp.armeria.common.HttpRequest; | ||
import com.linecorp.armeria.common.HttpResponse; | ||
|
@@ -64,6 +66,16 @@ public final class ManagementService extends AbstractHttpService { | |
* Returns a singleton {@link ManagementService}. | ||
*/ | ||
public static ManagementService of() { | ||
AppInfoService.INSTANCE.setAppInfo(null); | ||
return INSTANCE; | ||
} | ||
|
||
/** | ||
* Returns a singleton {@link ManagementService} with specified {@link AppInfo}. | ||
*/ | ||
public static ManagementService of(AppInfo appInfo) { | ||
requireNonNull(appInfo, "appInfo"); | ||
AppInfoService.INSTANCE.setAppInfo(appInfo); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be overwritten if there are multiple servers in the same JVM. e.g. public static ManagementService of() {
return INSTANCE;
}
public static ManagementService of(AppInfo appInfo) {
requireNonNull(appInfo, "appInfo");
return new ManagementService(appInfo);
} |
||
return INSTANCE; | ||
} | ||
|
||
|
@@ -77,6 +89,8 @@ public HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) throws Exc | |
return ThreadDumpService.INSTANCE.serve(ctx, req); | ||
case "/jvm/heapdump": | ||
return HeapDumpService.INSTANCE.serve(ctx, req); | ||
case "/info": | ||
return AppInfoService.INSTANCE.serve(ctx, req); | ||
default: | ||
return HttpResponse.of(HttpStatus.NOT_FOUND); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,8 @@ | |
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.RegisterExtension; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
||
import com.linecorp.armeria.client.WebClient; | ||
|
@@ -43,13 +45,16 @@ | |
|
||
class ManagementServiceTest { | ||
private static final ObjectMapper mapper = new ObjectMapper(); | ||
private static final AppInfo TEST_APP_INFO = new AppInfo( | ||
"1.0.0", "test-app-name", "test-app-description" | ||
); | ||
|
||
@RegisterExtension | ||
static ServerExtension server = new ServerExtension() { | ||
@Override | ||
protected void configure(ServerBuilder sb) { | ||
sb.requestTimeout(Duration.ofSeconds(45)); // Heap dump can take time. | ||
sb.serviceUnder("/internal/management", ManagementService.of()); | ||
sb.serviceUnder("/internal/management", ManagementService.of(TEST_APP_INFO)); | ||
} | ||
}; | ||
|
||
|
@@ -103,4 +108,22 @@ void heapDump() throws InterruptedException { | |
// Make sure that the returned file has a valid hprof format | ||
assertThat(Arrays.copyOf(actual, fileHeader.length)).isEqualTo(fileHeader); | ||
} | ||
|
||
@Test | ||
void appInfo() throws JsonProcessingException { | ||
final WebClient client = WebClient.builder(server.httpUri()) | ||
.build(); | ||
|
||
final AggregatedHttpResponse response = client.get("/internal/management/info").aggregate().join(); | ||
final String content = response.contentUtf8(); | ||
final JsonNode jsonNode = mapper.readTree(content).path("app"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional) We could use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you also validate that the |
||
|
||
final String actualVersion = jsonNode.path("version").textValue(); | ||
final String actualName = jsonNode.path("name").textValue(); | ||
final String actualDescription = jsonNode.path("description").textValue(); | ||
|
||
assertThat(actualVersion).isEqualTo(TEST_APP_INFO.version); | ||
assertThat(actualName).isEqualTo(TEST_APP_INFO.name); | ||
assertThat(actualDescription).isEqualTo(TEST_APP_INFO.description); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.