Skip to content
Open
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
115 changes: 48 additions & 67 deletions Guide/logging.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -51,31 +51,32 @@ import qualified IHP.Log as Log
import IHP.Log.Types
```

Using the [`newLogger`](https://ihp.digitallyinduced.com/api-docs/IHP-Log-Types.html#v:newLogger) function, create a logger with the desired options. For example, here is a logger that formats
Use the [`configureLogger`](https://ihp.digitallyinduced.com/api-docs/IHP-FrameworkConfig.html#v:configureLogger) helper to set up a logger with custom options. For example, here is a logger that formats
logs with a timestamp at the `Debug` log level:

```haskell
logger <- liftIO $ newLogger def {
level = Debug,
formatter = withTimeFormatter
}
option logger
config :: ConfigBuilder
config = do
configureLogger Debug withTimeFormatter (LogStdout defaultBufSize) simpleTimeFormat'
```

The available configuration options can be found in the [`LoggerSettings`](https://ihp.digitallyinduced.com/api-docs/IHP-Log-Types.html#t:LoggerSettings) record.
The `configureLogger` function takes four arguments:

- **level** - The minimum log level (e.g. `Debug`, `Info`, `Warn`, `Error`)
- **formatter** - How to format log messages (see below)
- **destination** - Where to send logs (a `LogType'` value)
- **timeFormat** - The time format string for timestamps

