View source with raw comments or as raw
    1/*  Part of SWI-Prolog
    2
    3    Author:        Jan Wielemaker
    4    E-mail:        J.Wielemaker@vu.nl
    5    WWW:           http://www.swi-prolog.org
    6    Copyright (c)  2008-2025, University of Amsterdam
    7                              VU University Amsterdam
    8                              CWI, Amsterdam
    9                              SWI-Prolog Solutions b.v.
   10    All rights reserved.
   11
   12    Redistribution and use in source and binary forms, with or without
   13    modification, are permitted provided that the following conditions
   14    are met:
   15
   16    1. Redistributions of source code must retain the above copyright
   17       notice, this list of conditions and the following disclaimer.
   18
   19    2. Redistributions in binary form must reproduce the above copyright
   20       notice, this list of conditions and the following disclaimer in
   21       the documentation and/or other materials provided with the
   22       distribution.
   23
   24    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   25    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   26    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
   27    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
   28    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   29    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
   30    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   31    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
   32    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   33    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
   34    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   35    POSSIBILITY OF SUCH DAMAGE.
   36*/
   37
   38:- module(http_log,
   39          [ http_log_stream/1,          % -Stream
   40            http_log/2,                 % +Format, +Args
   41            http_log_close/1,           % +Reason
   42            post_data_encoded/2,        % ?Bytes, ?Encoded
   43            http_logrotate/1,           % +Options
   44            http_schedule_logrotate/2   % +When, +Options
   45          ]).   46:- use_module(library(http/http_header)).   47:- use_module(library(settings)).   48:- use_module(library(option)).   49:- use_module(library(error)).   50:- use_module(library(debug)).   51:- use_module(library(broadcast)).   52
   53:- setting(http:logfile, callable, 'httpd.log',
   54           'File in which to log HTTP requests').   55:- setting(http:log_post_data, integer, 0,
   56           'Log POST data up to N bytes long').   57:- setting(http:on_log_error, any, retry,
   58           'Action if logging fails').

HTTP Logging module

Simple module for logging HTTP requests to a file. Logging is enabled by loading this file and ensure the setting http:logfile is not the empty atom. The default file for writing the log is httpd.log. See library(settings) for details.

The level of logging can be modified using the multifile predicate nolog/1 to hide HTTP request fields from the logfile and password_field/1 to hide passwords from HTTP search specifications (e.g. /topsecret?password=secret).

   73:- multifile
   74    nolog/1,
   75    password_field/1,
   76    nolog_post_content_type/1.   77
   78% If the log settings change,  simply  close   the  log  and  it will be
   79% reopened with the new settings.
   80
   81:- listen(settings(changed(http:logfile, _, New)),
   82          http_log_close(changed(New))).   83:- listen(http(Message),
   84          http_message(Message)).   85:- listen(logrotate,
   86          http_log_close(logrotate)).   87
   88http_message(Message) :-
   89    log_message(Message),
   90    http_log_stream(Stream),
   91    E = error(_,_),
   92    catch(http_message(Message, Stream), E,
   93          log_error(E)).
   94
   95log_message(request_start(_Id, _Request)).
   96log_message(request_finished(_Id, _Code, _Status, _CPU, _Bytes)).
   97
   98http_message(request_start(Id, Request), Stream) :-
   99    log_started(Request, Id, Stream).
  100http_message(request_finished(Id, Code, Status, CPU, Bytes), Stream) :-
  101    log_completed(Code, Status, Bytes, Id, CPU, Stream).
 log_error(+Error)
There was an error writing the log file. The message is printed using print_message/2 and execution continues according to the setting http:on_log_error, which is one of:
retry
Close the log file. The system will try to reopen it on the next log event, recovering from the error. Note that the most common case for this is probably running out of disc space.
exit
exit(Code)
Stop the server using halt(Code). The exit variant is equivalent to exit(1).

