Closed trbarrett closed 5 years ago
Yes, Suave.Logging is intended for internal logging.
I have used Serilog with success via the following https://github.com/Zaid-Ajaj/Suave.SerilogExtensions
Looks great. Thanks @ademar.
Is the simple logging suggested by @trbarrett above bad practice are just "unusual"? I have a small web app and don't want to deal with big logging libraries.
I was even thinking about a simple
ctx.runtime.logger.debug (eventX "My Api Event")
inside my request handler functions / WebParts.
Is there anything wrong with that?
I guess you could, do note that calls to debug
and verbose
will be dropped when compiled in release mode.
https://github.com/SuaveIO/suave/blob/master/src/Suave/Utils/Logging.fs#L216
Oh wow, thanks for the warning. I was not aware of that.
Could you explain quickly the outputTemplate
format here, please:
https://github.com/SuaveIO/suave/blob/master/examples/Example/Program.fs#L27-L32
What do the brackets mean? Are they meant literally or do they indicate optional values?
What if some of the template variables are not present, e.g. exceptions
?
Also, what purpose does the logger name have? So far I haven't seen where this is displayed or used.
Thanks a lot in advance.
The brackets in the template are just output, do not mean anything to the parser.
If my memory serves right when a variable is not provided it just doesn't print anything or prints the name of the missing variable.
For example you want to do:
ctx.runtime.logger.error (eventX "error while writing response {error}" >> setFieldValue "error" err)
If you miss setting the field.
newCtx.runtime.logger.error (eventX "Socket error while writing response {error}" )
it would just output
Socket error while writing response {error}
Thanks @ademar for the explanations. Could you also say something about the logger name? Do you know where is it used?
The logger name currently have no function. It is an artifact from when we were compatible with Loggary. The idea was that you could create different loggers and later get them 'by name'
Thanks @ademar. One final question. Is it possible to register new fields? I would like to do something like this:
outputTemplate = "[{timestamp} {level} {clientAddress}] {message} {exceptions}
and then in my custom error handler:
let clientAddress : string = ctx.connection.ipAddr.ToString()+":"+ctx.connection.port.ToString()
Message.eventX msg
>> Message.setSingleName "MyErrorHandler"
>> Message.setFieldValue "clientAddress" clientAddress
>> Message.addExn ex
|> ctx.runtime.logger.error
However, the custom field is always printed as {client_address}
literally.
You can use any field names. Make sure the names match; you have typed two different names above; client_address and clientAddress.
Sorry, that was a typo in my comment. However, please note that I did NOT use two different names in my sample code.
Unfortunately the field name is not replaced by the value. I will check again tomorrow.
Sorry for the delay. I can confirm that with Release 2.5.6 the field template is not replaced by the value. I double-checked that the field name is correct.
@ademar can you reproduce this? Apart from this, I am pretty happy with the whole built-in logging and error handling system.
@svdHero I'll look at as soon as possible.
@ademar Any progress on this or do you have a time estimate when you will look into this? Thanks.
@ademar Any update on this?
Hi @svdHero
Sorry for the late response. This seems to be working.
I have written a unit test for it that passes successfully. See https://github.com/SuaveIO/suave/commit/3fddb3be5caad552a70f73b060f3427d63d0192d
I also see the output from running Suave indicates that it is working.
[10:03:03 INF] Smooth! Suave listener started in 75.611ms with binding 10.0.0.5:8083
It comes from here:
logger.info (
eventX "Smooth! Suave listener started in {startedListeningMilliseconds:#.###}ms with binding {ipAddress}:{port}"
>> setFieldValue "startedListeningMilliseconds" (startData.GetStartedListeningElapsedMilliseconds())
// .Address can throw exceptions, just log its string representation
>> setFieldValue "ipAddress" (startData.binding.ip.ToString())
>> setFieldValue "port" startData.binding.port
>> setSingleName "Suave.Tcp.runServer")
If you could share with us an example exhibiting the issue we can troubleshoot further.
Thank you so much for your reply, @ademar .
I want to add the Correlation Id to the logging and I have the following real-life code:
namespace BT.RPE.BackEnd.Web.Config
type ErrorHandlerInfo = {
ErrorMessage: string
ExceptionMessage : string
ExceptionDetails : string
}
type UnhandledServerRuntimeError = UnhandledServerRuntimeError of ErrorHandlerInfo
[<RequireQualifiedAccess>]
module internal Server =
open System.IO
open Suave
open Suave.Logging
open BT.FSharp.Util.Data
open BT.RPE.BackEnd.Web
// ########################################################################
[<RequireQualifiedAccess>]
module private EnvVars =
[<Literal>]
let RPE_IP_ADDRESS = "RPE_IP_ADDRESS"
[<Literal>]
let RPE_PORT = "RPE_PORT"
let private getEnvVar (envVarName: string) =
let varValue = System.Environment.GetEnvironmentVariable(envVarName)
if System.String.IsNullOrWhiteSpace varValue then
nullArg (sprintf "environment variable \"%s\"" varValue)
else
printfn "Environment variable \"%s\" read for configuration." envVarName
varValue
// ########################################################################
let private createLogger () =
// Also see https://github.com/SuaveIO/suave/blob/v2.5.6/examples/Example/Program.fs#L23-L32
// and https://github.com/SuaveIO/suave/issues/721#issuecomment-578553378
let target = LiterateConsoleTarget (
name = [|"BT";"RPE";"BackEnd"|],
minLevel = LogLevel.Verbose,
options = Literate.LiterateOptions.create(),
outputTemplate = "[{timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {level} {corrId}]{newline}{message} [{source}] {exceptions}{newline}"
)
let logger = target :> Logger
logger
let private createErrorHandler () =
// Also see https://dusted.codes/custom-error-handling-and-logging-in-suave
let errorHandler : ErrorHandler =
let assemblyName = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name
fun ex msg ->
fun ctx ->
let corrIdOpt = ctx.request |> CorrelationId.getCorrIdHeader
let corrId =
match corrIdOpt with
| Some c -> c |> HierarchicalCorrelationId.toString
| None -> "NOT AVAILABLE"
Message.eventX msg
>> Message.setSingleName (assemblyName+".ErrorHandler")
>> Message.setFieldValue "corrId" corrId
>> Message.addExn ex
|> ctx.runtime.logger.error
if ctx.isLocal then // change to ctx.isLocalTrustProxy with new release
let runtimeError = UnhandledServerRuntimeError {
ErrorMessage = msg
ExceptionMessage = if isNull ex then null else ex.Message
ExceptionDetails = if isNull ex then null else ex.ToString()
}
JsonResponses.INTERNAL_ERROR (Util.toJson runtimeError) ctx
else
let runtimeError = UnhandledServerRuntimeError {
ErrorMessage = msg
ExceptionMessage = if isNull ex then null else ex.Message
ExceptionDetails = null
}
JsonResponses.INTERNAL_ERROR (Util.toJson runtimeError) ctx
errorHandler
let private createBindings () =
let port = getEnvVar EnvVars.RPE_PORT |> int
let ip_address = getEnvVar EnvVars.RPE_IP_ADDRESS
let bindings = [
HttpBinding.createSimple HTTP ip_address port;
]
bindings
// ########################################################################
/// Creates the Suave.io configuration
let createServerConfig () =
{ defaultConfig with
bindings = createBindings ()
homeFolder = Some (Path.GetFullPath "./public")
logger = createLogger ()
errorHandler = createErrorHandler ()
}
// ########################################################################
However, when my web app produces an exception, all I get is this logging console output:
[2020-07-16 09:19:17.489 +02:00 ERR {corrId}]
request failed [BT.RPE.BackEnd.Web.ErrorHandler]
System.Exception: Boom! Nasty Exception!
at BT.RPE.BackEnd.Web.QueryRequestHandler.query(RequestHandlerDependencies deps, FSharpOption`1 uidOpt, HttpContext ctx) in W:\RPE_BackEnd\projects\BT.RPE.BackEnd.Web\QueryRequestHandler.fs:line 63
at BT.RPE.BackEnd.Web.Routing.postRoutes@68-2.Invoke(RequestHandlerDependencies deps, FSharpOption`1 uidOpt, HttpContext ctx) in W:\RPE_BackEnd\projects\BT.RPE.BackEnd.Web\Routing.fs:line 68
at BT.RPE.BackEnd.Web.Util.toWebPart(FSharpFunc`2 handler, HttpContext ctx) in W:\RPE_BackEnd\projects\BT.RPE.BackEnd.Web\Common.fs:line 61
at BT.RPE.BackEnd.Web.Routing.postRoutes@68-3.Invoke(HttpContext ctx) in W:\RPE_BackEnd\projects\BT.RPE.BackEnd.Web\Routing.fs:line 68
at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, FSharpFunc`2 userCode, b result1) in E:\A\_work\130\s\src\fsharp\FSharp.Core\async.fs:line 417
at <StartupCode$FSharp-Core>.$Async.Return@1066.Invoke(AsyncActivation`1 ctxt) in E:\A\_work\130\s\src\fsharp\FSharp.Core\async.fs:line 1066
[...]
Notice how the field template {corrId}
is not replaced by any value, not even the NOT AVAILABLE
value. What am I doing wrong?
If it's too much code, I can open a new issue and try to link a simple hello world repo.
Update: I edited my example above and provided an exception scenario.
OK, I see the problem.
Note the first argument to Message.eventX
is the message template. In the error handler above it should be called like this.
Message.eventX "[{corrId}]{message}"
>> Message.setSingleName (assemblyName+".ErrorHandler")
>> Message.setFieldValue "corrId" corrId
>> Message.setFieldValue "message" msg
>> Message.addExn ex
|> ctx.runtime.logger.error
Also do not need to override the default logger's template; if you were to do so you would need to write an actual implementation for the logger since LiterateConsoleTarget
doesn't know about the field corrId
.
The logger's template and the message template are different and treated at different levels. The message
string value is built before reaching down there to the logger. It is confusing here that we are using also a field message
in our message template.
Let me know if you need more clarity, I know is confusing.
Okay, thanks. I will give it a try tomorrow morning. Just to clarify: Can I overwrite the logger template the way I did, if I it's only about formatting and if do not add new, additional fields?
Yes, that'll work.
@ademar Okay, that worked. Almost...
I've modified my code to:
let corrIdOpt = ctx.request |> CorrelationId.getCorrIdHeader
let corrId =
match corrIdOpt with
| Some c -> c |> HierarchicalCorrelationId.toString
| None -> "NOT AVAILABLE"
Message.eventX "[{corrId}] {message}"
>> Message.setSingleName (assemblyName+".ErrorHandler")
>> Message.setFieldValue "corrId" corrId
>> Message.setFieldValue "message" msg
>> Message.addExn ex
|> ctx.runtime.logger.error
according to your suggestions. The template {corrId}
is now replaced by NOT AVAILABLE
. The problem: It is ALWAYS replaced by NOT AVAILABLE
, although my routing roughly looks like this:
ensureCorrId // Makes sure that a correlation id is present and updated
>=> choose [
path "/rpe/api/queries" >=> (Util.toWebPart QueryRequestHandler.query)
[...]
]
In the WebPart ensureCorrId
I check for a correaltion id header. If there is none, I generate a new one and add it to both ctx.request
and ctx.response
, in order to have it available further down the pipeline. So, in theory, if there is an exception in QueryRequestHandler.query
then the error handler should already have a correlation id available.
Unless of course, the error handler takes the context
from the beginning of the routing pipeline.
Do you have any suggestions or idea on this? How can I access my correlation id in the error handler?
Unless of course, the error handler takes the context from the beginning of the routing pipeline.
Yeah, this is precisely what is happening.
Here HttpContext.userState
was supposed to address this use case but I realize it was made an immutable map and as such will have the same problem.
Because I've run into this before and the immutable map is not useful here I'll be changing it into an mutable dictionary so we can have global user state per request.
Then you could do:
let ensureCorrId =
context(fun ctx ->
ctx.userState.Add("corrId", (Guid.NewGuid().ToString()))
succeed)
And in the error handler:
let corrId =
if ctx.userState.ContainsKey "corrId" then
ctx.userState.["corrId"] :?> string
else
"NOT AVAILABLE"
Give me a couple of hours to test that this change won't break anything.
I'm killing the OWIN module; I hope you are not using it.
Is that the embedded web server? I'm new to .NET backends. If yes, please don't!
No, OWIN is an interface to run OWIN applications on Suave.
If you do not know my bet is you don't need it.
I'll be pushing new nugets later today implementing these changes.
I've just published a new pre-release v2.6.0-beta; try my suggestion above of using the ctx.userState
dictionary to carry the correlation id.
Let me know how it goes.
@ademar I haven't had a chance to test it, yet, but I will.
I have another question concerning this: What would be the easiest way to
I noticed the outputWriter
parameter in the LiterateConsoleTarget
constructor, but I don't know how to use it. Are there any example code snippets somewhere?
Hi. I've been trying to figure out how to do some simple logging with suave. The documentation in https://suave.io/logs.html seems somewhat outdated. The first "sample" link on that page is neither relevant to suave or logging? And none of the code seems to work with the latest versions.
More relevant example code appears to be: https://github.com/SuaveIO/suave/blob/master/examples/Example/Program.fs#L27 which indicates suave has it's own logger which doesn't involve a Logary dependency. And by looking at https://github.com/SuaveIO/suave/blob/master/src/Suave/Tcp.fs I can see how that is used.
Am I right thinking I can do something like the following in my own code if I want some simple logging:
Or is Suave.Logging really meant more for internal use by the Suave project, and I need to figure out Logary?