Skip to content

Commit 8fd323c

Browse files
authored
Merge pull request #1034 from appsignal/docs
Add documentation for architectural decisions
2 parents 0993b4e + 03a8999 commit 8fd323c

16 files changed

Lines changed: 733 additions & 21 deletions

docs/configuration-system.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Configuration System
2+
3+
The AppSignal Elixir integration uses a layered configuration system that merges settings from multiple sources.
4+
This document explains how configuration works, the priority of different sources, and important design decisions.
5+
6+
## Overview
7+
8+
Configuration flows through several stages:
9+
10+
1. **Sources** - Configuration is loaded from multiple sources
11+
2. **Merging** - Sources are merged in priority order
12+
3. **Validation** - Configuration is validated for required values
13+
4. **Storage** - Configuration is stored in the Application environment
14+
5. **Writing** - Configuration is written to the NIF for the Rust agent
15+
16+
## Configuration Sources (Priority Order)
17+
18+
Configuration is loaded from four sources, listed from lowest to highest priority.
19+
20+
### 1. Default Configuration
21+
22+
Hard-coded defaults in the package.
23+
24+
Examples:
25+
- `active: false` - AppSignal is inactive by default
26+
- `instrument_ecto: true` - Ecto instrumentation is enabled by default
27+
- `log: "file"` - Logs go to file by default
28+
- `send_params: true` - Request parameters are sent by default
29+
30+
### 2. System Detection
31+
32+
Automatically detected system configuration.
33+
If `APPSIGNAL_PUSH_API_KEY` is set, AppSignal is automatically activated.
34+
35+
### 3. Application Configuration
36+
37+
Configuration from `config/config.exs` or environment-specific config files:
38+
Can be a keyword list or a map.
39+
40+
### 4. Environment Variables
41+
42+
Environment variables have the highest priority.
43+
44+
**Type Conversion:**
45+
46+
Environment variables are parsed based on their expected type:
47+
48+
- **Strings** - Used as-is: `APPSIGNAL_APP_NAME`, `APPSIGNAL_HOSTNAME`
49+
- **Booleans** - Parsed as `true` or `false`: `APPSIGNAL_ACTIVE`, `APPSIGNAL_DEBUG`
50+
- **Atoms** - Converted to atoms: `APPSIGNAL_APP_ENV`, `APPSIGNAL_OTP_APP`
51+
- **String lists** - Split on commas: `APPSIGNAL_FILTER_PARAMETERS`, `APPSIGNAL_IGNORE_ACTIONS`
52+
- **Floats** - Parsed as floats: `APPSIGNAL_CPU_COUNT`
53+
54+
## Configuration Overrides
55+
56+
After merging all sources, some configuration values trigger automatic overrides.
57+
58+
### Backwards Compatibility: `skip_session_data`
59+
60+
The `skip_session_data` option was deprecated in favor of `send_session_data`.
61+
The system handles both for backwards compatibility.
62+
63+
- If only `send_session_data` is set, `skip_session_data` is derived from it
64+
- If only `skip_session_data` is set, `send_session_data` is derived from it
65+
- If both are set, `send_session_data` takes precedence
66+
- A deprecation warning is shown if `skip_session_data` is used
67+
68+
## Validation
69+
70+
Configuration is considered valid if it has a non-empty `push_api_key`.
71+
72+
**Why Only Check the Push API Key?**
73+
74+
The push API key is the only strictly required value.
75+
All other configuration has sensible defaults, so the agent can start without them.
76+
Without a valid push API key, the agent cannot send data to AppSignal, making it pointless to run.
77+
78+
## Active vs. Configured
79+
80+
There's an important distinction between **configured as active** and **actually active**:
81+
82+
### `configured_as_active?/0`
83+
84+
Returns `true` if the `active` config option is set to `true`:
85+
86+
### `active?/0`
87+
88+
Returns `true` only if BOTH the config is marked as active AND it's valid.
89+
90+
**Why the Distinction?**
91+
92+
This allows users to set `active: true` in development but omit the `push_api_key`.
93+
The agent won't actually start (because `active?/0` returns false), but the configuration intent is clear.
94+
95+
## Writing Configuration to the NIF
96+
97+
Before starting the Rust agent, configuration must be written to it through the NIF.
98+
99+
### Why Write to NIF?
100+
101+
The Rust agent runs in a separate process and doesn't have access to Elixir's Application environment.
102+
Configuration must be explicitly passed through the NIF using `Appsignal.Nif.env_put/2`.
103+
104+
### Configuration Keys
105+
106+
All configuration keys are prefixed with `_APPSIGNAL_` when written to the NIF.
107+
108+
**Why the `_` Prefix?**
109+
110+
The underscore prefix distinguishes AppSignal's internal environment from the system's actual environment variables.
111+
This prevents conflicts and makes it clear these are for the agent, not the host system.
112+
113+
### Type Conversion
114+
115+
All values are converted to strings before passing to the NIF:
116+
117+
- Atoms/booleans: `to_string(config[:active])`
118+
- Lists: `config[:ignore_actions] |> Enum.join(",")`
119+
- Nil values: `config[:push_api_key] || ""`
120+
121+
The Rust agent parses these strings back into the appropriate types.
122+
123+
## Feature Flags
124+
125+
Several boolean configuration options control which integrations are enabled.
126+
127+
### Default-Enabled Integrations
128+
129+
These are `true` by default:
130+
131+
- `instrument_ecto: true` - Ecto query instrumentation
132+
- `instrument_finch: true` - Finch HTTP client instrumentation
133+
- `instrument_oban: true` - Oban job instrumentation
134+
- `instrument_tesla: true` - Tesla HTTP client instrumentation
135+
- `instrument_absinthe: true` - Absinthe GraphQL instrumentation
136+
137+
**Why Default to True?**
138+
139+
The instrumentation libraries use telemetry attachment, which has minimal overhead if the integration isn't being used.
140+
Defaulting to enabled provides the best out-of-box experience.
141+
142+
### Default-Disabled Integrations
143+
144+
- `enable_error_backend: false` - Error backend for crash reporting
145+
- `enable_minutely_probes: true` - System metrics (CPU, memory)
146+
- `enable_statsd: false` - StatsD server
147+
- `enable_nginx_metrics: false` - Nginx metrics collection
148+
149+
## Special Configuration: Oban Error Reporting
150+
151+
The `report_oban_errors` option has three possible values:
152+
153+
- `"all"` (default) - Report all Oban errors
154+
- `"discard"` - Only report errors for discarded jobs
155+
- `"none"` or `"false"` - Don't report Oban errors
156+
157+
**Why Special Handling?**
158+
159+
Oban jobs can fail and be retried multiple times.
160+
Users may want to report only permanently failed (discarded) jobs to reduce noise in AppSignal.

