1# Copyright 2016 Google LLC 
    2# 
    3# Licensed under the Apache License, Version 2.0 (the "License"); 
    4# you may not use this file except in compliance with the License. 
    5# You may obtain a copy of the License at 
    6# 
    7#     http://www.apache.org/licenses/LICENSE-2.0 
    8# 
    9# Unless required by applicable law or agreed to in writing, software 
    10# distributed under the License is distributed on an "AS IS" BASIS, 
    11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
    12# See the License for the specific language governing permissions and 
    13# limitations under the License. 
    14 
    15"""Log entries within the Google Cloud Logging API.""" 
    16 
    17import collections 
    18import json 
    19import re 
    20 
    21from google.protobuf.json_format import MessageToDict 
    22from google.protobuf.json_format import Parse 
    23from google.protobuf.message import Message 
    24 
    25from google.cloud.logging_v2.resource import Resource 
    26from google.cloud._helpers import _name_from_project_path 
    27from google.cloud._helpers import _rfc3339_nanos_to_datetime 
    28from google.cloud._helpers import _datetime_to_rfc3339 
    29 
    30# import officially supported proto definitions 
    31import google.cloud.audit.audit_log_pb2  # noqa: F401 
    32import google.cloud.appengine_logging  # noqa: F401 
    33from google.iam.v1.logging import audit_data_pb2  # noqa: F401 
    34 
    35_GLOBAL_RESOURCE = Resource(type="global", labels={}) 
    36 
    37 
    38_LOGGER_TEMPLATE = re.compile( 
    39    r""" 
    40    projects/            # static prefix 
    41    (?P<project>[^/]+)   # initial letter, wordchars + hyphen 
    42    /logs/               # static midfix 
    43    (?P<name>[^/]+)      # initial letter, wordchars + allowed punc 
    44""", 
    45    re.VERBOSE, 
    46) 
    47 
    48 
    49def logger_name_from_path(path, project=None): 
    50    """Validate a logger URI path and get the logger name. 
    51 
    52    Args: 
    53        path (str): URI path for a logger API request 
    54        project (str): The project the path is expected to belong to 
    55 
    56    Returns: 
    57        str: Logger name parsed from ``path``. 
    58 
    59    Raises: 
    60        ValueError: If the ``path`` is ill-formed of if the project 
    61            from ``path`` does not agree with the ``project`` passed in. 
    62    """ 
    63    return _name_from_project_path(path, project, _LOGGER_TEMPLATE) 
    64 
    65 
    66def _int_or_none(value): 
    67    """Helper: return an integer or ``None``.""" 
    68    if value is not None: 
    69        value = int(value) 
    70    return value 
    71 
    72 
    73_LOG_ENTRY_FIELDS = (  # (name, default) 
    74    ("log_name", None), 
    75    ("labels", None), 
    76    ("insert_id", None), 
    77    ("severity", None), 
    78    ("http_request", None), 
    79    ("timestamp", None), 
    80    ("resource", _GLOBAL_RESOURCE), 
    81    ("trace", None), 
    82    ("span_id", None), 
    83    ("trace_sampled", None), 
    84    ("source_location", None), 
    85    ("operation", None), 
    86    ("logger", None), 
    87    ("payload", None), 
    88) 
    89 
    90 
    91_LogEntryTuple = collections.namedtuple( 
    92    "LogEntry", (field for field, _ in _LOG_ENTRY_FIELDS) 
    93) 
    94 
    95_LogEntryTuple.__new__.__defaults__ = tuple(default for _, default in _LOG_ENTRY_FIELDS) 
    96 
    97 
    98_LOG_ENTRY_PARAM_DOCSTRING = """\ 
    99 
    100    Args: 
    101        log_name (str): The name of the logger used to post the entry. 
    102        labels (Optional[dict]): Mapping of labels for the entry 
    103        insert_id (Optional[str]): The ID used to identify an entry 
    104            uniquely. 
    105        severity (Optional[str]): The severity of the event being logged. 
    106        http_request (Optional[dict]): Info about HTTP request associated 
    107            with the entry. 
    108        timestamp (Optional[datetime.datetime]): Timestamp for the entry. 
    109        resource (Optional[google.cloud.logging_v2.resource.Resource]): 
    110            Monitored resource of the entry. 
    111        trace (Optional[str]): Trace ID to apply to the entry. 
    112        span_id (Optional[str]): Span ID within the trace for the log 
    113            entry. Specify the trace parameter if ``span_id`` is set. 
    114        trace_sampled (Optional[bool]): The sampling decision of the trace 
    115            associated with the log entry. 
    116        source_location (Optional[dict]): Location in source code from which 
    117            the entry was emitted. 
    118        operation (Optional[dict]): Additional information about a potentially 
    119            long-running operation associated with the log entry. 
    120        logger (logging_v2.logger.Logger): the logger used 
    121            to write the entry. 
    122""" 
    123 
    124_LOG_ENTRY_SEE_ALSO_DOCSTRING = """\ 
    125 
    126    See: 
    127    https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry 
    128""" 
    129 
    130 
    131class LogEntry(_LogEntryTuple): 
    132    __doc__ = ( 
    133        """ 
    134    Log entry. 
    135 
    136    """ 
    137        + _LOG_ENTRY_PARAM_DOCSTRING 
    138        + _LOG_ENTRY_SEE_ALSO_DOCSTRING 
    139    ) 
    140 
    141    received_timestamp = None 
    142 
    143    @classmethod 
    144    def _extract_payload(cls, resource): 
    145        """Helper for :meth:`from_api_repr`""" 
    146        return None 
    147 
    148    @classmethod 
    149    def from_api_repr(cls, resource, client, *, loggers=None): 
    150        """Construct an entry given its API representation 
    151 
    152        Args: 
    153            resource (dict): text entry resource representation returned from 
    154                the API 
    155            client (~logging_v2.client.Client): 
    156                Client which holds credentials and project configuration. 
    157            loggers (Optional[dict]): 
    158                A mapping of logger fullnames -> loggers.  If not 
    159                passed, the entry will have a newly-created logger if possible, 
    160                or an empty logger field if not. 
    161 
    162        Returns: 
    163            google.cloud.logging.entries.LogEntry: Log entry parsed from ``resource``. 
    164        """ 
    165        if loggers is None: 
    166            loggers = {} 
    167        logger_fullname = resource["logName"] 
    168        logger = loggers.get(logger_fullname) 
    169        if logger is None: 
    170            # attempt to create a logger if possible 
    171            try: 
    172                logger_name = logger_name_from_path(logger_fullname, client.project) 
    173                logger = loggers[logger_fullname] = client.logger(logger_name) 
    174            except ValueError: 
    175                # log name is not scoped to a project. Leave logger as None 
    176                pass 
    177        payload = cls._extract_payload(resource) 
    178        insert_id = resource.get("insertId") 
    179        timestamp = resource.get("timestamp") 
    180        if timestamp is not None: 
    181            timestamp = _rfc3339_nanos_to_datetime(timestamp) 
    182        labels = resource.get("labels") 
    183        severity = resource.get("severity") 
    184        http_request = resource.get("httpRequest") 
    185        trace = resource.get("trace") 
    186        span_id = resource.get("spanId") 
    187        trace_sampled = resource.get("traceSampled") 
    188        source_location = resource.get("sourceLocation") 
    189        if source_location is not None: 
    190            line = source_location.pop("line", None) 
    191            source_location["line"] = _int_or_none(line) 
    192        operation = resource.get("operation") 
    193 
    194        monitored_resource_dict = resource.get("resource") 
    195        monitored_resource = None 
    196        if monitored_resource_dict is not None: 
    197            monitored_resource = Resource._from_dict(monitored_resource_dict) 
    198 
    199        inst = cls( 
    200            log_name=logger_fullname, 
    201            insert_id=insert_id, 
    202            timestamp=timestamp, 
    203            labels=labels, 
    204            severity=severity, 
    205            http_request=http_request, 
    206            resource=monitored_resource, 
    207            trace=trace, 
    208            span_id=span_id, 
    209            trace_sampled=trace_sampled, 
    210            source_location=source_location, 
    211            operation=operation, 
    212            logger=logger, 
    213            payload=payload, 
    214        ) 
    215        received = resource.get("receiveTimestamp") 
    216        if received is not None: 
    217            inst.received_timestamp = _rfc3339_nanos_to_datetime(received) 
    218        return inst 
    219 
    220    def to_api_repr(self): 
    221        """API repr (JSON format) for entry.""" 
    222        info = {} 
    223        if self.log_name is not None: 
    224            info["logName"] = self.log_name 
    225        if self.resource is not None: 
    226            info["resource"] = self.resource._to_dict() 
    227        if self.labels is not None: 
    228            info["labels"] = self.labels 
    229        if self.insert_id is not None: 
    230            info["insertId"] = self.insert_id 
    231        if self.severity is not None: 
    232            if isinstance(self.severity, str): 
    233                info["severity"] = self.severity.upper() 
    234            else: 
    235                info["severity"] = self.severity 
    236        if self.http_request is not None: 
    237            info["httpRequest"] = self.http_request 
    238        if self.timestamp is not None: 
    239            info["timestamp"] = _datetime_to_rfc3339(self.timestamp) 
    240        if self.trace is not None: 
    241            info["trace"] = self.trace 
    242        if self.span_id is not None: 
    243            info["spanId"] = self.span_id 
    244        if self.trace_sampled is not None: 
    245            info["traceSampled"] = self.trace_sampled 
    246        if self.source_location is not None: 
    247            source_location = self.source_location.copy() 
    248            source_location["line"] = str(source_location.pop("line", 0)) 
    249            info["sourceLocation"] = source_location 
    250        if self.operation is not None: 
    251            info["operation"] = self.operation 
    252        return info 
    253 
    254 
    255class TextEntry(LogEntry): 
    256    __doc__ = ( 
    257        """ 
    258    Log entry with text payload. 
    259 
    260    """ 
    261        + _LOG_ENTRY_PARAM_DOCSTRING 
    262        + """ 
    263 
    264        payload (str): payload for the log entry. 
    265    """ 
    266        + _LOG_ENTRY_SEE_ALSO_DOCSTRING 
    267    ) 
    268 
    269    @classmethod 
    270    def _extract_payload(cls, resource): 
    271        """Helper for :meth:`from_api_repr`""" 
    272        return resource["textPayload"] 
    273 
    274    def to_api_repr(self): 
    275        """API repr (JSON format) for entry.""" 
    276        info = super(TextEntry, self).to_api_repr() 
    277        info["textPayload"] = self.payload 
    278        return info 
    279 
    280 
    281class StructEntry(LogEntry): 
    282    __doc__ = ( 
    283        """ 
    284    Log entry with JSON payload. 
    285 
    286    """ 
    287        + _LOG_ENTRY_PARAM_DOCSTRING 
    288        + """ 
    289 
    290        payload (dict): payload for the log entry. 
    291    """ 
    292        + _LOG_ENTRY_SEE_ALSO_DOCSTRING 
    293    ) 
    294 
    295    @classmethod 
    296    def _extract_payload(cls, resource): 
    297        """Helper for :meth:`from_api_repr`""" 
    298        return resource["jsonPayload"] 
    299 
    300    def to_api_repr(self): 
    301        """API repr (JSON format) for entry.""" 
    302        info = super(StructEntry, self).to_api_repr() 
    303        info["jsonPayload"] = self.payload 
    304        return info 
    305 
    306 
    307class ProtobufEntry(LogEntry): 
    308    __doc__ = ( 
    309        """ 
    310    Log entry with protobuf message payload. 
    311 
    312    """ 
    313        + _LOG_ENTRY_PARAM_DOCSTRING 
    314        + """ 
    315 
    316        payload (google.protobuf.Message): payload for the log entry. 
    317    """ 
    318        + _LOG_ENTRY_SEE_ALSO_DOCSTRING 
    319    ) 
    320 
    321    @classmethod 
    322    def _extract_payload(cls, resource): 
    323        """Helper for :meth:`from_api_repr`""" 
    324        return resource["protoPayload"] 
    325 
    326    @property 
    327    def payload_pb(self): 
    328        if isinstance(self.payload, Message): 
    329            return self.payload 
    330 
    331    @property 
    332    def payload_json(self): 
    333        if isinstance(self.payload, collections.abc.Mapping): 
    334            return self.payload 
    335 
    336    def to_api_repr(self): 
    337        """API repr (JSON format) for entry.""" 
    338        info = super(ProtobufEntry, self).to_api_repr() 
    339        proto_payload = None 
    340        if self.payload_pb: 
    341            proto_payload = MessageToDict(self.payload) 
    342        elif self.payload_json: 
    343            proto_payload = dict(self.payload) 
    344        info["protoPayload"] = proto_payload 
    345        return info 
    346 
    347    def parse_message(self, message): 
    348        """Parse payload into a protobuf message. 
    349 
    350        Mutates the passed-in ``message`` in place. 
    351 
    352        Args: 
    353            message (google.protobuf.Message): the message to be logged 
    354        """ 
    355        # NOTE: This assumes that ``payload`` is already a deserialized 
    356        #       ``Any`` field and ``message`` has come from an imported 
    357        #       ``pb2`` module with the relevant protobuf message type. 
    358        Parse(json.dumps(self.payload), message)