Skip to content

Commit

Permalink
implement "outcome" property for transactions and spans (elastic#899)
Browse files Browse the repository at this point in the history
  • Loading branch information
beniwohli authored and romulorosa committed Oct 15, 2020
1 parent 18bd083 commit c75b834
Show file tree
Hide file tree
Showing 29 changed files with 441 additions and 13 deletions.
49 changes: 49 additions & 0 deletions docs/api.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,55 @@ elasticapm.set_transaction_result('SUCCESS')
* `override`: if `True` (the default), overrides any previously set result.
If `False`, only sets the result if the result hasn't already been set.

[float]
[[api-set-transaction-outcome]]
==== `elasticapm.set_transaction_outcome()`

Sets the outcome of the transaction. The value can either be `"success"`, `"failure"` or `"unknown"`.
This should only be called at the end of a transaction after the outcome is determined.

The `outcome` is used for error rate calculations.
`success` denotes that a transaction has concluded successful, while `failure` indicates that the transaction failed
to finish successfully.
If the `outcome` is set to `unknown`, the transaction will not be included in error rate calculations.

For supported web frameworks, the transaction outcome is set automatically if it has not been set yet, based on the
HTTP status code.
A status code below `500` is considered a `success`, while any value of `500` or higher is counted as a `failure`.

If your transaction results in an HTTP response, you can alternatively provide the HTTP status code.

NOTE: While the `outcome` and `result` field look very similar, they serve different purposes.
Other than the `result` field, which canhold an arbitrary string value,
`outcome` is limited to three different values,
`"success"`, `"failure"` and `"unknown"`.
This allows the APM app to perform error rate calculations on these values.

Example:

[source,python]
----
import elasticapm
elasticapm.set_transaction_outcome("success")
# Using an HTTP status code
elasticapm.set_transaction_outcome(http_status_code=200)
# Using predefined constants:
from elasticapm.conf.constants import OUTCOME
elasticapm.set_transaction_outcome(OUTCOME.SUCCESS)
elasticapm.set_transaction_outcome(OUTCOME.FAILURE)
elasticapm.set_transaction_outcome(OUTCOME.UNKNOWN)
----

* `outcome`: One of `"success"`, `"failure"` or `"unknown"`. Can be omitted if `http_status_code` is provided.
* `http_status_code`: if the transaction represents an HTTP response, its status code can be provided to determine the `outcome` automatically.
* `override`: if `True` (the default), any previously set `outcome` will be overriden.
If `False`, the outcome will only be set if it was not set before.


[float]
[[api-get-transaction-id]]
Expand Down
1 change: 1 addition & 0 deletions elasticapm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
set_context,
set_custom_context,
set_transaction_name,
set_transaction_outcome,
set_transaction_result,
set_user_context,
tag,
Expand Down
5 changes: 5 additions & 0 deletions elasticapm/conf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import decimal
import re
from collections import namedtuple

EVENTS_API_PATH = "intake/v2/events"
AGENT_CONFIG_PATH = "config/v1/agents"
Expand Down Expand Up @@ -69,6 +70,10 @@
"sessionid",
]

OUTCOME = namedtuple("OUTCOME", ["SUCCESS", "FAILURE", "UNKNOWN"])(
SUCCESS="success", FAILURE="failure", UNKNOWN="unknown"
)

