1# -*- coding: utf-8 -*- 2# Copyright 2017 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""A library for emitting traces and spans to Google Cloud trace.""" 7from __future__ import print_function 8 9import contextlib 10import errno 11import functools 12import json 13import os 14import random 15import re 16 17try: 18 from google.protobuf import timestamp_pb2 19except ImportError: 20 import google.protobuf.internal.well_known_types as timestamp_pb2 21 22from infra_libs import ts_mon 23 24from autotest_lib.utils.frozen_chromite.lib import cros_logging as log 25from autotest_lib.utils.frozen_chromite.lib import metrics 26from autotest_lib.utils.frozen_chromite.lib import structured 27 28 29SPANS_LOG = '/var/log/trace/{pid}-{span_id}.json' 30_SPAN_COUNT_METRIC = 'chromeos/trace/client/logged_count' 31 32 33# --- Code for logging spans to a file for later processing. ------------------- 34def GetSpanLogFilePath(span): 35 """Gets the path to write a span to. 36 37 Args: 38 span: The span to write. 39 """ 40 return SPANS_LOG.format(pid=os.getpid(), span_id=span.spanId) 41 42 43def LogSpan(span): 44 """Serializes and logs a Span to a file. 45 46 Args: 47 span: A Span instance to serialize. 48 """ 49 _RecordSpanMetrics(span) 50 try: 51 with open(GetSpanLogFilePath(span), 'w') as fh: 52 fh.write(json.dumps(span.ToDict())) 53 # Catch various configuration errors. 54 # TODO(vapier): Drop IOError when we're Python 3-only. 55 # pylint: disable=overlapping-except 56 except (OSError, IOError) as error: 57 if error.errno == errno.EPERM: 58 log.warning( 59 'Received permissions error while trying to open the span log file.') 60 return None 61 elif error.errno == errno.ENOENT: 62 log.warning('/var/log/traces does not exist; skipping trace log.') 63 return None 64 else: 65 raise 66 67 68def _RecordSpanMetrics(span): 69 """Increments the count of spans logged. 70 71 Args: 72 span: The span to record. 73 """ 74 m = metrics.Counter( 75 _SPAN_COUNT_METRIC, 76 description='A count of spans logged by a client.', 77 field_spec=[ts_mon.StringField('name')]) 78 m.increment(fields={'name': span.name}) 79 80 81# -- User-facing API ----------------------------------------------------------- 82class Span(structured.Structured): 83 """An object corresponding to a cloud trace Span.""" 84 85 VISIBLE_KEYS = ( 86 'name', 'spanId', 'parentSpanId', 'labels', 87 'startTime', 'endTime', 'status') 88 89 def __init__(self, name, spanId=None, labels=None, parentSpanId=None, 90 traceId=None): 91 """Creates a Span object. 92 93 Args: 94 name: The name of the span 95 spanId: (optional) A 64-bit number as a string. If not provided, it will 96 be generated randomly with .GenerateSpanId(). 97 labels: (optional) a dict<string, string> of key/values 98 traceId: (optional) A 32 hex digit string referring to the trace 99 containing this span. If not provided, a new trace will be created 100 with a random id. 101 parentSpanId: (optional) The spanId of the parent. 102 """ 103 # Visible attributes 104 self.name = name 105 self.spanId = spanId or Span.GenerateSpanId() 106 self.parentSpanId = parentSpanId 107 self.labels = labels or {} 108 self.startTime = None 109 self.endTime = None 110 # Non-visible attributes 111 self.traceId = traceId or Span.GenerateTraceId() 112 113 @staticmethod 114 def GenerateSpanId(): 115 """Returns a random 64-bit number as a string.""" 116 return str(random.randint(0, 2**64)) 117 118 @staticmethod 119 def GenerateTraceId(): 120 """Returns a random 128-bit number as a 32-byte hex string.""" 121 id_number = random.randint(0, 2**128) 122 return '%0.32X' % id_number 123 124 def __enter__(self): 125 """Enters the span context. 126 127 Side effect: Records the start time as a Timestamp. 128 """ 129 start = timestamp_pb2.Timestamp() 130 start.GetCurrentTime() 131 self.startTime = start.ToJsonString() 132 return self 133 134 def __exit__(self, _type, _value, _traceback): 135 """Exits the span context. 136 137 Side-effect: 138 Record the end Timestamp. 139 """ 140 end = timestamp_pb2.Timestamp() 141 end.GetCurrentTime() 142 self.endTime = end.ToJsonString() 143 144 145class SpanStack(object): 146 """A stack of Span contexts.""" 147 148 CLOUD_TRACE_CONTEXT_ENV = 'CLOUD_TRACE_CONTEXT' 149 CLOUD_TRACE_CONTEXT_PATTERN = re.compile( 150 r'(?P<traceId>[0-9A-Fa-f]+)' 151 r'/' 152 r'(?P<parentSpanId>\d+)' 153 r';o=(?P<options>\d+)' 154 ) 155 156 def __init__(self, global_context=None, traceId=None, parentSpanId=None, 157 labels=None, enabled=True): 158 """Initializes the Span. 159 160 global_context: (optional) A global context str, perhaps read from the 161 X-Cloud-Trace-Context header. 162 traceId: (optional) A 32 hex digit string referring to the trace 163 containing this span. If not provided, a new trace will be created 164 with a random id. 165 parentSpanId: (optional) The spanId of the parent. 166 labels: (optional) a dict<string, string> of key/values to attach to 167 each Span created, or None. 168 enabled: (optional) a bool indicating whether we should log the spans 169 to a file for later uploading by the cloud trace log consumer daemon. 170 """ 171 self.traceId = traceId 172 self.spans = [] 173 self.last_span_id = parentSpanId 174 self.labels = labels 175 self.enabled = enabled 176 177 global_context = (global_context or 178 os.environ.get(self.CLOUD_TRACE_CONTEXT_ENV, '')) 179 context = SpanStack._ParseCloudTraceContext(global_context) 180 181 if traceId is None: 182 self.traceId = context.get('traceId') 183 if parentSpanId is None: 184 self.last_span_id = context.get('parentSpanId') 185 if context.get('options') == '0': 186 self.enabled = False 187 188 def _CreateSpan(self, name, **kwargs): 189 """Creates a span instance, setting certain defaults. 190 191 Args: 192 name: The name of the span 193 **kwargs: The keyword arguments to configure the span with. 194 """ 195 kwargs.setdefault('traceId', self.traceId) 196 kwargs.setdefault('labels', self.labels) 197 kwargs.setdefault('parentSpanId', self.last_span_id) 198 span = Span(name, **kwargs) 199 if self.traceId is None: 200 self.traceId = span.traceId 201 return span 202 203 @contextlib.contextmanager 204 def Span(self, name, **kwargs): 205 """Enter a new Span context contained within the top Span of the stack. 206 207 Args: 208 name: The name of the span to enter 209 **kwargs: The kwargs to construct the span with. 210 211 Side effect: 212 Appends the new span object to |spans|, and yields span while in its 213 context. Pops the span object when exiting the context. 214 215 Returns: 216 A contextmanager whose __enter__() returns the new Span. 217 """ 218 span = self._CreateSpan(name, **kwargs) 219 old_span_id, self.last_span_id = self.last_span_id, span.spanId 220 self.spans.append(span) 221 222 with span: 223 with self.EnvironmentContext(): 224 yield span 225 226 self.spans.pop() 227 self.last_span_id = old_span_id 228 229 # Log each span to a file for later processing. 230 if self.enabled: 231 LogSpan(span) 232 233 # pylint: disable=docstring-misnamed-args 234 def Spanned(self, *span_args, **span_kwargs): 235 """A decorator equivalent of 'with span_stack.Span(...)' 236 237 Args: 238 *span_args: *args to use with the .Span 239 **span_kwargs: **kwargs to use with the .Span 240 241 Returns: 242 A decorator to wrap the body of a function in a span block. 243 """ 244 def SpannedDecorator(f): 245 """Wraps the body of |f| with a .Span block.""" 246 @functools.wraps(f) 247 def inner(*args, **kwargs): 248 with self.Span(*span_args, **span_kwargs): 249 f(*args, **kwargs) 250 return inner 251 return SpannedDecorator 252 253 def _GetCloudTraceContextHeader(self): 254 """Gets the Cloud Trace HTTP header context. 255 256 From the cloud trace doc explaining this ( 257 https://cloud.google.com/trace/docs/support?hl=bg) 258 259 'X-Cloud-Trace-Context: TRACE_ID/SPAN_ID;o=TRACE_TRUE' 260 Where: 261 - TRACE_ID is a 32-character hex value representing a 128-bit number. 262 It should be unique between your requests, unless you intentionally 263 want to bundle the requests together. You can use UUIDs. 264 - SPAN_ID should be 0 for the first span in your trace. For 265 subsequent requests, set SPAN_ID to the span ID of the parent 266 request. See the description of TraceSpan (REST, RPC) for more 267 information about nested traces. 268 - TRACE_TRUE must be 1 to trace this request. Specify 0 to not trace 269 the request. For example, to force a trace with cURL: 270 curl 'http://www.example.com' --header 'X-Cloud-Trace-Context: 271 105445aa7843bc8bf206b120001000/0;o=1' 272 """ 273 if not self.traceId: 274 return '' 275 span_postfix = '/%s' % self.spans[-1].spanId if self.spans else '' 276 enabled = '1' if self.enabled else '0' 277 return '{trace_id}{span_postfix};o={enabled}'.format( 278 trace_id=self.traceId, 279 span_postfix=span_postfix, 280 enabled=enabled) 281 282 @contextlib.contextmanager 283 def EnvironmentContext(self): 284 """Sets CLOUD_TRACE_CONTEXT to the value of X-Cloud-Trace-Context. 285 286 Cloud Trace uses an HTTP header to propagate trace context across RPC 287 boundaries. This method does the same across process boundaries using an 288 environment variable. 289 """ 290 old_value = os.environ.get(self.CLOUD_TRACE_CONTEXT_ENV) 291 try: 292 os.environ[self.CLOUD_TRACE_CONTEXT_ENV] = ( 293 self._GetCloudTraceContextHeader()) 294 yield 295 finally: 296 if old_value is not None: 297 os.environ[self.CLOUD_TRACE_CONTEXT_ENV] = old_value 298 elif self.CLOUD_TRACE_CONTEXT_ENV in os.environ: 299 del os.environ[self.CLOUD_TRACE_CONTEXT_ENV] 300 301 @staticmethod 302 def _ParseCloudTraceContext(context): 303 """Sets current_span_id and trace_id from the |context|. 304 305 See _GetCloudTraceContextHeader. 306 307 Args: 308 context: The context variable, either from X-Cloud-Trace-Context 309 or from the CLOUD_TRACE_CONTEXT environment variable. 310 311 Returns: 312 A dictionary, which if the context string matches 313 CLOUD_TRACE_CONTEXT_PATTERN, contains the matched groups. If not matched, 314 returns an empty dictionary. 315 """ 316 m = SpanStack.CLOUD_TRACE_CONTEXT_PATTERN.match(context) 317 if m: 318 return m.groupdict() 319 else: 320 return {}