Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions custom_addons/docs/logfire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Traceability & Observability

<br>

<div align="center">
<a href="https://logfire.pydantic.dev/docs/" target="_blank">
<img src="https://img.shields.io/badge/Pydantic_Logfire-FF5A5F?style=for-the-badge&logo=pydantic&logoColor=white" alt="Logfire Logo">
</a>
</div>

<br>

This project implements robust observability using **[Pydantic Logfire](https://logfire.pydantic.dev/docs/)**.

Logfire allows us to trace the complete lifecycle of our webhook events, internal Odoo model logic and background threads, providing a clear **live dashboard** for debugging and performance monitoring across different addons.

<div align="center">
<img src="./img/logfire-dashboard-odoo.png" alt="Logfire General Dashboard">
<p><em>Overview of the Logfire tracing dashboard</em></p>
</div>

---

## Configuration Setup

Logfire is configured individually inside each Odoo addon module (e.g., `perry_webhook/` and `perry_human_loop/`) via their respective `logfire_config.py` files.

It ensures telemetry is initialized only once per module and gracefully disables itself if no token is found, preventing crashes in environments without Logfire configured.

```python
import os
import logfire

_configured = False

def configure_logfire() -> None:
global _configured

if _configured:
return

token = os.getenv("LOGFIRE_TOKEN")

# Disable if no token is present
if not token:
logfire.configure(send_to_logfire=False)
_configured = True
return

# Configure specific service names per addon
logfire.configure(
token=token,
# For perry_webhook
service_name=os.getenv("LOGFIRE_SERVICE_NAME_WEBHOOK", "perry-odoo-webhook"),
# For perry_human_loop
service_name=os.geten("LOGFIRE_SERVICE_NAME_HUMAN_LOOP", "perry-odoo-human-loop"),
environment=os.getenv("LOGFIRE_ENVIRONMENT", "development"),
)

_configured = True
```

### Environment variables
To enable Logfire across the Odoo environment, the following variables must be present in the `.env` file:

* `LOGFIRE_TOKEN`: This is the authentication token generated from the Logfire dashboard. To generate one:
1. Log into your Pydantic Logfire account and select your project.
2. Navigate to **Project settings** > **Write tokens** > **New**.
3. Enter a **Description** to identify your token.
4. Set the **Expiration** (e.g., "No expiration").
5. Click the **Create token** button and copy the generated value.

* `LOGFIRE_ENVIRONMENT`: Identifies the deployment context (`development`, `staging`, `production`).

* `LOGFIRE_SERVICE_NAME_WEBHOOK`: Identifies the source of the logs for the Webhook addon (defaults to `perry-odoo-webhook`).

* `LOGFIRE_SERVICE_NAME_HUMAN_LOOP`: Identifies the source of the logs for the Human Loop addon (defaults to `perry-odoo-human-loop`).

---

## How to use Logfire in the code

We use Logfire to explicitly track different flows, like Odoo record creations, action confirmations and background API requests.

Here is how the different logging methods should be used when developing new features or interacting with Odoo models:

### 1. Contextual spans (`logfire.span`)
Use spans to measure the duration and track the internal steps of a specific block of code (like processing an Odoo action or a threaded FastAPI call).
```python
# The span automatically measures how long the block takes to execute
with logfire.span(
"Send Odoo webhook to FastAPI",
dbname=dbname,
channel_id=channel_id,
session_id=payload.get("session_id")
):
# Code executed inside this context will be grouped in the Logfire dashboard
db_registry = registry(dbname)
with db_registry.cursor() as cr:
# ... Odoo environment logic ...
```
<div align="center">
<img src="./img/logfire-span-odoo.png" alt="Logfire Open Span">
<p><em>Example of an open span showing execution details and metadata</em></p>
</div>

### 2. Informational events (`logfire.info`)
Use this for standard events that indicate the normal flow of the application. Always include relevant Odoo metadata like Record IDs.
```python
logfire.info(
"Pending lead created in Odoo",
action_id=getattr(rec, "id", None),
has_name=bool(data.get("name")),
has_email_from=bool(data.get("email_from"))
)
```

### 3. Warnings (`logfire.warning`)
Use this for non-fatal errors or unexpected states that do not stop the execution but require attention (e.g., ignoring a state or skipping an action).
```python
logfire.warning(
"Pending action ignored because state is not executable",
action_id=getattr(rec, "id", None),
action_type=getattr(rec, "action_type", None),
state=getattr(rec, "state", None)
)
```

### 4. Errors without Exceptions (`logfire.error`)
Use this when a business logic failure occurs (e.g., a related Odoo record is missing or an external API returns a bad status code) but Python itself has not automatically raised an Exception.
```python
logfire.error(
"Partner or Journal not found for pending payment",
action_id=getattr(rec, "id", None),
partner_found=bool(partner),
journal_found=bool(journal)
)
```

### 5. Caught Exceptions (`logfire.exception`)
Use this specifically inside `except` blocks. Logfire will automatically capture the full Python stack trace and send it to the dashboard.
```python
try:
# Triggering the webhook
response = re.post(...)
except Exception as e:
logfire.exception(
"Failed to trigger webhook",
error=str(e),
session_id=payload.get("session_id"),
message_id=payload.get("message", {}).get("id")
)
```

---

## Maintaining observability in new features

When adding new functionalities (like a new Odoo Model, Controller or background thread), follow these strict guidelines to ensure end-to-end traceability:

1. **Instrument Odoo hooks:** Add a `logfire.span` or `logfire.info` inside `@api.model_create_multi` blocks or custom methods (like `action_confirm`) to track record lifecycles.

2. **Wrap main logic blocks in spans:** Any method that involves network requests (`requests.post`), thread creation or heavy data processing (like `json.loads` over loops) should be enclosed in a `with logfire.span("Context Name"):` block.

3. **Inject context metrics:** Do not just log strings. Always pass relevant key-value pairs to the logfire functions (e.g., `action_id`, `session_id`, `partner_found`, `status_code`). This is crucial for filtering and searching logs in the dashboard.

4. **Handle exceptions gracefully:** Ensure that every major `try/except` block utilizes `logfire.exception()` before raising an `odoo.exceptions.ValidationError` or logging it locally. This guarantees that no silent errors slip past Logfire.

---

## Scaling: Adding new Addons

If you develop a new Odoo addon (e.g., `perry_accounting`) and want to include it in the observability stack, you must follow these steps to ensure its logs are correctly segmented in the dashboard:

### 1. Create a local configuration
Copy the `logfire_config.py` template into the root of the new addon. This ensures that the addon can initialize its own connection to Logfire.

### 2. Define a unique Service Name
Add a new variable to the `.env` file to identify the new addon. This prevents logs from different modules from mixing together.

**Example for a new addon:**
```bash
# .env
LOGFIRE_SERVICE_NAME_NEW_ADDON=perry-odoo-new-feature
```

### 3. Update the addon's config logic
In the new `logfire_config.py`, make sure to reference that specific environment variable:

```python
# logfire_config.py inside the new addon
logfire.configure(
token=token,
service_name=os.getenv("LOGFIRE_SERVICE_NAME_NEW_ADDON", "perry-odoo-new-feature"),
environment=os.getenv("LOGFIRE_ENVIRONMENT", "development"),
)
```

### 4. Initialize in the Addon
Import and call `configure_logfire()` in the `__init__.py` of the models or at the top of the main model files.

> [!IMPORTANT]
> Since Odoo loads modules dynamically, failing to call the configuration inside the new addon will result in Logfire being disabled for that specific module's logic, even if it is enabled elsewhere.

---

## Navigating the Logfire Dashboard

Once the local application is running and generating traffic, you can view the traces in real-time:

<div align="center">
<img src="./img/logfire-live-odoo.png" alt="Logfire Live Dashboard">
<p><em>The 'Live' view in Logfire showing a timeline and captured spans</em></p>
</div>

To inspect the logs effectively:

1. Log in to your **[Pydantic Logfire Dashboard](https://logfire.pydantic.dev/)** and select the active project.

2. In the left sidebar, under the **OBSERVE** section, click on **Live** (to see logs coming in right now) or **Explore** (to search through past logs).

3. **Locate Spans:** Look for entries in the list that have a blue `+ [number]` badge next to them (e.g., `+ 95`). This indicates a **Span** containing multiple nested steps.

4. **Inspect details:** Click anywhere on that row to expand it. A side panel will open showing the complete execution tree, background thread events, API responses, and all the contextual Odoo variables (like `action_id` or `session_id`) that are injected in the code.
Loading