Skip to content

Commit 113897a

Browse files
authored
Merge branch 'main' into revert-old-python-support-removal
2 parents e3936b7 + dee49af commit 113897a

File tree

20 files changed

+1260
-12
lines changed

20 files changed

+1260
-12
lines changed

.ci/.matrix_framework.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ FRAMEWORK:
1212
- flask-3.0
1313
- jinja2-3
1414
- opentelemetry-newest
15+
- opentracing-newest
1516
- twisted-newest
1617
- celery-5-flask-2
1718
- celery-5-django-4

.ci/.matrix_framework_fips.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ FRAMEWORK:
66
- flask-3.0
77
- jinja2-3
88
- opentelemetry-newest
9+
- opentracing-newest
910
- twisted-newest
1011
- celery-5-flask-2
1112
- celery-5-django-5

.ci/.matrix_framework_full.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ FRAMEWORK:
3030
- celery-5-django-4
3131
- celery-5-django-5
3232
- opentelemetry-newest
33+
- opentracing-newest
34+
- opentracing-2.0
3335
- twisted-newest
3436
- twisted-18
3537
- twisted-17

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ jobs:
135135

136136
- name: Extract metadata (tags, labels)
137137
id: docker-meta
138-
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
138+
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
139139
with:
140140
images: ${{ env.DOCKER_IMAGE_NAME }}
141141
tags: |

elasticapm/conf/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,15 @@ def setup_logging(handler):
883883
884884
For a typical Python install:
885885
886+
>>> from elasticapm.handlers.logging import LoggingHandler
887+
>>> client = ElasticAPM(...)
888+
>>> setup_logging(LoggingHandler(client))
889+
890+
Within Django:
891+
892+
>>> from elasticapm.contrib.django.handlers import LoggingHandler
893+
>>> setup_logging(LoggingHandler())
894+
886895
Returns a boolean based on if logging was configured or not.
887896
"""
888897
# TODO We should probably revisit this. Does it make more sense as

elasticapm/contrib/django/handlers.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,44 @@
3131

3232
from __future__ import absolute_import
3333

34+
import logging
3435
import sys
3536
import warnings
3637

3738
from django.conf import settings as django_settings
3839

40+
from elasticapm import get_client
41+
from elasticapm.handlers.logging import LoggingHandler as BaseLoggingHandler
42+
from elasticapm.utils.logging import get_logger
43+
44+
logger = get_logger("elasticapm.logging")
45+
46+
47+
class LoggingHandler(BaseLoggingHandler):
48+
def __init__(self, level=logging.NOTSET) -> None:
49+
warnings.warn(
50+
"The LoggingHandler is deprecated and will be removed in v7.0 of the agent. "
51+
"Please use `log_ecs_reformatting` and ship the logs with Elastic "
52+
"Agent or Filebeat instead. "
53+
"https://www.elastic.co/guide/en/apm/agent/python/current/logs.html",
54+
DeprecationWarning,
55+
)
56+
# skip initialization of BaseLoggingHandler
57+
logging.Handler.__init__(self, level=level)
58+
59+
@property
60+
def client(self):
61+
return get_client()
62+
63+
def _emit(self, record, **kwargs):
64+
from elasticapm.contrib.django.middleware import LogMiddleware
65+
66+
# Fetch the request from a threadlocal variable, if available
67+
request = getattr(LogMiddleware.thread, "request", None)
68+
request = getattr(record, "request", request)
69+
70+
return super(LoggingHandler, self)._emit(record, request=request, **kwargs)
71+
3972

4073
def exception_handler(client, request=None, **kwargs):
4174
def actually_do_stuff(request=None, **kwargs) -> None:

elasticapm/contrib/flask/__init__.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,19 @@
3131

3232
from __future__ import absolute_import
3333

34+
import logging
35+
import warnings
36+
3437
import flask
3538
from flask import request, signals
3639

3740
import elasticapm
3841
import elasticapm.instrumentation.control
3942
from elasticapm import get_client
4043
from elasticapm.base import Client
41-
from elasticapm.conf import constants
44+
from elasticapm.conf import constants, setup_logging
4245
from elasticapm.contrib.flask.utils import get_data_from_request, get_data_from_response
46+
from elasticapm.handlers.logging import LoggingHandler
4347
from elasticapm.traces import execution_context
4448
from elasticapm.utils import build_name_with_http_method_prefix
4549
from elasticapm.utils.disttracing import TraceParent
@@ -77,14 +81,17 @@ class ElasticAPM(object):
7781
>>> elasticapm.capture_message('hello, world!')
7882
"""
7983

80-
def __init__(self, app=None, client=None, client_cls=Client, **defaults) -> None:
84+
def __init__(self, app=None, client=None, client_cls=Client, logging=False, **defaults) -> None:
8185
self.app = app
86+
self.logging = logging
87+
if self.logging:
88+
warnings.warn(
89+
"Flask log shipping is deprecated. See the Flask docs for more info and alternatives.",
90+
DeprecationWarning,
91+
)
8292
self.client = client or get_client()
8393
self.client_cls = client_cls
8494

85-
if "logging" in defaults:
86-
raise ValueError("Flask log shipping has been removed, drop the ElasticAPM logging parameter")
87-
8895
if app:
8996
self.init_app(app, **defaults)
9097

@@ -120,6 +127,14 @@ def init_app(self, app, **defaults) -> None:
120127

121128
self.client = self.client_cls(config, **defaults)
122129

