Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report user_id to New Relic for error tracking (Python 2.7) #28

Merged
merged 2 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
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
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')
Loading