Merge branch 'develop' of github.com:TriliumNext/Notes into develop

This commit is contained in:
Elian Doran 2025-05-27 10:47:25 +03:00
commit bf082fdd40
No known key found for this signature in database
13 changed files with 856 additions and 202 deletions

View File

@ -0,0 +1,43 @@
### Test regular API metrics endpoint (requires session authentication)
### Get metrics from regular API (default Prometheus format)
GET {{triliumHost}}/api/metrics
> {%
client.test("API metrics endpoint returns Prometheus format by default", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain");
client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric");
client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric");
client.assert(response.body.includes("# HELP"), "Should contain HELP comments");
client.assert(response.body.includes("# TYPE"), "Should contain TYPE comments");
});
%}
### Get metrics in JSON format
GET {{triliumHost}}/api/metrics?format=json
> {%
client.test("API metrics endpoint returns JSON when requested", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.headers["content-type"].includes("application/json"), "Content-Type should be application/json");
client.assert(response.body.version, "Version info not present");
client.assert(response.body.database, "Database info not present");
client.assert(response.body.timestamp, "Timestamp not present");
client.assert(typeof response.body.database.totalNotes === 'number', "Total notes should be a number");
client.assert(typeof response.body.database.activeNotes === 'number', "Active notes should be a number");
client.assert(response.body.noteTypes, "Note types breakdown not present");
client.assert(response.body.attachmentTypes, "Attachment types breakdown not present");
client.assert(response.body.statistics, "Statistics not present");
});
%}
### Test invalid format parameter
GET {{triliumHost}}/api/metrics?format=xml
> {%
client.test("Invalid format parameter returns error", function() {
client.assert(response.status === 500, "Response status should be 500");
client.assert(response.body.message.includes("prometheus"), "Error message should mention supported formats");
});
%}

View File

@ -0,0 +1,82 @@
### Test ETAPI metrics endpoint
# First login to get a token
POST {{triliumHost}}/etapi/auth/login
Content-Type: application/json
{
"password": "{{password}}"
}
> {%
client.test("Login successful", function() {
client.assert(response.status === 201, "Response status is not 201");
client.assert(response.body.authToken, "Auth token not present");
client.global.set("authToken", response.body.authToken);
});
%}
### Get metrics with authentication (default Prometheus format)
GET {{triliumHost}}/etapi/metrics
Authorization: {{authToken}}
> {%
client.test("Metrics endpoint returns Prometheus format by default", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain");
client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric");
client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric");
client.assert(response.body.includes("# HELP"), "Should contain HELP comments");
client.assert(response.body.includes("# TYPE"), "Should contain TYPE comments");
});
%}
### Get metrics in JSON format
GET {{triliumHost}}/etapi/metrics?format=json
Authorization: {{authToken}}
> {%
client.test("Metrics endpoint returns JSON when requested", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.headers["content-type"].includes("application/json"), "Content-Type should be application/json");
client.assert(response.body.version, "Version info not present");
client.assert(response.body.database, "Database info not present");
client.assert(response.body.timestamp, "Timestamp not present");
client.assert(typeof response.body.database.totalNotes === 'number', "Total notes should be a number");
client.assert(typeof response.body.database.activeNotes === 'number', "Active notes should be a number");
});
%}
### Get metrics in Prometheus format explicitly
GET {{triliumHost}}/etapi/metrics?format=prometheus
Authorization: {{authToken}}
> {%
client.test("Metrics endpoint returns Prometheus format when requested", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain");
client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric");
client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric");
});
%}
### Test invalid format parameter
GET {{triliumHost}}/etapi/metrics?format=xml
Authorization: {{authToken}}
> {%
client.test("Invalid format parameter returns error", function() {
client.assert(response.status === 400, "Response status should be 400");
client.assert(response.body.code === "INVALID_FORMAT", "Error code should be INVALID_FORMAT");
client.assert(response.body.message.includes("prometheus"), "Error message should mention supported formats");
});
%}
### Test without authentication (should fail)
GET {{triliumHost}}/etapi/metrics
> {%
client.test("Metrics endpoint requires authentication", function() {
client.assert(response.status === 401, "Response status should be 401");
});
%}

File diff suppressed because one or more lines are too long

View File

@ -275,9 +275,9 @@
content via the injected <code>now</code> and <code>parentNote</code> variables.</p> content via the injected <code>now</code> and <code>parentNote</code> variables.</p>
<p>Examples:</p> <p>Examples:</p>
<ul> <ul>
<li><code>${parentNote.getLabel('authorName')}'s literary works</code> <li><code><span class="math-tex">\({parentNote.getLabel('authorName')}'s literary works</span></code>
</li> </li>
<li><code>Log for ${now.format('YYYY-MM-DD HH:mm:ss')}</code> <li><code>Log for \){now.format('YYYY-MM-DD HH:mm:ss')}</code>
</li> </li>
<li>to mirror the parent's template.</li> <li>to mirror the parent's template.</li>
</ul> </ul>

View File