The best choice depends on your priorities. Using retry gives priority to keep the server running. Using exit guarantees proper log files and thus the ability to examine these for security reasons. An attacker may try to flood the disc, causing a successful DoS attack if exit is used and the ability to interact without being logged if retry is used.

  125log_error(E) :-
  126    print_message(warning, E),
  127    log_error_continue.
  128
  129log_error_continue :-
  130    setting(http:on_log_error, Action),
  131    log_error_continue(Action).
  132
  133log_error_continue(retry) :-
  134    http_log_close(error).
  135log_error_continue(exit) :-
  136    log_error_continue(exit(1)).
  137log_error_continue(exit(Code)) :-
  138    halt(Code).
  139
  140
  141
  142                 /*******************************
  143                 *         LOG ACTIVITY         *
  144                 *******************************/
  145
  146:- dynamic
  147    log_stream/2,                   % Stream, TimeTried
  148    halt_registered/0.
 http_log_stream(-Stream) is semidet
True when Stream is a stream to the opened HTTP log file. Opens the log file in append mode if the file is not yet open. The log file is determined from the setting http:logfile. If this setting is set to the empty atom (''), this predicate fails.

If a file error is encountered, this is reported using print_message/2, after which this predicate silently fails. Opening is retried every minute when a new message arrives.

Before opening the log file, the message http_log_open(Term) is broadcasted. This message allows for creating the directory, renaming, deleting or truncating an existing log file.

  165http_log_stream(Stream) :-
  166    log_stream(Stream, _Opened),
  167    !,
  168    Stream \== [].
  169http_log_stream([]) :-
  170    setting(http:logfile, ''),
  171    !,
  172    get_time(Now),
  173    assert(log_stream([], Now)),
  174    fail.
  175http_log_stream(Stream) :-
  176    setting(http:logfile, Term),
  177    broadcast(http_log_open(Term)),
  178    catch(absolute_file_name(Term, File,
  179                             [ access(append)
  180                             ]),
  181          E, open_error(E)),
  182    with_mutex(http_log, open_log(File, Stream0)),
  183    Stream = Stream0.
  184
  185open_log(_File, Stream) :-
  186    log_stream(Stream, Opened),
  187    (   Stream == []
  188    ->  (   get_time(Now),
  189            Now - Opened > 60
  190        ->  retractall(log_stream(_,_)),
  191            fail
  192        ;   !, fail
  193        )
  194    ;   true
  195    ), !.
  196open_log(File, Stream) :-
  197    catch(open(File, append, Stream,
  198               [ close_on_abort(false),
  199                 encoding(utf8),
  200                 buffer(line)
  201               ]), E, open_error(E)),
  202    get_time(Time),
  203    register_close_log,
  204    Error = error(_,_),
  205    catch(format(Stream,
  206                 'server(started, ~0f).~n',
  207                 [ Time ]),
  208          Error,
  209          open_error(Stream, Error)),
  210    assert(log_stream(Stream, Time)).
  211
  212open_error(Stream, E) :-
  213    close(Stream, [force(true)]),
  214    open_error(E).
  215
  216open_error(E) :-
  217    print_message(error, E),
  218    log_open_error_continue.
  219
  220log_open_error_continue :-
  221    setting(http:on_log_error, Action),
  222    log_open_error_continue(Action).
  223
  224log_open_error_continue(retry) :-
  225    !,
  226    get_time(Now),
  227    assert(log_stream([], Now)),
  228    fail.
  229log_open_error_continue(Action) :-
  230    log_error_continue(Action).
  231
  232register_close_log :-
  233    halt_registered,
  234    !.
  235register_close_log :-
  236    at_halt(close_log(stopped)),
  237    asserta(halt_registered).
 http_log_close(+Reason) is det
If there is a currently open HTTP logfile, close it after adding a term server(Reason, Time). to the logfile. This call is intended for cooperation with the Unix logrotate facility using the following schema:
author
- Suggested by Jacco van Ossenbruggen
  254http_log_close(Reason) :-
  255    with_mutex(http_log, close_log(Reason)).
  256
  257close_log(Reason) :-
  258    retract(log_stream(Stream, _Opened)),
  259    !,
  260    (   is_stream(Stream)
  261    ->  get_time(Time),
  262        catch(( format(Stream, 'server(~q, ~0f).~n', [ Reason, Time ]),
  263                close(Stream)
  264              ), E, print_message(warning, E))
  265    ;   true
  266    ).
  267close_log(_).
 http_log(+Format, +Args) is det
