@littlebearapps/platform-admin-sdk 1.2.0 → 1.4.1
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/README.md +140 -45
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +4 -1
- package/package.json +13 -3
- package/templates/full/workers/lib/pattern-discovery/index.ts +13 -0
- package/templates/full/workers/lib/pattern-discovery/storage.ts +1 -0
- package/templates/shared/docs/kv-key-patterns.md +101 -0
- package/templates/shared/migrations/002_usage_warehouse.sql +1 -0
- package/templates/shared/migrations/seed.sql.hbs +2 -2
- package/templates/shared/scripts/sync-config.ts +59 -5
- package/templates/shared/workers/lib/platform-settings.ts +16 -1
- package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +11 -5
- package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +23 -8
- package/templates/shared/workers/lib/usage/scheduled/rollups.ts +8 -5
- package/templates/standard/workers/error-collector.ts +188 -365
- package/templates/standard/workers/lib/sentinel/gap-detection.ts +48 -8
- package/templates/standard/workers/platform-sentinel.ts +4 -4
|
@@ -428,9 +428,18 @@ curl -X POST ${PLATFORM_USAGE_URL}/usage/gaps/backfill -H 'Content-Type: applica
|
|
|
428
428
|
// =============================================================================
|
|
429
429
|
|
|
430
430
|
/**
|
|
431
|
-
*
|
|
431
|
+
* Default threshold for per-project coverage alerts (percentage).
|
|
432
|
+
* Can be overridden at runtime via PlatformSettings.gapCoverageThresholdPct.
|
|
432
433
|
*/
|
|
433
|
-
const
|
|
434
|
+
const DEFAULT_PROJECT_COVERAGE_THRESHOLD = 90;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* KV cache key and TTL for gap detection results.
|
|
438
|
+
* Gap detection doesn't need 15-minute freshness — 1-hour cache prevents
|
|
439
|
+
* 96 expensive D1 scans/day from becoming 24.
|
|
440
|
+
*/
|
|
441
|
+
const PROJECT_GAP_CACHE_KEY = 'sentinel:project-gaps:result';
|
|
442
|
+
const PROJECT_GAP_CACHE_TTL = 3600;
|
|
434
443
|
|
|
435
444
|
/**
|
|
436
445
|
* On-demand resources excluded from coverage "expected" denominator.
|
|
@@ -440,6 +449,7 @@ const PROJECT_COVERAGE_THRESHOLD = 90;
|
|
|
440
449
|
* TODO: Customise this list for your projects' on-demand resources
|
|
441
450
|
*/
|
|
442
451
|
const ON_DEMAND_RESOURCE_EXCLUSIONS = new Set([
|
|
452
|
+
'do:platform-notifications', // Durable Object, on-demand only
|
|
443
453
|
'worker:platform-settings', // Admin-only, on-demand API
|
|
444
454
|
'worker:platform-search', // Admin-only, on-demand API
|
|
445
455
|
'worker:platform-alert-router', // Only invoked by Gatus/GitHub webhooks
|
|
@@ -474,14 +484,30 @@ export interface ProjectGap {
|
|
|
474
484
|
/**
|
|
475
485
|
* Detect gaps in per-project data coverage.
|
|
476
486
|
* Queries resource_usage_snapshots table for projects with less than
|
|
477
|
-
*
|
|
487
|
+
* the configured coverage threshold in the last 24 hours.
|
|
488
|
+
*
|
|
489
|
+
* Results are cached in KV for 1 hour to prevent excessive D1 reads.
|
|
478
490
|
*
|
|
491
|
+
* @param coverageThreshold - Coverage percentage threshold (default from PlatformSettings)
|
|
479
492
|
* @returns Array of projects with low coverage, including their repo mapping
|
|
480
493
|
*/
|
|
481
494
|
export async function detectProjectGaps(
|
|
482
495
|
env: GapDetectionEnv,
|
|
483
|
-
log: Logger
|
|
496
|
+
log: Logger,
|
|
497
|
+
coverageThreshold: number = DEFAULT_PROJECT_COVERAGE_THRESHOLD
|
|
484
498
|
): Promise<ProjectGap[]> {
|
|
499
|
+
// Check KV cache first — gap detection doesn't need 15-minute freshness
|
|
500
|
+
try {
|
|
501
|
+
const cached = await env.PLATFORM_CACHE.get(PROJECT_GAP_CACHE_KEY);
|
|
502
|
+
if (cached) {
|
|
503
|
+
const parsed = JSON.parse(cached) as ProjectGap[];
|
|
504
|
+
log.debug('Using cached project gap results', { count: parsed.length });
|
|
505
|
+
return parsed;
|
|
506
|
+
}
|
|
507
|
+
} catch {
|
|
508
|
+
// Cache miss or parse error — proceed with fresh query
|
|
509
|
+
}
|
|
510
|
+
|
|
485
511
|
const gaps: ProjectGap[] = [];
|
|
486
512
|
|
|
487
513
|
try {
|
|
@@ -500,9 +526,10 @@ export async function detectProjectGaps(
|
|
|
500
526
|
AND project NOT IN ('unknown', 'all')
|
|
501
527
|
),
|
|
502
528
|
known AS (
|
|
503
|
-
SELECT project, resource_type, resource_id
|
|
529
|
+
SELECT DISTINCT project, resource_type, resource_id
|
|
504
530
|
FROM resource_usage_snapshots
|
|
505
|
-
WHERE
|
|
531
|
+
WHERE snapshot_hour >= datetime('now', '-7 days')
|
|
532
|
+
AND project IS NOT NULL
|
|
506
533
|
AND project NOT IN ('unknown', 'all')
|
|
507
534
|
AND (resource_type || ':' || resource_id) NOT IN (${exclusionPlaceholders})
|
|
508
535
|
)
|
|
@@ -525,7 +552,7 @@ export async function detectProjectGaps(
|
|
|
525
552
|
HAVING coverage_pct < ?
|
|
526
553
|
`
|
|
527
554
|
)
|
|
528
|
-
.bind(...exclusionKeys,
|
|
555
|
+
.bind(...exclusionKeys, coverageThreshold)
|
|
529
556
|
.all<{
|
|
530
557
|
project: string;
|
|
531
558
|
expected_resources: number;
|
|
@@ -547,6 +574,7 @@ export async function detectProjectGaps(
|
|
|
547
574
|
SELECT DISTINCT resource_type || ':' || resource_id as resource_key
|
|
548
575
|
FROM resource_usage_snapshots
|
|
549
576
|
WHERE project = ?
|
|
577
|
+
AND snapshot_hour >= datetime('now', '-7 days')
|
|
550
578
|
AND resource_type || ':' || resource_id NOT IN (
|
|
551
579
|
SELECT DISTINCT resource_type || ':' || resource_id
|
|
552
580
|
FROM resource_usage_snapshots
|
|
@@ -585,9 +613,10 @@ export async function detectProjectGaps(
|
|
|
585
613
|
AND project = ?
|
|
586
614
|
),
|
|
587
615
|
known AS (
|
|
588
|
-
SELECT resource_type, resource_id
|
|
616
|
+
SELECT DISTINCT resource_type, resource_id
|
|
589
617
|
FROM resource_usage_snapshots
|
|
590
618
|
WHERE project = ?
|
|
619
|
+
AND snapshot_hour >= datetime('now', '-7 days')
|
|
591
620
|
)
|
|
592
621
|
SELECT
|
|
593
622
|
k.resource_type,
|
|
@@ -636,6 +665,17 @@ export async function detectProjectGaps(
|
|
|
636
665
|
projectCount: gaps.length,
|
|
637
666
|
projects: gaps.map((g) => `${g.project}:${g.coveragePct}%`),
|
|
638
667
|
});
|
|
668
|
+
|
|
669
|
+
// Cache results in KV to avoid repeated expensive D1 queries
|
|
670
|
+
try {
|
|
671
|
+
await env.PLATFORM_CACHE.put(
|
|
672
|
+
PROJECT_GAP_CACHE_KEY,
|
|
673
|
+
JSON.stringify(gaps),
|
|
674
|
+
{ expirationTtl: PROJECT_GAP_CACHE_TTL }
|
|
675
|
+
);
|
|
676
|
+
} catch {
|
|
677
|
+
// Cache write failure is non-fatal
|
|
678
|
+
}
|
|
639
679
|
} catch (error) {
|
|
640
680
|
log.error('Failed to detect project gaps', error);
|
|
641
681
|
}
|
|
@@ -525,8 +525,8 @@ async function fetchCostsFromD1(env: Env, log: Logger): Promise<CostBreakdown |
|
|
|
525
525
|
SUM(COALESCE(workflows_cost_usd, 0)) as workflows,
|
|
526
526
|
SUM(COALESCE(total_cost_usd, 0)) as total
|
|
527
527
|
FROM hourly_usage_snapshots
|
|
528
|
-
WHERE project = 'all' AND
|
|
529
|
-
`).bind(billing.start).first<CostBreakdown>();
|
|
528
|
+
WHERE project = 'all' AND snapshot_hour >= ?
|
|
529
|
+
`).bind(billing.start + 'T00:00:00Z').first<CostBreakdown>();
|
|
530
530
|
|
|
531
531
|
if (!result) {
|
|
532
532
|
log.warn('D1 fallback returned no data');
|
|
@@ -805,10 +805,10 @@ async function queryAllowanceStatus(
|
|
|
805
805
|
try {
|
|
806
806
|
// Build a single query for all metrics of this service
|
|
807
807
|
const selectExprs = metricDefs.map((m, i) => `${m.sqlExpr} as metric_${i}`).join(', ');
|
|
808
|
-
const sql = `SELECT ${selectExprs} FROM hourly_usage_snapshots WHERE project = 'all' AND
|
|
808
|
+
const sql = `SELECT ${selectExprs} FROM hourly_usage_snapshots WHERE project = 'all' AND snapshot_hour >= ?`;
|
|
809
809
|
|
|
810
810
|
const result = await env.PLATFORM_DB.prepare(sql)
|
|
811
|
-
.bind(billingStart)
|
|
811
|
+
.bind(billingStart + 'T00:00:00Z')
|
|
812
812
|
.first<Record<string, number>>();
|
|
813
813
|
|
|
814
814
|
if (!result) {
|