luat-log.lmt /size: 22 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['luat-log'] = {
2    version   = 1.001,
3    comment   = "companion to trac-log.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- In fact all writes could go through lua and we could write the console and
10-- terminal handler in lua then. Ok, maybe it's slower then, so a no-go.
11
12-- This used to be combined in trac-log but as we also split between mkiv and lmtx
13-- we now have dedicated files. A side effect is a smaller format and a smaller
14-- mtxrun.
15--
16-- We use different targets: "terminal" "logfile", "both" and have no number
17-- channel. Actually, the log channels are now numeric (1,2,3) as with other
18-- symbolics.
19
20local next, type, select, print = next, type, select, print
21local format, gmatch, find = string.format, string.gmatch, string.find
22local concat, insert, remove = table.concat, table.insert, table.remove
23local topattern = string.topattern
24local utfchar = utf.char
25
26local writenl = texio.writeselectornl
27local writelf = texio.writeselectorlf
28local write   = texio.writeselector
29
30local setmetatableindex = table.setmetatableindex
31local formatters        = string.formatters
32local settings_to_hash  = utilities.parsers.settings_to_hash
33local sortedkeys        = table.sortedkeys
34
35-- variant is set now
36
37local variant = "default"
38----- variant = "ansi"
39
40logs       = logs or { }
41local logs = logs
42
43-- we extend the formatters:
44
45formatters.add (
46    formatters, "unichr",
47    [["U+" .. format("%%05X",%s) .. " (" .. utfchar(%s) .. ")"]]
48)
49
50formatters.add (
51    formatters, "chruni",
52    [[utfchar(%s) .. " (U+" .. format("%%05X",%s) .. ")"]]
53)
54
55-- basic loggers
56
57local function ignore() end
58
59setmetatableindex(logs, function(t,k) t[k] = ignore ; return ignore end)
60
61local report, subreport, status, settarget, setformats, settranslations
62
63local direct, subdirect, writer, pushtarget, poptarget, setlogfile, settimedlog, setprocessor, setformatters, newline
64
65-- we use formatters but best check for % then because for simple messages but
66-- we don't want this overhead for single messages (not that there are that
67-- many; we could have a special weak table)
68
69local function ansisupported(specification)
70    if specification ~= "ansi" and specification ~= "ansilog" then
71        return false
72    elseif os and os.enableansi then
73        return os.enableansi()
74    else
75        return false
76    end
77end
78
79do
80
81    if arg and ansisupported then
82        -- we're don't have environment.arguments yet
83        for k, v in next, arg do -- k can be negative !
84            if v == "--ansi" or v == "--c:ansi" then
85                if ansisupported("ansi") then
86                    variant = "ansi"
87                end
88                break
89            elseif v == "--ansilog" or v == "--c:ansilog" then
90                if ansisupported("ansilog") then
91                    variant = "ansilog"
92                end
93                break
94            end
95        end
96    end
97
98    local whereto      = "both"
99    local target       = nil
100    local targets      = nil
101
102    local formats      = table.setmetatableindex("self")
103    local translations = table.setmetatableindex("self")
104
105    local report_yes, subreport_yes, direct_yes, subdirect_yes, status_yes
106    local report_nop, subreport_nop, direct_nop, subdirect_nop, status_nop
107
108    -- texio.getselectorvalues()
109
110    local variants = {
111        default = {
112            formats = {
113                report_yes    = formatters["%-15s > %s\n"],
114                report_nop    = formatters["%-15s >\n"],
115                direct_yes    = formatters["%-15s > %s"],
116                direct_nop    = formatters["%-15s >"],
117                subreport_yes = formatters["%-15s > %s > %s\n"],
118                subreport_nop = formatters["%-15s > %s >\n"],
119                subdirect_yes = formatters["%-15s > %s > %s"],
120                subdirect_nop = formatters["%-15s > %s >"],
121                status_yes    = formatters["%-15s : %s\n"],
122                status_nop    = formatters["%-15s :\n"],
123            },
124            targets = setmetatableindex( { -- 1, 2, 3,
125                logfile  = 2,
126                log      = 2,
127                file     = 2,
128                console  = 1,
129                terminal = 1,
130                both     = 3,
131            },
132            function(t,k)
133                local v = 3 t[k] = v return v
134            end),
135        },
136        ansi = {
137            formats = {
138                report_yes    = formatters["%-15s > %s\n"],
139                report_nop    = formatters["%-15s >\n"],
140                direct_yes    = formatters["%-15s > %s"],
141                direct_nop    = formatters["%-15s >"],
142                subreport_yes = formatters["%-15s > %s > %s\n"],
143                subreport_nop = formatters["%-15s > %s >\n"],
144                subdirect_yes = formatters["%-15s > %s > %s"],
145                subdirect_nop = formatters["%-15s > %s >"],
146                status_yes    = formatters["%-15s : %s\n"],
147                status_nop    = formatters["%-15s :\n"],
148            },
149            targets = setmetatableindex( { 1, 1, 1,
150                logfile  = false,
151                log      = false,
152                file     = false,
153                console  = 1,
154                terminal = 1,
155                both     = 1,
156            }, function(t,k) local v = 1 t[k] = v return v end),
157        }
158    }
159
160    variants.ansilog = {
161        formats = variants.ansi.formats,
162        targets = variants.default.targets,
163    }
164
165-- do
166--     local iosize   = 512
167--     local iomode   = "line"
168--     local ioflush  = io.flush
169--     local iooutput = io.output
170--     iooutput():setvbuf(iomode,iosize)
171--     io.flush = function()
172--     --         ioflush()
173--     --         iooutput():setvbuf(iomode,iosize)
174--     end
175-- end
176
177    logs.flush = io.flush
178
179    writer = function(...)
180        if target then
181            writenl(target,...)
182        end
183    end
184
185    newline = function()
186        if target then
187            writelf(target)
188         -- writenl(target,"")
189        end
190    end
191
192    report = function(a,b,c,...)
193        if not target then
194            -- ignore
195        elseif c ~= nil then
196            writenl(target,report_yes(translations[a],formatters[formats[b]](c,...)))
197        elseif b then
198            writenl(target,report_yes(translations[a],formats[b]))
199        elseif a then
200            writenl(target,report_nop(translations[a]))
201        else
202            writenl(target)
203        end
204    end
205
206    direct = function(a,b,c,...)
207        if not target then
208            return ""
209        elseif c ~= nil then
210            return direct_yes(translations[a],formatters[formats[b]](c,...))
211        elseif b then
212            return direct_yes(translations[a],formats[b])
213        elseif a then
214            return direct_nop(translations[a])
215        else
216            return ""
217        end
218    end
219
220    subreport = function(a,s,b,c,...)
221        if not target then
222            -- ignore
223        elseif c ~= nil then
224            writenl(target,subreport_yes(translations[a],translations[s],formatters[formats[b]](c,...)))
225        elseif b then
226            writenl(target,subreport_yes(translations[a],translations[s],formats[b]))
227        elseif a then
228            writenl(target,subreport_nop(translations[a],translations[s]))
229        else
230            writenl(target)
231        end
232    end
233
234    subdirect = function(a,s,b,c,...)
235        if not target then
236            return ""
237        elseif c ~= nil then
238            return subdirect_yes(translations[a],translations[s],formatters[formats[b]](c,...))
239        elseif b then
240            return subdirect_yes(translations[a],translations[s],formats[b])
241        elseif a then
242            return subdirect_nop(translations[a],translations[s])
243        else
244            return ""
245        end
246    end
247
248    status = function(a,b,c,...)
249        if not target then
250            -- ignore
251        elseif c ~= nil then
252            writenl(target,status_yes(translations[a],formatters[formats[b]](c,...)))
253        elseif b then
254            writenl(target,status_yes(translations[a],formats[b]))
255        elseif a then
256            writenl(target,status_nop(translations[a]))
257        else
258            writenl(target)
259        end
260    end
261
262    settarget = function(askedwhereto)
263        whereto = askedwhereto or whereto or "both"
264        target = targets[whereto]
265        if not target then
266            whereto = "both"
267            target  = targets[whereto]
268        end
269        if target == targets.both or target == targets.terminal then
270            logs.flush = io.flush
271        else
272            logs.flush = ignore
273        end
274    end
275
276    local stack = { }
277
278    pushtarget = function(newtarget)
279        insert(stack,target)
280        settarget(newtarget)
281    end
282
283    poptarget = function()
284        if #stack > 0 then
285            settarget(remove(stack))
286        end
287    end
288
289    setformats = function(f)
290        formats = f
291    end
292
293    settranslations = function(t)
294        translations = t
295    end
296
297    setprocessor = function(f)
298        local writeline = writenl
299        writenl = function(target,...)
300            if target then
301                writeline(target,f(...))
302            end
303        end
304    end
305
306    setformatters = function(specification)
307        local t = nil
308        local f = nil
309        local d = variants.default
310        if not specification then
311            --
312        elseif type(specification) == "table" then
313            t = specification.targets
314            f = specification.formats or specification
315        else
316            if not ansisupported(specification) then
317                specification = "default"
318            end
319            local v = variants[specification]
320            if v then
321                t = v.targets
322                f = v.formats
323                variant = specification
324            end
325        end
326        targets = t or d.targets
327        target = targets[whereto]
328        if f then
329            d = d.formats
330        else
331            f = d.formats
332            d = f
333        end
334        setmetatableindex(f,d)
335        report_yes    = f.report_yes
336        report_nop    = f.report_nop
337        subreport_yes = f.subreport_yes
338        subreport_nop = f.subreport_nop
339        direct_yes    = f.direct_yes
340        direct_nop    = f.direct_nop
341        subdirect_yes = f.subdirect_yes
342        subdirect_nop = f.subdirect_nop
343        status_yes    = f.status_yes
344        status_nop    = f.status_nop
345        settarget(whereto)
346    end
347
348    setformatters(variant)
349
350    setlogfile  = ignore
351    settimedlog = ignore
352
353end
354
355-- io.output():setvbuf("line",1024)
356
357logs.report          = report
358logs.subreport       = subreport
359logs.status          = status
360logs.settarget       = settarget
361logs.pushtarget      = pushtarget
362logs.poptarget       = poptarget
363logs.setformats      = setformats
364logs.settranslations = settranslations
365
366logs.setlogfile      = setlogfile
367logs.settimedlog     = settimedlog
368logs.setprocessor    = setprocessor
369logs.setformatters   = setformatters
370
371logs.direct          = direct
372logs.subdirect       = subdirect
373logs.writer          = writer
374logs.newline         = newline
375
376local data   = { }
377local states = nil
378local force  = false
379
380function logs.reporter(category,subcategory)
381    local logger = data[category]
382    if not logger then
383        local state = states == true
384        if not state and type(states) == "table" then
385            for c, _ in next, states do
386                if find(category,c) then
387                    state = true
388                    break
389                end
390            end
391        end
392        logger = {
393            reporters = { },
394            state     = state,
395        }
396        data[category] = logger
397    end
398    local reporter = logger.reporters[subcategory or "default"]
399    if not reporter then
400        if subcategory then
401            reporter = function(...)
402                if force or not logger.state then
403                    subreport(category,subcategory,...)
404                end
405            end
406            logger.reporters[subcategory] = reporter
407        else
408            local tag = category
409            reporter = function(...)
410                if force or not logger.state then
411                    report(category,...)
412                end
413            end
414            logger.reporters.default = reporter
415        end
416    end
417    return reporter
418end
419
420logs.new = logs.reporter -- for old times sake
421
422-- context specicific: this ends up in the macro stream
423
424local ctxreport = logs.writer
425
426function logs.setmessenger(m)
427    ctxreport = m
428end
429
430function logs.messenger(category,subcategory)
431    -- we need to avoid catcode mess (todo: fast context)
432    if subcategory then
433        return function(...)
434            ctxreport(subdirect(category,subcategory,...))
435        end
436    else
437        return function(...)
438            ctxreport(direct(category,...))
439        end
440    end
441end
442
443-- so far
444
445local function setblocked(category,value) -- v.state == value == true : disable
446    if category == true or category == "all" then
447        -- lock all
448        category, value = "*", true
449    elseif category == false then
450        -- unlock all
451        category, value = "*", false
452    elseif value == nil then
453        -- lock selective
454        value = true
455    end
456    if category == "*" then
457        states = value
458        for k, v in next, data do
459            v.state = value
460        end
461    else
462        alllocked = false
463        states = settings_to_hash(category,type(states)=="table" and states or nil)
464        for c in next, states do
465            local v = data[c]
466            if v then
467                v.state = value
468            else
469                local p = topattern(c,true,true)
470                for k, v in next, data do
471                    if find(k,p) then
472                        v.state = value
473                    end
474                end
475            end
476        end
477    end
478end
479
480function logs.disable(category,value)
481    setblocked(category,value == nil and true or value)
482end
483
484function logs.enable(category)
485    setblocked(category,false)
486end
487
488function logs.categories()
489    return sortedkeys(data)
490end
491
492function logs.show()
493    local n, c, s, max = 0, 0, 0, 0
494    for category, v in table.sortedpairs(data) do
495        n = n + 1
496        local state = v.state
497        local reporters = v.reporters
498        local nc = #category
499        if nc > c then
500            c = nc
501        end
502        for subcategory, _ in next, reporters do
503            local ns = #subcategory
504            if ns > c then
505                s = ns
506            end
507            local m = nc + ns
508            if m > max then
509                max = m
510            end
511        end
512        local subcategories = concat(sortedkeys(reporters),", ")
513        if state == true then
514            state = "disabled"
515        elseif state == false then
516            state = "enabled"
517        else
518            state = "unknown"
519        end
520        -- no new here
521        report("logging","category %a, subcategories %a, state %a",category,subcategories,state)
522    end
523    report("logging","categories: %s, max category: %s, max subcategory: %s, max combined: %s",n,c,s,max)
524end
525
526local delayed_reporters = { }
527
528setmetatableindex(delayed_reporters,function(t,k)
529    local v = logs.reporter(k.name)
530    t[k] = v
531    return v
532end)
533
534function utilities.setters.report(setter,...)
535    delayed_reporters[setter](...)
536end
537
538directives.register("logs.blocked", function(v)
539    setblocked(v,true)
540end)
541
542directives.register("logs.target", function(v)
543    settarget(v)
544end)
545
546do
547
548    local report      = logs.reporter("pages") -- not needed but saves checking when we grep for it
549    local texgetcount = tex and tex.getcount
550
551    local real, user, sub = 0, 0, 0
552
553    function logs.start_page_number()
554        real = texgetcount("realpageno")
555        user = texgetcount("userpageno")
556        sub  = texgetcount("subpageno")
557    end
558
559    local timing   = false
560    local usage    = false
561    local lasttime = nil
562
563    logs.private = {
564        enablepagetiming = function()
565            usage = true
566        end,
567        getpagetiming = function()
568            return type(usage) == "table" and usage
569        end,
570    }
571
572    trackers.register("pages.timing", function() timing = "" end)
573
574    function logs.stop_page_number() -- the first page can includes the initialization so we omit this in average
575        if timing or usage then
576            local elapsed = statistics.currenttime(statistics)
577            local average, page
578            if not lasttime or real < 2 then
579                average = elapsed
580                page    = elapsed
581            else
582                average = elapsed / (real - 1)
583                page    = elapsed - lasttime
584            end
585            lasttime = elapsed
586            if timing then
587                timing = formatters[", total %0.03f, page %0.03f, average %0.03f"](elapsed,page,average)
588            end
589            if usage then
590                usage = {
591                    page = {
592                        real = real,
593                        user = user,
594                        sub  = sub,
595                    },
596                    time = {
597                        elapsed = elapsed,
598                        page    = page,
599                        average = average,
600                    }
601                }
602            end
603        end
604        if real <= 0 then
605            report("flushing page%s",timing)
606        elseif user <= 0 then
607            report("flushing realpage %s%s",real,timing)
608        elseif sub <= 0 then
609            report("flushing realpage %s, userpage %s%s",real,user,timing)
610        else
611            report("flushing realpage %s, userpage %s, subpage %s%s",real,user,sub,timing)
612        end
613        logs.flush()
614    end
615
616end
617
618-- we don't have show_open and show_close callbacks yet
619
620do
621
622    local texerror = tex and tex.error or print
623
624    function logs.texerrormessage(fmt,first,...) -- for the moment we put this function here
625        texerror(first and formatters[fmt](first,...) or fmt)
626    end
627
628end
629
630-- this is somewhat slower but prevents out-of-order messages when print is mixed
631-- with texio.write
632
633-- io.stdout:setvbuf('no')
634-- io.stderr:setvbuf('no')
635
636-- windows: > nul  2>&1
637-- unix   : > null 2>&1
638
639if package.helpers.report then
640    package.helpers.report = logs.reporter("package loader") -- when used outside mtxrun
641end
642
643-- logs.errors=missing references,missing characters
644-- logs.errors=characters
645-- logs.errors=missing
646-- logs.errors=*
647-- logs.quitonerror=missing modules
648
649do
650
651    local finalactions  = { }
652    local fatalerrors   = { }
653    local possiblefatal = { }
654    local quitonerror   = { }
655    local loggingerrors = false
656
657    function logs.loggingerrors()
658        return loggingerrors
659    end
660
661    local function register(v)
662        loggingerrors = v
663        if type(v) == "string" then
664            local target = settings_to_hash(v)
665            for k, v in next, target do
666                target[k] = string.topattern(k)
667            end
668            return target
669        else
670            return { }
671        end
672    end
673
674    directives.register("logs.errors",     function(v) fatalerrors = register(v) end)
675    directives.register("logs.quitonerror",function(v) quitonerror = register(v) end)
676
677    function logs.registerfinalactions(...)
678        insert(finalactions,...) -- so we can force an order if needed
679    end
680
681    local what   = nil
682    local report = nil
683    local state  = nil
684    local target = nil
685    local fatal  = false
686
687    local function startlogging(t,r,w,s)
688        target = t
689        state  = force
690        force  = true
691        report = type(r) == "function" and r or logs.reporter(r)
692        what   = w
693        pushtarget(target)
694        newline()
695        if s then
696            report("start %s: %s",what,s)
697        else
698            report("start %s",what or "")
699        end
700        if target == "logfile" then
701            newline()
702        end
703        return report
704    end
705
706    local function stoplogging()
707        if target == "logfile" then
708            newline()
709        end
710        report("stop %s",what or "")
711        if target == "logfile" then
712            newline()
713        end
714        poptarget()
715        state = oldstate
716        if fatal then
717            logs.report("error logging","error marked as fatal")
718            luatex.abort()
719        end
720    end
721
722    function logs.startfilelogging(...)
723        return startlogging("logfile", ...)
724    end
725
726    logs.stopfilelogging = stoplogging
727
728    local done = false
729
730    function logs.starterrorlogging(r,w,...)
731        if not done then
732            pushtarget("terminal")
733            newline()
734            logs.report("error logging","start possible issues")
735            poptarget()
736            done = true
737        end
738        if fatalerrors[w] then
739            possiblefatal[w] = true
740        else
741            for k, v in next, quitonerror do
742                if find(w,v) then
743                    fatal = true
744                end
745            end
746            for k, v in next, fatalerrors do
747                if find(w,v) then
748                    possiblefatal[w] = true
749                    break
750                end
751            end
752        end
753        return startlogging("terminal",r,w,...)
754    end
755
756    logs.stoperrorlogging = stoplogging
757
758    function logs.finalactions()
759        if #finalactions > 0 then
760            for i=1,#finalactions do
761                finalactions[i]()
762            end
763            if done then
764                pushtarget("terminal")
765                newline()
766                logs.report("error logging","stop possible issues")
767                poptarget()
768            end
769            return next(possiblefatal) and sortedkeys(possiblefatal) or false
770        end
771    end
772
773end
774
775-- just in case we load from context
776
777local dummy = function() end
778
779function logs.application(t)
780    return {
781        name     = t.name or tex.jobname,
782        banner   = t.banner,
783        report   = logs.reporter(t.name),
784        moreinfo = dummy,
785        export   = dummy,
786        help     = dummy,
787        identify = dummy,
788        version  = dummy,
789    }
790end
791
792-- for old times sake
793
794do
795
796    local datetime = os.date
797    local sleep = os.sleep
798    local openfile = io.open
799
800    local f_syslog = formatters["%s %s => %s => %s => %s\r"]
801
802    function logs.system(whereto,process,jobname,category,fmt,arg,...)
803        local message = f_syslog(datetime("%d/%m/%y %H:%m:%S"),process,jobname,category,arg == nil and fmt or format(fmt,arg,...))
804        for i=1,10 do
805            local f = openfile(whereto,"a") -- we can consider keeping the file open
806            if f then
807                f:write(message)
808                f:close()
809                break
810            else
811                sleep(0.1)
812            end
813        end
814    end
815
816end
817