docs/error-handling.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Error Handling and Error Backend
2+
3+
The AppSignal Elixir integration provides multiple mechanisms for capturing and reporting errors.
4+
This document explains the error handling architecture, the error backend, and why that's disabled by default.
5+
6+
## Error Reporting Overview
7+
8+
There are three ways errors get reported to AppSignal:
9+
10+
1. **Manual error reporting** - `Appsignal.set_error/2` and `Appsignal.send_error/2`
11+
2. **Automatic instrumentation** - Framework integrations (Phoenix, Oban) catch errors
12+
3. **Error backend** - Logger backend that catches unhandled crashes (disabled by default)
13+
14+
## Manual Error Reporting
15+
16+
See instrumentation-decorators.md for details on `set_error` and `send_error`.
17+
18+
Quick summary:
19+
- `set_error/2` - Adds error to existing span (within a web request or job)
20+
- `send_error/2` - Creates new span for error (outside any trace)
21+
22+
## Error Backend
23+
24+
The error backend is a Logger backend that intercepts error reports from Erlang's error logger.
25+
26+
### Architecture
27+
28+
The error backend implements the `:gen_event` behavior.
29+
It attaches to Erlang's internal `:error_logger` as a backend.
30+
31+
### What It Catches
32+
33+
The error backend processes error events from the Logger.
34+
35+
**Event Structure:**
36+
37+
- **Level**: `:error` (only error-level events are processed)
38+
- **Group leader**: Must be on the current node (prevents duplicate reporting in distributed systems)
39+
- **Metadata**: Contains crash information including `crash_reason`, `pid`, `conn`, etc.
40+
41+
### Crash Reason Handling
42+
43+
The backend looks for a `crash_reason` in the metadata.
44+
The crash reason is a tuple: `{reason, stacktrace}`
45+
46+
### PID Extraction
47+
48+
The PID is extracted from different report structures.
49+
50+
**Why Different Sources?**
51+
52+
- Phoenix/Plug crashes include a `conn` struct with the request process's PID in `conn.owner`
53+
- Generic crashes include a `pid` field directly
54+
- Some crashes don't include any process information
55+
56+
### Filtering Cowboy Errors
57+
58+
Cowboy errors are explicitly filtered out.
59+
60+
**Why?**
61+
62+
Cowboy errors were causing issues:
63+
64+
1. They don't have useful context for debugging
65+
2. They can have non-list stacktraces that crashed the handler
66+
3. Phoenix's built-in error handling already reports these errors properly
67+
68+
Before this fix, only cowboy errors without a `conn` were filtered.
69+
The fix ensures ALL cowboy errors are ignored, even those with conns.
70+
71+
## Why It's Disabled by Default
72+
73+
The error backend was changed to be **disabled by default** in June 2024.
74+
75+
### Historical Context
76+
77+
The error backend was created before AppSignal had proper framework integrations.
78+
It was the primary way to catch unhandled errors in Elixir applications.
79+
80+
### Problems with the Error Backend
81+
82+
**1. Lack of Context**
83+
84+
Errors caught by the backend have minimal context:
85+
- No request parameters
86+
- No session data
87+
- No custom attributes
88+
- Just the raw exception and stacktrace
89+
90+
This makes debugging difficult.
91+
92+
**2. Duplicate Reporting**
93+
94+
Modern framework integrations (Phoenix, Oban) already report errors properly with full context.
95+
Although we have error ignoring, sometimes the error backend would catch the same errors again, leading to duplicates.
96+
97+
**3. Confusing Results**
98+
99+
Because the errors lack context, they appear in AppSignal without the information needed to understand or fix them.

0 commit comments

Comments
 (0)