Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file log.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576(* This file is part of Dream, released under the MIT license. See LICENSE.md
for details, or visit https://github.com/aantron/dream.
Copyright 2021 Anton Bachin *)(* Among other things, this module wraps the Logs library so as to prepend
request ids to log messages.
However, instead of prepending the id at the front end of Logs, in the
wrappers, we prepend the id at the back end instead - in the reporter. The
rationale for this is that we want to (try to) prepend the id even to strings
that don't come from Dream or the user's Dream app, and thus *definitely* do
not call this module's wrappers.
The wrappers try to get the request id from their argument ~request, and pass
it down to the reporter in a Logs tag.
The reporter reads the tag and uses that request id if it is available. If
the request id is not available, it is because the log message comes from a
point in the Dream app where an id has not been assigned to the request, or
because the log message comes from another dependency (or a sloppy call
site!). In that case, the reporter tries to retrieve the id from the
promise-chain-local storage of Lwt.
This is sufficient for attaching a request id to most log messages, in
practice. *)moduleMessage=Dream_pure.MessagemoduleMethod=Dream_pure.MethodmoduleStatus=Dream_pure.Statustypelog_level=[|`Error|`Warning|`Info|`Debug](* The logging middleware assigns request ids to requests, and tries to show
them in the logs. The scheme works as follows:
- Request ids are strings stored in request-local variables.
- The automatically assigned request ids are taken from a simple global
sequence.
- The user can override the automatic request id by assigning a request id
in a middleware that runs before the logger. User-provided request ids can
be per-thread, can come from a proxy header, etc.
- The logger makes a best effort to forward the request id to all logging
statements that are being formatted. If the ~request argument is provided
during a logging call, that request's id is shown. To handle all other
cases, the logger puts the request's id into an Lwt sequence-associated
storage key, and the log message formatter tries to get it from there. *)(* TODO Necessary helpers for the user setting the request id are not yet
exposed in the API, pending some other refactoring (request mutability). *)letrequest_id_label="dream.request_id"(* Logs library tag uesd to pass an id from a request provided through
~request. *)letlogs_lib_tag:stringLogs.Tag.def=Logs.Tag.defrequest_id_labelFormat.pp_print_string(* Lwt sequence-associated storage key used to pass request ids for use when
~request is not provided. *)letid_lwt_key:stringLwt.key=Lwt.new_key()(* The actual request id "field" associated with each request by the logger. If
this field is missing, the logger assigns the request a fresh id. *)letid_field=Message.new_field~name:request_id_label~show_value:(funid->id)()(* Makes a best-effort attempt to retrieve the request id. *)letget_request_id?request()=letrequest_id=matchrequestwith|None->None|Somerequest->Message.fieldrequestid_fieldinmatchrequest_idwith|Some_->request_id|None->Lwt.getid_lwt_key(* The current state of the request id sequence. *)letlast_id=ref0(* TODO Nice logging for multiline strings? *)(* The "back end." I inlined several examples from the Logs, Logs_lwt, and Fmt
docs into each other, and modified the result, to arrive at this function.
See those docs for the meanings of the various helpers and values.
The reporter needs to be suspended in a function because Dream sets up the
logger lazily; it doesn't query the output streams for whether they are TTYs
until needed. Setting up the reporter before TTY checking will cause it to
not output color. *)letreporter~now()=(* Format into an internal buffer. *)letbuffer=Buffer.create512inletformatter=Fmt.with_buffer~like:Fmt.stderrbufferinletflush()=letmessage=Buffer.contentsbufferinBuffer.resetbuffer;messagein(* Gets called by Logs for each log call that passes its level threshold.
~over is to be called when the I/O underlying the log operation is fully
complete. In practice, since most call sites are not using Lwt, they will
continue executing anyway. This means that the message must be formatted
and the buffer flushed before doing anything asynchronous, so that
subsequent logging operations don't get into the same generation of the
buffer.
The user's_callback argument is not exactly the user's callback - it's the
callback that got wrapped in function source (the "front end") below. That
wrapper is the actual user's callback, and it calls user's_callback. *)letreportsrclevel~overkuser's_callback=letlevel_style,level=matchlevelwith|Logs.App->`White," "|Logs.Error->`Red,"ERROR"|Logs.Warning->`Yellow," WARN"|Logs.Info->`Green," INFO"|Logs.Debug->`Blue,"DEBUG"inletwrite_=(* Get the formatted message out of the buffer right away, because we are
doing Lwt operations next, and the caller might not wait. *)letmessage=flush()in(* Write the message. *)prerr_stringmessage;Stdlib.flushstderr;over();k()in(* Call the user's callback to get the actual message and trigger
formatting, and, eventually, writing. The wrappers don't use the ?header
argument, so we ignore it. *)user's_callback@@fun?header?tagsformat_and_arguments->ignoreheader;(* Format the current local time. For the millisecond fraction, be careful
of rounding 999.5+ to 1000 on output. *)lettime=letunix_time=now()inlettime=Option.get(Ptime.of_float_sunix_time)inletfraction=fst(modfunix_time)*.1000.inletclamped_fraction=iffraction>999.then999.elsefractioninlet((y,m,d),((hh,mm,ss),_tz_offset_s))=Ptime.to_date_timetimeinPrintf.sprintf"%02i.%02i.%02i %02i:%02i:%02i.%03.0f"dm(ymod100)hhmmssclamped_fractionin(* Format the source name column. It is the right-aligned log source name,
clipped to the column width. If the source is the default application
source, leave the column empty. *)letsource=letwidth=15inifLogs.Src.namesrc=Logs.Src.nameLogs.defaultthenString.makewidth' 'elseletname=Logs.Src.namesrcinifString.lengthname>widththenString.subname(String.lengthname-width)widthelse(String.make(width-String.lengthname)' ')^nameinletsource_prefix,source=tryletdot_index=String.rindexsource'.'+1inString.subsource0dot_index,String.subsourcedot_index(String.lengthsource-dot_index)withNot_found->"",sourcein(* Check if a request id is available in the tags passed from the front
end. If not, try to get it from the promise-chain-local storage. If
we end up with a request id, format it. *)letrequest_id_from_tags=matchtagswith|None->None|Sometags->Logs.Tag.findlogs_lib_tagtagsinletrequest_id=matchrequest_id_from_tagswith|Some_->request_id_from_tags|None->get_request_id()inletrequest_id,request_style=matchrequest_idwith|Some""|None->"",`White|Somerequest_id->(* The last byte of the request id is basically always going to be a
digit, growing incrementally, so we can use the parity of its
ASCII code to stripe the requests in the log. *)letlast_byte=request_id.[String.lengthrequest_id-1]inletcolor=if(Char.codelast_byte)land1=0then`Cyanelse`Magentain" REQ "^request_id,colorin(* The formatting proper. *)Format.kfprintfwriteformatter("%a %a%s %a%a @["^^format_and_arguments^^"@]@.")Fmt.(styled`Faintstring)timeFmt.(styled`Whitestring)source_prefixsourceFmt.(styledlevel_stylestring)levelFmt.(styledrequest_style(styled`Italicstring))request_idin{Logs.report}(* Lazy initialization upon first use or call to initialize. *)letenable=reftrueletlevel=refLogs.Infoletcustom_log_levels:(string*Logs.level)listref=ref[]letsources:(string*Logs.src)listref=ref[]letset_printexc=reftrueletset_async_exception_hook=reftruelet_initialized=refNoneletto_logs_levell=matchlwith|`Error->Logs.Error|`Warning->Logs.Warning|`Info->Logs.Info|`Debug->Logs.DebugexceptionLogs_are_not_initializedletsetup_logs="\nTo initialize logs with a default reporter, and set up Dream, \
do the following:\
\n If you are using MirageOS, use the Dream device in config.ml
\n If you are using Lwt/Unix, execute `Dream.log_initialize ()`
\n"let()=Printexc.register_printer@@function|Logs_are_not_initialized->Some("The default logger is not yet initialized. "^setup_logs)|_->Noneletinitialized():[`Initialized]=match!_initializedwith|None->raiseLogs_are_not_initialized|Somev->Lazy.forcev(* The "front end." *)type('a,'b)conditional_log=((?request:Message.request->('a,Stdlib.Format.formatter,unit,'b)Stdlib.format4->'a)->'b)->unittypesub_log={error:'a.('a,unit)conditional_log;warning:'a.('a,unit)conditional_log;info:'a.('a,unit)conditional_log;debug:'a.('a,unit)conditional_log;}letsub_log?level:level_name=(* This creates a wrapper, as described above. The wrapper forwards to a
logger of the Logs library, but instead of passing the formatter m to the
user's callback, it passes a formatter m', which is like m, but lacks a
?tags argument. It has a ?request argument instead. If ~request is given,
m' immediately tries to retrieve the request id, put it into a Logs tag,
and call Logs' m with the user's formatting arguments and the tag. *)letforward~(destination_log:_Logs.log)user's_k=let`Initialized=initialized()indestination_log(funlog->user's_k(fun?requestformat_and_arguments->lettags=matchrequestwith|None->Logs.Tag.empty|Somerequest->matchget_request_id~request()with|None->Logs.Tag.empty|Somerequest_id->Logs.Tag.addlogs_lib_tagrequest_idLogs.Tag.emptyinlog~tagsformat_and_arguments))inletlevel=List.findOption.is_some[Option.mapto_logs_levellevel_;List.assoc_optname!custom_log_levels;Some!level]in(* Create the actual Logs source, and then wrap all the interesting
functions. *)letsrc=Logs.Src.createnameinlet(moduleLog)=Logs.src_logsrcinLogs.Src.set_levelsrclevel;custom_log_levels:=(name,Option.getlevel)::(List.remove_assocname!custom_log_levels);sources:=(name,src)::(List.remove_assocname!sources);{error=(funk->forward~destination_log:Log.errk);warning=(funk->forward~destination_log:Log.warnk);info=(funk->forward~destination_log:Log.infok);debug=(funk->forward~destination_log:Log.debugk);}letconvenience_logformat_and_arguments=Fmt.kstr(funmessage->let`Initialized=initialized()inLogs.app(funlog->log"%s"message))format_and_arguments(* Logs.app (fun log -> log format_and_arguments) *)(* let report = Logs.((reporter ()).report) in
report Logs.default Logs.App ~over:ignore ignore format_and_arguments *)(* A helper used in several places. *)letiter_backtracefbacktrace=backtrace|>String.split_on_char'\n'|>List.filter(funline->line<>"")|>List.iterf(* Use the above function to create a log source for Log's own middleware, the
same way any other middleware would. *)letlog=sub_log"dream.logger"letset_up_exception_hook()=if!set_async_exception_hookthenbeginset_async_exception_hook:=false;Lwt.async_exception_hook:=funexn->letbacktrace=Printexc.get_backtrace()inlog.error(funlog->log"Async exception: %s"(Printexc.to_stringexn));backtrace|>iter_backtrace(funline->log.error(funlog->log"%s"line))endletinitialize_log?(backtraces=true)?(async_exception_hook=true)?level:level_?enable:(enable_=true)()=ifbacktracesthenPrintexc.record_backtracetrue;set_printexc:=false;ifasync_exception_hookthenset_up_exception_hook();set_async_exception_hook:=false;letlevel_=Option.mapto_logs_levellevel_|>Option.value~default:Logs.Infoinenable:=enable_;level:=level_;let`Initialized=initialized()in()letset_log_levelnamelevel=(* If logging hasn't been initialized, trigger this so that
configuration of log levels can proceed. *)let`Initialized=initialized()inletlevel=to_logs_levellevelincustom_log_levels:=(name,level)::(List.remove_assocname!custom_log_levels);letsrc=List.assoc_optname!sourcesinOption.iter(funs->Logs.Src.set_levels(Somelevel))srcletfd_field:intMessage.field=Message.new_field~name:"dream.fd"~show_value:string_of_int()moduleMake(Pclock:Mirage_clock.PCLOCK)=structletnow()=Ptime.to_float_s(Ptime.v(Pclock.now_d_ps()))letinitializer_~setup_outputs=lazybeginif!enablethenbeginsetup_outputs();Logs.set_level~all:true(Some!level);!custom_log_levels|>List.iter(fun(name,level)->List.assoc_optname!sources|>Option.iter(funsource->Logs.Src.set_levelsource(Somelevel)));Logs.set_reporter(reporter~now())end;`Initializedendletset=reffalseletinitialize~setup_outputs=if!setthenLogs.debug(funlog->log"Dream__log.initialize has already been called, ignoring this call.")elsebegin(trylet`Initialized=initialized()inFormat.eprintf"Dream__log.initialized has already been set, check that this call \
is intentional";withLogs_are_not_initialized->());set:=true;_initialized:=Some(initializer_~setup_outputs)end(* The request logging middleware. *)letloggernext_handlerrequest=letstart=now()in(* Turn on backtrace recording. *)if!set_printexcthenbeginPrintexc.record_backtracetrue;set_printexc:=falseend;(* Get the request's id or assign a new one. *)letid=matchMessage.fieldrequestid_fieldwith|Someid->id|None->last_id:=!last_id+1;letid=string_of_int!last_idinMessage.set_fieldrequestid_fieldid;idinletfd_string=matchMessage.fieldrequestfd_fieldwith|None->""|Somefd->" fd "^(string_of_intfd)in(* Identify the request in the log. *)letuser_agent=Message.headersrequest"User-Agent"|>String.concat" "inlog.info(funlog->log~request"%s %s %s%s %s"(Method.method_to_string(Message.method_request))(Message.targetrequest)(Helpers.clientrequest)fd_stringuser_agent);(* Call the rest of the app. *)Lwt.try_bind(fun()->Lwt.with_valueid_lwt_key(Someid)(fun()->next_handlerrequest))(funresponse->(* Log the elapsed time. If the response is a redirection, log the
target. *)letlocation=ifStatus.is_redirection(Message.statusresponse)thenmatchMessage.headerresponse"Location"with|Somelocation->" "^location|None->""else""inletstatus=Message.statusresponseinletreport:(?request:Message.request->('a,Format.formatter,unit,'b)format4->'a)->'b=funlog->letelapsed=now()-.startinlog~request"%i%s in %.0f μs"(Status.status_to_intstatus)location(elapsed*.1e6)inbeginifStatus.is_server_errorstatusthenlog.errorreportelseifStatus.is_client_errorstatusthenlog.warningreportelselog.inforeportend;Lwt.returnresponse)(funexn->letbacktrace=Printexc.get_backtrace()in(* In case of exception, log the exception. We alsp log the backtrace
here, even though it is likely to be redundant, because some OCaml
libraries install exception printers that will clobber the backtrace
right during Printexc.to_string! *)log.warning(funlog->log~request"Aborted by: %s"(Printexc.to_stringexn));backtrace|>iter_backtrace(funline->log.warning(funlog->log"%s"line));Lwt.failexn)end(* TODO DOC Include logging itself in the timing. Or? Isn't that pointless?
End-to -end timing should include the HTTP parser as well. The logger
provides much more useful information if it helps the user optimize the app.
Sp, should probably craete some helpers for the user to do end-to-end timing
of the HTTP server and document how to use them. *)(* TODO DOC Add docs on how to avoid OCamlbuild dep. *)(* TODO DOC why it's good to use the initializer early. *)(* TODO LATER implement fire. *)(* TODO LATER In case of streamed bodies, it is useful for the logger to be told
by the HTTP layer when streaming was actually completed. *)