24
24
from pyth_observer .crosschain import CrosschainPrice
25
25
from pyth_observer .crosschain import CrosschainPriceObserver as Crosschain
26
26
from pyth_observer .dispatch import Dispatch
27
+ from pyth_observer .metrics import metrics
27
28
from pyth_observer .models import Publisher
28
29
29
30
PYTHTEST_HTTP_ENDPOINT = "https://api.pythtest.pyth.network/"
@@ -71,7 +72,16 @@ def __init__(
71
72
self .crosschain_throttler = Throttler (rate_limit = 1 , period = 1 )
72
73
self .coingecko_mapping = coingecko_mapping
73
74
75
+ metrics .set_observer_info (
76
+ network = config ["network" ]["name" ],
77
+ config = config ,
78
+ )
79
+
80
+ metrics .observer_up = 1
81
+
74
82
async def run (self ):
83
+ # global states
84
+ states = []
75
85
while True :
76
86
try :
77
87
logger .info ("Running checks" )
@@ -81,6 +91,10 @@ async def run(self):
81
91
crosschain_prices = await self .get_crosschain_prices ()
82
92
83
93
health_server .observer_ready = True
94
+ metrics .observer_ready = 1
95
+
96
+ processed_feeds = 0
97
+ active_publishers_by_symbol = {}
84
98
85
99
for product in products :
86
100
# Skip tombstone accounts with blank metadata
@@ -121,80 +135,139 @@ async def run(self):
121
135
if not price_account .aggregate_price_info :
122
136
raise RuntimeError ("Aggregate price info is missing" )
123
137
124
- states .append (
125
- PriceFeedState (
126
- symbol = product .attrs ["symbol" ],
127
- asset_type = product .attrs ["asset_type" ],
128
- schedule = MarketSchedule (product .attrs ["schedule" ]),
129
- public_key = price_account .key ,
130
- status = price_account .aggregate_price_status ,
131
- # this is the solana block slot when price account was fetched
132
- latest_block_slot = latest_block_slot ,
133
- latest_trading_slot = price_account .last_slot ,
134
- price_aggregate = price_account .aggregate_price_info .price ,
135
- confidence_interval_aggregate = price_account .aggregate_price_info .confidence_interval ,
136
- coingecko_price = coingecko_prices .get (
137
- product .attrs ["base" ]
138
- ),
139
- coingecko_update = coingecko_updates .get (
140
- product .attrs ["base" ]
141
- ),
142
- crosschain_price = crosschain_price ,
143
- )
138
+ price_feed_state = PriceFeedState (
139
+ symbol = product .attrs ["symbol" ],
140
+ asset_type = product .attrs ["asset_type" ],
141
+ schedule = MarketSchedule (product .attrs ["schedule" ]),
142
+ public_key = price_account .key ,
143
+ status = price_account .aggregate_price_status ,
144
+ # this is the solana block slot when price account was fetched
145
+ latest_block_slot = latest_block_slot ,
146
+ latest_trading_slot = price_account .last_slot ,
147
+ price_aggregate = price_account .aggregate_price_info .price ,
148
+ confidence_interval_aggregate = price_account .aggregate_price_info .confidence_interval ,
149
+ coingecko_price = coingecko_prices .get (product .attrs ["base" ]),
150
+ coingecko_update = coingecko_updates .get (
151
+ product .attrs ["base" ]
152
+ ),
153
+ crosschain_price = crosschain_price ,
144
154
)
145
155
156
+ states .append (price_feed_state )
157
+ processed_feeds += 1
158
+
159
+ metrics .update_price_feed_metrics (price_feed_state )
160
+
161
+ symbol = product .attrs ["symbol" ]
162
+ if symbol not in active_publishers_by_symbol :
163
+ active_publishers_by_symbol [symbol ] = {
164
+ "count" : 0 ,
165
+ "asset_type" : product .attrs ["asset_type" ],
166
+ }
167
+
146
168
for component in price_account .price_components :
147
169
pub = self .publishers .get (component .publisher_key .key , None )
148
170
publisher_name = (
149
171
(pub .name if pub else "" )
150
172
+ f" ({ component .publisher_key .key } )"
151
173
).strip ()
152
- states .append (
153
- PublisherState (
154
- publisher_name = publisher_name ,
155
- symbol = product .attrs ["symbol" ],
156
- asset_type = product .attrs ["asset_type" ],
157
- schedule = MarketSchedule (product .attrs ["schedule" ]),
158
- public_key = component .publisher_key ,
159
- confidence_interval = component .latest_price_info .confidence_interval ,
160
- confidence_interval_aggregate = price_account .aggregate_price_info .confidence_interval ,
161
- price = component .latest_price_info .price ,
162
- price_aggregate = price_account .aggregate_price_info .price ,
163
- slot = component .latest_price_info .pub_slot ,
164
- aggregate_slot = price_account .last_slot ,
165
- # this is the solana block slot when price account was fetched
166
- latest_block_slot = latest_block_slot ,
167
- status = component .latest_price_info .price_status ,
168
- aggregate_status = price_account .aggregate_price_status ,
169
- )
174
+
175
+ publisher_state = PublisherState (
176
+ publisher_name = publisher_name ,
177
+ symbol = product .attrs ["symbol" ],
178
+ asset_type = product .attrs ["asset_type" ],
179
+ schedule = MarketSchedule (product .attrs ["schedule" ]),
180
+ public_key = component .publisher_key ,
181
+ confidence_interval = component .latest_price_info .confidence_interval ,
182
+ confidence_interval_aggregate = price_account .aggregate_price_info .confidence_interval ,
183
+ price = component .latest_price_info .price ,
184
+ price_aggregate = price_account .aggregate_price_info .price ,
185
+ slot = component .latest_price_info .pub_slot ,
186
+ aggregate_slot = price_account .last_slot ,
187
+ # this is the solana block slot when price account was fetched
188
+ latest_block_slot = latest_block_slot ,
189
+ status = component .latest_price_info .price_status ,
190
+ aggregate_status = price_account .aggregate_price_status ,
170
191
)
171
192
172
- await self .dispatch .run (states )
193
+ states .append (publisher_state )
194
+ active_publishers_by_symbol [symbol ]["count" ] += 1
195
+
196
+ metrics .price_feeds_processed .set (processed_feeds )
197
+
198
+ for symbol , info in active_publishers_by_symbol .items ():
199
+ metrics .publishers_active .labels (
200
+ symbol = symbol , asset_type = info ["asset_type" ]
201
+ ).set (info ["count" ])
202
+
203
+ await self .dispatch .run (states )
204
+
173
205
except Exception as e :
174
206
logger .error (f"Error in run loop: { e } " )
175
207
health_server .observer_ready = False
208
+ metrics .observer_ready = 0
209
+ metrics .loop_errors_total .labels (error_type = type (e ).__name__ ).inc ()
176
210
177
- logger . debug ( "Sleeping..." )
211
+ metrics . observer_ready = 0
178
212
await asyncio .sleep (5 )
179
213
180
214
async def get_pyth_products (self ) -> List [PythProductAccount ]:
181
215
logger .debug ("Fetching Pyth product accounts..." )
182
216
183
- async with self .pyth_throttler :
184
- return await self .pyth_client .refresh_products ()
217
+ try :
218
+ async with self .pyth_throttler :
219
+ with metrics .time_operation (
220
+ metrics .api_request_duration , service = "pyth" , endpoint = "products"
221
+ ):
222
+ result = await self .pyth_client .refresh_products ()
223
+ metrics .api_request_total .labels (
224
+ service = "pyth" , endpoint = "products" , status = "success"
225
+ ).inc ()
226
+ return result
227
+ except Exception :
228
+ metrics .api_request_total .labels (
229
+ service = "pyth" , endpoint = "products" , status = "error"
230
+ ).inc ()
231
+ raise
185
232
186
233
async def get_pyth_prices (
187
234
self , product : PythProductAccount
188
235
) -> Dict [PythPriceType , PythPriceAccount ]:
189
236
logger .debug ("Fetching Pyth price accounts..." )
190
237
191
- async with self .pyth_throttler :
192
- return await product .refresh_prices ()
238
+ try :
239
+ async with self .pyth_throttler :
240
+ with metrics .time_operation (
241
+ metrics .api_request_duration , service = "pyth" , endpoint = "prices"
242
+ ):
243
+ result = await product .refresh_prices ()
244
+ metrics .api_request_total .labels (
245
+ service = "pyth" , endpoint = "prices" , status = "success"
246
+ ).inc ()
247
+ return result
248
+ except Exception :
249
+ metrics .api_request_total .labels (
250
+ service = "pyth" , endpoint = "prices" , status = "error"
251
+ ).inc ()
252
+ raise
193
253
194
254
async def get_coingecko_prices (self ):
195
255
logger .debug ("Fetching CoinGecko prices..." )
196
256
197
- data = await get_coingecko_prices (self .coingecko_mapping )
257
+ try :
258
+ with metrics .time_operation (
259
+ metrics .api_request_duration , service = "coingecko" , endpoint = "prices"
260
+ ):
261
+ data = await get_coingecko_prices (self .coingecko_mapping )
262
+ metrics .api_request_total .labels (
263
+ service = "coingecko" , endpoint = "prices" , status = "success"
264
+ ).inc ()
265
+ except Exception :
266
+ metrics .api_request_total .labels (
267
+ service = "coingecko" , endpoint = "prices" , status = "error"
268
+ ).inc ()
269
+ raise
270
+
198
271
prices : Dict [str , float ] = {}
199
272
updates : Dict [str , int ] = {} # Unix timestamps
200
273
@@ -205,4 +278,17 @@ async def get_coingecko_prices(self):
205
278
return (prices , updates )
206
279
207
280
async def get_crosschain_prices (self ) -> Dict [str , CrosschainPrice ]:
208
- return await self .crosschain .get_crosschain_prices ()
281
+ try :
282
+ with metrics .time_operation (
283
+ metrics .api_request_duration , service = "crosschain" , endpoint = "prices"
284
+ ):
285
+ result = await self .crosschain .get_crosschain_prices ()
286
+ metrics .api_request_total .labels (
287
+ service = "crosschain" , endpoint = "prices" , status = "success"
288
+ ).inc ()
289
+ return result
290
+ except Exception :
291
+ metrics .api_request_total .labels (
292
+ service = "crosschain" , endpoint = "prices" , status = "error"
293
+ ).inc ()
294
+ raise
0 commit comments