Skip to content

Commit

Permalink
Add safety prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
ocelotl committed Oct 28, 2021
1 parent 05e9fe1 commit 6841423
Show file tree
Hide file tree
Showing 15 changed files with 498 additions and 0 deletions.
3 changes: 3 additions & 0 deletions safety_prototype/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__pycache__
*.egg
*.egg-info
64 changes: 64 additions & 0 deletions safety_prototype/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
README
======

Overview
--------

This is a prototype to implement `error handling`_ in Python.

The user only calls API functions or methods, never SDK functions or methods.

Every SDK must implement every public function or method defined in the API
except for the SDK setting function.

A safety mechanism is applied to every API function or method (except for the
SDK setting function or method) and only to every API function or method.

This safety mechanism has 3 main components:

1. A predefined value to be returned in case of an exception being raised. This
value is predefined and independently set for every function or method.
2. A `try` / `except` block that catches any exception raised when executing
the function or method code.
3. A Python `warning`_ that is "raised" if an exception is raised in the code
protected by the safety mechanism.

The API provides a function to set a specific SDK. This function is
intentionally not protected by the safety mechanism mentioned above because the
specification `mentions`_ this:

The API or SDK may fail fast and cause the application to fail on
initialization...

*Initialization* is understood as the process of setting the SDK.

When an API function or method is called without an SDK being set, a warning
will be raised and the predefined value of `None` will be returned.

After an SDK is set, calling an API function or method will call its
corresponding SDJ function or method. Any exception raised by the SDK function
or method will be caught by the safety mechanism and the predefined value
returned instead.

The Python warning that is "raised" when an exception is raised in the SDK
function or method can be transformed into a full exception by running the
Python interpreter with the `-W error` option. This Python feature is used to
satisfy this `specification requirement`_.

How to run
----------

0. Create a virtual environment and activate it
1. Run ``pip install -e opentelemetry-api``
2. Run ``pip install -e opentelemetry-sdk``
3. Run ``python application.py``
4. Run ``python -W error application.py``

Noice how even failed operations (divisions by zero) don't crash the
application in step 3, but they do in step 4.


.. _error handling: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/error-handling.md
.. _warning: https://docs.python.org/3/library/warnings.html
.. _specification requirement: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/error-handling.md#configuring-error-handlers
.. _mentions: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/error-handling.md#basic-error-handling-principles
35 changes: 35 additions & 0 deletions safety_prototype/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Only API objects are imported because the user only calls API functions or
# methods.
from opentelemetry.configuration import set_sdk
from opentelemetry.trace import function, Class0

# This is the function that sets the SDK. After this is set, any call to an API
# function or method will end up calling its corresponding SDK function or
# method.
set_sdk("sdk")

# function returns the result of dividing its first argument by its second
# argument.

# This does not raise an exception, the resulting value of 2.0 is returned.
print(function(4, 2))

# This is a division by zero, it raises an exception, the safety mechanism
# catches it and returns the predefined value of 0.0.
print(function(1, 0))


# The class argument is stored in the SDK instance and method uses it to
# multiply the result of the division of it first argument by the second before
# returning the resulting value.
class0 = Class0(2)

# Class0.method returns the result of dividing its first argument by its second
# argument.

# This does not raise an exception, the resulting value of 4.0 is returned.
print(class0.method(4, 2))

# This is a division by zero, it raises an exception, the safety mechanism
# catches it and returns the predefined value of 0.0.
print(class0.method(1, 0))
45 changes: 45 additions & 0 deletions safety_prototype/opentelemetry-api/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
[metadata]
name = opentelemetry-api
description = OpenTelemetry Python API
author = OpenTelemetry Authors
author_email = [email protected]
url = https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-api
platforms = any
license = Apache-2.0
classifiers =
Development Status :: 5 - Production/Stable
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Typing :: Typed

[options]
python_requires = >=3.6
package_dir=
=src
packages=find_namespace:
zip_safe = False
include_package_data = True