@ -0,0 +1,88 @@
<p>&nbsp;</p>
<h1><strong>Trilium Metrics API</strong></h1>
<p>The Trilium metrics API provides comprehensive monitoring data about your
Trilium instance, designed for external monitoring systems like Prometheus.</p>
<h2><strong>Endpoint</strong></h2>
<ul>
<li><strong>URL</strong>: <code>/etapi/metrics</code>
</li>
<li><strong>Method</strong>: <code>GET</code>
</li>
<li><strong>Authentication</strong>: ETAPI token required</li>
<li><strong>Default Format</strong>: Prometheus text format</li>
</ul>
<h2><strong>Authentication</strong></h2>
<p>You need an ETAPI token to access the metrics endpoint. Get one by:</p><pre><code class="language-text-x-trilium-auto"># Get an ETAPI token
curl -X POST http://localhost:8080/etapi/auth/login \
-H "Content-Type: application/json" \
-d '{"password": "your_password"}'
</code></pre>
<h2><strong>Usage</strong></h2>
<h3><strong>Prometheus Format (Default)</strong></h3><pre><code class="language-text-x-trilium-auto">curl -H "Authorization: YOUR_ETAPI_TOKEN" \
http://localhost:8080/etapi/metrics
</code></pre>
<p>Returns metrics in Prometheus text format:</p><pre><code class="language-text-x-trilium-auto"># HELP trilium_info Trilium instance information
# TYPE trilium_info gauge
trilium_info{version="0.91.6",db_version="231",node_version="v18.17.0"} 1 1701432000
# HELP trilium_notes_total Total number of notes including deleted
# TYPE trilium_notes_total gauge
trilium_notes_total 1234 1701432000
</code></pre>
<h3><strong>JSON Format</strong></h3><pre><code class="language-text-x-trilium-auto">curl -H "Authorization: YOUR_ETAPI_TOKEN" \
"http://localhost:8080/etapi/metrics?format=json"
</code></pre>
<p>Returns detailed metrics in JSON format for debugging or custom integrations.</p>
<h2><strong>Available Metrics</strong></h2>
<h3><strong>Instance Information</strong></h3>
<ul>
<li><code>trilium_info</code> - Version and build information with labels</li>
</ul>
<h3><strong>Database Metrics</strong></h3>
<ul>
<li><code>trilium_notes_total</code> - Total notes (including deleted)</li>
<li><code>trilium_notes_deleted</code> - Number of deleted notes</li>
<li><code>trilium_notes_active</code> - Number of active notes</li>
<li><code>trilium_notes_protected</code> - Number of protected notes</li>
<li><code>trilium_attachments_total</code> - Total attachments</li>
<li><code>trilium_attachments_active</code> - Active attachments</li>
<li><code>trilium_revisions_total</code> - Total note revisions</li>
<li><code>trilium_branches_total</code> - Active branches</li>
<li><code>trilium_attributes_total</code> - Active attributes</li>
<li><code>trilium_blobs_total</code> - Total blob records</li>
<li><code>trilium_etapi_tokens_total</code> - Active ETAPI tokens</li>
<li><code>trilium_embeddings_total</code> - Note embeddings (if available)</li>
</ul>
<h3><strong>Categorized Metrics</strong></h3>
<ul>
<li><code>trilium_notes_by_type{type="text|code|image|file"}</code> - Notes
by type</li>
<li><code>trilium_attachments_by_type{mime_type="..."}</code> - Attachments
by MIME type</li>
</ul>
<h3><strong>Statistics</strong></h3>
<ul>
<li><code>trilium_database_size_bytes</code> - Database size in bytes</li>
<li><code>trilium_oldest_note_timestamp</code> - Timestamp of oldest note</li>
<li><code>trilium_newest_note_timestamp</code> - Timestamp of newest note</li>
<li><code>trilium_last_modified_timestamp</code> - Last modification timestamp</li>
</ul>
<h2><strong>Prometheus Configuration</strong></h2>
<p>Add to your <code>prometheus.yml</code>:</p><pre><code class="language-text-x-trilium-auto">scrape_configs:
- job_name: 'trilium'
static_configs:
- targets: ['localhost:8080']
metrics_path: '/etapi/metrics'
headers:
Authorization: 'YOUR_ETAPI_TOKEN'
scrape_interval: 30s
</code></pre>
<h2><strong>Error Responses</strong></h2>
<ul>
<li><code>400</code> - Invalid format parameter</li>
<li><code>401</code> - Missing or invalid ETAPI token</li>
<li><code>500</code> - Internal server error</li>
</ul>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>

View File

