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