Skip to content

Commit 476a124

Browse files
committed
fix: Prefix SDK log messages
1 parent ee80810 commit 476a124

10 files changed

Lines changed: 129 additions & 10 deletions

.changeset/polite-wolves-warn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'pypi/posthog': patch
3+
---
4+
5+
Prefix PostHog SDK log messages so they remain identifiable with message-only logging formatters.

posthog/client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
mark_exception_as_captured,
3838
try_attach_code_variables_to_frames,
3939
)
40+
from posthog.logging_utils import get_posthog_logger
4041
from posthog.feature_flag_evaluations import (
4142
FeatureFlagEvaluations,
4243
_EvaluatedFlagRecord,
@@ -174,7 +175,7 @@ class Client(object):
174175
```
175176
"""
176177

177-
log = logging.getLogger("posthog")
178+
log = get_posthog_logger()
178179

179180
def __init__(
180181
self,

posthog/consumer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from typing import Any
22
import json
3-
import logging
43
import time
54
from threading import Thread
65

6+
from posthog.logging_utils import get_posthog_logger
77
from posthog.request import (
88
AI_EVENTS_ENDPOINT,
99
EVENTS_ENDPOINT,
@@ -31,7 +31,7 @@
3131
class Consumer(Thread):
3232
"""Consumes the messages from the client's queue."""
3333

34-
log = logging.getLogger("posthog")
34+
log = get_posthog_logger()
3535

3636
def __init__(
3737
self,

posthog/logging_utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import logging
2+
3+
4+
POSTHOG_LOG_PREFIX = "[PostHog]"
5+
POSTHOG_LOGGER_NAME = "posthog"
6+
7+
8+
class PostHogLogPrefixFilter(logging.Filter):
9+
"""Ensure PostHog SDK log messages are identifiable with message-only formatters."""
10+
11+
def filter(self, record: logging.LogRecord) -> bool:
12+
if getattr(record, "_posthog_log_prefix_applied", False):
13+
return True
14+
15+
message = record.getMessage()
16+
if not message.startswith(POSTHOG_LOG_PREFIX):
17+
record.msg = f"{POSTHOG_LOG_PREFIX} {message}"
18+
record.args = ()
19+
20+
record._posthog_log_prefix_applied = True
21+
return True
22+
23+
24+
def configure_posthog_logging() -> None:
25+
logger = logging.getLogger(POSTHOG_LOGGER_NAME)
26+
if not any(isinstance(f, PostHogLogPrefixFilter) for f in logger.filters):
27+
logger.addFilter(PostHogLogPrefixFilter())
28+
29+
30+
def get_posthog_logger() -> logging.Logger:
31+
configure_posthog_logging()
32+
return logging.getLogger(POSTHOG_LOGGER_NAME)

posthog/request.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import json
2-
import logging
32
import re
43
import socket
54
from dataclasses import dataclass
@@ -13,6 +12,7 @@
1312
from urllib3.connection import HTTPConnection
1413
from urllib3.util.retry import Retry
1514

15+
from posthog.logging_utils import get_posthog_logger
1616
from posthog.utils import remove_trailing_slash
1717
from posthog.version import VERSION
1818

@@ -226,7 +226,7 @@ def post(
226226
**kwargs,
227227
) -> requests.Response:
228228
"""Post the `kwargs` to the API"""
229-
log = logging.getLogger("posthog")
229+
log = get_posthog_logger()
230230
body = kwargs
231231
body["sent_at"] = datetime.now(tz=timezone.utc).isoformat()
232232
trimmed_host = remove_trailing_slash(normalize_host(host))
@@ -257,7 +257,7 @@ def post(
257257
def _process_response(
258258
res: requests.Response, success_message: str, *, return_json: bool = True
259259
) -> Union[requests.Response, Any]:
260-
log = logging.getLogger("posthog")
260+
log = get_posthog_logger()
261261
if res.status_code == 200:
262262
log.debug(success_message)
263263
response = res.json() if return_json else res
@@ -378,7 +378,7 @@ def get(
378378
- not_modified=True and data=None if server returns 304
379379
- not_modified=False and data=response if server returns 200
380380
"""
381-
log = logging.getLogger("posthog")
381+
log = get_posthog_logger()
382382
trimmed_host = remove_trailing_slash(normalize_host(host))
383383
full_url = trimmed_host + url
384384
headers = {"Authorization": "Bearer %s" % api_key, "User-Agent": USER_AGENT}

posthog/test/logging_helpers.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import io
2+
import logging
3+
from contextlib import contextmanager
4+
from typing import Iterator
5+
6+
7+
@contextmanager
8+
def capture_message_only_logs(level: int = logging.DEBUG) -> Iterator[io.StringIO]:
9+
logger = logging.getLogger("posthog")
10+
stream = io.StringIO()
11+
handler = logging.StreamHandler(stream)
12+
handler.setFormatter(logging.Formatter("%(message)s"))
13+
14+
previous_level = logger.level
15+
previous_propagate = logger.propagate
16+
logger.addHandler(handler)
17+
logger.setLevel(level)
18+
logger.propagate = False
19+
try:
20+
yield stream
21+
finally:
22+
logger.removeHandler(handler)
23+
handler.close()
24+
logger.setLevel(previous_level)
25+
logger.propagate = previous_propagate

posthog/test/test_client.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import time
23
import unittest
34
from datetime import datetime
@@ -9,6 +10,7 @@
910
from posthog.client import Client
1011
from posthog.contexts import get_context_session_id, new_context, set_context_session
1112
from posthog.request import APIError, GetResponse
13+
from posthog.test.logging_helpers import capture_message_only_logs
1214
from posthog.test.test_utils import FAKE_TEST_API_KEY
1315
from posthog.types import FeatureFlag, LegacyFlagMetadata
1416
from posthog.version import VERSION
@@ -80,6 +82,24 @@ def test_client_with_empty_api_key_is_noop(self):
8082

8183
self.assertIsNone(client.capture("event", distinct_id="distinct_id"))
8284

85+
def test_message_only_info_logs_include_posthog_prefix(self):
86+
self.client.flag_cache = mock.Mock()
87+
self.client.flag_cache.get_stale_cached_flag.return_value = mock.Mock()
88+
89+
with capture_message_only_logs(level=logging.INFO) as logs:
90+
self.client._get_stale_flag_fallback("distinct_id", "flag-key")
91+
92+
self.assertEqual(
93+
logs.getvalue().strip(),
94+
"[PostHog] [FEATURE FLAGS] Using stale cached value for flag flag-key",
95+
)
96+
97+
def test_message_only_logs_do_not_duplicate_existing_posthog_prefix(self):
98+
with capture_message_only_logs(level=logging.ERROR) as logs:
99+
self.client.log.error("[PostHog] already prefixed")
100+
101+
self.assertEqual(logs.getvalue().strip(), "[PostHog] already prefixed")
102+
83103
@mock.patch("posthog.client.get")
84104
def test_disabled_client_does_not_load_feature_flags(self, patch_get):
85105
client = Client("", personal_api_key="test", send=False)
@@ -386,7 +406,7 @@ def test_basic_capture_exception_with_no_exception_happening(self):
386406
self.assertFalse(patch_capture.called)
387407
self.assertEqual(
388408
logs.output[0],
389-
"WARNING:posthog:No exception information available",
409+
"WARNING:posthog:[PostHog] No exception information available",
390410
)
391411

392412
def test_capture_exception_logs_when_enabled(self):
@@ -396,7 +416,7 @@ def test_capture_exception_logs_when_enabled(self):
396416
Exception("test exception"), distinct_id="distinct_id"
397417
)
398418
self.assertEqual(
399-
logs.output[0], "ERROR:posthog:test exception\nNoneType: None"
419+
logs.output[0], "ERROR:posthog:[PostHog] test exception\nNoneType: None"
400420
)
401421

402422
@mock.patch("posthog.client.flags")

posthog/test/test_consumer.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from posthog.consumer import MAX_MSG_SIZE, Consumer
1515
from posthog.request import APIError
16+
from posthog.test.logging_helpers import capture_message_only_logs
1617
from posthog.test.test_utils import TEST_API_KEY
1718

1819

@@ -54,6 +55,18 @@ def test_upload(self) -> None:
5455
success = consumer.upload()
5556
self.assertTrue(success)
5657

58+
def test_message_only_error_logs_include_posthog_prefix(self) -> None:
59+
q = Queue()
60+
consumer = Consumer(q, TEST_API_KEY)
61+
q.put(_track_event())
62+
63+
with mock.patch.object(consumer, "request", side_effect=Exception("boom")):
64+
with capture_message_only_logs() as logs:
65+
success = consumer.upload()
66+
67+
self.assertFalse(success)
68+
self.assertEqual(logs.getvalue().strip(), "[PostHog] error uploading: boom")
69+
5770
def test_flush_interval(self) -> None:
5871
# Put _n_ items in the queue, pausing a little bit more than
5972
# _flush_interval_ after each one.

posthog/test/test_exception_capture.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def test_excepthook(tmpdir):
149149

150150
assert b"ZeroDivisionError" in output
151151
assert b"LOL" in output
152-
assert b"DEBUG:posthog:data uploaded successfully" in output
152+
assert b"DEBUG:posthog:[PostHog] data uploaded successfully" in output
153153
assert (
154154
b'"$exception_list": [{"mechanism": {"type": "generic", "handled": true}, "module": null, "type": "ZeroDivisionError", "value": "division by zero", "stacktrace": {"frames": [{"platform": "python", "filename": "app.py", "abs_path"'
155155
in output

posthog/test/test_request.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import requests
88

99
import posthog.request as request_module
10+
from posthog.test.logging_helpers import capture_message_only_logs
1011
from posthog.request import (
1112
APIError,
1213
DatetimeSerializer,
@@ -84,6 +85,28 @@ def test_post_sends_snake_case_sent_at(key, expected_present):
8485
assert (key in data) is expected_present
8586

8687

88+
def test_message_only_debug_logs_include_posthog_prefix():
89+
mock_response = requests.Response()
90+
mock_response.status_code = 200
91+
mock_session = mock.MagicMock()
92+
mock_session.post.return_value = mock_response
93+
94+
with capture_message_only_logs() as logs:
95+
request_module.post(
96+
TEST_API_KEY,
97+
host="https://test.posthog.com",
98+
path="/batch/",
99+
session=mock_session,
100+
batch=[],
101+
)
102+
103+
assert logs.getvalue().splitlines() == [
104+
mock.ANY,
105+
"[PostHog] data uploaded successfully",
106+
]
107+
assert logs.getvalue().splitlines()[0].startswith("[PostHog] making request: ")
108+
109+
87110
class TestRequests(unittest.TestCase):
88111
def test_valid_request(self):
89112
res = batch_post(

0 commit comments

Comments
 (0)