| name | obsidian-observability |
| description | Set up comprehensive logging and monitoring for Obsidian plugins.
Use when implementing debug logging, tracking plugin performance,
or setting up error reporting for your Obsidian plugin.
Trigger with phrases like "obsidian logging", "obsidian monitoring",
"obsidian debug", "track obsidian plugin".
|
| allowed-tools | Read, Write, Edit |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Obsidian Observability
Overview
Implement comprehensive logging, monitoring, and debugging capabilities for Obsidian plugins.
Prerequisites
- Working Obsidian plugin
- Understanding of Developer Tools
- Basic TypeScript knowledge
Observability Components
Key Metrics
| Metric | Type | Purpose |
|---|
| Command execution time | Timer | Performance |
| File operations count | Counter | Usage patterns |
| Error rate | Counter | Reliability |
| Cache hit ratio | Gauge | Efficiency |
| Memory usage | Gauge | Resource health |
Instructions
Step 1: Structured Logger
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
context?: Record<string, any>;
duration?: number;
}
export class Logger {
private pluginId: string;
private level: LogLevel;
private history: LogEntry[] = [];
private maxHistory: number = 100;
private readonly levelPriority: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
constructor(pluginId: string, : = ) {
. = pluginId;
. = level;
}
(: ): {
. = level;
}
(: ): {
.[level] >= .[.];
}
(: ): {
prefix = ;
time = entry..()[].()[];
level = entry..().();
message = ;
(entry. !== ) {
message += ;
}
message;
}
(: , : , ?: <, >): {
(!.(level)) ;
: = {
: ().(),
level,
message,
context,
};
..(entry);
(.. > .) {
..();
}
formatted = .(entry);
consoleMethod = level === ? : level;
(context) {
[consoleMethod](formatted, context);
} {
[consoleMethod](formatted);
}
}
(: , ?: <, >): {
.(, message, context);
}
(: , ?: <, >): {
.(, message, context);
}
(: , ?: <, >): {
.(, message, context);
}
(: , ?: , ?: <, >): {
errorContext = error ? {
...context,
: {
: error.,
: error.,
: error.,
},
} : context;
.(, message, errorContext);
}
(: ): {
start = performance.();
{
duration = performance.() - start;
: = {
: ().(),
: ,
: label,
duration,
};
(.()) {
.(.(entry));
}
..(entry);
};
}
(): [] {
[....];
}
(): {
..( {
line = ;
(entry.) line += ;
(entry.) line += ;
line;
}).();
}
}
Step 2: Metrics Collector
interface MetricValue {
value: number;
timestamp: number;
}
export class MetricsCollector {
private counters = new Map<string, number>();
private gauges = new Map<string, number>();
private timers = new Map<string, MetricValue[]>();
private maxTimerHistory = 100;
increment(name: string, value: number = 1): void {
const current = this.counters.get(name) || 0;
this.counters.set(name, current + value);
}
getCounter(name: string): number {
return this.counters.get(name) || 0;
}
(: , : ): {
..(name, value);
}
(: ): {
..(name) || ;
}
(: , : ): {
timings = ..(name) || [];
timings.({ : durationMs, : .() });
(timings. > .) {
timings.();
}
..(name, timings);
}
(: ): {
: ;
: ;
: ;
: ;
: ;
} | {
timings = ..(name);
(!timings || timings. === ) ;
values = timings.( t.).( a - b);
sum = values.( a + b, );
{
: values.,
: sum / values.,
: values[],
: values[values. - ],
: values[.(values. * )],
};
}
timeAsync<T>(: , : <T>): <T> {
start = performance.();
{
();
} {
.(name, performance.() - start);
}
}
timeSync<T>(: , : T): T {
start = performance.();
{
();
} {
.(name, performance.() - start);
}
}
(): {
: <, >;
: <, >;
: <, < .>>;
} {
: <, < .>> = {};
( name ..()) {
timerStats[name] = .(name);
}
{
: .(.),
: .(.),
: timerStats,
};
}
(): {
..();
..();
..();
}
}
Step 3: Error Tracking
interface TrackedError {
timestamp: string;
error: {
name: string;
message: string;
stack?: string;
};
context: Record<string, any>;
count: number;
}
export class ErrorTracker {
private errors = new Map<string, TrackedError>();
private maxErrors = 50;
track(error: Error, context: Record<string, any> = {}): void {
const key = `${error.name}:${error.message}`;
const existing = this.errors.get(key);
if (existing) {
existing.count++;
existing.timestamp = new Date().toISOString();
existing.context = { ...existing.context, ...context };
} {
(.. >= .) {
oldestKey = ..().().;
..(oldestKey);
}
..(key, {
: ().(),
: {
: error.,
: error.,
: error.,
},
context,
: ,
});
}
}
(): [] {
.(..())
.( b. - a.);
}
(: = ): [] {
.().(, limit);
}
(): {
..();
}
wrapAsync<T>(
: <T>,
: <, > = {}
): <T> {
().( {
.(error, context);
error;
});
}
(): {
.(.(), , );
}
}
Step 4: Debug Panel View
import { ItemView, WorkspaceLeaf } from 'obsidian';
import type MyPlugin from '../../main';
export const DEBUG_VIEW_TYPE = 'plugin-debug-view';
export class DebugView extends ItemView {
private plugin: MyPlugin;
private refreshInterval: number;
constructor(leaf: WorkspaceLeaf, plugin: MyPlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType(): string {
return DEBUG_VIEW_TYPE;
}
getDisplayText(): string {
return 'Plugin Debug';
}
getIcon(): string {
return 'bug';
}
async onOpen() {
const container = this..[];
container.();
container.();
.(container);
. = .( {
.(container);
}, );
.(.);
}
() {
container.();
container.(, { : });
metricsDiv = container.({ : });
.(metricsDiv);
container.(, { : });
errorsDiv = container.({ : });
.(errorsDiv);
container.(, { : });
logsDiv = container.({ : });
.(logsDiv);
actionsDiv = container.({ : });
.(actionsDiv);
}
() {
metrics = ...();
container.(, { : });
countersList = container.();
( [name, value] .(metrics.)) {
countersList.(, { : });
}
container.(, { : });
timersList = container.();
( [name, stats] .(metrics.)) {
(stats) {
timersList.(, {
: ,
});
}
}
}
() {
errors = ...();
(errors. === ) {
container.(, { : , : });
;
}
list = container.();
( error errors) {
item = list.();
item.(, { : });
item.({ : error.. });
item.(, {
: ,
: ,
});
}
}
() {
logs = ...().(-).();
pre = container.(, { : });
( log logs) {
line = pre.(, { : });
line. = ;
}
}
() {
exportBtn = container.(, { : });
exportBtn.(, .());
clearBtn = container.(, { : });
clearBtn.(, {
...();
...();
.(..[]);
});
}
() {
data = {
: ().(),
: ...,
: ...,
: ...(),
: ...(),
: ...(),
};
blob = ([.(data, , )], {
: ,
});
url = .(blob);
a = .();
a. = url;
a. = ;
a.();
.(url);
}
() {
(.) {
.(.);
}
}
}
Step 5: Integration in Main Plugin
import { Plugin } from 'obsidian';
import { Logger } from './utils/logger';
import { MetricsCollector } from './utils/metrics';
import { ErrorTracker } from './utils/error-tracker';
import { DebugView, DEBUG_VIEW_TYPE } from './ui/views/debug-view';
export default class MyPlugin extends Plugin {
logger: Logger;
metrics: MetricsCollector;
errorTracker: ErrorTracker;
async onload() {
this.logger = new Logger(this.manifest.id, 'debug');
this.metrics = new MetricsCollector();
this.errorTracker = ();
..();
endLoadTime = ..();
(process.. !== ) {
.(
,
(leaf, )
);
.({
: ,
: ,
: .(),
});
}
.({
: ,
: ,
: () => {
..();
..(, () => {
});
},
});
();
..();
}
() {
leaf = ...();
leaf.({ : , : });
...(leaf);
}
}
Output
- Structured logger with levels and history
- Metrics collection (counters, gauges, timers)
- Error tracking with deduplication
- Debug panel view for runtime inspection
- Export capability for support
Error Handling
| Issue | Cause | Solution |
|---|
| Too much logging | Debug level in prod | Set level to 'error' |
| Memory growth | Unbounded history | Limit history size |
| Performance impact | Sync logging | Use async logging |
| Missing context | No error tracking | Wrap async calls |
Examples
Styles for Debug View
.plugin-debug-view {
padding: 16px;
}
.debug-section {
margin-bottom: 24px;
padding: 12px;
background: var(--background-secondary);
border-radius: 4px;
}
.log-output {
font-family: monospace;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
}
.log-debug { color: var(--text-muted); }
.log-info { color: var(--text-normal); }
.log-warn { color: var(--text-warning); }
.log-error { color: var(--text-error); }
.error-count {
color: var(--text-error);
font-weight: bold;
}
.debug-actions button {
margin-right: 8px;
}
Resources
Next Steps
For incident response, see obsidian-incident-runbook.