diff --git a/Makefile b/Makefile index 0e895965..f2ace2a3 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ test-integration: cd ${TEST_DIR}/gunicorn; HT_LOG_LEVEL=${LOG_LEVEL} tox -e ${PY_TARGET} cd ${TEST_DIR}/requests; HT_LOG_LEVEL=${LOG_LEVEL} tox -e ${PY_TARGET} cd ${TEST_DIR}/aiohttp; HT_LOG_LEVEL=${LOG_LEVEL} tox -e ${PY_TARGET} - cd ${TEST_DIR}/docker; HT_LOG_LEVEL=${LOG_LEVEL} tox -e ${PY_TARGET} + cd ${TEST_DIR}/autoinstrumentation; HT_LOG_LEVEL=${LOG_LEVEL} tox -e ${PY_TARGET} .PHONY: test test: test-unit test-integration diff --git a/README.md b/README.md index e3d2b67f..6bcbe1f9 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,22 @@ from hypertrace.agent import Agent ... agent = Agent() # initialize the agent -agent.registerFlaskApp(app) # instrument a flask application -agent.registerMySQL() # instrument the MySQL client +agent.register_flask_app(app) # instrument a flask application +agent.register_mysql() # instrument the MySQL client ... ``` +or + +- Use the autoinstrumentation CLI (without any modification to application code) +``` +HT_INSTRUMENTED_MODULES=flask,mysql +hypertrace-instrument python app.py +``` +By default, all supported modules are instrumented. + +*Important:* do not attempt to instantiate a hypertrace.agent.Agent object while using hypertrace-instrument. + For further examples, check our [examples section](./examples) ### Configuration diff --git a/dev-requirements.txt b/dev-requirements.txt index fa765703..6d8fd014 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,21 +1,21 @@ -opentelemetry-api==1.1.0 -opentelemetry-exporter-otlp==1.1.0 -opentelemetry-exporter-zipkin==1.1.0 -opentelemetry-instrumentation==0.20b0 -opentelemetry-instrumentation-aiohttp-client==0.20b0 -opentelemetry-instrumentation-wsgi==0.20b0 -opentelemetry-instrumentation-flask==0.20b0 -opentelemetry-instrumentation-mysql==0.20b0 -opentelemetry-instrumentation-psycopg2==0.20b0 -opentelemetry-instrumentation-requests==0.20b0 -opentelemetry-instrumentation-grpc==0.20b0 -opentelemetry-propagator-b3==1.1.0 -opentelemetry-sdk==1.1.0 -opentelemetry-util-http==0.20b0 -google==3.0.0 +opentelemetry-api>=1.1.0 +opentelemetry-exporter-otlp>=1.1.0 +opentelemetry-exporter-zipkin>=1.1.0 +opentelemetry-instrumentation>=0.20b0 +opentelemetry-instrumentation-aiohttp-client>=0.20b0 +opentelemetry-instrumentation-wsgi>=0.20b0 +opentelemetry-instrumentation-flask>=0.20b0 +opentelemetry-instrumentation-mysql>=0.20b0 +opentelemetry-instrumentation-psycopg2>=0.20b0 +opentelemetry-instrumentation-requests>=0.20b0 +opentelemetry-instrumentation-grpc>=0.20b0 +opentelemetry-propagator-b3>=1.1.0 +opentelemetry-sdk>=1.1.0 +opentelemetry-util-http>=0.20b0 +google>=3.0.0 pyyaml -pytest==6.2.3 -protobuf==3.15.8 -tox==3.23.0 -pylint==2.7.4 -pdoc3==0.9.2 +pytest>=6.2.3 +protobuf>=3.15.8 +tox>=3.23.0 +pylint>=2.7.4 +pdoc3>=0.9.2 diff --git a/docs/hypertrace/agent/autoinstrumentation/hypertrace_instrument.html b/docs/hypertrace/agent/autoinstrumentation/hypertrace_instrument.html new file mode 100644 index 00000000..eec8a47f --- /dev/null +++ b/docs/hypertrace/agent/autoinstrumentation/hypertrace_instrument.html @@ -0,0 +1,247 @@ + + + + + + +hypertrace.agent.autoinstrumentation.hypertrace_instrument API documentation + + + + + + + + + + + +
+
+
+

Module hypertrace.agent.autoinstrumentation.hypertrace_instrument

+
+
+

This module implements a CLI command that be used to +autoinstrument existing pythong programs that use supported +modules.

+
+ +Expand source code + +
# Based upon the OTel autoinstrumentation feature
+'''This module implements a CLI command that be used to
+autoinstrument existing pythong programs that use supported
+modules.'''
+import argparse
+from logging import getLogger
+from os import environ, execl, getcwd
+from os.path import abspath, dirname, pathsep
+from shutil import which
+
+logger = getLogger(__file__)
+
+def parse_args():
+    '''Parse CLI arguments.'''
+    parser = argparse.ArgumentParser(
+        description="""
+        hypertrace-instrument automatically instruments a Python
+        program and runs the program
+        """
+    )
+
+    parser.add_argument("command", help="Your Python application.")
+
+    parser.add_argument(
+        "command_args",
+        help="Arguments for your application.",
+        nargs=argparse.REMAINDER,
+    )
+
+    return parser.parse_args()
+
+def update_python_path() -> None:
+    '''Retrieve existing PYTHONPATH'''
+    python_path = environ.get("PYTHONPATH")
+
+    # Split the paths
+    if not python_path:
+        python_path = []
+    else:
+        python_path = python_path.split(pathsep)
+
+    # Get the current working directory
+    cwd_path = getcwd()
+
+    # If this directory is already in python_path, remove it.
+    python_path = [path for path in python_path if path != cwd_path]
+
+    # If cwd is not in the PYTHONPATH, add it to the front.
+    if cwd_path not in python_path:
+        python_path.insert(0, cwd_path)
+
+    # What is the directory containing this python file?
+    filedir_path = dirname(abspath(__file__))
+
+    # If this directory is already in python_path, remove it.
+    python_path = [path for path in python_path if path != filedir_path]
+
+    # If this diretory is not in python_path, add it to the front
+    python_path.insert(0, filedir_path)
+
+    # Reset PYTHONPATH environment variable
+    environ["PYTHONPATH"] = pathsep.join(python_path)
+
+def run() -> None:
+    '''hypertrace-instrument Entry point'''
+    args = parse_args()
+
+    # update PYTHONPATH env var
+    update_python_path()
+
+    # Get full path to the command that was passed in as an
+    # argument
+    executable = which(args.command)
+
+    # Execute the app
+    execl(executable, executable, *args.command_args)
+
+if __name__ == '__main__':
+    run()
+
+
+
+
+
+
+
+

Functions

+
+
+def parse_args() +
+
+

Parse CLI arguments.

+
+ +Expand source code + +
def parse_args():
+    '''Parse CLI arguments.'''
+    parser = argparse.ArgumentParser(
+        description="""
+        hypertrace-instrument automatically instruments a Python
+        program and runs the program
+        """
+    )
+
+    parser.add_argument("command", help="Your Python application.")
+
+    parser.add_argument(
+        "command_args",
+        help="Arguments for your application.",
+        nargs=argparse.REMAINDER,
+    )
+
+    return parser.parse_args()
+
+
+
+def run() ‑> NoneType +
+
+

hypertrace-instrument Entry point

+
+ +Expand source code + +
def run() -> None:
+    '''hypertrace-instrument Entry point'''
+    args = parse_args()
+
+    # update PYTHONPATH env var
+    update_python_path()
+
+    # Get full path to the command that was passed in as an
+    # argument
+    executable = which(args.command)
+
+    # Execute the app
+    execl(executable, executable, *args.command_args)
+
+
+
+def update_python_path() ‑> NoneType +
+
+

Retrieve existing PYTHONPATH

+
+ +Expand source code + +
def update_python_path() -> None:
+    '''Retrieve existing PYTHONPATH'''
+    python_path = environ.get("PYTHONPATH")
+
+    # Split the paths
+    if not python_path:
+        python_path = []
+    else:
+        python_path = python_path.split(pathsep)
+
+    # Get the current working directory
+    cwd_path = getcwd()
+
+    # If this directory is already in python_path, remove it.
+    python_path = [path for path in python_path if path != cwd_path]
+
+    # If cwd is not in the PYTHONPATH, add it to the front.
+    if cwd_path not in python_path:
+        python_path.insert(0, cwd_path)
+
+    # What is the directory containing this python file?
+    filedir_path = dirname(abspath(__file__))
+
+    # If this directory is already in python_path, remove it.
+    python_path = [path for path in python_path if path != filedir_path]
+
+    # If this diretory is not in python_path, add it to the front
+    python_path.insert(0, filedir_path)
+
+    # Reset PYTHONPATH environment variable
+    environ["PYTHONPATH"] = pathsep.join(python_path)
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/hypertrace/agent/autoinstrumentation/index.html b/docs/hypertrace/agent/autoinstrumentation/index.html new file mode 100644 index 00000000..4d64a44a --- /dev/null +++ b/docs/hypertrace/agent/autoinstrumentation/index.html @@ -0,0 +1,72 @@ + + + + + + +hypertrace.agent.autoinstrumentation API documentation + + + + + + + + + + + +
+
+
+

Module hypertrace.agent.autoinstrumentation

+
+
+
+
+

Sub-modules

+
+
hypertrace.agent.autoinstrumentation.hypertrace_instrument
+
+

This module implements a CLI command that be used to +autoinstrument existing pythong programs that use supported +modules.

+
+
hypertrace.agent.autoinstrumentation.sitecustomize
+
+

Enable instrumentationon all supported modules.

+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/hypertrace/agent/autoinstrumentation/sitecustomize.html b/docs/hypertrace/agent/autoinstrumentation/sitecustomize.html new file mode 100644 index 00000000..89d1906d --- /dev/null +++ b/docs/hypertrace/agent/autoinstrumentation/sitecustomize.html @@ -0,0 +1,113 @@ + + + + + + +hypertrace.agent.autoinstrumentation.sitecustomize API documentation + + + + + + + + + + + +
+
+
+

Module hypertrace.agent.autoinstrumentation.sitecustomize

+
+
+

Enable instrumentationon all supported modules.

+
+ +Expand source code + +
'''Enable instrumentationon all supported modules.''' # pylint: disable=R0401
+import os
+import logging
+from hypertrace.agent import Agent
+
+DEFAULTS = [
+  'flask',
+  'mysql',
+  'postgresql',
+  'grpc:server',
+  'grpc:client',
+  'requests',
+  'aiohttp-client'
+]
+
+# Initialize logger
+logger = logging.getLogger(__name__)  # pylint: disable=C0103
+
+MODULES = ''
+if 'HT_INSTRUMENTED_MODULES' in os.environ:
+    logger.debug("[env] Loaded HT_INSTRUMENTED_MODULES from env")
+    MODULES = os.environ['HT_INSTRUMENTED_MODULES']
+    if len(MODULES) > 0:
+        MODULES = MODULES.replace(' ', '')
+
+if len(MODULES) > 0:
+    modules_array = MODULES.split(',')
+else:
+    modules_array = DEFAULTS
+
+# Create Hypertrace agent
+agent = Agent()
+
+# Initialize desired instrumentation wrappers
+for mod in modules_array:
+    if mod is None or len(mod) == 0:
+        continue
+
+    if mod == 'flask':
+        agent.register_flask_app()
+    elif mod == 'grpc:server':
+        agent.register_grpc_server()
+    elif mod == 'grpc:client':
+        agent.register_grpc_client()
+    elif mod == 'mysql':
+        agent.register_mysql()
+    elif mod == 'postgresql':
+        agent.register_postgresql()
+    elif mod == 'requests':
+        agent.register_requests()
+    elif mod == 'aiohttp-client':
+        agent.register_aiohttp_client()
+    else:
+        logger.error('Unknown module name: %s', mod)
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/docs/hypertrace/agent/config/config_pb2.html b/docs/hypertrace/agent/config/config_pb2.html index 5dbed03b..34c053a1 100644 --- a/docs/hypertrace/agent/config/config_pb2.html +++ b/docs/hypertrace/agent/config/config_pb2.html @@ -5,7 +5,7 @@ hypertrace.agent.config.config_pb2 API documentation - + @@ -22,15 +22,15 @@

Module hypertrace.agent.config.config_pb2

-

Generated protocol buffer code.

Expand source code -
# -*- coding: utf-8 -*-
-# Generated by the protocol buffer compiler.  DO NOT EDIT!
+
# Generated by the protocol buffer compiler.  DO NOT EDIT!
 # source: config.proto
-"""Generated protocol buffer code."""
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
 from google.protobuf.internal import enum_type_wrapper
 from google.protobuf import descriptor as _descriptor
 from google.protobuf import message as _message
@@ -48,9 +48,8 @@ 

Module hypertrace.agent.config.config_pb2

