Skip to content

Commit

Permalink
Report user_id to New Relic for error tracking (Python 2.7) (#28)
Browse files Browse the repository at this point in the history
* Report user_id to New Relic for error tracking

* Log user id reporting to NR
  • Loading branch information
hammady authored Oct 28, 2024
1 parent 0296a52 commit 3daa9fb
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 21 deletions.
118 changes: 98 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ should subclass to implement your pure python jobs. It basically replicates
the [delayed_job](https://github.com/collectiveidea/delayed_job) Ruby gem behavior.

## Installation
### From Pypi:

### From PyPI

pip install rubydj-pyworker==0.2.0

You can also pick up any version from [Pypi](https://pypi.org/project/rubydj-pyworker/)
You can also pick up any version from [PyPI](https://pypi.org/project/rubydj-pyworker/)
that supports Python 2.7 (< 1.0.0).
### From Github branch:

### From Github branch

pip install git+https://github.com/rayyansys/pyworker.git@<branch_name>#egg=rubydj-pyworker

## Usage
Expand Down Expand Up @@ -97,14 +101,95 @@ configuration options:

Youc an also provide a logger class (from `logging` module) to have full control on logging configuration:

import logging
```python
import logging

logging.basicConfig()
logger = logging.getLogger('pyworker')
logger.setLevel(logging.INFO)
logging.basicConfig()
logger = logging.getLogger('pyworker')
logger.setLevel(logging.INFO)

w = Worker(dbstring, logger)
w.run()
w = Worker(dbstring, logger)
w.run()
```

## Monitoring

Workers can be monitored using [New Relic](https://newrelic.com/). All you need
to do is to create a free account there, then add the following to your
environment variables:

```bash
NEW_RELIC_LICENSE_KEY=<your_newrelic_license_key>
NEW_RELIC_APP_NAME=<your_newrelic_app_name>
```

All jobs will be reported under the `BackgroundTask` category. This includes
standard metrics like throughput, response time (job duration), error rate, etc.
Additional transaction custom attributes are also reported out of the box:

1. `jobName`: the name of the job class
1. `jobId`: the id of the delayed job in the database
1. `jobQueue`: the queue name of the job
1. `jobAttempts`: the number of attempts for the job
1. `jobLatency`: the time between the job creation and the time it was picked up by the worker

If you wish to automatically report additional attributes from the delayed job table
(e.g. the priority of the job or any other custom fields), you can do so by
providing a list of fields to the worker:

```python
worker = Worker(dbstring,
logger=logger,
extra_delayed_job_fields=['priority', 'custom_field'])
```

Columns of types `string`/`text`, `int`, `float` and `bool` are reported as is.
`json`/`jsonb` types are expanded into separate attributes. All other fields are converted
to JSON strings.

You can also automatically prefix all attributes with a string before reporting:

```python
worker = Worker(dbstring,
logger=logger,
reported_attributes_prefix='myapp.')
```

If you wish to report additional custom attributes from your job, you can do so
by calling the `reporter` object that is available in the job instance:

```python
class MyJob(Job):
def __init__(self, *args, **kwargs):
super(MyJob, self).__init__(*args, **kwargs)

def run(self):
...
self.reporter.report(
my_custom_attribute='my_custom_value',
another_custom_attribute='another_custom_value',
...)
...
```

The prefix will be applied to all attributes reported from the job as well
as the camel case conversion. If you wish to skip both, you can use the
`report_raw` function instead of `report`.

In all cases, two additional attributes are reported:

1. `error`: a Boolean indicating whether the job has failed or not
1. `jobFailure`: a Boolean indicating whether the job has permanently failed or not

The `error` attribute is reported as is, i.e. without prefix or camel case conversion.

Additionally, the end user id is reported if the custom attributes contain the id
in any of the below formats:

1. `user_id` (with and without prefix)
1. `userId` (with and without prefix)

This is useful in identifying impacted users count in case of job errors.

## Limitations

Expand Down Expand Up @@ -133,21 +218,14 @@ Do your changes, then send a pull request.
## Publish

### Using Python

1. Increment the version number in `setup.py`
1. Install twine: `pip install twine`. You may need to upgrade pip first: `pip install --upgrade pip`
1. Create the distribution files: `python setup.py sdist bdist_wheel`
1. Optionally upload to [Test PyPi](https://test.pypi.org/) as a dry run: `twine upload -r testpypi dist/*`. You will need a separate account there
1. Upload to [PyPi](https://pypi.org/): `twine upload dist/*`

### Using Docker
Increment the version as in the first step above then:

```bash
docker build . -t pyworker:0.1.0
docker run -it --rm pyworker:0.1.0
```
1. Optionally upload to [Test PyPI](https://test.pypi.org/) as a dry run: `twine upload -r testpypi dist/*`. You will need a separate account there
1. Upload to [PyPI](https://pypi.org/): `twine upload dist/*`

Enter your PyPi username and password when prompted.
Enter your PyPI username and password when prompted.

## License

Expand Down
8 changes: 8 additions & 0 deletions pyworker/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ def _convert_value(value):
def _report_newrelic(self, attributes):
if self._logger:
self._logger.debug('Reporter: reporting to NewRelic: %s' % attributes)
# report user id if available in attributes in any form
possible_keys = [self._prefix + 'userId', self._prefix + 'user_id', 'userId', 'user_id']
possible_values = [attributes.get(key) for key in possible_keys if key in attributes]
if possible_values:
user_id = str(possible_values[0])
newrelic.agent.set_user_id(user_id)
if self._logger:
self._logger.debug('Reporter: reporting to NewRelic user_id: %s' % user_id)
# convert attributes dict to list of tuples
attributes = list(attributes.items())
newrelic.agent.add_custom_attributes(attributes)
56 changes: 55 additions & 1 deletion tests/test_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def test_reporter_convert_value_converts_value_to_json_if_not_supported(self):
#********** ._report_newrelic tests **********#

@patch('pyworker.reporter.newrelic.agent')
def test_reporter_report_newrelic_calls_newrelic_record_exception(self, newrelic_agent):
def test_reporter_report_newrelic_calls_newrelic_add_custom_attributes(self, newrelic_agent):
reporter = Reporter()
reporter._report_newrelic({
'test_key1': 'test_value',
Expand All @@ -199,3 +199,57 @@ def test_reporter_report_newrelic_calls_newrelic_record_exception(self, newrelic
('test_key1', 'test_value'),
('test_key2', 2)
])

@patch('pyworker.reporter.newrelic.agent')
def test_reporter_report_newrelic_does_not_call_newrelic_set_user_id_if_user_id_not_in_attributes(self, newrelic_agent):
reporter = Reporter()
reporter._report_newrelic({
'test_key1': 'test_value',
'test_key2': 2
})

newrelic_agent.set_user_id.assert_not_called()

@patch('pyworker.reporter.newrelic.agent')
def test_reporter_report_newrelic_calls_newrelic_set_user_id_if_user_id_in_attributes(self, newrelic_agent):
reporter = Reporter()
reporter._report_newrelic({
'test_key1': 'test_value',
'test_key2': 2,
'user_id': 123
})

newrelic_agent.set_user_id.assert_called_once_with('123')

@patch('pyworker.reporter.newrelic.agent')
def test_reporter_report_newrelic_calls_newrelic_set_user_id_if_userId_in_attributes(self, newrelic_agent):
reporter = Reporter()
reporter._report_newrelic({
'test_key1': 'test_value',
'test_key2': 2,
'userId': 123
})

newrelic_agent.set_user_id.assert_called_once_with('123')

@patch('pyworker.reporter.newrelic.agent')
def test_reporter_report_newrelic_calls_newrelic_set_user_id_if_prefixed_user_id_in_attributes(self, newrelic_agent):
reporter = Reporter(attribute_prefix='test_')
reporter._report_newrelic({
'test_key1': 'test_value',
'test_key2': 2,
'test_user_id': 123
})

newrelic_agent.set_user_id.assert_called_once_with('123')

@patch('pyworker.reporter.newrelic.agent')
def test_reporter_report_newrelic_calls_newrelic_set_user_id_if_prefixed_userId_in_attributes(self, newrelic_agent):
reporter = Reporter(attribute_prefix='test_')
reporter._report_newrelic({
'test_key1': 'test_value',
'test_key2': 2,
'test_userId': 123
})

newrelic_agent.set_user_id.assert_called_once_with('123')

0 comments on commit 3daa9fb

Please sign in to comment.