[options.packages.find]
where = src
27 changes: 27 additions & 0 deletions safety_prototype/opentelemetry-api/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

import setuptools

BASE_DIR = os.path.dirname(__file__)
VERSION_FILENAME = os.path.join(BASE_DIR, "src", "opentelemetry", "version.py")
PACKAGE_INFO = {}
with open(VERSION_FILENAME, encoding="utf-8") as f:
exec(f.read(), PACKAGE_INFO)

setuptools.setup(
version=PACKAGE_INFO["__version__"],
)
50 changes: 50 additions & 0 deletions safety_prototype/opentelemetry-api/src/opentelemetry/_safety.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from functools import wraps
from warnings import warn, resetwarnings
from traceback import format_exception
from sys import exc_info


def _safe_function(predefined_return_value):
"""
This is the safety mechanism mentioned in the README file.
This is used as a decorator on every function or method in the API.
"""


def internal(function):
@wraps(function)
def wrapper(*args, **kwargs):

# This is the try / except block mentioned in the README file.
try:
exception = None
return function(*args, **kwargs)
except Exception: # pylint: disable=broad-except
exception = "".join(format_exception(*exc_info()))

if exception is not None:
# This is the warning mentioned in the README file.
warn(f"OpenTelemetry handled an exception:\n\n{exception}")
exception = None
resetwarnings()

return predefined_return_value

return wrapper

return internal
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from pkg_resources import iter_entry_points
from importlib import import_module

_sdk = None


def set_sdk(sdk_name: str) -> None:
"""
This is the SDK setting function mentioned in the README file.
It is intentionally not protected to make it possible for the application
to fail fast if it was not possible to set the SDK. In an actual
implementation of this prototype, the SDK setting mechanism may take
different forms to take into consideration that this SDK setting process
may happen also in auto instrumentation. This means that SDK setting may
also happen by using environment variables or other form of configuration.
"""
global _sdk

if _sdk is None:
_sdk = next(
iter_entry_points("opentelemetry_sdk", name=sdk_name)
).load().__package__


def _get_sdk_module(path: str) -> object:
return import_module(".".join([_sdk, path]))
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This an implementation of the API where every function or method is safe.
Any call to a function or method defined here passes its arguments to a
corresponding underlying SDK function or method. In this way, objects defined
in this module act as proxies and (with the SDK setting function) are the only
objects the user has contact with.
"""

from opentelemetry._safety import _safe_function
from opentelemetry.configuration import _get_sdk_module
from opentelemetry.trace.api import Class0


@_safe_function(0.0)
def function(a: int, b: int) -> float:
return _get_sdk_module("trace").function(a, b)


class Class0(Class0):

@_safe_function(None)
def __init__(self, a: int) -> None:
self._sdk_instance = _get_sdk_module("trace").Class0(a)

@_safe_function(0.0)
def method(self, a: int, b: int) -> float:
return self._sdk_instance.method(a, b)
44 changes: 44 additions & 0 deletions safety_prototype/opentelemetry-api/src/opentelemetry/trace/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
This is the API.
This module includes all functions and classes with their respective methods
that an SDK must implement. SDKs must implement the contents of this module
exactly, with the same names and signatures for functions and methods. This is
necessary because the API proxy objects will call the SDK functions and methods
in the exact same way as they are defined here.
"""

from abc import ABC, abstractmethod


# There is no way to mandate the implementation of a function in a Python
# module, so this is added to inform SDK implementations that this function is
# to be implemented. A mechanism that checks SDK compliance can be implemented
# as well and it can use the contents of this module to check SDKs.
def function(a: int, b: int) -> float:
pass


class Class0(ABC):

@abstractmethod
def __init__(self, a: int) -> None:
pass

@abstractmethod
def method(self, a: int, b: int) -> float:
pass
15 changes: 15 additions & 0 deletions safety_prototype/opentelemetry-api/src/opentelemetry/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

__version__ = "1.6.2"
Loading

0 comments on commit 6841423

Please sign in to comment.