name='config.proto', package='org.hypertrace.agent.config', syntax='proto3', - serialized_options=b'\n\033org.hypertrace.agent.configZ$github.com/hypertrace/goagent/config', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x0c\x63onfig.proto\x12\x1borg.hypertrace.agent.config\x1a\x1egoogle/protobuf/wrappers.proto\"\x8b\x04\n\x0b\x41gentConfig\x12\x32\n\x0cservice_name\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x39\n\treporting\x18\x02 \x01(\x0b\x32&.org.hypertrace.agent.config.Reporting\x12>\n\x0c\x64\x61ta_capture\x18\x03 \x01(\x0b\x32(.org.hypertrace.agent.config.DataCapture\x12K\n\x13propagation_formats\x18\x04 \x03(\x0e\x32..org.hypertrace.agent.config.PropagationFormat\x12+\n\x07\x65nabled\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x39\n\tjavaagent\x18\x06 \x01(\x0b\x32&.org.hypertrace.agent.config.JavaAgent\x12]\n\x13resource_attributes\x18\x07 \x03(\x0b\x32@.org.hypertrace.agent.config.AgentConfig.ResourceAttributesEntry\x1a\x39\n\x17ResourceAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x90\x02\n\tReporting\x12.\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x06secure\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12+\n\x05token\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x03opa\x18\x04 \x01(\x0b\x32 .org.hypertrace.agent.config.Opa\x12K\n\x13trace_reporter_type\x18\x05 \x01(\x0e\x32..org.hypertrace.agent.config.TraceReporterType\"\x9c\x01\n\x03Opa\x12.\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x38\n\x13poll_period_seconds\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12+\n\x07\x65nabled\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\"d\n\x07Message\x12+\n\x07request\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12,\n\x08response\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\"\xb0\x02\n\x0b\x44\x61taCapture\x12:\n\x0chttp_headers\x18\x01 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x37\n\thttp_body\x18\x02 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12:\n\x0crpc_metadata\x18\x03 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x36\n\x08rpc_body\x18\x04 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x38\n\x13\x62ody_max_size_bytes\x18\x05 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\"C\n\tJavaAgent\x12\x36\n\x10\x66ilter_jar_paths\x18\x01 \x03(\x0b\x32\x1c.google.protobuf.StringValue*-\n\x11PropagationFormat\x12\x06\n\x02\x42\x33\x10\x00\x12\x10\n\x0cTRACECONTEXT\x10\x01*:\n\x11TraceReporterType\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\n\n\x06ZIPKIN\x10\x01\x12\x08\n\x04OTLP\x10\x02\x42\x43\n\x1borg.hypertrace.agent.configZ$github.com/hypertrace/goagent/configb\x06proto3' + serialized_options=_b('\n\033org.hypertrace.agent.configZ$github.com/hypertrace/goagent/config'), + serialized_pb=_b('\n\x0c\x63onfig.proto\x12\x1borg.hypertrace.agent.config\x1a\x1egoogle/protobuf/wrappers.proto\"\x8b\x04\n\x0b\x41gentConfig\x12\x32\n\x0cservice_name\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x39\n\treporting\x18\x02 \x01(\x0b\x32&.org.hypertrace.agent.config.Reporting\x12>\n\x0c\x64\x61ta_capture\x18\x03 \x01(\x0b\x32(.org.hypertrace.agent.config.DataCapture\x12K\n\x13propagation_formats\x18\x04 \x03(\x0e\x32..org.hypertrace.agent.config.PropagationFormat\x12+\n\x07\x65nabled\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x39\n\tjavaagent\x18\x06 \x01(\x0b\x32&.org.hypertrace.agent.config.JavaAgent\x12]\n\x13resource_attributes\x18\x07 \x03(\x0b\x32@.org.hypertrace.agent.config.AgentConfig.ResourceAttributesEntry\x1a\x39\n\x17ResourceAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x90\x02\n\tReporting\x12.\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x06secure\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12+\n\x05token\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x03opa\x18\x04 \x01(\x0b\x32 .org.hypertrace.agent.config.Opa\x12K\n\x13trace_reporter_type\x18\x05 \x01(\x0e\x32..org.hypertrace.agent.config.TraceReporterType\"\x9c\x01\n\x03Opa\x12.\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x38\n\x13poll_period_seconds\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12+\n\x07\x65nabled\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\"d\n\x07Message\x12+\n\x07request\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12,\n\x08response\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\"\xb0\x02\n\x0b\x44\x61taCapture\x12:\n\x0chttp_headers\x18\x01 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x37\n\thttp_body\x18\x02 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12:\n\x0crpc_metadata\x18\x03 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x36\n\x08rpc_body\x18\x04 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x38\n\x13\x62ody_max_size_bytes\x18\x05 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\"C\n\tJavaAgent\x12\x36\n\x10\x66ilter_jar_paths\x18\x01 \x03(\x0b\x32\x1c.google.protobuf.StringValue*-\n\x11PropagationFormat\x12\x06\n\x02\x42\x33\x10\x00\x12\x10\n\x0cTRACECONTEXT\x10\x01*:\n\x11TraceReporterType\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\n\n\x06ZIPKIN\x10\x01\x12\x08\n\x04OTLP\x10\x02\x42\x43\n\x1borg.hypertrace.agent.configZ$github.com/hypertrace/goagent/configb\x06proto3') , dependencies=[google_dot_protobuf_dot_wrappers__pb2.DESCRIPTOR,]) @@ -59,18 +58,15 @@

Module hypertrace.agent.config.config_pb2

full_name='org.hypertrace.agent.config.PropagationFormat', filename=None, file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='B3', index=0, number=0, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='TRACECONTEXT', index=1, number=1, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), ], containing_type=None, serialized_options=None, @@ -85,23 +81,19 @@

Module hypertrace.agent.config.config_pb2

full_name='org.hypertrace.agent.config.TraceReporterType', filename=None, file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='UNSPECIFIED', index=0, number=0, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='ZIPKIN', index=1, number=1, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='OTLP', index=2, number=2, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), ], containing_type=None, serialized_options=None, @@ -125,29 +117,28 @@

Module hypertrace.agent.config.config_pb2

filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='key', full_name='org.hypertrace.agent.config.AgentConfig.ResourceAttributesEntry.key', index=0, number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), + has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='value', full_name='org.hypertrace.agent.config.AgentConfig.ResourceAttributesEntry.value', index=1, number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), + has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], nested_types=[], enum_types=[ ], - serialized_options=b'8\001', + serialized_options=_b('8\001'), is_extendable=False, syntax='proto3', extension_ranges=[], @@ -163,7 +154,6 @@

Module hypertrace.agent.config.config_pb2

filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='service_name', full_name='org.hypertrace.agent.config.AgentConfig.service_name', index=0, @@ -171,49 +161,49 @@

Module hypertrace.agent.config.config_pb2

has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='reporting', full_name='org.hypertrace.agent.config.AgentConfig.reporting', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='data_capture', full_name='org.hypertrace.agent.config.AgentConfig.data_capture', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='propagation_formats', full_name='org.hypertrace.agent.config.AgentConfig.propagation_formats', index=3, number=4, type=14, cpp_type=8, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='enabled', full_name='org.hypertrace.agent.config.AgentConfig.enabled', index=4, number=5, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='javaagent', full_name='org.hypertrace.agent.config.AgentConfig.javaagent', index=5, number=6, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='resource_attributes', full_name='org.hypertrace.agent.config.AgentConfig.resource_attributes', index=6, number=7, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -237,7 +227,6 @@

Module hypertrace.agent.config.config_pb2

filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='endpoint', full_name='org.hypertrace.agent.config.Reporting.endpoint', index=0, @@ -245,35 +234,35 @@

Module hypertrace.agent.config.config_pb2

has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='secure', full_name='org.hypertrace.agent.config.Reporting.secure', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='token', full_name='org.hypertrace.agent.config.Reporting.token', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='opa', full_name='org.hypertrace.agent.config.Reporting.opa', index=3, number=4, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='trace_reporter_type', full_name='org.hypertrace.agent.config.Reporting.trace_reporter_type', index=4, number=5, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -297,7 +286,6 @@

Module hypertrace.agent.config.config_pb2

filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='endpoint', full_name='org.hypertrace.agent.config.Opa.endpoint', index=0, @@ -305,21 +293,21 @@

Module hypertrace.agent.config.config_pb2

has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='poll_period_seconds', full_name='org.hypertrace.agent.config.Opa.poll_period_seconds', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='enabled', full_name='org.hypertrace.agent.config.Opa.enabled', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -343,7 +331,6 @@

Module hypertrace.agent.config.config_pb2

filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='request', full_name='org.hypertrace.agent.config.Message.request', index=0, @@ -351,14 +338,14 @@

Module hypertrace.agent.config.config_pb2

has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='response', full_name='org.hypertrace.agent.config.Message.response', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -382,7 +369,6 @@

Module hypertrace.agent.config.config_pb2

filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='http_headers', full_name='org.hypertrace.agent.config.DataCapture.http_headers', index=0, @@ -390,35 +376,35 @@

Module hypertrace.agent.config.config_pb2

has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='http_body', full_name='org.hypertrace.agent.config.DataCapture.http_body', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='rpc_metadata', full_name='org.hypertrace.agent.config.DataCapture.rpc_metadata', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='rpc_body', full_name='org.hypertrace.agent.config.DataCapture.rpc_body', index=3, number=4, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='body_max_size_bytes', full_name='org.hypertrace.agent.config.DataCapture.body_max_size_bytes', index=4, number=5, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -442,7 +428,6 @@

Module hypertrace.agent.config.config_pb2

filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='filter_jar_paths', full_name='org.hypertrace.agent.config.JavaAgent.filter_jar_paths', index=0, @@ -450,7 +435,7 @@

Module hypertrace.agent.config.config_pb2

has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -501,54 +486,54 @@

Module hypertrace.agent.config.config_pb2

DESCRIPTOR.enum_types_by_name['TraceReporterType'] = _TRACEREPORTERTYPE _sym_db.RegisterFileDescriptor(DESCRIPTOR) -AgentConfig = _reflection.GeneratedProtocolMessageType('AgentConfig', (_message.Message,), { +AgentConfig = _reflection.GeneratedProtocolMessageType('AgentConfig', (_message.Message,), dict( - 'ResourceAttributesEntry' : _reflection.GeneratedProtocolMessageType('ResourceAttributesEntry', (_message.Message,), { - 'DESCRIPTOR' : _AGENTCONFIG_RESOURCEATTRIBUTESENTRY, - '__module__' : 'config_pb2' + ResourceAttributesEntry = _reflection.GeneratedProtocolMessageType('ResourceAttributesEntry', (_message.Message,), dict( + DESCRIPTOR = _AGENTCONFIG_RESOURCEATTRIBUTESENTRY, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.AgentConfig.ResourceAttributesEntry) - }) + )) , - 'DESCRIPTOR' : _AGENTCONFIG, - '__module__' : 'config_pb2' + DESCRIPTOR = _AGENTCONFIG, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.AgentConfig) - }) + )) _sym_db.RegisterMessage(AgentConfig) _sym_db.RegisterMessage(AgentConfig.ResourceAttributesEntry) -Reporting = _reflection.GeneratedProtocolMessageType('Reporting', (_message.Message,), { - 'DESCRIPTOR' : _REPORTING, - '__module__' : 'config_pb2' +Reporting = _reflection.GeneratedProtocolMessageType('Reporting', (_message.Message,), dict( + DESCRIPTOR = _REPORTING, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.Reporting) - }) + )) _sym_db.RegisterMessage(Reporting) -Opa = _reflection.GeneratedProtocolMessageType('Opa', (_message.Message,), { - 'DESCRIPTOR' : _OPA, - '__module__' : 'config_pb2' +Opa = _reflection.GeneratedProtocolMessageType('Opa', (_message.Message,), dict( + DESCRIPTOR = _OPA, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.Opa) - }) + )) _sym_db.RegisterMessage(Opa) -Message = _reflection.GeneratedProtocolMessageType('Message', (_message.Message,), { - 'DESCRIPTOR' : _MESSAGE, - '__module__' : 'config_pb2' +Message = _reflection.GeneratedProtocolMessageType('Message', (_message.Message,), dict( + DESCRIPTOR = _MESSAGE, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.Message) - }) + )) _sym_db.RegisterMessage(Message) -DataCapture = _reflection.GeneratedProtocolMessageType('DataCapture', (_message.Message,), { - 'DESCRIPTOR' : _DATACAPTURE, - '__module__' : 'config_pb2' +DataCapture = _reflection.GeneratedProtocolMessageType('DataCapture', (_message.Message,), dict( + DESCRIPTOR = _DATACAPTURE, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.DataCapture) - }) + )) _sym_db.RegisterMessage(DataCapture) -JavaAgent = _reflection.GeneratedProtocolMessageType('JavaAgent', (_message.Message,), { - 'DESCRIPTOR' : _JAVAAGENT, - '__module__' : 'config_pb2' +JavaAgent = _reflection.GeneratedProtocolMessageType('JavaAgent', (_message.Message,), dict( + DESCRIPTOR = _JAVAAGENT, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.JavaAgent) - }) + )) _sym_db.RegisterMessage(JavaAgent) diff --git a/docs/hypertrace/agent/config/file.html b/docs/hypertrace/agent/config/file.html index f9904e16..7a5c5579 100644 --- a/docs/hypertrace/agent/config/file.html +++ b/docs/hypertrace/agent/config/file.html @@ -47,7 +47,7 @@

Module hypertrace.agent.config.file

try: path = os.path.abspath(filepath) - file = open(path, 'r') + file = open(path, 'r') # pylint: disable=R1732 from_file_config = yaml.load(file, Loader=yaml.FullLoader) file.close() @@ -87,7 +87,7 @@

Functions

try: path = os.path.abspath(filepath) - file = open(path, 'r') + file = open(path, 'r') # pylint: disable=R1732 from_file_config = yaml.load(file, Loader=yaml.FullLoader) file.close() diff --git a/docs/hypertrace/agent/config/index.html b/docs/hypertrace/agent/config/index.html index b5325cfb..9503ad73 100644 --- a/docs/hypertrace/agent/config/index.html +++ b/docs/hypertrace/agent/config/index.html @@ -252,7 +252,7 @@

Sub-modules

hypertrace.agent.config.config_pb2
-

Generated protocol buffer code.

+
hypertrace.agent.config.default
diff --git a/docs/hypertrace/agent/index.html b/docs/hypertrace/agent/index.html index fea47d67..3ddf65fe 100644 --- a/docs/hypertrace/agent/index.html +++ b/docs/hypertrace/agent/index.html @@ -31,6 +31,7 @@

Module hypertrace.agent

import os import os.path import sys +import threading import logging import traceback import flask @@ -46,8 +47,6 @@

Module hypertrace.agent

try: formatter = logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') - handler = logging.FileHandler('agent.log', mode='a') - handler.setFormatter(formatter) screen_handler = logging.StreamHandler(stream=sys.stdout) screen_handler.setFormatter(formatter) log_level = logging.INFO @@ -69,7 +68,6 @@

