@cybermem/cli 0.6.3 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/reset.js +63 -0
- package/dist/index.js +5 -0
- package/dist/templates/docker-compose.yml +21 -1
- package/dist/templates/monitoring/db_exporter/exporter.py +303 -94
- package/dist/templates/monitoring/instructions_injector/Dockerfile +15 -0
- package/dist/templates/monitoring/instructions_injector/injector.py +137 -0
- package/dist/templates/monitoring/instructions_injector/requirements.txt +3 -0
- package/dist/templates/monitoring/log_exporter/exporter.py +7 -3
- package/package.json +1 -1
- package/templates/docker-compose.yml +21 -1
- package/templates/monitoring/db_exporter/exporter.py +303 -94
- package/templates/monitoring/instructions_injector/Dockerfile +15 -0
- package/templates/monitoring/instructions_injector/injector.py +137 -0
- package/templates/monitoring/instructions_injector/requirements.txt +3 -0
- package/templates/monitoring/log_exporter/exporter.py +7 -3
|
@@ -6,14 +6,14 @@ Queries OpenMemory's database and exports per-client metrics to Prometheus.
|
|
|
6
6
|
Replaces the complex Vector + Traefik access logs pipeline with simple DB queries.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import logging
|
|
9
10
|
import os
|
|
10
|
-
import time
|
|
11
11
|
import sqlite3
|
|
12
|
-
import json
|
|
13
|
-
from prometheus_client import Gauge, Info, generate_latest, CONTENT_TYPE_LATEST
|
|
14
|
-
from flask import Flask, Response, request, jsonify
|
|
15
|
-
import logging
|
|
16
12
|
import threading
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from flask import Flask, Response, jsonify, request
|
|
16
|
+
from prometheus_client import CONTENT_TYPE_LATEST, Gauge, Info, generate_latest
|
|
17
17
|
|
|
18
18
|
# Configuration
|
|
19
19
|
DB_PATH = os.getenv("DB_PATH", "/data/openmemory.sqlite")
|
|
@@ -22,71 +22,59 @@ EXPORTER_PORT = int(os.getenv("EXPORTER_PORT", "8000"))
|
|
|
22
22
|
|
|
23
23
|
# Setup logging
|
|
24
24
|
logging.basicConfig(
|
|
25
|
-
level=logging.INFO,
|
|
26
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
25
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
27
26
|
)
|
|
28
27
|
logger = logging.getLogger("db_exporter")
|
|
29
28
|
|
|
30
29
|
# Prometheus metrics
|
|
31
|
-
info = Info(
|
|
32
|
-
info.info({
|
|
30
|
+
info = Info("cybermem_exporter", "CyberMem Database Exporter Info")
|
|
31
|
+
info.info({"version": "1.0.0", "db_path": DB_PATH})
|
|
33
32
|
|
|
34
33
|
memories_total = Gauge(
|
|
35
|
-
|
|
36
|
-
'Total number of memories stored',
|
|
37
|
-
['client']
|
|
34
|
+
"openmemory_memories_total", "Total number of memories stored", ["client"]
|
|
38
35
|
)
|
|
39
36
|
|
|
40
37
|
memories_recent_24h = Gauge(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
[
|
|
38
|
+
"openmemory_memories_recent_24h",
|
|
39
|
+
"Memories created in the last 24 hours",
|
|
40
|
+
["client"],
|
|
44
41
|
)
|
|
45
42
|
|
|
46
43
|
memories_recent_1h = Gauge(
|
|
47
|
-
|
|
48
|
-
'Memories created in the last hour',
|
|
49
|
-
['client']
|
|
44
|
+
"openmemory_memories_recent_1h", "Memories created in the last hour", ["client"]
|
|
50
45
|
)
|
|
51
46
|
|
|
52
47
|
requests_by_operation = Gauge(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
[
|
|
48
|
+
"openmemory_requests_total",
|
|
49
|
+
"Total requests by client and operation (from cybermem_stats table)",
|
|
50
|
+
["client_name", "operation"],
|
|
56
51
|
)
|
|
57
52
|
|
|
58
53
|
errors_by_operation = Gauge(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
[
|
|
54
|
+
"openmemory_errors_total",
|
|
55
|
+
"Total errors by client and operation (from cybermem_stats table)",
|
|
56
|
+
["client_name", "operation"],
|
|
62
57
|
)
|
|
63
58
|
|
|
64
59
|
sectors_count = Gauge(
|
|
65
|
-
|
|
66
|
-
'Number of unique sectors per client',
|
|
67
|
-
['client']
|
|
60
|
+
"openmemory_sectors_total", "Number of unique sectors per client", ["client"]
|
|
68
61
|
)
|
|
69
62
|
|
|
70
|
-
avg_score = Gauge(
|
|
71
|
-
'openmemory_avg_score',
|
|
72
|
-
'Average score of memories',
|
|
73
|
-
['client']
|
|
74
|
-
)
|
|
63
|
+
avg_score = Gauge("openmemory_avg_score", "Average score of memories", ["client"])
|
|
75
64
|
|
|
76
65
|
# Aggregate metrics (not per-client)
|
|
77
66
|
total_requests_aggregate = Gauge(
|
|
78
|
-
|
|
79
|
-
|
|
67
|
+
"openmemory_requests_aggregate_total",
|
|
68
|
+
"Total API requests (aggregate, from stats table)",
|
|
80
69
|
)
|
|
81
70
|
|
|
82
71
|
total_errors_aggregate = Gauge(
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
"openmemory_errors_aggregate_total",
|
|
73
|
+
"Total API errors (aggregate, from stats table)",
|
|
85
74
|
)
|
|
86
75
|
|
|
87
76
|
success_rate_aggregate = Gauge(
|
|
88
|
-
|
|
89
|
-
'API success rate percentage (aggregate)'
|
|
77
|
+
"openmemory_success_rate_aggregate", "API success rate percentage (aggregate)"
|
|
90
78
|
)
|
|
91
79
|
|
|
92
80
|
|
|
@@ -108,79 +96,97 @@ def collect_metrics():
|
|
|
108
96
|
cursor = db.cursor()
|
|
109
97
|
|
|
110
98
|
# Metric 1: Total memories per client
|
|
111
|
-
cursor.execute(
|
|
99
|
+
cursor.execute("""
|
|
112
100
|
SELECT user_id as client, COUNT(*) as count
|
|
113
101
|
FROM memories
|
|
114
102
|
GROUP BY user_id
|
|
115
|
-
|
|
103
|
+
""")
|
|
116
104
|
for row in cursor.fetchall():
|
|
117
|
-
client = row[
|
|
118
|
-
memories_total.labels(client=client).set(row[
|
|
105
|
+
client = row["client"] or "anonymous"
|
|
106
|
+
memories_total.labels(client=client).set(row["count"])
|
|
119
107
|
|
|
120
108
|
logger.debug(f"Collected total memories for {cursor.rowcount} clients")
|
|
121
109
|
|
|
122
110
|
# Metric 2: Recent memories (24h)
|
|
123
111
|
# Note: created_at is stored as milliseconds since epoch
|
|
124
|
-
cursor.execute(
|
|
112
|
+
cursor.execute(
|
|
113
|
+
"""
|
|
125
114
|
SELECT user_id as client, COUNT(*) as count
|
|
126
115
|
FROM memories
|
|
127
116
|
WHERE created_at > ?
|
|
128
117
|
GROUP BY user_id
|
|
129
|
-
|
|
118
|
+
""",
|
|
119
|
+
[int((time.time() - 86400) * 1000)],
|
|
120
|
+
)
|
|
130
121
|
for row in cursor.fetchall():
|
|
131
|
-
client = row[
|
|
132
|
-
memories_recent_24h.labels(client=client).set(row[
|
|
122
|
+
client = row["client"] or "anonymous"
|
|
123
|
+
memories_recent_24h.labels(client=client).set(row["count"])
|
|
133
124
|
|
|
134
125
|
logger.debug(f"Collected 24h memories for {cursor.rowcount} clients")
|
|
135
126
|
|
|
136
127
|
# Metric 3: Recent memories (1h)
|
|
137
|
-
cursor.execute(
|
|
128
|
+
cursor.execute(
|
|
129
|
+
"""
|
|
138
130
|
SELECT user_id as client, COUNT(*) as count
|
|
139
131
|
FROM memories
|
|
140
132
|
WHERE created_at > ?
|
|
141
133
|
GROUP BY user_id
|
|
142
|
-
|
|
134
|
+
""",
|
|
135
|
+
[int((time.time() - 3600) * 1000)],
|
|
136
|
+
)
|
|
143
137
|
for row in cursor.fetchall():
|
|
144
|
-
client = row[
|
|
145
|
-
memories_recent_1h.labels(client=client).set(row[
|
|
138
|
+
client = row["client"] or "anonymous"
|
|
139
|
+
memories_recent_1h.labels(client=client).set(row["count"])
|
|
146
140
|
|
|
147
141
|
logger.debug(f"Collected 1h memories for {cursor.rowcount} clients")
|
|
148
142
|
|
|
149
143
|
# Metric 4: Per-client request stats from cybermem_stats table
|
|
150
|
-
cursor.execute(
|
|
144
|
+
cursor.execute("""
|
|
151
145
|
SELECT client_name, operation, count, errors
|
|
152
146
|
FROM cybermem_stats
|
|
153
|
-
|
|
147
|
+
""")
|
|
154
148
|
for row in cursor.fetchall():
|
|
155
|
-
client_name = row[
|
|
156
|
-
operation = row[
|
|
157
|
-
count = row[
|
|
158
|
-
errors = row[
|
|
159
|
-
requests_by_operation.labels(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
149
|
+
client_name = row["client_name"] or "unknown"
|
|
150
|
+
operation = row["operation"]
|
|
151
|
+
count = row["count"]
|
|
152
|
+
errors = row["errors"]
|
|
153
|
+
requests_by_operation.labels(
|
|
154
|
+
client_name=client_name, operation=operation
|
|
155
|
+
).set(count)
|
|
156
|
+
errors_by_operation.labels(
|
|
157
|
+
client_name=client_name, operation=operation
|
|
158
|
+
).set(errors)
|
|
159
|
+
|
|
160
|
+
logger.debug(
|
|
161
|
+
f"Collected request stats for {cursor.rowcount} client/operation pairs"
|
|
162
|
+
)
|
|
163
163
|
|
|
164
164
|
# Metric 5: Aggregate request stats from OpenMemory's stats table
|
|
165
165
|
# Note: stats table has no client_id, so these are aggregate only
|
|
166
166
|
hour_ago_ms = int((time.time() - 3600) * 1000)
|
|
167
167
|
|
|
168
168
|
# Get total requests (sum of qps snapshots)
|
|
169
|
-
cursor.execute(
|
|
169
|
+
cursor.execute(
|
|
170
|
+
"""
|
|
170
171
|
SELECT SUM(count) as total
|
|
171
172
|
FROM stats
|
|
172
173
|
WHERE type = 'qps' AND ts > ?
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
""",
|
|
175
|
+
[hour_ago_ms],
|
|
176
|
+
)
|
|
177
|
+
total_reqs = cursor.fetchone()["total"] or 0
|
|
175
178
|
total_requests_aggregate.set(total_reqs)
|
|
176
179
|
|
|
177
180
|
# Get total errors
|
|
178
|
-
cursor.execute(
|
|
181
|
+
cursor.execute(
|
|
182
|
+
"""
|
|
179
183
|
SELECT COUNT(*) as total
|
|
180
184
|
FROM stats
|
|
181
185
|
WHERE type = 'error' AND ts > ?
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
""",
|
|
187
|
+
[hour_ago_ms],
|
|
188
|
+
)
|
|
189
|
+
total_errs = cursor.fetchone()["total"] or 0
|
|
184
190
|
total_errors_aggregate.set(total_errs)
|
|
185
191
|
|
|
186
192
|
# Calculate success rate
|
|
@@ -193,28 +199,28 @@ def collect_metrics():
|
|
|
193
199
|
logger.debug(f"Collected aggregate stats: {total_reqs} reqs, {total_errs} errs")
|
|
194
200
|
|
|
195
201
|
# Metric 5: Number of unique sectors per client
|
|
196
|
-
cursor.execute(
|
|
202
|
+
cursor.execute("""
|
|
197
203
|
SELECT user_id as client, COUNT(DISTINCT primary_sector) as count
|
|
198
204
|
FROM memories
|
|
199
205
|
WHERE primary_sector IS NOT NULL
|
|
200
206
|
GROUP BY user_id
|
|
201
|
-
|
|
207
|
+
""")
|
|
202
208
|
for row in cursor.fetchall():
|
|
203
|
-
client = row[
|
|
204
|
-
sectors_count.labels(client=client).set(row[
|
|
209
|
+
client = row["client"] or "anonymous"
|
|
210
|
+
sectors_count.labels(client=client).set(row["count"])
|
|
205
211
|
|
|
206
212
|
logger.debug(f"Collected sectors count for {cursor.rowcount} clients")
|
|
207
213
|
|
|
208
214
|
# Metric 6: Average feedback score per client
|
|
209
|
-
cursor.execute(
|
|
215
|
+
cursor.execute("""
|
|
210
216
|
SELECT user_id as client, AVG(feedback_score) as avg_score
|
|
211
217
|
FROM memories
|
|
212
218
|
WHERE feedback_score IS NOT NULL
|
|
213
219
|
GROUP BY user_id
|
|
214
|
-
|
|
220
|
+
""")
|
|
215
221
|
for row in cursor.fetchall():
|
|
216
|
-
client = row[
|
|
217
|
-
avg_score.labels(client=client).set(row[
|
|
222
|
+
client = row["client"] or "anonymous"
|
|
223
|
+
avg_score.labels(client=client).set(row["avg_score"] or 0)
|
|
218
224
|
|
|
219
225
|
logger.debug(f"Collected average scores for {cursor.rowcount} clients")
|
|
220
226
|
|
|
@@ -231,26 +237,31 @@ def get_logs_from_db(start_ms: int, limit: int = 100):
|
|
|
231
237
|
db = get_db_connection()
|
|
232
238
|
cursor = db.cursor()
|
|
233
239
|
|
|
234
|
-
cursor.execute(
|
|
240
|
+
cursor.execute(
|
|
241
|
+
"""
|
|
235
242
|
SELECT timestamp, client_name, client_version, method, endpoint, operation, status, is_error
|
|
236
243
|
FROM cybermem_access_log
|
|
237
244
|
WHERE timestamp >= ?
|
|
238
245
|
ORDER BY timestamp DESC
|
|
239
246
|
LIMIT ?
|
|
240
|
-
|
|
247
|
+
""",
|
|
248
|
+
[start_ms, limit],
|
|
249
|
+
)
|
|
241
250
|
|
|
242
251
|
logs = []
|
|
243
252
|
for row in cursor.fetchall():
|
|
244
|
-
logs.append(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
253
|
+
logs.append(
|
|
254
|
+
{
|
|
255
|
+
"timestamp": row["timestamp"],
|
|
256
|
+
"client_name": row["client_name"],
|
|
257
|
+
"client_version": row["client_version"],
|
|
258
|
+
"method": row["method"],
|
|
259
|
+
"endpoint": row["endpoint"],
|
|
260
|
+
"operation": row["operation"],
|
|
261
|
+
"status": row["status"],
|
|
262
|
+
"is_error": bool(row["is_error"]),
|
|
263
|
+
}
|
|
264
|
+
)
|
|
254
265
|
|
|
255
266
|
db.close()
|
|
256
267
|
return logs
|
|
@@ -262,23 +273,220 @@ def get_logs_from_db(start_ms: int, limit: int = 100):
|
|
|
262
273
|
# Create Flask app
|
|
263
274
|
app = Flask(__name__)
|
|
264
275
|
|
|
265
|
-
|
|
276
|
+
|
|
277
|
+
@app.route("/metrics")
|
|
266
278
|
def metrics():
|
|
267
279
|
"""Prometheus metrics endpoint"""
|
|
268
280
|
return Response(generate_latest(), mimetype=CONTENT_TYPE_LATEST)
|
|
269
281
|
|
|
270
|
-
|
|
282
|
+
|
|
283
|
+
@app.route("/api/logs")
|
|
271
284
|
def api_logs():
|
|
272
285
|
"""Access logs API endpoint"""
|
|
273
286
|
try:
|
|
274
|
-
start_ms = int(request.args.get(
|
|
275
|
-
limit = int(request.args.get(
|
|
287
|
+
start_ms = int(request.args.get("start", 0))
|
|
288
|
+
limit = int(request.args.get("limit", 100))
|
|
276
289
|
|
|
277
290
|
logs = get_logs_from_db(start_ms, limit)
|
|
278
|
-
return jsonify({
|
|
291
|
+
return jsonify({"logs": logs})
|
|
279
292
|
except Exception as e:
|
|
280
293
|
logger.error(f"Error in /api/logs: {e}", exc_info=True)
|
|
281
|
-
return jsonify({
|
|
294
|
+
return jsonify({"error": str(e)}), 500
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@app.route("/api/stats")
|
|
298
|
+
def api_stats():
|
|
299
|
+
"""Dashboard stats API endpoint - single source of truth for dashboard metrics"""
|
|
300
|
+
try:
|
|
301
|
+
db = get_db_connection()
|
|
302
|
+
cursor = db.cursor()
|
|
303
|
+
|
|
304
|
+
# 1. Total memory records
|
|
305
|
+
cursor.execute("SELECT COUNT(*) as count FROM memories")
|
|
306
|
+
memory_records = cursor.fetchone()["count"] or 0
|
|
307
|
+
|
|
308
|
+
# 2. Unique clients (from memories table)
|
|
309
|
+
cursor.execute("SELECT COUNT(DISTINCT user_id) as count FROM memories")
|
|
310
|
+
total_clients = cursor.fetchone()["count"] or 0
|
|
311
|
+
|
|
312
|
+
# 3. Total requests and errors from cybermem_stats
|
|
313
|
+
cursor.execute(
|
|
314
|
+
"SELECT SUM(count) as total, SUM(errors) as errors FROM cybermem_stats"
|
|
315
|
+
)
|
|
316
|
+
row = cursor.fetchone()
|
|
317
|
+
total_requests = row["total"] or 0
|
|
318
|
+
total_errors = row["errors"] or 0
|
|
319
|
+
success_rate = (
|
|
320
|
+
((total_requests - total_errors) / total_requests * 100)
|
|
321
|
+
if total_requests > 0
|
|
322
|
+
else 100.0
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# 4. Top writer (client with most writes)
|
|
326
|
+
cursor.execute("""
|
|
327
|
+
SELECT client_name, SUM(count) as total
|
|
328
|
+
FROM cybermem_stats
|
|
329
|
+
WHERE operation IN ('create', 'update', 'delete')
|
|
330
|
+
GROUP BY client_name
|
|
331
|
+
ORDER BY total DESC
|
|
332
|
+
LIMIT 1
|
|
333
|
+
""")
|
|
334
|
+
top_writer_row = cursor.fetchone()
|
|
335
|
+
top_writer = {
|
|
336
|
+
"name": top_writer_row["client_name"] if top_writer_row else "N/A",
|
|
337
|
+
"count": top_writer_row["total"] if top_writer_row else 0,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# 5. Top reader
|
|
341
|
+
cursor.execute("""
|
|
342
|
+
SELECT client_name, SUM(count) as total
|
|
343
|
+
FROM cybermem_stats
|
|
344
|
+
WHERE operation IN ('read', 'list', 'query', 'search', 'other')
|
|
345
|
+
GROUP BY client_name
|
|
346
|
+
ORDER BY total DESC
|
|
347
|
+
LIMIT 1
|
|
348
|
+
""")
|
|
349
|
+
top_reader_row = cursor.fetchone()
|
|
350
|
+
top_reader = {
|
|
351
|
+
"name": top_reader_row["client_name"] if top_reader_row else "N/A",
|
|
352
|
+
"count": top_reader_row["total"] if top_reader_row else 0,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
# 6. Last writer (most recent write operation from access log)
|
|
356
|
+
cursor.execute("""
|
|
357
|
+
SELECT client_name, timestamp
|
|
358
|
+
FROM cybermem_access_log
|
|
359
|
+
WHERE operation IN ('create', 'update', 'delete')
|
|
360
|
+
ORDER BY timestamp DESC
|
|
361
|
+
LIMIT 1
|
|
362
|
+
""")
|
|
363
|
+
last_writer_row = cursor.fetchone()
|
|
364
|
+
last_writer = {
|
|
365
|
+
"name": last_writer_row["client_name"] if last_writer_row else "N/A",
|
|
366
|
+
"timestamp": last_writer_row["timestamp"] if last_writer_row else 0,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
# 7. Last reader
|
|
370
|
+
cursor.execute("""
|
|
371
|
+
SELECT client_name, timestamp
|
|
372
|
+
FROM cybermem_access_log
|
|
373
|
+
WHERE operation IN ('read', 'list', 'query', 'search', 'other')
|
|
374
|
+
ORDER BY timestamp DESC
|
|
375
|
+
LIMIT 1
|
|
376
|
+
""")
|
|
377
|
+
last_reader_row = cursor.fetchone()
|
|
378
|
+
last_reader = {
|
|
379
|
+
"name": last_reader_row["client_name"] if last_reader_row else "N/A",
|
|
380
|
+
"timestamp": last_reader_row["timestamp"] if last_reader_row else 0,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
db.close()
|
|
384
|
+
|
|
385
|
+
return jsonify(
|
|
386
|
+
{
|
|
387
|
+
"memoryRecords": memory_records,
|
|
388
|
+
"totalClients": total_clients,
|
|
389
|
+
"successRate": round(success_rate, 1),
|
|
390
|
+
"totalRequests": total_requests,
|
|
391
|
+
"topWriter": top_writer,
|
|
392
|
+
"topReader": top_reader,
|
|
393
|
+
"lastWriter": last_writer,
|
|
394
|
+
"lastReader": last_reader,
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
except Exception as e:
|
|
398
|
+
logger.error(f"Error in /api/stats: {e}", exc_info=True)
|
|
399
|
+
return jsonify({"error": str(e)}), 500
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@app.route("/api/timeseries")
|
|
403
|
+
def api_timeseries():
|
|
404
|
+
"""Time series data for dashboard charts - bucketed by hour"""
|
|
405
|
+
try:
|
|
406
|
+
period = request.args.get("period", "24h")
|
|
407
|
+
|
|
408
|
+
# Parse period to milliseconds
|
|
409
|
+
period_map = {"1h": 3600, "24h": 86400, "7d": 604800, "30d": 2592000}
|
|
410
|
+
period_seconds = period_map.get(period, 86400)
|
|
411
|
+
start_ms = int((time.time() - period_seconds) * 1000)
|
|
412
|
+
|
|
413
|
+
# Bucket size: 1h for 24h, 6h for 7d, 1d for 30d
|
|
414
|
+
if period in ["1h", "24h"]:
|
|
415
|
+
bucket_format = "%Y-%m-%d %H:00"
|
|
416
|
+
bucket_seconds = 3600
|
|
417
|
+
elif period == "7d":
|
|
418
|
+
bucket_format = "%Y-%m-%d %H:00"
|
|
419
|
+
bucket_seconds = 21600 # 6 hours
|
|
420
|
+
else:
|
|
421
|
+
bucket_format = "%Y-%m-%d"
|
|
422
|
+
bucket_seconds = 86400
|
|
423
|
+
|
|
424
|
+
db = get_db_connection()
|
|
425
|
+
cursor = db.cursor()
|
|
426
|
+
|
|
427
|
+
# Get operations grouped by time bucket and client
|
|
428
|
+
cursor.execute(
|
|
429
|
+
"""
|
|
430
|
+
SELECT
|
|
431
|
+
datetime(timestamp/1000, 'unixepoch', 'localtime') as dt,
|
|
432
|
+
client_name,
|
|
433
|
+
operation,
|
|
434
|
+
COUNT(*) as count
|
|
435
|
+
FROM cybermem_access_log
|
|
436
|
+
WHERE timestamp >= ?
|
|
437
|
+
GROUP BY strftime(?, datetime(timestamp/1000, 'unixepoch', 'localtime')), client_name, operation
|
|
438
|
+
ORDER BY dt
|
|
439
|
+
""",
|
|
440
|
+
[start_ms, bucket_format],
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Organize by operation type
|
|
444
|
+
creates = {}
|
|
445
|
+
reads = {}
|
|
446
|
+
updates = {}
|
|
447
|
+
deletes = {}
|
|
448
|
+
|
|
449
|
+
for row in cursor.fetchall():
|
|
450
|
+
dt = row["dt"]
|
|
451
|
+
client = row["client_name"] or "unknown"
|
|
452
|
+
op = row["operation"]
|
|
453
|
+
count = row["count"]
|
|
454
|
+
|
|
455
|
+
# Round to bucket
|
|
456
|
+
ts = int(time.mktime(time.strptime(dt, "%Y-%m-%d %H:%M:%S")))
|
|
457
|
+
bucket_ts = (ts // bucket_seconds) * bucket_seconds
|
|
458
|
+
|
|
459
|
+
if op == "create":
|
|
460
|
+
if bucket_ts not in creates:
|
|
461
|
+
creates[bucket_ts] = {"time": bucket_ts}
|
|
462
|
+
creates[bucket_ts][client] = creates[bucket_ts].get(client, 0) + count
|
|
463
|
+
elif op in ["read", "list", "query", "search", "other"]:
|
|
464
|
+
if bucket_ts not in reads:
|
|
465
|
+
reads[bucket_ts] = {"time": bucket_ts}
|
|
466
|
+
reads[bucket_ts][client] = reads[bucket_ts].get(client, 0) + count
|
|
467
|
+
elif op == "update":
|
|
468
|
+
if bucket_ts not in updates:
|
|
469
|
+
updates[bucket_ts] = {"time": bucket_ts}
|
|
470
|
+
updates[bucket_ts][client] = updates[bucket_ts].get(client, 0) + count
|
|
471
|
+
elif op == "delete":
|
|
472
|
+
if bucket_ts not in deletes:
|
|
473
|
+
deletes[bucket_ts] = {"time": bucket_ts}
|
|
474
|
+
deletes[bucket_ts][client] = deletes[bucket_ts].get(client, 0) + count
|
|
475
|
+
|
|
476
|
+
db.close()
|
|
477
|
+
|
|
478
|
+
return jsonify(
|
|
479
|
+
{
|
|
480
|
+
"creates": list(creates.values()),
|
|
481
|
+
"reads": list(reads.values()),
|
|
482
|
+
"updates": list(updates.values()),
|
|
483
|
+
"deletes": list(deletes.values()),
|
|
484
|
+
}
|
|
485
|
+
)
|
|
486
|
+
except Exception as e:
|
|
487
|
+
logger.error(f"Error in /api/timeseries: {e}", exc_info=True)
|
|
488
|
+
return jsonify({"error": str(e)}), 500
|
|
489
|
+
|
|
282
490
|
|
|
283
491
|
def metrics_collection_loop():
|
|
284
492
|
"""Background thread for collecting metrics"""
|
|
@@ -291,6 +499,7 @@ def metrics_collection_loop():
|
|
|
291
499
|
logger.error(f"Error in metrics collection: {e}", exc_info=True)
|
|
292
500
|
time.sleep(SCRAPE_INTERVAL)
|
|
293
501
|
|
|
502
|
+
|
|
294
503
|
def main():
|
|
295
504
|
"""Start the exporter and metrics collection loop."""
|
|
296
505
|
logger.info(f"Starting CyberMem Database Exporter on port {EXPORTER_PORT}")
|
|
@@ -306,8 +515,8 @@ def main():
|
|
|
306
515
|
logger.info(f" Metrics: http://0.0.0.0:{EXPORTER_PORT}/metrics")
|
|
307
516
|
logger.info(f" Logs API: http://0.0.0.0:{EXPORTER_PORT}/api/logs")
|
|
308
517
|
|
|
309
|
-
app.run(host=
|
|
518
|
+
app.run(host="0.0.0.0", port=EXPORTER_PORT, threaded=True)
|
|
310
519
|
|
|
311
520
|
|
|
312
|
-
if __name__ ==
|
|
521
|
+
if __name__ == "__main__":
|
|
313
522
|
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
FROM python:3.11-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
COPY requirements.txt .
|
|
6
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
7
|
+
|
|
8
|
+
COPY injector.py .
|
|
9
|
+
|
|
10
|
+
ENV UPSTREAM_URL=http://openmemory:8080
|
|
11
|
+
ENV PORT=8081
|
|
12
|
+
|
|
13
|
+
EXPOSE 8081
|
|
14
|
+
|
|
15
|
+
CMD ["gunicorn", "-b", "0.0.0.0:8081", "-w", "2", "injector:app"]
|