def _verify_gcp_credentials(self): """If the flag is enabled then attempt to load the trace client used for posting spans to the Trace API.""" if self._post_spans_to_stackdriver_api: try: self.stackdriver_trace_client = TraceServiceClient() except DefaultCredentialsError: raise StackDriverAuthError( 'Cannot post spans to API, no authentication credentials found.' )
def __init__( self, project_id=None, client=None, ): self.client = client or TraceServiceClient() if not project_id: _, self.project_id = google.auth.default() else: self.project_id = project_id
def test_export(self): trace_id = "6e0c63257de34c92bf9efcd03927272e" span_id = "95bb5edabd45950f" # Create span and associated data. resource_info = Resource( { "cloud.account.id": 123, "host.id": "host", "cloud.zone": "US", "cloud.provider": "gcp", "gcp.resource_type": "gce_instance", } ) span = Span( name="span_name", context=SpanContext( trace_id=int(trace_id, 16), span_id=int(span_id, 16), is_remote=False, ), parent=None, kind=SpanKind.INTERNAL, resource=resource_info, attributes={"attr_key": "attr_value"}, ) # pylint: disable=protected-access span._start_time = int(time_ns() - (60 * 1e9)) span._end_time = time_ns() span_data = [span] # Setup the trace exporter. channel = grpc.insecure_channel(self.address) transport = trace_service_grpc_transport.TraceServiceGrpcTransport( channel=channel ) client = TraceServiceClient(transport=transport) trace_exporter = CloudTraceSpanExporter(self.project_id, client=client) # Export the spans and verify the results. result = trace_exporter.export(span_data) self.assertEqual(result, SpanExportResult.SUCCESS)
class Tracer: def __init__(self, json_logger_factory, post_spans_to_stackdriver_api=False): """ Class to manage creation and deletion of spans. This should be initialised once within an app then reused across it. Arguments: json_logger_factory (logtracer.jsonlog.JSONLoggerFactory): logger factory instance to attach for logging tracing events. post_spans_to_stackdriver_api (bool): toggle for posting spans to the Stackdriver API (requires google credentials) Attributes: self.project_name (str): Name of your project, the GCP project name if posting to Stackdriver Trace self.service_name (str): Name of your service self.logger (logging.Logger): Logger to be used to log trace-related events self.requests (logtracer.tracing.RequestsWrapper): a wrapper for the `requests` library to conveniently trace outgoing requests self._spans (dict): dict to store span information indexed by span id self._memory (threading.local()): thread local memory to store the current span ID self._post_spans_to_stackdriver_api (bool): toggle for posting spans to Stackdriver API """ self.project_name = json_logger_factory.project_name self.service_name = json_logger_factory.service_name self.logger = json_logger_factory.get_logger('logtracer') self.requests = RequestsWrapper(self) self.unsupported_requests = UnsupportedRequestsWrapper(self) self.stackdriver_trace_client = None self._spans = {} self._memory = None self._post_spans_to_stackdriver_api = post_spans_to_stackdriver_api self._add_tracer_to_logger_formatter(json_logger_factory) self._verify_gcp_credentials() def _verify_gcp_credentials(self): """If the flag is enabled then attempt to load the trace client used for posting spans to the Trace API.""" if self._post_spans_to_stackdriver_api: try: self.stackdriver_trace_client = TraceServiceClient() except DefaultCredentialsError: raise StackDriverAuthError( 'Cannot post spans to API, no authentication credentials found.' ) def _add_tracer_to_logger_formatter(self, json_logger_factory): """Add this instance to the logging formatter to allow the logger to format logs with trace information.""" json_logger_factory.get_logger( ).root.handlers[0].formatter.tracer = self def set_logging_level(self, level): """ Set the logging level of the tracer level (str): 'DEBUG': Span creation, closure, and deletion information (not useful in production) 'INFO': Request logging, used by child classes to log incoming and outgoing requests 'ERROR': Summaries of any errors that have occurred 'EXCEPTION': Stack traces of any exceptions to have occurred """ self.logger.setLevel(level) def start_traced_span(self, incoming_headers, span_name): """ Create a span and set it as the current span in the thread local memory. Retrieves span details from inbound call, otherwise generates new values. Arguments: incoming_headers: Incoming request headers. These could be http, or part of a GRPC message. span_name (str): Path of the endpoint of the incoming request. """ incoming_headers = self._extract_google_trace_headers_if_present( incoming_headers) span_values = { B3_TRACE_ID: incoming_headers.get(B3_TRACE_ID) or generate_identifier(TRACE_LEN), B3_PARENT_SPAN_ID: incoming_headers.get(B3_PARENT_SPAN_ID), B3_SPAN_ID: incoming_headers.get(B3_SPAN_ID) or generate_identifier(SPAN_LEN), B3_SAMPLED: incoming_headers.get(B3_SAMPLED), B3_FLAGS: incoming_headers.get(B3_FLAGS) } span_id = span_values[B3_SPAN_ID] self._spans[span_id] = { "start_timestamp": get_timestamp(), "display_name": f'{self.service_name}:{span_name}', "child_span_count": 0, "values": span_values } self.memory.current_span_id = span_id self.logger.debug(f'Span started {self.memory.current_span_id}') def _extract_google_trace_headers_if_present(self, incoming_headers): """ Extract Google tracing headers from incoming requests if they are present. Regex expression based on: https://groups.google.com/forum/#!topic/google-appengine/ik5fMyvO4PQ More info on format of trace: https://github.com/census-instrumentation/opencensus-python/blob/1df8f58e55a0dd5eeab991b984420ba7a35721b8/openc ensus/trace/propagation/google_cloud_format.py """ if B3_TRACE_ID not in incoming_headers and GOOGLE_LOAD_BALANCER_TRACE_HEADERS in incoming_headers: incoming_headers = dict(incoming_headers) try: match = re.search( _TRACE_CONTEXT_HEADER_RE, incoming_headers[GOOGLE_LOAD_BALANCER_TRACE_HEADERS]) except TypeError: pass else: if match: incoming_headers[B3_TRACE_ID] = match.group(1) incoming_headers[B3_SPAN_ID] = match.group(3) # trace_options = match.group(5) del incoming_headers[GOOGLE_LOAD_BALANCER_TRACE_HEADERS] return incoming_headers @property def current_span(self): """Attempt to return current span data.""" if self.memory.current_span_id is not None: try: return self._spans[self.memory.current_span_id] except KeyError: pass raise SpanNotStartedError('No current span found.') def start_traced_subspan(self, span_name): """Start a traced subspan, for usage with wrapping an unsupported downstream service.""" if self.memory.current_span_id is None: raise SpanNotStartedError( 'Span must be started before starting a subspan') subspan_values = self.generate_new_traced_subspan_values() self.memory.parent_spans.append(self.memory.current_span_id) self.memory.current_span_id = None self.start_traced_span(subspan_values, span_name) def end_traced_subspan(self, exclude_from_posting=False): """Close a traced subspan.""" self.end_traced_span(exclude_from_posting) self.memory.current_span_id = self.memory.parent_spans.pop() def end_traced_span(self, exclude_from_posting=False): """ End a span and collect details about the span, then post it to the API. Arguments: exclude_from_posting (bool): exclude this particular trace from being posted """ self.logger.debug(f'Closing span {self.memory.current_span_id}') if self._post_spans_to_stackdriver_api and not exclude_from_posting: span_values = self.current_span['values'] end_timestamp = get_timestamp() if self._post_spans_to_stackdriver_api: name = self.stackdriver_trace_client.span_path( self.project_name, span_values[B3_TRACE_ID], span_values[B3_SPAN_ID]) else: name = f'{self.project_name}/{span_values[B3_TRACE_ID]}/{span_values[B3_SPAN_ID]}' span_info = { 'name': name, 'span_id': span_values[B3_SPAN_ID], 'display_name': truncate_str(self.current_span['display_name'], limit=SPAN_DISPLAY_NAME_BYTE_LIMIT), 'start_time': self.current_span['start_timestamp'], 'end_time': end_timestamp, 'parent_span_id': span_values[B3_PARENT_SPAN_ID], 'same_process_as_parent_span': BoolValue(value=False), 'child_span_count': Int32Value(value=self.current_span['child_span_count']) } post_to_api_job = Thread(target=post_span, args=(self.stackdriver_trace_client, span_info)) post_to_api_job.start() self._delete_current_span() def _delete_current_span(self): """Deletes span details.""" self.logger.debug(f'Deleting span {self.memory.current_span_id}') del self._spans[self.memory.current_span_id] self.memory.current_span_id = None def generate_new_traced_subspan_values(self): """ For use in a downstream/outbound call. Use this to generate the values to pass to a downstream service. If sending outbound requests to a HTTP service using the `requests` library, then use the `self.requests` wrapper instead of this function to trace outgoing requests. If calling a gRPC service then use the channel interceptor (logtracer.helpers.grpc.interceptors.GRPCTracer) instead of this function. Entries with the value `None` are filtered out. """ self.current_span["child_span_count"] += 1 parent_span_values = self.current_span['values'] subspan_values = { B3_TRACE_ID: parent_span_values[B3_TRACE_ID], B3_PARENT_SPAN_ID: parent_span_values[B3_SPAN_ID], B3_SPAN_ID: generate_identifier(SPAN_LEN), B3_SAMPLED: parent_span_values[B3_SAMPLED], B3_FLAGS: parent_span_values[B3_FLAGS] } subspan_values = {k: v for k, v in subspan_values.items() if v} return subspan_values @property def memory(self): """ Thread local memory for storing the _current_ span id, needed for if this class is used in a multi-threaded environment. """ class SpanMemory(local): def __init__(self): self.current_span_id = None self.parent_spans = [] if self._memory is None: self._memory = SpanMemory() return self._memory