Write message from Format and Args to log-stream. See format/2 for details. Succeed without side effects if logging is not enabled.
  275http_log(Format, Args) :-
  276    (   http_log_stream(Stream)
  277    ->  Error = error(_,_),
  278        catch(system:format(Stream, Format, Args), % use operators from `system`
  279              Error,
  280              log_error(Error))
  281    ;   true
  282    ).
 log_started(+Request, +Id, +Stream) is det
Write log message that Request was started to Stream.
Arguments:
Filled- with sequence identifier for the request
  291log_started(Request, Id, Stream) :-
  292    is_stream(Stream),
  293    !,
  294    get_time(Now),
  295    add_post_data(Request, Request1),
  296    log_request(Request1, LogRequest),
  297    format_time(string(HDate), '%+', Now),
  298    format(Stream,
  299           '/*~s*/ request(~q, ~3f, ~q).~n',
  300           [HDate, Id, Now, LogRequest]).
  301log_started(_, _, _).
 log_request(+Request, -Log)
Remove passwords from the request to avoid sending them to the logfiles.
  308log_request([], []).
  309log_request([search(Search0)|T0], [search(Search)|T]) :-
  310    !,
  311    mask_passwords(Search0, Search),
  312    log_request(T0, T).
  313log_request([H|T0], T) :-
  314    nolog(H),
  315    !,
  316    log_request(T0, T).
  317log_request([H|T0], [H|T]) :-
  318    log_request(T0, T).
  319
  320mask_passwords([], []).
  321mask_passwords([Name=_|T0], [Name=xxx|T]) :-
  322    password_field(Name),
  323    !,
  324    mask_passwords(T0, T).
  325mask_passwords([H|T0], [H|T]) :-
  326    mask_passwords(T0, T).
 password_field(+Field) is semidet
Multifile predicate that can be defined to hide passwords from the logfile.
  333password_field(password).
  334password_field(pwd0).
  335password_field(pwd1).
  336password_field(pwd2).
 nolog(+HTTPField)
Multifile predicate that can be defined to hide request parameters from the request logfile.
  344nolog(input(_)).
  345nolog(accept(_)).
  346nolog(accept_language(_)).
  347nolog(accept_encoding(_)).
  348nolog(accept_charset(_)).
  349nolog(pool(_)).
  350nolog(protocol(_)).
  351nolog(referer(R)) :-
  352    sub_atom(R, _, _, _, password),
  353    !.
 nolog_post_content_type(+Type) is semidet
Multifile hook called with the Content-type header. If the hook succeeds, the POST data is not logged. For example, to stop logging anything but application/json messages:
:- multifile http_log:nolog_post_content_type/1.

http_log:nolog_post_content_type(Type) :-
   Type \= (application/json).
Arguments:
Type- is a term MainType/SubType
 add_post_data(+Request0, -Request) is det
Add a request field post_data(Data) if the setting http:log_post_data is an integer > 0, the content length < this setting and nolog_post_content_type/1 does not succeed on the provided content type.
  377add_post_data(Request0, Request) :-
  378    setting(http:log_post_data, MaxLen),
  379    integer(MaxLen), MaxLen > 0,
  380    memberchk(input(In), Request0),
  381    memberchk(content_length(CLen), Request0),
  382    CLen =< MaxLen,
  383    memberchk(content_type(Type), Request0),
  384    http_parse_header_value(content_type, Type, media(MType/MSubType, _)),
  385    \+ nolog_post_content_type(MType/MSubType),
  386    catch(peek_string(In, CLen, PostData), _, fail),
  387    !,
  388    post_data_encoded(PostData, Encoded),
  389    Request = [post_data(Encoded)|Request0].
  390add_post_data(Request, Request).
 post_data_encoded(?Bytes:string, ?Encoded:string) is det
