@stravigor/devtools 0.1.0
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/package.json +22 -0
- package/src/collectors/collector.ts +65 -0
- package/src/collectors/exception_collector.ts +56 -0
- package/src/collectors/job_collector.ts +117 -0
- package/src/collectors/log_collector.ts +69 -0
- package/src/collectors/query_collector.ts +106 -0
- package/src/collectors/request_collector.ts +126 -0
- package/src/commands/devtools_prune.ts +55 -0
- package/src/dashboard/middleware.ts +42 -0
- package/src/dashboard/routes.ts +509 -0
- package/src/devtools_manager.ts +244 -0
- package/src/errors.ts +3 -0
- package/src/helpers.ts +82 -0
- package/src/index.ts +41 -0
- package/src/recorders/recorder.ts +51 -0
- package/src/recorders/slow_queries.ts +44 -0
- package/src/recorders/slow_requests.ts +44 -0
- package/src/storage/aggregate_store.ts +195 -0
- package/src/storage/entry_store.ts +160 -0
- package/src/types.ts +81 -0
- package/stubs/config/devtools.ts +24 -0
- package/stubs/schemas/devtools_aggregates.ts +14 -0
- package/stubs/schemas/devtools_entries.ts +13 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { SQL } from 'bun'
|
|
2
|
+
import type { AggregateRecord, AggregateFunction } from '../types.ts'
|
|
3
|
+
|
|
4
|
+
/** Aggregation periods in seconds. */
|
|
5
|
+
export const PERIODS = {
|
|
6
|
+
ONE_HOUR: 3600,
|
|
7
|
+
SIX_HOURS: 21600,
|
|
8
|
+
ONE_DAY: 86400,
|
|
9
|
+
SEVEN_DAYS: 604800,
|
|
10
|
+
} as const
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Stores and queries pre-aggregated metric buckets in `_strav_devtools_aggregates`.
|
|
14
|
+
*
|
|
15
|
+
* Used by recorders to store slow request counts, slow query counts, etc.
|
|
16
|
+
* The dashboard reads these for time-series charts without scanning raw entries.
|
|
17
|
+
*/
|
|
18
|
+
export default class AggregateStore {
|
|
19
|
+
constructor(private sql: SQL) {}
|
|
20
|
+
|
|
21
|
+
/** Create the aggregates table if it doesn't exist. */
|
|
22
|
+
async ensureTable(): Promise<void> {
|
|
23
|
+
await this.sql`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS "_strav_devtools_aggregates" (
|
|
25
|
+
"id" BIGSERIAL PRIMARY KEY,
|
|
26
|
+
"bucket" INT NOT NULL,
|
|
27
|
+
"period" INT NOT NULL,
|
|
28
|
+
"type" VARCHAR(30) NOT NULL,
|
|
29
|
+
"key" TEXT NOT NULL DEFAULT '',
|
|
30
|
+
"aggregate" VARCHAR(10) NOT NULL,
|
|
31
|
+
"value" NUMERIC(20, 2) NOT NULL DEFAULT 0,
|
|
32
|
+
"count" INT,
|
|
33
|
+
UNIQUE ("bucket", "period", "type", "aggregate", "key")
|
|
34
|
+
)
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
await this.sql`
|
|
38
|
+
CREATE INDEX IF NOT EXISTS "idx_strav_devtools_aggregates_lookup"
|
|
39
|
+
ON "_strav_devtools_aggregates" ("type", "period", "bucket" DESC)
|
|
40
|
+
`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Record a value into the appropriate time buckets.
|
|
45
|
+
* Updates existing bucket rows via upsert (INSERT ... ON CONFLICT).
|
|
46
|
+
*/
|
|
47
|
+
async record(
|
|
48
|
+
type: string,
|
|
49
|
+
key: string,
|
|
50
|
+
value: number,
|
|
51
|
+
aggregates: AggregateFunction[]
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const now = Math.floor(Date.now() / 1000)
|
|
54
|
+
|
|
55
|
+
for (const period of Object.values(PERIODS)) {
|
|
56
|
+
const bucket = Math.floor(now / period) * period
|
|
57
|
+
|
|
58
|
+
for (const agg of aggregates) {
|
|
59
|
+
await this.upsert(bucket, period, type, key, agg, value)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Query aggregated data for a type over a time range. */
|
|
65
|
+
async query(
|
|
66
|
+
type: string,
|
|
67
|
+
period: number,
|
|
68
|
+
aggregate: AggregateFunction,
|
|
69
|
+
limit = 24
|
|
70
|
+
): Promise<AggregateRecord[]> {
|
|
71
|
+
const rows = await this.sql`
|
|
72
|
+
SELECT * FROM "_strav_devtools_aggregates"
|
|
73
|
+
WHERE "type" = ${type}
|
|
74
|
+
AND "period" = ${period}
|
|
75
|
+
AND "aggregate" = ${aggregate}
|
|
76
|
+
ORDER BY "bucket" DESC
|
|
77
|
+
LIMIT ${limit}
|
|
78
|
+
`
|
|
79
|
+
return rows.map(hydrateAggregate)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Query top keys by aggregate value for a type. */
|
|
83
|
+
async topKeys(
|
|
84
|
+
type: string,
|
|
85
|
+
period: number,
|
|
86
|
+
aggregate: AggregateFunction,
|
|
87
|
+
limit = 10
|
|
88
|
+
): Promise<AggregateRecord[]> {
|
|
89
|
+
const now = Math.floor(Date.now() / 1000)
|
|
90
|
+
const since = now - period
|
|
91
|
+
|
|
92
|
+
const rows = await this.sql`
|
|
93
|
+
SELECT "key", SUM("value") AS "value", SUM("count") AS "count",
|
|
94
|
+
MAX("bucket") AS "bucket", ${period}::int AS "period",
|
|
95
|
+
${type} AS "type", ${aggregate} AS "aggregate", 0 AS "id"
|
|
96
|
+
FROM "_strav_devtools_aggregates"
|
|
97
|
+
WHERE "type" = ${type}
|
|
98
|
+
AND "period" = ${period}
|
|
99
|
+
AND "aggregate" = ${aggregate}
|
|
100
|
+
AND "bucket" >= ${since}
|
|
101
|
+
GROUP BY "key"
|
|
102
|
+
ORDER BY SUM("value") DESC
|
|
103
|
+
LIMIT ${limit}
|
|
104
|
+
`
|
|
105
|
+
return rows.map(hydrateAggregate)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Delete aggregates older than the given number of hours. */
|
|
109
|
+
async prune(hours: number): Promise<number> {
|
|
110
|
+
const cutoff = Math.floor(Date.now() / 1000) - hours * 3600
|
|
111
|
+
|
|
112
|
+
const rows = await this.sql`
|
|
113
|
+
DELETE FROM "_strav_devtools_aggregates"
|
|
114
|
+
WHERE "bucket" < ${cutoff}
|
|
115
|
+
`
|
|
116
|
+
return rows.count
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async upsert(
|
|
120
|
+
bucket: number,
|
|
121
|
+
period: number,
|
|
122
|
+
type: string,
|
|
123
|
+
key: string,
|
|
124
|
+
aggregate: AggregateFunction,
|
|
125
|
+
value: number
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
switch (aggregate) {
|
|
128
|
+
case 'count':
|
|
129
|
+
await this.sql`
|
|
130
|
+
INSERT INTO "_strav_devtools_aggregates" ("bucket", "period", "type", "key", "aggregate", "value", "count")
|
|
131
|
+
VALUES (${bucket}, ${period}, ${type}, ${key}, 'count', 1, 1)
|
|
132
|
+
ON CONFLICT ("bucket", "period", "type", "aggregate", "key")
|
|
133
|
+
DO UPDATE SET "value" = "_strav_devtools_aggregates"."value" + 1,
|
|
134
|
+
"count" = COALESCE("_strav_devtools_aggregates"."count", 0) + 1
|
|
135
|
+
`
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
case 'sum':
|
|
139
|
+
await this.sql`
|
|
140
|
+
INSERT INTO "_strav_devtools_aggregates" ("bucket", "period", "type", "key", "aggregate", "value", "count")
|
|
141
|
+
VALUES (${bucket}, ${period}, ${type}, ${key}, 'sum', ${value}, 1)
|
|
142
|
+
ON CONFLICT ("bucket", "period", "type", "aggregate", "key")
|
|
143
|
+
DO UPDATE SET "value" = "_strav_devtools_aggregates"."value" + ${value},
|
|
144
|
+
"count" = COALESCE("_strav_devtools_aggregates"."count", 0) + 1
|
|
145
|
+
`
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
case 'max':
|
|
149
|
+
await this.sql`
|
|
150
|
+
INSERT INTO "_strav_devtools_aggregates" ("bucket", "period", "type", "key", "aggregate", "value", "count")
|
|
151
|
+
VALUES (${bucket}, ${period}, ${type}, ${key}, 'max', ${value}, 1)
|
|
152
|
+
ON CONFLICT ("bucket", "period", "type", "aggregate", "key")
|
|
153
|
+
DO UPDATE SET "value" = GREATEST("_strav_devtools_aggregates"."value", ${value}),
|
|
154
|
+
"count" = COALESCE("_strav_devtools_aggregates"."count", 0) + 1
|
|
155
|
+
`
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
case 'min':
|
|
159
|
+
await this.sql`
|
|
160
|
+
INSERT INTO "_strav_devtools_aggregates" ("bucket", "period", "type", "key", "aggregate", "value", "count")
|
|
161
|
+
VALUES (${bucket}, ${period}, ${type}, ${key}, 'min', ${value}, 1)
|
|
162
|
+
ON CONFLICT ("bucket", "period", "type", "aggregate", "key")
|
|
163
|
+
DO UPDATE SET "value" = LEAST("_strav_devtools_aggregates"."value", ${value}),
|
|
164
|
+
"count" = COALESCE("_strav_devtools_aggregates"."count", 0) + 1
|
|
165
|
+
`
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
case 'avg':
|
|
169
|
+
await this.sql`
|
|
170
|
+
INSERT INTO "_strav_devtools_aggregates" ("bucket", "period", "type", "key", "aggregate", "value", "count")
|
|
171
|
+
VALUES (${bucket}, ${period}, ${type}, ${key}, 'avg', ${value}, 1)
|
|
172
|
+
ON CONFLICT ("bucket", "period", "type", "aggregate", "key")
|
|
173
|
+
DO UPDATE SET "value" = (
|
|
174
|
+
"_strav_devtools_aggregates"."value" * COALESCE("_strav_devtools_aggregates"."count", 1)
|
|
175
|
+
+ ${value}
|
|
176
|
+
) / (COALESCE("_strav_devtools_aggregates"."count", 1) + 1),
|
|
177
|
+
"count" = COALESCE("_strav_devtools_aggregates"."count", 0) + 1
|
|
178
|
+
`
|
|
179
|
+
break
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function hydrateAggregate(row: Record<string, unknown>): AggregateRecord {
|
|
185
|
+
return {
|
|
186
|
+
id: Number(row.id),
|
|
187
|
+
bucket: Number(row.bucket),
|
|
188
|
+
period: Number(row.period),
|
|
189
|
+
type: row.type as string,
|
|
190
|
+
key: row.key as string,
|
|
191
|
+
aggregate: row.aggregate as AggregateFunction,
|
|
192
|
+
value: Number(row.value),
|
|
193
|
+
count: row.count != null ? Number(row.count) : null,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { SQL } from 'bun'
|
|
2
|
+
import type { DevtoolsEntry, EntryRecord, EntryType } from '../types.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stores and queries raw devtools entries in `_strav_devtools_entries`.
|
|
6
|
+
*
|
|
7
|
+
* Each entry represents a single recorded event (request, query, exception, etc.)
|
|
8
|
+
* and belongs to a batch (all entries from a single request/job share a batchId).
|
|
9
|
+
*/
|
|
10
|
+
export default class EntryStore {
|
|
11
|
+
constructor(private sql: SQL) {}
|
|
12
|
+
|
|
13
|
+
/** Create the entries table if it doesn't exist. */
|
|
14
|
+
async ensureTable(): Promise<void> {
|
|
15
|
+
await this.sql`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS "_strav_devtools_entries" (
|
|
17
|
+
"id" BIGSERIAL PRIMARY KEY,
|
|
18
|
+
"uuid" UUID NOT NULL,
|
|
19
|
+
"batch_id" UUID NOT NULL,
|
|
20
|
+
"type" VARCHAR(30) NOT NULL,
|
|
21
|
+
"family_hash" VARCHAR(64),
|
|
22
|
+
"content" JSONB NOT NULL DEFAULT '{}',
|
|
23
|
+
"tags" TEXT[] NOT NULL DEFAULT '{}',
|
|
24
|
+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
25
|
+
)
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
await this.sql`
|
|
29
|
+
CREATE INDEX IF NOT EXISTS "idx_strav_devtools_entries_batch"
|
|
30
|
+
ON "_strav_devtools_entries" ("batch_id")
|
|
31
|
+
`
|
|
32
|
+
|
|
33
|
+
await this.sql`
|
|
34
|
+
CREATE INDEX IF NOT EXISTS "idx_strav_devtools_entries_type_created"
|
|
35
|
+
ON "_strav_devtools_entries" ("type", "created_at" DESC)
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
await this.sql`
|
|
39
|
+
CREATE INDEX IF NOT EXISTS "idx_strav_devtools_entries_family_hash"
|
|
40
|
+
ON "_strav_devtools_entries" ("family_hash")
|
|
41
|
+
WHERE "family_hash" IS NOT NULL
|
|
42
|
+
`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Insert one or more entries. */
|
|
46
|
+
async store(entries: DevtoolsEntry[]): Promise<void> {
|
|
47
|
+
if (entries.length === 0) return
|
|
48
|
+
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const tagsLiteral = `{${entry.tags.map(t => `"${t.replace(/"/g, '\\"')}"`).join(',')}}`
|
|
51
|
+
await this.sql`
|
|
52
|
+
INSERT INTO "_strav_devtools_entries"
|
|
53
|
+
("uuid", "batch_id", "type", "family_hash", "content", "tags", "created_at")
|
|
54
|
+
VALUES (
|
|
55
|
+
${entry.uuid},
|
|
56
|
+
${entry.batchId},
|
|
57
|
+
${entry.type},
|
|
58
|
+
${entry.familyHash},
|
|
59
|
+
${JSON.stringify(entry.content)},
|
|
60
|
+
${tagsLiteral}::TEXT[],
|
|
61
|
+
${entry.createdAt}
|
|
62
|
+
)
|
|
63
|
+
`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** List entries by type, most recent first. */
|
|
68
|
+
async list(type?: EntryType, limit = 50, offset = 0): Promise<EntryRecord[]> {
|
|
69
|
+
let rows
|
|
70
|
+
|
|
71
|
+
if (type) {
|
|
72
|
+
rows = await this.sql`
|
|
73
|
+
SELECT * FROM "_strav_devtools_entries"
|
|
74
|
+
WHERE "type" = ${type}
|
|
75
|
+
ORDER BY "created_at" DESC
|
|
76
|
+
LIMIT ${limit} OFFSET ${offset}
|
|
77
|
+
`
|
|
78
|
+
} else {
|
|
79
|
+
rows = await this.sql`
|
|
80
|
+
SELECT * FROM "_strav_devtools_entries"
|
|
81
|
+
ORDER BY "created_at" DESC
|
|
82
|
+
LIMIT ${limit} OFFSET ${offset}
|
|
83
|
+
`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return rows.map(hydrateEntry)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Find a single entry by UUID. */
|
|
90
|
+
async find(uuid: string): Promise<EntryRecord | null> {
|
|
91
|
+
const rows = await this.sql`
|
|
92
|
+
SELECT * FROM "_strav_devtools_entries"
|
|
93
|
+
WHERE "uuid" = ${uuid}
|
|
94
|
+
LIMIT 1
|
|
95
|
+
`
|
|
96
|
+
return rows.length > 0 ? hydrateEntry(rows[0] as Record<string, unknown>) : null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Find all entries in a batch, for cross-referencing. */
|
|
100
|
+
async batch(batchId: string): Promise<EntryRecord[]> {
|
|
101
|
+
const rows = await this.sql`
|
|
102
|
+
SELECT * FROM "_strav_devtools_entries"
|
|
103
|
+
WHERE "batch_id" = ${batchId}
|
|
104
|
+
ORDER BY "created_at" ASC
|
|
105
|
+
`
|
|
106
|
+
return rows.map(hydrateEntry)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Search entries by tag. */
|
|
110
|
+
async byTag(tag: string, limit = 50): Promise<EntryRecord[]> {
|
|
111
|
+
const rows = await this.sql`
|
|
112
|
+
SELECT * FROM "_strav_devtools_entries"
|
|
113
|
+
WHERE ${tag} = ANY("tags")
|
|
114
|
+
ORDER BY "created_at" DESC
|
|
115
|
+
LIMIT ${limit}
|
|
116
|
+
`
|
|
117
|
+
return rows.map(hydrateEntry)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Delete entries older than the given number of hours. */
|
|
121
|
+
async prune(hours: number): Promise<number> {
|
|
122
|
+
const rows = await this.sql`
|
|
123
|
+
DELETE FROM "_strav_devtools_entries"
|
|
124
|
+
WHERE "created_at" < NOW() - MAKE_INTERVAL(hours => ${hours})
|
|
125
|
+
`
|
|
126
|
+
return rows.count
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Count entries, optionally filtered by type. */
|
|
130
|
+
async count(type?: EntryType): Promise<number> {
|
|
131
|
+
if (type) {
|
|
132
|
+
const rows = await this.sql`
|
|
133
|
+
SELECT COUNT(*)::int AS count FROM "_strav_devtools_entries"
|
|
134
|
+
WHERE "type" = ${type}
|
|
135
|
+
`
|
|
136
|
+
return (rows[0] as Record<string, unknown>).count as number
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const rows = await this.sql`
|
|
140
|
+
SELECT COUNT(*)::int AS count FROM "_strav_devtools_entries"
|
|
141
|
+
`
|
|
142
|
+
return (rows[0] as Record<string, unknown>).count as number
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function hydrateEntry(row: Record<string, unknown>): EntryRecord {
|
|
147
|
+
return {
|
|
148
|
+
id: Number(row.id),
|
|
149
|
+
uuid: row.uuid as string,
|
|
150
|
+
batchId: row.batch_id as string,
|
|
151
|
+
type: row.type as EntryType,
|
|
152
|
+
familyHash: (row.family_hash as string) ?? null,
|
|
153
|
+
content: (typeof row.content === 'string' ? JSON.parse(row.content) : row.content) as Record<
|
|
154
|
+
string,
|
|
155
|
+
unknown
|
|
156
|
+
>,
|
|
157
|
+
tags: (row.tags ?? []) as string[],
|
|
158
|
+
createdAt: row.created_at as Date,
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Types of entries that collectors produce. */
|
|
2
|
+
export type EntryType =
|
|
3
|
+
| 'request'
|
|
4
|
+
| 'query'
|
|
5
|
+
| 'exception'
|
|
6
|
+
| 'log'
|
|
7
|
+
| 'job'
|
|
8
|
+
| 'cache'
|
|
9
|
+
| 'mail'
|
|
10
|
+
| 'event'
|
|
11
|
+
| 'schedule'
|
|
12
|
+
|
|
13
|
+
/** A single recorded devtools entry, ready for storage. */
|
|
14
|
+
export interface DevtoolsEntry {
|
|
15
|
+
uuid: string
|
|
16
|
+
batchId: string
|
|
17
|
+
type: EntryType
|
|
18
|
+
familyHash: string | null
|
|
19
|
+
content: Record<string, unknown>
|
|
20
|
+
tags: string[]
|
|
21
|
+
createdAt: Date
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A row from the _strav_devtools_entries table. */
|
|
25
|
+
export interface EntryRecord {
|
|
26
|
+
id: number
|
|
27
|
+
uuid: string
|
|
28
|
+
batchId: string
|
|
29
|
+
type: EntryType
|
|
30
|
+
familyHash: string | null
|
|
31
|
+
content: Record<string, unknown>
|
|
32
|
+
tags: string[]
|
|
33
|
+
createdAt: Date
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Aggregate function names stored in the aggregates table. */
|
|
37
|
+
export type AggregateFunction = 'count' | 'min' | 'max' | 'sum' | 'avg'
|
|
38
|
+
|
|
39
|
+
/** A row from the _strav_devtools_aggregates table. */
|
|
40
|
+
export interface AggregateRecord {
|
|
41
|
+
id: number
|
|
42
|
+
bucket: number
|
|
43
|
+
period: number
|
|
44
|
+
type: string
|
|
45
|
+
key: string
|
|
46
|
+
aggregate: AggregateFunction
|
|
47
|
+
value: number
|
|
48
|
+
count: number | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Configuration shape for the devtools package. */
|
|
52
|
+
export interface DevtoolsConfig {
|
|
53
|
+
enabled: boolean
|
|
54
|
+
storage: {
|
|
55
|
+
pruneAfter: number
|
|
56
|
+
}
|
|
57
|
+
collectors: {
|
|
58
|
+
request: { enabled: boolean; sizeLimit: number }
|
|
59
|
+
query: { enabled: boolean; slow: number }
|
|
60
|
+
exception: { enabled: boolean }
|
|
61
|
+
log: { enabled: boolean; level: string }
|
|
62
|
+
job: { enabled: boolean }
|
|
63
|
+
}
|
|
64
|
+
recorders: {
|
|
65
|
+
slowRequests: { enabled: boolean; threshold: number; sampleRate: number }
|
|
66
|
+
slowQueries: { enabled: boolean; threshold: number; sampleRate: number }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Collector configuration passed to each collector. */
|
|
71
|
+
export interface CollectorOptions {
|
|
72
|
+
enabled: boolean
|
|
73
|
+
[key: string]: unknown
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Recorder configuration passed to each recorder. */
|
|
77
|
+
export interface RecorderOptions {
|
|
78
|
+
enabled: boolean
|
|
79
|
+
threshold?: number
|
|
80
|
+
sampleRate?: number
|
|
81
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { env } from '@stravigor/core/helpers'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
/** Enable or disable devtools entirely. */
|
|
5
|
+
enabled: env('DEVTOOLS_ENABLED', 'true').bool(),
|
|
6
|
+
|
|
7
|
+
storage: {
|
|
8
|
+
/** Automatically prune entries older than this many hours. */
|
|
9
|
+
pruneAfter: 24,
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
collectors: {
|
|
13
|
+
request: { enabled: true, sizeLimit: 64 },
|
|
14
|
+
query: { enabled: true, slow: 100 },
|
|
15
|
+
exception: { enabled: true },
|
|
16
|
+
log: { enabled: true, level: 'debug' },
|
|
17
|
+
job: { enabled: true },
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
recorders: {
|
|
21
|
+
slowRequests: { enabled: true, threshold: 1000, sampleRate: 1.0 },
|
|
22
|
+
slowQueries: { enabled: true, threshold: 1000, sampleRate: 1.0 },
|
|
23
|
+
},
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineSchema, t, Archetype } from '@stravigor/core/schema'
|
|
2
|
+
|
|
3
|
+
export default defineSchema('_strav_devtools_aggregates', {
|
|
4
|
+
archetype: Archetype.Event,
|
|
5
|
+
fields: {
|
|
6
|
+
bucket: t.integer().required(),
|
|
7
|
+
period: t.integer().required(),
|
|
8
|
+
type: t.varchar(30).required(),
|
|
9
|
+
key: t.text().required(),
|
|
10
|
+
aggregate: t.varchar(10).required(),
|
|
11
|
+
value: t.numeric(20, 2).required(),
|
|
12
|
+
count: t.integer().nullable(),
|
|
13
|
+
},
|
|
14
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineSchema, t, Archetype } from '@stravigor/core/schema'
|
|
2
|
+
|
|
3
|
+
export default defineSchema('_strav_devtools_entries', {
|
|
4
|
+
archetype: Archetype.Event,
|
|
5
|
+
fields: {
|
|
6
|
+
uuid: t.uuid().required(),
|
|
7
|
+
batchId: t.uuid().required().index(),
|
|
8
|
+
type: t.varchar(30).required().index(),
|
|
9
|
+
familyHash: t.varchar(64).nullable().index(),
|
|
10
|
+
content: t.jsonb().required(),
|
|
11
|
+
tags: t.text().array().required(),
|
|
12
|
+
},
|
|
13
|
+
})
|
package/tsconfig.json
ADDED