@kadi.build/deploy-ability 0.0.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 +523 -0
- package/dist/constants.d.ts +82 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +82 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors/certificate-error.d.ts +95 -0
- package/dist/errors/certificate-error.d.ts.map +1 -0
- package/dist/errors/certificate-error.js +111 -0
- package/dist/errors/certificate-error.js.map +1 -0
- package/dist/errors/deployment-error.d.ts +122 -0
- package/dist/errors/deployment-error.d.ts.map +1 -0
- package/dist/errors/deployment-error.js +185 -0
- package/dist/errors/deployment-error.js.map +1 -0
- package/dist/errors/index.d.ts +13 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +18 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/profile-error.d.ts +106 -0
- package/dist/errors/profile-error.d.ts.map +1 -0
- package/dist/errors/profile-error.js +127 -0
- package/dist/errors/profile-error.js.map +1 -0
- package/dist/errors/provider-error.d.ts +104 -0
- package/dist/errors/provider-error.d.ts.map +1 -0
- package/dist/errors/provider-error.js +120 -0
- package/dist/errors/provider-error.js.map +1 -0
- package/dist/errors/wallet-error.d.ts +131 -0
- package/dist/errors/wallet-error.d.ts.map +1 -0
- package/dist/errors/wallet-error.js +154 -0
- package/dist/errors/wallet-error.js.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/targets/akash/bid-selectors.d.ts +251 -0
- package/dist/targets/akash/bid-selectors.d.ts.map +1 -0
- package/dist/targets/akash/bid-selectors.js +322 -0
- package/dist/targets/akash/bid-selectors.js.map +1 -0
- package/dist/targets/akash/bid-types.d.ts +297 -0
- package/dist/targets/akash/bid-types.d.ts.map +1 -0
- package/dist/targets/akash/bid-types.js +89 -0
- package/dist/targets/akash/bid-types.js.map +1 -0
- package/dist/targets/akash/blockchain-client.d.ts +577 -0
- package/dist/targets/akash/blockchain-client.d.ts.map +1 -0
- package/dist/targets/akash/blockchain-client.js +803 -0
- package/dist/targets/akash/blockchain-client.js.map +1 -0
- package/dist/targets/akash/certificate-manager.d.ts +228 -0
- package/dist/targets/akash/certificate-manager.d.ts.map +1 -0
- package/dist/targets/akash/certificate-manager.js +395 -0
- package/dist/targets/akash/certificate-manager.js.map +1 -0
- package/dist/targets/akash/constants.d.ts +231 -0
- package/dist/targets/akash/constants.d.ts.map +1 -0
- package/dist/targets/akash/constants.js +225 -0
- package/dist/targets/akash/constants.js.map +1 -0
- package/dist/targets/akash/deployer.d.ts +136 -0
- package/dist/targets/akash/deployer.d.ts.map +1 -0
- package/dist/targets/akash/deployer.js +599 -0
- package/dist/targets/akash/deployer.js.map +1 -0
- package/dist/targets/akash/environment.d.ts +241 -0
- package/dist/targets/akash/environment.d.ts.map +1 -0
- package/dist/targets/akash/environment.js +245 -0
- package/dist/targets/akash/environment.js.map +1 -0
- package/dist/targets/akash/index.d.ts +1113 -0
- package/dist/targets/akash/index.d.ts.map +1 -0
- package/dist/targets/akash/index.js +909 -0
- package/dist/targets/akash/index.js.map +1 -0
- package/dist/targets/akash/lease-monitor.d.ts +51 -0
- package/dist/targets/akash/lease-monitor.d.ts.map +1 -0
- package/dist/targets/akash/lease-monitor.js +110 -0
- package/dist/targets/akash/lease-monitor.js.map +1 -0
- package/dist/targets/akash/logs.d.ts +71 -0
- package/dist/targets/akash/logs.d.ts.map +1 -0
- package/dist/targets/akash/logs.js +311 -0
- package/dist/targets/akash/logs.js.map +1 -0
- package/dist/targets/akash/logs.types.d.ts +102 -0
- package/dist/targets/akash/logs.types.d.ts.map +1 -0
- package/dist/targets/akash/logs.types.js +9 -0
- package/dist/targets/akash/logs.types.js.map +1 -0
- package/dist/targets/akash/pricing.d.ts +247 -0
- package/dist/targets/akash/pricing.d.ts.map +1 -0
- package/dist/targets/akash/pricing.js +246 -0
- package/dist/targets/akash/pricing.js.map +1 -0
- package/dist/targets/akash/provider-client.d.ts +114 -0
- package/dist/targets/akash/provider-client.d.ts.map +1 -0
- package/dist/targets/akash/provider-client.js +318 -0
- package/dist/targets/akash/provider-client.js.map +1 -0
- package/dist/targets/akash/provider-metadata.d.ts +228 -0
- package/dist/targets/akash/provider-metadata.d.ts.map +1 -0
- package/dist/targets/akash/provider-metadata.js +14 -0
- package/dist/targets/akash/provider-metadata.js.map +1 -0
- package/dist/targets/akash/provider-service.d.ts +133 -0
- package/dist/targets/akash/provider-service.d.ts.map +1 -0
- package/dist/targets/akash/provider-service.js +391 -0
- package/dist/targets/akash/provider-service.js.map +1 -0
- package/dist/targets/akash/query-client.d.ts +125 -0
- package/dist/targets/akash/query-client.d.ts.map +1 -0
- package/dist/targets/akash/query-client.js +332 -0
- package/dist/targets/akash/query-client.js.map +1 -0
- package/dist/targets/akash/sdl-generator.d.ts +31 -0
- package/dist/targets/akash/sdl-generator.d.ts.map +1 -0
- package/dist/targets/akash/sdl-generator.js +279 -0
- package/dist/targets/akash/sdl-generator.js.map +1 -0
- package/dist/targets/akash/types.d.ts +285 -0
- package/dist/targets/akash/types.d.ts.map +1 -0
- package/dist/targets/akash/types.js +54 -0
- package/dist/targets/akash/types.js.map +1 -0
- package/dist/targets/akash/wallet-manager.d.ts +526 -0
- package/dist/targets/akash/wallet-manager.d.ts.map +1 -0
- package/dist/targets/akash/wallet-manager.js +953 -0
- package/dist/targets/akash/wallet-manager.js.map +1 -0
- package/dist/targets/local/compose-generator.d.ts +244 -0
- package/dist/targets/local/compose-generator.d.ts.map +1 -0
- package/dist/targets/local/compose-generator.js +324 -0
- package/dist/targets/local/compose-generator.js.map +1 -0
- package/dist/targets/local/deployer.d.ts +82 -0
- package/dist/targets/local/deployer.d.ts.map +1 -0
- package/dist/targets/local/deployer.js +367 -0
- package/dist/targets/local/deployer.js.map +1 -0
- package/dist/targets/local/engine-manager.d.ts +155 -0
- package/dist/targets/local/engine-manager.d.ts.map +1 -0
- package/dist/targets/local/engine-manager.js +250 -0
- package/dist/targets/local/engine-manager.js.map +1 -0
- package/dist/targets/local/index.d.ts +40 -0
- package/dist/targets/local/index.d.ts.map +1 -0
- package/dist/targets/local/index.js +43 -0
- package/dist/targets/local/index.js.map +1 -0
- package/dist/targets/local/network-manager.d.ts +160 -0
- package/dist/targets/local/network-manager.d.ts.map +1 -0
- package/dist/targets/local/network-manager.js +337 -0
- package/dist/targets/local/network-manager.js.map +1 -0
- package/dist/targets/local/types.d.ts +327 -0
- package/dist/targets/local/types.d.ts.map +1 -0
- package/dist/targets/local/types.js +9 -0
- package/dist/targets/local/types.js.map +1 -0
- package/dist/types/common.d.ts +585 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +13 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/options.d.ts +329 -0
- package/dist/types/options.d.ts.map +1 -0
- package/dist/types/options.js +10 -0
- package/dist/types/options.js.map +1 -0
- package/dist/types/profiles.d.ts +329 -0
- package/dist/types/profiles.d.ts.map +1 -0
- package/dist/types/profiles.js +27 -0
- package/dist/types/profiles.js.map +1 -0
- package/dist/types/results.d.ts +443 -0
- package/dist/types/results.d.ts.map +1 -0
- package/dist/types/results.js +64 -0
- package/dist/types/results.js.map +1 -0
- package/dist/types/validators.d.ts +118 -0
- package/dist/types/validators.d.ts.map +1 -0
- package/dist/types/validators.js +198 -0
- package/dist/types/validators.js.map +1 -0
- package/dist/utils/command-runner.d.ts +128 -0
- package/dist/utils/command-runner.d.ts.map +1 -0
- package/dist/utils/command-runner.js +210 -0
- package/dist/utils/command-runner.js.map +1 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +68 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +93 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/profile-loader.d.ts +76 -0
- package/dist/utils/profile-loader.d.ts.map +1 -0
- package/dist/utils/profile-loader.js +194 -0
- package/dist/utils/profile-loader.js.map +1 -0
- package/dist/utils/registry/index.d.ts +27 -0
- package/dist/utils/registry/index.d.ts.map +1 -0
- package/dist/utils/registry/index.js +29 -0
- package/dist/utils/registry/index.js.map +1 -0
- package/dist/utils/registry/manager.d.ts +319 -0
- package/dist/utils/registry/manager.d.ts.map +1 -0
- package/dist/utils/registry/manager.js +671 -0
- package/dist/utils/registry/manager.js.map +1 -0
- package/dist/utils/registry/setup.d.ts +135 -0
- package/dist/utils/registry/setup.d.ts.map +1 -0
- package/dist/utils/registry/setup.js +207 -0
- package/dist/utils/registry/setup.js.map +1 -0
- package/dist/utils/registry/transformer.d.ts +92 -0
- package/dist/utils/registry/transformer.d.ts.map +1 -0
- package/dist/utils/registry/transformer.js +131 -0
- package/dist/utils/registry/transformer.js.map +1 -0
- package/dist/utils/registry/types.d.ts +241 -0
- package/dist/utils/registry/types.d.ts.map +1 -0
- package/dist/utils/registry/types.js +10 -0
- package/dist/utils/registry/types.js.map +1 -0
- package/docs/EXAMPLES.md +293 -0
- package/docs/PLACEMENT.md +433 -0
- package/docs/STORAGE.md +318 -0
- package/docs/building-provider-reliability-tracker.md +2581 -0
- package/package.json +109 -0
|
@@ -0,0 +1,2581 @@
|
|
|
1
|
+
# Building a Provider Reliability Tracker for Akash Network
|
|
2
|
+
|
|
3
|
+
> **Purpose:** A comprehensive guide to building your own provider monitoring and reliability tracking service for the Akash Network.
|
|
4
|
+
>
|
|
5
|
+
> **Author:** KADI Infrastructure Team
|
|
6
|
+
> **Last Updated:** 2025-10-15
|
|
7
|
+
> **Difficulty:** Intermediate
|
|
8
|
+
> **Estimated Time:** 1-2 weeks for full implementation
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
1. [Introduction](#introduction)
|
|
15
|
+
2. [Understanding the Components](#understanding-the-components)
|
|
16
|
+
3. [Architecture Overview](#architecture-overview)
|
|
17
|
+
4. [Prerequisites](#prerequisites)
|
|
18
|
+
5. [Implementation Guide](#implementation-guide)
|
|
19
|
+
6. [Deployment](#deployment)
|
|
20
|
+
7. [Maintenance](#maintenance)
|
|
21
|
+
8. [Advanced Features](#advanced-features)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Introduction
|
|
26
|
+
|
|
27
|
+
### What is a Provider Reliability Tracker?
|
|
28
|
+
|
|
29
|
+
A provider reliability tracker is a service that continuously monitors Akash Network providers to:
|
|
30
|
+
- Track uptime/downtime over time (1 day, 7 days, 30 days)
|
|
31
|
+
- Monitor response times and availability
|
|
32
|
+
- Aggregate provider metadata (location, version, capabilities)
|
|
33
|
+
- Provide an API for querying provider statistics
|
|
34
|
+
|
|
35
|
+
This data helps users make informed decisions when selecting providers for their deployments.
|
|
36
|
+
|
|
37
|
+
### What is a Blockchain Indexer?
|
|
38
|
+
|
|
39
|
+
A **blockchain indexer** is a service that:
|
|
40
|
+
1. **Monitors blockchain events** - Watches for new blocks and transactions
|
|
41
|
+
2. **Extracts relevant data** - Parses transactions to find specific information
|
|
42
|
+
3. **Stores data in a queryable format** - Saves to a database for fast access
|
|
43
|
+
4. **Provides fast lookups** - Offers API endpoints to query historical data
|
|
44
|
+
|
|
45
|
+
**Why do we need it?**
|
|
46
|
+
|
|
47
|
+
The Akash blockchain stores provider information, but querying it directly is:
|
|
48
|
+
- **Slow** - Each query requires RPC calls to blockchain nodes
|
|
49
|
+
- **Limited** - You can only query current state, not historical trends
|
|
50
|
+
- **Resource-intensive** - Repeated queries burden the network
|
|
51
|
+
|
|
52
|
+
An indexer solves this by:
|
|
53
|
+
- **Pre-fetching data** - Queries blockchain once, stores results
|
|
54
|
+
- **Enabling historical queries** - Tracks changes over time
|
|
55
|
+
- **Fast API responses** - Returns data from local database (milliseconds vs seconds)
|
|
56
|
+
|
|
57
|
+
**Example:**
|
|
58
|
+
- Without indexer: "Is this provider online right now?" → Query blockchain → 2-5 seconds
|
|
59
|
+
- With indexer: "What was this provider's uptime last month?" → Query local DB → 10-50ms
|
|
60
|
+
|
|
61
|
+
### What Problem Are We Solving?
|
|
62
|
+
|
|
63
|
+
When deploying to Akash, you receive multiple provider bids. But without reliability data:
|
|
64
|
+
- ❌ You can't tell which providers are reliable
|
|
65
|
+
- ❌ You don't know historical uptime
|
|
66
|
+
- ❌ You can't identify providers that frequently go offline
|
|
67
|
+
- ❌ You're choosing blindly based only on price
|
|
68
|
+
|
|
69
|
+
With a reliability tracker:
|
|
70
|
+
- ✅ See provider uptime percentages (99.8% vs 85%)
|
|
71
|
+
- ✅ Filter out unreliable providers
|
|
72
|
+
- ✅ Make data-driven deployment decisions
|
|
73
|
+
- ✅ Track provider performance over time
|
|
74
|
+
|
|
75
|
+
### Existing Solutions
|
|
76
|
+
|
|
77
|
+
**Cloudmos (formerly Akash Console)** runs their own tracker:
|
|
78
|
+
- API: `https://api.cloudmos.io/v1/providers`
|
|
79
|
+
- Indexes 100+ providers
|
|
80
|
+
- Tracks uptime, location, versions
|
|
81
|
+
- Free to use BUT:
|
|
82
|
+
- Closed source
|
|
83
|
+
- Centralized (single point of failure)
|
|
84
|
+
- May have rate limits or access restrictions
|
|
85
|
+
|
|
86
|
+
**Building your own gives you:**
|
|
87
|
+
- Full control over data collection
|
|
88
|
+
- Customizable metrics
|
|
89
|
+
- No rate limits
|
|
90
|
+
- Can add custom features
|
|
91
|
+
- Own your infrastructure
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Understanding the Components
|
|
96
|
+
|
|
97
|
+
Before building, let's understand each component:
|
|
98
|
+
|
|
99
|
+
### 1. Blockchain Indexer
|
|
100
|
+
|
|
101
|
+
**Purpose:** Discover and track providers registered on Akash Network
|
|
102
|
+
|
|
103
|
+
**What it does:**
|
|
104
|
+
```
|
|
105
|
+
Every 10-30 minutes:
|
|
106
|
+
↓
|
|
107
|
+
Query blockchain for all providers
|
|
108
|
+
↓
|
|
109
|
+
Check for new providers or updates
|
|
110
|
+
↓
|
|
111
|
+
Store in database
|
|
112
|
+
↓
|
|
113
|
+
Update provider metadata
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Key data collected:**
|
|
117
|
+
- Provider address (unique identifier)
|
|
118
|
+
- Host URI (provider's API endpoint)
|
|
119
|
+
- Attributes (audited-by, region, capabilities)
|
|
120
|
+
- Registration height (when they joined)
|
|
121
|
+
|
|
122
|
+
### 2. Health Checker
|
|
123
|
+
|
|
124
|
+
**Purpose:** Continuously ping providers to check if they're online
|
|
125
|
+
|
|
126
|
+
**What it does:**
|
|
127
|
+
```
|
|
128
|
+
Every 5-10 minutes:
|
|
129
|
+
↓
|
|
130
|
+
For each known provider:
|
|
131
|
+
↓
|
|
132
|
+
Send HTTP request to /status endpoint
|
|
133
|
+
↓
|
|
134
|
+
Record: online/offline, response time
|
|
135
|
+
↓
|
|
136
|
+
Store result in time-series database
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Key metrics collected:**
|
|
140
|
+
- Is online? (boolean)
|
|
141
|
+
- Response time (milliseconds)
|
|
142
|
+
- Error message (if offline)
|
|
143
|
+
- Timestamp
|
|
144
|
+
|
|
145
|
+
### 3. Metrics Calculator
|
|
146
|
+
|
|
147
|
+
**Purpose:** Aggregate health check data into uptime percentages
|
|
148
|
+
|
|
149
|
+
**What it does:**
|
|
150
|
+
```
|
|
151
|
+
Every 1 hour:
|
|
152
|
+
↓
|
|
153
|
+
For each provider:
|
|
154
|
+
↓
|
|
155
|
+
Calculate uptime for 1d, 7d, 30d periods
|
|
156
|
+
↓
|
|
157
|
+
Update provider_metrics table
|
|
158
|
+
↓
|
|
159
|
+
Calculate averages, percentages
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Calculations:**
|
|
163
|
+
```
|
|
164
|
+
uptime_7d = (successful_checks / total_checks) over last 7 days
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
- 2016 checks in 7 days (144 checks/day × 7 days, checking every 10 minutes)
|
|
168
|
+
- 2010 successful, 6 failed
|
|
169
|
+
- Uptime = 2010/2016 = 99.7%
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 4. API Server
|
|
173
|
+
|
|
174
|
+
**Purpose:** Serve provider data to your applications
|
|
175
|
+
|
|
176
|
+
**What it provides:**
|
|
177
|
+
```
|
|
178
|
+
GET /v1/providers
|
|
179
|
+
→ Returns all providers with uptime stats
|
|
180
|
+
|
|
181
|
+
GET /v1/providers/:address
|
|
182
|
+
→ Returns detailed info for one provider
|
|
183
|
+
|
|
184
|
+
GET /v1/providers/:address/history
|
|
185
|
+
→ Returns historical uptime data
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Architecture Overview
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
194
|
+
│ Akash Network Blockchain │
|
|
195
|
+
│ (Source of truth for provider registrations) │
|
|
196
|
+
└────────────────────┬────────────────────────────────────────────┘
|
|
197
|
+
│
|
|
198
|
+
│ RPC Queries (every 10-30 min)
|
|
199
|
+
│
|
|
200
|
+
▼
|
|
201
|
+
┌──────────────────────┐
|
|
202
|
+
│ Blockchain Indexer │
|
|
203
|
+
│ Discovers providers │
|
|
204
|
+
└──────────┬───────────┘
|
|
205
|
+
│
|
|
206
|
+
│ Stores provider info
|
|
207
|
+
│
|
|
208
|
+
▼
|
|
209
|
+
┌──────────────────────┐
|
|
210
|
+
│ PostgreSQL Database │◄───────────┐
|
|
211
|
+
│ (Provider registry) │ │
|
|
212
|
+
└──────────┬───────────┘ │
|
|
213
|
+
│ │
|
|
214
|
+
│ Reads provider list │ Stores health checks
|
|
215
|
+
│ │
|
|
216
|
+
▼ │
|
|
217
|
+
┌──────────────────────┐ │
|
|
218
|
+
│ Health Checker │────────────┘
|
|
219
|
+
│ Pings all providers │
|
|
220
|
+
│ every 5-10 minutes │
|
|
221
|
+
└──────────────────────┘
|
|
222
|
+
│
|
|
223
|
+
│ Triggers every hour
|
|
224
|
+
│
|
|
225
|
+
▼
|
|
226
|
+
┌──────────────────────┐
|
|
227
|
+
│ Metrics Calculator │
|
|
228
|
+
│ Computes uptime % │
|
|
229
|
+
└──────────┬───────────┘
|
|
230
|
+
│
|
|
231
|
+
│ Updates metrics
|
|
232
|
+
│
|
|
233
|
+
▼
|
|
234
|
+
┌──────────────────────┐
|
|
235
|
+
│ PostgreSQL Database │
|
|
236
|
+
│ (Metrics table) │
|
|
237
|
+
└──────────┬───────────┘
|
|
238
|
+
│
|
|
239
|
+
│ Queries data
|
|
240
|
+
│
|
|
241
|
+
▼
|
|
242
|
+
┌──────────────────────┐
|
|
243
|
+
│ API Server │
|
|
244
|
+
│ Express/Fastify │
|
|
245
|
+
└──────────┬───────────┘
|
|
246
|
+
│
|
|
247
|
+
│ HTTP/JSON
|
|
248
|
+
│
|
|
249
|
+
▼
|
|
250
|
+
┌──────────────────────┐
|
|
251
|
+
│ Your Applications │
|
|
252
|
+
│ (kadi-deploy, etc) │
|
|
253
|
+
└──────────────────────┘
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Data Flow Example
|
|
257
|
+
|
|
258
|
+
**Scenario:** A new provider joins Akash Network
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
1. Provider registers on blockchain
|
|
262
|
+
↓
|
|
263
|
+
2. Indexer detects new provider (within 30 min)
|
|
264
|
+
↓
|
|
265
|
+
3. Indexer stores provider in database
|
|
266
|
+
↓
|
|
267
|
+
4. Health Checker sees new provider in database
|
|
268
|
+
↓
|
|
269
|
+
5. Health Checker starts pinging provider every 10 min
|
|
270
|
+
↓
|
|
271
|
+
6. After 24 hours, enough data exists
|
|
272
|
+
↓
|
|
273
|
+
7. Metrics Calculator computes uptime_1d
|
|
274
|
+
↓
|
|
275
|
+
8. API serves provider data with uptime stats
|
|
276
|
+
↓
|
|
277
|
+
9. Your deployment tool uses data to filter providers
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Prerequisites
|
|
283
|
+
|
|
284
|
+
### Knowledge Requirements
|
|
285
|
+
|
|
286
|
+
- **TypeScript/Node.js** - Core implementation language
|
|
287
|
+
- **SQL/PostgreSQL** - Database queries and schema design
|
|
288
|
+
- **REST APIs** - Building and consuming HTTP APIs
|
|
289
|
+
- **Async Programming** - Handling concurrent operations
|
|
290
|
+
- **Blockchain basics** - Understanding RPC, queries, addresses
|
|
291
|
+
|
|
292
|
+
### System Requirements
|
|
293
|
+
|
|
294
|
+
**Development:**
|
|
295
|
+
- Node.js 18+ or Bun
|
|
296
|
+
- PostgreSQL 14+ (or Docker with PostgreSQL image)
|
|
297
|
+
- 4GB RAM minimum
|
|
298
|
+
- 10GB storage
|
|
299
|
+
|
|
300
|
+
**Production:**
|
|
301
|
+
- VPS or cloud instance (2-4GB RAM recommended)
|
|
302
|
+
- PostgreSQL with TimescaleDB extension (optional but recommended)
|
|
303
|
+
- 50GB+ storage (for historical data)
|
|
304
|
+
- Reliable network connection
|
|
305
|
+
|
|
306
|
+
### Dependencies
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"dependencies": {
|
|
311
|
+
"@akashnetwork/akashjs": "^0.7.0",
|
|
312
|
+
"@akashnetwork/akash-api": "^1.0.0",
|
|
313
|
+
"pg": "^8.11.0",
|
|
314
|
+
"express": "^4.18.2",
|
|
315
|
+
"node-cron": "^3.0.2",
|
|
316
|
+
"axios": "^1.6.0"
|
|
317
|
+
},
|
|
318
|
+
"devDependencies": {
|
|
319
|
+
"@types/node": "^20.0.0",
|
|
320
|
+
"@types/pg": "^8.10.0",
|
|
321
|
+
"@types/express": "^4.17.17",
|
|
322
|
+
"typescript": "^5.2.0",
|
|
323
|
+
"tsx": "^4.7.0"
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Implementation Guide
|
|
331
|
+
|
|
332
|
+
### Step 1: Project Setup
|
|
333
|
+
|
|
334
|
+
Create the project structure:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
mkdir akash-provider-tracker
|
|
338
|
+
cd akash-provider-tracker
|
|
339
|
+
npm init -y
|
|
340
|
+
|
|
341
|
+
# Install dependencies
|
|
342
|
+
npm install @akashnetwork/akashjs @akashnetwork/akash-api pg express node-cron axios
|
|
343
|
+
npm install -D @types/node @types/pg @types/express typescript tsx
|
|
344
|
+
|
|
345
|
+
# Initialize TypeScript
|
|
346
|
+
npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --esModuleInterop true --outDir dist
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Project structure:
|
|
350
|
+
```
|
|
351
|
+
akash-provider-tracker/
|
|
352
|
+
├── src/
|
|
353
|
+
│ ├── indexer/
|
|
354
|
+
│ │ └── blockchain-indexer.ts
|
|
355
|
+
│ ├── health/
|
|
356
|
+
│ │ └── health-checker.ts
|
|
357
|
+
│ ├── metrics/
|
|
358
|
+
│ │ └── metrics-calculator.ts
|
|
359
|
+
│ ├── api/
|
|
360
|
+
│ │ └── server.ts
|
|
361
|
+
│ ├── database/
|
|
362
|
+
│ │ ├── client.ts
|
|
363
|
+
│ │ └── schema.sql
|
|
364
|
+
│ ├── types/
|
|
365
|
+
│ │ └── index.ts
|
|
366
|
+
│ └── index.ts
|
|
367
|
+
├── package.json
|
|
368
|
+
├── tsconfig.json
|
|
369
|
+
└── README.md
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Step 2: Database Schema
|
|
373
|
+
|
|
374
|
+
Create `src/database/schema.sql`:
|
|
375
|
+
|
|
376
|
+
```sql
|
|
377
|
+
-- Enable UUID extension
|
|
378
|
+
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
379
|
+
|
|
380
|
+
-- Providers table
|
|
381
|
+
-- Stores basic information about each provider
|
|
382
|
+
CREATE TABLE providers (
|
|
383
|
+
-- Primary key: provider's blockchain address
|
|
384
|
+
address TEXT PRIMARY KEY,
|
|
385
|
+
|
|
386
|
+
-- Provider's API endpoint (e.g., https://provider.example.com:8443)
|
|
387
|
+
host_uri TEXT NOT NULL,
|
|
388
|
+
|
|
389
|
+
-- Optional human-readable name
|
|
390
|
+
name TEXT,
|
|
391
|
+
|
|
392
|
+
-- Blockchain height when provider was registered
|
|
393
|
+
created_height BIGINT,
|
|
394
|
+
|
|
395
|
+
-- Whether provider is audited by at least one auditor
|
|
396
|
+
is_audited BOOLEAN DEFAULT FALSE,
|
|
397
|
+
|
|
398
|
+
-- Provider attributes as JSON (flexible storage)
|
|
399
|
+
attributes JSONB DEFAULT '[]'::jsonb,
|
|
400
|
+
|
|
401
|
+
-- Metadata timestamps
|
|
402
|
+
first_seen_at TIMESTAMP DEFAULT NOW(),
|
|
403
|
+
last_updated_at TIMESTAMP DEFAULT NOW(),
|
|
404
|
+
|
|
405
|
+
-- Indexes for fast queries
|
|
406
|
+
CONSTRAINT valid_host_uri CHECK (host_uri ~ '^https?://')
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
CREATE INDEX idx_providers_is_audited ON providers(is_audited);
|
|
410
|
+
CREATE INDEX idx_providers_created_height ON providers(created_height);
|
|
411
|
+
|
|
412
|
+
-- Health checks table
|
|
413
|
+
-- Time-series data: one row per check per provider
|
|
414
|
+
CREATE TABLE health_checks (
|
|
415
|
+
-- Unique ID for each check
|
|
416
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
417
|
+
|
|
418
|
+
-- Which provider was checked
|
|
419
|
+
provider_address TEXT NOT NULL REFERENCES providers(address) ON DELETE CASCADE,
|
|
420
|
+
|
|
421
|
+
-- When the check was performed
|
|
422
|
+
checked_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
423
|
+
|
|
424
|
+
-- Result: was the provider online?
|
|
425
|
+
is_online BOOLEAN NOT NULL,
|
|
426
|
+
|
|
427
|
+
-- How long did the provider take to respond (NULL if offline)
|
|
428
|
+
response_time_ms INTEGER,
|
|
429
|
+
|
|
430
|
+
-- HTTP status code (200, 404, 500, etc)
|
|
431
|
+
http_status INTEGER,
|
|
432
|
+
|
|
433
|
+
-- Error message if check failed
|
|
434
|
+
error_message TEXT,
|
|
435
|
+
|
|
436
|
+
-- Additional metadata from /status endpoint
|
|
437
|
+
metadata JSONB
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
-- Indexes for fast time-series queries
|
|
441
|
+
CREATE INDEX idx_health_checks_provider ON health_checks(provider_address, checked_at DESC);
|
|
442
|
+
CREATE INDEX idx_health_checks_time ON health_checks(checked_at DESC);
|
|
443
|
+
|
|
444
|
+
-- Optional: Convert to TimescaleDB hypertable for better performance
|
|
445
|
+
-- Requires TimescaleDB extension
|
|
446
|
+
-- SELECT create_hypertable('health_checks', 'checked_at');
|
|
447
|
+
|
|
448
|
+
-- Provider metrics table
|
|
449
|
+
-- Aggregated statistics, updated hourly
|
|
450
|
+
CREATE TABLE provider_metrics (
|
|
451
|
+
-- One row per provider
|
|
452
|
+
provider_address TEXT PRIMARY KEY REFERENCES providers(address) ON DELETE CASCADE,
|
|
453
|
+
|
|
454
|
+
-- Uptime percentages (0.0 to 1.0)
|
|
455
|
+
uptime_1d DECIMAL(5,4), -- Last 24 hours
|
|
456
|
+
uptime_7d DECIMAL(5,4), -- Last 7 days
|
|
457
|
+
uptime_30d DECIMAL(5,4), -- Last 30 days
|
|
458
|
+
|
|
459
|
+
-- Current status
|
|
460
|
+
is_currently_online BOOLEAN,
|
|
461
|
+
last_online_at TIMESTAMP,
|
|
462
|
+
last_offline_at TIMESTAMP,
|
|
463
|
+
|
|
464
|
+
-- Response time statistics (milliseconds)
|
|
465
|
+
avg_response_time_1d INTEGER,
|
|
466
|
+
avg_response_time_7d INTEGER,
|
|
467
|
+
avg_response_time_30d INTEGER,
|
|
468
|
+
|
|
469
|
+
-- Count of checks performed
|
|
470
|
+
total_checks_1d INTEGER DEFAULT 0,
|
|
471
|
+
total_checks_7d INTEGER DEFAULT 0,
|
|
472
|
+
total_checks_30d INTEGER DEFAULT 0,
|
|
473
|
+
|
|
474
|
+
-- Metadata
|
|
475
|
+
last_calculated_at TIMESTAMP DEFAULT NOW()
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
CREATE INDEX idx_metrics_uptime_7d ON provider_metrics(uptime_7d DESC);
|
|
479
|
+
CREATE INDEX idx_metrics_currently_online ON provider_metrics(is_currently_online);
|
|
480
|
+
|
|
481
|
+
-- Provider locations table (optional)
|
|
482
|
+
-- IP geolocation data
|
|
483
|
+
CREATE TABLE provider_locations (
|
|
484
|
+
provider_address TEXT PRIMARY KEY REFERENCES providers(address) ON DELETE CASCADE,
|
|
485
|
+
|
|
486
|
+
-- Geographic data
|
|
487
|
+
country TEXT,
|
|
488
|
+
country_code TEXT,
|
|
489
|
+
region TEXT,
|
|
490
|
+
region_code TEXT,
|
|
491
|
+
city TEXT,
|
|
492
|
+
latitude DECIMAL(10,8),
|
|
493
|
+
longitude DECIMAL(11,8),
|
|
494
|
+
timezone TEXT,
|
|
495
|
+
|
|
496
|
+
-- IP information
|
|
497
|
+
ip_address TEXT,
|
|
498
|
+
|
|
499
|
+
-- When location was determined
|
|
500
|
+
detected_at TIMESTAMP DEFAULT NOW()
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
CREATE INDEX idx_locations_country ON provider_locations(country_code);
|
|
504
|
+
|
|
505
|
+
-- Provider versions table
|
|
506
|
+
-- Track provider software versions over time
|
|
507
|
+
CREATE TABLE provider_versions (
|
|
508
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
509
|
+
provider_address TEXT NOT NULL REFERENCES providers(address) ON DELETE CASCADE,
|
|
510
|
+
|
|
511
|
+
-- Version information
|
|
512
|
+
akash_version TEXT,
|
|
513
|
+
cosmos_sdk_version TEXT,
|
|
514
|
+
|
|
515
|
+
-- Kubernetes version (from /version endpoint)
|
|
516
|
+
k8s_version TEXT,
|
|
517
|
+
|
|
518
|
+
-- When this version was detected
|
|
519
|
+
detected_at TIMESTAMP DEFAULT NOW()
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
CREATE INDEX idx_versions_provider ON provider_versions(provider_address, detected_at DESC);
|
|
523
|
+
|
|
524
|
+
-- Create view for easy querying
|
|
525
|
+
CREATE VIEW provider_details AS
|
|
526
|
+
SELECT
|
|
527
|
+
p.address,
|
|
528
|
+
p.host_uri,
|
|
529
|
+
p.name,
|
|
530
|
+
p.is_audited,
|
|
531
|
+
p.created_height,
|
|
532
|
+
p.first_seen_at,
|
|
533
|
+
m.uptime_1d,
|
|
534
|
+
m.uptime_7d,
|
|
535
|
+
m.uptime_30d,
|
|
536
|
+
m.is_currently_online,
|
|
537
|
+
m.last_online_at,
|
|
538
|
+
m.avg_response_time_7d,
|
|
539
|
+
l.country,
|
|
540
|
+
l.country_code,
|
|
541
|
+
l.region,
|
|
542
|
+
l.city,
|
|
543
|
+
l.latitude,
|
|
544
|
+
l.longitude
|
|
545
|
+
FROM providers p
|
|
546
|
+
LEFT JOIN provider_metrics m ON p.address = m.provider_address
|
|
547
|
+
LEFT JOIN provider_locations l ON p.address = l.provider_address;
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Step 3: Database Client
|
|
551
|
+
|
|
552
|
+
Create `src/database/client.ts`:
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
import pg from 'pg';
|
|
556
|
+
const { Pool } = pg;
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Database configuration
|
|
560
|
+
*/
|
|
561
|
+
interface DatabaseConfig {
|
|
562
|
+
host: string;
|
|
563
|
+
port: number;
|
|
564
|
+
database: string;
|
|
565
|
+
user: string;
|
|
566
|
+
password: string;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Database client singleton
|
|
571
|
+
*/
|
|
572
|
+
class DatabaseClient {
|
|
573
|
+
private static instance: DatabaseClient;
|
|
574
|
+
private pool: pg.Pool;
|
|
575
|
+
|
|
576
|
+
private constructor(config: DatabaseConfig) {
|
|
577
|
+
this.pool = new Pool({
|
|
578
|
+
host: config.host,
|
|
579
|
+
port: config.port,
|
|
580
|
+
database: config.database,
|
|
581
|
+
user: config.user,
|
|
582
|
+
password: config.password,
|
|
583
|
+
max: 20, // Maximum number of connections
|
|
584
|
+
idleTimeoutMillis: 30000,
|
|
585
|
+
connectionTimeoutMillis: 2000,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Handle pool errors
|
|
589
|
+
this.pool.on('error', (err) => {
|
|
590
|
+
console.error('Unexpected database pool error:', err);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Get database client instance (singleton)
|
|
596
|
+
*/
|
|
597
|
+
static getInstance(config?: DatabaseConfig): DatabaseClient {
|
|
598
|
+
if (!DatabaseClient.instance) {
|
|
599
|
+
if (!config) {
|
|
600
|
+
throw new Error('Database config required for first initialization');
|
|
601
|
+
}
|
|
602
|
+
DatabaseClient.instance = new DatabaseClient(config);
|
|
603
|
+
}
|
|
604
|
+
return DatabaseClient.instance;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Execute a query
|
|
609
|
+
*/
|
|
610
|
+
async query<T = any>(text: string, params?: any[]): Promise<pg.QueryResult<T>> {
|
|
611
|
+
const start = Date.now();
|
|
612
|
+
try {
|
|
613
|
+
const result = await this.pool.query<T>(text, params);
|
|
614
|
+
const duration = Date.now() - start;
|
|
615
|
+
|
|
616
|
+
// Log slow queries (> 1 second)
|
|
617
|
+
if (duration > 1000) {
|
|
618
|
+
console.warn(`Slow query (${duration}ms):`, text);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return result;
|
|
622
|
+
} catch (error) {
|
|
623
|
+
console.error('Database query error:', error);
|
|
624
|
+
console.error('Query:', text);
|
|
625
|
+
console.error('Params:', params);
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Execute multiple queries in a transaction
|
|
632
|
+
*/
|
|
633
|
+
async transaction<T>(callback: (client: pg.PoolClient) => Promise<T>): Promise<T> {
|
|
634
|
+
const client = await this.pool.connect();
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
await client.query('BEGIN');
|
|
638
|
+
const result = await callback(client);
|
|
639
|
+
await client.query('COMMIT');
|
|
640
|
+
return result;
|
|
641
|
+
} catch (error) {
|
|
642
|
+
await client.query('ROLLBACK');
|
|
643
|
+
throw error;
|
|
644
|
+
} finally {
|
|
645
|
+
client.release();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Close database connection pool
|
|
651
|
+
*/
|
|
652
|
+
async close(): Promise<void> {
|
|
653
|
+
await this.pool.end();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Check if database is healthy
|
|
658
|
+
*/
|
|
659
|
+
async healthCheck(): Promise<boolean> {
|
|
660
|
+
try {
|
|
661
|
+
await this.query('SELECT 1');
|
|
662
|
+
return true;
|
|
663
|
+
} catch (error) {
|
|
664
|
+
console.error('Database health check failed:', error);
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Export singleton instance
|
|
671
|
+
export const getDatabaseClient = (config?: DatabaseConfig) => {
|
|
672
|
+
return DatabaseClient.getInstance(config);
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
export default DatabaseClient;
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### Step 4: Type Definitions
|
|
679
|
+
|
|
680
|
+
Create `src/types/index.ts`:
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
/**
|
|
684
|
+
* Provider information from blockchain
|
|
685
|
+
*/
|
|
686
|
+
export interface Provider {
|
|
687
|
+
address: string;
|
|
688
|
+
hostUri: string;
|
|
689
|
+
name?: string;
|
|
690
|
+
createdHeight: number;
|
|
691
|
+
isAudited: boolean;
|
|
692
|
+
attributes: ProviderAttribute[];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Provider attribute (key-value pair with auditor info)
|
|
697
|
+
*/
|
|
698
|
+
export interface ProviderAttribute {
|
|
699
|
+
key: string;
|
|
700
|
+
value: string;
|
|
701
|
+
auditedBy?: string[];
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Health check result
|
|
706
|
+
*/
|
|
707
|
+
export interface HealthCheck {
|
|
708
|
+
id?: string;
|
|
709
|
+
providerAddress: string;
|
|
710
|
+
checkedAt: Date;
|
|
711
|
+
isOnline: boolean;
|
|
712
|
+
responseTimeMs?: number;
|
|
713
|
+
httpStatus?: number;
|
|
714
|
+
errorMessage?: string;
|
|
715
|
+
metadata?: Record<string, any>;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Provider metrics (aggregated statistics)
|
|
720
|
+
*/
|
|
721
|
+
export interface ProviderMetrics {
|
|
722
|
+
providerAddress: string;
|
|
723
|
+
uptime1d?: number; // 0.0 to 1.0
|
|
724
|
+
uptime7d?: number;
|
|
725
|
+
uptime30d?: number;
|
|
726
|
+
isCurrentlyOnline: boolean;
|
|
727
|
+
lastOnlineAt?: Date;
|
|
728
|
+
lastOfflineAt?: Date;
|
|
729
|
+
avgResponseTime1d?: number;
|
|
730
|
+
avgResponseTime7d?: number;
|
|
731
|
+
avgResponseTime30d?: number;
|
|
732
|
+
totalChecks1d: number;
|
|
733
|
+
totalChecks7d: number;
|
|
734
|
+
totalChecks30d: number;
|
|
735
|
+
lastCalculatedAt: Date;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Provider location data
|
|
740
|
+
*/
|
|
741
|
+
export interface ProviderLocation {
|
|
742
|
+
providerAddress: string;
|
|
743
|
+
country?: string;
|
|
744
|
+
countryCode?: string;
|
|
745
|
+
region?: string;
|
|
746
|
+
regionCode?: string;
|
|
747
|
+
city?: string;
|
|
748
|
+
latitude?: number;
|
|
749
|
+
longitude?: number;
|
|
750
|
+
timezone?: string;
|
|
751
|
+
ipAddress?: string;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Provider status endpoint response
|
|
756
|
+
* (what we get from https://provider.example.com/status)
|
|
757
|
+
*/
|
|
758
|
+
export interface ProviderStatus {
|
|
759
|
+
cluster: {
|
|
760
|
+
leases: number;
|
|
761
|
+
inventory: {
|
|
762
|
+
error?: string;
|
|
763
|
+
active: ResourceUsage[];
|
|
764
|
+
pending: ResourceUsage[];
|
|
765
|
+
available: {
|
|
766
|
+
nodes: ResourceCapacity[];
|
|
767
|
+
};
|
|
768
|
+
};
|
|
769
|
+
};
|
|
770
|
+
bidengine: {
|
|
771
|
+
orders: number;
|
|
772
|
+
};
|
|
773
|
+
manifest: {
|
|
774
|
+
deployments: number;
|
|
775
|
+
};
|
|
776
|
+
cluster_public_hostname?: string;
|
|
777
|
+
address: string;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export interface ResourceUsage {
|
|
781
|
+
cpu: number;
|
|
782
|
+
gpu: number;
|
|
783
|
+
memory: number;
|
|
784
|
+
storage_ephemeral: number;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export interface ResourceCapacity {
|
|
788
|
+
cpu: number;
|
|
789
|
+
gpu: number;
|
|
790
|
+
memory: number;
|
|
791
|
+
storage_ephemeral: number;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Provider version endpoint response
|
|
796
|
+
* (what we get from https://provider.example.com/version)
|
|
797
|
+
*/
|
|
798
|
+
export interface ProviderVersion {
|
|
799
|
+
akash: {
|
|
800
|
+
version: string;
|
|
801
|
+
commit: string;
|
|
802
|
+
buildTags: string;
|
|
803
|
+
go: string;
|
|
804
|
+
cosmosSdkVersion: string;
|
|
805
|
+
};
|
|
806
|
+
kube: {
|
|
807
|
+
major: string;
|
|
808
|
+
minor: string;
|
|
809
|
+
gitVersion: string;
|
|
810
|
+
gitCommit: string;
|
|
811
|
+
gitTreeState: string;
|
|
812
|
+
buildDate: string;
|
|
813
|
+
goVersion: string;
|
|
814
|
+
compiler: string;
|
|
815
|
+
platform: string;
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Complete provider details (for API responses)
|
|
821
|
+
*/
|
|
822
|
+
export interface ProviderDetails {
|
|
823
|
+
address: string;
|
|
824
|
+
hostUri: string;
|
|
825
|
+
name?: string;
|
|
826
|
+
isAudited: boolean;
|
|
827
|
+
createdHeight: number;
|
|
828
|
+
firstSeenAt: Date;
|
|
829
|
+
|
|
830
|
+
// Metrics
|
|
831
|
+
uptime1d?: number;
|
|
832
|
+
uptime7d?: number;
|
|
833
|
+
uptime30d?: number;
|
|
834
|
+
isCurrentlyOnline: boolean;
|
|
835
|
+
lastOnlineAt?: Date;
|
|
836
|
+
avgResponseTime7d?: number;
|
|
837
|
+
|
|
838
|
+
// Location
|
|
839
|
+
country?: string;
|
|
840
|
+
countryCode?: string;
|
|
841
|
+
region?: string;
|
|
842
|
+
city?: string;
|
|
843
|
+
latitude?: number;
|
|
844
|
+
longitude?: number;
|
|
845
|
+
|
|
846
|
+
// Additional
|
|
847
|
+
attributes: ProviderAttribute[];
|
|
848
|
+
}
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
### Step 5: Blockchain Indexer
|
|
852
|
+
|
|
853
|
+
Create `src/indexer/blockchain-indexer.ts`:
|
|
854
|
+
|
|
855
|
+
```typescript
|
|
856
|
+
import { getRpc } from '@akashnetwork/akashjs/build/rpc';
|
|
857
|
+
import { QueryClientImpl as QueryProviderClient } from '@akashnetwork/akash-api/akash/provider/v1beta3';
|
|
858
|
+
import { getDatabaseClient } from '../database/client.js';
|
|
859
|
+
import type { Provider, ProviderAttribute } from '../types/index.js';
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Blockchain Indexer
|
|
863
|
+
*
|
|
864
|
+
* Discovers providers registered on Akash blockchain and stores them in database.
|
|
865
|
+
* Runs periodically to catch new providers and updates.
|
|
866
|
+
*/
|
|
867
|
+
export class BlockchainIndexer {
|
|
868
|
+
private rpcEndpoint: string;
|
|
869
|
+
private db: ReturnType<typeof getDatabaseClient>;
|
|
870
|
+
private isRunning = false;
|
|
871
|
+
|
|
872
|
+
constructor(rpcEndpoint: string = 'https://rpc.akashnet.net:443') {
|
|
873
|
+
this.rpcEndpoint = rpcEndpoint;
|
|
874
|
+
this.db = getDatabaseClient();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Start the indexer (runs in background)
|
|
879
|
+
*/
|
|
880
|
+
async start(intervalMinutes: number = 30): Promise<void> {
|
|
881
|
+
console.log('🔍 Starting blockchain indexer...');
|
|
882
|
+
console.log(` RPC: ${this.rpcEndpoint}`);
|
|
883
|
+
console.log(` Interval: ${intervalMinutes} minutes`);
|
|
884
|
+
|
|
885
|
+
// Initial run
|
|
886
|
+
await this.indexProviders();
|
|
887
|
+
|
|
888
|
+
// Schedule periodic runs
|
|
889
|
+
setInterval(async () => {
|
|
890
|
+
if (!this.isRunning) {
|
|
891
|
+
await this.indexProviders();
|
|
892
|
+
}
|
|
893
|
+
}, intervalMinutes * 60 * 1000);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Main indexing logic
|
|
898
|
+
*/
|
|
899
|
+
private async indexProviders(): Promise<void> {
|
|
900
|
+
if (this.isRunning) {
|
|
901
|
+
console.log('⏩ Indexer already running, skipping...');
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
this.isRunning = true;
|
|
906
|
+
const startTime = Date.now();
|
|
907
|
+
|
|
908
|
+
try {
|
|
909
|
+
console.log('🔄 Fetching providers from blockchain...');
|
|
910
|
+
|
|
911
|
+
// Step 1: Connect to Akash RPC
|
|
912
|
+
const rpc = await getRpc(this.rpcEndpoint);
|
|
913
|
+
const queryClient = new QueryProviderClient(rpc);
|
|
914
|
+
|
|
915
|
+
// Step 2: Query all providers
|
|
916
|
+
// Note: This returns paginated results, we need to fetch all pages
|
|
917
|
+
let allProviders: any[] = [];
|
|
918
|
+
let nextKey: Uint8Array | undefined = undefined;
|
|
919
|
+
|
|
920
|
+
do {
|
|
921
|
+
const response = await queryClient.Providers({
|
|
922
|
+
pagination: {
|
|
923
|
+
key: nextKey || new Uint8Array(),
|
|
924
|
+
offset: 0n,
|
|
925
|
+
limit: 100n,
|
|
926
|
+
countTotal: true,
|
|
927
|
+
reverse: false,
|
|
928
|
+
},
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
if (response.providers) {
|
|
932
|
+
allProviders = allProviders.concat(response.providers);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
nextKey = response.pagination?.nextKey;
|
|
936
|
+
} while (nextKey && nextKey.length > 0);
|
|
937
|
+
|
|
938
|
+
console.log(` Found ${allProviders.length} providers on blockchain`);
|
|
939
|
+
|
|
940
|
+
// Step 3: Process each provider
|
|
941
|
+
let newProviders = 0;
|
|
942
|
+
let updatedProviders = 0;
|
|
943
|
+
|
|
944
|
+
for (const provider of allProviders) {
|
|
945
|
+
const transformed = this.transformProvider(provider);
|
|
946
|
+
const exists = await this.providerExists(transformed.address);
|
|
947
|
+
|
|
948
|
+
if (exists) {
|
|
949
|
+
await this.updateProvider(transformed);
|
|
950
|
+
updatedProviders++;
|
|
951
|
+
} else {
|
|
952
|
+
await this.insertProvider(transformed);
|
|
953
|
+
newProviders++;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
958
|
+
console.log(`✅ Indexing complete in ${duration}s`);
|
|
959
|
+
console.log(` New providers: ${newProviders}`);
|
|
960
|
+
console.log(` Updated providers: ${updatedProviders}`);
|
|
961
|
+
console.log(` Total in database: ${allProviders.length}`);
|
|
962
|
+
|
|
963
|
+
} catch (error) {
|
|
964
|
+
console.error('❌ Indexer error:', error);
|
|
965
|
+
throw error;
|
|
966
|
+
} finally {
|
|
967
|
+
this.isRunning = false;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Transform blockchain provider data to our format
|
|
973
|
+
*/
|
|
974
|
+
private transformProvider(provider: any): Provider {
|
|
975
|
+
// Extract attributes
|
|
976
|
+
const attributes: ProviderAttribute[] = (provider.attributes || []).map((attr: any) => ({
|
|
977
|
+
key: attr.key || '',
|
|
978
|
+
value: attr.value || '',
|
|
979
|
+
auditedBy: attr.auditedBy || [],
|
|
980
|
+
}));
|
|
981
|
+
|
|
982
|
+
// Check if provider is audited (has "audited-by" attribute)
|
|
983
|
+
const isAudited = attributes.some(attr => attr.key === 'audited-by' && attr.value);
|
|
984
|
+
|
|
985
|
+
return {
|
|
986
|
+
address: provider.owner || '',
|
|
987
|
+
hostUri: provider.hostUri || '',
|
|
988
|
+
name: undefined, // Will be populated from /status endpoint by health checker
|
|
989
|
+
createdHeight: Number(provider.createdHeight || 0),
|
|
990
|
+
isAudited,
|
|
991
|
+
attributes,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Check if provider exists in database
|
|
997
|
+
*/
|
|
998
|
+
private async providerExists(address: string): Promise<boolean> {
|
|
999
|
+
const result = await this.db.query(
|
|
1000
|
+
'SELECT address FROM providers WHERE address = $1',
|
|
1001
|
+
[address]
|
|
1002
|
+
);
|
|
1003
|
+
return result.rows.length > 0;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Insert new provider into database
|
|
1008
|
+
*/
|
|
1009
|
+
private async insertProvider(provider: Provider): Promise<void> {
|
|
1010
|
+
await this.db.query(
|
|
1011
|
+
`INSERT INTO providers
|
|
1012
|
+
(address, host_uri, name, created_height, is_audited, attributes, first_seen_at, last_updated_at)
|
|
1013
|
+
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())`,
|
|
1014
|
+
[
|
|
1015
|
+
provider.address,
|
|
1016
|
+
provider.hostUri,
|
|
1017
|
+
provider.name,
|
|
1018
|
+
provider.createdHeight,
|
|
1019
|
+
provider.isAudited,
|
|
1020
|
+
JSON.stringify(provider.attributes),
|
|
1021
|
+
]
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Update existing provider in database
|
|
1027
|
+
*/
|
|
1028
|
+
private async updateProvider(provider: Provider): Promise<void> {
|
|
1029
|
+
await this.db.query(
|
|
1030
|
+
`UPDATE providers
|
|
1031
|
+
SET host_uri = $2,
|
|
1032
|
+
is_audited = $3,
|
|
1033
|
+
attributes = $4,
|
|
1034
|
+
last_updated_at = NOW()
|
|
1035
|
+
WHERE address = $1`,
|
|
1036
|
+
[
|
|
1037
|
+
provider.address,
|
|
1038
|
+
provider.hostUri,
|
|
1039
|
+
provider.isAudited,
|
|
1040
|
+
JSON.stringify(provider.attributes),
|
|
1041
|
+
]
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Get count of providers in database
|
|
1047
|
+
*/
|
|
1048
|
+
async getProviderCount(): Promise<number> {
|
|
1049
|
+
const result = await this.db.query('SELECT COUNT(*) as count FROM providers');
|
|
1050
|
+
return parseInt(result.rows[0].count);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
### Step 6: Health Checker
|
|
1056
|
+
|
|
1057
|
+
Create `src/health/health-checker.ts`:
|
|
1058
|
+
|
|
1059
|
+
```typescript
|
|
1060
|
+
import axios from 'axios';
|
|
1061
|
+
import { getDatabaseClient } from '../database/client.js';
|
|
1062
|
+
import type { HealthCheck, ProviderStatus, ProviderVersion } from '../types/index.js';
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Health Checker
|
|
1066
|
+
*
|
|
1067
|
+
* Continuously pings all providers to check if they're online.
|
|
1068
|
+
* Records results in time-series database for uptime calculations.
|
|
1069
|
+
*/
|
|
1070
|
+
export class HealthChecker {
|
|
1071
|
+
private db: ReturnType<typeof getDatabaseClient>;
|
|
1072
|
+
private isRunning = false;
|
|
1073
|
+
private checkTimeout: number;
|
|
1074
|
+
private userAgent: string;
|
|
1075
|
+
|
|
1076
|
+
constructor(checkTimeoutMs: number = 5000) {
|
|
1077
|
+
this.db = getDatabaseClient();
|
|
1078
|
+
this.checkTimeout = checkTimeoutMs;
|
|
1079
|
+
this.userAgent = 'KADI-Provider-Tracker/1.0';
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Start the health checker (runs in background)
|
|
1084
|
+
*/
|
|
1085
|
+
async start(intervalMinutes: number = 10): Promise<void> {
|
|
1086
|
+
console.log('🏥 Starting health checker...');
|
|
1087
|
+
console.log(` Check timeout: ${this.checkTimeout}ms`);
|
|
1088
|
+
console.log(` Interval: ${intervalMinutes} minutes`);
|
|
1089
|
+
|
|
1090
|
+
// Initial run
|
|
1091
|
+
await this.checkAllProviders();
|
|
1092
|
+
|
|
1093
|
+
// Schedule periodic checks
|
|
1094
|
+
setInterval(async () => {
|
|
1095
|
+
if (!this.isRunning) {
|
|
1096
|
+
await this.checkAllProviders();
|
|
1097
|
+
}
|
|
1098
|
+
}, intervalMinutes * 60 * 1000);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Check all providers
|
|
1103
|
+
*/
|
|
1104
|
+
private async checkAllProviders(): Promise<void> {
|
|
1105
|
+
if (this.isRunning) {
|
|
1106
|
+
console.log('⏩ Health checker already running, skipping...');
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
this.isRunning = true;
|
|
1111
|
+
const startTime = Date.now();
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
// Fetch all providers from database
|
|
1115
|
+
const result = await this.db.query<{ address: string; host_uri: string }>(
|
|
1116
|
+
'SELECT address, host_uri FROM providers'
|
|
1117
|
+
);
|
|
1118
|
+
|
|
1119
|
+
const providers = result.rows;
|
|
1120
|
+
console.log(`🔍 Checking health of ${providers.length} providers...`);
|
|
1121
|
+
|
|
1122
|
+
// Check all providers in parallel (with concurrency limit)
|
|
1123
|
+
const concurrencyLimit = 10; // Check 10 providers at once
|
|
1124
|
+
const results: HealthCheck[] = [];
|
|
1125
|
+
|
|
1126
|
+
for (let i = 0; i < providers.length; i += concurrencyLimit) {
|
|
1127
|
+
const batch = providers.slice(i, i + concurrencyLimit);
|
|
1128
|
+
const batchResults = await Promise.all(
|
|
1129
|
+
batch.map(provider => this.checkProvider(provider.address, provider.host_uri))
|
|
1130
|
+
);
|
|
1131
|
+
results.push(...batchResults);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Store all results in database
|
|
1135
|
+
await this.storeHealthChecks(results);
|
|
1136
|
+
|
|
1137
|
+
// Calculate summary
|
|
1138
|
+
const onlineCount = results.filter(r => r.isOnline).length;
|
|
1139
|
+
const offlineCount = results.length - onlineCount;
|
|
1140
|
+
const avgResponseTime = results
|
|
1141
|
+
.filter(r => r.responseTimeMs)
|
|
1142
|
+
.reduce((sum, r) => sum + (r.responseTimeMs || 0), 0) / onlineCount;
|
|
1143
|
+
|
|
1144
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
1145
|
+
console.log(`✅ Health check complete in ${duration}s`);
|
|
1146
|
+
console.log(` Online: ${onlineCount} (${((onlineCount / results.length) * 100).toFixed(1)}%)`);
|
|
1147
|
+
console.log(` Offline: ${offlineCount}`);
|
|
1148
|
+
console.log(` Avg response time: ${avgResponseTime.toFixed(0)}ms`);
|
|
1149
|
+
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
console.error('❌ Health checker error:', error);
|
|
1152
|
+
throw error;
|
|
1153
|
+
} finally {
|
|
1154
|
+
this.isRunning = false;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Check a single provider's health
|
|
1160
|
+
*/
|
|
1161
|
+
private async checkProvider(address: string, hostUri: string): Promise<HealthCheck> {
|
|
1162
|
+
const checkedAt = new Date();
|
|
1163
|
+
|
|
1164
|
+
try {
|
|
1165
|
+
// Ping provider's /status endpoint
|
|
1166
|
+
const startTime = Date.now();
|
|
1167
|
+
|
|
1168
|
+
const response = await axios.get<ProviderStatus>(`${hostUri}/status`, {
|
|
1169
|
+
timeout: this.checkTimeout,
|
|
1170
|
+
headers: {
|
|
1171
|
+
'User-Agent': this.userAgent,
|
|
1172
|
+
'Accept': 'application/json',
|
|
1173
|
+
},
|
|
1174
|
+
validateStatus: () => true, // Don't throw on non-200 status
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
const responseTimeMs = Date.now() - startTime;
|
|
1178
|
+
const isOnline = response.status === 200;
|
|
1179
|
+
|
|
1180
|
+
// Try to get provider name from status response
|
|
1181
|
+
if (isOnline && response.data.address) {
|
|
1182
|
+
await this.updateProviderName(address, response.data.cluster_public_hostname);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
return {
|
|
1186
|
+
providerAddress: address,
|
|
1187
|
+
checkedAt,
|
|
1188
|
+
isOnline,
|
|
1189
|
+
responseTimeMs,
|
|
1190
|
+
httpStatus: response.status,
|
|
1191
|
+
errorMessage: isOnline ? undefined : `HTTP ${response.status}`,
|
|
1192
|
+
metadata: isOnline ? {
|
|
1193
|
+
leases: response.data.cluster?.leases,
|
|
1194
|
+
deployments: response.data.manifest?.deployments,
|
|
1195
|
+
} : undefined,
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
// Provider is offline or unreachable
|
|
1200
|
+
let errorMessage = 'Unknown error';
|
|
1201
|
+
|
|
1202
|
+
if (axios.isAxiosError(error)) {
|
|
1203
|
+
if (error.code === 'ECONNREFUSED') {
|
|
1204
|
+
errorMessage = 'Connection refused';
|
|
1205
|
+
} else if (error.code === 'ETIMEDOUT') {
|
|
1206
|
+
errorMessage = 'Timeout';
|
|
1207
|
+
} else if (error.code === 'ENOTFOUND') {
|
|
1208
|
+
errorMessage = 'DNS resolution failed';
|
|
1209
|
+
} else if (error.code === 'ECONNRESET') {
|
|
1210
|
+
errorMessage = 'Connection reset';
|
|
1211
|
+
} else {
|
|
1212
|
+
errorMessage = error.message;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
return {
|
|
1217
|
+
providerAddress: address,
|
|
1218
|
+
checkedAt,
|
|
1219
|
+
isOnline: false,
|
|
1220
|
+
errorMessage,
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Update provider name in database (if we discovered it from /status)
|
|
1227
|
+
*/
|
|
1228
|
+
private async updateProviderName(address: string, name?: string): Promise<void> {
|
|
1229
|
+
if (!name) return;
|
|
1230
|
+
|
|
1231
|
+
await this.db.query(
|
|
1232
|
+
'UPDATE providers SET name = $2 WHERE address = $1 AND name IS NULL',
|
|
1233
|
+
[address, name]
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Store health check results in database
|
|
1239
|
+
*/
|
|
1240
|
+
private async storeHealthChecks(checks: HealthCheck[]): Promise<void> {
|
|
1241
|
+
if (checks.length === 0) return;
|
|
1242
|
+
|
|
1243
|
+
// Build bulk insert query
|
|
1244
|
+
const values: any[] = [];
|
|
1245
|
+
const placeholders: string[] = [];
|
|
1246
|
+
|
|
1247
|
+
checks.forEach((check, index) => {
|
|
1248
|
+
const offset = index * 7;
|
|
1249
|
+
placeholders.push(
|
|
1250
|
+
`($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6}, $${offset + 7})`
|
|
1251
|
+
);
|
|
1252
|
+
values.push(
|
|
1253
|
+
check.providerAddress,
|
|
1254
|
+
check.checkedAt,
|
|
1255
|
+
check.isOnline,
|
|
1256
|
+
check.responseTimeMs || null,
|
|
1257
|
+
check.httpStatus || null,
|
|
1258
|
+
check.errorMessage || null,
|
|
1259
|
+
check.metadata ? JSON.stringify(check.metadata) : null
|
|
1260
|
+
);
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
const query = `
|
|
1264
|
+
INSERT INTO health_checks
|
|
1265
|
+
(provider_address, checked_at, is_online, response_time_ms, http_status, error_message, metadata)
|
|
1266
|
+
VALUES ${placeholders.join(', ')}
|
|
1267
|
+
`;
|
|
1268
|
+
|
|
1269
|
+
await this.db.query(query, values);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Get recent health checks for a provider
|
|
1274
|
+
*/
|
|
1275
|
+
async getRecentChecks(providerAddress: string, limit: number = 100): Promise<HealthCheck[]> {
|
|
1276
|
+
const result = await this.db.query<HealthCheck>(
|
|
1277
|
+
`SELECT * FROM health_checks
|
|
1278
|
+
WHERE provider_address = $1
|
|
1279
|
+
ORDER BY checked_at DESC
|
|
1280
|
+
LIMIT $2`,
|
|
1281
|
+
[providerAddress, limit]
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
return result.rows;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
### Step 7: Metrics Calculator
|
|
1290
|
+
|
|
1291
|
+
Create `src/metrics/metrics-calculator.ts`:
|
|
1292
|
+
|
|
1293
|
+
```typescript
|
|
1294
|
+
import { getDatabaseClient } from '../database/client.js';
|
|
1295
|
+
import type { ProviderMetrics } from '../types/index.js';
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* Metrics Calculator
|
|
1299
|
+
*
|
|
1300
|
+
* Aggregates health check data into uptime percentages and other statistics.
|
|
1301
|
+
* Runs hourly to keep metrics up to date.
|
|
1302
|
+
*/
|
|
1303
|
+
export class MetricsCalculator {
|
|
1304
|
+
private db: ReturnType<typeof getDatabaseClient>;
|
|
1305
|
+
private isRunning = false;
|
|
1306
|
+
|
|
1307
|
+
constructor() {
|
|
1308
|
+
this.db = getDatabaseClient();
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Start the metrics calculator (runs in background)
|
|
1313
|
+
*/
|
|
1314
|
+
async start(intervalMinutes: number = 60): Promise<void> {
|
|
1315
|
+
console.log('📊 Starting metrics calculator...');
|
|
1316
|
+
console.log(` Interval: ${intervalMinutes} minutes`);
|
|
1317
|
+
|
|
1318
|
+
// Initial run
|
|
1319
|
+
await this.calculateAllMetrics();
|
|
1320
|
+
|
|
1321
|
+
// Schedule periodic calculations
|
|
1322
|
+
setInterval(async () => {
|
|
1323
|
+
if (!this.isRunning) {
|
|
1324
|
+
await this.calculateAllMetrics();
|
|
1325
|
+
}
|
|
1326
|
+
}, intervalMinutes * 60 * 1000);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Calculate metrics for all providers
|
|
1331
|
+
*/
|
|
1332
|
+
private async calculateAllMetrics(): Promise<void> {
|
|
1333
|
+
if (this.isRunning) {
|
|
1334
|
+
console.log('⏩ Metrics calculator already running, skipping...');
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
this.isRunning = true;
|
|
1339
|
+
const startTime = Date.now();
|
|
1340
|
+
|
|
1341
|
+
try {
|
|
1342
|
+
console.log('🔄 Calculating provider metrics...');
|
|
1343
|
+
|
|
1344
|
+
// Get all provider addresses
|
|
1345
|
+
const result = await this.db.query<{ address: string }>(
|
|
1346
|
+
'SELECT address FROM providers'
|
|
1347
|
+
);
|
|
1348
|
+
|
|
1349
|
+
const providers = result.rows;
|
|
1350
|
+
console.log(` Processing ${providers.length} providers...`);
|
|
1351
|
+
|
|
1352
|
+
// Calculate metrics for each provider
|
|
1353
|
+
for (const provider of providers) {
|
|
1354
|
+
await this.calculateProviderMetrics(provider.address);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
1358
|
+
console.log(`✅ Metrics calculation complete in ${duration}s`);
|
|
1359
|
+
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
console.error('❌ Metrics calculator error:', error);
|
|
1362
|
+
throw error;
|
|
1363
|
+
} finally {
|
|
1364
|
+
this.isRunning = false;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Calculate metrics for a single provider
|
|
1370
|
+
*/
|
|
1371
|
+
private async calculateProviderMetrics(providerAddress: string): Promise<void> {
|
|
1372
|
+
// Calculate uptime for different time periods
|
|
1373
|
+
const uptime1d = await this.calculateUptime(providerAddress, 1);
|
|
1374
|
+
const uptime7d = await this.calculateUptime(providerAddress, 7);
|
|
1375
|
+
const uptime30d = await this.calculateUptime(providerAddress, 30);
|
|
1376
|
+
|
|
1377
|
+
// Calculate average response times
|
|
1378
|
+
const avgResponseTime1d = await this.calculateAvgResponseTime(providerAddress, 1);
|
|
1379
|
+
const avgResponseTime7d = await this.calculateAvgResponseTime(providerAddress, 7);
|
|
1380
|
+
const avgResponseTime30d = await this.calculateAvgResponseTime(providerAddress, 30);
|
|
1381
|
+
|
|
1382
|
+
// Get check counts
|
|
1383
|
+
const totalChecks1d = await this.getCheckCount(providerAddress, 1);
|
|
1384
|
+
const totalChecks7d = await this.getCheckCount(providerAddress, 7);
|
|
1385
|
+
const totalChecks30d = await this.getCheckCount(providerAddress, 30);
|
|
1386
|
+
|
|
1387
|
+
// Get current status
|
|
1388
|
+
const currentStatus = await this.getCurrentStatus(providerAddress);
|
|
1389
|
+
|
|
1390
|
+
// Build metrics object
|
|
1391
|
+
const metrics: ProviderMetrics = {
|
|
1392
|
+
providerAddress,
|
|
1393
|
+
uptime1d,
|
|
1394
|
+
uptime7d,
|
|
1395
|
+
uptime30d,
|
|
1396
|
+
isCurrentlyOnline: currentStatus.isCurrentlyOnline,
|
|
1397
|
+
lastOnlineAt: currentStatus.lastOnlineAt,
|
|
1398
|
+
lastOfflineAt: currentStatus.lastOfflineAt,
|
|
1399
|
+
avgResponseTime1d,
|
|
1400
|
+
avgResponseTime7d,
|
|
1401
|
+
avgResponseTime30d,
|
|
1402
|
+
totalChecks1d,
|
|
1403
|
+
totalChecks7d,
|
|
1404
|
+
totalChecks30d,
|
|
1405
|
+
lastCalculatedAt: new Date(),
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
// Upsert metrics into database
|
|
1409
|
+
await this.upsertMetrics(metrics);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* Calculate uptime percentage for a time period
|
|
1414
|
+
*/
|
|
1415
|
+
private async calculateUptime(providerAddress: string, days: number): Promise<number | undefined> {
|
|
1416
|
+
const startDate = new Date();
|
|
1417
|
+
startDate.setDate(startDate.getDate() - days);
|
|
1418
|
+
|
|
1419
|
+
const result = await this.db.query<{ online_count: string; total_count: string }>(
|
|
1420
|
+
`SELECT
|
|
1421
|
+
COUNT(CASE WHEN is_online THEN 1 END) as online_count,
|
|
1422
|
+
COUNT(*) as total_count
|
|
1423
|
+
FROM health_checks
|
|
1424
|
+
WHERE provider_address = $1
|
|
1425
|
+
AND checked_at >= $2`,
|
|
1426
|
+
[providerAddress, startDate]
|
|
1427
|
+
);
|
|
1428
|
+
|
|
1429
|
+
const row = result.rows[0];
|
|
1430
|
+
if (!row || parseInt(row.total_count) === 0) {
|
|
1431
|
+
return undefined; // Not enough data
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
const onlineCount = parseInt(row.online_count);
|
|
1435
|
+
const totalCount = parseInt(row.total_count);
|
|
1436
|
+
|
|
1437
|
+
return onlineCount / totalCount;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/**
|
|
1441
|
+
* Calculate average response time for a time period (in milliseconds)
|
|
1442
|
+
*/
|
|
1443
|
+
private async calculateAvgResponseTime(
|
|
1444
|
+
providerAddress: string,
|
|
1445
|
+
days: number
|
|
1446
|
+
): Promise<number | undefined> {
|
|
1447
|
+
const startDate = new Date();
|
|
1448
|
+
startDate.setDate(startDate.getDate() - days);
|
|
1449
|
+
|
|
1450
|
+
const result = await this.db.query<{ avg_response_time: string }>(
|
|
1451
|
+
`SELECT AVG(response_time_ms)::INTEGER as avg_response_time
|
|
1452
|
+
FROM health_checks
|
|
1453
|
+
WHERE provider_address = $1
|
|
1454
|
+
AND checked_at >= $2
|
|
1455
|
+
AND is_online = TRUE
|
|
1456
|
+
AND response_time_ms IS NOT NULL`,
|
|
1457
|
+
[providerAddress, startDate]
|
|
1458
|
+
);
|
|
1459
|
+
|
|
1460
|
+
const avgTime = result.rows[0]?.avg_response_time;
|
|
1461
|
+
return avgTime ? parseInt(avgTime) : undefined;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/**
|
|
1465
|
+
* Get total number of checks performed in a time period
|
|
1466
|
+
*/
|
|
1467
|
+
private async getCheckCount(providerAddress: string, days: number): Promise<number> {
|
|
1468
|
+
const startDate = new Date();
|
|
1469
|
+
startDate.setDate(startDate.getDate() - days);
|
|
1470
|
+
|
|
1471
|
+
const result = await this.db.query<{ count: string }>(
|
|
1472
|
+
`SELECT COUNT(*) as count
|
|
1473
|
+
FROM health_checks
|
|
1474
|
+
WHERE provider_address = $1
|
|
1475
|
+
AND checked_at >= $2`,
|
|
1476
|
+
[providerAddress, startDate]
|
|
1477
|
+
);
|
|
1478
|
+
|
|
1479
|
+
return parseInt(result.rows[0]?.count || '0');
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Get current online/offline status
|
|
1484
|
+
*/
|
|
1485
|
+
private async getCurrentStatus(
|
|
1486
|
+
providerAddress: string
|
|
1487
|
+
): Promise<{
|
|
1488
|
+
isCurrentlyOnline: boolean;
|
|
1489
|
+
lastOnlineAt?: Date;
|
|
1490
|
+
lastOfflineAt?: Date;
|
|
1491
|
+
}> {
|
|
1492
|
+
// Get most recent check
|
|
1493
|
+
const recentCheck = await this.db.query<{ is_online: boolean; checked_at: Date }>(
|
|
1494
|
+
`SELECT is_online, checked_at
|
|
1495
|
+
FROM health_checks
|
|
1496
|
+
WHERE provider_address = $1
|
|
1497
|
+
ORDER BY checked_at DESC
|
|
1498
|
+
LIMIT 1`,
|
|
1499
|
+
[providerAddress]
|
|
1500
|
+
);
|
|
1501
|
+
|
|
1502
|
+
const isCurrentlyOnline = recentCheck.rows[0]?.is_online || false;
|
|
1503
|
+
|
|
1504
|
+
// Get last time provider was online
|
|
1505
|
+
const lastOnlineResult = await this.db.query<{ checked_at: Date }>(
|
|
1506
|
+
`SELECT checked_at
|
|
1507
|
+
FROM health_checks
|
|
1508
|
+
WHERE provider_address = $1
|
|
1509
|
+
AND is_online = TRUE
|
|
1510
|
+
ORDER BY checked_at DESC
|
|
1511
|
+
LIMIT 1`,
|
|
1512
|
+
[providerAddress]
|
|
1513
|
+
);
|
|
1514
|
+
|
|
1515
|
+
// Get last time provider was offline
|
|
1516
|
+
const lastOfflineResult = await this.db.query<{ checked_at: Date }>(
|
|
1517
|
+
`SELECT checked_at
|
|
1518
|
+
FROM health_checks
|
|
1519
|
+
WHERE provider_address = $1
|
|
1520
|
+
AND is_online = FALSE
|
|
1521
|
+
ORDER BY checked_at DESC
|
|
1522
|
+
LIMIT 1`,
|
|
1523
|
+
[providerAddress]
|
|
1524
|
+
);
|
|
1525
|
+
|
|
1526
|
+
return {
|
|
1527
|
+
isCurrentlyOnline,
|
|
1528
|
+
lastOnlineAt: lastOnlineResult.rows[0]?.checked_at,
|
|
1529
|
+
lastOfflineAt: lastOfflineResult.rows[0]?.checked_at,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Insert or update provider metrics
|
|
1535
|
+
*/
|
|
1536
|
+
private async upsertMetrics(metrics: ProviderMetrics): Promise<void> {
|
|
1537
|
+
await this.db.query(
|
|
1538
|
+
`INSERT INTO provider_metrics (
|
|
1539
|
+
provider_address,
|
|
1540
|
+
uptime_1d, uptime_7d, uptime_30d,
|
|
1541
|
+
is_currently_online, last_online_at, last_offline_at,
|
|
1542
|
+
avg_response_time_1d, avg_response_time_7d, avg_response_time_30d,
|
|
1543
|
+
total_checks_1d, total_checks_7d, total_checks_30d,
|
|
1544
|
+
last_calculated_at
|
|
1545
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
1546
|
+
ON CONFLICT (provider_address) DO UPDATE SET
|
|
1547
|
+
uptime_1d = EXCLUDED.uptime_1d,
|
|
1548
|
+
uptime_7d = EXCLUDED.uptime_7d,
|
|
1549
|
+
uptime_30d = EXCLUDED.uptime_30d,
|
|
1550
|
+
is_currently_online = EXCLUDED.is_currently_online,
|
|
1551
|
+
last_online_at = EXCLUDED.last_online_at,
|
|
1552
|
+
last_offline_at = EXCLUDED.last_offline_at,
|
|
1553
|
+
avg_response_time_1d = EXCLUDED.avg_response_time_1d,
|
|
1554
|
+
avg_response_time_7d = EXCLUDED.avg_response_time_7d,
|
|
1555
|
+
avg_response_time_30d = EXCLUDED.avg_response_time_30d,
|
|
1556
|
+
total_checks_1d = EXCLUDED.total_checks_1d,
|
|
1557
|
+
total_checks_7d = EXCLUDED.total_checks_7d,
|
|
1558
|
+
total_checks_30d = EXCLUDED.total_checks_30d,
|
|
1559
|
+
last_calculated_at = EXCLUDED.last_calculated_at`,
|
|
1560
|
+
[
|
|
1561
|
+
metrics.providerAddress,
|
|
1562
|
+
metrics.uptime1d,
|
|
1563
|
+
metrics.uptime7d,
|
|
1564
|
+
metrics.uptime30d,
|
|
1565
|
+
metrics.isCurrentlyOnline,
|
|
1566
|
+
metrics.lastOnlineAt,
|
|
1567
|
+
metrics.lastOfflineAt,
|
|
1568
|
+
metrics.avgResponseTime1d,
|
|
1569
|
+
metrics.avgResponseTime7d,
|
|
1570
|
+
metrics.avgResponseTime30d,
|
|
1571
|
+
metrics.totalChecks1d,
|
|
1572
|
+
metrics.totalChecks7d,
|
|
1573
|
+
metrics.totalChecks30d,
|
|
1574
|
+
metrics.lastCalculatedAt,
|
|
1575
|
+
]
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* Get metrics for a specific provider
|
|
1581
|
+
*/
|
|
1582
|
+
async getProviderMetrics(providerAddress: string): Promise<ProviderMetrics | null> {
|
|
1583
|
+
const result = await this.db.query<ProviderMetrics>(
|
|
1584
|
+
'SELECT * FROM provider_metrics WHERE provider_address = $1',
|
|
1585
|
+
[providerAddress]
|
|
1586
|
+
);
|
|
1587
|
+
|
|
1588
|
+
return result.rows[0] || null;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
### Step 8: API Server
|
|
1594
|
+
|
|
1595
|
+
Create `src/api/server.ts`:
|
|
1596
|
+
|
|
1597
|
+
```typescript
|
|
1598
|
+
import express from 'express';
|
|
1599
|
+
import type { Request, Response } from 'express';
|
|
1600
|
+
import { getDatabaseClient } from '../database/client.js';
|
|
1601
|
+
import type { ProviderDetails } from '../types/index.js';
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* API Server
|
|
1605
|
+
*
|
|
1606
|
+
* Exposes provider data via REST API endpoints.
|
|
1607
|
+
*/
|
|
1608
|
+
export class ApiServer {
|
|
1609
|
+
private app: express.Application;
|
|
1610
|
+
private db: ReturnType<typeof getDatabaseClient>;
|
|
1611
|
+
private port: number;
|
|
1612
|
+
|
|
1613
|
+
constructor(port: number = 3000) {
|
|
1614
|
+
this.app = express();
|
|
1615
|
+
this.db = getDatabaseClient();
|
|
1616
|
+
this.port = port;
|
|
1617
|
+
|
|
1618
|
+
this.setupMiddleware();
|
|
1619
|
+
this.setupRoutes();
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* Setup Express middleware
|
|
1624
|
+
*/
|
|
1625
|
+
private setupMiddleware(): void {
|
|
1626
|
+
// CORS headers
|
|
1627
|
+
this.app.use((req, res, next) => {
|
|
1628
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
1629
|
+
res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
1630
|
+
res.header('Access-Control-Allow-Headers', 'Content-Type');
|
|
1631
|
+
next();
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
// JSON parsing
|
|
1635
|
+
this.app.use(express.json());
|
|
1636
|
+
|
|
1637
|
+
// Request logging
|
|
1638
|
+
this.app.use((req, res, next) => {
|
|
1639
|
+
const start = Date.now();
|
|
1640
|
+
res.on('finish', () => {
|
|
1641
|
+
const duration = Date.now() - start;
|
|
1642
|
+
console.log(`${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
|
|
1643
|
+
});
|
|
1644
|
+
next();
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Setup API routes
|
|
1650
|
+
*/
|
|
1651
|
+
private setupRoutes(): void {
|
|
1652
|
+
// Health check
|
|
1653
|
+
this.app.get('/health', (req, res) => {
|
|
1654
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
// Get all providers
|
|
1658
|
+
this.app.get('/v1/providers', this.getAllProviders.bind(this));
|
|
1659
|
+
|
|
1660
|
+
// Get single provider by address
|
|
1661
|
+
this.app.get('/v1/providers/:address', this.getProviderByAddress.bind(this));
|
|
1662
|
+
|
|
1663
|
+
// Get provider history
|
|
1664
|
+
this.app.get('/v1/providers/:address/history', this.getProviderHistory.bind(this));
|
|
1665
|
+
|
|
1666
|
+
// Get provider statistics
|
|
1667
|
+
this.app.get('/v1/stats', this.getStats.bind(this));
|
|
1668
|
+
|
|
1669
|
+
// 404 handler
|
|
1670
|
+
this.app.use((req, res) => {
|
|
1671
|
+
res.status(404).json({ error: 'Not found' });
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
// Error handler
|
|
1675
|
+
this.app.use((err: Error, req: Request, res: Response, next: any) => {
|
|
1676
|
+
console.error('API error:', err);
|
|
1677
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* GET /v1/providers
|
|
1683
|
+
* Returns all providers with their metrics
|
|
1684
|
+
*/
|
|
1685
|
+
private async getAllProviders(req: Request, res: Response): Promise<void> {
|
|
1686
|
+
try {
|
|
1687
|
+
const result = await this.db.query<ProviderDetails>(
|
|
1688
|
+
`SELECT * FROM provider_details ORDER BY uptime_7d DESC NULLS LAST`
|
|
1689
|
+
);
|
|
1690
|
+
|
|
1691
|
+
res.json(result.rows);
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
console.error('Error fetching providers:', error);
|
|
1694
|
+
res.status(500).json({ error: 'Failed to fetch providers' });
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* GET /v1/providers/:address
|
|
1700
|
+
* Returns detailed information for a specific provider
|
|
1701
|
+
*/
|
|
1702
|
+
private async getProviderByAddress(req: Request, res: Response): Promise<void> {
|
|
1703
|
+
try {
|
|
1704
|
+
const { address } = req.params;
|
|
1705
|
+
|
|
1706
|
+
const result = await this.db.query<ProviderDetails>(
|
|
1707
|
+
`SELECT * FROM provider_details WHERE address = $1`,
|
|
1708
|
+
[address]
|
|
1709
|
+
);
|
|
1710
|
+
|
|
1711
|
+
if (result.rows.length === 0) {
|
|
1712
|
+
res.status(404).json({ error: 'Provider not found' });
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
res.json(result.rows[0]);
|
|
1717
|
+
} catch (error) {
|
|
1718
|
+
console.error('Error fetching provider:', error);
|
|
1719
|
+
res.status(500).json({ error: 'Failed to fetch provider' });
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/**
|
|
1724
|
+
* GET /v1/providers/:address/history
|
|
1725
|
+
* Returns historical health check data for a provider
|
|
1726
|
+
*/
|
|
1727
|
+
private async getProviderHistory(req: Request, res: Response): Promise<void> {
|
|
1728
|
+
try {
|
|
1729
|
+
const { address } = req.params;
|
|
1730
|
+
const days = parseInt(req.query.days as string) || 7;
|
|
1731
|
+
const limit = Math.min(parseInt(req.query.limit as string) || 1000, 10000);
|
|
1732
|
+
|
|
1733
|
+
const startDate = new Date();
|
|
1734
|
+
startDate.setDate(startDate.getDate() - days);
|
|
1735
|
+
|
|
1736
|
+
const result = await this.db.query(
|
|
1737
|
+
`SELECT
|
|
1738
|
+
checked_at,
|
|
1739
|
+
is_online,
|
|
1740
|
+
response_time_ms,
|
|
1741
|
+
error_message
|
|
1742
|
+
FROM health_checks
|
|
1743
|
+
WHERE provider_address = $1
|
|
1744
|
+
AND checked_at >= $2
|
|
1745
|
+
ORDER BY checked_at DESC
|
|
1746
|
+
LIMIT $3`,
|
|
1747
|
+
[address, startDate, limit]
|
|
1748
|
+
);
|
|
1749
|
+
|
|
1750
|
+
res.json({
|
|
1751
|
+
provider: address,
|
|
1752
|
+
days,
|
|
1753
|
+
dataPoints: result.rows.length,
|
|
1754
|
+
history: result.rows,
|
|
1755
|
+
});
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
console.error('Error fetching provider history:', error);
|
|
1758
|
+
res.status(500).json({ error: 'Failed to fetch provider history' });
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* GET /v1/stats
|
|
1764
|
+
* Returns overall statistics about the tracker
|
|
1765
|
+
*/
|
|
1766
|
+
private async getStats(req: Request, res: Response): Promise<void> {
|
|
1767
|
+
try {
|
|
1768
|
+
// Total providers
|
|
1769
|
+
const totalResult = await this.db.query<{ count: string }>(
|
|
1770
|
+
'SELECT COUNT(*) as count FROM providers'
|
|
1771
|
+
);
|
|
1772
|
+
const totalProviders = parseInt(totalResult.rows[0]?.count || '0');
|
|
1773
|
+
|
|
1774
|
+
// Online providers
|
|
1775
|
+
const onlineResult = await this.db.query<{ count: string }>(
|
|
1776
|
+
'SELECT COUNT(*) as count FROM provider_metrics WHERE is_currently_online = TRUE'
|
|
1777
|
+
);
|
|
1778
|
+
const onlineProviders = parseInt(onlineResult.rows[0]?.count || '0');
|
|
1779
|
+
|
|
1780
|
+
// Average uptime
|
|
1781
|
+
const avgUptimeResult = await this.db.query<{ avg_uptime: string }>(
|
|
1782
|
+
`SELECT AVG(uptime_7d)::DECIMAL(5,4) as avg_uptime
|
|
1783
|
+
FROM provider_metrics
|
|
1784
|
+
WHERE uptime_7d IS NOT NULL`
|
|
1785
|
+
);
|
|
1786
|
+
const avgUptime = parseFloat(avgUptimeResult.rows[0]?.avg_uptime || '0');
|
|
1787
|
+
|
|
1788
|
+
// Total health checks performed
|
|
1789
|
+
const checksResult = await this.db.query<{ count: string }>(
|
|
1790
|
+
'SELECT COUNT(*) as count FROM health_checks'
|
|
1791
|
+
);
|
|
1792
|
+
const totalChecks = parseInt(checksResult.rows[0]?.count || '0');
|
|
1793
|
+
|
|
1794
|
+
res.json({
|
|
1795
|
+
totalProviders,
|
|
1796
|
+
onlineProviders,
|
|
1797
|
+
offlineProviders: totalProviders - onlineProviders,
|
|
1798
|
+
avgUptime7d: avgUptime,
|
|
1799
|
+
totalHealthChecks: totalChecks,
|
|
1800
|
+
timestamp: new Date().toISOString(),
|
|
1801
|
+
});
|
|
1802
|
+
} catch (error) {
|
|
1803
|
+
console.error('Error fetching stats:', error);
|
|
1804
|
+
res.status(500).json({ error: 'Failed to fetch stats' });
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/**
|
|
1809
|
+
* Start the API server
|
|
1810
|
+
*/
|
|
1811
|
+
async start(): Promise<void> {
|
|
1812
|
+
return new Promise((resolve) => {
|
|
1813
|
+
this.app.listen(this.port, () => {
|
|
1814
|
+
console.log(`🚀 API server running on http://localhost:${this.port}`);
|
|
1815
|
+
console.log(` Health: http://localhost:${this.port}/health`);
|
|
1816
|
+
console.log(` Providers: http://localhost:${this.port}/v1/providers`);
|
|
1817
|
+
resolve();
|
|
1818
|
+
});
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
```
|
|
1823
|
+
|
|
1824
|
+
### Step 9: Main Application
|
|
1825
|
+
|
|
1826
|
+
Create `src/index.ts`:
|
|
1827
|
+
|
|
1828
|
+
```typescript
|
|
1829
|
+
import { getDatabaseClient } from './database/client.js';
|
|
1830
|
+
import { BlockchainIndexer } from './indexer/blockchain-indexer.js';
|
|
1831
|
+
import { HealthChecker } from './health/health-checker.js';
|
|
1832
|
+
import { MetricsCalculator } from './metrics/metrics-calculator.js';
|
|
1833
|
+
import { ApiServer } from './api/server.js';
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Configuration from environment variables
|
|
1837
|
+
*/
|
|
1838
|
+
const config = {
|
|
1839
|
+
// Database configuration
|
|
1840
|
+
database: {
|
|
1841
|
+
host: process.env.DB_HOST || 'localhost',
|
|
1842
|
+
port: parseInt(process.env.DB_PORT || '5432'),
|
|
1843
|
+
database: process.env.DB_NAME || 'akash_tracker',
|
|
1844
|
+
user: process.env.DB_USER || 'postgres',
|
|
1845
|
+
password: process.env.DB_PASSWORD || 'postgres',
|
|
1846
|
+
},
|
|
1847
|
+
|
|
1848
|
+
// Akash RPC endpoint
|
|
1849
|
+
rpcEndpoint: process.env.AKASH_RPC || 'https://rpc.akashnet.net:443',
|
|
1850
|
+
|
|
1851
|
+
// Intervals (in minutes)
|
|
1852
|
+
indexerInterval: parseInt(process.env.INDEXER_INTERVAL || '30'),
|
|
1853
|
+
healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL || '10'),
|
|
1854
|
+
metricsInterval: parseInt(process.env.METRICS_INTERVAL || '60'),
|
|
1855
|
+
|
|
1856
|
+
// API server port
|
|
1857
|
+
apiPort: parseInt(process.env.API_PORT || '3000'),
|
|
1858
|
+
|
|
1859
|
+
// Health check timeout
|
|
1860
|
+
checkTimeout: parseInt(process.env.CHECK_TIMEOUT || '5000'),
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
/**
|
|
1864
|
+
* Main application
|
|
1865
|
+
*/
|
|
1866
|
+
async function main() {
|
|
1867
|
+
console.log('🚀 Starting Akash Provider Tracker');
|
|
1868
|
+
console.log('');
|
|
1869
|
+
console.log('Configuration:');
|
|
1870
|
+
console.log(` Database: ${config.database.host}:${config.database.port}/${config.database.database}`);
|
|
1871
|
+
console.log(` RPC: ${config.rpcEndpoint}`);
|
|
1872
|
+
console.log(` Indexer interval: ${config.indexerInterval} minutes`);
|
|
1873
|
+
console.log(` Health check interval: ${config.healthCheckInterval} minutes`);
|
|
1874
|
+
console.log(` Metrics interval: ${config.metricsInterval} minutes`);
|
|
1875
|
+
console.log(` API port: ${config.apiPort}`);
|
|
1876
|
+
console.log('');
|
|
1877
|
+
|
|
1878
|
+
// Initialize database client
|
|
1879
|
+
const db = getDatabaseClient(config.database);
|
|
1880
|
+
|
|
1881
|
+
// Check database connection
|
|
1882
|
+
console.log('📊 Checking database connection...');
|
|
1883
|
+
const isHealthy = await db.healthCheck();
|
|
1884
|
+
if (!isHealthy) {
|
|
1885
|
+
console.error('❌ Database connection failed');
|
|
1886
|
+
process.exit(1);
|
|
1887
|
+
}
|
|
1888
|
+
console.log('✅ Database connection successful');
|
|
1889
|
+
console.log('');
|
|
1890
|
+
|
|
1891
|
+
// Initialize components
|
|
1892
|
+
const indexer = new BlockchainIndexer(config.rpcEndpoint);
|
|
1893
|
+
const healthChecker = new HealthChecker(config.checkTimeout);
|
|
1894
|
+
const metricsCalculator = new MetricsCalculator();
|
|
1895
|
+
const apiServer = new ApiServer(config.apiPort);
|
|
1896
|
+
|
|
1897
|
+
// Start all components
|
|
1898
|
+
await Promise.all([
|
|
1899
|
+
indexer.start(config.indexerInterval),
|
|
1900
|
+
healthChecker.start(config.healthCheckInterval),
|
|
1901
|
+
metricsCalculator.start(config.metricsInterval),
|
|
1902
|
+
apiServer.start(),
|
|
1903
|
+
]);
|
|
1904
|
+
|
|
1905
|
+
console.log('');
|
|
1906
|
+
console.log('✅ All systems operational');
|
|
1907
|
+
console.log('');
|
|
1908
|
+
|
|
1909
|
+
// Handle graceful shutdown
|
|
1910
|
+
process.on('SIGINT', async () => {
|
|
1911
|
+
console.log('');
|
|
1912
|
+
console.log('🛑 Shutting down...');
|
|
1913
|
+
await db.close();
|
|
1914
|
+
process.exit(0);
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// Run the application
|
|
1919
|
+
main().catch((error) => {
|
|
1920
|
+
console.error('❌ Fatal error:', error);
|
|
1921
|
+
process.exit(1);
|
|
1922
|
+
});
|
|
1923
|
+
```
|
|
1924
|
+
|
|
1925
|
+
### Step 10: Environment Configuration
|
|
1926
|
+
|
|
1927
|
+
Create `.env.example`:
|
|
1928
|
+
|
|
1929
|
+
```bash
|
|
1930
|
+
# Database Configuration
|
|
1931
|
+
DB_HOST=localhost
|
|
1932
|
+
DB_PORT=5432
|
|
1933
|
+
DB_NAME=akash_tracker
|
|
1934
|
+
DB_USER=postgres
|
|
1935
|
+
DB_PASSWORD=postgres
|
|
1936
|
+
|
|
1937
|
+
# Akash Configuration
|
|
1938
|
+
AKASH_RPC=https://rpc.akashnet.net:443
|
|
1939
|
+
|
|
1940
|
+
# Intervals (in minutes)
|
|
1941
|
+
INDEXER_INTERVAL=30 # How often to check blockchain for new providers
|
|
1942
|
+
HEALTH_CHECK_INTERVAL=10 # How often to ping providers
|
|
1943
|
+
METRICS_INTERVAL=60 # How often to calculate uptime metrics
|
|
1944
|
+
|
|
1945
|
+
# API Configuration
|
|
1946
|
+
API_PORT=3000
|
|
1947
|
+
|
|
1948
|
+
# Health Check Configuration
|
|
1949
|
+
CHECK_TIMEOUT=5000 # Timeout in milliseconds
|
|
1950
|
+
```
|
|
1951
|
+
|
|
1952
|
+
### Step 11: Package Scripts
|
|
1953
|
+
|
|
1954
|
+
Update `package.json`:
|
|
1955
|
+
|
|
1956
|
+
```json
|
|
1957
|
+
{
|
|
1958
|
+
"name": "akash-provider-tracker",
|
|
1959
|
+
"version": "1.0.0",
|
|
1960
|
+
"type": "module",
|
|
1961
|
+
"scripts": {
|
|
1962
|
+
"build": "tsc",
|
|
1963
|
+
"start": "node dist/index.js",
|
|
1964
|
+
"dev": "tsx watch src/index.ts",
|
|
1965
|
+
"db:setup": "psql -U postgres -f src/database/schema.sql",
|
|
1966
|
+
"db:reset": "dropdb akash_tracker && createdb akash_tracker && npm run db:setup"
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
```
|
|
1970
|
+
|
|
1971
|
+
---
|
|
1972
|
+
|
|
1973
|
+
## Deployment
|
|
1974
|
+
|
|
1975
|
+
### Option 1: Local Development
|
|
1976
|
+
|
|
1977
|
+
```bash
|
|
1978
|
+
# 1. Install PostgreSQL
|
|
1979
|
+
brew install postgresql # macOS
|
|
1980
|
+
# or
|
|
1981
|
+
sudo apt install postgresql # Ubuntu
|
|
1982
|
+
|
|
1983
|
+
# 2. Create database
|
|
1984
|
+
createdb akash_tracker
|
|
1985
|
+
|
|
1986
|
+
# 3. Initialize schema
|
|
1987
|
+
psql -U postgres -d akash_tracker -f src/database/schema.sql
|
|
1988
|
+
|
|
1989
|
+
# 4. Create .env file
|
|
1990
|
+
cp .env.example .env
|
|
1991
|
+
# Edit .env with your configuration
|
|
1992
|
+
|
|
1993
|
+
# 5. Install dependencies
|
|
1994
|
+
npm install
|
|
1995
|
+
|
|
1996
|
+
# 6. Run in development mode
|
|
1997
|
+
npm run dev
|
|
1998
|
+
```
|
|
1999
|
+
|
|
2000
|
+
### Option 2: Docker
|
|
2001
|
+
|
|
2002
|
+
Create `Dockerfile`:
|
|
2003
|
+
|
|
2004
|
+
```dockerfile
|
|
2005
|
+
FROM node:20-alpine
|
|
2006
|
+
|
|
2007
|
+
WORKDIR /app
|
|
2008
|
+
|
|
2009
|
+
# Install dependencies
|
|
2010
|
+
COPY package*.json ./
|
|
2011
|
+
RUN npm ci --production
|
|
2012
|
+
|
|
2013
|
+
# Copy source
|
|
2014
|
+
COPY . .
|
|
2015
|
+
|
|
2016
|
+
# Build TypeScript
|
|
2017
|
+
RUN npm run build
|
|
2018
|
+
|
|
2019
|
+
# Run application
|
|
2020
|
+
CMD ["npm", "start"]
|
|
2021
|
+
```
|
|
2022
|
+
|
|
2023
|
+
Create `docker-compose.yml`:
|
|
2024
|
+
|
|
2025
|
+
```yaml
|
|
2026
|
+
version: '3.8'
|
|
2027
|
+
|
|
2028
|
+
services:
|
|
2029
|
+
postgres:
|
|
2030
|
+
image: postgres:15-alpine
|
|
2031
|
+
environment:
|
|
2032
|
+
POSTGRES_DB: akash_tracker
|
|
2033
|
+
POSTGRES_USER: postgres
|
|
2034
|
+
POSTGRES_PASSWORD: postgres
|
|
2035
|
+
volumes:
|
|
2036
|
+
- postgres_data:/var/lib/postgresql/data
|
|
2037
|
+
- ./src/database/schema.sql:/docker-entrypoint-initdb.d/schema.sql
|
|
2038
|
+
ports:
|
|
2039
|
+
- "5432:5432"
|
|
2040
|
+
|
|
2041
|
+
tracker:
|
|
2042
|
+
build: .
|
|
2043
|
+
depends_on:
|
|
2044
|
+
- postgres
|
|
2045
|
+
environment:
|
|
2046
|
+
DB_HOST: postgres
|
|
2047
|
+
DB_PORT: 5432
|
|
2048
|
+
DB_NAME: akash_tracker
|
|
2049
|
+
DB_USER: postgres
|
|
2050
|
+
DB_PASSWORD: postgres
|
|
2051
|
+
AKASH_RPC: https://rpc.akashnet.net:443
|
|
2052
|
+
INDEXER_INTERVAL: 30
|
|
2053
|
+
HEALTH_CHECK_INTERVAL: 10
|
|
2054
|
+
METRICS_INTERVAL: 60
|
|
2055
|
+
API_PORT: 3000
|
|
2056
|
+
ports:
|
|
2057
|
+
- "3000:3000"
|
|
2058
|
+
|
|
2059
|
+
volumes:
|
|
2060
|
+
postgres_data:
|
|
2061
|
+
```
|
|
2062
|
+
|
|
2063
|
+
Run with Docker:
|
|
2064
|
+
|
|
2065
|
+
```bash
|
|
2066
|
+
# Build and start
|
|
2067
|
+
docker-compose up -d
|
|
2068
|
+
|
|
2069
|
+
# View logs
|
|
2070
|
+
docker-compose logs -f tracker
|
|
2071
|
+
|
|
2072
|
+
# Stop
|
|
2073
|
+
docker-compose down
|
|
2074
|
+
```
|
|
2075
|
+
|
|
2076
|
+
### Option 3: Deploy to VPS
|
|
2077
|
+
|
|
2078
|
+
```bash
|
|
2079
|
+
# 1. SSH to your server
|
|
2080
|
+
ssh user@your-server.com
|
|
2081
|
+
|
|
2082
|
+
# 2. Install Node.js and PostgreSQL
|
|
2083
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
|
2084
|
+
sudo apt install -y nodejs postgresql
|
|
2085
|
+
|
|
2086
|
+
# 3. Clone your repository
|
|
2087
|
+
git clone https://github.com/your-repo/akash-provider-tracker.git
|
|
2088
|
+
cd akash-provider-tracker
|
|
2089
|
+
|
|
2090
|
+
# 4. Setup database
|
|
2091
|
+
sudo -u postgres createdb akash_tracker
|
|
2092
|
+
sudo -u postgres psql -d akash_tracker -f src/database/schema.sql
|
|
2093
|
+
|
|
2094
|
+
# 5. Install dependencies and build
|
|
2095
|
+
npm install
|
|
2096
|
+
npm run build
|
|
2097
|
+
|
|
2098
|
+
# 6. Setup systemd service
|
|
2099
|
+
sudo nano /etc/systemd/system/akash-tracker.service
|
|
2100
|
+
```
|
|
2101
|
+
|
|
2102
|
+
Create systemd service file:
|
|
2103
|
+
|
|
2104
|
+
```ini
|
|
2105
|
+
[Unit]
|
|
2106
|
+
Description=Akash Provider Tracker
|
|
2107
|
+
After=network.target postgresql.service
|
|
2108
|
+
|
|
2109
|
+
[Service]
|
|
2110
|
+
Type=simple
|
|
2111
|
+
User=your-user
|
|
2112
|
+
WorkingDirectory=/home/your-user/akash-provider-tracker
|
|
2113
|
+
Environment="NODE_ENV=production"
|
|
2114
|
+
Environment="DB_HOST=localhost"
|
|
2115
|
+
Environment="DB_PORT=5432"
|
|
2116
|
+
Environment="DB_NAME=akash_tracker"
|
|
2117
|
+
Environment="DB_USER=postgres"
|
|
2118
|
+
Environment="DB_PASSWORD=your-password"
|
|
2119
|
+
ExecStart=/usr/bin/node dist/index.js
|
|
2120
|
+
Restart=always
|
|
2121
|
+
|
|
2122
|
+
[Install]
|
|
2123
|
+
WantedBy=multi-user.target
|
|
2124
|
+
```
|
|
2125
|
+
|
|
2126
|
+
Start the service:
|
|
2127
|
+
|
|
2128
|
+
```bash
|
|
2129
|
+
# Enable and start
|
|
2130
|
+
sudo systemctl enable akash-tracker
|
|
2131
|
+
sudo systemctl start akash-tracker
|
|
2132
|
+
|
|
2133
|
+
# Check status
|
|
2134
|
+
sudo systemctl status akash-tracker
|
|
2135
|
+
|
|
2136
|
+
# View logs
|
|
2137
|
+
sudo journalctl -u akash-tracker -f
|
|
2138
|
+
```
|
|
2139
|
+
|
|
2140
|
+
### Option 4: Deploy to Akash Network
|
|
2141
|
+
|
|
2142
|
+
Create `deploy.yaml` for Akash:
|
|
2143
|
+
|
|
2144
|
+
```yaml
|
|
2145
|
+
---
|
|
2146
|
+
version: "2.0"
|
|
2147
|
+
|
|
2148
|
+
services:
|
|
2149
|
+
tracker:
|
|
2150
|
+
image: your-docker-hub/akash-tracker:latest
|
|
2151
|
+
env:
|
|
2152
|
+
- DB_HOST=postgres
|
|
2153
|
+
- DB_NAME=akash_tracker
|
|
2154
|
+
- DB_USER=postgres
|
|
2155
|
+
- DB_PASSWORD=postgres
|
|
2156
|
+
- AKASH_RPC=https://rpc.akashnet.net:443
|
|
2157
|
+
expose:
|
|
2158
|
+
- port: 3000
|
|
2159
|
+
as: 80
|
|
2160
|
+
to:
|
|
2161
|
+
- global: true
|
|
2162
|
+
|
|
2163
|
+
postgres:
|
|
2164
|
+
image: postgres:15-alpine
|
|
2165
|
+
env:
|
|
2166
|
+
- POSTGRES_DB=akash_tracker
|
|
2167
|
+
- POSTGRES_USER=postgres
|
|
2168
|
+
- POSTGRES_PASSWORD=postgres
|
|
2169
|
+
expose:
|
|
2170
|
+
- port: 5432
|
|
2171
|
+
to:
|
|
2172
|
+
- service: tracker
|
|
2173
|
+
|
|
2174
|
+
profiles:
|
|
2175
|
+
compute:
|
|
2176
|
+
tracker:
|
|
2177
|
+
resources:
|
|
2178
|
+
cpu:
|
|
2179
|
+
units: 1
|
|
2180
|
+
memory:
|
|
2181
|
+
size: 2Gi
|
|
2182
|
+
storage:
|
|
2183
|
+
size: 10Gi
|
|
2184
|
+
postgres:
|
|
2185
|
+
resources:
|
|
2186
|
+
cpu:
|
|
2187
|
+
units: 1
|
|
2188
|
+
memory:
|
|
2189
|
+
size: 2Gi
|
|
2190
|
+
storage:
|
|
2191
|
+
size: 50Gi
|
|
2192
|
+
|
|
2193
|
+
placement:
|
|
2194
|
+
akash:
|
|
2195
|
+
pricing:
|
|
2196
|
+
tracker:
|
|
2197
|
+
denom: uakt
|
|
2198
|
+
amount: 1000
|
|
2199
|
+
postgres:
|
|
2200
|
+
denom: uakt
|
|
2201
|
+
amount: 1000
|
|
2202
|
+
|
|
2203
|
+
deployment:
|
|
2204
|
+
tracker:
|
|
2205
|
+
akash:
|
|
2206
|
+
profile: tracker
|
|
2207
|
+
count: 1
|
|
2208
|
+
postgres:
|
|
2209
|
+
akash:
|
|
2210
|
+
profile: postgres
|
|
2211
|
+
count: 1
|
|
2212
|
+
```
|
|
2213
|
+
|
|
2214
|
+
---
|
|
2215
|
+
|
|
2216
|
+
## Maintenance
|
|
2217
|
+
|
|
2218
|
+
### Daily Tasks
|
|
2219
|
+
|
|
2220
|
+
**Monitor System Health:**
|
|
2221
|
+
|
|
2222
|
+
```bash
|
|
2223
|
+
# Check if all services are running
|
|
2224
|
+
systemctl status akash-tracker # or docker-compose ps
|
|
2225
|
+
|
|
2226
|
+
# Check recent logs for errors
|
|
2227
|
+
tail -n 100 /var/log/akash-tracker.log
|
|
2228
|
+
|
|
2229
|
+
# Check database size
|
|
2230
|
+
psql -U postgres -d akash_tracker -c "
|
|
2231
|
+
SELECT
|
|
2232
|
+
pg_size_pretty(pg_database_size('akash_tracker')) as size;
|
|
2233
|
+
"
|
|
2234
|
+
```
|
|
2235
|
+
|
|
2236
|
+
**Monitor Metrics:**
|
|
2237
|
+
|
|
2238
|
+
```bash
|
|
2239
|
+
# Check API health
|
|
2240
|
+
curl http://localhost:3000/health
|
|
2241
|
+
|
|
2242
|
+
# Get current stats
|
|
2243
|
+
curl http://localhost:3000/v1/stats
|
|
2244
|
+
|
|
2245
|
+
# Check specific provider
|
|
2246
|
+
curl http://localhost:3000/v1/providers/akash1...
|
|
2247
|
+
```
|
|
2248
|
+
|
|
2249
|
+
### Weekly Tasks
|
|
2250
|
+
|
|
2251
|
+
**Database Maintenance:**
|
|
2252
|
+
|
|
2253
|
+
```sql
|
|
2254
|
+
-- Vacuum database (reclaim storage)
|
|
2255
|
+
VACUUM ANALYZE health_checks;
|
|
2256
|
+
VACUUM ANALYZE provider_metrics;
|
|
2257
|
+
|
|
2258
|
+
-- Check table sizes
|
|
2259
|
+
SELECT
|
|
2260
|
+
schemaname,
|
|
2261
|
+
tablename,
|
|
2262
|
+
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
|
|
2263
|
+
FROM pg_tables
|
|
2264
|
+
WHERE schemaname = 'public'
|
|
2265
|
+
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
|
2266
|
+
|
|
2267
|
+
-- Check index usage
|
|
2268
|
+
SELECT
|
|
2269
|
+
schemaname,
|
|
2270
|
+
tablename,
|
|
2271
|
+
indexname,
|
|
2272
|
+
idx_scan,
|
|
2273
|
+
pg_size_pretty(pg_relation_size(indexrelid)) as size
|
|
2274
|
+
FROM pg_stat_user_indexes
|
|
2275
|
+
ORDER BY idx_scan ASC;
|
|
2276
|
+
```
|
|
2277
|
+
|
|
2278
|
+
**Backup Database:**
|
|
2279
|
+
|
|
2280
|
+
```bash
|
|
2281
|
+
# Create backup
|
|
2282
|
+
pg_dump -U postgres akash_tracker > backup_$(date +%Y%m%d).sql
|
|
2283
|
+
|
|
2284
|
+
# Compress backup
|
|
2285
|
+
gzip backup_$(date +%Y%m%d).sql
|
|
2286
|
+
|
|
2287
|
+
# Upload to S3 (optional)
|
|
2288
|
+
aws s3 cp backup_$(date +%Y%m%d).sql.gz s3://your-bucket/backups/
|
|
2289
|
+
```
|
|
2290
|
+
|
|
2291
|
+
### Monthly Tasks
|
|
2292
|
+
|
|
2293
|
+
**Archive Old Data:**
|
|
2294
|
+
|
|
2295
|
+
```sql
|
|
2296
|
+
-- Archive health checks older than 60 days
|
|
2297
|
+
CREATE TABLE health_checks_archive (LIKE health_checks INCLUDING ALL);
|
|
2298
|
+
|
|
2299
|
+
INSERT INTO health_checks_archive
|
|
2300
|
+
SELECT * FROM health_checks
|
|
2301
|
+
WHERE checked_at < NOW() - INTERVAL '60 days';
|
|
2302
|
+
|
|
2303
|
+
DELETE FROM health_checks
|
|
2304
|
+
WHERE checked_at < NOW() - INTERVAL '60 days';
|
|
2305
|
+
```
|
|
2306
|
+
|
|
2307
|
+
**Review Performance:**
|
|
2308
|
+
|
|
2309
|
+
```sql
|
|
2310
|
+
-- Find slow queries
|
|
2311
|
+
SELECT
|
|
2312
|
+
calls,
|
|
2313
|
+
total_time,
|
|
2314
|
+
mean_time,
|
|
2315
|
+
query
|
|
2316
|
+
FROM pg_stat_statements
|
|
2317
|
+
ORDER BY mean_time DESC
|
|
2318
|
+
LIMIT 10;
|
|
2319
|
+
|
|
2320
|
+
-- Check for missing indexes
|
|
2321
|
+
SELECT
|
|
2322
|
+
schemaname,
|
|
2323
|
+
tablename,
|
|
2324
|
+
attname,
|
|
2325
|
+
n_distinct,
|
|
2326
|
+
correlation
|
|
2327
|
+
FROM pg_stats
|
|
2328
|
+
WHERE schemaname = 'public'
|
|
2329
|
+
AND n_distinct > 100
|
|
2330
|
+
ORDER BY abs(correlation) ASC
|
|
2331
|
+
LIMIT 20;
|
|
2332
|
+
```
|
|
2333
|
+
|
|
2334
|
+
### Troubleshooting
|
|
2335
|
+
|
|
2336
|
+
**Problem: High Memory Usage**
|
|
2337
|
+
|
|
2338
|
+
```bash
|
|
2339
|
+
# Check Node.js memory usage
|
|
2340
|
+
ps aux | grep node
|
|
2341
|
+
|
|
2342
|
+
# If memory is high, restart service
|
|
2343
|
+
sudo systemctl restart akash-tracker
|
|
2344
|
+
|
|
2345
|
+
# Increase Node.js memory limit (in systemd service file)
|
|
2346
|
+
Environment="NODE_OPTIONS=--max-old-space-size=4096"
|
|
2347
|
+
```
|
|
2348
|
+
|
|
2349
|
+
**Problem: Database Growing Too Large**
|
|
2350
|
+
|
|
2351
|
+
```bash
|
|
2352
|
+
# Check table sizes
|
|
2353
|
+
psql -U postgres -d akash_tracker -c "
|
|
2354
|
+
SELECT
|
|
2355
|
+
pg_size_pretty(pg_table_size('health_checks')) as health_checks_size,
|
|
2356
|
+
pg_size_pretty(pg_table_size('provider_metrics')) as metrics_size;
|
|
2357
|
+
"
|
|
2358
|
+
|
|
2359
|
+
# Archive old data (see Monthly Tasks)
|
|
2360
|
+
# Or reduce retention period in health checker
|
|
2361
|
+
```
|
|
2362
|
+
|
|
2363
|
+
**Problem: Providers Not Being Discovered**
|
|
2364
|
+
|
|
2365
|
+
```bash
|
|
2366
|
+
# Check indexer logs
|
|
2367
|
+
journalctl -u akash-tracker | grep "Indexer"
|
|
2368
|
+
|
|
2369
|
+
# Manually trigger indexer (if you add a CLI command)
|
|
2370
|
+
node dist/index.js --index-now
|
|
2371
|
+
|
|
2372
|
+
# Check RPC connectivity
|
|
2373
|
+
curl https://rpc.akashnet.net:443/status
|
|
2374
|
+
```
|
|
2375
|
+
|
|
2376
|
+
**Problem: API Slow Response Times**
|
|
2377
|
+
|
|
2378
|
+
```sql
|
|
2379
|
+
-- Add missing indexes
|
|
2380
|
+
CREATE INDEX CONCURRENTLY idx_health_checks_provider_time
|
|
2381
|
+
ON health_checks(provider_address, checked_at DESC);
|
|
2382
|
+
|
|
2383
|
+
-- Analyze query performance
|
|
2384
|
+
EXPLAIN ANALYZE
|
|
2385
|
+
SELECT * FROM provider_details;
|
|
2386
|
+
|
|
2387
|
+
-- Update statistics
|
|
2388
|
+
ANALYZE;
|
|
2389
|
+
```
|
|
2390
|
+
|
|
2391
|
+
---
|
|
2392
|
+
|
|
2393
|
+
## Advanced Features
|
|
2394
|
+
|
|
2395
|
+
### Feature 1: IP Geolocation
|
|
2396
|
+
|
|
2397
|
+
Add IP geolocation to track provider locations:
|
|
2398
|
+
|
|
2399
|
+
```typescript
|
|
2400
|
+
import axios from 'axios';
|
|
2401
|
+
|
|
2402
|
+
/**
|
|
2403
|
+
* Get IP geolocation using ipapi.co (free tier: 1000 requests/day)
|
|
2404
|
+
*/
|
|
2405
|
+
async function getProviderLocation(hostUri: string): Promise<ProviderLocation | null> {
|
|
2406
|
+
try {
|
|
2407
|
+
// Extract hostname from URI
|
|
2408
|
+
const hostname = new URL(hostUri).hostname;
|
|
2409
|
+
|
|
2410
|
+
// Query ipapi.co
|
|
2411
|
+
const response = await axios.get(`https://ipapi.co/${hostname}/json/`);
|
|
2412
|
+
const data = response.data;
|
|
2413
|
+
|
|
2414
|
+
return {
|
|
2415
|
+
providerAddress: '', // Set by caller
|
|
2416
|
+
country: data.country_name,
|
|
2417
|
+
countryCode: data.country_code,
|
|
2418
|
+
region: data.region,
|
|
2419
|
+
regionCode: data.region_code,
|
|
2420
|
+
city: data.city,
|
|
2421
|
+
latitude: data.latitude,
|
|
2422
|
+
longitude: data.longitude,
|
|
2423
|
+
timezone: data.timezone,
|
|
2424
|
+
ipAddress: data.ip,
|
|
2425
|
+
};
|
|
2426
|
+
} catch (error) {
|
|
2427
|
+
console.error('Failed to get geolocation:', error);
|
|
2428
|
+
return null;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// Use in health checker after detecting new provider
|
|
2433
|
+
```
|
|
2434
|
+
|
|
2435
|
+
### Feature 2: Alerting System
|
|
2436
|
+
|
|
2437
|
+
Add Slack/Discord alerts for provider downtime:
|
|
2438
|
+
|
|
2439
|
+
```typescript
|
|
2440
|
+
import axios from 'axios';
|
|
2441
|
+
|
|
2442
|
+
class AlertManager {
|
|
2443
|
+
private webhookUrl: string;
|
|
2444
|
+
private alertedProviders = new Set<string>();
|
|
2445
|
+
|
|
2446
|
+
constructor(webhookUrl: string) {
|
|
2447
|
+
this.webhookUrl = webhookUrl;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
async sendAlert(provider: string, message: string): Promise<void> {
|
|
2451
|
+
// Avoid duplicate alerts
|
|
2452
|
+
if (this.alertedProviders.has(provider)) return;
|
|
2453
|
+
|
|
2454
|
+
try {
|
|
2455
|
+
await axios.post(this.webhookUrl, {
|
|
2456
|
+
text: `🚨 Provider Alert: ${message}`,
|
|
2457
|
+
blocks: [
|
|
2458
|
+
{
|
|
2459
|
+
type: 'section',
|
|
2460
|
+
text: {
|
|
2461
|
+
type: 'mrkdwn',
|
|
2462
|
+
text: `*Provider:* ${provider}\n*Alert:* ${message}`,
|
|
2463
|
+
},
|
|
2464
|
+
},
|
|
2465
|
+
],
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
this.alertedProviders.add(provider);
|
|
2469
|
+
|
|
2470
|
+
// Clear alert after 1 hour
|
|
2471
|
+
setTimeout(() => {
|
|
2472
|
+
this.alertedProviders.delete(provider);
|
|
2473
|
+
}, 60 * 60 * 1000);
|
|
2474
|
+
|
|
2475
|
+
} catch (error) {
|
|
2476
|
+
console.error('Failed to send alert:', error);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
// Use in health checker when provider goes down
|
|
2482
|
+
```
|
|
2483
|
+
|
|
2484
|
+
### Feature 3: Historical Charts
|
|
2485
|
+
|
|
2486
|
+
Generate uptime charts using Chart.js or similar:
|
|
2487
|
+
|
|
2488
|
+
```typescript
|
|
2489
|
+
// API endpoint: GET /v1/providers/:address/chart
|
|
2490
|
+
async getProviderChart(req: Request, res: Response): Promise<void> {
|
|
2491
|
+
const { address } = req.params;
|
|
2492
|
+
const days = parseInt(req.query.days as string) || 7;
|
|
2493
|
+
|
|
2494
|
+
const startDate = new Date();
|
|
2495
|
+
startDate.setDate(startDate.getDate() - days);
|
|
2496
|
+
|
|
2497
|
+
// Get hourly uptime data
|
|
2498
|
+
const result = await this.db.query(`
|
|
2499
|
+
SELECT
|
|
2500
|
+
date_trunc('hour', checked_at) as hour,
|
|
2501
|
+
AVG(CASE WHEN is_online THEN 1.0 ELSE 0.0 END) as uptime
|
|
2502
|
+
FROM health_checks
|
|
2503
|
+
WHERE provider_address = $1
|
|
2504
|
+
AND checked_at >= $2
|
|
2505
|
+
GROUP BY hour
|
|
2506
|
+
ORDER BY hour ASC
|
|
2507
|
+
`, [address, startDate]);
|
|
2508
|
+
|
|
2509
|
+
const data = {
|
|
2510
|
+
labels: result.rows.map(r => r.hour),
|
|
2511
|
+
datasets: [{
|
|
2512
|
+
label: 'Uptime',
|
|
2513
|
+
data: result.rows.map(r => (r.uptime * 100).toFixed(2)),
|
|
2514
|
+
}],
|
|
2515
|
+
};
|
|
2516
|
+
|
|
2517
|
+
res.json(data);
|
|
2518
|
+
}
|
|
2519
|
+
```
|
|
2520
|
+
|
|
2521
|
+
### Feature 4: Provider Comparison
|
|
2522
|
+
|
|
2523
|
+
Compare multiple providers side-by-side:
|
|
2524
|
+
|
|
2525
|
+
```typescript
|
|
2526
|
+
// API endpoint: GET /v1/compare?providers=addr1,addr2,addr3
|
|
2527
|
+
async compareProviders(req: Request, res: Response): Promise<void> {
|
|
2528
|
+
const addresses = (req.query.providers as string).split(',');
|
|
2529
|
+
|
|
2530
|
+
const result = await this.db.query(`
|
|
2531
|
+
SELECT * FROM provider_details
|
|
2532
|
+
WHERE address = ANY($1)
|
|
2533
|
+
`, [addresses]);
|
|
2534
|
+
|
|
2535
|
+
// Calculate comparison metrics
|
|
2536
|
+
const comparison = result.rows.map(provider => ({
|
|
2537
|
+
address: provider.address,
|
|
2538
|
+
name: provider.name,
|
|
2539
|
+
uptime7d: provider.uptime_7d,
|
|
2540
|
+
avgResponseTime: provider.avg_response_time_7d,
|
|
2541
|
+
isOnline: provider.is_currently_online,
|
|
2542
|
+
isAudited: provider.is_audited,
|
|
2543
|
+
location: `${provider.city}, ${provider.country}`,
|
|
2544
|
+
}));
|
|
2545
|
+
|
|
2546
|
+
res.json({
|
|
2547
|
+
providers: comparison,
|
|
2548
|
+
winner: comparison.sort((a, b) => b.uptime7d - a.uptime7d)[0],
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
```
|
|
2552
|
+
|
|
2553
|
+
---
|
|
2554
|
+
|
|
2555
|
+
## Conclusion
|
|
2556
|
+
|
|
2557
|
+
You now have a complete guide to building your own provider reliability tracker for Akash Network. This system will:
|
|
2558
|
+
|
|
2559
|
+
- ✅ Automatically discover providers from the blockchain
|
|
2560
|
+
- ✅ Continuously monitor provider health
|
|
2561
|
+
- ✅ Calculate uptime percentages over time
|
|
2562
|
+
- ✅ Provide a REST API for your applications
|
|
2563
|
+
- ✅ Track provider metadata and performance
|
|
2564
|
+
|
|
2565
|
+
### Next Steps
|
|
2566
|
+
|
|
2567
|
+
1. **Deploy the tracker** to a VPS or Akash itself
|
|
2568
|
+
2. **Integrate with kadi-deploy** to use reliability data when selecting providers
|
|
2569
|
+
3. **Add more features** like alerting, geolocation, and historical charts
|
|
2570
|
+
4. **Monitor and maintain** the system to ensure data quality
|
|
2571
|
+
|
|
2572
|
+
### Resources
|
|
2573
|
+
|
|
2574
|
+
- **Akash Network Docs:** https://docs.akash.network/
|
|
2575
|
+
- **AkashJS Library:** https://github.com/akash-network/akashjs
|
|
2576
|
+
- **PostgreSQL TimescaleDB:** https://www.timescale.com/ (for time-series optimization)
|
|
2577
|
+
- **KADI Infrastructure:** https://github.com/kadi-build
|
|
2578
|
+
|
|
2579
|
+
---
|
|
2580
|
+
|
|
2581
|
+
**Happy tracking! 🚀**
|