Encode the POST body for inclusion into the HTTP log file. The POST data is (in/de)flated using zopen/3 and base64 encoded using base64//1. The encoding makes long text messages shorter and keeps readable logfiles if binary data is posted.
  399post_data_encoded(Bytes, Hex) :-
  400    nonvar(Bytes),
  401    !,
  402    setup_call_cleanup(
  403        new_memory_file(HMem),
  404        ( setup_call_cleanup(
  405              ( open_memory_file(HMem, write, Out, [encoding(octet)]),
  406                zopen(Out, ZOut, [])
  407              ),
  408              format(ZOut, '~s', [Bytes]),
  409              close(ZOut)),
  410          memory_file_to_codes(HMem, Codes, octet)
  411        ),
  412        free_memory_file(HMem)),
  413    phrase(base64(Codes), EncCodes),
  414    string_codes(Hex, EncCodes).
  415post_data_encoded(Bytes, Hex) :-
  416    string_codes(Hex, EncCodes),
  417    phrase(base64(Codes), EncCodes),
  418    string_codes(ZBytes, Codes),
  419    setup_call_cleanup(
  420        open_string(ZBytes, In),
  421        zopen(In, Zin, []),
  422        read_string(Zin, _, Bytes)).
 log_completed(+Code, +Status, +Bytes, +Id, +CPU, +Stream) is det
Write log message to Stream from a call_cleanup/3 call.
Arguments:
Status- 2nd argument of call_cleanup/3
Id- Term identifying the completed request
CPU0- CPU time at time of entrance
Stream- Stream to write to (normally from http_log_stream/1).
  433log_completed(Code, Status, Bytes, Id, CPU, Stream) :-
  434    is_stream(Stream),
  435    log_check_deleted(Stream),
  436    !,
  437    log(Code, Status, Bytes, Id, CPU, Stream).
  438log_completed(Code, Status, Bytes, Id, CPU0, _) :-
  439    http_log_stream(Stream),       % Logfile has changed!
  440    !,
  441    log_completed(Code, Status, Bytes, Id, CPU0, Stream).
  442log_completed(_,_,_,_,_,_).
  443
  444
  445log(Code, ok, Bytes, Id, CPU, Stream) :-
  446    !,
  447    format(Stream, 'completed(~q, ~q, ~q, ~q, ok).~n',
  448           [ Id, CPU, Bytes, Code ]).
  449log(Code, Status, Bytes, Id, CPU, Stream) :-
  450    (   map_exception(Status, Term)
  451    ->  true
  452    ;   message_to_string(Status, String),
  453        Term = error(String)
  454    ),
  455    format(Stream, 'completed(~q, ~q, ~q, ~q, ~W).~n',
  456           [ Id, CPU, Bytes, Code,
  457             Term, [ quoted(true),
  458                     ignore_ops(true),
  459                     blobs(portray),
  460                     portray_goal(write_blob)
  461                   ]
  462           ]).
  463
  464:- public write_blob/2.  465write_blob(Blob, _Options) :-
  466    format(string(S), '~q', [Blob]),
  467    writeq(blob(S)).
  468
  469map_exception(http_reply(bytes(ContentType,Bytes),_), bytes(ContentType,L)) :-
  470    string_length(Bytes, L).        % also does lists
  471map_exception(http_reply(Reply), Reply).
  472map_exception(http_reply(Reply, _), Reply).
  473map_exception(error(existence_error(http_location, Location), _Stack),
  474              error(404, Location)).
  475
  476
  477                 /*******************************
  478                 *      LOGROTATE SUPPORT       *
  479                 *******************************/
 log_check_deleted(+Stream) is semidet
If the link-count of the stream has dropped to zero, the file has been deleted/moved. In this case the log file is closed and log_check_deleted/6 will open a new one. This provides some support for cleaning up the logfile without shutting down the server.
See also
- logrotate(1) to manage logfiles on Unix systems.
  491log_check_deleted(Stream) :-
  492    stream_property(Stream, nlink(Links)),
  493    Links == 0,
  494    !,
  495    http_log_close(log_file_deleted),
  496    fail.
  497log_check_deleted(_).
 http_logrotate(+Options) is det
Rotate the available log files. Note that there are two ways to deal with the rotation of log files:
  1. Use the OS log rotation facility. In that case the OS must (1) move the logfile and (2) have something calling http_log_close/1 to close the (moved) file and make this server create a new one on the next log message. If library(http/http_unix_daemon) is used, closing is achieved by sending SIGHUP or SIGUSR1 to the process.
  2. Call this predicate at scheduled intervals. This can be achieved by calling http_schedule_logrotate/2 in the context of library(http/http_unix_daemon) which schedules the maintenance actions.

Options:

