Skip to content

Commit afe3bec

Browse files
authored
Frrist/metrics/chain (#8)
Implement contract metric colleciton via gh cron
1 parent c9f80c0 commit afe3bec

16 files changed

Lines changed: 931 additions & 30 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Metrics - Faults (Mainnet)
2+
3+
on:
4+
schedule:
5+
- cron: '0 */12 * * *' # Every 12 hours (at minute 0)
6+
workflow_dispatch: # Allow manual triggers
7+
8+
jobs:
9+
collect-metrics:
10+
runs-on: ubuntu-latest
11+
environment: mainnet
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-go@v5
16+
with:
17+
go-version-file: 'go.mod'
18+
19+
- name: Build forgectl
20+
run: make build
21+
22+
- name: Collect fault metrics
23+
env:
24+
FORGECTL_RPC_URL: ${{ vars.FORGECTL_RPC_URL }}
25+
FORGECTL_SERVICE_CONTRACT_ADDRESS: ${{ vars.FORGECTL_SERVICE_CONTRACT_ADDRESS }}
26+
FORGECTL_SERVICE_REGISTRY_CONTRACT_ADDRESS: ${{ vars.FORGECTL_SERVICE_REGISTRY_CONTRACT_ADDRESS }}
27+
FORGECTL_VERIFIER_CONTRACT_ADDRESS: ${{ vars.FORGECTL_VERIFIER_CONTRACT_ADDRESS }}
28+
FORGECTL_PAYMENTS_CONTRACT_ADDRESS: ${{ vars.FORGECTL_PAYMENTS_CONTRACT_ADDRESS }}
29+
FORGECTL_TOKEN_CONTRACT_ADDRESS: ${{ vars.FORGECTL_TOKEN_CONTRACT_ADDRESS }}
30+
FORGECTL_SESSION_KEY_REGISTRY_CONTRACT_ADDRESS: ${{ vars.FORGECTL_SESSION_KEY_REGISTRY_CONTRACT_ADDRESS }}
31+
run: |
32+
./forgectl metrics faults \
33+
--payer ${{ secrets.PAYER_ADDRESS }} \
34+
--otlp-endpoint ${{ secrets.OTLP_ENDPOINT }}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Metrics - Payments (Mainnet)
2+
3+
on:
4+
schedule:
5+
- cron: '*/30 * * * *' # Every 30 minutes
6+
workflow_dispatch: # Allow manual triggers
7+
8+
jobs:
9+
collect-metrics:
10+
runs-on: ubuntu-latest
11+
environment: mainnet
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-go@v5
16+
with:
17+
go-version-file: 'go.mod'
18+
19+
- name: Build forgectl
20+
run: make build
21+
22+
- name: Collect payment metrics
23+
env:
24+
FORGECTL_RPC_URL: ${{ vars.FORGECTL_RPC_URL }}
25+
FORGECTL_SERVICE_CONTRACT_ADDRESS: ${{ vars.FORGECTL_SERVICE_CONTRACT_ADDRESS }}
26+
FORGECTL_SERVICE_REGISTRY_CONTRACT_ADDRESS: ${{ vars.FORGECTL_SERVICE_REGISTRY_CONTRACT_ADDRESS }}
27+
FORGECTL_VERIFIER_CONTRACT_ADDRESS: ${{ vars.FORGECTL_VERIFIER_CONTRACT_ADDRESS }}
28+
FORGECTL_PAYMENTS_CONTRACT_ADDRESS: ${{ vars.FORGECTL_PAYMENTS_CONTRACT_ADDRESS }}
29+
FORGECTL_TOKEN_CONTRACT_ADDRESS: ${{ vars.FORGECTL_TOKEN_CONTRACT_ADDRESS }}
30+
FORGECTL_SESSION_KEY_REGISTRY_CONTRACT_ADDRESS: ${{ vars.FORGECTL_SESSION_KEY_REGISTRY_CONTRACT_ADDRESS }}
31+
run: |
32+
./forgectl metrics payments \
33+
--payer ${{ secrets.PAYER_ADDRESS }} \
34+
--otlp-endpoint ${{ secrets.OTLP_ENDPOINT }}

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ keystore/
3838
# Config file
3939
config.yaml
4040
*.yaml
41+
42+
# AI
43+
CLAUDE.md

‎README.md‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,59 @@ make install
1414

1515
1. Create your network config file using `config.template.yaml` as a base.
1616
2. Use CLI command passing the config: `forgectl --config ./config.yaml ...`
17+
18+
## Metrics
19+
20+
Export payment and fault metrics via OTLP for monitoring.
21+
22+
### Payment Metrics
23+
24+
Collect payment channel metrics (funds, lockup, runway):
25+
26+
```sh
27+
forgectl metrics payments \
28+
--config ./config.yaml \
29+
--payer 0xYourPayerAddress \
30+
--otlp-endpoint otel-collector.example.com:4318
31+
```
32+
33+
**Metrics exported:**
34+
- `forgectl_payer_funds` - Total deposited funds (USDFC)
35+
- `forgectl_payer_lockup_current` - Current lockup amount (USDFC)
36+
- `forgectl_payer_runway_seconds` - Estimated time until funds depleted
37+
38+
### Fault Metrics
39+
40+
Collect proof fault metrics (missed proving periods):
41+
42+
```sh
43+
forgectl metrics faults \
44+
--config ./config.yaml \
45+
--payer 0xYourPayerAddress \
46+
--otlp-endpoint otel-collector.example.com:4318
47+
```
48+
49+
**Metrics exported:**
50+
- `forgectl_rail_missed_periods` - Count of missed proving periods per dataset
51+
52+
### Flags
53+
54+
| Flag | Description |
55+
|------|-------------|
56+
| `--payer` | Payer address to monitor (required) |
57+
| `--otlp-endpoint` | OTLP HTTP endpoint (required) |
58+
| `--otlp-insecure` | Use HTTP instead of HTTPS |
59+
60+
### Cron Usage
61+
62+
Both commands collect metrics once and exit, making them suitable for cron jobs:
63+
64+
```sh
65+
# Payment metrics every 30 minutes
66+
*/30 * * * * /path/to/forgectl metrics payments --config /path/to/config.yaml --payer 0x... --otlp-endpoint ...
67+
68+
# Fault metrics every 12 hours
69+
0 */12 * * * /path/to/forgectl metrics faults --config /path/to/config.yaml --payer 0x... --otlp-endpoint ...
70+
```
71+
72+
GitHub Actions workflows are also provided in `.github/workflows/` for automated collection.

‎cli/cmd/metrics/faults.go‎

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package metrics
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"math/big"
7+
"time"
8+
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/spf13/cobra"
11+
"go.opentelemetry.io/otel/attribute"
12+
"go.opentelemetry.io/otel/metric"
13+
14+
"github.com/storacha/forgectl/cli/config"
15+
"github.com/storacha/forgectl/pkg/services/inspector"
16+
"github.com/storacha/forgectl/pkg/telemetry"
17+
"github.com/storacha/forgectl/pkg/telemetry/metrics"
18+
)
19+
20+
var (
21+
faultsPayerFlag string
22+
faultsOtlpEndpoint string
23+
faultsOtlpInsecure bool
24+
)
25+
26+
var faultsCmd = &cobra.Command{
27+
Use: "faults",
28+
Short: "Export proof fault metrics via OTLP",
29+
Long: `Export proof fault metrics via OTLP to a collector.
30+
31+
Metrics exported:
32+
- forgectl_rail_missed_periods: Count of proving periods where proof was missed (per rail/dataset)
33+
34+
This command collects metrics once and exits, making it suitable for cron jobs.`,
35+
RunE: runFaultsMetrics,
36+
}
37+
38+
func init() {
39+
faultsCmd.Flags().StringVar(&faultsPayerFlag, "payer", "", "Payer address to monitor (required)")
40+
cobra.CheckErr(faultsCmd.MarkFlagRequired("payer"))
41+
42+
faultsCmd.Flags().StringVar(&faultsOtlpEndpoint, "otlp-endpoint", "", "OTLP HTTP endpoint (required, e.g., localhost:4318)")
43+
cobra.CheckErr(faultsCmd.MarkFlagRequired("otlp-endpoint"))
44+
45+
faultsCmd.Flags().BoolVar(&faultsOtlpInsecure, "otlp-insecure", false, "Use insecure connection for OTLP")
46+
}
47+
48+
type faultMetrics struct {
49+
railMissedPeriods *telemetry.Float64Gauge
50+
}
51+
52+
func runFaultsMetrics(cmd *cobra.Command, args []string) error {
53+
if !common.IsHexAddress(faultsPayerFlag) {
54+
return fmt.Errorf("invalid payer address: %s", faultsPayerFlag)
55+
}
56+
payerAddr := common.HexToAddress(faultsPayerFlag)
57+
58+
ctx, cancel := context.WithTimeout(cmd.Context(), 60*time.Second)
59+
defer cancel()
60+
61+
cfg, err := config.LoadReadOnly()
62+
if err != nil {
63+
return fmt.Errorf("failed to load config: %w", err)
64+
}
65+
66+
// Initialize telemetry with a short publish interval for one-shot collection
67+
tel, err := telemetry.New(
68+
ctx,
69+
"production",
70+
"forgectl",
71+
"0.0.1",
72+
payerAddr.Hex(),
73+
metrics.Config{
74+
Collectors: []metrics.CollectorConfig{
75+
{
76+
Endpoint: faultsOtlpEndpoint,
77+
Insecure: faultsOtlpInsecure,
78+
PublishInterval: 5 * time.Second,
79+
},
80+
},
81+
},
82+
)
83+
if err != nil {
84+
return fmt.Errorf("failed to initialize telemetry: %w", err)
85+
}
86+
defer func() {
87+
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
88+
defer shutdownCancel()
89+
if err := tel.Shutdown(shutdownCtx); err != nil {
90+
log.Errorw("failed to shutdown telemetry", "error", err)
91+
}
92+
}()
93+
94+
// Create metrics
95+
meter := tel.Metrics.Meter("forgectl")
96+
fm, err := newFaultMetrics(meter)
97+
if err != nil {
98+
return fmt.Errorf("failed to create metrics: %w", err)
99+
}
100+
101+
// Initialize inspector
102+
inspctr, err := inspector.New(inspector.Config{
103+
ClientEndpoint: cfg.RPCUrl,
104+
PaymentsContractAddress: cfg.PaymentsAddr(),
105+
ServiceContractAddress: cfg.ServiceAddr(),
106+
ProviderRegistryAddress: cfg.ServiceRegistryAddr(),
107+
SessionKeyRegistryAddress: cfg.SessionKeyRegistryAddr(),
108+
TokenAddress: cfg.TokenAddr(),
109+
PDPVerifierAddress: cfg.VerifierAddr(),
110+
})
111+
if err != nil {
112+
return fmt.Errorf("failed to create inspector: %w", err)
113+
}
114+
115+
tokenAddr := cfg.TokenAddr()
116+
117+
log.Infow("collecting fault metrics",
118+
"payer", payerAddr.Hex(),
119+
"endpoint", faultsOtlpEndpoint,
120+
)
121+
122+
// Collect and record metrics
123+
if err := collectFaultMetrics(ctx, inspctr, fm, tokenAddr, payerAddr); err != nil {
124+
return fmt.Errorf("failed to collect fault metrics: %w", err)
125+
}
126+
127+
log.Info("fault metrics collected successfully")
128+
return nil
129+
}
130+
131+
func newFaultMetrics(meter metric.Meter) (*faultMetrics, error) {
132+
railMissedPeriods, err := telemetry.NewFloat64Gauge(meter, "forgectl_rail_missed_periods", "Count of proving periods where proof was missed", "periods")
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
return &faultMetrics{
138+
railMissedPeriods: railMissedPeriods,
139+
}, nil
140+
}
141+
142+
func collectFaultMetrics(
143+
ctx context.Context,
144+
inspctr *inspector.Service,
145+
fm *faultMetrics,
146+
tokenAddr, payerAddr common.Address,
147+
) error {
148+
fetchCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
149+
defer cancel()
150+
151+
status, err := inspctr.PaymentsStatus(fetchCtx, tokenAddr, payerAddr, 0, 100)
152+
if err != nil {
153+
return fmt.Errorf("failed to fetch payment status: %w", err)
154+
}
155+
156+
// Record per-rail missed proving periods
157+
maxProvingPeriod := status.MaxProvingPeriod
158+
for _, payee := range status.Payees {
159+
for _, rail := range payee.Rails {
160+
if rail.LifetimeTotalEpochs != nil && rail.LifetimeProvenEpochs != nil && maxProvingPeriod > 0 && rail.DataSetId != nil {
161+
missedEpochs := new(big.Int).Sub(rail.LifetimeTotalEpochs, rail.LifetimeProvenEpochs)
162+
// Floor division: definite count of fully missed proving periods
163+
missedPeriods := missedEpochs.Uint64() / maxProvingPeriod
164+
165+
railAttrs := []attribute.KeyValue{
166+
attribute.String("payer", payerAddr.Hex()),
167+
attribute.String("payee", payee.Address.Hex()),
168+
attribute.String("dataset_id", rail.DataSetId.String()),
169+
}
170+
fm.railMissedPeriods.Record(ctx, float64(missedPeriods), railAttrs...)
171+
172+
log.Infow("recorded rail fault metrics",
173+
"payee", payee.Address.Hex(),
174+
"dataset_id", rail.DataSetId.String(),
175+
"missed_periods", missedPeriods,
176+
)
177+
}
178+
}
179+
}
180+
181+
return nil
182+
}

0 commit comments

Comments
 (0)