439 lines
12 KiB
Plaintext
439 lines
12 KiB
Plaintext
' ===================================================================
|
||
' File: logger.puml
|
||
' Purpose: Single source of truth for module-level activity + per-method sequences.
|
||
' Module: logger.js — Structured logger with level gating, buffer, anti-spam utilities.
|
||
' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation.
|
||
' ===================================================================
|
||
|
||
' Neutral defaults — typography/layout only (keeps partition colors intact)
|
||
skinparam Shadowing false
|
||
skinparam SequenceMessageAlign center
|
||
skinparam SequenceLifeLineBorderColor #666666
|
||
skinparam SequenceLifeLineBorderThickness 1
|
||
|
||
' ==== VIEW: Branch Flow (logger.js) ========================================
|
||
@startuml
|
||
title logger.js — Branch Flow (full module)
|
||
|
||
start
|
||
:logger;
|
||
|
||
fork
|
||
' -------- Logger.constructor() --------
|
||
partition "Logger.constructor()" #E7FAE3 {
|
||
:constructor;
|
||
:config = window.AI_REPO_CONFIG;\nbuffer=[]; loopCounts=Map(); startedAt=Date.now();
|
||
:setInterval(clean loopCounts if > 2× watchMs);
|
||
kill
|
||
}
|
||
fork again
|
||
' -------- error/warn/info/verbose/trace --------
|
||
partition "level helpers (error/warn/info/verbose/trace)" #FFF6D1 {
|
||
:error(msg,data) -> _log(1,'ERROR',...);
|
||
:warn(msg,data) -> _log(2,'WARN',...);
|
||
:info(msg,data) -> _log(3,'INFO',...);
|
||
:verbose(msg,data)-> _log(4,'VERBOSE',...);
|
||
:trace(msg,data) -> _log(5,'TRACE',...);
|
||
kill
|
||
}
|
||
fork again
|
||
' -------- command(action,status,extra) --------
|
||
partition "command(action, status, extra)" #FFE1DB {
|
||
:command;
|
||
:icon by status; info(`${icon} ${action} [${status}]`, extra);
|
||
kill
|
||
}
|
||
fork again
|
||
' -------- logLoop(kind, msg) --------
|
||
partition "logLoop(kind, msg)" #DCF9EE {
|
||
:logLoop;
|
||
:k=kind+msg; cur=loopCounts.get(k)||0;
|
||
:withinWatch = now-startedAt <= watchMs;
|
||
:if !withinWatch && kind!='WARN' -> return;
|
||
:if cur>=10 -> return;
|
||
:loopCounts.set(k,cur+1); suffix=(cur+1>1?` (${cur+1}x)`:'');
|
||
:route to error/warn/info(kind, msg+suffix);
|
||
kill
|
||
}
|
||
fork again
|
||
' -------- _log(levelNum, levelName, msg, data) --------
|
||
partition "_log(levelNum, levelName, msg, data)" #FFE6F0 {
|
||
:_log;
|
||
:enabled = !!config.debug.enabled;\nlevel = config.debug.level ?? 0;\nif !enabled or levelNum>level -> return;
|
||
:entry={ts, levelName, message, data:_sanitize(data)};
|
||
:buffer.push(entry); trim to debug.maxLines (default 400);
|
||
:console.log(`[AI RC ${levelName}]`, msg, optional sanitized data);
|
||
kill
|
||
}
|
||
fork again
|
||
' -------- _sanitize(data) --------
|
||
partition "_sanitize(data)" #E6F3FF {
|
||
:_sanitize;
|
||
:null -> null;\nHTMLElement -> "HTMLElement<TAG>";
|
||
:long string (>200) -> truncated + ellipsis;
|
||
:plain object -> shallow sanitize values recursively (HTMLElement/long string);
|
||
:else return data;
|
||
kill
|
||
}
|
||
fork again
|
||
' -------- getRecentLogs(n=50) --------
|
||
partition "getRecentLogs(n=50)" #F0E6FA {
|
||
:getRecentLogs;
|
||
:tail buffer n; map to lines: ISO LEVEL message {jsonData?}; join with \\n;
|
||
kill
|
||
}
|
||
fork again
|
||
' -------- setLevel(n) --------
|
||
partition "setLevel(n)" #E7FAF7 {
|
||
:setLevel;
|
||
:lv = clamp(n,0..5); config.set('debug.level', lv);\ninfo(`Log level => ${lv}`);
|
||
kill
|
||
}
|
||
fork again
|
||
' -------- bootstrap (global) --------
|
||
partition "bootstrap (global)" #FFF7E7 {
|
||
:window.AI_REPO_LOGGER = new Logger();
|
||
kill
|
||
}
|
||
end fork
|
||
@enduml
|
||
|
||
|
||
' ==== METHOD: Logger.constructor() =========================================
|
||
@startuml
|
||
title logger:constructor(): \n Prepare config, buffers, anti-spam counters, and cleanup timer
|
||
|
||
participant "Caller" as CL
|
||
participant "constructor()" as CTOR
|
||
participant "Config" as CFG
|
||
participant "Timer" as TMR
|
||
|
||
activate CL
|
||
CL -> CTOR : new Logger()
|
||
activate CTOR
|
||
|
||
CTOR -> CFG : window.AI_REPO_CONFIG
|
||
CFG --> CTOR : config
|
||
CTOR -> CTOR : buffer=[]; loopCounts=new Map(); startedAt=Date.now()
|
||
|
||
' periodic cleanup ~ every watchMs (default 120s)
|
||
CTOR -> CFG : get('debug.watchMs') || 120000
|
||
CFG --> CTOR : watchMs
|
||
CTOR -> TMR : setInterval(fn, watchMs)
|
||
TMR --> CTOR : id
|
||
CTOR -> CTOR : fn: if (Date.now()-startedAt > watchMs*2)\n loopCounts.clear(); startedAt=Date.now()
|
||
|
||
CTOR --> CL : instance
|
||
deactivate CTOR
|
||
deactivate CL
|
||
@enduml
|
||
|
||
|
||
' ==== METHOD: level helpers (error/warn/info/verbose/trace) ================
|
||
@startuml
|
||
title logger:level helpers: \n Route to _log() with appropriate numeric level
|
||
|
||
participant "Caller" as CL
|
||
participant "error()" as ERR
|
||
participant "warn()" as WRN
|
||
participant "info()" as INF
|
||
participant "verbose()" as VRB
|
||
participant "trace()" as TRC
|
||
participant "_log(...)" as LOG
|
||
|
||
activate CL
|
||
CL -> ERR : msg, data
|
||
ERR -> LOG : _log(1,'ERROR',msg,data)
|
||
LOG --> ERR : (void)
|
||
|
||
CL -> WRN : msg, data
|
||
WRN -> LOG : _log(2,'WARN',msg,data)
|
||
LOG --> WRN : (void)
|
||
|
||
CL -> INF : msg, data
|
||
INF -> LOG : _log(3,'INFO',msg,data)
|
||
LOG --> INF : (void)
|
||
|
||
CL -> VRB : msg, data
|
||
VRB -> LOG : _log(4,'VERBOSE',msg,data)
|
||
LOG --> VRB : (void)
|
||
|
||
CL -> TRC : msg, data
|
||
TRC -> LOG : _log(5,'TRACE',msg,data)
|
||
LOG --> TRC : (void)
|
||
|
||
TRC --> CL : done
|
||
deactivate TRC
|
||
deactivate CL
|
||
@enduml
|
||
|
||
|
||
' ==== METHOD: command(action, status, extra) ================================
|
||
@startuml
|
||
title logger:command(action, status, extra): \n Friendly lifecycle log with icon
|
||
|
||
participant "Caller" as CL
|
||
participant "command(action,status,extra)" as CMD
|
||
participant "info()" as INF
|
||
|
||
activate CL
|
||
CL -> CMD : action, status, extra
|
||
activate CMD
|
||
CMD -> CMD : icon = map(status)\n('👁️','📝','✓','⏳','⚙️','✅','❌','•')
|
||
CMD -> INF : info(`${icon} ${action} [${status}]`, extra)
|
||
INF --> CMD : (void)
|
||
CMD --> CL : (void)
|
||
deactivate CMD
|
||
deactivate CL
|
||
@enduml
|
||
|
||
|
||
' ==== METHOD: logLoop(kind, msg) ===========================================
|
||
@startuml
|
||
title logger:logLoop(kind, msg): \n Anti-spam logging for hot paths (max 10 per watch window)
|
||
|
||
participant "Caller" as CL
|
||
participant "logLoop(kind,msg)" as LLP
|
||
participant "Config" as CFG
|
||
participant "error()" as ERR
|
||
participant "warn()" as WRN
|
||
participant "info()" as INF
|
||
|
||
activate CL
|
||
CL -> LLP : kind, msg
|
||
activate LLP
|
||
|
||
LLP -> LLP : k=`${kind}:${msg}`; cur=loopCounts.get(k)||0
|
||
LLP -> CFG : get('debug.watchMs') || 120000
|
||
CFG --> LLP : watchMs
|
||
LLP -> LLP : withinWatch = (now-startedAt) <= watchMs
|
||
alt !withinWatch && kind != 'WARN'
|
||
LLP --> CL : (void)
|
||
deactivate LLP
|
||
deactivate CL
|
||
return
|
||
end
|
||
alt cur >= 10
|
||
LLP --> CL : (void)
|
||
deactivate LLP
|
||
deactivate CL
|
||
return
|
||
end
|
||
|
||
LLP -> LLP : loopCounts.set(k, cur+1);\nsuffix = (cur+1>1) ? ` (${cur+1}x)` : ''
|
||
alt kind == 'ERROR'
|
||
LLP -> ERR : error(msg+suffix)
|
||
ERR --> LLP : ok
|
||
else kind == 'WARN'
|
||
LLP -> WRN : warn(msg+suffix)
|
||
WRN --> LLP : ok
|
||
else
|
||
LLP -> INF : info(msg+suffix)
|
||
INF --> LLP : ok
|
||
end
|
||
|
||
LLP --> CL : (void)
|
||
deactivate LLP
|
||
deactivate CL
|
||
@enduml
|
||
|
||
|
||
' ==== METHOD: _log(levelNum, levelName, msg, data) =========================
|
||
@startuml
|
||
title logger:_log(levelNum, levelName, msg, data): \n Gate by config, buffer tail, print to console
|
||
|
||
participant "Caller" as CL
|
||
participant "_log(...)" as LOG
|
||
participant "Config" as CFG
|
||
participant "_sanitize(data)" as SAN
|
||
participant "Console" as CON
|
||
|
||
activate CL
|
||
CL -> LOG : levelNum, levelName, msg, data
|
||
activate LOG
|
||
|
||
LOG -> CFG : get('debug.enabled')
|
||
CFG --> LOG : enabled (bool)
|
||
LOG -> CFG : get('debug.level') ?? 0
|
||
CFG --> LOG : level (0..5)
|
||
|
||
alt !enabled or levelNum > level
|
||
LOG --> CL : (void)
|
||
deactivate LOG
|
||
deactivate CL
|
||
return
|
||
end
|
||
|
||
LOG -> SAN : _sanitize(data)
|
||
SAN --> LOG : safeData
|
||
|
||
LOG -> LOG : entry = {timestamp, level:levelName, message:String(msg), data:safeData}
|
||
LOG -> CFG : get('debug.maxLines') || 400
|
||
CFG --> LOG : maxLines
|
||
LOG -> LOG : buffer.push(entry); if buffer.length>maxLines -> splice head
|
||
|
||
' console output
|
||
alt with data
|
||
LOG -> CON : console.log(`[AI RC ${levelName}]`, msg, entry.data)
|
||
else no data
|
||
LOG -> CON : console.log(`[AI RC ${levelName}]`, msg)
|
||
end
|
||
CON --> LOG : printed
|
||
|
||
LOG --> CL : (void)
|
||
deactivate LOG
|
||
deactivate CL
|
||
@enduml
|
||
|
||
|
||
' ==== METHOD: _sanitize(data) ==============================================
|
||
@startuml
|
||
title logger:_sanitize(data): \n Redact DOM elements, truncate long strings, shallow-sanitize objects
|
||
|
||
participant "Caller" as CL
|
||
participant "_sanitize(data)" as SAN
|
||
participant "HTMLElement" as HTM
|
||
|
||
activate CL
|
||
CL -> SAN : data
|
||
activate SAN
|
||
|
||
alt !data
|
||
SAN --> CL : null
|
||
deactivate SAN
|
||
deactivate CL
|
||
return
|
||
end
|
||
|
||
alt data instanceof HTMLElement
|
||
SAN -> HTM : tagName
|
||
HTM --> SAN : TAG
|
||
SAN --> CL : "HTMLElement<TAG>"
|
||
deactivate SAN
|
||
deactivate CL
|
||
return
|
||
end
|
||
|
||
alt typeof data === 'string' && length > 200
|
||
SAN --> CL : data.slice(0,200) + '…'
|
||
deactivate SAN
|
||
deactivate CL
|
||
return
|
||
end
|
||
|
||
alt typeof data === 'object'
|
||
SAN -> SAN : out = {}
|
||
loop for each [k,v] of Object.entries(data)
|
||
alt v instanceof HTMLElement
|
||
SAN -> SAN : out[k] = `HTMLElement<${v.tagName}>`
|
||
else typeof v === 'string' && v.length>200
|
||
SAN -> SAN : out[k] = v.slice(0,200) + '…'
|
||
else
|
||
SAN -> SAN : out[k] = v
|
||
end
|
||
end
|
||
SAN --> CL : out
|
||
else other primitive
|
||
SAN --> CL : data
|
||
end
|
||
|
||
deactivate SAN
|
||
deactivate CL
|
||
@enduml
|
||
|
||
|
||
' ==== METHOD: getRecentLogs(n=50) ==========================================
|
||
@startuml
|
||
title logger:getRecentLogs(n): \n Tail buffer to plain-text lines for copy/export
|
||
|
||
participant "Caller" as CL
|
||
participant "getRecentLogs(n)" as GRL
|
||
|
||
activate CL
|
||
CL -> GRL : n (default 50)
|
||
activate GRL
|
||
|
||
GRL -> GRL : tail = buffer.slice(-n)
|
||
GRL -> GRL : lines = tail.map(e => `${e.timestamp} ${e.level.padEnd(7)} ${e.message}${e.data ? ' ' + JSON.stringify(e.data) : ''}`)
|
||
GRL --> CL : lines.join('\\n')
|
||
deactivate GRL
|
||
deactivate CL
|
||
@enduml
|
||
|
||
|
||
' ==== METHOD: setLevel(n) ==================================================
|
||
@startuml
|
||
title logger:setLevel(n): \n Clamp and persist numeric level, then self-log the change
|
||
|
||
participant "Caller" as CL
|
||
participant "setLevel(n)" as SLV
|
||
participant "Config" as CFG
|
||
participant "info()" as INF
|
||
|
||
activate CL
|
||
CL -> SLV : n
|
||
activate SLV
|
||
|
||
SLV -> SLV : lv = Math.max(0, Math.min(5, n))
|
||
SLV -> CFG : set('debug.level', lv)
|
||
CFG --> SLV : ok
|
||
SLV -> INF : info(`Log level => ${lv}`)
|
||
INF --> SLV : logged
|
||
|
||
SLV --> CL : (void)
|
||
deactivate SLV
|
||
deactivate CL
|
||
@enduml
|
||
|
||
|
||
' ==== METHOD: bootstrap (global) ===========================================
|
||
@startuml
|
||
title logger:bootstrap: \n Expose singleton logger on window
|
||
|
||
participant "Caller" as CL
|
||
participant "bootstrap" as BOOT
|
||
participant "Logger()" as CTOR
|
||
participant "window" as WIN
|
||
|
||
activate CL
|
||
CL -> BOOT : module load
|
||
activate BOOT
|
||
BOOT -> CTOR : new Logger()
|
||
CTOR --> BOOT : instance
|
||
BOOT -> WIN : window.AI_REPO_LOGGER = instance
|
||
WIN --> BOOT : ok
|
||
BOOT --> CL : (void)
|
||
deactivate BOOT
|
||
deactivate CL
|
||
@enduml
|
||
|
||
|
||
' ==== LEGEND ===============================================================
|
||
@startuml
|
||
legend bottom
|
||
== logger UML Style Guide (for future edits) ==
|
||
• Scope: One .puml per module. Keep two views:
|
||
(1) Activity "Branch Flow" for the whole module (partitions + soft colors),
|
||
(2) Per-function/Per-method Sequence diagrams for each exported or significant internal method.
|
||
|
||
• Sequence conventions:
|
||
1) First participant is the external caller (use "Caller" or a concrete origin).
|
||
2) Do NOT add a module/class lifeline; the name appears in the title only.
|
||
3) Include every directly-called helper/system as a participant
|
||
(e.g., "info()", "_log()", "_sanitize()", "Config", "Console", "Timer", "HTMLElement").
|
||
4) Prefer simple messages; Use --> for returns; -> for calls.
|
||
5) Use activate/deactivate as you see fit for clarity.
|
||
6) Use alt blocks only when branches meaningfully change the message flow.
|
||
|
||
• Activity view conventions:
|
||
A) Start with module node then fork partitions for each function/method.
|
||
B) One partition per function; soft background color; terminate branches with 'kill'.
|
||
C) Keep wording aligned with code (e.g., watchMs default 120000, maxLines default 400, 10x cap in logLoop).
|
||
|
||
• Color palette (soft pastels)
|
||
• Use --> for returns; -> for calls.
|
||
• Participants use quoted method names for internals; plain nouns for systems ("Config", "Console", "Timer").
|
||
• Keep this legend at the end of the file to standardize edits.
|
||
endlegend
|
||
@enduml
|