Skip to content

Commit e00ce36

Browse files
committed
feat: make default / rich messages nice
Signed-off-by: GRBurst <[email protected]>
1 parent a8801fc commit e00ce36

File tree

2 files changed

+66
-36
lines changed

2 files changed

+66
-36
lines changed

slack_logger/__init__.py

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from abc import ABC, abstractmethod
55
from enum import Enum
66
from logging import LogRecord
7-
from typing import Any, Dict, Optional, Sequence, Union
7+
from typing import Any, Dict, List, Optional, Sequence, Union
88

99
from attrs import define
1010
from slack_sdk.models.attachments import Attachment
11-
from slack_sdk.models.blocks import Block, DividerBlock, HeaderBlock, SectionBlock
11+
from slack_sdk.models.blocks import Block, ContextBlock, DividerBlock, HeaderBlock, SectionBlock
1212
from slack_sdk.models.blocks.basic_components import MarkdownTextObject, PlainTextObject
1313
from slack_sdk.webhook.async_client import AsyncWebhookClient
1414
from slack_sdk.webhook.webhook_response import WebhookResponse
@@ -33,6 +33,7 @@
3333
class Configuration:
3434
service: Optional[str] = None
3535
environment: Optional[str] = None
36+
context: List[str] = []
3637
emojis: Dict[int, str] = DEFAULT_EMOJIS
3738
extra_fields: Dict[str, str] = {}
3839

@@ -43,6 +44,51 @@ class MessageDesign(ABC):
4344
def format_blocks(self, record: LogRecord) -> Sequence[Optional[Block]]:
4445
pass
4546

47+
def get_env(self, config: Configuration, record: LogRecord) -> Optional[str]:
48+
dynamic_env: Optional[str] = getattr(record, "environment", None)
49+
if dynamic_env is not None:
50+
return dynamic_env
51+
if config.environment is not None:
52+
return config.environment
53+
return None
54+
55+
def get_service(self, config: Configuration, record: LogRecord) -> Optional[str]:
56+
dynamic_service: Optional[str] = getattr(record, "service", None)
57+
if dynamic_service is not None:
58+
return dynamic_service
59+
if config.service is not None:
60+
return config.service
61+
return None
62+
63+
def construct_header(
64+
self, record: LogRecord, config: Configuration, icon: Optional[str], level: str
65+
) -> HeaderBlock:
66+
service: Optional[str] = self.get_service(config=config, record=record)
67+
header_msg: str
68+
if icon is not None:
69+
header_msg = f"{icon} "
70+
header_msg += level
71+
if config.service is not None:
72+
header_msg += f" | {service}"
73+
else:
74+
header_msg += f" | {record.name}"
75+
76+
return HeaderBlock(text=PlainTextObject(text=header_msg))
77+
78+
def construct_context(
79+
self, config: Configuration, env: Optional[str], service: Optional[str]
80+
) -> Optional[ContextBlock]:
81+
if config.context != []:
82+
context_msg = ", ".join(config.context)
83+
return ContextBlock(elements=[MarkdownTextObject(text=context_msg)])
84+
elif env is not None and service is not None:
85+
return ContextBlock(elements=[MarkdownTextObject(text=f":point_right: {env}, {service}")])
86+
elif env is None:
87+
return ContextBlock(elements=[MarkdownTextObject(text=f":point_right: {env}")])
88+
elif service is None:
89+
return ContextBlock(elements=[MarkdownTextObject(text=f":point_right: {service}")])
90+
return None
91+
4692
def format(self, record: LogRecord) -> str:
4793
maybe_blocks: Sequence[Optional[Block]] = self.format_blocks(record=record)
4894
blocks: Sequence[Block] = [b for b in maybe_blocks if b is not None]
@@ -66,11 +112,7 @@ def format_blocks(self, record: LogRecord) -> Sequence[Optional[Block]]:
66112
message = record.getMessage()
67113
icon = self.config.emojis.get(record.levelno)
68114

