tangled
alpha
login
or
join now
xan.lol
/
wisp.place-monorepo
forked from
nekomimi.pet/wisp.place-monorepo
0
fork
atom
Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
0
fork
atom
overview
issues
pulls
pipelines
modal work
nekomimi.pet
2 months ago
ce2baebb
6cb1e859
+260
-109
6 changed files
expand all
collapse all
unified
split
apps
hosting-service
src
lib
file-serving.ts
main-app
public
editor
tabs
DomainsTab.tsx
bun.lock
packages
@wisp
observability
package.json
src
exporters.ts
middleware
elysia.ts
+5
-2
apps/hosting-service/src/lib/file-serving.ts
···
25
25
const result = await storage.getWithMetadata(key);
26
26
27
27
if (result) {
28
28
-
const tier = result.metadata?.tier || 'unknown';
28
28
+
const metadata = result.metadata;
29
29
+
const tier =
30
30
+
metadata && typeof metadata === 'object' && 'tier' in metadata
31
31
+
? String((metadata as Record<string, unknown>).tier)
32
32
+
: 'unknown';
29
33
const size = result.data ? (result.data as Uint8Array).length : 0;
30
34
console.log(`[Storage] Served ${filePath} from ${tier} tier (${size} bytes) - ${did}:${rkey}`);
31
35
}
···
833
837
},
834
838
});
835
839
}
836
836
-
+213
-99
apps/main-app/public/editor/tabs/DomainsTab.tsx
···
76
76
const [customDomain, setCustomDomain] = useState('')
77
77
const [isAddingDomain, setIsAddingDomain] = useState(false)
78
78
const [viewDomainDNS, setViewDomainDNS] = useState<string | null>(null)
79
79
+
const [copiedField, setCopiedField] = useState<string | null>(null)
80
80
+
81
81
+
const copyToClipboard = async (value: string, label: string) => {
82
82
+
try {
83
83
+
await navigator.clipboard.writeText(value)
84
84
+
setCopiedField(label)
85
85
+
window.setTimeout(() => {
86
86
+
setCopiedField((current) => (current === label ? null : current))
87
87
+
}, 1400)
88
88
+
} catch {
89
89
+
setCopiedField(null)
90
90
+
}
91
91
+
}
79
92
80
93
const checkWispAvailability = async (handle: string) => {
81
94
const trimmedHandle = handle.trim().toLowerCase()
···
463
476
open={viewDomainDNS !== null}
464
477
onOpenChange={(open) => !open && setViewDomainDNS(null)}
465
478
>
466
466
-
<DialogContent className="sm:max-w-lg">
479
479
+
<DialogContent className="sm:max-w-lg max-h-[80vh] overflow-hidden">
467
480
<DialogHeader>
468
481
<DialogTitle>DNS Configuration</DialogTitle>
469
482
<DialogDescription>
···
471
484
</DialogDescription>
472
485
</DialogHeader>
473
486
{viewDomainDNS && userInfo && (
474
474
-
<>
487
487
+
<div className="relative max-h-[62vh] overflow-y-auto pr-2">
488
488
+
<div className="pointer-events-none sticky top-0 z-10 h-3 bg-gradient-to-b from-background to-transparent" />
475
489
{(() => {
476
490
const domain = customDomains.find(
477
491
(d) => d.id === viewDomainDNS
···
481
495
return (
482
496
<div className="space-y-4 py-4">
483
497
<div className="p-3 bg-muted/30 rounded-lg">
484
484
-
<p className="text-sm font-medium mb-1">
485
485
-
Domain:
498
498
+
<p className="text-xs uppercase tracking-wide text-muted-foreground">
499
499
+
Domain
486
500
</p>
487
487
-
<p className="font-mono text-sm">
501
501
+
<p className="font-mono text-sm mt-1">
488
502
{domain.domain}
489
503
</p>
490
504
</div>
491
505
492
506
<div className="space-y-3">
493
493
-
<div className="p-3 bg-background rounded border border-border">
494
494
-
<div className="flex justify-between items-start mb-2">
495
495
-
<span className="text-xs font-semibold text-muted-foreground">
496
496
-
TXT Record (Verification)
497
497
-
</span>
498
498
-
</div>
499
499
-
<div className="font-mono text-xs space-y-2">
507
507
+
<div className="p-4 bg-background rounded border border-border">
508
508
+
<div className="flex items-center justify-between gap-3">
500
509
<div>
501
501
-
<span className="text-muted-foreground">
502
502
-
Name:
503
503
-
</span>{' '}
504
504
-
<span className="select-all">
505
505
-
_wisp.{domain.domain}
506
506
-
</span>
510
510
+
<p className="text-xs uppercase tracking-wide text-muted-foreground">
511
511
+
Step 1
512
512
+
</p>
513
513
+
<p className="text-sm font-semibold">
514
514
+
Verify ownership (TXT)
515
515
+
</p>
507
516
</div>
508
508
-
<div>
509
509
-
<span className="text-muted-foreground">
510
510
-
Value:
511
511
-
</span>{' '}
512
512
-
<span className="select-all break-all">
513
513
-
{userInfo.did}
514
514
-
</span>
517
517
+
<Badge variant="secondary" className="text-xs">
518
518
+
Required
519
519
+
</Badge>
520
520
+
</div>
521
521
+
<div className="mt-3 space-y-2">
522
522
+
<div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/30 px-3 py-2">
523
523
+
<div className="min-w-0">
524
524
+
<p className="text-xs text-muted-foreground">
525
525
+
Name
526
526
+
</p>
527
527
+
<p className="font-mono text-sm select-all">
528
528
+
_wisp.{domain.domain}
529
529
+
</p>
530
530
+
</div>
531
531
+
<Button
532
532
+
variant="outline"
533
533
+
size="sm"
534
534
+
onClick={() =>
535
535
+
copyToClipboard(
536
536
+
`_wisp.${domain.domain}`,
537
537
+
'txt-name'
538
538
+
)
539
539
+
}
540
540
+
>
541
541
+
{copiedField === 'txt-name'
542
542
+
? 'Copied'
543
543
+
: 'Copy'}
544
544
+
</Button>
545
545
+
</div>
546
546
+
<div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/30 px-3 py-2">
547
547
+
<div className="min-w-0">
548
548
+
<p className="text-xs text-muted-foreground">
549
549
+
Value
550
550
+
</p>
551
551
+
<p className="font-mono text-sm break-all select-all">
552
552
+
{userInfo.did}
553
553
+
</p>
554
554
+
</div>
555
555
+
<Button
556
556
+
variant="outline"
557
557
+
size="sm"
558
558
+
onClick={() =>
559
559
+
copyToClipboard(userInfo.did, 'txt-value')
560
560
+
}
561
561
+
>
562
562
+
{copiedField === 'txt-value'
563
563
+
? 'Copied'
564
564
+
: 'Copy'}
565
565
+
</Button>
515
566
</div>
516
567
</div>
517
568
</div>
518
569
519
519
-
<div className="p-3 bg-background rounded border border-border">
520
520
-
<div className="flex justify-between items-start mb-2">
521
521
-
<span className="text-xs font-semibold text-muted-foreground">
522
522
-
CNAME Record (Pointing) — Recommended
523
523
-
</span>
570
570
+
<div className="p-4 bg-background rounded border border-border">
571
571
+
<div className="flex items-center justify-between gap-3">
572
572
+
<div>
573
573
+
<p className="text-xs uppercase tracking-wide text-muted-foreground">
574
574
+
Step 2
575
575
+
</p>
576
576
+
<p className="text-sm font-semibold">
577
577
+
Point your domain (CNAME)
578
578
+
</p>
579
579
+
</div>
580
580
+
<Badge variant="secondary" className="text-xs">
581
581
+
Recommended
582
582
+
</Badge>
524
583
</div>
525
525
-
<div className="font-mono text-xs space-y-2">
526
526
-
<div>
527
527
-
<span className="text-muted-foreground">
528
528
-
Name:
529
529
-
</span>{' '}
530
530
-
<span className="select-all">
531
531
-
{domain.domain}
532
532
-
</span>
584
584
+
<div className="mt-3 space-y-2">
585
585
+
<div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/30 px-3 py-2">
586
586
+
<div className="min-w-0">
587
587
+
<p className="text-xs text-muted-foreground">
588
588
+
Name
589
589
+
</p>
590
590
+
<p className="font-mono text-sm select-all">
591
591
+
{domain.domain}
592
592
+
</p>
593
593
+
</div>
594
594
+
<Button
595
595
+
variant="outline"
596
596
+
size="sm"
597
597
+
onClick={() =>
598
598
+
copyToClipboard(domain.domain, 'cname-name')
599
599
+
}
600
600
+
>
601
601
+
{copiedField === 'cname-name'
602
602
+
? 'Copied'
603
603
+
: 'Copy'}
604
604
+
</Button>
533
605
</div>
534
534
-
<div>
535
535
-
<span className="text-muted-foreground">
536
536
-
Value:
537
537
-
</span>{' '}
538
538
-
<span className="select-all">
539
539
-
{domain.id}.dns.wisp.place
540
540
-
</span>
606
606
+
<div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/30 px-3 py-2">
607
607
+
<div className="min-w-0">
608
608
+
<p className="text-xs text-muted-foreground">
609
609
+
Value
610
610
+
</p>
611
611
+
<p className="font-mono text-sm select-all">
612
612
+
{domain.id}.dns.wisp.place
613
613
+
</p>
614
614
+
</div>
615
615
+
<Button
616
616
+
variant="outline"
617
617
+
size="sm"
618
618
+
onClick={() =>
619
619
+
copyToClipboard(
620
620
+
`${domain.id}.dns.wisp.place`,
621
621
+
'cname-value'
622
622
+
)
623
623
+
}
624
624
+
>
625
625
+
{copiedField === 'cname-value'
626
626
+
? 'Copied'
627
627
+
: 'Copy'}
628
628
+
</Button>
541
629
</div>
542
630
</div>
543
543
-
<p className="text-xs text-muted-foreground mt-2">
544
544
-
Note: Some DNS providers (like Cloudflare) flatten CNAMEs to A records - this is fine and won't affect verification.
545
545
-
</p>
631
631
+
<div className="mt-3 flex gap-2 rounded-md border border-blue-500/20 bg-blue-500/10 p-2 text-xs text-blue-200">
632
632
+
<AlertCircle className="w-4 h-4 shrink-0 mt-0.5 text-blue-300" />
633
633
+
<p>
634
634
+
Some DNS providers (like Cloudflare) flatten CNAMEs to A records.
635
635
+
That's okay and won't affect verification.
636
636
+
</p>
637
637
+
</div>
546
638
</div>
547
639
548
548
-
<div className="p-3 bg-background rounded border border-border">
549
549
-
<div className="flex items-start gap-2 mb-2">
550
550
-
<span className="text-xs font-semibold text-muted-foreground">
551
551
-
A Records (Fallback Option)
552
552
-
</span>
553
553
-
</div>
554
554
-
<div className="p-2 bg-yellow-500/10 border border-yellow-500/20 rounded mb-3 flex gap-2">
555
555
-
<AlertCircle className="w-4 h-4 text-yellow-600 shrink-0 mt-0.5" />
556
556
-
<p className="text-xs text-yellow-700 dark:text-yellow-500">
557
557
-
<strong>Warning:</strong> Using A records instead of CNAME means you lose GeoDNS capabilities.
558
558
-
Your site will always be served from the specific node you choose below, regardless of visitor location.
559
559
-
</p>
560
560
-
</div>
561
561
-
<div className="space-y-3">
562
562
-
{HOSTING_NODES.map((node) => (
563
563
-
<div key={node.ip} className="font-mono text-xs space-y-1 pl-3 border-l-2 border-muted">
564
564
-
<div className="font-semibold text-muted-foreground mb-1">
565
565
-
{node.region}
566
566
-
</div>
567
567
-
<div>
568
568
-
<span className="text-muted-foreground">
569
569
-
Name:
570
570
-
</span>{' '}
571
571
-
<span className="select-all">
572
572
-
{domain.domain}
573
573
-
</span>
574
574
-
</div>
575
575
-
<div>
576
576
-
<span className="text-muted-foreground">
577
577
-
Type:
578
578
-
</span>{' '}
579
579
-
<span>A</span>
580
580
-
</div>
581
581
-
<div>
582
582
-
<span className="text-muted-foreground">
583
583
-
Value:
584
584
-
</span>{' '}
585
585
-
<span className="select-all">
586
586
-
{node.ip}
587
587
-
</span>
640
640
+
<details className="p-4 bg-background rounded border border-border">
641
641
+
<summary className="text-sm font-semibold cursor-pointer select-none">
642
642
+
Use A Records Instead (Fallback)
643
643
+
</summary>
644
644
+
<div className="mt-3">
645
645
+
<div className="p-2 bg-yellow-500/10 border border-yellow-500/20 rounded mb-3 flex gap-2">
646
646
+
<AlertCircle className="w-4 h-4 text-yellow-600 shrink-0 mt-0.5" />
647
647
+
<p className="text-sm text-yellow-700 dark:text-yellow-500">
648
648
+
<strong>Warning:</strong> A records disable GeoDNS. Your site
649
649
+
will always be served from the single region you choose.
650
650
+
</p>
651
651
+
</div>
652
652
+
<div className="space-y-3">
653
653
+
{HOSTING_NODES.map((node) => (
654
654
+
<div key={node.ip} className="space-y-2 pl-3 border-l-2 border-muted">
655
655
+
<div className="font-semibold text-muted-foreground mb-1">
656
656
+
{node.region}
657
657
+
</div>
658
658
+
<div className="font-mono text-xs space-y-1">
659
659
+
<div>
660
660
+
<span className="text-muted-foreground">
661
661
+
Name:
662
662
+
</span>{' '}
663
663
+
<span className="select-all">
664
664
+
{domain.domain}
665
665
+
</span>
666
666
+
</div>
667
667
+
<div>
668
668
+
<span className="text-muted-foreground">
669
669
+
Type:
670
670
+
</span>{' '}
671
671
+
<span>A</span>
672
672
+
</div>
673
673
+
</div>
674
674
+
<div className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/30 px-3 py-2 font-mono text-xs">
675
675
+
<div className="min-w-0">
676
676
+
<p className="text-xs text-muted-foreground">
677
677
+
Value
678
678
+
</p>
679
679
+
<p className="select-all">
680
680
+
{node.ip}
681
681
+
</p>
682
682
+
</div>
683
683
+
<Button
684
684
+
variant="outline"
685
685
+
size="sm"
686
686
+
onClick={() =>
687
687
+
copyToClipboard(
688
688
+
node.ip,
689
689
+
`a-value-${node.ip}`
690
690
+
)
691
691
+
}
692
692
+
>
693
693
+
{copiedField === `a-value-${node.ip}`
694
694
+
? 'Copied'
695
695
+
: 'Copy'}
696
696
+
</Button>
697
697
+
</div>
588
698
</div>
589
589
-
</div>
590
590
-
))}
699
699
+
))}
700
700
+
</div>
701
701
+
<p className="text-sm text-muted-foreground mt-3">
702
702
+
Choose the region closest to your primary audience.
703
703
+
</p>
591
704
</div>
592
592
-
<p className="text-xs text-muted-foreground mt-3">
593
593
-
Choose one region that best matches your primary audience location.
594
594
-
</p>
595
595
-
</div>
705
705
+
</details>
596
706
</div>
597
707
598
708
<div className="p-3 bg-muted/30 rounded-lg">
599
599
-
<p className="text-xs text-muted-foreground">
600
600
-
💡 After configuring DNS, click "Verify DNS"
601
601
-
to check if everything is set up correctly.
602
602
-
DNS changes can take a few minutes to
709
709
+
<p className="text-sm text-muted-foreground">
710
710
+
After configuring DNS, click "Verify DNS" to check
711
711
+
everything. DNS changes can take a few minutes to
603
712
propagate.
604
713
</p>
605
714
</div>
606
715
</div>
607
716
)
608
717
})()}
609
609
-
</>
718
718
+
<div className="pointer-events-none sticky bottom-0 z-10 flex h-8 items-end justify-center bg-gradient-to-t from-background to-transparent">
719
719
+
<span className="text-[10px] text-muted-foreground">
720
720
+
Scroll for more
721
721
+
</span>
722
722
+
</div>
723
723
+
</div>
610
724
)}
611
725
<DialogFooter>
612
726
<Button
+22
-1
bun.lock
···
153
153
"dependencies": {
154
154
"@opentelemetry/api": "^1.9.0",
155
155
"@opentelemetry/exporter-metrics-otlp-http": "^0.56.0",
156
156
+
"@opentelemetry/otlp-exporter-base": "^0.211.0",
156
157
"@opentelemetry/resources": "^1.29.0",
157
158
"@opentelemetry/sdk-metrics": "^1.29.0",
158
159
"@opentelemetry/semantic-conventions": "^1.29.0",
···
163
164
"typescript": "^5.9.3",
164
165
},
165
166
"peerDependencies": {
167
167
+
"elysia": "^1.0.0",
166
168
"hono": "^4.10.7",
167
169
},
168
170
"optionalPeers": [
171
171
+
"elysia",
169
172
"hono",
170
173
],
171
174
},
···
481
484
482
485
"@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.200.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.200.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg=="],
483
486
484
484
-
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.56.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/otlp-transformer": "0.56.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw=="],
487
487
+
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.211.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/otlp-transformer": "0.211.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bp1+63V8WPV+bRI9EQG6E9YID1LIHYSZVbp7f+44g9tRzCq+rtw/o4fpL5PC31adcUsFiz/oN0MdLISSrZDdrg=="],
485
488
486
489
"@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.200.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.0.0", "@opentelemetry/otlp-exporter-base": "0.200.0", "@opentelemetry/otlp-transformer": "0.200.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw=="],
487
490
···
1361
1364
1362
1365
"@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1363
1366
1367
1367
+
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.56.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/otlp-transformer": "0.56.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw=="],
1368
1368
+
1364
1369
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ=="],
1365
1370
1366
1371
"@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.29.0", "", { "dependencies": { "@opentelemetry/core": "1.29.0", "@opentelemetry/resources": "1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw=="],
···
1410
1415
"@opentelemetry/exporter-zipkin/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1411
1416
1412
1417
"@opentelemetry/exporter-zipkin/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1418
1418
+
1419
1419
+
"@opentelemetry/otlp-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="],
1420
1420
+
1421
1421
+
"@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/sdk-logs": "0.211.0", "@opentelemetry/sdk-metrics": "2.5.0", "@opentelemetry/sdk-trace-base": "2.5.0", "protobufjs": "8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-julhCJ9dXwkOg9svuuYqqjXLhVaUgyUvO2hWbTxwjvLXX2rG3VtAaB0SzxMnGTuoCZizBT7Xqqm2V7+ggrfCXA=="],
1413
1422
1414
1423
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="],
1415
1424
···
1540
1549
"@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1541
1550
1542
1551
"@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="],
1552
1552
+
1553
1553
+
"@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg=="],
1554
1554
+
1555
1555
+
"@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g=="],
1556
1556
+
1557
1557
+
"@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA=="],
1558
1558
+
1559
1559
+
"@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA=="],
1560
1560
+
1561
1561
+
"@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.5.0", "", { "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ=="],
1562
1562
+
1563
1563
+
"@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@8.0.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw=="],
1543
1564
1544
1565
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="],
1545
1566
+2
-1
packages/@wisp/observability/package.json
···
37
37
},
38
38
"dependencies": {
39
39
"@opentelemetry/api": "^1.9.0",
40
40
-
"@opentelemetry/sdk-metrics": "^1.29.0",
41
40
"@opentelemetry/exporter-metrics-otlp-http": "^0.56.0",
41
41
+
"@opentelemetry/otlp-exporter-base": "^0.211.0",
42
42
"@opentelemetry/resources": "^1.29.0",
43
43
+
"@opentelemetry/sdk-metrics": "^1.29.0",
43
44
"@opentelemetry/semantic-conventions": "^1.29.0"
44
45
},
45
46
"devDependencies": {
+3
-2
packages/@wisp/observability/src/exporters.ts
···
7
7
import { metrics, type MeterProvider, type Counter, type Histogram } from '@opentelemetry/api'
8
8
import { MeterProvider as SdkMeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
9
9
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
10
10
+
import type { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base'
10
11
import { Resource } from '@opentelemetry/resources'
11
12
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'
12
13
import os from 'node:os'
···
326
327
url: `${this.config.prometheusUrl}${prometheusPath}`,
327
328
headers: this.getAuthHeaders(),
328
329
timeoutMillis: 10000,
329
329
-
compression: 'gzip'
330
330
+
compression: 'gzip' as CompressionAlgorithm
330
331
})
331
332
332
333
// Create meter provider with periodic exporting
···
454
455
if (typeof process !== 'undefined') {
455
456
process.on('SIGTERM', shutdownGrafanaExporters)
456
457
process.on('SIGINT', shutdownGrafanaExporters)
457
457
-
}
458
458
+
}
+15
-4
packages/@wisp/observability/src/middleware/elysia.ts
···
6
6
* Tracks request metrics and logs errors
7
7
*/
8
8
export function observabilityMiddleware(service: string) {
9
9
+
const normalizeStatus = (status: unknown, fallback: number) => {
10
10
+
if (typeof status === 'number') return status
11
11
+
if (typeof status === 'string') {
12
12
+
const parsed = Number(status)
13
13
+
if (!Number.isNaN(parsed)) return parsed
14
14
+
}
15
15
+
return fallback
16
16
+
}
17
17
+
9
18
return {
10
19
beforeHandle: ({ request }: Context) => {
11
20
// Store start time on request object
···
15
24
const startTime = (request as any).__startTime || Date.now()
16
25
const duration = Date.now() - startTime
17
26
const url = new URL(request.url)
27
27
+
const statusCode = normalizeStatus(set.status, 200)
18
28
19
29
metricsCollector.recordRequest(
20
30
url.pathname,
21
31
request.method,
22
22
-
set.status || 200,
32
32
+
statusCode,
23
33
duration,
24
34
service
25
35
)
26
36
},
27
27
-
onError: ({ request, error, set }: Context & { error: Error }) => {
37
37
+
onError: (context: any) => {
38
38
+
const { request, error, set } = context as Context & { error: Error }
28
39
const startTime = (request as any).__startTime || Date.now()
29
40
const duration = Date.now() - startTime
30
41
const url = new URL(request.url)
42
42
+
const statusCode = normalizeStatus(set.status, 500)
31
43
32
44
metricsCollector.recordRequest(
33
45
url.pathname,
34
46
request.method,
35
35
-
set.status || 500,
47
47
+
statusCode,
36
48
duration,
37
49
service
38
50
)
39
51
40
52
// Don't log 404 errors
41
41
-
const statusCode = set.status || 500
42
53
if (statusCode !== 404) {
43
54
logCollector.error(
44
55
`Request failed: ${request.method} ${url.pathname}`,