Skip to content
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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto log for dev services in containers #43402

Merged
merged 1 commit into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 57 additions & 4 deletions docs/src/main/asciidoc/dev-ui.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Extensions can:

- <<add-links-to-an-extension-card,Add links to an extension card>>
- <<add-full-pages,Add full custom pages>>
- <<add-a-log-file,Add a log stream>>
- <<add-a-footer-tab,Add a footer tab>>
- <<add-a-section-menu,Add a section menu>>
- <<custom-cards,Create a custom card>>

Expand Down Expand Up @@ -822,7 +822,7 @@ https://github.com/quarkusio/quarkus/blob/main/extensions/vertx-http/dev-ui-reso
====== Log

The log controller is used to add control buttons to a (footer) log.
See <<Add a log file>>.
See <<Add a footer tab>>.

image::dev-ui-log-control-v2.png[alt=Dev UI Log control,role="center"]

Expand Down Expand Up @@ -1145,9 +1145,9 @@ See the https://github.com/quarkusio/quarkus/tree/main/extensions/vertx-http/dev
The state in Dev UI uses https://github.com/gitaarik/lit-state[LitState]. You can read more about it in their https://gitaarik.github.io/lit-state/build/[documentation].


== Add a log file
== Add a footer tab

Apart from adding a card and a page, extensions can add a log to the footer. This is useful for logging things that are happening continuously. A page will lose connection to the backend when navigating away from that page, and a log in the footer will be permanently connected.
Apart from adding a card and a page, extensions can add a tab to the footer. This is useful for things that are happening continuously. A page will lose connection to the backend when navigating away from that page, and a log in the footer will be permanently connected.

Adding something to the footer works exactly like adding a Card, except you use a `FooterPageBuildItem` rather than a `CardPageBuildItem`.