You can also construct a logger directly using [`newLogger`](https://ihp.digitallyinduced.com/api-docs/IHP-Log-Types.html#v:newLogger):

```haskell
data LoggerSettings = LoggerSettings {
level :: LogLevel,
formatter :: LogFormatter,
destination :: LogDestination,
timeFormat :: TimeFormat
}
(logger, cleanup) <- liftIO $ newLogger Debug withTimeFormatter (LogStdout defaultBufSize) simpleTimeFormat'
option logger
```

#### Configuring log level

Set [`level`](https://ihp.digitallyinduced.com/api-docs/IHP-Log-Types.html#t:LoggerSettings) to one of the available constructors for the [`LogLevel`](https://ihp.digitallyinduced.com/api-docs/IHP-Log-Types.html#t:LogLevel) type:
Set the level to one of the available constructors for the [`LogLevel`](https://ihp.digitallyinduced.com/api-docs/IHP-Log-Types.html#t:LogLevel) type:

```haskell
data LogLevel
Expand Down Expand Up @@ -124,74 +125,59 @@ Which logs a message like:
#### Configuring log destination

By default, messages are logged to standard out.
IHP includes all the destinations included in `fast-logger` wrapped in a custom API.
IHP uses the `LogType'` constructors from the `fast-logger` package to configure destinations:

```haskell
data LogDestination
= None
-- | Log messages to standard output.
| Stdout BufSize
-- | Log messages to standard error.
| Stderr BufSize
-- | Log message to a file. Rotate the log file with the behavior given by 'RotateSettings'.
| File FilePath RotateSettings BufSize
-- | Send logged messages to a callback. Flush action called after every log.
| Callback (LogStr -> IO ()) (IO ())
```
- `LogStdout BufSize` - Log to standard output
- `LogStderr BufSize` - Log to standard error
- `LogFileNoRotate FilePath BufSize` - Log to a file without rotation
- `LogFile FileLogSpec BufSize` - Log to a file with size-based rotation
- `LogFileTimedRotate TimedFileLogSpec BufSize` - Log to a file with time-based rotation
- `LogCallback (LogStr -> IO ()) (IO ())` - Log to a custom callback

##### Logging to a file

When logging to a file, it is common to rotate the file logged to in order to prevent
the log file from getting too big. IHP allows for this in three ways, through the [`RotateSettings`](https://ihp.digitallyinduced.com/api-docs/IHP-Log-Types.html#t:RotateSettings) record.
When logging to a file, you can configure rotation to prevent log files from getting too large.

- [`NoRotate`](https://ihp.digitallyinduced.com/api-docs/IHP-Log-Types.html#t:RotateSettings) never rotates the file, meaning the log file can become arbitrarily large.
Use with caution. The following example will log all messages to a file at `Log/production.log`.
**No rotation** - the log file can grow without limit. Use with caution:

```haskell
newLogger def {
destination = File "Log/production.log" NoRotate defaultBufSize
}
config :: ConfigBuilder
config = do
configureLogger Debug defaultFormatter (LogFileNoRotate "Log/production.log" defaultBufSize) simpleTimeFormat'
```

- [`SizeRotate`](https://ihp.digitallyinduced.com/api-docs/IHP-Log-Types.html#t:RotateSettings) rotates the file after reaching a specified size (in bytes).
The following example will log all messages to a file at `Log/production.log`,
and rotate the file once it reaches 4 megabytes in size. It will
keep 7 log files before overwriting the first file.
**Size-based rotation** using `FileLogSpec` - rotates the file after reaching a specified size.
The following example rotates at 4 megabytes, keeping 7 backup files:

```haskell
newLogger def {
destination = File "Log/production.log" (SizeRotate (Bytes (4 * 1024 * 1024)) 7) defaultBufSize
}
config :: ConfigBuilder
config = do
let fileSpec = FileLogSpec "Log/production.log" (4 * 1024 * 1024) 7
configureLogger Debug defaultFormatter (LogFile fileSpec defaultBufSize) simpleTimeFormat'
```

- [`TimedRotate`](https://ihp.digitallyinduced.com/api-docs/IHP-Log-Types.html#t:RotateSettings) rotates the file based on a time format string and a function which compares two times formatted by said format string. It also passes the rotated log's file path to a function, which can be used to compress old logs as in this example which rotates once per day:
**Time-based rotation** using `TimedFileLogSpec` - rotates based on a time format string:

```haskell
let
filePath = "Log/production.log"
formatString = "%FT%H%M%S"
timeCompare = (==) on C8.takeWhile (/=T))
compressFile fp = void . forkIO $
callProcess "tar" [ "--remove-files", "-caf", fp <> ".gz", fp ]
in
newLogger def {
destination = File
filePath
(TimedRotate formatString timeCompare compressFile)
defaultBufSize
}
config :: ConfigBuilder
config = do
let timedSpec = TimedFileLogSpec
"Log/production.log"
"%FT%H%M%S"
(\oldPath -> void . forkIO $ callProcess "tar" ["--remove-files", "-caf", oldPath <> ".gz", oldPath])
configureLogger Debug defaultFormatter (LogFileTimedRotate timedSpec defaultBufSize) simpleTimeFormat'
```

#### Configuring timestamp format

[`timeFormat`](https://ihp.digitallyinduced.com/api-docs/IHP-Log-Types.html#t:TimeFormat) expects a time format string as defined [here](https://man7.org/linux/man-pages/man3/strptime.3.html).
The time format argument expects a time format string as defined [here](https://man7.org/linux/man-pages/man3/strptime.3.html).

Example:

```haskell
newLogger def {
timeFormat = "%A, %Y-%m-%d %H:%M:%S"
}
config :: ConfigBuilder
config = do
configureLogger Debug defaultFormatter (LogStdout defaultBufSize) "%A, %Y-%m-%d %H:%M:%S"
```

Would log a timestamp as:
Expand Down Expand Up @@ -219,14 +205,9 @@ instance InitControllerContext WebApplication where

userIdLogger :: (?context :: ControllerContext) => Logger
userIdLogger =
defaultLogger { Log.formatter = userIdFormatter defaultLogger.formatter }
baseLogger { Log.log = \lvl msg -> baseLogger.log lvl (prependUserId msg) }
where
defaultLogger = ?context.frameworkConfig.logger


userIdFormatter :: (?context :: ControllerContext) => Log.LogFormatter -> Log.LogFormatter
userIdFormatter existingFormatter time level string =
existingFormatter time level (prependUserId string)
baseLogger = ?context.frameworkConfig.logger

prependUserId :: (?context :: ControllerContext) => LogStr -> LogStr
prependUserId string =
Expand All @@ -250,4 +231,4 @@ In your log output, you will see the user info prepended to the log message.

```
[30-Mar-2024 18:28:29] Authenticated user ID: 5f32a9e3-da09-48d8-9712-34c935a72c7a "This log message should have user info"
```
```
2 changes: 1 addition & 1 deletion ihp-hspec/IHP/Hspec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ withIHPApp application configBuilder hspecAction = do
FrameworkConfig.withFrameworkConfig configBuilder \frameworkConfig -> do
let FrameworkConfig { dbPoolMaxConnections, dbPoolIdleTime } = frameworkConfig

logger <- newLogger def { level = Warn } -- don't log queries
(logger, _) <- newLogger Warn defaultFormatter (LogStdout defaultBufSize) simpleTimeFormat' -- don't log queries

withTestDatabase frameworkConfig.databaseUrl \testDatabaseUrl -> do
modelContext <- createModelContext dbPoolIdleTime dbPoolMaxConnections testDatabaseUrl logger
Expand Down
15 changes: 4 additions & 11 deletions ihp-ide/IHP/IDE/SchemaDesigner/Controller/Migrations.hs
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,13 @@ findMigratedRevisions = emptyListIfTablesDoesntExists (withAppModelContext Schem

withAppModelContext :: ((?modelContext :: ModelContext) => IO result) -> IO result
withAppModelContext inner =
Exception.bracket initModelContext cleanupModelContext callback
where
callback (frameworkConfig, logger, modelContext) = let ?modelContext = modelContext in inner
initModelContext = do
frameworkConfig <- buildFrameworkConfig (pure ())
logger <- defaultLogger

withFrameworkConfig (pure ()) \frameworkConfig -> do
withDefaultLogger \logger -> do
modelContext <- createModelContext
(frameworkConfig.dbPoolIdleTime)
(frameworkConfig.dbPoolMaxConnections)
(frameworkConfig.databaseUrl)
logger

pure (frameworkConfig, logger, modelContext)

cleanupModelContext (frameworkConfig, logger, modelContext) = do
logger |> cleanup
let ?modelContext = modelContext
inner
2 changes: 1 addition & 1 deletion ihp-ide/IHP/IDE/ToolServer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ data ToolServerApplicationWithConfig = ToolServerApplicationWithConfig
-- - websocket support (for live reload)
buildToolServerApplication :: ToolServerApplication -> Int -> _ -> IO ToolServerApplicationWithConfig
buildToolServerApplication toolServerApplication port liveReloadClients = do
frameworkConfig <- Config.buildFrameworkConfig do
(frameworkConfig, _) <- Config.buildFrameworkConfig do
Config.option $ Config.AppHostname "localhost"
Config.option $ Config.AppPort port
Config.option $ Config.AssetVersion Version.ihpVersion
Expand Down
3 changes: 1 addition & 2 deletions ihp-ide/exe/IHP/IDE/DevServer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import qualified IHP.Version as Version

import qualified IHP.Log.Types as Log
import qualified IHP.Log as Log
import Data.Default (def, Default (..))
import qualified IHP.IDE.CodeGen.MigrationGenerator as MigrationGenerator
import Main.Utf8 (withUtf8)
import qualified IHP.FrameworkConfig as FrameworkConfig
Expand Down Expand Up @@ -84,7 +83,7 @@ mainWithOptions wrapWithDirenv = withUtf8 do
-- ensuring seamless transitions during app restarts (no connection refused errors)
appSocket <- createListeningSocket portConfig.appPort

bracket (Log.newLogger def) (\logger -> logger.cleanup) \logger -> do
Log.withDefaultLogger \logger -> do
(ghciInChan, ghciOutChan) <- Queue.newChan
liveReloadClients <- newIORef mempty
lastSchemaCompilerError <- newIORef Nothing
Expand Down
8 changes: 2 additions & 6 deletions ihp-log/IHP/Log.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ module IHP.Log
) where

import Prelude hiding (error, log)
import Control.Monad (when)
import IHP.Log.Types
import Network.Wai (Middleware)
import Network.Wai.Middleware.RequestLogger (mkRequestLogger, RequestLoggerSettings, destination)
Expand Down Expand Up @@ -96,12 +95,9 @@ unknown :: (?context :: context, LoggingProvider context, FastLogger.ToLogStr st
unknown = log Unknown

-- | Write a log if the given log level is greater than or equal to the logger's log level.
-- Level checking, formatting, and time caching are baked into the logger's 'log' closure.
writeLog :: (FastLogger.ToLogStr string) => LogLevel -> Logger -> string -> IO ()
writeLog level logger text = do
let write = logger.write
let formatter = logger.formatter
when (level >= logger.level) $
write (\time -> formatter time level (toLogStr text))
writeLog level logger text = logger.log level (toLogStr text)

-- | Wraps 'RequestLogger' from wai-extra to log to an IHP logger.
-- See 'Network.Wai.Middleware.RequestLogger'.
Expand Down
Loading