try:
# Python 2
LABEL_TYPES = (bool, int, long, float, decimal.Decimal)
Expand Down
2 changes: 2 additions & 0 deletions elasticapm/contrib/aiohttp/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ async def handle_request(request, handler):
try:
response = await handler(request)
elasticapm.set_transaction_result("HTTP {}xx".format(response.status // 100), override=False)
elasticapm.set_transaction_outcome(http_status_code=response.status, override=False)
elasticapm.set_context(
lambda: get_data_from_response(response, elasticapm_client.config, constants.TRANSACTION), "response"
)
Expand All @@ -82,6 +83,7 @@ async def handle_request(request, handler):
context={"request": get_data_from_request(request, elasticapm_client.config, constants.ERROR)}
)
elasticapm.set_transaction_result("HTTP 5xx", override=False)
elasticapm.set_transaction_outcome(http_status_code=500, override=False)
elasticapm.set_context({"status_code": 500}, "response")
# some exceptions are response-like, e.g. have headers and status code. Let's try and capture them
if isinstance(exc, (Response, HTTPException)):
Expand Down
14 changes: 12 additions & 2 deletions elasticapm/contrib/celery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE


from celery import signals
from celery import signals, states

import elasticapm
from elasticapm.conf import constants
from elasticapm.utils import get_name_from_func


Expand Down Expand Up @@ -61,7 +63,15 @@ def begin_transaction(*args, **kwargs):

def end_transaction(task_id, task, *args, **kwargs):
name = get_name_from_func(task)
client.end_transaction(name, kwargs.get("state", "None"))
state = kwargs.get("state", "None")
if state == states.SUCCESS:
outcome = constants.OUTCOME.SUCCESS
elif state in states.EXCEPTION_STATES:
outcome = constants.OUTCOME.FAILURE
else:
outcome = constants.OUTCOME.UNKNOWN
elasticapm.set_transaction_outcome(outcome, override=False)
client.end_transaction(name, state)

dispatch_uid = "elasticapm-tracing-%s"

Expand Down
1 change: 1 addition & 0 deletions elasticapm/contrib/django/middleware/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ def process_response(self, request, response):
)
elasticapm.set_context(lambda: self.client.get_user_info(request), "user")
elasticapm.set_transaction_result("HTTP {}xx".format(response.status_code // 100), override=False)
elasticapm.set_transaction_outcome(http_status_code=response.status_code, override=False)
except Exception:
self.client.error_logger.error("Exception during timing of request", exc_info=True)
return response
Expand Down
2 changes: 2 additions & 0 deletions elasticapm/contrib/flask/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,10 @@ def request_finished(self, app, response):
)
if response.status_code:
result = "HTTP {}xx".format(response.status_code // 100)
elasticapm.set_transaction_outcome(http_status_code=response.status_code, override=False)
else:
result = response.status
elasticapm.set_transaction_outcome(http_status_code=response.status, override=False)
elasticapm.set_transaction_result(result, override=False)
# Instead of calling end_transaction here, we defer the call until the response is closed.
# This ensures that we capture things that happen until the WSGI server closes the response.
Expand Down
2 changes: 2 additions & 0 deletions elasticapm/contrib/starlette/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,13 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -

try:
response = await call_next(request)
elasticapm.set_transaction_outcome(constants.OUTCOME.SUCCESS, override=False)
except Exception:
await self.capture_exception(
context={"request": await get_data_from_request(request, self.client.config, constants.ERROR)}
)
elasticapm.set_transaction_result("HTTP 5xx", override=False)
elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE, override=False)
elasticapm.set_context({"status_code": 500}, "response")

raise
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ async def call(self, module, method, wrapped, instance, args, kwargs):
headers = kwargs.get("headers") or {}
self._set_disttracing_headers(headers, trace_parent, transaction)
kwargs["headers"] = headers
return await wrapped(*args, **kwargs)
response = await wrapped(*args, **kwargs)
if response:
if span.context:
span.context["http"]["status_code"] = response.status
span.set_success() if response.status < 400 else span.set_failure()
return response

def mutate_unsampled_call_args(self, module, method, wrapped, instance, args, kwargs, transaction):
# since we don't have a span, we set the span id to the transaction id
Expand Down
10 changes: 8 additions & 2 deletions elasticapm/instrumentation/packages/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,11 @@ def call(self, module, method, wrapped, instance, args, kwargs):
span_subtype="http",
extra={"http": {"url": url}, "destination": destination},
leaf=True,
):
return wrapped(*args, **kwargs)
) as span:
response = wrapped(*args, **kwargs)
# requests.Response objects are falsy if status code > 400, so we have to check for None instead
if response is not None:
if span.context:
span.context["http"]["status_code"] = response.status_code
span.set_success() if response.status_code < 400 else span.set_failure()
return response
5 changes: 4 additions & 1 deletion elasticapm/instrumentation/packages/tornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ async def call(self, module, method, wrapped, instance, args, kwargs):
elasticapm.set_context(
lambda: get_data_from_response(instance, client.config, constants.TRANSACTION), "response"
)
result = "HTTP {}xx".format(instance.get_status() // 100)
status = instance.get_status()
result = "HTTP {}xx".format(status // 100)
elasticapm.set_transaction_result(result, override=False)
elasticapm.set_transaction_outcome(http_status_code=status)
client.end_transaction()

return ret
Expand Down Expand Up @@ -98,6 +100,7 @@ def call(self, module, method, wrapped, instance, args, kwargs):
client.capture_exception(
context={"request": get_data_from_request(instance, request, client.config, constants.ERROR)}
)
elasticapm.set_transaction_outcome(constants.OUTCOME.FAILURE)
if isinstance(e, HTTPError):
elasticapm.set_transaction_result("HTTP {}xx".format(int(e.status_code / 100)), override=False)
elasticapm.set_context({"status_code": e.status_code}, "response")
Expand Down
8 changes: 7 additions & 1 deletion elasticapm/instrumentation/packages/urllib.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,13 @@ def call(self, module, method, wrapped, instance, args, kwargs):
span_id=parent_id, trace_options=TracingOptions(recorded=True)
)
self._set_disttracing_headers(request_object, trace_parent, transaction)
return wrapped(*args, **kwargs)
response = wrapped(*args, **kwargs)
if response:
status = getattr(response, "status", None) or response.getcode() # Python 2 compat
if span.context:
span.context["http"]["status_code"] = status
span.set_success() if status < 400 else span.set_failure()
return response

def mutate_unsampled_call_args(self, module, method, wrapped, instance, args, kwargs, transaction):
request_object = args[1] if len(args) > 1 else kwargs["req"]
Expand Down
7 changes: 6 additions & 1 deletion elasticapm/instrumentation/packages/urllib3.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,12 @@ def call(self, module, method, wrapped, instance, args, kwargs):
span_id=parent_id, trace_options=TracingOptions(recorded=True)
)
self._set_disttracing_headers(headers, trace_parent, transaction)
return wrapped(*args, **kwargs)
response = wrapped(*args, **kwargs)
if response:
if span.context:
span.context["http"]["status_code"] = response.status
span.set_success() if response.status < 400 else span.set_failure()
return response

def mutate_unsampled_call_args(self, module, method, wrapped, instance, args, kwargs, transaction):
# since we don't have a span, we set the span id to the transaction id
Expand Down
Loading

0 comments on commit c75b834

Please sign in to comment.