Module hypertrace.agent

if ht_log_level == 'NOTSET': log_level = logging.NOTSET logger_.setLevel(log_level) - logger_.addHandler(handler) logger_.addHandler(screen_handler) return logger_ except Exception as err: # pylint: disable=W0703 @@ -87,21 +85,36 @@

Module hypertrace.agent

class Agent: '''Top-level entry point for Hypertrace agent.''' + _instance = None + _singleton_lock = threading.Lock() + + def __new__(cls): + '''constructor''' + if cls._instance is None: + with cls._singleton_lock: + logger.debug('Creating Agent') + cls._instance = super(Agent, cls).__new__(cls) + cls._instance._initialized = False + else: + logger.debug('Using existing Agent.') + return cls._instance def __init__(self): - '''Constructor''' - logger.debug('Initializing Agent.') - if not self.is_enabled(): - return - try: - self._config = AgentConfig() - self._init = AgentInit(self._config) - except Exception as err: # pylint: disable=W0703 - logger.error('Failed to initialize Agent: exception=%s, stacktrace=%s', - err, - traceback.format_exc()) + '''Initializer''' + if not self._initialized: # pylint: disable=E0203: + logger.debug('Initializing Agent.') + if not self.is_enabled(): + return + try: + self._config = AgentConfig() + self._init = AgentInit(self._config) + self._initialized = True + except Exception as err: # pylint: disable=W0703 + logger.error('Failed to initialize Agent: exception=%s, stacktrace=%s', + err, + traceback.format_exc()) - def register_flask_app(self, app: flask.Flask) -> None: + def register_flask_app(self, app: flask.Flask = None) -> None: '''Register the flask instrumentation module wrapper''' logger.debug('Calling Agent.register_flask_app.') if not self.is_enabled(): @@ -219,6 +232,10 @@

Module hypertrace.agent

Sub-modules

+
hypertrace.agent.autoinstrumentation
+
+
+
hypertrace.agent.config

Agent configuration logic that pull in values from a defaults list, @@ -257,8 +274,6 @@

Functions

try: formatter = logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') - handler = logging.FileHandler('agent.log', mode='a') - handler.setFormatter(formatter) screen_handler = logging.StreamHandler(stream=sys.stdout) screen_handler.setFormatter(formatter) log_level = logging.INFO @@ -280,7 +295,6 @@

Functions

if ht_log_level == 'NOTSET': log_level = logging.NOTSET logger_.setLevel(log_level) - logger_.addHandler(handler) logger_.addHandler(screen_handler) return logger_ except Exception as err: # pylint: disable=W0703 @@ -300,28 +314,43 @@

Classes

Top-level entry point for Hypertrace agent.

-

Constructor

+

Initializer

Expand source code
class Agent:
     '''Top-level entry point for Hypertrace agent.'''
+    _instance = None
+    _singleton_lock = threading.Lock()
+
+    def __new__(cls):
+        '''constructor'''
+        if cls._instance is None:
+            with cls._singleton_lock:
+                logger.debug('Creating Agent')
+                cls._instance = super(Agent, cls).__new__(cls)
+                cls._instance._initialized = False
+        else:
+            logger.debug('Using existing Agent.')
+        return cls._instance
 
     def __init__(self):
-        '''Constructor'''
-        logger.debug('Initializing Agent.')
-        if not self.is_enabled():
-            return
-        try:
-            self._config = AgentConfig()
-            self._init = AgentInit(self._config)
-        except Exception as err:  # pylint: disable=W0703
-            logger.error('Failed to initialize Agent: exception=%s, stacktrace=%s',
-                         err,
-                         traceback.format_exc())
+        '''Initializer'''
+        if not self._initialized: # pylint: disable=E0203:
+            logger.debug('Initializing Agent.')
+            if not self.is_enabled():
+                return
+            try:
+                self._config = AgentConfig()
+                self._init = AgentInit(self._config)
+                self._initialized = True
+            except Exception as err:  # pylint: disable=W0703
+                logger.error('Failed to initialize Agent: exception=%s, stacktrace=%s',
+                             err,
+                             traceback.format_exc())
 
-    def register_flask_app(self, app: flask.Flask) -> None:
+    def register_flask_app(self, app: flask.Flask = None) -> None:
         '''Register the flask instrumentation module wrapper'''
         logger.debug('Calling Agent.register_flask_app.')
         if not self.is_enabled():
@@ -480,7 +509,7 @@ 

Methods

-def register_flask_app(self, app: flask.app.Flask) ‑> NoneType +def register_flask_app(self, app: flask.app.Flask = None) ‑> NoneType

Register the flask instrumentation module wrapper

@@ -488,7 +517,7 @@

Methods

Expand source code -
def register_flask_app(self, app: flask.Flask) -> None:
+
def register_flask_app(self, app: flask.Flask = None) -> None:
     '''Register the flask instrumentation module wrapper'''
     logger.debug('Calling Agent.register_flask_app.')
     if not self.is_enabled():
@@ -659,6 +688,7 @@ 

