Skip to content
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
5 changes: 0 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,6 @@
<artifactId>base-foundation</artifactId>
<version>${base.version}</version>
</dependency>
<dependency>
<groupId>build.base</groupId>
<artifactId>base-logging</artifactId>
<version>${base.version}</version>
</dependency>
<dependency>
<groupId>build.base</groupId>
<artifactId>base-network</artifactId>
Expand Down
5 changes: 0 additions & 5 deletions serve-application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,6 @@
<artifactId>base-configuration</artifactId>
</dependency>

<dependency>
<groupId>build.base</groupId>
<artifactId>base-logging</artifactId>
</dependency>

<dependency>
<groupId>build.base</groupId>
<artifactId>base-network</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*-
* #%L
* Serve Application
* %%
* Copyright (C) 2026 Reed von Redwitz
* %%
* 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.
* #L%
*/
package build.serve.application;

import build.base.telemetry.TelemetryRecorder;
import build.base.telemetry.foundation.PrintStreamTelemetryRecorder;

import java.net.URI;

/**
* Shared telemetry defaults for the {@code build.serve.application} module.
*
* @author reed.vonredwitz
* @since Aug-2026
*/
final class ApplicationTelemetry {

/**
* The {@link TelemetryRecorder} used when a caller does not supply one — records to
* {@code System.out}/{@code System.err}.
*/
static final TelemetryRecorder DEFAULT_RECORDER =
PrintStreamTelemetryRecorder.of(URI.create("serve://application"), System.out, System.err);

/**
* Prevent instantiation.
*/
private ApplicationTelemetry() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,15 @@
package build.serve.application;

import build.base.configuration.Configuration;
import build.base.logging.Logger;
import build.base.network.option.Port;
import build.base.telemetry.TelemetryRecorder;
import build.base.telemetry.foundation.PrintStreamTelemetryRecorder;
import build.codemodel.dependency.injection.ConfigurationResolver;
import build.codemodel.dependency.injection.Context;
import build.codemodel.dependency.injection.InjectionFramework;
import build.serve.foundation.option.ListenAddress;

import java.io.IOException;
import java.net.URI;
import java.util.Objects;

/**
* A simple launcher for {@link ServerApplication} instances.
Expand All @@ -42,11 +40,6 @@
*/
public final class Launcher {

/**
* The {@link Logger} for this class.
*/
private static final Logger LOGGER = Logger.get(Launcher.class);

/**
* Prevent instantiation.
*/
Expand All @@ -65,6 +58,21 @@ public static ServerApplication.Implementation launch(final ServerApplication.Im
return launch(server, ListenAddress.DEFAULT, listenPort);
}

/**
* Launches a {@link ServerApplication.Implementation} with the specified options.
*
* @param server the {@link ServerApplication.Implementation} to launch
* @param listenPort the {@link Port} to bind to
* @param telemetryRecorder the {@link TelemetryRecorder} to bind into the injection context and record
* server lifecycle events with
* @return the started server
*/
public static ServerApplication.Implementation launch(final ServerApplication.Implementation server,
final Port listenPort,
final TelemetryRecorder telemetryRecorder) {
return launch(server, ListenAddress.DEFAULT, listenPort, telemetryRecorder);
}

/**
* Launches a {@link ServerApplication.Implementation} with the specified options.
*
Expand All @@ -76,6 +84,25 @@ public static ServerApplication.Implementation launch(final ServerApplication.Im
public static ServerApplication.Implementation launch(final ServerApplication.Implementation server,
final ListenAddress listenAddress,
final Port listenPort) {
return launch(server, listenAddress, listenPort, ApplicationTelemetry.DEFAULT_RECORDER);
}

/**
* Launches a {@link ServerApplication.Implementation} with the specified options.
*
* @param server the {@link ServerApplication.Implementation} to launch
* @param listenAddress the {@link ListenAddress} to bind to
* @param listenPort the {@link Port} to bind to
* @param telemetryRecorder the {@link TelemetryRecorder} to bind into the injection context and record
* server lifecycle events with
* @return the started server
*/
public static ServerApplication.Implementation launch(final ServerApplication.Implementation server,
final ListenAddress listenAddress,
final Port listenPort,
final TelemetryRecorder telemetryRecorder) {
Objects.requireNonNull(telemetryRecorder, "telemetryRecorder");

try {
// Build configuration from the provided options
final var configuration = Configuration.of(listenAddress, listenPort);
Expand All @@ -90,10 +117,9 @@ public static ServerApplication.Implementation launch(final ServerApplication.Im
context.bind(Configuration.class).to(configuration);
context.bind(ServerApplication.class).to(server);

// Bind a default TelemetryRecorder
final var telemetryRecorder = PrintStreamTelemetryRecorder.of(
URI.create("serve://application"), System.out, System.err);
// Bind the TelemetryRecorder and use it for server lifecycle events
context.bind(TelemetryRecorder.class).to(telemetryRecorder);
server.recorder(telemetryRecorder);

// Add the ConfigurationResolver so Options are injectable
context.addResolver(ConfigurationResolver.of(configuration));
Expand All @@ -108,7 +134,7 @@ public static ServerApplication.Implementation launch(final ServerApplication.Im

return server;
} catch (final IOException e) {
LOGGER.fatal(e, "Failed to launch ServerApplication");
telemetryRecorder.fatal(e, "Failed to launch ServerApplication");

throw new RuntimeException("Failed to launch ServerApplication", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
*/
package build.serve.application;

import build.base.logging.Logger;
import build.base.network.option.Port;
import build.base.telemetry.TelemetryRecorder;
import build.codemodel.dependency.injection.Context;
import build.serve.foundation.Handler;
import build.serve.foundation.error.DefaultErrorHandler;
Expand All @@ -37,6 +37,7 @@
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
Expand Down Expand Up @@ -80,9 +81,11 @@ abstract class Implementation
implements ServerApplication {

/**
* The {@link Logger} for this class.
* The {@link TelemetryRecorder} for this server's lifecycle events. Defaults to
* {@link ApplicationTelemetry#DEFAULT_RECORDER}; {@link Launcher} replaces it with the
* recorder bound into the injection {@link Context}.
*/
private static final Logger LOGGER = Logger.get(Implementation.class);
private TelemetryRecorder recorder = ApplicationTelemetry.DEFAULT_RECORDER;

/**
* The {@link Router} for this server.
Expand Down Expand Up @@ -152,6 +155,15 @@ void context(final Context context) {
this.context = context;
}

/**
* Sets the {@link TelemetryRecorder} used for this server's lifecycle events.
*
* @param recorder the {@link TelemetryRecorder}
*/
void recorder(final TelemetryRecorder recorder) {
this.recorder = Objects.requireNonNull(recorder, "recorder must not be null");
}

/**
* Configures and returns the {@link Router} for this server.
* <p>
Expand Down Expand Up @@ -219,13 +231,13 @@ public void start(final ListenAddress listenAddress,

final var address = new InetSocketAddress(listenAddress.value(), listenPort.get());

this.transport = new HttpTransport(address, 0, applySecurityHeaders(router), errorHandler());
this.transport = new HttpTransport(address, 0, applySecurityHeaders(router), errorHandler(), recorder);
this.transport.start();

this.shutdownHook = new Thread(this::stop, "serve-shutdown");
Runtime.getRuntime().addShutdownHook(shutdownHook);

LOGGER.info("ServerApplication started on " + transport.address());
recorder.info("ServerApplication started on " + transport.address());

startFuture.complete(this);
}
Expand Down Expand Up @@ -263,13 +275,13 @@ public void start(final ListenAddress listenAddress,
final var address = new InetSocketAddress(listenAddress.value(), listenPort.get());

this.transport = HttpTransport.https(address, 0, applySecurityHeaders(router), errorHandler(),
tls.sslContext());
tls.sslContext(), recorder);
this.transport.start();

this.shutdownHook = new Thread(this::stop, "serve-shutdown");
Runtime.getRuntime().addShutdownHook(shutdownHook);

LOGGER.info("ServerApplication started on " + transport.address() + " (HTTPS)");
recorder.info("ServerApplication started on " + transport.address() + " (HTTPS)");

startFuture.complete(this);
}
Expand All @@ -295,7 +307,7 @@ public void stop() {

exitFuture.complete(this);

LOGGER.info("ServerApplication stopped");
recorder.info("ServerApplication stopped");
}
}

Expand Down
1 change: 0 additions & 1 deletion serve-application/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
requires transitive build.codemodel.dependency.injection;
requires transitive jakarta.inject;

requires build.base.logging;
requires build.base.telemetry;
requires build.base.telemetry.foundation;

Expand Down
7 changes: 6 additions & 1 deletion serve-auth/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@

<dependency>
<groupId>build.base</groupId>
<artifactId>base-logging</artifactId>
<artifactId>base-telemetry</artifactId>
</dependency>

<dependency>
<groupId>build.base</groupId>
<artifactId>base-telemetry-foundation</artifactId>
</dependency>

<!-- Test Dependencies -->
Expand Down
30 changes: 27 additions & 3 deletions serve-auth/src/main/java/build/serve/auth/ApiKeyStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
*/
package build.serve.auth;

import build.base.logging.Logger;
import build.base.telemetry.TelemetryRecorder;
import build.base.telemetry.foundation.PrintStreamTelemetryRecorder;
import build.serve.foundation.Request;

import java.net.URI;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
Expand All @@ -38,7 +40,8 @@
*/
public final class ApiKeyStrategy implements AuthStrategy {

private static final Logger LOGGER = Logger.get(ApiKeyStrategy.class);
private static final TelemetryRecorder DEFAULT_RECORDER =
PrintStreamTelemetryRecorder.of(URI.create("serve://auth"), System.out, System.err);

private final Function<Request, Optional<String>> extractor;
private final Function<String, Optional<Principal>> validator;
Expand Down Expand Up @@ -78,9 +81,30 @@ public static ApiKeyStrategy fromHeader(final String headerName,
*/
public static ApiKeyStrategy fromQueryParam(final String paramName,
final Function<String, Optional<Principal>> validator) {
return fromQueryParam(paramName, validator, DEFAULT_RECORDER);
}

/**
* Creates an {@link ApiKeyStrategy} that reads the key from a query parameter.
* <p>
* <strong>Prefer {@link #fromHeader(String, Function)} where possible.</strong> A key carried
* in the URL ends up in server access logs, proxy logs, and browser history, and is exposed
* by anything that logs or shares the request URL — none of which apply to a header value.
* Use this only when the caller genuinely cannot set a header (e.g. a webhook provider that
* only supports query-string authentication).
*
* @param paramName the query parameter name (e.g., {@code "api_key"})
* @param validator a function mapping the key to a {@link Principal}, or empty if invalid
* @param recorder the {@link TelemetryRecorder} to record the query-string usage warning with
* @return a new {@link ApiKeyStrategy}
*/
public static ApiKeyStrategy fromQueryParam(final String paramName,
final Function<String, Optional<Principal>> validator,
final TelemetryRecorder recorder) {
Objects.requireNonNull(paramName, "paramName");
Objects.requireNonNull(validator, "validator");
LOGGER.warn("ApiKeyStrategy.fromQueryParam(\"" + paramName + "\") carries the API key in the URL —"
Objects.requireNonNull(recorder, "recorder");
recorder.warn("ApiKeyStrategy.fromQueryParam(\"" + paramName + "\") carries the API key in the URL —"
+ " it will appear in access logs, proxy logs, and browser history. Prefer fromHeader(...)"
+ " unless the caller cannot set a header.");
return new ApiKeyStrategy(req -> req.queryParam(paramName), validator);
Expand Down
3 changes: 2 additions & 1 deletion serve-auth/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
*/
module build.serve.auth {
requires transitive build.serve.foundation;
requires build.base.logging;
requires build.base.telemetry;
requires build.base.telemetry.foundation;

exports build.serve.auth;
}
7 changes: 6 additions & 1 deletion serve-cors/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@

<dependency>
<groupId>build.base</groupId>
<artifactId>base-logging</artifactId>
<artifactId>base-telemetry</artifactId>
</dependency>

<dependency>
<groupId>build.base</groupId>
<artifactId>base-telemetry-foundation</artifactId>
</dependency>

<!-- Test Dependencies -->
Expand Down
Loading