diff --git a/custom_addons/docs/README.md b/custom_addons/docs/README.md new file mode 100644 index 0000000..78215cc --- /dev/null +++ b/custom_addons/docs/README.md @@ -0,0 +1,224 @@ +# Traceability & Observability + +
+ +
+ + Logfire Logo + +
+ +
+ +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. + +
+ Logfire General Dashboard +

Overview of the Logfire tracing dashboard

+
+ +--- + +## 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 ... +``` +
+ Logfire Open Span +

Example of an open span showing execution details and metadata

+
+ +### 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: + +
+ Logfire Live Dashboard +

The 'Live' view in Logfire showing a timeline and captured spans

+
+ +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. diff --git a/custom_addons/docs/img/logfire-dashboard-odoo.png b/custom_addons/docs/img/logfire-dashboard-odoo.png new file mode 100644 index 0000000..83793be Binary files /dev/null and b/custom_addons/docs/img/logfire-dashboard-odoo.png differ diff --git a/custom_addons/docs/img/logfire-live-odoo.png b/custom_addons/docs/img/logfire-live-odoo.png new file mode 100644 index 0000000..f6225a2 Binary files /dev/null and b/custom_addons/docs/img/logfire-live-odoo.png differ diff --git a/custom_addons/docs/img/logfire-span-odoo.png b/custom_addons/docs/img/logfire-span-odoo.png new file mode 100644 index 0000000..a70be93 Binary files /dev/null and b/custom_addons/docs/img/logfire-span-odoo.png differ