Skip to content

Commit 74b4a9f

Browse files
authored
format log events in json (#63)
* format log events in json * ensure the fields are included in the log record as json * remove unused import, bump poetry * linting * fixes
1 parent 0186c64 commit 74b4a9f

File tree

4 files changed

+86
-104
lines changed

4 files changed

+86
-104
lines changed

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ ignore_missing_imports = true
44

55
[tool.poetry]
66
name = "pyth-observer"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
description = "Alerts and stuff"
99
authors = []
1010
readme = "README.md"

pyth_observer/check/price_feed.py

+39-46
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import time
22
from dataclasses import dataclass
33
from datetime import datetime
4-
from textwrap import dedent
54
from typing import Dict, Optional, Protocol, runtime_checkable
65
from zoneinfo import ZoneInfo
76

@@ -42,7 +41,7 @@ def state(self) -> PriceFeedState:
4241
def run(self) -> bool:
4342
...
4443

45-
def error_message(self) -> str:
44+
def error_message(self) -> dict:
4645
...
4746

4847

@@ -80,17 +79,15 @@ def run(self) -> bool:
8079
# Fail
8180
return False
8281

83-
def error_message(self) -> str:
82+
def error_message(self) -> dict:
8483
distance = self.__state.latest_block_slot - self.__state.latest_trading_slot
85-
return dedent(
86-
f"""
87-
{self.__state.symbol} is offline (either non-trading/stale).
88-
It is not updated for {distance} slots.
89-
90-
Latest trading slot: {self.__state.latest_trading_slot}
91-
Block slot: {self.__state.latest_block_slot}
92-
"""
93-
).strip()
84+
return {
85+
"msg": f"{self.__state.symbol} is offline (either non-trading/stale). Last update {distance} slots ago.",
86+
"type": "PriceFeedCheck",
87+
"symbol": self.__state.symbol,
88+
"latest_trading_slot": self.__state.latest_trading_slot,
89+
"block_slot": self.__state.latest_block_slot,
90+
}
9491

9592

9693
class PriceFeedCoinGeckoCheck(PriceFeedCheck):
@@ -127,15 +124,14 @@ def run(self) -> bool:
127124
# Fail
128125
return False
129126

130-
def error_message(self) -> str:
131-
return dedent(
132-
f"""
133-
{self.__state.symbol} is too far from Coingecko's price.
134-
135-
Pyth price: {self.__state.price_aggregate}
136-
Coingecko price: {self.__state.coingecko_price}
137-
"""
138-
).strip()
127+
def error_message(self) -> dict:
128+
return {
129+
"msg": f"{self.__state.symbol} is too far from Coingecko's price.",
130+
"type": "PriceFeedCheck",
131+
"symbol": self.__state.symbol,
132+
"pyth_price": self.__state.price_aggregate,
133+
"coingecko_price": self.__state.coingecko_price,
134+
}
139135

140136

141137
class PriceFeedConfidenceIntervalCheck(PriceFeedCheck):
@@ -158,14 +154,13 @@ def run(self) -> bool:
158154
# Fail
159155
return False
160156

161-
def error_message(self) -> str:
162-
return dedent(
163-
f"""
164-
{self.__state.symbol} confidence interval is too low.
165-
166-
Confidence interval: {self.__state.confidence_interval_aggregate}
167-
"""
168-
).strip()
157+
def error_message(self) -> dict:
158+
return {
159+
"msg": f"{self.__state.symbol} confidence interval is too low.",
160+
"type": "PriceFeedCheck",
161+
"symbol": self.__state.symbol,
162+
"confidence_interval": self.__state.confidence_interval_aggregate,
163+
}
169164

170165

171166
class PriceFeedCrossChainOnlineCheck(PriceFeedCheck):
@@ -210,19 +205,18 @@ def run(self) -> bool:
210205
# Fail
211206
return False
212207

213-
def error_message(self) -> str:
208+
def error_message(self) -> dict:
214209
if self.__state.crosschain_price:
215210
publish_time = arrow.get(self.__state.crosschain_price["publish_time"])
216211
else:
217212
publish_time = arrow.get(0)
218213

219-
return dedent(
220-
f"""
221-
{self.__state.symbol} isn't online at the price service.
222-
223-
Last publish time: {publish_time.format('YYYY-MM-DD HH:mm:ss ZZ')}
224-
"""
225-
).strip()
214+
return {
215+
"msg": f"{self.__state.symbol} isn't online at the price service.",
216+
"type": "PriceFeedCheck",
217+
"symbol": self.__state.symbol,
218+
"last_publish_time": publish_time.format("YYYY-MM-DD HH:mm:ss ZZ"),
219+
}
226220

227221

228222
class PriceFeedCrossChainDeviationCheck(PriceFeedCheck):
@@ -270,21 +264,20 @@ def run(self) -> bool:
270264
# Fail
271265
return False
272266

273-
def error_message(self) -> str:
267+
def error_message(self) -> dict:
274268
# It can never happen because of the check logic but linter could not understand it.
275269
price = (
276270
self.__state.crosschain_price["price"]
277271
if self.__state.crosschain_price
278272
else None
279273
)
280-
return dedent(
281-
f"""
282-
{self.__state.symbol} is too far at the price service.
283-
284-
Price: {self.__state.price_aggregate}
285-
Price at price service: {price}
286-
"""
287-
).strip()
274+
return {
275+
"msg": f"{self.__state.symbol} is too far at the price service.",
276+
"type": "PriceFeedCheck",
277+
"symbol": self.__state.symbol,
278+
"price": self.__state.price_aggregate,
279+
"price_at_price_service": price,
280+
}
288281

289282

290283
PRICE_FEED_CHECKS = [

pyth_observer/check/publisher.py

+38-46
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from dataclasses import dataclass
2-
from textwrap import dedent
32
from typing import Dict, Protocol, runtime_checkable
43

54
from pythclient.pythaccounts import PythPriceStatus
@@ -38,7 +37,7 @@ def state(self) -> PublisherState:
3837
def run(self) -> bool:
3938
...
4039

41-
def error_message(self) -> str:
40+
def error_message(self) -> dict:
4241
...
4342

4443

@@ -78,20 +77,17 @@ def run(self) -> bool:
7877
# Fail
7978
return False
8079

81-
def error_message(self) -> str:
80+
def error_message(self) -> dict:
8281
diff = self.__state.price - self.__state.price_aggregate
8382
intervals_away = abs(diff / self.__state.confidence_interval_aggregate)
84-
85-
return dedent(
86-
f"""
87-
{self.__state.publisher_name} price not within aggregate confidence.
88-
It is {intervals_away} times away from confidence.
89-
90-
Symbol: {self.__state.symbol}
91-
Publisher price: {self.__state.price} ± {self.__state.confidence_interval}
92-
Aggregate price: {self.__state.price_aggregate} ± {self.__state.confidence_interval_aggregate}
93-
"""
94-
).strip()
83+
return {
84+
"msg": f"{self.__state.publisher_name} price is {intervals_away} times away from confidence.",
85+
"type": "PublisherCheck",
86+
"publisher": self.__state.publisher_name,
87+
"symbol": self.__state.symbol,
88+
"publisher_price": f"{self.__state.price} ± {self.__state.confidence_interval}",
89+
"aggregate_price": f"{self.__state.price_aggregate} ± {self.__state.confidence_interval_aggregate}",
90+
}
9591

9692

9793
class PublisherConfidenceIntervalCheck(PublisherCheck):
@@ -119,16 +115,15 @@ def run(self) -> bool:
119115
# Fail
120116
return False
121117

122-
def error_message(self) -> str:
123-
return dedent(
124-
f"""
125-
{self.__state.publisher_name} confidence interval is too tight.
126-
127-
Symbol: {self.__state.symbol}
128-
Price: {self.__state.price}
129-
Confidence interval: {self.__state.confidence_interval}
130-
"""
131-
).strip()
118+
def error_message(self) -> dict:
119+
return {
120+
"msg": f"{self.__state.publisher_name} confidence interval is too tight.",
121+
"type": "PublisherCheck",
122+
"publisher": self.__state.publisher_name,
123+
"symbol": self.__state.symbol,
124+
"price": self.__state.price,
125+
"confidence_interval": self.__state.confidence_interval,
126+
}
132127

133128

134129
class PublisherOfflineCheck(PublisherCheck):
@@ -154,17 +149,16 @@ def run(self) -> bool:
154149
# Fail
155150
return False
156151

157-
def error_message(self) -> str:
152+
def error_message(self) -> dict:
158153
distance = self.__state.latest_block_slot - self.__state.slot
159-
return dedent(
160-
f"""
161-
{self.__state.publisher_name} hasn't published recently for {distance} slots.
162-
163-
Symbol: {self.__state.symbol}
164-
Publisher slot: {self.__state.slot}
165-
Aggregate slot: {self.__state.aggregate_slot}
166-
"""
167-
).strip()
154+
return {
155+
"msg": f"{self.__state.publisher_name} hasn't published recently for {distance} slots.",
156+
"type": "PublisherCheck",
157+
"publisher": self.__state.publisher_name,
158+
"symbol": self.__state.symbol,
159+
"publisher_slot": self.__state.slot,
160+
"aggregate_slot": self.__state.aggregate_slot,
161+
}
168162

169163

170164
class PublisherPriceCheck(PublisherCheck):
@@ -203,19 +197,17 @@ def run(self) -> bool:
203197
# Fail
204198
return False
205199

206-
def error_message(self) -> str:
200+
def error_message(self) -> dict:
207201
deviation = (self.ci_adjusted_price_diff() / self.__state.price_aggregate) * 100
208-
209-
return dedent(
210-
f"""
211-
{self.__state.publisher_name} price is too far from aggregate price.
212-
213-
Symbol: {self.__state.symbol}
214-
Publisher price: {self.__state.price} ± {self.__state.confidence_interval}
215-
Aggregate price: {self.__state.price_aggregate} ± {self.__state.confidence_interval_aggregate}
216-
Deviation: {deviation}%
217-
"""
218-
).strip()
202+
return {
203+
"msg": f"{self.__state.publisher_name} price is too far from aggregate price.",
204+
"type": "PublisherCheck",
205+
"publisher": self.__state.publisher_name,
206+
"symbol": self.__state.symbol,
207+
"publisher_price": f"{self.__state.price} ± {self.__state.confidence_interval}",
208+
"aggregate_price": f"{self.__state.price_aggregate} ± {self.__state.confidence_interval_aggregate}",
209+
"deviation": deviation,
210+
}
219211

220212
# Returns the distance between the aggregate price and the closest side of the publisher's confidence interval
221213
# Returns 0 if the aggregate price is within the publisher's confidence interval.

pyth_observer/event.py

+8-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import json
12
import os
2-
from typing import Dict, Literal, Protocol, TypedDict, cast
3+
from typing import Dict, Protocol, TypedDict, cast
34

45
import aiohttp
56
from datadog_api_client.api_client import AsyncApiClient as DatadogAPI
@@ -38,7 +39,7 @@ def __init__(self, check: Check, context: Context):
3839
async def send(self):
3940
# Publisher checks expect the key -> name mapping of publishers when
4041
# generating the error title/message.
41-
text = self.check.error_message()
42+
event = self.check.error_message()
4243

4344
# An example is: PriceFeedOfflineCheck-Crypto.AAVE/USD
4445
aggregation_key = f"{self.check.__class__.__name__}-{self.check.state().symbol}"
@@ -50,8 +51,8 @@ async def send(self):
5051

5152
event = EventCreateRequest(
5253
aggregation_key=aggregation_key,
53-
title=text.split("\n")[0],
54-
text=text,
54+
title=event["msg"],
55+
text=json.dumps(event),
5556
tags=[
5657
"service:observer",
5758
f"network:{self.context['network']}",
@@ -84,9 +85,6 @@ async def send(self):
8485
)
8586

8687

87-
LogEventLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR"]
88-
89-
9088
class LogEvent(Event):
9189
def __init__(self, check: Check, context: Context):
9290
self.check = check
@@ -95,10 +93,9 @@ def __init__(self, check: Check, context: Context):
9593
async def send(self):
9694
# Publisher checks expect the key -> name mapping of publishers when
9795
# generating the error title/message.
98-
text = self.check.error_message()
99-
100-
level = cast(LogEventLevel, os.environ.get("LOG_EVENT_LEVEL", "INFO"))
101-
logger.log(level, text.replace("\n", ". "))
96+
event = self.check.error_message()
97+
with logger.contextualize(**event):
98+
logger.info(event["msg"])
10299

103100

104101
class TelegramEvent(Event):

0 commit comments

Comments
 (0)