Skip to content

Commit

Permalink
Add Ergonomic Control of Logging Settings
Browse files Browse the repository at this point in the history
    Add ergonomic control of logging settings via new WebAppSettings
    fields. Web apps now have an environment concept that describes
    where the app is being run. Logging can be configured separately
    for each environment. Multiple loggers can also be added per
    environment, each with their own log levels and format.

    We also now ensure that fetched settings are read-only. Unprotected 
    writes to settings does not sound like a good idea in a 
    multi-threaded application.

    Correct a bug in path/handler validation error messaging.
    
    Remove half-baked templating flexibility. The flag for adding
    template folders caused issues with picking up template changes
    during compilation.

    Add direct access to vibe.d settings in WebAppSetting. We don't
    want to hide that we're running on vibe.d so why unnecessarily wrap
    its settings?
  • Loading branch information
kyleingraham committed Jul 27, 2023
1 parent 7a729d8 commit 1b182a1
Show file tree
Hide file tree
Showing 7 changed files with 483 additions and 36 deletions.
42 changes: 34 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,31 @@ int main()
}
```

## Environment
You can control the behaviour of your web app based on the environment it's running in via the `WebAppSettings.environment`
setting. Potcake is configured out of the box to react to `WebAppEnvironment` values but any string value can be used.

## Logging
Potcake allows for setting logging settings keyed on environment. This allows for:
- varying logging between development and production
- varying log levels and formats between loggers in an environment

Logging settings can be set via `WebAppSettings.logging`. Configured loggers can be any subclass of vibe.d's `Logger`.

For example:

```d
auto settings = new WebAppSettings;
settings.logging = [
WebAppEnvironment.development: [
LoggerSetting(LogLevel.info, new VibedStdoutLogger, FileLogger.Format.threadTime),
],
WebAppEnvironment.production: [
LoggerSetting(LogLevel.warn, new FileLogger("application.log"), FileLogger.Format.threadTime),
],
];
```

## FAQ
Q - Why the name Potcake?

Expand All @@ -295,14 +320,15 @@ and dependable. All great aspirational qualities for a web framework.


## Roadmap
- Potcake libraries that can provide templates, static files, and routes on import. This will need:
- [ ] DIET template loading from a library
- [ ] Static file collection from a library
- [ ] Route inclusion from a library
- Middleware
- [x] Middleware system
- [X] Middleware system
- Convenience middleware
- [x] Static files
- [X] Static files
- [ ] CSRF
- [ ] CORS
- [x] Post-routing middleware
- [ ] Health-check endpoint middleware
- Matching the API for vibe.d's `URLRouter`
- [ ] Set of valid handler signatures
- [ ] Handler registration functions e.g. `post`
- [ ] Per-router path prefixes
- [X] Post-routing middleware
- [ ] Health-check endpoint middleware
2 changes: 0 additions & 2 deletions dub.sdl
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,4 @@ subPackage {

sourcePaths "web"
importPaths "web"

dflags "-J \".\"" // Allows the user to specify template locations in code
}
20 changes: 10 additions & 10 deletions examples/collect_static_files/source/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ import potcake.web;

int main(string[] args)
{
auto settings = new WebAppSettings;
settings.staticDirectories = ["static_a", "static_b"];
settings.rootStaticDirectory = "staticroot";
settings.staticRoutePath = "/static/";

auto routes = [
route("/", &handler),
route("/diet/<int:num>/", &dietHandler),
];

auto settings = new WebAppSettings;
settings.staticDirectories = ["static_a", "static_b"];
settings.rootStaticDirectory = "staticroot";
settings.staticRoutePath = "/static/";
settings.rootRouteConfig = routes;

auto webApp = new WebApp(settings);
webApp
.addRoutes(routes)
.serveStaticFiles();

return webApp.run(args); // For detection of the --collectstatic flag.
return webApp
.serveStaticFiles()
.run(args); // For detection of the --collectstatic flag.
}

void handler(HTTPServerRequest req, HTTPServerResponse res)
Expand All @@ -44,5 +44,5 @@ void handler(HTTPServerRequest req, HTTPServerResponse res)

void dietHandler(HTTPServerRequest req, HTTPServerResponse res, int num)
{
res.render!("templates/test.dt", num);
res.render!("test.dt", num);
}
File renamed without changes.
4 changes: 2 additions & 2 deletions http/potcake/http/router.d
Original file line number Diff line number Diff line change
Expand Up @@ -318,10 +318,10 @@ alias RouteName = string;
assert(
parsedPath.pathCaptureGroups.length == nonReqResParamCount,
format(
"Path (%s) handler's non-request/response parameter count (%s) does not match path parameter count (%s)",
"Path's (%s) handler's non-request/response parameter count (%s) does not match the path's parameter count (%s). ",
path,
nonReqResParamCount,
parsedPath.pathCaptureGroups.length,
nonReqResParamCount
)
);

Expand Down
176 changes: 163 additions & 13 deletions web/potcake/web/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,144 @@ module potcake.web.app;

import potcake.http.router : Router;
import std.functional : memoize;
import std.process : processEnv = environment;
import std.variant : Variant;
import vibe.core.log : Logger;
import vibe.http.server : HTTPServerSettings;

public import vibe.core.log : FileLogger, LogLevel;
public import vibe.http.server : HTTPServerRequest, HTTPServerRequestDelegate, HTTPServerResponse, render;
public import potcake.http.router : MiddlewareFunction, MiddlewareDelegate, pathConverter, PathConverterSpec;

alias SettingsDelegate = Variant delegate(string setting);

/**
* Fetch a setting from the currently running web app. Fetched settings are read-only.
*
* Initialized by WebApp at instantiation.
*/
alias SettingsDelegate = const(Variant) delegate(string setting);
///
SettingsDelegate getSetting;

alias RouteAdder = void delegate(WebApp webApp);
alias RouteConfig = RouteAdder[];

/**
* Core settings for Potcake web apps. Provides reasonable defaults.
*
* Subclass to make custom settings available to your app.
*/
class WebAppSettings
{
string[] allowedHosts = ["localhost", "127.0.0.1"];
ushort port = 9000;
/**
* The routes that your web app makes available.
*
* Eliminates the need to add routes manually. Intended to be given an array of calls to [route].
*
* Example:
* ---
* auto settings = new WebAppSettings();
* settings.rootRouteConfig = [
* route("/", &index),
* route("/hello/", &hello),
* ];
* ---
*/
RouteConfig rootRouteConfig = [];

/// Directories containing static files for collection via the '--collectstatic' utility.
string[] staticDirectories = [];

/**
* Directory from which static files are served and also where they are collected into.
*
* Static files are collected here by the '--collectstatic' utility. Relied on by [WebApp.serveStaticFiles()].
*/
string rootStaticDirectory;

/**
* The route prefix at which to serve static files e.g. "/static/".
*
* Relied on by [WebApp.serveStaticFiles()].
*/
string staticRoutePath;

/// Direct access to the settings controlling the underlying vibe.d server.
HTTPServerSettings vibed;

/// Called to set vibe.d-related defaults.
void initializeVibedSettings()
{
vibed = new HTTPServerSettings;
vibed.bindAddresses = ["localhost", "127.0.0.1"];
vibed.port = 9000;
}

/**
* Logging configuration for your web app keyed on environment.
*
* This setting allows for:
* - varying logging between development and production.
* - varying log levels and formats between loggers in an environment.
*
* vibe.d provides a well-configured console logger. To use it supply [VibedStdoutLogger] via [LoggerSetting].
*
* See [initializeLoggingSettings] for a usage example.
*/
LoggerSetting[][string] logging;

/// Called to set logging-related defaults.
void initializeLoggingSettings()
{
logging = [
WebAppEnvironment.development: [
LoggerSetting(LogLevel.info, new VibedStdoutLogger(), FileLogger.Format.threadTime),
],
WebAppEnvironment.production: [],
];
}

/// Controls whether vibe.d server access logs should be displayed in the 'development' environment.
bool logAccessInDevelopment = true;

/**
* Signals the evironment that your app is running in.
*
* Can be set with the POTCAKE_ENVIRONMENT environment variable.
*
* Potcake is pre-configured for [WebAppEnvironment] values but any string can be used.
*/
string environment = WebAppEnvironment.development;

/// Sets the web app environment via the process environment.
void initializeEnvironment()
{
environment = processEnv.get("POTCAKE_ENVIRONMENT", WebAppEnvironment.development);
}

this()
{
initializeEnvironment();
initializeVibedSettings();
initializeLoggingSettings();
}
}

/// Environment designators that Potcake supports out of the box.
enum WebAppEnvironment : string
{
development = "development",
production = "production",
}

/// A logger that signals Potcake to use vibe.d's built-in console logger.
final class VibedStdoutLogger : Logger {}

/// Supply to [WebAppSettings.logging] to add a logger for an environment.
struct LoggerSetting
{
LogLevel logLevel;
Logger logger;
FileLogger.Format format = FileLogger.Format.plain; /// Only read for FileLogger loggers.
}

RouteAdder route(Handler)(string path, Handler handler, string name=null)
Expand All @@ -45,7 +163,7 @@ string staticPathImpl(string relativePath)
{
import urllibparse : urlJoin;

auto basePath = (() @trusted => getSetting("staticRoutePath").get!string)();
auto basePath = (() @trusted => getSetting("staticRoutePath").get!string)()[];
assert(0 < basePath.length, "The 'staticRoutePath' setting must be set to generate static paths.");

if (basePath[$-1] != urlSeparator)
Expand Down Expand Up @@ -93,10 +211,12 @@ final class WebApp
router = new Router;
router.addPathConverters(pathConverters);

initializeSettings(webAppSettings);
initializeGetSetting(webAppSettings);

initializeLogging();
}

private void initializeSettings(T)(T webAppSettings)
private void initializeGetSetting(T)(T webAppSettings)
if (is(T : WebAppSettings))
{
getSetting = (setting) {
Expand All @@ -122,6 +242,37 @@ final class WebApp
};
}

private void initializeLogging()
{
import vibe.core.log : setLogLevel, registerLogger, setLogFormat;

setLogLevel(LogLevel.none);

auto loggerSettings = webAppSettings.logging[webAppSettings.environment];

foreach (loggerSetting; loggerSettings)
{
if (typeid(loggerSetting.logger) == typeid(VibedStdoutLogger))
{
setLogLevel(loggerSetting.logLevel);
setLogFormat(loggerSetting.format, loggerSetting.format);
continue;
}

if (auto logger = cast(FileLogger) loggerSetting.logger)
{
logger.format = loggerSetting.format;
}

loggerSetting.logger.minLevel = loggerSetting.logLevel;
auto register = (() @trusted => registerLogger(cast(shared)loggerSetting.logger));
register();
}

if (webAppSettings.environment == WebAppEnvironment.development && webAppSettings.logAccessInDevelopment)
webAppSettings.vibed.accessLogToConsole = true;
}

WebApp addMiddleware(MiddlewareFunction middleware)
{
import std.functional : toDelegate;
Expand All @@ -144,7 +295,7 @@ final class WebApp

WebApp addRoutes(RouteConfig routeConfig)
{
foreach(addRouteTo; routeConfig)
foreach (addRouteTo; routeConfig)
addRouteTo(this);

return this;
Expand Down Expand Up @@ -183,10 +334,7 @@ final class WebApp
{
import vibe.http.server : listenHTTP;
import vibe.core.core : runApplication;

vibeSettings = new HTTPServerSettings;
vibeSettings.bindAddresses = webAppSettings.allowedHosts;
vibeSettings.port = webAppSettings.port;
import vibe.core.log : logInfo;

addRoutes(webAppSettings.rootRouteConfig);

Expand All @@ -196,13 +344,15 @@ final class WebApp
if (returnCode != -1)
return returnCode;

auto listener = listenHTTP(vibeSettings, router);
auto listener = listenHTTP(webAppSettings.vibed, router);

scope (exit)
{
listener.stopListening();
}

logInfo("Running in '%s' environment.", webAppSettings.environment);

return runApplication();
}

Expand Down
Loading

0 comments on commit 1b182a1

Please sign in to comment.