min_size(+Bytes)
Do not rotate if the log file is smaller than Bytes. The default is 1Mbytes.
keep_logs(+Count)
Number of rotated log files to keep (default 10)
compress_logs(+Format)
Compress the log files to the given format. Default gzip and this is currently the only supported compressor.
background(+Boolean)
If true, rotate the log files in the background.
  529http_logrotate(Options) :-
  530    select_option(background(true), Options, Options1),
  531    !,
  532    thread_create(http_logrotate(Options1), _,
  533                  [ alias('__logrotate'),
  534                    detached(true)
  535                  ]).
  536http_logrotate(Options) :-
  537    option(keep_logs(Keep), Options, 10),
  538    option(compress_logs(Format), Options, gzip),
  539    compress_extension(Format, ZExt),
  540    log_file_and_ext(Base, Ext),
  541    (   log_too_small(Base, Ext, Options)
  542    ->  true
  543    ;   rotate_logs(Base, Ext, ZExt, Keep)
  544    ).
  545
  546rotate_logs(Base, Ext, ZExt, N1) :-
  547    N1 > 0,
  548    !,
  549    N0 is N1 - 1,
  550    old_log_file(Base, Ext, N0, ZO, Old),
  551    (   exists_file(Old)
  552    ->  new_log_file(Base, Ext, N1, ZO, ZExt, ZN, New),
  553        rename_log_file(ZO, Old, ZN, New)
  554    ;   true
  555    ),
  556    rotate_logs(Base, Ext, ZExt, N0).
  557rotate_logs(_, _, _, _).
  558
  559rename_log_file(ZExt, Old, ZExt, New) :-
  560    !,
  561    debug(logrotate, 'Rename ~p --> ~p', [Old, New]),
  562    rename_file(Old, New).
  563rename_log_file('', Old, ZExt, New) :-
  564    file_name_extension(NoExt, ZExt, New),
  565    debug(logrotate, 'Rename ~p --> ~p', [Old, NoExt]),
  566    rename_file(Old, NoExt),
  567    debug(logrotate, 'Closing log file', []),
  568    http_log_close(logrotate),
  569    compress_extension(Format, ZExt),
  570    debug(logrotate, 'Compressing (~w) ~p', [Format, NoExt]),
  571    compress_file(NoExt, Format).
  572
  573old_log_file(Base, Ext, N, ZExt, File) :-
  574    log_file(Base, Ext, N, File0),
  575    (   compress_extension(_, ZExt),
  576        file_name_extension(File0, ZExt, File1),
  577        exists_file(File1)
  578    ->  File = File1
  579    ;   ZExt = '',
  580        File = File0
  581    ).
  582
  583new_log_file(Base, Ext, N, '', '', '', File) :-
  584    !,
  585    log_file(Base, Ext, N, File).
  586new_log_file(Base, Ext, N, '', ZExt, ZExt, File) :-
  587    !,
  588    log_file(Base, Ext, N, File0),
  589    file_name_extension(File0, ZExt, File).
  590new_log_file(Base, Ext, N, ZExt, _, ZExt, File) :-
  591    log_file(Base, Ext, N, File0),
  592    file_name_extension(File0, ZExt, File).
  593
  594log_file(Base, Ext, 0, File) :-
  595    !,
  596    file_name_extension(Base, Ext, File).
  597log_file(Base, Ext, N, File) :-
  598    atomic_list_concat([Base, -, N], Base1),
  599    file_name_extension(Base1, Ext, File).
  600
  601log_file_and_ext(Base, Ext) :-
  602    setting(http:logfile, Term),
  603    catch(absolute_file_name(Term, File,
  604                             [ access(exist)
  605                             ]), _, fail),
  606    file_name_extension(Base, Ext, File).
  607
  608log_too_small(Base, Ext, Options) :-
  609    DefMin is 1024*1024,
  610    option(min_size(MinBytes), Options, DefMin),
  611    file_name_extension(Base, Ext, File),
  612    (   exists_file(File)
  613    ->  size_file(File, Bytes),
  614        Bytes < MinBytes,
  615        debug(logrotate, '~w has ~D bytes; not rotating', [File, Bytes])
  616    ;   debug(logrotate, '~w does not exist; not rotating', [File])
  617    ).
 compress_file(+File, +Format)