69-
header: HeaderBlock
70-
if icon is not None:
71-
header = HeaderBlock(text=PlainTextObject(text=f"{icon} {level} | {self.config.service}"))
72-
else:
73-
header = HeaderBlock(text=PlainTextObject(text=f"{level} | {self.config.service}"))
115+
header: HeaderBlock = self.construct_header(record=record, config=self.config, icon=icon, level=level)
74116

75117
body = SectionBlock(text=MarkdownTextObject(text=message))
76118
default_blocks: Sequence[Block] = [
@@ -90,31 +132,28 @@ def format_blocks(self, record: LogRecord) -> Sequence[Optional[Block]]:
90132
message = record.getMessage()
91133
icon = self.config.emojis.get(record.levelno)
92134

93-
dynamic_extra_fields = getattr(record, "extra_fields", {})
94-
all_extra_fields = {**self.config.extra_fields, **dynamic_extra_fields}
95-
96-
header: HeaderBlock
97-
if icon is not None:
98-
header = HeaderBlock(text=PlainTextObject(text=f"{icon} {level} | {self.config.service}"))
99-
else:
100-
header = HeaderBlock(text=PlainTextObject(text=f"{level} | {self.config.service}"))
135+
env: Optional[str] = self.get_env(config=self.config, record=record)
136+
service: Optional[str] = self.get_service(config=self.config, record=record)
101137

138+
header: HeaderBlock = self.construct_header(record=record, config=self.config, icon=icon, level=level)
139+
context: Optional[ContextBlock] = self.construct_context(config=self.config, env=env, service=service)
102140
body = SectionBlock(text=MarkdownTextObject(text=message))
103141

104142
error: Optional[SectionBlock] = None
105143
if record.exc_info is not None:
106144
error = SectionBlock(text=MarkdownTextObject(text=f"```{record.exc_text}```"))
107145

108-
fields = SectionBlock(
109-
fields=[
110-
MarkdownTextObject(text=f"*Environment*\n{self.config.environment}"),
111-
MarkdownTextObject(text=f"*Service*\n{self.config.service}"),
112-
]
113-
+ [MarkdownTextObject(text=f"*{key}*\n{value}") for key, value in all_extra_fields.items()]
114-
)
146+
dynamic_extra_fields = getattr(record, "extra_fields", {})
147+
all_extra_fields = {**self.config.extra_fields, **dynamic_extra_fields}
148+
fields: Optional[SectionBlock] = None
149+
if all_extra_fields != {}:
150+
fields = SectionBlock(
151+
fields=[MarkdownTextObject(text=f"*{key}*\n{value}") for key, value in all_extra_fields.items()]
152+
)
115153

116154
maybe_blocks: Sequence[Optional[Block]] = [
117155
header,
156+
context,
118157
DividerBlock(),
119158
body,
120159
error,
@@ -147,6 +186,7 @@ def default(cls, config: Configuration) -> "SlackFormatter":
147186
return cls(design=RichDesign(config), config=config)
148187

149188
def format(self, record: LogRecord) -> str:
189+
super().format(record)
150190
return self.design.format(record)
151191

152192

@@ -266,7 +306,6 @@ def dummy(cls) -> "SlackHandler":
266306
return cls(client=DummyClient())
267307

268308
async def send_text_via_webhook(self, text: str) -> str:
269-
log.debug(text)
270309
response = await self.client.send(text=text)
271310
assert response.status_code == 200
272311
assert response.body == "ok"
@@ -281,13 +320,10 @@ async def send_blocks_via_webhook(self, blocks: str) -> str:
281320

282321
def emit(self, record: LogRecord) -> None:
283322
try:
323+
formatted_message = self.format(record)
284324
if isinstance(self.formatter, SlackFormatter):
285-
formatted_message = self.format(record)
286-
log.debug(f"formatted_message: {formatted_message}")
287325
asyncio.run(self.send_blocks_via_webhook(blocks=formatted_message))
288326
else:
289-
formatted_message = self.format(record)
290-
log.debug(f"formatted_message: {formatted_message}")
291327
asyncio.run(self.send_text_via_webhook(text=formatted_message))
292328

293329
except Exception:

tests/test_basic.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@
88

99
logger = logging.getLogger("LocalTest")
1010

11-
# Log to console as well
12-
stream_handler = logging.StreamHandler()
13-
stream_handler.setFormatter(logging.Formatter("%(asctime)s %(name)s|%(levelname)s: %(message)s"))
14-
logger.addHandler(stream_handler)
15-
1611
# Setup test handler
1712
slack_handler = SlackHandler.dummy()
1813
slack_handler.setLevel(logging.WARN)
@@ -169,8 +164,6 @@ def basic_blocks_filter(log_msg: str) -> None: # type: ignore
169164

170165

171166
DEFAULT_ADDITIONAL_FIELDS: Dict[str, Dict[str, str]] = {
172-
"env": {"text": "*Environment*\ntest", "type": "mrkdwn"},
173-
"service": {"text": "*Service*\ntestrunner", "type": "mrkdwn"},
174167
"foo": {"text": "*foo*\nbar", "type": "mrkdwn"},
175168
"raven": {"text": "*raven*\ncaw", "type": "mrkdwn"},
176169
}
@@ -186,6 +179,7 @@ def default_msg(
186179
{
187180
"blocks": [
188181
{"text": {"text": f"{emoji} {level_name} | testrunner", "type": "plain_text"}, "type": "header"},
182+
{"elements": [{"text": ":point_right: test, testrunner", "type": "mrkdwn"}], "type": "context"},
189183
{"type": "divider"},
190184
{
191185
"text": {"text": log_msg, "type": "mrkdwn"},
@@ -289,7 +283,7 @@ def test_exception_logging(self, caplog) -> None: # type: ignore
289283
with pytest.raises(ZeroDivisionError):
290284
exception_logging(log_msg)
291285

292-
blocks_prefix = '{"blocks": [{"text": {"text": ":x: ERROR | testrunner", "type": "plain_text"}, "type": "header"}, {"type": "divider"}, {"text": {"text": "Error!", "type": "mrkdwn"}, "type": "section"}, {"text": {"text": "```Traceback (most recent call last):'
286+
blocks_prefix = '{"blocks": [{"text": {"text": ":x: ERROR | testrunner", "type": "plain_text"}, "type": "header"}, {"elements": [{"text": ":point_right: test, testrunner", "type": "mrkdwn"}], "type": "context"}, {"type": "divider"}, {"text": {"text": "Error!", "type": "mrkdwn"}, "type": "section"}, {"text": {"text": "```Traceback (most recent call last):'
293287

294288
assert any(map(lambda m: blocks_prefix in m, caplog.messages))
295289

@@ -301,7 +295,7 @@ def test_auto_exception_logging(self, caplog) -> None: # type: ignore
301295
with pytest.raises(Exception):
302296
auto_exception_logging(log_msg)
303297

304-
blocks_prefix = '{"blocks": [{"text": {"text": ":x: ERROR | testrunner", "type": "plain_text"}, "type": "header"}, {"type": "divider"}, {"text": {"text": "Exception!", "type": "mrkdwn"}, "type": "section"}, {"text": {"text": "```Traceback (most recent call last):'
298+
blocks_prefix = '{"blocks": [{"text": {"text": ":x: ERROR | testrunner", "type": "plain_text"}, "type": "header"}, {"elements": [{"text": ":point_right: test, testrunner", "type": "mrkdwn"}], "type": "context"}, {"type": "divider"}, {"text": {"text": "Exception!", "type": "mrkdwn"}, "type": "section"}, {"text": {"text": "```Traceback (most recent call last):'
305299

306300
assert any(map(lambda m: blocks_prefix in m, caplog.messages))
307301

0 commit comments

Comments
 (0)