@ -0,0 +1,268 @@
import type { Router, Request, Response, NextFunction } from "express";
import eu from "./etapi_utils.js";
import sql from "../services/sql.js";
import appInfo from "../services/app_info.js";
interface MetricsData {
version: {
app: string;
db: number;
node: string;
sync: number;
buildDate: string;
buildRevision: string;
};
database: {
totalNotes: number;
deletedNotes: number;
activeNotes: number;
protectedNotes: number;
totalAttachments: number;
deletedAttachments: number;
activeAttachments: number;
totalRevisions: number;
totalBranches: number;
totalAttributes: number;
totalBlobs: number;
totalEtapiTokens: number;
totalRecentNotes: number;
totalEmbeddings: number;
totalEmbeddingProviders: number;
};
noteTypes: Record<string, number>;
attachmentTypes: Record<string, number>;
statistics: {
oldestNote: string | null;
newestNote: string | null;
lastModified: string | null;
databaseSizeBytes: number | null;
};
timestamp: string;
}
/**
* Converts metrics data to Prometheus text format
*/
function formatPrometheusMetrics(data: MetricsData): string {
const lines: string[] = [];
const timestamp = Math.floor(new Date(data.timestamp).getTime() / 1000);
// Helper function to add a metric
const addMetric = (name: string, value: number | null, help: string, type: string = 'gauge', labels: Record<string, string> = {}) => {
if (value === null) return;
lines.push(`# HELP ${name} ${help}`);
lines.push(`# TYPE ${name} ${type}`);
const labelStr = Object.entries(labels).length > 0
? `{${Object.entries(labels).map(([k, v]) => `${k}="${v}"`).join(',')}}`
: '';
lines.push(`${name}${labelStr} ${value} ${timestamp}`);
lines.push('');
};
// Version info
addMetric('trilium_info', 1, 'Trilium instance information', 'gauge', {
version: data.version.app,
db_version: data.version.db.toString(),
node_version: data.version.node,
sync_version: data.version.sync.toString(),
build_date: data.version.buildDate,
build_revision: data.version.buildRevision
});
// Database metrics
addMetric('trilium_notes_total', data.database.totalNotes, 'Total number of notes including deleted');
addMetric('trilium_notes_deleted', data.database.deletedNotes, 'Number of deleted notes');
addMetric('trilium_notes_active', data.database.activeNotes, 'Number of active notes');
addMetric('trilium_notes_protected', data.database.protectedNotes, 'Number of protected notes');
addMetric('trilium_attachments_total', data.database.totalAttachments, 'Total number of attachments including deleted');
addMetric('trilium_attachments_deleted', data.database.deletedAttachments, 'Number of deleted attachments');
addMetric('trilium_attachments_active', data.database.activeAttachments, 'Number of active attachments');
addMetric('trilium_revisions_total', data.database.totalRevisions, 'Total number of note revisions');
addMetric('trilium_branches_total', data.database.totalBranches, 'Number of active branches');
addMetric('trilium_attributes_total', data.database.totalAttributes, 'Number of active attributes');
addMetric('trilium_blobs_total', data.database.totalBlobs, 'Total number of blob records');
addMetric('trilium_etapi_tokens_total', data.database.totalEtapiTokens, 'Number of active ETAPI tokens');
addMetric('trilium_recent_notes_total', data.database.totalRecentNotes, 'Number of recent notes tracked');
addMetric('trilium_embeddings_total', data.database.totalEmbeddings, 'Number of note embeddings');
addMetric('trilium_embedding_providers_total', data.database.totalEmbeddingProviders, 'Number of embedding providers');
// Note types
for (const [type, count] of Object.entries(data.noteTypes)) {
addMetric('trilium_notes_by_type', count, 'Number of notes by type', 'gauge', { type });
}
// Attachment types
for (const [mime, count] of Object.entries(data.attachmentTypes)) {
addMetric('trilium_attachments_by_type', count, 'Number of attachments by MIME type', 'gauge', { mime_type: mime });
}
// Statistics
if (data.statistics.databaseSizeBytes !== null) {
addMetric('trilium_database_size_bytes', data.statistics.databaseSizeBytes, 'Database size in bytes');
}
if (data.statistics.oldestNote) {
const oldestTimestamp = Math.floor(new Date(data.statistics.oldestNote).getTime() / 1000);
addMetric('trilium_oldest_note_timestamp', oldestTimestamp, 'Timestamp of the oldest note');
}
if (data.statistics.newestNote) {
const newestTimestamp = Math.floor(new Date(data.statistics.newestNote).getTime() / 1000);
addMetric('trilium_newest_note_timestamp', newestTimestamp, 'Timestamp of the newest note');
}
if (data.statistics.lastModified) {
const lastModifiedTimestamp = Math.floor(new Date(data.statistics.lastModified).getTime() / 1000);
addMetric('trilium_last_modified_timestamp', lastModifiedTimestamp, 'Timestamp of the last modification');
}
return lines.join('\n');
}
/**
* Collects comprehensive metrics about the Trilium instance
*/
function collectMetrics(): MetricsData {
// Version information
const version = {
app: appInfo.appVersion,
db: appInfo.dbVersion,
node: appInfo.nodeVersion,
sync: appInfo.syncVersion,
buildDate: appInfo.buildDate,
buildRevision: appInfo.buildRevision
};
// Database counts
const totalNotes = sql.getValue<number>("SELECT COUNT(*) FROM notes");
const deletedNotes = sql.getValue<number>("SELECT COUNT(*) FROM notes WHERE isDeleted = 1");
const activeNotes = totalNotes - deletedNotes;
const protectedNotes = sql.getValue<number>("SELECT COUNT(*) FROM notes WHERE isProtected = 1 AND isDeleted = 0");
const totalAttachments = sql.getValue<number>("SELECT COUNT(*) FROM attachments");
const deletedAttachments = sql.getValue<number>("SELECT COUNT(*) FROM attachments WHERE isDeleted = 1");
const activeAttachments = totalAttachments - deletedAttachments;
const totalRevisions = sql.getValue<number>("SELECT COUNT(*) FROM revisions");
const totalBranches = sql.getValue<number>("SELECT COUNT(*) FROM branches WHERE isDeleted = 0");
const totalAttributes = sql.getValue<number>("SELECT COUNT(*) FROM attributes WHERE isDeleted = 0");
const totalBlobs = sql.getValue<number>("SELECT COUNT(*) FROM blobs");
const totalEtapiTokens = sql.getValue<number>("SELECT COUNT(*) FROM etapi_tokens WHERE isDeleted = 0");
const totalRecentNotes = sql.getValue<number>("SELECT COUNT(*) FROM recent_notes");
// Embedding-related metrics (these tables might not exist in older versions)
let totalEmbeddings = 0;
let totalEmbeddingProviders = 0;
try {
totalEmbeddings = sql.getValue<number>("SELECT COUNT(*) FROM note_embeddings");
totalEmbeddingProviders = sql.getValue<number>("SELECT COUNT(*) FROM embedding_providers");
} catch (e) {
// Tables don't exist, keep defaults
}
const database = {
totalNotes,
deletedNotes,
activeNotes,
protectedNotes,
totalAttachments,
deletedAttachments,
activeAttachments,
totalRevisions,
totalBranches,
totalAttributes,
totalBlobs,
totalEtapiTokens,
totalRecentNotes,
totalEmbeddings,
totalEmbeddingProviders
};
// Note types breakdown
const noteTypesRows = sql.getRows<{ type: string; count: number }>(
"SELECT type, COUNT(*) as count FROM notes WHERE isDeleted = 0 GROUP BY type ORDER BY count DESC"
);
const noteTypes: Record<string, number> = {};
for (const row of noteTypesRows) {
noteTypes[row.type] = row.count;
}
// Attachment types breakdown
const attachmentTypesRows = sql.getRows<{ mime: string; count: number }>(
"SELECT mime, COUNT(*) as count FROM attachments WHERE isDeleted = 0 GROUP BY mime ORDER BY count DESC"
);
const attachmentTypes: Record<string, number> = {};
for (const row of attachmentTypesRows) {
attachmentTypes[row.mime] = row.count;
}
// Statistics
const oldestNote = sql.getValue<string | null>(
"SELECT utcDateCreated FROM notes WHERE isDeleted = 0 ORDER BY utcDateCreated ASC LIMIT 1"
);
const newestNote = sql.getValue<string | null>(
"SELECT utcDateCreated FROM notes WHERE isDeleted = 0 ORDER BY utcDateCreated DESC LIMIT 1"
);
const lastModified = sql.getValue<string | null>(
"SELECT utcDateModified FROM notes WHERE isDeleted = 0 ORDER BY utcDateModified DESC LIMIT 1"
);
// Database size (this might not work on all systems)
let databaseSizeBytes: number | null = null;
try {
const sizeResult = sql.getValue<number>("SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()");
databaseSizeBytes = sizeResult;
} catch (e) {
// Pragma might not be available
}
const statistics = {
oldestNote,
newestNote,
lastModified,
databaseSizeBytes
};
return {
version,
database,
noteTypes,
attachmentTypes,
statistics,
timestamp: new Date().toISOString()
};
}
function register(router: Router): void {
eu.route(router, "get", "/etapi/metrics", (req: Request, res: Response, next: NextFunction) => {
try {
const metrics = collectMetrics();
const format = (req.query.format as string)?.toLowerCase() || 'prometheus';
if (format === 'json') {
res.status(200).json(metrics);
} else if (format === 'prometheus') {
const prometheusText = formatPrometheusMetrics(metrics);
res.status(200)
.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8')
.send(prometheusText);
} else {
throw new eu.EtapiError(400, "INVALID_FORMAT", "Supported formats: 'prometheus' (default), 'json'");
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new eu.EtapiError(500, "METRICS_ERROR", `Failed to collect metrics: ${errorMessage}`);
}
});
}
export default {
register,
collectMetrics,
formatPrometheusMetrics
};

View File

@ -0,0 +1,171 @@
import type { Request, Response } from "express";
import etapiMetrics from "../../etapi/metrics.js";
type MetricsData = ReturnType<typeof etapiMetrics.collectMetrics>;
/**
* @swagger
* /api/metrics:
* get:
* summary: Get Trilium instance metrics
* operationId: metrics
* parameters:
* - in: query
* name: format
* schema:
* type: string
* enum: [prometheus, json]
* default: prometheus
* description: Response format - 'prometheus' (default) for Prometheus text format, 'json' for JSON
* responses:
* '200':
* description: Instance metrics
* content:
* text/plain:
* schema:
* type: string
* example: |
* # HELP trilium_info Trilium instance information
* # TYPE trilium_info gauge
* trilium_info{version="0.91.6",db_version="231",node_version="v18.17.0"} 1 1701432000
*
* # HELP trilium_notes_total Total number of notes including deleted
* # TYPE trilium_notes_total gauge
* trilium_notes_total 1234 1701432000
* application/json:
* schema:
* type: object
* properties:
* version:
* type: object
* properties:
* app:
* type: string
* example: "0.91.6"
* db:
* type: integer
* example: 231
* node:
* type: string
* example: "v18.17.0"
* sync:
* type: integer
* example: 35
* buildDate:
* type: string
* example: "2024-09-07T18:36:34Z"
* buildRevision:
* type: string
* example: "7c0d6930fa8f20d269dcfbcbc8f636a25f6bb9a7"
* database:
* type: object
* properties:
* totalNotes:
* type: integer
* example: 1234
* deletedNotes:
* type: integer
* example: 56
* activeNotes:
* type: integer
* example: 1178
* protectedNotes:
* type: integer
* example: 23
* totalAttachments:
* type: integer
* example: 89
* deletedAttachments:
* type: integer
* example: 5
* activeAttachments:
* type: integer
* example: 84
* totalRevisions:
* type: integer
* example: 567
* totalBranches:
* type: integer
* example: 1200
* totalAttributes:
* type: integer
* example: 345
* totalBlobs:
* type: integer
* example: 678
* totalEtapiTokens:
* type: integer
* example: 3
* totalRecentNotes:
* type: integer
* example: 50
* totalEmbeddings:
* type: integer
* example: 123
* totalEmbeddingProviders:
* type: integer
* example: 2
* noteTypes:
* type: object
* additionalProperties:
* type: integer
* example:
* text: 800
* code: 200
* image: 100
* file: 50
* attachmentTypes:
* type: object
* additionalProperties:
* type: integer
* example:
* "image/png": 45
* "image/jpeg": 30
* "application/pdf": 14
* statistics:
* type: object
* properties:
* oldestNote:
* type: string
* nullable: true
* example: "2020-01-01T00:00:00.000Z"
* newestNote:
* type: string
* nullable: true
* example: "2024-12-01T12:00:00.000Z"
* lastModified:
* type: string
* nullable: true
* example: "2024-12-01T11:30:00.000Z"
* databaseSizeBytes:
* type: integer
* nullable: true
* example: 52428800
* timestamp:
* type: string
* example: "2024-12-01T12:00:00.000Z"
* '400':
* description: Invalid format parameter
* '500':
* description: Error collecting metrics
* security:
* - session: []
*/
function getMetrics(req: Request, res: Response): string | MetricsData {
const format = (req.query?.format as string)?.toLowerCase() || 'prometheus';
if (format === 'json') {
return etapiMetrics.collectMetrics();
} else if (format === 'prometheus') {
const metrics = etapiMetrics.collectMetrics();
const prometheusText = etapiMetrics.formatPrometheusMetrics(metrics);
res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
return prometheusText;
} else {
throw new Error("Supported formats: 'prometheus' (default), 'json'");
}
}
export default {
getMetrics
};

View File

@ -52,6 +52,7 @@ import fontsRoute from "./api/fonts.js";
import etapiTokensApiRoutes from "./api/etapi_tokens.js"; import etapiTokensApiRoutes from "./api/etapi_tokens.js";
import relationMapApiRoute from "./api/relation-map.js"; import relationMapApiRoute from "./api/relation-map.js";
import otherRoute from "./api/other.js"; import otherRoute from "./api/other.js";
import metricsRoute from "./api/metrics.js";
import shareRoutes from "../share/routes.js"; import shareRoutes from "../share/routes.js";
import embeddingsRoute from "./api/embeddings.js"; import embeddingsRoute from "./api/embeddings.js";
import ollamaRoute from "./api/ollama.js"; import ollamaRoute from "./api/ollama.js";
@ -68,6 +69,7 @@ import etapiNoteRoutes from "../etapi/notes.js";
import etapiSpecialNoteRoutes from "../etapi/special_notes.js"; import etapiSpecialNoteRoutes from "../etapi/special_notes.js";
import etapiSpecRoute from "../etapi/spec.js"; import etapiSpecRoute from "../etapi/spec.js";
import etapiBackupRoute from "../etapi/backup.js"; import etapiBackupRoute from "../etapi/backup.js";
import etapiMetricsRoute from "../etapi/metrics.js";
import apiDocsRoute from "./api_docs.js"; import apiDocsRoute from "./api_docs.js";
import { apiResultHandler, apiRoute, asyncApiRoute, asyncRoute, route, router, uploadMiddlewareWithErrorHandling } from "./route_api.js"; import { apiResultHandler, apiRoute, asyncApiRoute, asyncRoute, route, router, uploadMiddlewareWithErrorHandling } from "./route_api.js";
@ -236,6 +238,7 @@ function register(app: express.Application) {
apiRoute(PST, "/api/recent-notes", recentNotesRoute.addRecentNote); apiRoute(PST, "/api/recent-notes", recentNotesRoute.addRecentNote);
apiRoute(GET, "/api/app-info", appInfoRoute.getAppInfo); apiRoute(GET, "/api/app-info", appInfoRoute.getAppInfo);
apiRoute(GET, "/api/metrics", metricsRoute.getMetrics);
// docker health check // docker health check
route(GET, "/api/health-check", [], () => ({ status: "ok" }), apiResultHandler); route(GET, "/api/health-check", [], () => ({ status: "ok" }), apiResultHandler);
@ -363,6 +366,7 @@ function register(app: express.Application) {
etapiSpecialNoteRoutes.register(router); etapiSpecialNoteRoutes.register(router);
etapiSpecRoute.register(router); etapiSpecRoute.register(router);
etapiBackupRoute.register(router); etapiBackupRoute.register(router);
etapiMetricsRoute.register(router);
// LLM Chat API // LLM Chat API
asyncApiRoute(PST, "/api/llm/chat", llmRoute.createSession); asyncApiRoute(PST, "/api/llm/chat", llmRoute.createSession);

View File

@ -53,6 +53,7 @@
* For editable notes, clicking on a code block will reveal a toolbar with a way to easily change the programming language and another button to copy the text to clipboard. * For editable notes, clicking on a code block will reveal a toolbar with a way to easily change the programming language and another button to copy the text to clipboard.
* For read-only notes, a floating button allows copying the code snippet to clipboard. * For read-only notes, a floating button allows copying the code snippet to clipboard.
* [Math in text notes: equations can now be displayed on multiple lines](https://github.com/TriliumNext/Notes/pull/2003) by @SiriusXT * [Math in text notes: equations can now be displayed on multiple lines](https://github.com/TriliumNext/Notes/pull/2003) by @SiriusXT
* [Metrics endpoint](https://github.com/TriliumNext/Notes/pull/2024) by @perfectra1n
## 📖 Documentation ## 📖 Documentation

View File

@ -1735,37 +1735,30 @@
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_optionsAppearance", "value": "nRhnJkTT8cPs",
"isInheritable": false, "isInheritable": false,
"position": 180 "position": 180
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "nRhnJkTT8cPs", "value": "KSZ04uQ2D1St",
"isInheritable": false, "isInheritable": false,
"position": 190 "position": 190
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "KSZ04uQ2D1St", "value": "WOcw2SLH6tbX",
"isInheritable": false, "isInheritable": false,
"position": 200 "position": 200
}, },
{
"type": "relation",
"name": "internalLink",
"value": "WOcw2SLH6tbX",
"isInheritable": false,
"position": 210
},
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "veGu4faJErEM", "value": "veGu4faJErEM",
"isInheritable": false, "isInheritable": false,
"position": 220 "position": 210
}, },
{ {
"type": "label", "type": "label",
@ -2053,30 +2046,23 @@
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_options", "value": "oPVyFC7WL2Lp",
"isInheritable": false, "isInheritable": false,
"position": 30 "position": 30
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "oPVyFC7WL2Lp", "value": "3seOhtN8uLIY",
"isInheritable": false, "isInheritable": false,
"position": 40 "position": 40
}, },
{
"type": "relation",
"name": "internalLink",
"value": "3seOhtN8uLIY",
"isInheritable": false,
"position": 50
},
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "cbkrhQjrkKrh", "value": "cbkrhQjrkKrh",
"isInheritable": false, "isInheritable": false,
"position": 60 "position": 50
}, },
{ {
"type": "label", "type": "label",
@ -3076,65 +3062,51 @@
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_optionsTextNotes", "value": "zEY4DaJG4YT5",
"isInheritable": false, "isInheritable": false,
"position": 10 "position": 10
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_optionsCodeNotes", "value": "iPIMuisry3hd",
"isInheritable": false, "isInheritable": false,
"position": 20 "position": 20
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "zEY4DaJG4YT5", "value": "6f9hih2hXXZk",
"isInheritable": false, "isInheritable": false,
"position": 30 "position": 30
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "iPIMuisry3hd", "value": "4TIF1oA4VQRO",
"isInheritable": false, "isInheritable": false,
"position": 40 "position": 40
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "6f9hih2hXXZk", "value": "BlN9DFI679QC",
"isInheritable": false, "isInheritable": false,
"position": 50 "position": 50
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "4TIF1oA4VQRO", "value": "XpOYSgsLkTJy",
"isInheritable": false, "isInheritable": false,
"position": 60 "position": 60
}, },
{
"type": "relation",
"name": "internalLink",
"value": "BlN9DFI679QC",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "XpOYSgsLkTJy",
"isInheritable": false,
"position": 80
},
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "s1aBHPd79XYj", "value": "s1aBHPd79XYj",
"isInheritable": false, "isInheritable": false,
"position": 90 "position": 70
}, },
{ {
"type": "label", "type": "label",
@ -5479,19 +5451,12 @@
"isInheritable": false, "isInheritable": false,
"position": 10 "position": 10
}, },
{
"type": "relation",
"name": "internalLink",
"value": "_optionsTextNotes",
"isInheritable": false,
"position": 20
},
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "BlN9DFI679QC", "value": "BlN9DFI679QC",
"isInheritable": false, "isInheritable": false,
"position": 30 "position": 20
}, },
{ {
"type": "label", "type": "label",
@ -5668,23 +5633,16 @@
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_optionsTextNotes", "value": "zEY4DaJG4YT5",
"isInheritable": false, "isInheritable": false,
"position": 30 "position": 30
}, },
{
"type": "relation",
"name": "internalLink",
"value": "zEY4DaJG4YT5",
"isInheritable": false,
"position": 40
},
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "BFvAtE74rbP6", "value": "BFvAtE74rbP6",
"isInheritable": false, "isInheritable": false,
"position": 50 "position": 40
}, },
{ {
"type": "label", "type": "label",
@ -6694,19 +6652,12 @@
"isInheritable": false, "isInheritable": false,
"position": 20 "position": 20
}, },
{
"type": "relation",
"name": "internalLink",
"value": "_optionsTextNotes",
"isInheritable": false,
"position": 30
},
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "zEY4DaJG4YT5", "value": "zEY4DaJG4YT5",
"isInheritable": false, "isInheritable": false,
"position": 40 "position": 30
}, },
{ {
"type": "label", "type": "label",
@ -8521,191 +8472,184 @@
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_help_YKWqdJhzi2VY", "value": "OFXdgB2nNk1F",
"isInheritable": false, "isInheritable": false,
"position": 50 "position": 50
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "OFXdgB2nNk1F", "value": "BlN9DFI679QC",
"isInheritable": false, "isInheritable": false,
"position": 60 "position": 60
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "BlN9DFI679QC", "value": "vZWERwf8U3nx",
"isInheritable": false, "isInheritable": false,
"position": 70 "position": 70
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "vZWERwf8U3nx", "value": "oPVyFC7WL2Lp",
"isInheritable": false, "isInheritable": false,
"position": 80 "position": 80
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "oPVyFC7WL2Lp", "value": "GPERMystNGTB",
"isInheritable": false, "isInheritable": false,
"position": 90 "position": 90
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "GPERMystNGTB", "value": "CoFPLs3dRlXc",
"isInheritable": false, "isInheritable": false,
"position": 100 "position": 100
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "CoFPLs3dRlXc", "value": "AlhDUqhENtH7",
"isInheritable": false, "isInheritable": false,
"position": 110 "position": 110
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "AlhDUqhENtH7", "value": "pKK96zzmvBGf",
"isInheritable": false, "isInheritable": false,
"position": 120 "position": 120
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "pKK96zzmvBGf", "value": "WFGzWeUK6arS",
"isInheritable": false, "isInheritable": false,
"position": 130 "position": 130
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "WFGzWeUK6arS", "value": "0ESUbbAxVnoK",
"isInheritable": false, "isInheritable": false,
"position": 140 "position": 140
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "0ESUbbAxVnoK", "value": "J5Ex1ZrMbyJ6",
"isInheritable": false, "isInheritable": false,
"position": 150 "position": 150
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "J5Ex1ZrMbyJ6", "value": "d3fAXQ2diepH",
"isInheritable": false, "isInheritable": false,
"position": 160 "position": 160
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "d3fAXQ2diepH", "value": "MgibgPcfeuGz",
"isInheritable": false, "isInheritable": false,
"position": 170 "position": 170
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "MgibgPcfeuGz", "value": "m523cpzocqaD",
"isInheritable": false, "isInheritable": false,
"position": 180 "position": 180
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "m523cpzocqaD", "value": "9sRHySam5fXb",
"isInheritable": false, "isInheritable": false,
"position": 190 "position": 190
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "9sRHySam5fXb", "value": "u3YFHC9tQlpm",
"isInheritable": false, "isInheritable": false,
"position": 200 "position": 200
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "u3YFHC9tQlpm", "value": "R9pX4DGra2Vt",
"isInheritable": false, "isInheritable": false,
"position": 210 "position": 210
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "R9pX4DGra2Vt", "value": "iRwzGnHPzonm",
"isInheritable": false, "isInheritable": false,
"position": 220 "position": 220
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "iRwzGnHPzonm", "value": "BCkXAVs63Ttv",
"isInheritable": false, "isInheritable": false,
"position": 230 "position": 230
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "BCkXAVs63Ttv", "value": "47ZrP6FNuoG8",
"isInheritable": false, "isInheritable": false,
"position": 240 "position": 240
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "47ZrP6FNuoG8", "value": "KC1HB96bqqHX",
"isInheritable": false, "isInheritable": false,
"position": 250 "position": 250
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "KC1HB96bqqHX", "value": "BFvAtE74rbP6",
"isInheritable": false, "isInheritable": false,
"position": 260 "position": 260
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "BFvAtE74rbP6", "value": "bdUJEHsAPYQR",
"isInheritable": false, "isInheritable": false,
"position": 270 "position": 270
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "bdUJEHsAPYQR", "value": "AxshuNRegLAv",
"isInheritable": false, "isInheritable": false,
"position": 280 "position": 280
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "AxshuNRegLAv", "value": "81SGnPGMk7Xc",
"isInheritable": false, "isInheritable": false,
"position": 290 "position": 290
}, },
{
"type": "relation",
"name": "internalLink",
"value": "81SGnPGMk7Xc",
"isInheritable": false,
"position": 300
},
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "xWbu3jpNWapp", "value": "xWbu3jpNWapp",
"isInheritable": false, "isInheritable": false,
"position": 310 "position": 300
}, },
{ {
"type": "label", "type": "label",
@ -10639,177 +10583,93 @@
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_globalNoteMap", "value": "YKWqdJhzi2VY",
"isInheritable": false, "isInheritable": false,
"position": 40 "position": 40
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_sqlConsole", "value": "ivYnonVFBxbQ",
"isInheritable": false, "isInheritable": false,
"position": 50 "position": 50
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "YKWqdJhzi2VY", "value": "eIg8jdvaoNNd",
"isInheritable": false, "isInheritable": false,
"position": 60 "position": 60
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_search", "value": "QEAPj01N5f7w",
"isInheritable": false, "isInheritable": false,
"position": 70 "position": 70
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_bulkAction", "value": "m1lbrzyKDaRB",
"isInheritable": false, "isInheritable": false,
"position": 80 "position": 80
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "ivYnonVFBxbQ", "value": "x3i7MxGccDuM",
"isInheritable": false, "isInheritable": false,
"position": 90 "position": 90
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_backendLog", "value": "bdUJEHsAPYQR",
"isInheritable": false, "isInheritable": false,
"position": 100 "position": 100
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_userHidden", "value": "xYmIYSP6wE3F",
"isInheritable": false, "isInheritable": false,
"position": 110 "position": 110
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_lbTplRoot", "value": "u3YFHC9tQlpm",
"isInheritable": false, "isInheritable": false,
"position": 120 "position": 120
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_share", "value": "qzNzp9LYQyPT",
"isInheritable": false, "isInheritable": false,
"position": 130 "position": 130
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_lbRoot", "value": "CdNpE2pqjmI6",
"isInheritable": false, "isInheritable": false,
"position": 140 "position": 140
}, },
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "_options", "value": "R9pX4DGra2Vt",
"isInheritable": false, "isInheritable": false,
"position": 150 "position": 150
}, },
{
"type": "relation",
"name": "internalLink",
"value": "_lbMobileRoot",
"isInheritable": false,
"position": 160
},
{
"type": "relation",
"name": "internalLink",
"value": "_help",
"isInheritable": false,
"position": 170
},
{
"type": "relation",
"name": "internalLink",
"value": "eIg8jdvaoNNd",
"isInheritable": false,
"position": 180
},
{
"type": "relation",
"name": "internalLink",
"value": "QEAPj01N5f7w",
"isInheritable": false,
"position": 190
},
{
"type": "relation",
"name": "internalLink",
"value": "m1lbrzyKDaRB",
"isInheritable": false,
"position": 200
},
{
"type": "relation",
"name": "internalLink",
"value": "x3i7MxGccDuM",
"isInheritable": false,
"position": 210
},
{
"type": "relation",
"name": "internalLink",
"value": "bdUJEHsAPYQR",
"isInheritable": false,
"position": 220
},
{
"type": "relation",
"name": "internalLink",
"value": "xYmIYSP6wE3F",
"isInheritable": false,
"position": 230
},
{
"type": "relation",
"name": "internalLink",
"value": "u3YFHC9tQlpm",
"isInheritable": false,
"position": 240
},
{
"type": "relation",
"name": "internalLink",
"value": "qzNzp9LYQyPT",
"isInheritable": false,
"position": 250
},
{
"type": "relation",
"name": "internalLink",
"value": "CdNpE2pqjmI6",
"isInheritable": false,
"position": 260
},
{
"type": "relation",
"name": "internalLink",
"value": "R9pX4DGra2Vt",
"isInheritable": false,
"position": 270
},
{ {
"type": "relation", "type": "relation",
"name": "internalLink", "name": "internalLink",
"value": "4TIF1oA4VQRO", "value": "4TIF1oA4VQRO",
"isInheritable": false, "isInheritable": false,
"position": 280 "position": 160
}, },
{ {
"type": "label", "type": "label",
@ -10831,6 +10691,33 @@
"dataFileName": "Hidden Notes_image.png" "dataFileName": "Hidden Notes_image.png"
} }
] ]
},
{
"isClone": false,
"noteId": "uYF7pmepw27K",
"notePath": [
"pOsGYCXsbNQG",
"tC7s2alapj8V",
"uYF7pmepw27K"
],
"title": "Metrics",
"notePosition": 240,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "iconClass",
"value": "bx bxs-data",
"isInheritable": false,
"position": 10
}
],
"format": "markdown",
"dataFileName": "Metrics.md",
"attachments": []
} }
] ]
}, },

