Files
2023-04-23 22:55:05 +05:30

482 lines
16 KiB
Python

# 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 json
import os
import threading
from ast import literal_eval
from unittest import mock
import elasticsearch
import elasticsearch.exceptions
from elasticsearch import Elasticsearch
from elasticsearch_dsl import Search
import opentelemetry.instrumentation.elasticsearch
from opentelemetry import trace
from opentelemetry.instrumentation.elasticsearch import (
ElasticsearchInstrumentor,
)
from opentelemetry.instrumentation.elasticsearch.utils import sanitize_body
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.test.test_base import TestBase
from opentelemetry.trace import StatusCode
from . import sanitization_queries # pylint: disable=no-name-in-module
major_version = elasticsearch.VERSION[0]
if major_version == 7:
from . import helpers_es7 as helpers # pylint: disable=no-name-in-module
elif major_version == 6:
from . import helpers_es6 as helpers # pylint: disable=no-name-in-module
elif major_version == 5:
from . import helpers_es5 as helpers # pylint: disable=no-name-in-module
else:
from . import helpers_es2 as helpers # pylint: disable=no-name-in-module
Article = helpers.Article
@mock.patch(
"elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request"
)
class TestElasticsearchIntegration(TestBase):
search_attributes = {
SpanAttributes.DB_SYSTEM: "elasticsearch",
"elasticsearch.url": "/test-index/_search",
"elasticsearch.method": helpers.dsl_search_method,
"elasticsearch.target": "test-index",
SpanAttributes.DB_STATEMENT: str({"query": {"bool": {"filter": "?"}}}),
}
create_attributes = {
SpanAttributes.DB_SYSTEM: "elasticsearch",
"elasticsearch.url": "/test-index",
"elasticsearch.method": "HEAD",
}
def setUp(self):
super().setUp()
self.tracer = self.tracer_provider.get_tracer(__name__)
ElasticsearchInstrumentor().instrument()
def tearDown(self):
super().tearDown()
with self.disable_logging():
ElasticsearchInstrumentor().uninstrument()
def test_instrumentor(self, request_mock):
request_mock.return_value = (1, {}, {})
es = Elasticsearch()
es.index(index="sw", doc_type="_doc", id=1, body={"name": "adam"})
spans_list = self.get_finished_spans()
self.assertEqual(len(spans_list), 1)
span = spans_list[0]
# Check version and name in span's instrumentation info
# self.assertEqualSpanInstrumentationInfo(span, opentelemetry.instrumentation.elasticsearch)
self.assertEqualSpanInstrumentationInfo(
span, opentelemetry.instrumentation.elasticsearch
)
# check that no spans are generated after uninstrument
ElasticsearchInstrumentor().uninstrument()
es.index(index="sw", doc_type="_doc", id=1, body={"name": "adam"})
spans_list = self.get_finished_spans()
self.assertEqual(len(spans_list), 1)
def test_span_not_recording(self, request_mock):
request_mock.return_value = (1, {}, {})
mock_tracer = mock.Mock()
mock_span = mock.Mock()
mock_span.is_recording.return_value = False
mock_tracer.start_span.return_value = mock_span
with mock.patch("opentelemetry.trace.get_tracer") as tracer:
tracer.return_value = mock_tracer
Elasticsearch()
self.assertFalse(mock_span.is_recording())
self.assertTrue(mock_span.is_recording.called)
self.assertFalse(mock_span.set_attribute.called)
self.assertFalse(mock_span.set_status.called)
ElasticsearchInstrumentor().uninstrument()
def test_prefix_arg(self, request_mock):
prefix = "prefix-from-env"
ElasticsearchInstrumentor().uninstrument()
ElasticsearchInstrumentor(span_name_prefix=prefix).instrument()
request_mock.return_value = (1, {}, {})
self._test_prefix(prefix)
def test_prefix_env(self, request_mock):
prefix = "prefix-from-args"
env_var = "OTEL_PYTHON_ELASTICSEARCH_NAME_PREFIX"
os.environ[env_var] = prefix
ElasticsearchInstrumentor().uninstrument()
ElasticsearchInstrumentor().instrument()
request_mock.return_value = (1, {}, {})
del os.environ[env_var]
self._test_prefix(prefix)
def _test_prefix(self, prefix):
es = Elasticsearch()
es.index(index="sw", doc_type="_doc", id=1, body={"name": "adam"})
spans_list = self.get_finished_spans()
self.assertEqual(len(spans_list), 1)
span = spans_list[0]
self.assertTrue(span.name.startswith(prefix))
def test_result_values(self, request_mock):
request_mock.return_value = (
1,
{},
'{"found": false, "timed_out": true, "took": 7}',
)
es = Elasticsearch()
es.get(index="test-index", doc_type="_doc", id=1)
spans = self.get_finished_spans()
self.assertEqual(1, len(spans))
self.assertEqual(spans[0].name, "Elasticsearch/test-index/_doc/:id")
self.assertEqual("False", spans[0].attributes["elasticsearch.found"])
self.assertEqual(
"True", spans[0].attributes["elasticsearch.timed_out"]
)
self.assertEqual("7", spans[0].attributes["elasticsearch.took"])
def test_trace_error_unknown(self, request_mock):
exc = RuntimeError("custom error")
request_mock.side_effect = exc
self._test_trace_error(StatusCode.ERROR, exc)
def test_trace_error_not_found(self, request_mock):
msg = "record not found"
exc = elasticsearch.exceptions.NotFoundError(404, msg)
request_mock.return_value = (1, {}, {})
request_mock.side_effect = exc
self._test_trace_error(StatusCode.ERROR, exc)
def _test_trace_error(self, code, exc):
es = Elasticsearch()
try:
es.get(index="test-index", doc_type="_doc", id=1)
except Exception: # pylint: disable=broad-except
pass
spans = self.get_finished_spans()
self.assertEqual(1, len(spans))
span = spans[0]
self.assertFalse(span.status.is_ok)
self.assertEqual(span.status.status_code, code)
self.assertEqual(
span.status.description, f"{type(exc).__name__}: {exc}"
)
def test_parent(self, request_mock):
request_mock.return_value = (1, {}, {})
es = Elasticsearch()
with self.tracer.start_as_current_span("parent"):
es.index(index="sw", doc_type="_doc", id=1, body={"name": "adam"})
spans = self.get_finished_spans()
self.assertEqual(len(spans), 2)
parent = spans.by_name("parent")
child = spans.by_name("Elasticsearch/sw/_doc/:id")
self.assertIsNotNone(child.parent)
self.assertEqual(child.parent.span_id, parent.context.span_id)
def test_multithread(self, request_mock):
request_mock.return_value = (1, {}, {})
es = Elasticsearch()
ev = threading.Event()
# 1. Start tracing from thread-1; make thread-2 wait
# 2. Trace something from thread-2, make thread-1 join before finishing.
# 3. Check the spans got different parents, and are in the expected order.
def target1(parent_span):
with trace.use_span(parent_span):
es.get(index="test-index", doc_type="_doc", id=1)
ev.set()
ev.wait()
def target2():
ev.wait()
es.get(index="test-index", doc_type="_doc", id=2)
ev.set()
with self.tracer.start_as_current_span("parent") as span:
t1 = threading.Thread(target=target1, args=(span,))
t1.start()
t2 = threading.Thread(target=target2)
t2.start()
t1.join()
t2.join()
spans = self.get_finished_spans()
self.assertEqual(3, len(spans))
s1 = spans.by_name("parent")
s2 = spans.by_attr("elasticsearch.id", "1")
s3 = spans.by_attr("elasticsearch.id", "2")
self.assertIsNotNone(s2.parent)
self.assertEqual(s2.parent.span_id, s1.context.span_id)
self.assertIsNone(s3.parent)
def test_dsl_search(self, request_mock):
request_mock.return_value = (1, {}, '{"hits": {"hits": []}}')
client = Elasticsearch()
search = Search(using=client, index="test-index").filter(
"term", author="testing"
)
search.execute()
spans = self.get_finished_spans()
span = spans[0]
self.assertEqual(1, len(spans))
self.assertEqual(span.name, "Elasticsearch/<target>/_search")
self.assertIsNotNone(span.end_time)
self.assertEqual(
span.attributes,
self.search_attributes,
)
def test_dsl_search_sanitized(self, request_mock):
request_mock.return_value = (1, {}, '{"hits": {"hits": []}}')
client = Elasticsearch()
search = Search(using=client, index="test-index").filter(
"term", author="testing"
)
search.execute()
spans = self.get_finished_spans()
span = spans[0]
self.assertEqual(1, len(spans))
self.assertEqual(span.name, "Elasticsearch/<target>/_search")
self.assertIsNotNone(span.end_time)
self.assertEqual(
span.attributes,
self.search_attributes,
)
def test_dsl_create(self, request_mock):
request_mock.return_value = (1, {}, {})
client = Elasticsearch()
Article.init(using=client)
spans = self.get_finished_spans()
assert spans
self.assertEqual(2, len(spans))
span1 = spans.by_attr(key="elasticsearch.method", value="HEAD")
span2 = spans.by_attr(key="elasticsearch.method", value="PUT")
self.assertEqual(
span1.attributes,
self.create_attributes,
)
attributes = {
SpanAttributes.DB_SYSTEM: "elasticsearch",
"elasticsearch.url": "/test-index",
"elasticsearch.method": "PUT",
}
self.assertSpanHasAttributes(span2, attributes)
self.assertEqual(
literal_eval(span2.attributes[SpanAttributes.DB_STATEMENT]),
helpers.dsl_create_statement,
)
def test_dsl_create_sanitized(self, request_mock):
request_mock.return_value = (1, {}, {})
client = Elasticsearch()
Article.init(using=client)
spans = self.get_finished_spans()
assert spans
self.assertEqual(2, len(spans))
span = spans.by_attr(key="elasticsearch.method", value="HEAD")
self.assertEqual(
span.attributes,
self.create_attributes,
)
def test_dsl_index(self, request_mock):
request_mock.return_value = helpers.dsl_index_result
client = Elasticsearch()
article = Article(
meta={"id": 2},
title="About searching",
body="A few words here, a few words there",
)
res = article.save(using=client)
self.assertTrue(res)
spans = self.get_finished_spans()
self.assertEqual(1, len(spans))
span = spans[0]
self.assertEqual(span.name, helpers.dsl_index_span_name)
attributes = {
SpanAttributes.DB_SYSTEM: "elasticsearch",
"elasticsearch.url": helpers.dsl_index_url,
"elasticsearch.method": "PUT",
}
self.assertSpanHasAttributes(span, attributes)
self.assertEqual(
literal_eval(span.attributes[SpanAttributes.DB_STATEMENT]),
{
"body": "A few words here, a few words there",
"title": "About searching",
},
)
def test_request_hook(self, request_mock):
request_hook_method_attribute = "request_hook.method"
request_hook_url_attribute = "request_hook.url"
request_hook_kwargs_attribute = "request_hook.kwargs"
def request_hook(span, method, url, kwargs):
attributes = {
request_hook_method_attribute: method,
request_hook_url_attribute: url,
request_hook_kwargs_attribute: json.dumps(kwargs),
}
if span and span.is_recording():
span.set_attributes(attributes)
ElasticsearchInstrumentor().uninstrument()
ElasticsearchInstrumentor().instrument(request_hook=request_hook)
request_mock.return_value = (
1,
{},
'{"found": false, "timed_out": true, "took": 7}',
)
es = Elasticsearch()
index = "test-index"
doc_id = 1
kwargs = {"params": {"test": True}}
es.get(index=index, doc_type="_doc", id=doc_id, **kwargs)
spans = self.get_finished_spans()
self.assertEqual(1, len(spans))
self.assertEqual(
"GET", spans[0].attributes[request_hook_method_attribute]
)
self.assertEqual(
f"/{index}/_doc/{doc_id}",
spans[0].attributes[request_hook_url_attribute],
)
self.assertEqual(
json.dumps(kwargs),
spans[0].attributes[request_hook_kwargs_attribute],
)
def test_response_hook(self, request_mock):
response_attribute_name = "db.query_result"
def response_hook(span, response):
if span and span.is_recording():
span.set_attribute(
response_attribute_name, json.dumps(response)
)
ElasticsearchInstrumentor().uninstrument()
ElasticsearchInstrumentor().instrument(response_hook=response_hook)
response_payload = {
"took": 9,
"timed_out": False,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0,
},
"hits": {
"total": {"value": 1, "relation": "eq"},
"max_score": 0.18232156,
"hits": [
{
"_index": "test-index",
"_type": "_doc",
"_id": "1",
"_score": 0.18232156,
"_source": {"name": "tester"},
}
],
},
}
request_mock.return_value = (
1,
{},
json.dumps(response_payload),
)
es = Elasticsearch()
es.get(index="test-index", doc_type="_doc", id=1)
spans = self.get_finished_spans()
self.assertEqual(1, len(spans))
self.assertEqual(
json.dumps(response_payload),
spans[0].attributes[response_attribute_name],
)
def test_no_op_tracer_provider(self, request_mock):
ElasticsearchInstrumentor().uninstrument()
ElasticsearchInstrumentor().instrument(
tracer_provider=trace.NoOpTracerProvider()
)
response_payload = '{"found": false, "timed_out": true, "took": 7}'
request_mock.return_value = (
1,
{},
response_payload,
)
es = Elasticsearch()
res = es.get(index="test-index", doc_type="_doc", id=1)
self.assertEqual(
res.get("found"), json.loads(response_payload).get("found")
)
spans_list = self.get_finished_spans()
self.assertEqual(len(spans_list), 0)
def test_body_sanitization(self, _):
self.assertEqual(
sanitize_body(sanitization_queries.interval_query),
str(sanitization_queries.interval_query_sanitized),
)
self.assertEqual(
sanitize_body(sanitization_queries.match_query),
str(sanitization_queries.match_query_sanitized),
)
self.assertEqual(
sanitize_body(sanitization_queries.filter_query),
str(sanitization_queries.filter_query_sanitized),
)