Error handlingΒΆ

Writing custom OneAgent plugins isn't much different than any other programming tasks. This means that errors may occur. Some errors are fairly predictable while others are less predictable. Plugin developers have two options for handling errors:

  1. Have OneAgent handle errors
  2. Intercept predictable erroneous situations, categorize them, and provide a custom error message.

The first approach doesn't necessarily require any additional work. If for some reason the plugin code throws an exception, error details will be sent to Dynatrace Cluster Node and the plugin will resume within a few seconds. There is a failure limit however, after which the plugin will not resume. To learn more about the details of this mechanism, see the plugin lifecycle guide

The second approach is a bit more involved, but it gives you more control over the error messages that are provided to users of the plugin. Imagine a situation in which a library you use to connect to a database server responds with an error 80010 message, which means invalid credentials. Would you rather that the user read the error 80010 message as-s, or would you prefer a more user-friendly message, like "The provided credentials were rejected by the server"?

To demonstrate this second approach, we will extend the plugin we created earlier in plugin configuration. The plugin in question receives some configuration from the server and uses it to authenticate itself with our demo application. For the purposes of this tutorial we will assume that everything in the previous tutorial proceeded as expected. Now, let's discuss how to handle various problems.

As a reminder, we're monitoring a demo application that can be started using:

python -m oneagent_sdk.demo_app_auth

The demo application listens on port 8090 and expects authentication with ruxit:ruxit credentials. Please remember that you can start python -m oneagent_sdk.demo_app_auth --help to learn more about how to use the demo application.

Let's take a look at the extended plugin:

import requests
import requests.exceptions
import json
from ruxit.api.base_plugin import BasePlugin
from ruxit.api.exceptions import AuthException, ConfigException
from ruxit.api.snapshot import pgi_name


class DemoPlugin(BasePlugin):
    def query(self, **kwargs):
        config = kwargs["config"]
        user = config["user"]
        password = config["password"]
        pgi = self.find_single_process_group(pgi_name('oneagent_sdk.demo_app_auth'))
        pgi_id = pgi.group_instance_id
        stats_url = "http://localhost:8779"

        try:
            response = requests.get(stats_url, auth=(user, password))
            if response.status_code == 401:
                raise AuthException(response)
            stats = json.loads(response.content.decode())
        except requests.exceptions.ConnectTimeout as ex:
            raise ConfigException('Timeout on connecting with "%s"' % stats_url) from ex
        except requests.exceptions.RequestException as ex:
            raise ConfigException('Unable to connect to "%s"' % stats_url) from ex
        except json.JSONDecodeError as ex:
            raise ConfigException('Server response from %s is not json' % stats_url) from ex

        self.results_builder.absolute(key='random', value=stats['random'], entity_id=pgi_id)
        self.results_builder.relative(key='counter', value=stats['counter'], entity_id=pgi_id)

Note the corresponding plugin.json doesn't need to be changed:

{
  "version": "1.2",
  "name": "custom.python.demo_plugin_auth",
  "type": "python",
  "entity": "PROCESS_GROUP_INSTANCE",
  "processTypeNames": ["PYTHON"],
  "source": {
    "package": "demo_plugin_auth",
    "className": "DemoPlugin",
    "install_requires": ["requests>=2.6.0"],
    "activation": "Singleton"
  },
  "metrics": [
    {
      "timeseries": {
        "key": "random",
        "unit": "Count",
        "dimensions": []
      }
    },
    {
      "timeseries": {
        "key": "counter",
        "unit": "Count",
        "dimensions": []
      }
    }
  ],
  "configUI": {
    "displayName": "custom.python.demo_plugin_auth"
  },
  "properties": [
    {
      "key": "user",
      "type": "String"
    },
    {
      "key": "password",
      "type": "Password"
    }
  ]
}

As you can see, the way to inform OneAgent of a specific category of error is by raising exceptions of a particular type.

            if response.status_code == 401:
                raise AuthException(response)

The first type of error we handle is a problem with the server not providing access to the stats we want to gather. HTTP applications usually signal this with an 401 error code. We check the error and raise an AuthException. The results on the server may look like this:

../_images/demo_02_configuration_auth_error.png

Apart from invalid credentials, other errors may occur. If you want to intercept them and provide a custom error message, do it like this:

        except requests.exceptions.ConnectTimeout as ex:
            raise ConfigException('Timeout on connecting with "%s"' % stats_url) from ex
        except requests.exceptions.RequestException as ex:
            raise ConfigException('Unable to connect to "%s"' % stats_url) from ex
        except json.JSONDecodeError as ex:
            raise ConfigException('Server response from %s is not json' % stats_url) from ex

Here a few specific errors are caught and transformed into ConfigException:

  • A timeout on connection might occur (the server might be busy)
  • You may be unable to connect (maybe it's working on a different port?)
  • The received response is unexpected (maybe we connected to a different server, or got a text response instead of JSON?)

If any of these situations should occur, the web UI will display your error message like this:

../_images/demo_02_configuration_config_error.png

Note, that as in real scenarios, a many different errors types can occur. The server may respond with an HTTP 500 code. The server might respond with valid JSON, but it may not contain the requested data (random and counter keys), or the values may be filled with data that can't be converted into numbers. We leave the task of deciding which of these issues requires special attention and handling to you.