Compress a file according to Format. Currently only supports gzip.
  624compress_file(File, Format) :-
  625    (   compress_extension(Format, Ext)
  626    ->  true
  627    ;   domain_error(compress_format, Format)
  628    ),
  629    file_name_extension(File, Ext, ZFile),
  630    catch(setup_call_cleanup(
  631              gzopen(ZFile, write, Out, [type(binary)]),
  632              setup_call_cleanup(
  633                  open(File, read, In, [type(binary)]),
  634                  copy_stream_data(In, Out),
  635                  close(In)),
  636              close(Out)),
  637          Error,
  638          ( print_message(error, Error),
  639            catch(delete_file(Out), _, true),
  640            throw(Error)
  641          )),
  642    delete_file(File).
  643
  644compress_extension(gzip, gz).
  645
  646:- dynamic
  647    scheduled_logrotate/2.  % Schedule, Options
 http_schedule_logrotate(When, Options)
Schedule log rotation based on maintenance broadcasts. When is one of:
daily(Hour:Min)
Run each day at Hour:Min. Min is rounded to a multitude of 5.
weekly(Day, Hour:Min)
Run at the given Day and Time each week. Day is either a number 1..7 (1 is Monday) or a weekday name or abbreviation.
monthly(DayOfTheMonth, Hour:Min)
Run each month at the given Day (1..31). Note that not all months have all days.

This must be used with a timer that broadcasts a maintenance(_,_) message (see broadcast/1). Such a timer is part of library(http/http_unix_daemon).

  668http_schedule_logrotate(When, Options) :-
  669    listen(maintenance(_,_), http_consider_logrotate),
  670    compile_schedule(When, Schedule),
  671    retractall(scheduled_logrotate(_,_)),
  672    asserta(scheduled_logrotate(Schedule, Options)).
  673
  674compile_schedule(Var, _) :-
  675    var(Var),
  676    !,
  677    instantiation_error(Var).
  678compile_schedule(daily(Time0), daily(Time)) :-
  679    compile_time(Time0, Time).
  680compile_schedule(weekly(Day0, Time0), weekly(Day, Time)) :-
  681    compile_weekday(Day0, Day),
  682    compile_time(Time0, Time).
  683compile_schedule(monthly(Day, Time0), monthly(Day, Time)) :-
  684    must_be(between(0, 31), Day),
  685    compile_time(Time0, Time).
  686
  687compile_time(HH:MM0, HH:MM) :-
  688    must_be(between(0, 23), HH),
  689    must_be(between(0, 59), MM0),
  690    MM is ((MM0+4)//5)*5.
  691
  692compile_weekday(N, _) :-
  693    var(N),
  694    !,
  695    instantiation_error(N).
  696compile_weekday(N, N) :-
  697    integer(N),
  698    !,
  699    must_be(between(1,7), N).
  700compile_weekday(Day, N) :-
  701    downcase_atom(Day, Lwr),
  702    (   sub_atom(Lwr, 0, 3, _, Abbr),
  703        day(N, Abbr)
  704    ->  !
  705    ;   domain_error(day, Day)
  706    ).
 http_consider_logrotate
Perform a log rotation if the schedule is met
  712http_consider_logrotate :-
  713    scheduled_logrotate(Schedule, Options),
  714    get_time(NowF),
  715    Now is round(NowF/60.0)*60,
  716    scheduled(Schedule, Now),
  717    !,
  718    http_logrotate([background(true)|Options]).
  719
  720scheduled(daily(HH:MM), Now) :-
  721    stamp_date_time(Now, DateTime, local),
  722    date_time_value(time, DateTime, time(HH,MM,_)).
  723scheduled(weekly(Day, Time), Now) :-
  724    stamp_date_time(Now, DateTime, local),
  725    date_time_value(date, DateTime, Date),
  726    day_of_the_week(Date, Day),
  727    scheduled(daily(Time), Now).
  728scheduled(monthly(Day, Time), Now) :-
  729    stamp_date_time(Now, DateTime, local),
  730    date_time_value(day, DateTime, Day),
  731    scheduled(daily(Time), Now).
  732
  733day(1, mon).
  734day(2, tue).
  735day(3, wed).
  736day(4, thu).
  737day(5, fri).
  738day(6, sat).
  739day(7, sun)