Skip to content

Commit facf5dc

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat-google-adk-agent-instrumentation
2 parents 378260f + b24828e commit facf5dc

13 files changed

Lines changed: 564 additions & 109 deletions

File tree

.claude/CLAUDE.md

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
The New Relic Python Agent is an Application Performance Monitoring (APM) agent that instruments Python applications for performance monitoring and analytics. It supports Python 3.9+ and monitors applications by wrapping functions, tracking transactions, collecting metrics, and sending telemetry data to New Relic's backend services.
8+
9+
## Development Commands
10+
11+
### Running Tests
12+
13+
Tests are run using `tox` from the root of the repository. The test suite is organized by component/framework. Each component's test suite is a separate subdirectory under `tests/`. To run that test suite, call the tox environment that contains the folder name of the test suite. `tox` will handle calling `cd` into the correct directory (via the `changedir` setting in `tox.ini`).
14+
15+
```bash
16+
# List test suites available to run
17+
tox -l
18+
19+
# Run tests for a specific component (from repository root)
20+
tox run -e linux-agent_features-py312-with_extensions
21+
22+
# Run tests for a framework integration (run `tox -l` for exact env names)
23+
tox run -e python-framework_django-py312-Djangolatest
24+
```
25+
26+
To iterate on a single test file with pytest directly, `cd` into that suite's directory first:
27+
28+
```bash
29+
cd tests/agent_features
30+
pytest test_agent_control_health_check.py -v
31+
```
32+
33+
### Running Tests with Coverage
34+
35+
Running tests with `tox` automatically produces a coverage data file for that environment combination.
36+
37+
```bash
38+
# Clear out old coverage files
39+
rm .tox/**/.coverage.*
40+
# Run tox to collect coverage for any number of environments
41+
tox run-parallel -e python-framework_falcon-py313-falconlatest,python-framework_falcon-py314-falconlatest
42+
# Combine data files (required, even for 1 input file)
43+
coverage combine .tox/**/.coverage.*
44+
# Generate the XML report, scoped to the file(s) you care about
45+
coverage xml --include=newrelic/hooks/framework_falcon.py
46+
# Read output file
47+
cat coverage.xml
48+
```
49+
50+
### Tox Environment Naming Convention
51+
52+
Tox environments follow this pattern:
53+
`services_required-tests_folder-python_version-library_version[optional]-extensions[optional]`
54+
55+
Examples:
56+
- `linux-agent_features-py312-with_extensions`
57+
- `postgres-datastore_psycopg-py313-psycopg0302`
58+
- `python-framework_flask-py311-flasklatest`
59+
60+
### Linting and Formatting
61+
62+
```bash
63+
# Run ruff linter and formatter (line length is 120; see [tool.ruff] in pyproject.toml)
64+
ruff check --fix && ruff format
65+
66+
# Run pre-commit hooks manually (ruff hooks are registered at the pre-push stage)
67+
pre-commit run --all-files --hook-stage pre-push
68+
```
69+
70+
### Building the Agent
71+
72+
```bash
73+
# Install the agent in development mode
74+
pip install -e .
75+
76+
# Install with C extensions explicitly enabled
77+
NEW_RELIC_EXTENSIONS=true pip install -e .
78+
79+
# Install without C extensions
80+
NEW_RELIC_EXTENSIONS=false pip install -e .
81+
```
82+
83+
## Architecture
84+
85+
### Core Components
86+
87+
#### 1. **Import Hook System** (`newrelic/api/import_hook.py`)
88+
The agent uses Python's import hook mechanism to automatically instrument third-party libraries. Import hooks are registered for specific modules and fire when those modules are first imported, allowing the agent to wrap functions before they're used. These are registered by calling `_process_module_definition(target, module, function)` where `target` is a string form of the instrumented library's module path, `module` is a string form of the module containing the instrument function under `newrelic.hooks.*`, and `function` is the name of the instrumentation hook to run on that module.
89+
90+
#### 2. **Instrumentation Hooks** (`newrelic/hooks/`)
91+
Each file in the `hooks/` directory contains instrumentation for a specific library or framework. Hooks use `wrap_function_wrapper` and similar utilities to instrument code without modifying the original source. Each instrument function requires at least 1 import hook which will apply it, made by a call to `_process_module_definition` in the file `newrelic/config.py` under the function `_process_module_builtin_defaults`.
92+
93+
Pattern:
94+
```python
95+
def instrument_module_name(module):
96+
wrap_function_wrapper(module, 'ClassName.method', wrapper_function)
97+
```
98+
99+
#### 3. **API Layer** (`newrelic/api/`)
100+
Provides public APIs for:
101+
- Transaction management (`transaction.py`, `background_task.py`)
102+
- Trace decorators/context managers (`function_trace.py`, `datastore_trace.py`, `external_trace.py`)
103+
- Error tracking (`error_trace.py`)
104+
- Custom instrumentation points
105+
106+
#### 4. **Core Engine** (`newrelic/core/`)
107+
Contains the core agent logic:
108+
- `agent.py` - Main agent singleton and lifecycle management
109+
- `application.py` - Application instance management
110+
- `*_node.py` - Node types for the transaction trace tree (database, external, function, etc.)
111+
- `config.py` - Configuration management
112+
113+
#### 5. **Transaction Model**
114+
Transactions are represented as trees of nodes:
115+
- Each trace type (function, database, external call) creates a node
116+
- Nodes track timing, metadata, and relationships
117+
- The root transaction aggregates all nodes and generates metrics
118+
- Transactions are context-local using thread-local or async context storage
119+
120+
#### 6. **Wrapper Architecture** (`newrelic/common/object_wrapper.py`)
121+
The agent extensively uses function wrapping to inject instrumentation:
122+
- `wrap_function_wrapper()` - Wraps functions/methods
123+
- `FunctionWrapper` - Wrapper object that preserves function metadata
124+
- Wrappers can be nested and maintain proper call order
125+
126+
#### 7. **Data Collection & Streaming**
127+
- `data_collector.py` - HTTP-based protocol for sending telemetry
128+
- `agent_streaming.py` - gRPC-based infinite tracing for distributed tracing
129+
- Harvest cycle collects and sends metrics periodically (default: 60 seconds)
130+
131+
### Key Design Patterns
132+
133+
1. **Lazy Initialization**: The agent must be initialized before importing instrumented libraries for best results, but handles late initialization gracefully.
134+
135+
2. **Manual Instrumentation API**: Decorators and context managers (`@background_task`, `@function_trace`, `with` blocks) mark transaction and trace boundaries.
136+
137+
3. **Thread Safety**: Heavily uses thread-local storage and locks for managing per-thread transaction state.
138+
139+
4. **Async Support**: Special handling for asyncio, gevent, and other async frameworks with context propagation.
140+
141+
### Test Organization
142+
143+
Tests are organized by component type:
144+
- `agent_features/` - Core agent functionality
145+
- `agent_unittests/` - Unit tests
146+
- `adapter_*/` - WSGI/ASGI server adapters
147+
- `datastore_*/` - Database client libraries
148+
- `framework_*/` - Web frameworks
149+
- `external_*/` - HTTP client libraries
150+
- `messagebroker_*/` - Message queue libraries
151+
- `mlmodel_*/` - ML/AI framework integrations
152+
- `logger_*/` - Logging framework integrations
153+
- `testing_support/` - Test utilities and fixtures
154+
155+
### Configuration
156+
157+
Configuration sources (in order of precedence):
158+
1. Environment variables (`NEW_RELIC_*`)
159+
2. `newrelic.ini` config file
160+
3. Programmatic configuration via `newrelic.agent.global_settings()`
161+
162+
Common environment variables:
163+
- `NEW_RELIC_LICENSE_KEY` - License key for authentication
164+
- `NEW_RELIC_APP_NAME` - Application name in APM
165+
- `NEW_RELIC_CONFIG_FILE` - Path to config file
166+
- `NEW_RELIC_DEVELOPER_MODE` - Enable developer mode for testing
167+
- `NEW_RELIC_EXTENSIONS` - Control C extension compilation (true/false)
168+
169+
## Important Conventions
170+
171+
### Adding New Instrumentation
172+
173+
When adding support for a new library:
174+
175+
1. Create a hook file in `newrelic/hooks/` (e.g., `newrelic/hooks/framework_newlib.py`)
176+
2. Implement `instrument_module_name()` function
177+
3. Add test directory under `tests/` with matching name
178+
4. Add tox environment definition in `tox.ini`
179+
5. Register import hooks that trigger instrumentation (via `_process_module_definition` in `newrelic/config.py`)
180+
6. Create tests that validate metrics, traces, and attributes
181+
182+
### Wrapper Function Signature
183+
184+
Wrappers should follow this pattern:
185+
```python
186+
def wrapper(wrapped, instance, args, kwargs):
187+
# wrapped: original function
188+
# instance: object instance (for bound methods) or object class (for class methods), or None (for functions and static methods)
189+
# args, kwargs: original call arguments for the wrapped function
190+
return wrapped(*args, **kwargs)
191+
```
192+
193+
### Error Handling in Instrumentation
194+
195+
Instrumentation code must never break the application:
196+
- Wrap instrumentation in try/except blocks
197+
- Log instrumentation errors at debug level
198+
- Always call the original wrapped function
199+
200+
### Testing Requirements
201+
202+
- Tests must be runnable via tox
203+
- Use `tests/testing_support/validators/` for validating collected metrics/traces
204+
- Each test directory needs its own `conftest.py` with necessary fixtures
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
services:
16+
firestore:
17+
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:437.0.1-emulators
18+
command:
19+
[
20+
"/bin/bash",
21+
"-c",
22+
"gcloud emulators firestore start --host-port=0.0.0.0:8080",
23+
]
24+
ports:
25+
- 8080:8080
26+
healthcheck:
27+
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/ || exit 1"]
28+
interval: 5s
29+
timeout: 3s
30+
retries: 12
31+
start_period: 10s
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# syntax=docker/dockerfile:1.4
2+
# Copyright 2010 New Relic, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
FROM redis:7.0.12
17+
18+
COPY <<"EOF" /etc/redis.conf
19+
bind 0.0.0.0
20+
cluster-enabled yes
21+
cluster-config-file nodes.conf
22+
cluster-node-timeout 5000
23+
cluster-preferred-endpoint-type hostname
24+
appendonly yes
25+
EOF
26+
27+
ENV REDIS_PORT=6379 \
28+
REDIS_ANNOUNCE_HOSTNAME=localhost
29+
30+
CMD ["sh", "-c", "exec redis-server /etc/redis.conf --port \"$REDIS_PORT\" --cluster-announce-hostname \"$REDIS_ANNOUNCE_HOSTNAME\" --cluster-announce-port \"$REDIS_PORT\""]
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
x-redis-node: &redis-node
16+
build: .
17+
image: redis-cluster-node:local
18+
tmpfs:
19+
- /data
20+
healthcheck:
21+
test: ["CMD-SHELL", "redis-cli -p $$REDIS_PORT ping"]
22+
interval: 2s
23+
timeout: 3s
24+
retries: 15
25+
26+
services:
27+
redis1:
28+
<<: *redis-node
29+
environment:
30+
REDIS_PORT: "6379"
31+
REDIS_ANNOUNCE_HOSTNAME: ${REDIS_ANNOUNCE_HOSTNAME:-localhost}
32+
ports:
33+
- 6379:6379
34+
35+
redis2:
36+
<<: *redis-node
37+
environment:
38+
REDIS_PORT: "6380"
39+
REDIS_ANNOUNCE_HOSTNAME: ${REDIS_ANNOUNCE_HOSTNAME:-localhost}
40+
ports:
41+
- 6380:6380
42+
43+
redis3:
44+
<<: *redis-node
45+
environment:
46+
REDIS_PORT: "6381"
47+
REDIS_ANNOUNCE_HOSTNAME: ${REDIS_ANNOUNCE_HOSTNAME:-localhost}
48+
ports:
49+
- 6381:6381
50+
51+
redis4:
52+
<<: *redis-node
53+
environment:
54+
REDIS_PORT: "6382"
55+
REDIS_ANNOUNCE_HOSTNAME: ${REDIS_ANNOUNCE_HOSTNAME:-localhost}
56+
ports:
57+
- 6382:6382
58+
59+
redis5:
60+
<<: *redis-node
61+
environment:
62+
REDIS_PORT: "6383"
63+
REDIS_ANNOUNCE_HOSTNAME: ${REDIS_ANNOUNCE_HOSTNAME:-localhost}
64+
ports:
65+
- 6383:6383
66+
67+
redis6:
68+
<<: *redis-node
69+
environment:
70+
REDIS_PORT: "6384"
71+
REDIS_ANNOUNCE_HOSTNAME: ${REDIS_ANNOUNCE_HOSTNAME:-localhost}
72+
ports:
73+
- 6384:6384
74+
75+
cluster-setup:
76+
image: redis-cluster-node:local
77+
restart: "no"
78+
command:
79+
- bash
80+
- -c
81+
- >-
82+
redis-cli --cluster create
83+
redis1:6379 redis2:6380 redis3:6381 redis4:6382 redis5:6383 redis6:6384
84+
--cluster-replicas 1
85+
--cluster-yes &&
86+
exec sleep infinity
87+
healthcheck:
88+
test:
89+
[
90+
"CMD-SHELL",
91+
"redis-cli -h redis1 cluster info | grep -q cluster_state:ok",
92+
]
93+
interval: 2s
94+
timeout: 3s
95+
retries: 30
96+
start_period: 30s
97+
depends_on:
98+
redis1: { condition: service_healthy }
99+
redis2: { condition: service_healthy }
100+
redis3: { condition: service_healthy }
101+
redis4: { condition: service_healthy }
102+
redis5: { condition: service_healthy }
103+
redis6: { condition: service_healthy }

.github/workflows/build-ci-image.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ on:
2222
paths:
2323
- ".github/containers/ci/**"
2424
- ".github/workflows/build-ci-image.yml"
25+
branches:
26+
- main
2527
push: # Trigger rebuilds when pushing relevant files to main.
2628
paths:
2729
- ".github/containers/ci/**"

0 commit comments

Comments
 (0)