Index

  • Sub-modules

      +
    • hypertrace.agent.autoinstrumentation
    • hypertrace.agent.config
    • hypertrace.agent.constants
    • hypertrace.agent.init
    • diff --git a/docs/hypertrace/agent/init/index.html b/docs/hypertrace/agent/init/index.html index f4fa1c7b..4fc4ce3f 100644 --- a/docs/hypertrace/agent/init/index.html +++ b/docs/hypertrace/agent/init/index.html @@ -63,7 +63,6 @@

      Module hypertrace.agent.init

      "requests": False, "aiohttp_client": False } - self._tracer_provider = None try: @@ -152,12 +151,30 @@

      Module hypertrace.agent.init

      '''Creates a flask instrumentation wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.flaskInit().') try: + if self.is_registered('flask'): + return from hypertrace.agent.instrumentation.flask import FlaskInstrumentorWrapper # pylint: disable=C0415 self._modules_initialized['flask'] = True self._flask_instrumentor_wrapper = FlaskInstrumentorWrapper() - self._flask_instrumentor_wrapper.instrument_app(app) + call_default_instrumentor = True + # There are two ways to initialize the flask instrumenation + # wrapper. The first (and original way) instruments the specific + # Flask object that is passed in). The second way is to globally + # replace the Flask class definition with the hypertrace instrumentation + # wrapper class. + # + # If an app object is provided, then the flask wrapper is initialized + # by calling the instrument_app method. Then, there is no need to call + # instrument() (so, we pass False as the second argument to + # self.init_instrumentor_wrapper_base_for_http(). + # + # If no app object was provided, then instrument() is called. + if app: + self._flask_instrumentor_wrapper.instrument_app(app) + call_default_instrumentor = False self.init_instrumentor_wrapper_base_for_http( - self._flask_instrumentor_wrapper) + self._flask_instrumentor_wrapper, + call_default_instrumentor) except Exception as err: # pylint: disable=W0703 logger.error(constants.INST_WRAP_EXCEPTION_MSSG, 'flask', @@ -168,6 +185,8 @@

      Module hypertrace.agent.init

      '''Creates a grpc server wrapper based on hypertrace config''' logger.debug('Calling AgentInit.grpcServerInit') try: + if self.is_registered('grpc:server'): + return from hypertrace.agent.instrumentation.grpc import ( # pylint: disable=C0415 GrpcInstrumentorServerWrapper ) @@ -195,6 +214,8 @@

      Module hypertrace.agent.init

      '''Creates a grpc client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.grpcClientInit') try: + if self.is_registered('grpc:client'): + return from hypertrace.agent.instrumentation.grpc import ( # pylint: disable=C0415 GrpcInstrumentorClientWrapper ) @@ -223,6 +244,8 @@

      Module hypertrace.agent.init

      '''Creates a mysql server wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.mysqlInit()') try: + if self.is_registered('mysql'): + return from hypertrace.agent.instrumentation.mysql import ( # pylint: disable=C0415 MySQLInstrumentorWrapper ) @@ -241,6 +264,8 @@

      Module hypertrace.agent.init

      '''Creates a postgresql client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.postgreSQLInit()') try: + if self.is_registered('postgresql'): + return from hypertrace.agent.instrumentation.postgresql import ( # pylint: disable=C0415 PostgreSQLInstrumentorWrapper ) @@ -259,6 +284,8 @@

      Module hypertrace.agent.init

      '''Creates a requests client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.requestsInit()') try: + if self.is_registered('requests'): + return from hypertrace.agent.instrumentation.requests import ( # pylint: disable=C0415 RequestsInstrumentorWrapper ) @@ -277,6 +304,8 @@

      Module hypertrace.agent.init

      '''Creates an aiohttp-client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.aioHttpClientInit()') try: + if self.is_registered('aiohttp_client'): + return from hypertrace.agent.instrumentation.aiohttp import ( # pylint: disable=C0415 AioHttpClientInstrumentorWrapper ) @@ -291,10 +320,13 @@

      Module hypertrace.agent.init

      traceback.format_exc()) # Common wrapper initialization logic - def init_instrumentor_wrapper_base_for_http(self, instrumentor) -> None: + def init_instrumentor_wrapper_base_for_http(self, + instrumentor, + call_instrument: bool = True) -> None: '''Common wrapper initialization logic''' logger.debug('Calling AgentInit.initInstrumentorWrapperBaseForHTTP().') - instrumentor.instrument() + if call_instrument: + instrumentor.instrument() instrumentor.set_process_request_headers( self._config.agent_config.data_capture.http_headers.request) instrumentor.set_process_request_body( @@ -352,7 +384,14 @@

      Module hypertrace.agent.init

      except Exception as err: # pylint: disable=W0703 logger.error('Failed to initialize OTLP exporter: exception=%s, stacktrace=%s', err, - traceback.format_exc())
  • + traceback.format_exc()) + + def is_registered(self, module: str) -> bool: + '''Is an instrumentation module already registered?''' + try: + return self._modules_initialized[module] + except Exception as err: # pylint: disable=W0703,W0612 + return False
    @@ -391,7 +430,6 @@

    Classes

    "requests": False, "aiohttp_client": False } - self._tracer_provider = None try: @@ -480,12 +518,30 @@

    Classes

    '''Creates a flask instrumentation wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.flaskInit().') try: + if self.is_registered('flask'): + return from hypertrace.agent.instrumentation.flask import FlaskInstrumentorWrapper # pylint: disable=C0415 self._modules_initialized['flask'] = True self._flask_instrumentor_wrapper = FlaskInstrumentorWrapper() - self._flask_instrumentor_wrapper.instrument_app(app) + call_default_instrumentor = True + # There are two ways to initialize the flask instrumenation + # wrapper. The first (and original way) instruments the specific + # Flask object that is passed in). The second way is to globally + # replace the Flask class definition with the hypertrace instrumentation + # wrapper class. + # + # If an app object is provided, then the flask wrapper is initialized + # by calling the instrument_app method. Then, there is no need to call + # instrument() (so, we pass False as the second argument to + # self.init_instrumentor_wrapper_base_for_http(). + # + # If no app object was provided, then instrument() is called. + if app: + self._flask_instrumentor_wrapper.instrument_app(app) + call_default_instrumentor = False self.init_instrumentor_wrapper_base_for_http( - self._flask_instrumentor_wrapper) + self._flask_instrumentor_wrapper, + call_default_instrumentor) except Exception as err: # pylint: disable=W0703 logger.error(constants.INST_WRAP_EXCEPTION_MSSG, 'flask', @@ -496,6 +552,8 @@

    Classes

    '''Creates a grpc server wrapper based on hypertrace config''' logger.debug('Calling AgentInit.grpcServerInit') try: + if self.is_registered('grpc:server'): + return from hypertrace.agent.instrumentation.grpc import ( # pylint: disable=C0415 GrpcInstrumentorServerWrapper ) @@ -523,6 +581,8 @@

    Classes

    '''Creates a grpc client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.grpcClientInit') try: + if self.is_registered('grpc:client'): + return from hypertrace.agent.instrumentation.grpc import ( # pylint: disable=C0415 GrpcInstrumentorClientWrapper ) @@ -551,6 +611,8 @@

    Classes

    '''Creates a mysql server wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.mysqlInit()') try: + if self.is_registered('mysql'): + return from hypertrace.agent.instrumentation.mysql import ( # pylint: disable=C0415 MySQLInstrumentorWrapper ) @@ -569,6 +631,8 @@

    Classes

    '''Creates a postgresql client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.postgreSQLInit()') try: + if self.is_registered('postgresql'): + return from hypertrace.agent.instrumentation.postgresql import ( # pylint: disable=C0415 PostgreSQLInstrumentorWrapper ) @@ -587,6 +651,8 @@

    Classes

    '''Creates a requests client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.requestsInit()') try: + if self.is_registered('requests'): + return from hypertrace.agent.instrumentation.requests import ( # pylint: disable=C0415 RequestsInstrumentorWrapper ) @@ -605,6 +671,8 @@

    Classes

    '''Creates an aiohttp-client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.aioHttpClientInit()') try: + if self.is_registered('aiohttp_client'): + return from hypertrace.agent.instrumentation.aiohttp import ( # pylint: disable=C0415 AioHttpClientInstrumentorWrapper ) @@ -619,10 +687,13 @@

    Classes

    traceback.format_exc()) # Common wrapper initialization logic - def init_instrumentor_wrapper_base_for_http(self, instrumentor) -> None: + def init_instrumentor_wrapper_base_for_http(self, + instrumentor, + call_instrument: bool = True) -> None: '''Common wrapper initialization logic''' logger.debug('Calling AgentInit.initInstrumentorWrapperBaseForHTTP().') - instrumentor.instrument() + if call_instrument: + instrumentor.instrument() instrumentor.set_process_request_headers( self._config.agent_config.data_capture.http_headers.request) instrumentor.set_process_request_body( @@ -680,7 +751,14 @@

    Classes

    except Exception as err: # pylint: disable=W0703 logger.error('Failed to initialize OTLP exporter: exception=%s, stacktrace=%s', err, - traceback.format_exc())
    + traceback.format_exc()) + + def is_registered(self, module: str) -> bool: + '''Is an instrumentation module already registered?''' + try: + return self._modules_initialized[module] + except Exception as err: # pylint: disable=W0703,W0612 + return False

    Methods

    @@ -697,6 +775,8 @@

    Methods

    '''Creates an aiohttp-client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.aioHttpClientInit()') try: + if self.is_registered('aiohttp_client'): + return from hypertrace.agent.instrumentation.aiohttp import ( # pylint: disable=C0415 AioHttpClientInstrumentorWrapper ) @@ -760,12 +840,30 @@

    Methods

    '''Creates a flask instrumentation wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.flaskInit().') try: + if self.is_registered('flask'): + return from hypertrace.agent.instrumentation.flask import FlaskInstrumentorWrapper # pylint: disable=C0415 self._modules_initialized['flask'] = True self._flask_instrumentor_wrapper = FlaskInstrumentorWrapper() - self._flask_instrumentor_wrapper.instrument_app(app) + call_default_instrumentor = True + # There are two ways to initialize the flask instrumenation + # wrapper. The first (and original way) instruments the specific + # Flask object that is passed in). The second way is to globally + # replace the Flask class definition with the hypertrace instrumentation + # wrapper class. + # + # If an app object is provided, then the flask wrapper is initialized + # by calling the instrument_app method. Then, there is no need to call + # instrument() (so, we pass False as the second argument to + # self.init_instrumentor_wrapper_base_for_http(). + # + # If no app object was provided, then instrument() is called. + if app: + self._flask_instrumentor_wrapper.instrument_app(app) + call_default_instrumentor = False self.init_instrumentor_wrapper_base_for_http( - self._flask_instrumentor_wrapper) + self._flask_instrumentor_wrapper, + call_default_instrumentor) except Exception as err: # pylint: disable=W0703 logger.error(constants.INST_WRAP_EXCEPTION_MSSG, 'flask', @@ -786,6 +884,8 @@

    Methods

    '''Creates a grpc client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.grpcClientInit') try: + if self.is_registered('grpc:client'): + return from hypertrace.agent.instrumentation.grpc import ( # pylint: disable=C0415 GrpcInstrumentorClientWrapper ) @@ -823,6 +923,8 @@

    Methods

    '''Creates a grpc server wrapper based on hypertrace config''' logger.debug('Calling AgentInit.grpcServerInit') try: + if self.is_registered('grpc:server'): + return from hypertrace.agent.instrumentation.grpc import ( # pylint: disable=C0415 GrpcInstrumentorServerWrapper ) @@ -859,6 +961,8 @@

    Methods

    '''Creates a mysql server wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.mysqlInit()') try: + if self.is_registered('mysql'): + return from hypertrace.agent.instrumentation.mysql import ( # pylint: disable=C0415 MySQLInstrumentorWrapper ) @@ -886,6 +990,8 @@

    Methods

    '''Creates a postgresql client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.postgreSQLInit()') try: + if self.is_registered('postgresql'): + return from hypertrace.agent.instrumentation.postgresql import ( # pylint: disable=C0415 PostgreSQLInstrumentorWrapper ) @@ -913,6 +1019,8 @@

    Methods

    '''Creates a requests client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.requestsInit()') try: + if self.is_registered('requests'): + return from hypertrace.agent.instrumentation.requests import ( # pylint: disable=C0415 RequestsInstrumentorWrapper ) @@ -928,7 +1036,7 @@

    Methods

    -def init_instrumentor_wrapper_base_for_http(self, instrumentor) ‑> NoneType +def init_instrumentor_wrapper_base_for_http(self, instrumentor, call_instrument: bool = True) ‑> NoneType

    Common wrapper initialization logic

    @@ -936,10 +1044,13 @@

    Methods

    Expand source code -
    def init_instrumentor_wrapper_base_for_http(self, instrumentor) -> None:
    +
    def init_instrumentor_wrapper_base_for_http(self,
    +                                            instrumentor,
    +                                            call_instrument: bool = True) -> None:
         '''Common wrapper initialization logic'''
         logger.debug('Calling AgentInit.initInstrumentorWrapperBaseForHTTP().')
    -    instrumentor.instrument()
    +    if call_instrument:
    +        instrumentor.instrument()
         instrumentor.set_process_request_headers(
             self._config.agent_config.data_capture.http_headers.request)
         instrumentor.set_process_request_body(
    @@ -1014,6 +1125,23 @@ 

    Methods

    trace.set_tracer_provider(self._tracer_provider)
    +
    +def is_registered(self, module: str) ‑> bool +
    +
    +

    Is an instrumentation module already registered?

    +
    + +Expand source code + +
    def is_registered(self, module: str) -> bool:
    +    '''Is an instrumentation module already registered?'''
    +    try:
    +        return self._modules_initialized[module]
    +    except Exception as err: # pylint: disable=W0703,W0612
    +        return False
    +
    +
    def register_processor(self, processor) ‑> NoneType
    @@ -1081,6 +1209,7 @@

    init_instrumentor_wrapper_base_for_http
  • init_propagation
  • init_trace_provider
  • +
  • is_registered
  • register_processor
  • set_console_span_processor
  • diff --git a/docs/hypertrace/agent/instrumentation/aiohttp/index.html b/docs/hypertrace/agent/instrumentation/aiohttp/index.html index d309dd76..6c612c29 100644 --- a/docs/hypertrace/agent/instrumentation/aiohttp/index.html +++ b/docs/hypertrace/agent/instrumentation/aiohttp/index.html @@ -253,7 +253,7 @@

    Module hypertrace.agent.instrumentation.aiohttpFunctions

    -def create_trace_config(url_filter: Optional[Callable[[str], str]] = None, span_name: Union[Callable[[aiohttp.tracing.TraceRequestStartParams], str], str, NoneType] = None, tracer_provider: opentelemetry.trace.TracerProvider = None, aiohttp_client_wrapper: AioHttpClientInstrumentorWrapper = None) ‑> aiohttp.tracing.TraceConfig +def create_trace_config(url_filter: Union[Callable[[str], str], NoneType] = None, span_name: Union[Callable[[aiohttp.tracing.TraceRequestStartParams], str], str, NoneType] = None, tracer_provider: opentelemetry.trace.TracerProvider = None, aiohttp_client_wrapper: AioHttpClientInstrumentorWrapper = None) ‑> aiohttp.tracing.TraceConfig

    Build an aiohttp-client trace config for use with Hypertrace

    diff --git a/docs/hypertrace/agent/instrumentation/flask/index.html b/docs/hypertrace/agent/instrumentation/flask/index.html index d4e0c45b..58633a19 100644 --- a/docs/hypertrace/agent/instrumentation/flask/index.html +++ b/docs/hypertrace/agent/instrumentation/flask/index.html @@ -27,7 +27,7 @@

    Module hypertrace.agent.instrumentation.flask

    Expand source code -
    '''Hypertrace flask instrumentor module wrapper.'''
    +
    '''Hypertrace flask instrumentor module wrapper.''' # pylint: disable=R0401
     import sys
     import os.path
     import logging
    @@ -36,6 +36,7 @@ 

    Module hypertrace.agent.instrumentation.flask

    Module hypertrace.agent.instrumentation.flaskModule hypertrace.agent.instrumentation.flaskModule hypertrace.agent.instrumentation.flaskModule hypertrace.agent.instrumentation.flaskClasses super().__init__() self._app = None - - + def _instrument(self, **kwargs): + '''Override OTel method that sets up global flask instrumentation''' + self._original_flask = flask.Flask # pylint: disable = W0201 + name_callback = kwargs.get("name_callback") + tracer_provider = kwargs.get("tracer_provider") + if callable(name_callback): + _HypertraceInstrumentedFlask.name_callback = name_callback + _HypertraceInstrumentedFlask._tracer_provider = tracer_provider # pylint: disable=W0212 + flask.Flask = _HypertraceInstrumentedFlask # Initialize instrumentation wrapper - def instrument_app(self, app, name_callback=get_default_span_name) -> None: + def instrument_app(self, + app, + name_callback=get_default_span_name, + tracer_provider=None) -> None: '''Initialize instrumentation''' logger.debug('Entering FlaskInstrumentorWrapper.instument_app().') try: # Call parent class's initialization - super().instrument_app(app, name_callback) + super().instrument_app(app, name_callback, tracer_provider) self._app = app # Set pre-request handler @@ -306,7 +345,7 @@

    Methods

    -def instrument_app(self, app, name_callback=<function get_default_span_name>) ‑> NoneType +def instrument_app(self, app, name_callback=<function get_default_span_name>, tracer_provider=None) ‑> NoneType

    Initialize instrumentation

    @@ -314,13 +353,16 @@

    Methods

    Expand source code -
    def instrument_app(self, app, name_callback=get_default_span_name) -> None:
    +
    def instrument_app(self,
    +                   app,
    +                   name_callback=get_default_span_name,
    +                   tracer_provider=None) -> None:
         '''Initialize instrumentation'''
         logger.debug('Entering FlaskInstrumentorWrapper.instument_app().')
         try:
     
             # Call parent class's initialization
    -        super().instrument_app(app, name_callback)
    +        super().instrument_app(app, name_callback, tracer_provider)
     
             self._app = app
             # Set pre-request handler
    diff --git a/docs/hypertrace/agent/instrumentation/grpc/index.html b/docs/hypertrace/agent/instrumentation/grpc/index.html
    index 102b18ab..fbef6e82 100644
    --- a/docs/hypertrace/agent/instrumentation/grpc/index.html
    +++ b/docs/hypertrace/agent/instrumentation/grpc/index.html
    @@ -639,8 +639,7 @@ 

    Inherited members

    ) -> None: '''process streaming request for hypertrace''' logger.debug( - 'Entering OpenTelemetryClientInterceptorWrapper.intercept_stream().') - # COME_BACK -- need to implement this
    + 'Entering OpenTelemetryClientInterceptorWrapper.intercept_stream().')

    Ancestors

      @@ -669,8 +668,7 @@

      Methods

      ) -> None: '''process streaming request for hypertrace''' logger.debug( - 'Entering OpenTelemetryClientInterceptorWrapper.intercept_stream().') - # COME_BACK -- need to implement this
      + 'Entering OpenTelemetryClientInterceptorWrapper.intercept_stream().')
    @@ -783,8 +781,7 @@

    Methods

    context) -> None: '''Setup interceptor helper for streaming requests.''' logger.debug( - 'Entering OpenTelemetryServerInterceptorWrapper.intercept_server_stream().') - # COME_BACK -- need to implement this
    + 'Entering OpenTelemetryServerInterceptorWrapper.intercept_server_stream().')

    Ancestors

      diff --git a/docs/hypertrace/agent/instrumentation/index.html b/docs/hypertrace/agent/instrumentation/index.html index c19487f9..1c58a116 100644 --- a/docs/hypertrace/agent/instrumentation/index.html +++ b/docs/hypertrace/agent/instrumentation/index.html @@ -811,6 +811,7 @@

      Subclasses

    diff --git a/examples/autoinstrumentation/Makefile b/examples/autoinstrumentation/Makefile new file mode 100644 index 00000000..63101e09 --- /dev/null +++ b/examples/autoinstrumentation/Makefile @@ -0,0 +1,12 @@ +install-deps: + pip3 install -r requirements.txt + +run: + @rm -rf __pycache__ || true + HT_CONFIG_FILE=./config.yaml FLASK_APP="server:create_app()" FLASK_ENV=development hypertrace-instrument flask run -p 9000 + +run-hypertrace: + docker-compose -f docker-compose-hypertrace.yml up --renew-anon-volumes -d + +run-mysql: + docker-compose -f ./mysql/docker-compose.yml up --renew-anon-volumes -d diff --git a/examples/autoinstrumentation/config.yaml b/examples/autoinstrumentation/config.yaml new file mode 100644 index 00000000..320f1699 --- /dev/null +++ b/examples/autoinstrumentation/config.yaml @@ -0,0 +1,6 @@ +service_name: "server" +propagation_formats: + - "B3" +reporting: + endpoint: http://localhost:9411/api/v2/spans + trace_reporter_type: ZIPKIN diff --git a/examples/autoinstrumentation/docker-compose-hypertrace.yml b/examples/autoinstrumentation/docker-compose-hypertrace.yml new file mode 100644 index 00000000..502c5183 --- /dev/null +++ b/examples/autoinstrumentation/docker-compose-hypertrace.yml @@ -0,0 +1,115 @@ +## Copied from https://github.com/hypertrace/hypertrace/blob/main/docker/docker-compose.yml + +## This does everything you need to get a hypertracing system started. +## You can connect to the UI at port 2020 and send data to it on any supported tracing solution. +## Note: Our stack is dependent on pinot and it is a cpu heavy during startup. +## The depends_on has a max wait time of 1 min, so if you don't have enough resources, you may have to re-run the same command. +## we are looking at improving this. +version: "2.4" +services: + + +# This container includes the UI and APIs it needs for storage queries. + hypertrace: + image: hypertrace/hypertrace:main + container_name: hypertrace + environment: + - MONGO_HOST=mongo + - ZK_CONNECT_STR=zookeeper:2181/hypertrace-views + ports: + - 2020:2020 + healthcheck: + start_period: 20s + depends_on: + mongo: + condition: service_healthy + kafka-zookeeper: + condition: service_healthy + pinot: + condition: service_started + +# Ingestion pipeline + + # Collects spans in different trace formats like Jaeger, Zipkin, etc + hypertrace-collector: + image: hypertrace/hypertrace-collector:main + container_name: hypertrace-collector + ports: + - 4317:4317 # grpc-otel + - 55681:55681 # http-otel + - 14268:14268 # Jaeger http + - 9411:9411 # Zipkin HTTP + environment: + - EXPORTER_KAFKA_BROKER=kafka-zookeeper:9092 + - EXPORTER_KAFKA_TOPIC=jaeger-spans + networks: + default: + # Allows sample apps to connect with platform-specific hostnames + aliases: + - jaeger + - jaeger-collector + - zipkin + depends_on: + kafka-zookeeper: + condition: service_healthy + # all-in-one ingestion pipeline for hypertrace + hypertrace-ingester: + image: hypertrace/hypertrace-ingester + container_name: hypertrace-ingester + environment: + - KAFKA_BOOTSTRAP_SERVERS=kafka:9092 + - DEFAULT_TENANT_ID=__default + - SPAN_GROUPBY_SESSION_WINDOW_INTERVAL=2 + - REPLICATION_FACTOR=1 + - ENTITY_SERVICE_HOST_CONFIG=hypertrace + - ENTITY_SERVICE_PORT_CONFIG=9001 + - ATTRIBUTE_SERVICE_HOST_CONFIG=hypertrace + - ATTRIBUTE_SERVICE_PORT_CONFIG=9001 + - CONFIG_SERVICE_HOST_CONFIG=hypertrace + - CONFIG_SERVICE_PORT_CONFIG=9001 + - NUM_STREAM_THREADS=1 + - PRE_CREATE_TOPICS=true + - PRODUCER_VALUE_SERDE=org.hypertrace.core.kafkastreams.framework.serdes.GenericAvroSerde + volumes: + - ../docker/configs/log4j2.properties:/app/resources/log4j2.properties:ro + depends_on: + kafka-zookeeper: + condition: service_healthy + hypertrace: + # service_started, not service_healthy as pinot and deps can take longer than 60s to start + condition: service_started + +# Third-party data services: + + # Kafka is used for streaming functionality. + # ZooKeeper is required by Kafka and Pinot + kafka-zookeeper: + image: hypertrace/kafka-zookeeper:main + container_name: kafka-zookeeper + networks: + default: + # prevents apps from having to use the hostname kafka-zookeeper + aliases: + - kafka + - zookeeper + # Stores entities like API, service and backend + mongo: + image: hypertrace/mongodb:main + container_name: mongo + # Stores spans and traces and provides aggregation functions + pinot: + image: hypertrace/pinot-servicemanager:main + container_name: pinot + environment: + - LOG_LEVEL=error + networks: + default: + # Usually, Pinot is distributed, and clients connect to the controller + aliases: + - pinot-controller + - pinot-server + - pinot-broker + cpu_shares: 2048 + depends_on: + kafka-zookeeper: + condition: service_healthy \ No newline at end of file diff --git a/examples/autoinstrumentation/mysql/docker-compose.yml b/examples/autoinstrumentation/mysql/docker-compose.yml new file mode 100644 index 00000000..148c3250 --- /dev/null +++ b/examples/autoinstrumentation/mysql/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.1' + +services: + + mysqldb: + image: mysql + command: --default-authentication-plugin=mysql_native_password + restart: always + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql diff --git a/examples/autoinstrumentation/mysql/init.sql b/examples/autoinstrumentation/mysql/init.sql new file mode 100644 index 00000000..b5c561d1 --- /dev/null +++ b/examples/autoinstrumentation/mysql/init.sql @@ -0,0 +1,4 @@ +CREATE TABLE `users`( + id INT AUTO_INCREMENT primary key NOT NULL, + `name` VARCHAR(100) NOT NULL +); diff --git a/examples/autoinstrumentation/requirements.txt b/examples/autoinstrumentation/requirements.txt new file mode 100644 index 00000000..9c6b95e2 --- /dev/null +++ b/examples/autoinstrumentation/requirements.txt @@ -0,0 +1,3 @@ +../../ +flask +mysql-connector-python==8.0.23 diff --git a/examples/autoinstrumentation/server.py b/examples/autoinstrumentation/server.py new file mode 100644 index 00000000..b2b34550 --- /dev/null +++ b/examples/autoinstrumentation/server.py @@ -0,0 +1,58 @@ +from flask import Flask, request, jsonify, make_response, Response +import time +import json +import logging +import mysql.connector + + +logging.basicConfig() +logging.getLogger().setLevel(logging.DEBUG) + + +def stream_body(res_body: str): + new_line_chars = ['{', '}', ',', '[', ']'] + for c in res_body: + time.sleep(0.3) + logging.debug("Sending response chunk") + if c in new_line_chars: + yield c + "\n" + else: + yield c + + +def insert_user(name: str) -> int: + cnx = mysql.connector.connect(database='test', + username='root', + password='root', + host='localhost', + port=3306) + cursor = cnx.cursor() + query = "INSERT INTO users (`name`) VALUES (%s)" + cursor.execute(query, (name,)) + id = cursor.lastrowid + cnx.close() + return id + + +def create_app(): + app = Flask(__name__) + + @app.route('/', methods=['POST']) + def hello(): + request_data = request.get_json() + name = request_data['name'] + + user_id = insert_user(name) + + res_body = {'id': user_id, 'message': f'Hello {name}'} + + stream = request.args.get('stream') + if stream == 'true': + # Send the response as stream + return Response(stream_body(json.dumps(res_body)), mimetype='application/json') + else: + # Send the entire response + response = make_response(jsonify(res_body)) + response.headers['Content-Type'] = 'application/json' + return response + return app diff --git a/examples/server/Makefile b/examples/server/Makefile index 6352c658..21b5f7bc 100644 --- a/examples/server/Makefile +++ b/examples/server/Makefile @@ -9,4 +9,4 @@ run-hypertrace: docker-compose -f docker-compose-hypertrace.yml up --renew-anon-volumes -d run-mysql: - docker-compose -f ./mysql/docker-compose.yml up --renew-anon-volumes -d \ No newline at end of file + docker-compose -f ./mysql/docker-compose.yml up --renew-anon-volumes -d diff --git a/requirements.txt b/requirements.txt index 4ab30ad5..a727a719 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,17 @@ -opentelemetry-api==1.1.0 -opentelemetry-exporter-otlp==1.1.0 -opentelemetry-exporter-zipkin==1.1.0 -opentelemetry-instrumentation==0.20b0 -opentelemetry-instrumentation-aiohttp-client==0.20b0 -opentelemetry-instrumentation-wsgi==0.20b0 -opentelemetry-instrumentation-flask==0.20b0 -opentelemetry-instrumentation-mysql==0.20b0 -opentelemetry-instrumentation-psycopg2==0.20b0 -opentelemetry-instrumentation-requests==0.20b0 -opentelemetry-instrumentation-grpc==0.20b0 -opentelemetry-propagator-b3==1.1.0 -opentelemetry-sdk==1.1.0 -opentelemetry-util-http==0.20b0 -google==3.0.0 +opentelemetry-api>=1.1.0 +opentelemetry-exporter-otlp>=1.1.0 +opentelemetry-exporter-zipkin>=1.1.0 +opentelemetry-instrumentation>=0.20b0 +opentelemetry-instrumentation-aiohttp-client>=0.20b0 +opentelemetry-instrumentation-wsgi>=0.20b0 +opentelemetry-instrumentation-flask>=0.20b0 +opentelemetry-instrumentation-mysql>=0.20b0 +opentelemetry-instrumentation-psycopg2>=0.20b0 +opentelemetry-instrumentation-requests>=0.20b0 +opentelemetry-instrumentation-grpc>=0.20b0 +opentelemetry-propagator-b3>=1.1.0 +opentelemetry-sdk>=1.1.0 +opentelemetry-util-http>=0.20b0 +google>=3.0.0 pyyaml -protobuf==3.15.8 +protobuf>=3.15.8 diff --git a/setup.py b/setup.py index 62c4c6ea..e7f899ef 100644 --- a/setup.py +++ b/setup.py @@ -26,22 +26,27 @@ packages=find_packages(where="src"), python_requires=">=3.7", install_requires=[ - "opentelemetry-api==1.1.0", - "opentelemetry-exporter-otlp==1.1.0", - "opentelemetry-exporter-zipkin==1.1.0", - "opentelemetry-instrumentation==0.20b0", - "opentelemetry-instrumentation-aiohttp-client==0.20b0", - "opentelemetry-instrumentation-wsgi==0.20b0", - "opentelemetry-instrumentation-flask==0.20b0", - "opentelemetry-instrumentation-mysql==0.20b0", - "opentelemetry-instrumentation-psycopg2==0.20b0", - "opentelemetry-instrumentation-requests==0.20b0", - "opentelemetry-instrumentation-grpc==0.20b0", - "opentelemetry-propagator-b3==1.1.0", - "opentelemetry-sdk==1.1.0", - "opentelemetry-util-http==0.20b0", - "google==3.0.0", + "opentelemetry-api>=1.1.0", + "opentelemetry-exporter-otlp>=1.1.0", + "opentelemetry-exporter-zipkin>=1.1.0", + "opentelemetry-instrumentation>=0.20b0", + "opentelemetry-instrumentation-aiohttp-client>=0.20b0", + "opentelemetry-instrumentation-wsgi>=0.20b0", + "opentelemetry-instrumentation-flask>=0.20b0", + "opentelemetry-instrumentation-mysql>=0.20b0", + "opentelemetry-instrumentation-psycopg2>=0.20b0", + "opentelemetry-instrumentation-requests>=0.20b0", + "opentelemetry-instrumentation-grpc>=0.20b0", + "opentelemetry-propagator-b3>=1.1.0", + "opentelemetry-sdk>=1.1.0", + "opentelemetry-util-http>=0.20b0", + "google>=3.0.0", "pyyaml", - "protobuf==3.15.8" - ] + "protobuf>=3.15.8" + ], + entry_points = { + 'console_scripts': [ + 'hypertrace-instrument = hypertrace.agent.autoinstrumentation.hypertrace_instrument:run', + ], + } ) diff --git a/src/hypertrace/agent/__init__.py b/src/hypertrace/agent/__init__.py index 781f10c3..199b256b 100644 --- a/src/hypertrace/agent/__init__.py +++ b/src/hypertrace/agent/__init__.py @@ -2,6 +2,7 @@ import os import os.path import sys +import threading import logging import traceback import flask @@ -17,8 +18,6 @@ def setup_custom_logger(name: str) -> logging.Logger: try: formatter = logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') - handler = logging.FileHandler('agent.log', mode='a') - handler.setFormatter(formatter) screen_handler = logging.StreamHandler(stream=sys.stdout) screen_handler.setFormatter(formatter) log_level = logging.INFO @@ -40,7 +39,6 @@ def setup_custom_logger(name: str) -> logging.Logger: if ht_log_level == 'NOTSET': log_level = logging.NOTSET logger_.setLevel(log_level) - logger_.addHandler(handler) logger_.addHandler(screen_handler) return logger_ except Exception as err: # pylint: disable=W0703 @@ -58,21 +56,36 @@ def setup_custom_logger(name: str) -> logging.Logger: class Agent: '''Top-level entry point for Hypertrace agent.''' + _instance = None + _singleton_lock = threading.Lock() + + def __new__(cls): + '''constructor''' + if cls._instance is None: + with cls._singleton_lock: + logger.debug('Creating Agent') + cls._instance = super(Agent, cls).__new__(cls) + cls._instance._initialized = False + else: + logger.debug('Using existing Agent.') + return cls._instance def __init__(self): - '''Constructor''' - logger.debug('Initializing Agent.') - if not self.is_enabled(): - return - try: - self._config = AgentConfig() - self._init = AgentInit(self._config) - except Exception as err: # pylint: disable=W0703 - logger.error('Failed to initialize Agent: exception=%s, stacktrace=%s', - err, - traceback.format_exc()) - - def register_flask_app(self, app: flask.Flask) -> None: + '''Initializer''' + if not self._initialized: # pylint: disable=E0203: + logger.debug('Initializing Agent.') + if not self.is_enabled(): + return + try: + self._config = AgentConfig() + self._init = AgentInit(self._config) + self._initialized = True + except Exception as err: # pylint: disable=W0703 + logger.error('Failed to initialize Agent: exception=%s, stacktrace=%s', + err, + traceback.format_exc()) + + def register_flask_app(self, app: flask.Flask = None) -> None: '''Register the flask instrumentation module wrapper''' logger.debug('Calling Agent.register_flask_app.') if not self.is_enabled(): diff --git a/src/hypertrace/agent/autoinstrumentation/__init__.py b/src/hypertrace/agent/autoinstrumentation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/hypertrace/agent/autoinstrumentation/hypertrace_instrument.py b/src/hypertrace/agent/autoinstrumentation/hypertrace_instrument.py new file mode 100644 index 00000000..636da868 --- /dev/null +++ b/src/hypertrace/agent/autoinstrumentation/hypertrace_instrument.py @@ -0,0 +1,79 @@ +# Based upon the OTel autoinstrumentation feature +'''This module implements a CLI command that be used to +autoinstrument existing pythong programs that use supported +modules.''' +import argparse +from logging import getLogger +from os import environ, execl, getcwd +from os.path import abspath, dirname, pathsep +from shutil import which + +logger = getLogger(__file__) + +def parse_args(): + '''Parse CLI arguments.''' + parser = argparse.ArgumentParser( + description=""" + hypertrace-instrument automatically instruments a Python + program and runs the program + """ + ) + + parser.add_argument("command", help="Your Python application.") + + parser.add_argument( + "command_args", + help="Arguments for your application.", + nargs=argparse.REMAINDER, + ) + + return parser.parse_args() + +def update_python_path() -> None: + '''Retrieve existing PYTHONPATH''' + python_path = environ.get("PYTHONPATH") + + # Split the paths + if not python_path: + python_path = [] + else: + python_path = python_path.split(pathsep) + + # Get the current working directory + cwd_path = getcwd() + + # If this directory is already in python_path, remove it. + python_path = [path for path in python_path if path != cwd_path] + + # If cwd is not in the PYTHONPATH, add it to the front. + if cwd_path not in python_path: + python_path.insert(0, cwd_path) + + # What is the directory containing this python file? + filedir_path = dirname(abspath(__file__)) + + # If this directory is already in python_path, remove it. + python_path = [path for path in python_path if path != filedir_path] + + # If this diretory is not in python_path, add it to the front + python_path.insert(0, filedir_path) + + # Reset PYTHONPATH environment variable + environ["PYTHONPATH"] = pathsep.join(python_path) + +def run() -> None: + '''hypertrace-instrument Entry point''' + args = parse_args() + + # update PYTHONPATH env var + update_python_path() + + # Get full path to the command that was passed in as an + # argument + executable = which(args.command) + + # Execute the app + execl(executable, executable, *args.command_args) + +if __name__ == '__main__': + run() diff --git a/src/hypertrace/agent/autoinstrumentation/sitecustomize.py b/src/hypertrace/agent/autoinstrumentation/sitecustomize.py new file mode 100644 index 00000000..fe5f0bda --- /dev/null +++ b/src/hypertrace/agent/autoinstrumentation/sitecustomize.py @@ -0,0 +1,54 @@ +'''Enable instrumentationon all supported modules.''' # pylint: disable=R0401 +import os +import logging +from hypertrace.agent import Agent + +DEFAULTS = [ + 'flask', + 'mysql', + 'postgresql', + 'grpc:server', + 'grpc:client', + 'requests', + 'aiohttp-client' +] + +# Initialize logger +logger = logging.getLogger(__name__) # pylint: disable=C0103 + +MODULES = '' +if 'HT_INSTRUMENTED_MODULES' in os.environ: + logger.debug("[env] Loaded HT_INSTRUMENTED_MODULES from env") + MODULES = os.environ['HT_INSTRUMENTED_MODULES'] + if len(MODULES) > 0: + MODULES = MODULES.replace(' ', '') + +if len(MODULES) > 0: + modules_array = MODULES.split(',') +else: + modules_array = DEFAULTS + +# Create Hypertrace agent +agent = Agent() + +# Initialize desired instrumentation wrappers +for mod in modules_array: + if mod is None or len(mod) == 0: + continue + + if mod == 'flask': + agent.register_flask_app() + elif mod == 'grpc:server': + agent.register_grpc_server() + elif mod == 'grpc:client': + agent.register_grpc_client() + elif mod == 'mysql': + agent.register_mysql() + elif mod == 'postgresql': + agent.register_postgresql() + elif mod == 'requests': + agent.register_requests() + elif mod == 'aiohttp-client': + agent.register_aiohttp_client() + else: + logger.error('Unknown module name: %s', mod) diff --git a/src/hypertrace/agent/config/config_pb2.py b/src/hypertrace/agent/config/config_pb2.py index acefb025..8d80422c 100644 --- a/src/hypertrace/agent/config/config_pb2.py +++ b/src/hypertrace/agent/config/config_pb2.py @@ -1,7 +1,8 @@ -# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: config.proto -"""Generated protocol buffer code.""" + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) from google.protobuf.internal import enum_type_wrapper from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message @@ -19,9 +20,8 @@ name='config.proto', package='org.hypertrace.agent.config', syntax='proto3', - serialized_options=b'\n\033org.hypertrace.agent.configZ$github.com/hypertrace/goagent/config', - create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x0c\x63onfig.proto\x12\x1borg.hypertrace.agent.config\x1a\x1egoogle/protobuf/wrappers.proto\"\x8b\x04\n\x0b\x41gentConfig\x12\x32\n\x0cservice_name\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x39\n\treporting\x18\x02 \x01(\x0b\x32&.org.hypertrace.agent.config.Reporting\x12>\n\x0c\x64\x61ta_capture\x18\x03 \x01(\x0b\x32(.org.hypertrace.agent.config.DataCapture\x12K\n\x13propagation_formats\x18\x04 \x03(\x0e\x32..org.hypertrace.agent.config.PropagationFormat\x12+\n\x07\x65nabled\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x39\n\tjavaagent\x18\x06 \x01(\x0b\x32&.org.hypertrace.agent.config.JavaAgent\x12]\n\x13resource_attributes\x18\x07 \x03(\x0b\x32@.org.hypertrace.agent.config.AgentConfig.ResourceAttributesEntry\x1a\x39\n\x17ResourceAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x90\x02\n\tReporting\x12.\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x06secure\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12+\n\x05token\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x03opa\x18\x04 \x01(\x0b\x32 .org.hypertrace.agent.config.Opa\x12K\n\x13trace_reporter_type\x18\x05 \x01(\x0e\x32..org.hypertrace.agent.config.TraceReporterType\"\x9c\x01\n\x03Opa\x12.\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x38\n\x13poll_period_seconds\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12+\n\x07\x65nabled\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\"d\n\x07Message\x12+\n\x07request\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12,\n\x08response\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\"\xb0\x02\n\x0b\x44\x61taCapture\x12:\n\x0chttp_headers\x18\x01 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x37\n\thttp_body\x18\x02 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12:\n\x0crpc_metadata\x18\x03 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x36\n\x08rpc_body\x18\x04 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x38\n\x13\x62ody_max_size_bytes\x18\x05 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\"C\n\tJavaAgent\x12\x36\n\x10\x66ilter_jar_paths\x18\x01 \x03(\x0b\x32\x1c.google.protobuf.StringValue*-\n\x11PropagationFormat\x12\x06\n\x02\x42\x33\x10\x00\x12\x10\n\x0cTRACECONTEXT\x10\x01*:\n\x11TraceReporterType\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\n\n\x06ZIPKIN\x10\x01\x12\x08\n\x04OTLP\x10\x02\x42\x43\n\x1borg.hypertrace.agent.configZ$github.com/hypertrace/goagent/configb\x06proto3' + serialized_options=_b('\n\033org.hypertrace.agent.configZ$github.com/hypertrace/goagent/config'), + serialized_pb=_b('\n\x0c\x63onfig.proto\x12\x1borg.hypertrace.agent.config\x1a\x1egoogle/protobuf/wrappers.proto\"\x8b\x04\n\x0b\x41gentConfig\x12\x32\n\x0cservice_name\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x39\n\treporting\x18\x02 \x01(\x0b\x32&.org.hypertrace.agent.config.Reporting\x12>\n\x0c\x64\x61ta_capture\x18\x03 \x01(\x0b\x32(.org.hypertrace.agent.config.DataCapture\x12K\n\x13propagation_formats\x18\x04 \x03(\x0e\x32..org.hypertrace.agent.config.PropagationFormat\x12+\n\x07\x65nabled\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x39\n\tjavaagent\x18\x06 \x01(\x0b\x32&.org.hypertrace.agent.config.JavaAgent\x12]\n\x13resource_attributes\x18\x07 \x03(\x0b\x32@.org.hypertrace.agent.config.AgentConfig.ResourceAttributesEntry\x1a\x39\n\x17ResourceAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x90\x02\n\tReporting\x12.\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x06secure\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12+\n\x05token\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x03opa\x18\x04 \x01(\x0b\x32 .org.hypertrace.agent.config.Opa\x12K\n\x13trace_reporter_type\x18\x05 \x01(\x0e\x32..org.hypertrace.agent.config.TraceReporterType\"\x9c\x01\n\x03Opa\x12.\n\x08\x65ndpoint\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x38\n\x13poll_period_seconds\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\x12+\n\x07\x65nabled\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\"d\n\x07Message\x12+\n\x07request\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12,\n\x08response\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\"\xb0\x02\n\x0b\x44\x61taCapture\x12:\n\x0chttp_headers\x18\x01 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x37\n\thttp_body\x18\x02 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12:\n\x0crpc_metadata\x18\x03 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x36\n\x08rpc_body\x18\x04 \x01(\x0b\x32$.org.hypertrace.agent.config.Message\x12\x38\n\x13\x62ody_max_size_bytes\x18\x05 \x01(\x0b\x32\x1b.google.protobuf.Int32Value\"C\n\tJavaAgent\x12\x36\n\x10\x66ilter_jar_paths\x18\x01 \x03(\x0b\x32\x1c.google.protobuf.StringValue*-\n\x11PropagationFormat\x12\x06\n\x02\x42\x33\x10\x00\x12\x10\n\x0cTRACECONTEXT\x10\x01*:\n\x11TraceReporterType\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\n\n\x06ZIPKIN\x10\x01\x12\x08\n\x04OTLP\x10\x02\x42\x43\n\x1borg.hypertrace.agent.configZ$github.com/hypertrace/goagent/configb\x06proto3') , dependencies=[google_dot_protobuf_dot_wrappers__pb2.DESCRIPTOR,]) @@ -30,18 +30,15 @@ full_name='org.hypertrace.agent.config.PropagationFormat', filename=None, file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='B3', index=0, number=0, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='TRACECONTEXT', index=1, number=1, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), ], containing_type=None, serialized_options=None, @@ -56,23 +53,19 @@ full_name='org.hypertrace.agent.config.TraceReporterType', filename=None, file=DESCRIPTOR, - create_key=_descriptor._internal_create_key, values=[ _descriptor.EnumValueDescriptor( name='UNSPECIFIED', index=0, number=0, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='ZIPKIN', index=1, number=1, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), _descriptor.EnumValueDescriptor( name='OTLP', index=2, number=2, serialized_options=None, - type=None, - create_key=_descriptor._internal_create_key), + type=None), ], containing_type=None, serialized_options=None, @@ -96,29 +89,28 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='key', full_name='org.hypertrace.agent.config.AgentConfig.ResourceAttributesEntry.key', index=0, number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), + has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='value', full_name='org.hypertrace.agent.config.AgentConfig.ResourceAttributesEntry.value', index=1, number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=b"".decode('utf-8'), + has_default_value=False, default_value=_b("").decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], nested_types=[], enum_types=[ ], - serialized_options=b'8\001', + serialized_options=_b('8\001'), is_extendable=False, syntax='proto3', extension_ranges=[], @@ -134,7 +126,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='service_name', full_name='org.hypertrace.agent.config.AgentConfig.service_name', index=0, @@ -142,49 +133,49 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='reporting', full_name='org.hypertrace.agent.config.AgentConfig.reporting', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='data_capture', full_name='org.hypertrace.agent.config.AgentConfig.data_capture', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='propagation_formats', full_name='org.hypertrace.agent.config.AgentConfig.propagation_formats', index=3, number=4, type=14, cpp_type=8, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='enabled', full_name='org.hypertrace.agent.config.AgentConfig.enabled', index=4, number=5, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='javaagent', full_name='org.hypertrace.agent.config.AgentConfig.javaagent', index=5, number=6, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='resource_attributes', full_name='org.hypertrace.agent.config.AgentConfig.resource_attributes', index=6, number=7, type=11, cpp_type=10, label=3, has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -208,7 +199,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='endpoint', full_name='org.hypertrace.agent.config.Reporting.endpoint', index=0, @@ -216,35 +206,35 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='secure', full_name='org.hypertrace.agent.config.Reporting.secure', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='token', full_name='org.hypertrace.agent.config.Reporting.token', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='opa', full_name='org.hypertrace.agent.config.Reporting.opa', index=3, number=4, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='trace_reporter_type', full_name='org.hypertrace.agent.config.Reporting.trace_reporter_type', index=4, number=5, type=14, cpp_type=8, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -268,7 +258,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='endpoint', full_name='org.hypertrace.agent.config.Opa.endpoint', index=0, @@ -276,21 +265,21 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='poll_period_seconds', full_name='org.hypertrace.agent.config.Opa.poll_period_seconds', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='enabled', full_name='org.hypertrace.agent.config.Opa.enabled', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -314,7 +303,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='request', full_name='org.hypertrace.agent.config.Message.request', index=0, @@ -322,14 +310,14 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='response', full_name='org.hypertrace.agent.config.Message.response', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -353,7 +341,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='http_headers', full_name='org.hypertrace.agent.config.DataCapture.http_headers', index=0, @@ -361,35 +348,35 @@ has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='http_body', full_name='org.hypertrace.agent.config.DataCapture.http_body', index=1, number=2, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='rpc_metadata', full_name='org.hypertrace.agent.config.DataCapture.rpc_metadata', index=2, number=3, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='rpc_body', full_name='org.hypertrace.agent.config.DataCapture.rpc_body', index=3, number=4, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), _descriptor.FieldDescriptor( name='body_max_size_bytes', full_name='org.hypertrace.agent.config.DataCapture.body_max_size_bytes', index=4, number=5, type=11, cpp_type=10, label=1, has_default_value=False, default_value=None, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -413,7 +400,6 @@ filename=None, file=DESCRIPTOR, containing_type=None, - create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='filter_jar_paths', full_name='org.hypertrace.agent.config.JavaAgent.filter_jar_paths', index=0, @@ -421,7 +407,7 @@ has_default_value=False, default_value=[], message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, - serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + serialized_options=None, file=DESCRIPTOR), ], extensions=[ ], @@ -472,54 +458,54 @@ DESCRIPTOR.enum_types_by_name['TraceReporterType'] = _TRACEREPORTERTYPE _sym_db.RegisterFileDescriptor(DESCRIPTOR) -AgentConfig = _reflection.GeneratedProtocolMessageType('AgentConfig', (_message.Message,), { +AgentConfig = _reflection.GeneratedProtocolMessageType('AgentConfig', (_message.Message,), dict( - 'ResourceAttributesEntry' : _reflection.GeneratedProtocolMessageType('ResourceAttributesEntry', (_message.Message,), { - 'DESCRIPTOR' : _AGENTCONFIG_RESOURCEATTRIBUTESENTRY, - '__module__' : 'config_pb2' + ResourceAttributesEntry = _reflection.GeneratedProtocolMessageType('ResourceAttributesEntry', (_message.Message,), dict( + DESCRIPTOR = _AGENTCONFIG_RESOURCEATTRIBUTESENTRY, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.AgentConfig.ResourceAttributesEntry) - }) + )) , - 'DESCRIPTOR' : _AGENTCONFIG, - '__module__' : 'config_pb2' + DESCRIPTOR = _AGENTCONFIG, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.AgentConfig) - }) + )) _sym_db.RegisterMessage(AgentConfig) _sym_db.RegisterMessage(AgentConfig.ResourceAttributesEntry) -Reporting = _reflection.GeneratedProtocolMessageType('Reporting', (_message.Message,), { - 'DESCRIPTOR' : _REPORTING, - '__module__' : 'config_pb2' +Reporting = _reflection.GeneratedProtocolMessageType('Reporting', (_message.Message,), dict( + DESCRIPTOR = _REPORTING, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.Reporting) - }) + )) _sym_db.RegisterMessage(Reporting) -Opa = _reflection.GeneratedProtocolMessageType('Opa', (_message.Message,), { - 'DESCRIPTOR' : _OPA, - '__module__' : 'config_pb2' +Opa = _reflection.GeneratedProtocolMessageType('Opa', (_message.Message,), dict( + DESCRIPTOR = _OPA, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.Opa) - }) + )) _sym_db.RegisterMessage(Opa) -Message = _reflection.GeneratedProtocolMessageType('Message', (_message.Message,), { - 'DESCRIPTOR' : _MESSAGE, - '__module__' : 'config_pb2' +Message = _reflection.GeneratedProtocolMessageType('Message', (_message.Message,), dict( + DESCRIPTOR = _MESSAGE, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.Message) - }) + )) _sym_db.RegisterMessage(Message) -DataCapture = _reflection.GeneratedProtocolMessageType('DataCapture', (_message.Message,), { - 'DESCRIPTOR' : _DATACAPTURE, - '__module__' : 'config_pb2' +DataCapture = _reflection.GeneratedProtocolMessageType('DataCapture', (_message.Message,), dict( + DESCRIPTOR = _DATACAPTURE, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.DataCapture) - }) + )) _sym_db.RegisterMessage(DataCapture) -JavaAgent = _reflection.GeneratedProtocolMessageType('JavaAgent', (_message.Message,), { - 'DESCRIPTOR' : _JAVAAGENT, - '__module__' : 'config_pb2' +JavaAgent = _reflection.GeneratedProtocolMessageType('JavaAgent', (_message.Message,), dict( + DESCRIPTOR = _JAVAAGENT, + __module__ = 'config_pb2' # @@protoc_insertion_point(class_scope:org.hypertrace.agent.config.JavaAgent) - }) + )) _sym_db.RegisterMessage(JavaAgent) diff --git a/src/hypertrace/agent/config/file.py b/src/hypertrace/agent/config/file.py index ac5faf08..f381fe5f 100644 --- a/src/hypertrace/agent/config/file.py +++ b/src/hypertrace/agent/config/file.py @@ -18,7 +18,7 @@ def load_config_from_file(filepath): try: path = os.path.abspath(filepath) - file = open(path, 'r') + file = open(path, 'r') # pylint: disable=R1732 from_file_config = yaml.load(file, Loader=yaml.FullLoader) file.close() diff --git a/src/hypertrace/agent/init/__init__.py b/src/hypertrace/agent/init/__init__.py index 3ec14d5b..7d648756 100644 --- a/src/hypertrace/agent/init/__init__.py +++ b/src/hypertrace/agent/init/__init__.py @@ -34,7 +34,6 @@ def __init__(self, agent_config: AgentConfig): "requests": False, "aiohttp_client": False } - self._tracer_provider = None try: @@ -123,12 +122,30 @@ def init_instrumentation_flask(self, app) -> None: '''Creates a flask instrumentation wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.flaskInit().') try: + if self.is_registered('flask'): + return from hypertrace.agent.instrumentation.flask import FlaskInstrumentorWrapper # pylint: disable=C0415 self._modules_initialized['flask'] = True self._flask_instrumentor_wrapper = FlaskInstrumentorWrapper() - self._flask_instrumentor_wrapper.instrument_app(app) + call_default_instrumentor = True + # There are two ways to initialize the flask instrumenation + # wrapper. The first (and original way) instruments the specific + # Flask object that is passed in). The second way is to globally + # replace the Flask class definition with the hypertrace instrumentation + # wrapper class. + # + # If an app object is provided, then the flask wrapper is initialized + # by calling the instrument_app method. Then, there is no need to call + # instrument() (so, we pass False as the second argument to + # self.init_instrumentor_wrapper_base_for_http(). + # + # If no app object was provided, then instrument() is called. + if app: + self._flask_instrumentor_wrapper.instrument_app(app) + call_default_instrumentor = False self.init_instrumentor_wrapper_base_for_http( - self._flask_instrumentor_wrapper) + self._flask_instrumentor_wrapper, + call_default_instrumentor) except Exception as err: # pylint: disable=W0703 logger.error(constants.INST_WRAP_EXCEPTION_MSSG, 'flask', @@ -139,6 +156,8 @@ def init_instrumentation_grpc_server(self) -> None: '''Creates a grpc server wrapper based on hypertrace config''' logger.debug('Calling AgentInit.grpcServerInit') try: + if self.is_registered('grpc:server'): + return from hypertrace.agent.instrumentation.grpc import ( # pylint: disable=C0415 GrpcInstrumentorServerWrapper ) @@ -166,6 +185,8 @@ def init_instrumentation_grpc_client(self) -> None: '''Creates a grpc client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.grpcClientInit') try: + if self.is_registered('grpc:client'): + return from hypertrace.agent.instrumentation.grpc import ( # pylint: disable=C0415 GrpcInstrumentorClientWrapper ) @@ -194,6 +215,8 @@ def init_instrumentation_mysql(self) -> None: '''Creates a mysql server wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.mysqlInit()') try: + if self.is_registered('mysql'): + return from hypertrace.agent.instrumentation.mysql import ( # pylint: disable=C0415 MySQLInstrumentorWrapper ) @@ -212,6 +235,8 @@ def init_instrumentation_postgresql(self) -> None: '''Creates a postgresql client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.postgreSQLInit()') try: + if self.is_registered('postgresql'): + return from hypertrace.agent.instrumentation.postgresql import ( # pylint: disable=C0415 PostgreSQLInstrumentorWrapper ) @@ -230,6 +255,8 @@ def init_instrumentation_requests(self) -> None: '''Creates a requests client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.requestsInit()') try: + if self.is_registered('requests'): + return from hypertrace.agent.instrumentation.requests import ( # pylint: disable=C0415 RequestsInstrumentorWrapper ) @@ -248,6 +275,8 @@ def aiohttp_client_init(self) -> None: '''Creates an aiohttp-client wrapper using the config defined in hypertraceconfig''' logger.debug('Calling AgentInit.aioHttpClientInit()') try: + if self.is_registered('aiohttp_client'): + return from hypertrace.agent.instrumentation.aiohttp import ( # pylint: disable=C0415 AioHttpClientInstrumentorWrapper ) @@ -262,10 +291,13 @@ def aiohttp_client_init(self) -> None: traceback.format_exc()) # Common wrapper initialization logic - def init_instrumentor_wrapper_base_for_http(self, instrumentor) -> None: + def init_instrumentor_wrapper_base_for_http(self, + instrumentor, + call_instrument: bool = True) -> None: '''Common wrapper initialization logic''' logger.debug('Calling AgentInit.initInstrumentorWrapperBaseForHTTP().') - instrumentor.instrument() + if call_instrument: + instrumentor.instrument() instrumentor.set_process_request_headers( self._config.agent_config.data_capture.http_headers.request) instrumentor.set_process_request_body( @@ -324,3 +356,10 @@ def _init_otlp_exporter(self) -> None: logger.error('Failed to initialize OTLP exporter: exception=%s, stacktrace=%s', err, traceback.format_exc()) + + def is_registered(self, module: str) -> bool: + '''Is an instrumentation module already registered?''' + try: + return self._modules_initialized[module] + except Exception as err: # pylint: disable=W0703,W0612 + return False diff --git a/src/hypertrace/agent/instrumentation/flask/__init__.py b/src/hypertrace/agent/instrumentation/flask/__init__.py index ee831c3d..4e50106f 100644 --- a/src/hypertrace/agent/instrumentation/flask/__init__.py +++ b/src/hypertrace/agent/instrumentation/flask/__init__.py @@ -1,4 +1,4 @@ -'''Hypertrace flask instrumentor module wrapper.''' +'''Hypertrace flask instrumentor module wrapper.''' # pylint: disable=R0401 import sys import os.path import logging @@ -7,6 +7,7 @@ import json import flask from opentelemetry.instrumentation.flask import ( + _InstrumentedFlask, FlaskInstrumentor, get_default_span_name, _teardown_request, @@ -14,6 +15,8 @@ ) from hypertrace.agent import constants # pylint: disable=R0801 from hypertrace.agent.instrumentation import BaseInstrumentorWrapper +from hypertrace.agent.init import AgentInit +from hypertrace.agent.config import AgentConfig # Initialize logger logger = logging.getLogger(__name__) # pylint: disable=C0103 @@ -69,8 +72,6 @@ def hypertrace_before_request() -> None: return hypertrace_before_request # Per request post-handler - - def _hypertrace_after_request(flask_wrapper) -> flask.wrappers.Response: '''This function is invoked by flask to set the handler''' def hypertrace_after_request(response): @@ -100,8 +101,26 @@ def hypertrace_after_request(response): return hypertrace_after_request - - +class _HypertraceInstrumentedFlask(_InstrumentedFlask, BaseInstrumentorWrapper): + """Hypertrace Wrapper class around OTel _InstrumentedFlask. This replaces + the flask.Flask class definition.""" + + def __init__(self, *args, **kwargs): + _InstrumentedFlask.__init__(self,*args, **kwargs) + BaseInstrumentorWrapper.__init__(self) + self.before_request(_hypertrace_before_request(self)) + self.after_request(_hypertrace_after_request(self)) + config: AgentConfig = AgentConfig() + self.set_process_request_headers( + config.agent_config.data_capture.http_headers.request) + self.set_process_request_body( + config.agent_config.data_capture.http_body.request) + self.set_process_response_headers( + config.agent_config.data_capture.http_headers.response) + self.set_process_response_body( + config.agent_config.data_capture.http_body.response) + self.set_body_max_size( + config.agent_config.data_capture.body_max_size_bytes) # Main Flask Instrumentor Wrapper class. class FlaskInstrumentorWrapper(FlaskInstrumentor, BaseInstrumentorWrapper): @@ -112,11 +131,21 @@ def __init__(self): super().__init__() self._app = None - - + def _instrument(self, **kwargs): + '''Override OTel method that sets up global flask instrumentation''' + self._original_flask = flask.Flask # pylint: disable = W0201 + name_callback = kwargs.get("name_callback") + tracer_provider = kwargs.get("tracer_provider") + if callable(name_callback): + _HypertraceInstrumentedFlask.name_callback = name_callback + _HypertraceInstrumentedFlask._tracer_provider = tracer_provider # pylint: disable=W0212 + flask.Flask = _HypertraceInstrumentedFlask # Initialize instrumentation wrapper - def instrument_app(self, app, name_callback=get_default_span_name) -> None: + def instrument_app(self, + app, + name_callback=get_default_span_name, + tracer_provider=None) -> None: '''Initialize instrumentation''' logger.debug('Entering FlaskInstrumentorWrapper.instument_app().') try: diff --git a/tests/autoinstrumentation/docker-compose.yml b/tests/autoinstrumentation/docker-compose.yml new file mode 100644 index 00000000..b08dfea9 --- /dev/null +++ b/tests/autoinstrumentation/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.1' + +services: + mysqldb: + image: mysql + container_name: mysqldb + command: --default-authentication-plugin=mysql_native_password + restart: always + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: hypertrace + volumes: + - ./sql:/docker-entrypoint-initdb.d + - ./docker-healthcheck:/docker-healthcheck + healthcheck: + test: [ "CMD", "/docker-healthcheck" ] + timeout: 40s + interval: 3s + retries: 20 diff --git a/tests/autoinstrumentation/docker-healthcheck b/tests/autoinstrumentation/docker-healthcheck new file mode 100755 index 00000000..6c22eedc --- /dev/null +++ b/tests/autoinstrumentation/docker-healthcheck @@ -0,0 +1,32 @@ +#!/bin/bash + +# Copied from https://github.com/docker-library/healthcheck/blob/master/mysql/docker-healthcheck + +set -eo pipefail + +if [ "$MYSQL_RANDOM_ROOT_PASSWORD" ] && [ -z "$MYSQL_USER" ] && [ -z "$MYSQL_PASSWORD" ]; then + # there's no way we can guess what the random MySQL password was + echo >&2 'healthcheck error: cannot determine random root password (and MYSQL_USER and MYSQL_PASSWORD were not set)' + exit 0 +fi + +host="$(hostname --ip-address || echo '127.0.0.1')" +user="${MYSQL_USER:-root}" +export MYSQL_PWD="${MYSQL_PASSWORD:-$MYSQL_ROOT_PASSWORD}" + +args=( + # force mysql to not use the local "mysqld.sock" (test "external" connectibility) + -h"$host" + -u"$user" + --silent +) + +if command -v mysqladmin &> /dev/null; then + if mysqladmin "${args[@]}" ping > /dev/null; then + if select="$(echo 'SELECT 1' | mysql "${args[@]}")" && [ "$select" = '1' ]; then + exit 0 + fi + fi +fi + +exit 1 \ No newline at end of file diff --git a/tests/autoinstrumentation/requirements.txt b/tests/autoinstrumentation/requirements.txt new file mode 100644 index 00000000..d63e7fb5 --- /dev/null +++ b/tests/autoinstrumentation/requirements.txt @@ -0,0 +1,17 @@ +opentelemetry-api==1.1.0 +opentelemetry-instrumentation +opentelemetry-exporter-zipkin +opentelemetry-exporter-otlp==1.1.0 +opentelemetry-instrumentation-flask +opentelemetry-instrumentation-mysql +opentelemetry-instrumentation-requests +opentelemetry-instrumentation-wsgi +opentelemetry-util-http +google +protobuf +pyyaml +flask +pytest +pytest-xdist +mysql-connector-python==8.0.23 + diff --git a/tests/autoinstrumentation/sql/docker-compose.yml b/tests/autoinstrumentation/sql/docker-compose.yml new file mode 100644 index 00000000..148c3250 --- /dev/null +++ b/tests/autoinstrumentation/sql/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.1' + +services: + + mysqldb: + image: mysql + command: --default-authentication-plugin=mysql_native_password + restart: always + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql diff --git a/tests/autoinstrumentation/sql/init.sql b/tests/autoinstrumentation/sql/init.sql new file mode 100644 index 00000000..7ec704d3 --- /dev/null +++ b/tests/autoinstrumentation/sql/init.sql @@ -0,0 +1,4 @@ +create table hypertrace_data( + col1 INT NOT NULL, + col2 VARCHAR(100) NOT NULL +); diff --git a/tests/autoinstrumentation/test_flask_1.py b/tests/autoinstrumentation/test_flask_1.py new file mode 100644 index 00000000..077e038e --- /dev/null +++ b/tests/autoinstrumentation/test_flask_1.py @@ -0,0 +1,96 @@ +import sys +import os +import logging +import flask +import pytest +import traceback +import json +from werkzeug.serving import make_server +from flask import request +import time +import atexit +import threading +from flask import Flask +#from opentelemetry.exporter.jaeger.thrift import JaegerExporter +from opentelemetry import trace as trace_api +from opentelemetry.sdk.trace import TracerProvider, export +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor + +def setup_custom_logger(name): + try: + formatter = logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') + handler = logging.FileHandler('agent.log', mode='a') + handler.setFormatter(formatter) + screen_handler = logging.StreamHandler(stream=sys.stdout) + screen_handler.setFormatter(formatter) + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + logger.addHandler(handler) + logger.addHandler(screen_handler) + return logger + except: + print('Failed to customize logger: exception=%s, stacktrace=%s', + sys.exc_info()[0], + traceback.format_exc()) + +# Run the flask web server in a separate thread +class FlaskServer(threading.Thread): + def __init__(self, app): + super().__init__() + self.daemon = True + threading.Thread.__init__(self) + self.srv = make_server('localhost', 5000, app) + self.ctx = app.app_context() + self.ctx.push() + + def run(self): + logger.info('starting server.') + self.srv.serve_forever() + self.start() + +def test_run(): + logger = setup_custom_logger(__name__) + logger.info('Initializing flask app.') + # Create Flask app + app = Flask(__name__) + @app.before_first_request + def before_first_request(): + logger.debug("test_program: before_first_request() called") + + @app.route("/route1") + def testAPI1(): + logger.info('Serving request for /route1.') + response = flask.Response(mimetype='application/json') + response.headers['tester3'] = 'tester3' + response.data = str('{ "a": "a", "xyz": "xyz" }') + return response + + logger.info('Flask app initialized.') + + server = FlaskServer(app) + + logger.info('Running test calls.') + with app.test_client() as c: + try: + logger.info('Making test call to /route1') + r1 = app.test_client().get( + 'http://localhost:5000/route1', + headers={ 'tester1': 'tester1', + 'tester2':'tester2' + } + ) + logger.info('Reading /route1 response.') + a1 = r1.get_json()['a'] + assert a1 == 'a' + logger.info('r1 result: ' + str(a1)) + return 0 + except: + logger.error('Failed to initialize flask instrumentation wrapper: exception=%s, stacktrace=%s', + sys.exc_info()[0], + traceback.format_exc()) + raise sys.exc_info()[0] + +if __name__ == '__main__': + test_run() diff --git a/tests/autoinstrumentation/test_flask_1.sh b/tests/autoinstrumentation/test_flask_1.sh new file mode 100755 index 00000000..dad9c29f --- /dev/null +++ b/tests/autoinstrumentation/test_flask_1.sh @@ -0,0 +1,61 @@ +#!/bin/bash +PYTHON_PATH=../../src:$PYTHON_PATH +SPAN=`HT_INSTRUMENTED_MODULES="flask" HT_ENABLE_CONSOLE_SPAN_EXPORTER=True python ../../src/hypertrace/agent/autoinstrumentation/hypertrace_instrument.py python test_flask_1.py | egrep -v "INFO|DEBUG"` +echo SPAN=${SPAN} +TRACE_ID=`echo ${SPAN} | jq .context.trace_id | sed 's/\"//g'` +echo TRACE_ID=$TRACE_ID +if [ -z "${TRACE_ID}" ]; +then + echo "Didn't find TRACE_ID." + exit 1 +fi +METHOD=`echo ${SPAN} | jq '.attributes."http.method"' | sed 's/\"//g'` +echo METHOD=${METHOD} +if [ "${METHOD}" != "GET" ]; +then + echo "Didn't find METHOD." + exit 1 +fi +TARGET=`echo ${SPAN} | jq '.attributes."http.target"' | sed 's/\"//g'` +echo TARGET=${TARGET} +if [ "${TARGET}" != "/route1" ]; +then + echo "Didn't find TARGET." + exit 1 +fi +RESPONSE_CONTENT_TYPE=`echo ${SPAN} | jq '.attributes."http.response.header.content-type"' | sed 's/\"//g'` +echo RESPONSE_CONTENT_TYPE=${RESPONSE_CONTENT_TYPE} +if [ "${RESPONSE_CONTENT_TYPE}" != "application/json" ]; +then + echo "Didn't find RESPONSE_CONTENT_TYPE." + exit 1 +fi +CUSTOM_REQUEST_HEADER=`echo ${SPAN} | jq '.attributes."http.request.header.tester1"' | sed 's/\"//g'` +echo CUSTOM_REQUEST_HEADER=${CUSTOM_REQUEST_HEADER} +if [ "${CUSTOM_REQUEST_HEADER}" != "tester1" ]; +then + echo "Didn't find CUSTOM_REQUEST_HEADER." + exit 1 +fi +CUSTOM_RESPONSE_HEADER=`echo ${SPAN} | jq '.attributes."http.response.header.tester3"' | sed 's/\"//g'` +echo CUSTOM_RESPONSE_HEADER=${CUSTOM_RESPONSE_HEADER} +if [ "${CUSTOM_RESPONSE_HEADER}" != "tester3" ]; +then + echo "Didn't find CUSTOM_RESPONSE_HEADER." + exit 1 +fi +RESPONSE_BODY=`echo ${SPAN} | jq '.attributes."http.response.body"' | sed 's/\"//g'` +echo RESPONSE_BODY=${RESPONSE_BODY} +if [ "${RESPONSE_BODY}" != "{ \\"a\\": \\"a\\", \\"xyz\\": \\"xyz\\" }" ]; +then + echo "Didn't find RESPONSE_BODY." + exit 1 +fi +HTTP_STATUS_CODE=`echo ${SPAN} | jq '.attributes."http.status_code"' | sed 's/\"//g'` +echo HTTP_STATUS_CODE=${HTTP_STATUS_CODE} +if [ "${HTTP_STATUS_CODE}" != "200" ]; +then + echo "Didn't find HTTP_STATUS_CODE." + exit 1 +fi +exit 0 diff --git a/tests/autoinstrumentation/test_flask_2.py b/tests/autoinstrumentation/test_flask_2.py new file mode 100644 index 00000000..cb0c303a --- /dev/null +++ b/tests/autoinstrumentation/test_flask_2.py @@ -0,0 +1,116 @@ +import sys +import os +import logging +import flask +import pytest +import traceback +import json +from werkzeug.serving import make_server +from flask import request +import time +import atexit +import threading +from flask import Flask +import mysql.connector + +def setup_custom_logger(name): + try: + formatter = logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') + handler = logging.FileHandler('agent.log', mode='a') + handler.setFormatter(formatter) + screen_handler = logging.StreamHandler(stream=sys.stdout) + screen_handler.setFormatter(formatter) + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + logger.addHandler(handler) + logger.addHandler(screen_handler) + return logger + except: + print('Failed to customize logger: exception=%s, stacktrace=%s', + sys.exc_info()[0], + traceback.format_exc()) + +# Run the flask web server in a separate thread +class FlaskServer(threading.Thread): + def __init__(self, app): + super().__init__() + self.daemon = True + threading.Thread.__init__(self) + self.srv = make_server('localhost', 5000, app) + self.ctx = app.app_context() + self.ctx.push() + + def run(self): + logger.info('starting server.') + self.srv.serve_forever() + self.start() + +def test_run(): + logger = setup_custom_logger(__name__) + logger.info('Initializing flask app.') + # Create Flask app + app = Flask(__name__) + @app.before_first_request + def before_first_request(): + logger.debug("test_program: before_first_request() called") + + @app.route("/route1") + def testAPI1(): + logger.info('Serving request for /route1.') + try: + logger.info('Making connection to mysql.') + cnx = mysql.connector.connect(database='hypertrace', + username='root', + password='root', + host='localhost', + port=3306) + logger.info('Connect successfully.') + cursor = cnx.cursor() + logger.info('Running INSERT statement.') + cursor.execute( + "INSERT INTO hypertrace_data (col1, col2) VALUES (123, 'abcdefghijklmnopqrstuvwxyz')") + logger.info('Statement ran successfully') + logger.info('Closing cursor.') + cursor.close() + logger.info('Closing connection.') + cnx.close() + logger.info('Connection closed.') + + response = flask.Response(mimetype='application/json') + response.headers['tester3'] = 'tester3' + response.data = str('{ "a": "a", "xyz": "xyz" }') + return response + except: + logger.error('Failed to initialize mysql instrumentation wrapper: exception=%s, stacktrace=%s', + sys.exc_info()[0], + traceback.format_exc()) + raise sys.exc_info()[0] + + logger.info('Flask app initialized.') + + server = FlaskServer(app) + + logger.info('Running test calls.') + with app.test_client() as c: + try: + logger.info('Making test call to /route1') + r1 = app.test_client().get( + 'http://localhost:5000/route1', + headers={ 'tester1': 'tester1', + 'tester2':'tester2' + } + ) + logger.info('Reading /route1 response.') + a1 = r1.get_json()['a'] + assert a1 == 'a' + logger.info('r1 result: ' + str(a1)) + return 0 + except: + logger.error('Failed to initialize flask instrumentation wrapper: exception=%s, stacktrace=%s', + sys.exc_info()[0], + traceback.format_exc()) + raise sys.exc_info()[0] + +if __name__ == '__main__': + test_run() diff --git a/tests/autoinstrumentation/test_flask_2.sh b/tests/autoinstrumentation/test_flask_2.sh new file mode 100755 index 00000000..888dfcc5 --- /dev/null +++ b/tests/autoinstrumentation/test_flask_2.sh @@ -0,0 +1,34 @@ +#!/bin/bash +PYTHON_PATH=../../src:$PYTHON_PATH +SPAN=`HT_INSTRUMENTED_MODULES="mysql" HT_ENABLE_CONSOLE_SPAN_EXPORTER=True python ../../src/hypertrace/agent/autoinstrumentation/hypertrace_instrument.py python test_flask_2.py | egrep -v "INFO|DEBUG"` +echo SPAN=${SPAN} +TRACE_ID=`echo ${SPAN} | jq .context.trace_id | sed 's/\"//g'` +echo TRACE_ID=$TRACE_ID +if [ -z "${TRACE_ID}" ]; +then + echo "Didn't find TRACE_ID." + exit 1 +fi +SQL_STATEMENT=`echo ${SPAN} | jq '.attributes."db.statement"' | sed 's/\"//g'` +echo SQL_STATEMENT=${SQL_STATEMENT} +if [ "${SQL_STATEMENT}" != "INSERT INTO hypertrace_data (col1, col2) VALUES (123, 'abcdefghijklmnopqrstuvwxyz')" ]; +then + echo "Didn't find HTTP_STATUS_CODE." + exit 1 +fi +DB_USER=`echo ${SPAN} | jq '.attributes."db.user"' | sed 's/\"//g'` +echo DB_USER=${DB_USER} +if [ "${DB_USER}" != "root" ]; +then + echo "Didn't find DB_USER." + exit 1 +fi +STATUS_CODE=`echo ${SPAN} | jq '.status."status_code"' | sed 's/\"//g'` +echo STATUS_CODE=${STATUS_CODE} +if [ "${STATUS_CODE}" != "UNSET" ]; +then + echo "Didn't find STATUS_CODE." + exit 1 +fi + +exit 0 diff --git a/tests/autoinstrumentation/tox.ini b/tests/autoinstrumentation/tox.ini new file mode 100644 index 00000000..f7871518 --- /dev/null +++ b/tests/autoinstrumentation/tox.ini @@ -0,0 +1,38 @@ +[tox] +skipsdist = true +envlist = py3{7,8,9} + +[testenv] +sitepackages = True +install_command = pip install {opts} {packages} + +deps = + -rrequirements.txt + +whitelist_externals = + pytest + sleep + docker-compose + test_flask_1.sh + bash + test_flask_2.sh + +setenv = + PYTHONPATH=../../src + HT_SERVICE_NAME=python_agent_test1 + HT_LOG_LEVEL={env:HT_LOG_LEVEL:} + HT_ENABLE_CONSOLE_SPAN_EXPORTER=True + +commands = + docker-compose stop mysqldb + # Set selinux permissions on volumes being mounted into docker containers + bash -ec "if [ `uname` = 'Linux' ] && [ "{env:GITHUB_ACTIONS:false}" != "true" ]; then chcon -h system_u:object_r:bin_t:s0 sql; chcon -Rt svirt_sandbox_file_t sql; fi" + bash -ec "if [ `uname` = 'Linux' ] && [ "{env:GITHUB_ACTIONS:false}" != "true" ]; then chcon -h system_u:object_r:bin_t:s0 docker-healthcheck; chcon -Rt svirt_sandbox_file_t docker-healthcheck; fi" + docker-compose up -d --remove-orphans --force-recreate -V mysqldb + bash -ec "set -x; while [ `docker inspect -f '\{\{ .State.Health.Status \}\}' mysqldb` != 'healthy' ]; do echo 'Waiting for MySQL to be up'; sleep 2; done" + ./test_flask_1.sh + ./test_flask_2.sh + docker-compose stop mysqldb + docker-compose down --rmi all + +recreate = True