1# -*- coding: utf-8 -*- #
2# Copyright 2015 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Displays log entries produced by Google Cloud Functions."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import unicode_literals
20
21import datetime
22from apitools.base.py.exceptions import HttpForbiddenError
23from apitools.base.py.exceptions import HttpNotFoundError
24from googlecloudsdk.api_lib.functions.v1 import util
25from googlecloudsdk.api_lib.logging import common as logging_common
26from googlecloudsdk.api_lib.logging import util as logging_util
27from googlecloudsdk.calliope import arg_parsers
28from googlecloudsdk.calliope import base
29from googlecloudsdk.command_lib.functions import flags
30from googlecloudsdk.core import log
31from googlecloudsdk.core import properties
32import six
33
34
35class GetLogs(base.ListCommand):
36  """Display log entries produced by Google Cloud Functions."""
37
38  @staticmethod
39  def Args(parser):
40    """Register flags for this command."""
41    flags.AddRegionFlag(
42        parser,
43        help_text='Only show logs generated by functions in the region.',
44    )
45    base.LIMIT_FLAG.RemoveFromParser(parser)
46    parser.add_argument(
47        'name',
48        nargs='?',
49        help=('Name of the function which logs are to be displayed. If no name '
50              'is specified, logs from all functions are displayed.'))
51    parser.add_argument(
52        '--execution-id',
53        help=('Execution ID for which logs are to be displayed.'))
54    parser.add_argument(
55        '--start-time',
56        required=False,
57        type=arg_parsers.Datetime.Parse,
58        help=('Return only log entries in which timestamps are not earlier '
59              'than the specified time. If *--start-time* is not specified, a '
60              'default start time of 1 week ago is assumed. See $ gcloud '
61              'topic datetimes for information on time formats.'))
62    parser.add_argument(
63        '--end-time',
64        required=False,
65        type=arg_parsers.Datetime.Parse,
66        help=('Return only log entries which timestamps are not later than '
67              'the specified time. If *--end-time* is specified but '
68              '*--start-time* is not, the command returns *--limit* latest '
69              'log entries which appeared before --end-time. See '
70              '*$ gcloud topic datetimes* for information on time formats.'))
71    parser.add_argument(
72        '--limit',
73        required=False,
74        type=arg_parsers.BoundedInt(1, 1000),
75        default=20,
76        help=('Number of log entries to be fetched; must not be greater than '
77              '1000.'))
78    flags.AddMinLogLevelFlag(parser)
79    parser.display_info.AddCacheUpdater(None)
80
81  @util.CatchHTTPErrorRaiseHTTPException
82  def Run(self, args):
83    """This is what gets called when the user runs this command.
84
85    Args:
86      args: an argparse namespace. All the arguments that were provided to this
87        command invocation.
88
89    Returns:
90      A generator of objects representing log entries.
91    """
92    if not args.IsSpecified('format'):
93      args.format = self._Format(args)
94
95    return self._Run(args)
96
97  def _Run(self, args):
98    region = properties.VALUES.functions.region.Get()
99    log_filter = [
100        'resource.type="cloud_function"',
101        'resource.labels.region="%s"' % region, 'logName:"cloud-functions"'
102    ]
103
104    if args.name:
105      log_filter.append('resource.labels.function_name="%s"' % args.name)
106    if args.execution_id:
107      log_filter.append('labels.execution_id="%s"' % args.execution_id)
108    if args.min_log_level:
109      log_filter.append('severity>=%s' % args.min_log_level.upper())
110
111    log_filter.append('timestamp>="%s"' % logging_util.FormatTimestamp(
112        args.start_time or
113        datetime.datetime.utcnow() - datetime.timedelta(days=7)))
114
115    if args.end_time:
116      log_filter.append('timestamp<="%s"' %
117                        logging_util.FormatTimestamp(args.end_time))
118
119    log_filter = ' '.join(log_filter)
120
121    entries = list(
122        logging_common.FetchLogs(log_filter, order_by='ASC', limit=args.limit))
123
124    if args.name and not entries:
125      # Check if the function even exists in the given region.
126      try:
127        client = util.GetApiClientInstance()
128        messages = client.MESSAGES_MODULE
129        client.projects_locations_functions.Get(
130            messages.CloudfunctionsProjectsLocationsFunctionsGetRequest(
131                name='projects/%s/locations/%s/functions/%s' %
132                (properties.VALUES.core.project.Get(required=True), region,
133                 args.name)))
134      except (HttpForbiddenError, HttpNotFoundError):
135        # The function doesn't exist in the given region.
136        log.warning(
137            'There is no function named `%s` in region `%s`. Perhaps you '
138            'meant to specify `--region` or update the `functions/region` '
139            'configuration property?' % (args.name, region))
140
141    for entry in entries:
142      message = entry.textPayload
143      if entry.jsonPayload:
144        props = [
145            prop.value
146            for prop in entry.jsonPayload.additionalProperties
147            if prop.key == 'message'
148        ]
149        if len(props) == 1 and hasattr(props[0], 'string_value'):
150          message = props[0].string_value
151      row = {'log': message}
152      if entry.severity:
153        severity = six.text_type(entry.severity)
154        if severity in flags.SEVERITIES:
155          # Use short form (first letter) for expected severities.
156          row['level'] = severity[0]
157        else:
158          # Print full form of unexpected severities.
159          row['level'] = severity
160      if entry.resource and entry.resource.labels:
161        for label in entry.resource.labels.additionalProperties:
162          if label.key == 'function_name':
163            row['name'] = label.value
164      if entry.labels:
165        for label in entry.labels.additionalProperties:
166          if label.key == 'execution_id':
167            row['execution_id'] = label.value
168      if entry.timestamp:
169        row['time_utc'] = util.FormatTimestamp(entry.timestamp)
170      yield row
171
172  def _Format(self, args):
173    return 'table(level,name,execution_id,time_utc,log)'
174