130+
# 0 is a valid log level (NOTSET), so we need to check explicitly for it
131+
if self.logging or self.logging is logging.NOTSET:
132+
if self.logging is not True:
133+
kwargs = {"level": self.logging}
134+
else:
135+
kwargs = {}
136+
setup_logging(LoggingHandler(self.client, **kwargs))
137+
123138
signals.got_request_exception.connect(self.handle_exception, sender=app, weak=False)
124139

125140
try:
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2019, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
32+
import warnings
33+
34+
from .span import OTSpan # noqa: F401
35+
from .tracer import Tracer # noqa: F401
36+
37+
warnings.warn(
38+
(
39+
"The OpenTracing bridge is deprecated and will be removed in the next major release. "
40+
"Please migrate to the OpenTelemetry bridge."
41+
),
42+
DeprecationWarning,
43+
)
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2019, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
from opentracing.span import Span as OTSpanBase
32+
from opentracing.span import SpanContext as OTSpanContextBase
33+
34+
from elasticapm import traces
35+
from elasticapm.utils import get_url_dict
36+
from elasticapm.utils.logging import get_logger
37+
38+
try:
39+
# opentracing-python 2.1+
40+
from opentracing import logs as ot_logs
41+
from opentracing import tags
42+
except ImportError:
43+
# opentracing-python <2.1
44+
from opentracing.ext import tags
45+
46+
ot_logs = None
47+
48+
49+
logger = get_logger("elasticapm.contrib.opentracing")
50+
51+
52+
class OTSpan(OTSpanBase):
53+
def __init__(self, tracer, context, elastic_apm_ref) -> None:
54+
super(OTSpan, self).__init__(tracer, context)
55+
self.elastic_apm_ref = elastic_apm_ref
56+
self.is_transaction = isinstance(elastic_apm_ref, traces.Transaction)
57+
self.is_dropped = isinstance(elastic_apm_ref, traces.DroppedSpan)
58+
if not context.span:
59+
context.span = self
60+
61+
def log_kv(self, key_values, timestamp=None):
62+
exc_type, exc_val, exc_tb = None, None, None
63+
if "python.exception.type" in key_values:
64+
exc_type = key_values["python.exception.type"]
65+
exc_val = key_values.get("python.exception.val")
66+
exc_tb = key_values.get("python.exception.tb")
67+
elif ot_logs and key_values.get(ot_logs.EVENT) == tags.ERROR:
68+
exc_type = key_values[ot_logs.ERROR_KIND]
69+
exc_val = key_values.get(ot_logs.ERROR_OBJECT)
70+
exc_tb = key_values.get(ot_logs.STACK)
71+
else:
72+
logger.debug("Can't handle non-exception type opentracing logs")
73+
if exc_type:
74+
agent = self.tracer._agent
75+
agent.capture_exception(exc_info=(exc_type, exc_val, exc_tb))
76+
return self
77+
78+
def set_operation_name(self, operation_name):
79+
self.elastic_apm_ref.name = operation_name
80+
return self
81+
82+
def set_tag(self, key, value):
83+
if self.is_transaction:
84+
if key == "type":
85+
self.elastic_apm_ref.transaction_type = value
86+
elif key == "result":
87+
self.elastic_apm_ref.result = value
88+
elif key == tags.HTTP_STATUS_CODE:
89+
self.elastic_apm_ref.result = "HTTP {}xx".format(str(value)[0])
90+
traces.set_context({"status_code": value}, "response")
91+
elif key == "user.id":
92+
traces.set_user_context(user_id=value)
93+
elif key == "user.username":
94+
traces.set_user_context(username=value)
95+
elif key == "user.email":
96+
traces.set_user_context(email=value)
97+
elif key == tags.HTTP_URL:
98+
traces.set_context({"url": get_url_dict(value)}, "request")
99+
elif key == tags.HTTP_METHOD:
100+
traces.set_context({"method": value}, "request")
101+
elif key == tags.COMPONENT:
102+
traces.set_context({"framework": {"name": value}}, "service")
103+
else:
104+
self.elastic_apm_ref.label(**{key: value})
105+
elif not self.is_dropped:
106+
if key.startswith("db."):
107+
span_context = self.elastic_apm_ref.context or {}
108+
if "db" not in span_context:
109+
span_context["db"] = {}
110+
if key == tags.DATABASE_STATEMENT:
111+
span_context["db"]["statement"] = value
112+
elif key == tags.DATABASE_USER:
113+
span_context["db"]["user"] = value
114+
elif key == tags.DATABASE_TYPE:
115+
span_context["db"]["type"] = value
116+
self.elastic_apm_ref.type = "db." + value
117+
else:
118+
self.elastic_apm_ref.label(**{key: value})
119+
self.elastic_apm_ref.context = span_context
120+
elif key == tags.SPAN_KIND:
121+
self.elastic_apm_ref.type = value
122+
else:
123+
self.elastic_apm_ref.label(**{key: value})
124+
return self
125+
126+
def finish(self, finish_time=None) -> None:
127+
if self.is_transaction:
128+
self.tracer._agent.end_transaction()
129+
elif not self.is_dropped:
130+
self.elastic_apm_ref.transaction.end_span()
131+
132+
133+
class OTSpanContext(OTSpanContextBase):
134+
def __init__(self, trace_parent, span=None) -> None:
135+
self.trace_parent = trace_parent
136+
self.span = span

0 commit comments

Comments
 (0)