Skip to main content
obsidian-observability Implement structured logging, metrics, error tracking, and a debug panel
for Obsidian plugins. Use when adding debug logging, tracking plugin
performance, building a diagnostics view, or setting up error reporting.
Trigger with phrases like "obsidian logging", "obsidian monitoring",
"obsidian debug panel", "track obsidian plugin performance".
설치로 이동 Skills Marketplace 커뮤니티가 만든 AI 스킬을 발견하고 탐색하세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/jeremylongshore/claude-code-plugins-plus-skills --skill obsidian-observability명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Zip 다운로드 다운로드 중... 이 저장소의 다른 Skills Implement user sign-up and sign-in flows with Clerk.
Use when building authentication UI, customizing sign-in experience,
or implementing OAuth social login.
Trigger with phrases like "clerk sign-in", "clerk sign-up",
"clerk login flow", "clerk OAuth", "clerk social login".
Implement session management and middleware with Clerk.
Use when managing user sessions, configuring route protection,
or implementing token refresh and custom JWT templates.
Trigger with phrases like "clerk session", "clerk middleware",
"clerk route protection", "clerk token", "clerk JWT".
Configure enterprise SSO, role-based access control, and organization management.
Use when implementing SSO integration, configuring role-based permissions,
or setting up organization-level controls.
Trigger with phrases like "clerk SSO", "clerk RBAC",
"clerk enterprise", "clerk roles", "clerk permissions", "clerk organizations".
jeremylongshore
jeremylongshore/claude-code-plugins-plus-skills
GitHub 저장소 열기 name obsidian-observability description Implement structured logging, metrics, error tracking, and a debug panel
for Obsidian plugins. Use when adding debug logging, tracking plugin
performance, building a diagnostics view, or setting up error reporting.
Trigger with phrases like "obsidian logging", "obsidian monitoring",
"obsidian debug panel", "track obsidian plugin performance".
allowed-tools Read, Write, Edit, Bash(npm:*) version 1.13.0 license MIT author Jeremy Longshore <jeremy@intentsolutions.io> tags ["obsidian","monitoring","debugging","performance","logging"] compatibility Designed for Claude Code, also compatible with Codex and OpenClaw
Obsidian Observability
Overview
Implement production observability for Obsidian plugins: a structured logger with levels and ring buffer history, a metrics collector with counters/gauges/timers, an error tracker with deduplication, and a debug sidebar panel that displays all of it in real time. Every component is copy-pasteable and uses only Obsidian's built-in APIs.
Prerequisites
Working Obsidian plugin (see obsidian-core-workflow-a)
TypeScript strict mode enabled
Familiarity with ItemView for the debug panel
Instructions
Step 1: Structured Logger with Levels and History
type LogLevel = 'debug' | 'info' | 'warn' | 'error' ;
const LEVEL_PRIORITY : Record <LogLevel , number > = {
debug : 0 , info : 1 , warn : 2 , error : 3 ,
};
interface LogEntry {
timestamp : number ;
level : LogLevel ;
message : string ;
data ?: unknown ;
}
export class Logger {
private history : LogEntry [] = [];
private maxHistory = 200 ;
private level : ;
: ;
( ) {
. = ;
. = level;
}
( ) { . = level; }
( ) { . ( , msg, data); }
( ) { . ( , msg, data); }
( ) { . ( , msg, data); }
( ) { . ( , msg, data); }
( : ): {
start = performance. ();
{
ms = performance. () - start;
. ( );
ms;
};
}
( ?: ): [] {
count ? . . (-count) : [... . ];
}
(): {
. ( . , , );
}
( ) {
( [level] < [ . ]) ;
: = { : . (), level, message, data };
. . (entry);
( . . > . ) {
. . ( , . . - . );
}
fn = level === ? .
: level === ? .
: level === ? .
: . ;
(data !== ) {
( . , message, data);
} {
( . , message);
}
}
}
LogLevel
private
prefix
string
constructor
pluginId : string , level : LogLevel = 'info'
this
prefix
`[${pluginId} ]`
this
level
setLevel
level : LogLevel
this
level
debug
msg : string , data ?: unknown
this
log
'debug'
info
msg : string , data ?: unknown
this
log
'info'
warn
msg : string , data ?: unknown
this
log
'warn'
error
msg : string , data ?: unknown
this
log
'error'
time
label
string
() =>
number
const
now
return
() =>
const
now
this
debug
`${label} (${ms.toFixed(2 )} ms)`
return
getHistory
count
number
LogEntry
return
this
history
slice
this
history
export
string
return
JSON
stringify
this
history
null
2
private
log
level : LogLevel , message : string , data ?: unknown
if
LEVEL_PRIORITY
LEVEL_PRIORITY
this
level
return
const
entry
LogEntry
timestamp
Date
now
this
history
push
if
this
history
length
this
maxHistory
this
history
splice
0
this
history
length
this
maxHistory
const
'debug'
console
debug
'warn'
console
warn
'error'
console
error
console
log
if
undefined
fn
this
prefix
else
fn
this
prefix
Step 2: Metrics Collector with Counters, Gauges, and Timers
interface TimerStats {
count : number ;
total : number ;
min : number ;
max : number ;
values : number [];
}
export class MetricsCollector {
private counters = new Map <string , number >();
private gauges = new Map <string , number >();
private timers = new Map <string , TimerStats >();
increment (name : string , amount = 1 ) {
this .counters .set (name, (this .counters .get (name) ?? 0 ) + amount);
}
getCounter (name : string ): number {
return this .counters .get (name) ?? 0 ;
}
setGauge (name : string , value : number ) {
this .gauges .set (name, value);
}
getGauge (name : string ): number {
return this .gauges .get (name) ?? 0 ;
}
recordTime (name : string , ms : number ) {
let stats = this .timers .get (name);
if (!stats) {
stats = { count : 0 , total : 0 , min : Infinity , max : 0 , values : [] };
this .timers .set (name, stats);
}
stats.count ++;
stats.total += ms;
stats.min = Math .min (stats.min , ms);
stats.max = Math .max (stats.max , ms);
stats.values .push (ms);
if (stats.values .length > 100 ) stats.values .shift ();
}
async timeAsync<T>(name : string , fn : () => Promise <T>): Promise <T> {
const start = performance.now ();
try {
return await fn ();
} finally {
this .recordTime (name, performance.now () - start);
}
}
getTimerStats (name : string ): { avg : number ; p95 : number ; min : number ; max : number ; count : number } | null {
const stats = this .timers .get (name);
if (!stats || stats.count === 0 ) return null ;
const sorted = [...stats.values ].sort ((a, b ) => a - b);
const p95Index = Math .floor (sorted.length * 0.95 );
return {
avg : stats.total / stats.count ,
p95 : sorted[p95Index] ?? sorted[sorted.length - 1 ],
min : stats.min ,
max : stats.max ,
count : stats.count ,
};
}
snapshot (): Record <string , unknown > {
const result : Record <string , unknown > = {};
for (const [k, v] of this .counters ) result[`counter.${k} ` ] = v;
for (const [k, v] of this .gauges ) result[`gauge.${k} ` ] = v;
for (const [k] of this .timers ) {
const s = this .getTimerStats (k);
if (s) result[`timer.${k} ` ] = s;
}
return result;
}
}
Step 3: Error Tracker with Deduplication
interface TrackedError {
name : string ;
message : string ;
stack ?: string ;
count : number ;
firstSeen : number ;
lastSeen : number ;
}
export class ErrorTracker {
private errors = new Map <string , TrackedError >();
track (err : Error ) {
const key = `${err.name} :${err.message} ` ;
const existing = this .errors .get (key);
if (existing) {
existing.count ++;
existing.lastSeen = Date .now ();
} else {
this .errors .set (key, {
name : err.name ,
message : err.message ,
stack : err.stack ,
count : 1 ,
firstSeen : Date .now (),
lastSeen : Date .now (),
});
}
}
async wrapAsync<T>(label : string , fn : () => Promise <T>): Promise <T | undefined > {
try {
return await fn ();
} catch (e) {
const err = e instanceof Error ? e : new Error (String (e));
err.message = `[${label} ] ${err.message} ` ;
this .track (err);
return undefined ;
}
}
getErrors (): TrackedError [] {
return [...this .errors .values ()].sort ((a, b ) => b.lastSeen - a.lastSeen );
}
getErrorCount (): number {
let total = 0 ;
for (const e of this .errors .values ()) total += e.count ;
return total;
}
clear ( ) { this .errors .clear (); }
}
Step 4: Debug Sidebar Panel
import { ItemView , WorkspaceLeaf } from 'obsidian' ;
import type { Logger } from '../services/logger' ;
import type { MetricsCollector } from '../services/metrics' ;
import type { ErrorTracker } from '../services/error-tracker' ;
export const DEBUG_VIEW_TYPE = 'plugin-debug-view' ;
export class DebugView extends ItemView {
private refreshTimer : number | null = null ;
constructor (
leaf : WorkspaceLeaf ,
private logger : Logger ,
private metrics : MetricsCollector ,
private errorTracker : ErrorTracker ,
) {
super (leaf);
}
getViewType ( ) { return DEBUG_VIEW_TYPE ; }
getDisplayText ( ) { return 'Plugin Debug' ; }
getIcon ( ) { return 'bug' ; }
async onOpen ( ) {
this .render ();
this .refreshTimer = window .setInterval (() => this .render (), 3000 );
}
async onClose ( ) {
if (this .refreshTimer ) clearInterval (this .refreshTimer );
}
private render ( ) {
const container = this .containerEl .children [1 ];
container.empty ();
container.addClass ('plugin-debug-view' );
container.createEl ('h4' , { text : 'Metrics' });
const snapshot = this .metrics .snapshot ();
const metricsTable = container.createEl ('table' );
for (const [key, value] of Object .entries (snapshot)) {
const row = metricsTable.createEl ('tr' );
row.createEl ('td' , { text : key, cls : 'debug-key' });
row.createEl ('td' , {
text : typeof value === 'object' ? JSON .stringify (value) : String (value),
});
}
const errors = this .errorTracker .getErrors ();
container.createEl ('h4' , { text : `Errors (${this .errorTracker.getErrorCount()} )` });
if (errors.length === 0 ) {
container.createEl ('p' , { text : 'No errors recorded.' , cls : 'debug-empty' });
} else {
for (const err of errors.slice (0 , 10 )) {
const el = container.createEl ('div' , { cls : 'debug-error' });
el.createEl ('strong' , { text : `${err.name} (x${err.count} )` });
el.createEl ('p' , { text : err.message });
}
}
container.createEl ('h4' , { text : 'Recent Logs' });
const logs = this .logger .getHistory (20 );
for (const entry of logs.reverse ()) {
const el = container.createEl ('div' , { cls : `debug-log debug-${entry.level} ` });
const time = new Date (entry.timestamp ).toLocaleTimeString ();
el.createEl ('span' , { text : `${time} [${entry.level} ]` , cls : 'debug-time' });
el.createEl ('span' , { text : ` ${entry.message} ` });
}
const btn = container.createEl ('button' , { text : 'Export Debug Bundle' });
btn.addEventListener ('click' , () => {
const bundle = {
timestamp : new Date ().toISOString (),
metrics : snapshot,
errors : errors,
recentLogs : this .logger .getHistory (50 ),
};
navigator.clipboard .writeText (JSON .stringify (bundle, null , 2 ));
new (require ('obsidian' ).Notice )('Debug bundle copied to clipboard' );
});
}
}
Step 5: Wire Everything into the Plugin
import { Plugin } from 'obsidian' ;
import { Logger } from './services/logger' ;
import { MetricsCollector } from './services/metrics' ;
import { ErrorTracker } from './services/error-tracker' ;
import { DebugView , DEBUG_VIEW_TYPE } from './views/debug-view' ;
export default class MyPlugin extends Plugin {
logger : Logger ;
metrics : MetricsCollector ;
errors : ErrorTracker ;
async onload ( ) {
this .logger = new Logger (this .manifest .id , 'debug' );
this .metrics = new MetricsCollector ();
this .errors = new ErrorTracker ();
this .registerView (DEBUG_VIEW_TYPE , (leaf ) =>
new DebugView (leaf, this .logger , this .metrics , this .errors )
);
this .addCommand ({
id : 'open-debug-panel' ,
name : 'Open debug panel' ,
callback : () => this .openDebugPanel (),
});
this .addCommand ({
id : 'my-command' ,
name : 'My Command' ,
callback : async () => {
this .metrics .increment ('commands.my-command' );
const endTimer = this .logger .time ('my-command' );
await this .errors .wrapAsync ('my-command' , async () => {
});
const ms = endTimer ();
this .metrics .recordTime ('commands.my-command' , ms);
},
});
this .registerInterval (
window .setInterval (() => {
this .metrics .setGauge ('vault.files' , this .app .vault .getMarkdownFiles ().length );
if (performance.memory ) {
this .metrics .setGauge ('memory.usedMB' ,
Math .round ((performance as any ).memory .usedJSHeapSize / 1048576 ));
}
}, 10_000 )
);
this .logger .info ('Plugin loaded' );
}
onunload ( ) {
this .app .workspace .detachLeavesOfType (DEBUG_VIEW_TYPE );
}
private async openDebugPanel ( ) {
const { workspace } = this .app ;
let leaf = workspace.getLeavesOfType (DEBUG_VIEW_TYPE )[0 ];
if (!leaf) {
const rightLeaf = workspace.getRightLeaf (false );
if (rightLeaf) {
await rightLeaf.setViewState ({ type : DEBUG_VIEW_TYPE , active : true });
leaf = rightLeaf;
}
}
if (leaf) workspace.revealLeaf (leaf);
}
}
Step 6: CSS for the Debug Panel
.plugin-debug-view { padding : 8px 12px ; font-size : var (--font-ui-small); }
.plugin-debug-view h4 { margin : 12px 0 4px ; color : var (--text-accent); }
.plugin-debug-view table { width : 100% ; border-collapse : collapse; }
.plugin-debug-view td { padding : 2px 6px ; border-bottom : 1px solid var (--background-modifier-border); }
.debug-key { font-family : var (--font-monospace); color : var (--text-muted); }
.debug-error { padding : 4px ; margin : 4px 0 ; background : var (--background-modifier-error); border-radius : var (--radius-s); }
.debug-log { padding : 1px 0 ; font-family : var (--font-monospace); font-size : 11px ; }
.debug-time { color : var (--text-faint); }
.debug-debug { color : var (--text-muted); }
.debug-info { color : var (--text-normal); }
.debug-warn { color : var (--text-accent); }
.debug-error { color : var (--text-error); }
.debug-empty { color : var (--text-faint); font-style : italic; }
Output
Structured logger with debug/info/warn/error levels and ring buffer history (200 entries)
Metrics collector: counters (monotonic), gauges (point-in-time), timers (with p95)
Error tracker with deduplication by name+message, occurrence count, and timestamps
Debug sidebar panel that auto-refreshes every 3 seconds showing metrics, errors, and logs
Export button that copies a full debug bundle to clipboard as JSON
Memory and vault stats tracked as gauges every 10 seconds
Error Handling Issue Cause Solution Too much logging output Debug level in production Set level to 'error' or 'warn' for release builds Memory growth from log history Unbounded buffer Ring buffer capped at 200 entries (adjustable) Performance impact from metrics Synchronous metric recording All operations are O(1) map lookups Debug panel slows Obsidian Rapid DOM updates Panel renders at most once per 3 seconds performance.memory undefinedNot available on all platforms Guard with if (performance.memory) Error tracker misses errors Error not thrown, just logged Use wrapAsync around all async operations
Examples
Quick Timing Check const endTimer = logger.time ('vault-scan' );
const files = app.vault .getMarkdownFiles ();
for (const f of files) await app.vault .cachedRead (f);
endTimer ();
Metrics-Wrapped Command this .addCommand ({
id : 'search-notes' ,
name : 'Search notes' ,
callback : () => this .metrics .timeAsync ('search-notes' , async () => {
this .metrics .increment ('search.invocations' );
const results = await this .search ();
this .metrics .setGauge ('search.lastResultCount' , results.length );
}),
});
Resources
Next Steps For incident response using debug bundles, see obsidian-incident-runbook.
For performance optimization, see obsidian-performance-tuning.