docs/published/handbook/engineering/conventions/backend-coding.md
As a general rule, we should have logs for every expected and unexpected actions of the application, using the appropriate log level.
We should also be logging these exceptions to Posthog. Python exceptions should almost always be captured automatically without extra instrumentation, but custom ones (such as failed requests to external services, query errors, or Celery task failures) can be tracked using capture_exception().
A log level or log severity is a piece of information telling how important a given log message is:
DEBUG: should be used for information that may be needed for diagnosing issues and troubleshooting or when running application
in the test environment for the purpose of making sure everything is running correctlyINFO: should be used as standard log level, indicating that something happenedWARN: should be used when something unexpected happened but the code can continue the workERROR: should be used when the application hits an issue preventing one or more functionalities from properly functioningdjango-structlog is the default logging library we use (see docs).
It's a structured logging framework that adds cohesive metadata on each logs that makes it easier to track events or incidents.
Structured logging means that you don’t write hard-to-parse and hard-to-keep-consistent prose in your logs but that you log events that happen in a context instead.
import structlog
logger = structlog.get_logger(__name__)
logger.debug("event_sent_to_kafka", event_uuid=str(event_uuid), kafka_topic=topic)
will produce:
2021-10-28T13:46:40.099007Z [debug] event_sent_to_kafka [posthog.api.capture] event_uuid=017cc727-1662-0000-630c-d35f6a29bae3 kafka_topic=default
As you can see above, the log contains all the information needed to understand the app behavior.
By default, most posthog.* loggers only output WARNING and above. This keeps production logs clean but means your logger.info() calls won't appear.
To enable INFO logging for a specific module, add it to posthog/settings/logs.py:
"loggers": {
# ... existing loggers ...
"posthog.tasks.my_module": {"level": "INFO", "handlers": ["console"], "propagate": False},
}
Note: calling logger.setLevel(logging.INFO) in your code doesn't work with structlog - you must add the config entry above.
Celery task lifecycle events (task_started, task_succeeded, etc.) are logged automatically by django-structlog at INFO level and are already enabled.
Don’t log sensitive information. Make sure you never log:
assert x == y instead of the self.assertEqual(x, y) format of tests
assert ['x', 'y'] == response.json()["results"] over assert len(response.json()["results"]) == 2
A good test should:
Always use HogQL instead of raw ClickHouse queries in product code.
Querying ClickHouse directly from product code is a bad idea for several reasons:
Data safety: HogQL automatically scopes queries to the current team, preventing accidental cross-team data access. Raw queries that fetch data for multiple teams and separate it in code are risky—even if correct now, future changes could introduce data breaches.
Consistency: HogQL handles property access, person mapping, and other PostHog-specific concerns correctly and consistently.
Query attribution: If you must query ClickHouse directly for a valid reason, ensure you tag your queries appropriately with the right product tag and ClickHouse user.
The only case where raw ClickHouse queries might be justified is cross-team queries, but even then consider alternatives:
We default to open but when adding a new feature we should consider if it should be MIT licensed or Enterprise edition licensed. Everything in the ee folder is covered by a different license. It's easy to move things from ee to open, but not the other way.
All the open source code is copied to the posthog-foss repo with the ee code stripped out. You need to consider whether your code will work if imports to ee are unavailable.
Sync note: This file is also copied to posthog/posthog/.claude/commands/conventions.md for Claude Code. When updating this file, please also update the copy there.