File diff suppressed because one or more lines are too long

View File

@ -27,4 +27,4 @@ The code will:
1. First load the `config.ini` file as before 1. First load the `config.ini` file as before
2. Then scan all environment variables for ones starting with `TRILIUM_` 2. Then scan all environment variables for ones starting with `TRILIUM_`
3. Parse these variables into section/key pairs 3. Parse these variables into section/key pairs
4. Merge them with the config from the file, with environment variables taking precedence 4. Merge them with the config from the file, with environment variables taking precedence

View File

@ -0,0 +1,110 @@
# **Trilium Metrics API**
The Trilium metrics API provides comprehensive monitoring data about your Trilium instance, designed for external monitoring systems like Prometheus.
## **Endpoint**
* **URL**: `/etapi/metrics`
* **Method**: `GET`
* **Authentication**: ETAPI token required
* **Default Format**: Prometheus text format
## **Authentication**
You need an ETAPI token to access the metrics endpoint. Get one by:
```
# Get an ETAPI token
curl -X POST http://localhost:8080/etapi/auth/login \
-H "Content-Type: application/json" \
-d '{"password": "your_password"}'
```
## **Usage**
### **Prometheus Format (Default)**
```
curl -H "Authorization: YOUR_ETAPI_TOKEN" \
http://localhost:8080/etapi/metrics
```
Returns metrics in Prometheus text format:
```
# HELP trilium_info Trilium instance information
# TYPE trilium_info gauge
trilium_info{version="0.91.6",db_version="231",node_version="v18.17.0"} 1 1701432000
# HELP trilium_notes_total Total number of notes including deleted
# TYPE trilium_notes_total gauge
trilium_notes_total 1234 1701432000
```
### **JSON Format**
```
curl -H "Authorization: YOUR_ETAPI_TOKEN" \
"http://localhost:8080/etapi/metrics?format=json"
```
Returns detailed metrics in JSON format for debugging or custom integrations.
## **Available Metrics**
### **Instance Information**
* `trilium_info` - Version and build information with labels
### **Database Metrics**
* `trilium_notes_total` - Total notes (including deleted)
* `trilium_notes_deleted` - Number of deleted notes
* `trilium_notes_active` - Number of active notes
* `trilium_notes_protected` - Number of protected notes
* `trilium_attachments_total` - Total attachments
* `trilium_attachments_active` - Active attachments
* `trilium_revisions_total` - Total note revisions
* `trilium_branches_total` - Active branches
* `trilium_attributes_total` - Active attributes
* `trilium_blobs_total` - Total blob records
* `trilium_etapi_tokens_total` - Active ETAPI tokens
* `trilium_embeddings_total` - Note embeddings (if available)
### **Categorized Metrics**
* `trilium_notes_by_type{type="text|code|image|file"}` - Notes by type
* `trilium_attachments_by_type{mime_type="..."}` - Attachments by MIME type
### **Statistics**
* `trilium_database_size_bytes` - Database size in bytes
* `trilium_oldest_note_timestamp` - Timestamp of oldest note
* `trilium_newest_note_timestamp` - Timestamp of newest note
* `trilium_last_modified_timestamp` - Last modification timestamp
## **Prometheus Configuration**
Add to your `prometheus.yml`:
```
scrape_configs:
- job_name: 'trilium'
static_configs:
- targets: ['localhost:8080']
metrics_path: '/etapi/metrics'
headers:
Authorization: 'YOUR_ETAPI_TOKEN'
scrape_interval: 30s
```
## **Error Responses**
* `400` - Invalid format parameter
* `401` - Missing or invalid ETAPI token
* `500` - Internal server error