Expand Down Expand Up @@ -1179,6 +1179,59 @@ export class QwcJokesLog extends LitElement {

https://github.com/phillip-kruger/quarkus-jokes/blob/main/deployment/src/main/resources/dev-ui/qwc-jokes-log.js[Example code]

=== Add a log to the footer

There is an easy way to add a log stream to the footer, without having to create the above mentioned footer.
If you just want to stream a log to a tab you can just produce a `FooterLogBuildItem`. This way you only provide a name and a `Flow.Publisher<String>` for your log.

Here is an example from the Dev Services deployment module:

[source,java]
----
@BuildStep(onlyIf = { IsDevelopment.class })
public List<DevServiceDescriptionBuildItem> config(
// ...
BuildProducer<FooterLogBuildItem> footerLogProducer){

// ...

// Dev UI Log stream
for (DevServiceDescriptionBuildItem service : serviceDescriptions) {
if (service.getContainerInfo() != null) {
footerLogProducer.produce(new FooterLogBuildItem(service.getName(), () -> {
return createLogPublisher(service.getContainerInfo().getId());
}));
}
}
}

// ...

private Flow.Publisher<String> createLogPublisher(String containerId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we explan what this is doing? I have 0️⃣ understanding just by eye-balling it for the first time

try (FrameConsumerResultCallback resultCallback = new FrameConsumerResultCallback()) {
SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
resultCallback.addConsumer(OutputFrame.OutputType.STDERR,
frame -> publisher.submit(frame.getUtf8String()));
resultCallback.addConsumer(OutputFrame.OutputType.STDOUT,
frame -> publisher.submit(frame.getUtf8String()));
LogContainerCmd logCmd = DockerClientFactory.lazyClient()
.logContainerCmd(containerId)
.withFollowStream(true)
.withTailAll()
.withStdErr(true)
.withStdOut(true);
logCmd.exec(resultCallback);

return publisher;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

----



== Add a section menu

This allows an extension to link a page directly in the section Menu.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Flow;
import java.util.concurrent.SubmissionPublisher;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.output.FrameConsumerResultCallback;
import org.testcontainers.containers.output.OutputFrame;

import com.github.dockerjava.api.command.LogContainerCmd;
import com.github.dockerjava.api.model.Container;
import com.github.dockerjava.api.model.ContainerNetwork;
import com.github.dockerjava.api.model.ContainerNetworkSettings;
Expand All @@ -45,6 +50,7 @@
import io.quarkus.deployment.util.ContainerRuntimeUtil;
import io.quarkus.deployment.util.ContainerRuntimeUtil.ContainerRuntime;
import io.quarkus.dev.spi.DevModeType;
import io.quarkus.devui.spi.buildtime.FooterLogBuildItem;

public class DevServicesProcessor {

Expand All @@ -58,6 +64,7 @@ public class DevServicesProcessor {
public List<DevServiceDescriptionBuildItem> config(
DockerStatusBuildItem dockerStatusBuildItem,
BuildProducer<ConsoleCommandBuildItem> commandBuildItemBuildProducer,
BuildProducer<FooterLogBuildItem> footerLogProducer,
LaunchModeBuildItem launchModeBuildItem,
Optional<DevServicesLauncherConfigResultBuildItem> devServicesLauncherConfig,
List<DevServicesResultBuildItem> devServicesResults) {
Expand All @@ -80,6 +87,15 @@ public List<DevServiceDescriptionBuildItem> config(
commandBuildItemBuildProducer.produce(
new ConsoleCommandBuildItem(new DevServicesCommand(serviceDescriptions)));

// Dev UI Log stream
for (DevServiceDescriptionBuildItem service : serviceDescriptions) {
if (service.getContainerInfo() != null) {
footerLogProducer.produce(new FooterLogBuildItem(service.getName(), () -> {
return createLogPublisher(service.getContainerInfo().getId());
}));
}
}

if (context == null) {
context = ConsoleStateManager.INSTANCE.createContext("Dev Services");
}
Expand All @@ -104,6 +120,27 @@ public List<DevServiceDescriptionBuildItem> config(
return serviceDescriptions;
}

private Flow.Publisher<String> createLogPublisher(String containerId) {
try (FrameConsumerResultCallback resultCallback = new FrameConsumerResultCallback()) {
SubmissionPublisher<String> publisher = new SubmissionPublisher<>();
resultCallback.addConsumer(OutputFrame.OutputType.STDERR,
frame -> publisher.submit(frame.getUtf8String()));
resultCallback.addConsumer(OutputFrame.OutputType.STDOUT,
frame -> publisher.submit(frame.getUtf8String()));
LogContainerCmd logCmd = DockerClientFactory.lazyClient()
.logContainerCmd(containerId)
.withFollowStream(true)
.withTailAll()
.withStdErr(true)
.withStdOut(true);
logCmd.exec(resultCallback);

return publisher;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

private List<DevServiceDescriptionBuildItem> buildServiceDescriptions(
DockerStatusBuildItem dockerStatusBuildItem,
List<DevServicesResultBuildItem> devServicesResults,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ InternalImportMapBuildItem createKnownInternalImportMap(NonApplicationRootPathBu
internalImportMapBuildItem.add("qwc-hot-reload-element", contextRoot + "qwc/qwc-hot-reload-element.js");
internalImportMapBuildItem.add("qwc-abstract-log-element", contextRoot + "qwc/qwc-abstract-log-element.js");
internalImportMapBuildItem.add("qwc-server-log", contextRoot + "qwc/qwc-server-log.js");
internalImportMapBuildItem.add("qwc-footer-log", contextRoot + "qwc/qwc-footer-log.js");
internalImportMapBuildItem.add("qwc-extension-link", contextRoot + "qwc/qwc-extension-link.js");
// Quarkus UI
internalImportMapBuildItem.add("qui-ide-link", contextRoot + "qui/qui-ide-link.js");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@
import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper;
import io.quarkus.devui.spi.DevUIContent;
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem;
import io.quarkus.devui.spi.buildtime.FooterLogBuildItem;
import io.quarkus.devui.spi.buildtime.StaticContentBuildItem;
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.devui.spi.page.FooterPageBuildItem;
import io.quarkus.devui.spi.page.MenuPageBuildItem;
import io.quarkus.devui.spi.page.Page;
import io.quarkus.devui.spi.page.PageBuilder;
import io.quarkus.devui.spi.page.QuteDataPageBuilder;
import io.quarkus.devui.spi.page.WebComponentPageBuilder;
import io.quarkus.maven.dependency.GACT;
import io.quarkus.maven.dependency.GACTV;
import io.quarkus.qute.Qute;
Expand All @@ -87,7 +90,7 @@
* This also find all jsonrpc methods and make them available in the jsonRPC Router
*/
public class DevUIProcessor {

private static final String FOOTER_LOG_NAMESPACE = "devui-footer-log";
private static final String DEVUI = "dev-ui";
private static final String SLASH = "/";
private static final String DOT = ".";
Expand Down Expand Up @@ -418,6 +421,54 @@ void createJsonRpcRouter(DevUIRecorder recorder,
}
}

/**
* This build all the pages for dev ui, based on the extension included
*/
@BuildStep(onlyIf = IsDevelopment.class)
void processFooterLogs(BuildProducer<BuildTimeActionBuildItem> buildTimeActionProducer,
BuildProducer<FooterPageBuildItem> footerPageProducer,
List<FooterLogBuildItem> footerLogBuildItems) {

List<BuildTimeActionBuildItem> devServiceLogs = new ArrayList<>();
List<FooterPageBuildItem> footers = new ArrayList<>();

for (FooterLogBuildItem footerLogBuildItem : footerLogBuildItems) {
// Create the Json-RPC service that will stream the log
String name = footerLogBuildItem.getName().replaceAll(" ", "");

BuildTimeActionBuildItem devServiceLogActions = new BuildTimeActionBuildItem(FOOTER_LOG_NAMESPACE);
devServiceLogActions.addSubscription(name + "Log", ignored -> {
try {
return footerLogBuildItem.getPublisher();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
devServiceLogs.add(devServiceLogActions);

// Create the Footer in the Dev UI
WebComponentPageBuilder log = Page.webComponentPageBuilder().internal()
.namespace(FOOTER_LOG_NAMESPACE)
.icon("font-awesome-regular:file-lines")
.title(capitalizeFirstLetter(footerLogBuildItem.getName()))
.metadata("jsonRpcMethodName", footerLogBuildItem.getName() + "Log")
.componentLink("qwc-footer-log.js");

FooterPageBuildItem footerPageBuildItem = new FooterPageBuildItem(FOOTER_LOG_NAMESPACE, log);
footers.add(footerPageBuildItem);
}

buildTimeActionProducer.produce(devServiceLogs);
footerPageProducer.produce(footers);
}

private String capitalizeFirstLetter(String input) {
if (input == null || input.isEmpty()) {
return input;
}
return input.substring(0, 1).toUpperCase() + input.substring(1);
}

/**
* This build all the pages for dev ui, based on the extension included
*/
Expand Down Expand Up @@ -449,7 +500,7 @@ void getAllExtensions(List<CardPageBuildItem> cardPageBuildItems,
// Now go through all extensions and check them for active components
Map<String, CardPageBuildItem> cardPagesMap = getCardPagesMap(curateOutcomeBuildItem, cardPageBuildItems);
Map<String, MenuPageBuildItem> menuPagesMap = getMenuPagesMap(curateOutcomeBuildItem, menuPageBuildItems);
Map<String, FooterPageBuildItem> footerPagesMap = getFooterPagesMap(curateOutcomeBuildItem, footerPageBuildItems);
Map<String, List<FooterPageBuildItem>> footerPagesMap = getFooterPagesMap(curateOutcomeBuildItem, footerPageBuildItems);
try {
final Yaml yaml = new Yaml();
List<Extension> activeExtensions = new ArrayList<>();
Expand Down Expand Up @@ -559,18 +610,21 @@ void getAllExtensions(List<CardPageBuildItem> cardPageBuildItems,

// Tabs in the footer
if (footerPagesMap.containsKey(namespace)) {
FooterPageBuildItem footerPageBuildItem = footerPagesMap.get(namespace);
List<PageBuilder> footerPageBuilders = footerPageBuildItem.getPages();

Map<String, Object> buildTimeData = footerPageBuildItem.getBuildTimeData();
for (PageBuilder pageBuilder : footerPageBuilders) {
Page page = buildFinalPage(pageBuilder, extension, buildTimeData);
extension.addFooterPage(page);
List<FooterPageBuildItem> fbis = footerPagesMap.get(namespace);
for (FooterPageBuildItem footerPageBuildItem : fbis) {
List<PageBuilder> footerPageBuilders = footerPageBuildItem.getPages();

Map<String, Object> buildTimeData = footerPageBuildItem.getBuildTimeData();
for (PageBuilder pageBuilder : footerPageBuilders) {
Page page = buildFinalPage(pageBuilder, extension, buildTimeData);
extension.addFooterPage(page);
}
// Also make sure the static resources for that static resource is available
produceResources(artifactId, webJarBuildProducer,
devUIWebJarProducer);
footerTabExtensions.add(extension);
}
// Also make sure the static resources for that static resource is available
produceResources(artifactId, webJarBuildProducer,
devUIWebJarProducer);
footerTabExtensions.add(extension);
}

}
Expand All @@ -582,6 +636,33 @@ void getAllExtensions(List<CardPageBuildItem> cardPageBuildItems,
log.error("Failed to process extension descriptor " + p.toUri(), e);
}
});

// Also add footers for extensions that might not have a runtime
if (!footerPagesMap.isEmpty()) {
for (Map.Entry<String, List<FooterPageBuildItem>> footer : footerPagesMap.entrySet()) {
List<FooterPageBuildItem> fbis = footer.getValue();
for (FooterPageBuildItem footerPageBuildItem : fbis) {
if (footerPageBuildItem.isInternal()) {
Extension deploymentOnlyExtension = new Extension();
deploymentOnlyExtension.setName(footer.getKey());
deploymentOnlyExtension.setNamespace(FOOTER_LOG_NAMESPACE);

List<PageBuilder> footerPageBuilders = footerPageBuildItem.getPages();

for (PageBuilder pageBuilder : footerPageBuilders) {
pageBuilder.namespace(deploymentOnlyExtension.getNamespace());
pageBuilder.extension(deploymentOnlyExtension.getName());
pageBuilder.internal();
Page page = pageBuilder.build();
deploymentOnlyExtension.addFooterPage(page);
}

footerTabExtensions.add(deploymentOnlyExtension);
}
}
}
}

extensionsProducer.produce(
new ExtensionsBuildItem(activeExtensions, inactiveExtensions, sectionMenuExtensions, footerTabExtensions));
} catch (IOException ex) {
Expand Down Expand Up @@ -754,11 +835,19 @@ private Map<String, MenuPageBuildItem> getMenuPagesMap(CurateOutcomeBuildItem cu
return m;
}

private Map<String, FooterPageBuildItem> getFooterPagesMap(CurateOutcomeBuildItem curateOutcomeBuildItem,
private Map<String, List<FooterPageBuildItem>> getFooterPagesMap(CurateOutcomeBuildItem curateOutcomeBuildItem,
List<FooterPageBuildItem> pages) {
Map<String, FooterPageBuildItem> m = new HashMap<>();
Map<String, List<FooterPageBuildItem>> m = new HashMap<>();
for (FooterPageBuildItem pageBuildItem : pages) {
m.put(pageBuildItem.getExtensionPathName(curateOutcomeBuildItem), pageBuildItem);

String key = pageBuildItem.getExtensionPathName(curateOutcomeBuildItem);
if (m.containsKey(key)) {
m.get(key).add(pageBuildItem);
} else {
List<FooterPageBuildItem> fbi = new ArrayList<>();
fbi.add(pageBuildItem);
m.put(key, fbi);
}
}
return m;
}
Expand Down
Loading
Loading