Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/cloud/logging_v2/logger.py: 32%
151 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:45 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:45 +0000
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.
15"""Define API Loggers."""
17import collections
18import re
20from google.cloud.logging_v2._helpers import _add_defaults_to_filter
21from google.cloud.logging_v2.entries import LogEntry
22from google.cloud.logging_v2.entries import ProtobufEntry
23from google.cloud.logging_v2.entries import StructEntry
24from google.cloud.logging_v2.entries import TextEntry
25from google.cloud.logging_v2.resource import Resource
26from google.cloud.logging_v2.handlers._monitored_resources import detect_resource
27from google.cloud.logging_v2._instrumentation import _add_instrumentation
29from google.api_core.exceptions import InvalidArgument
30from google.rpc.error_details_pb2 import DebugInfo
32import google.protobuf.message
34_GLOBAL_RESOURCE = Resource(type="global", labels={})
37_OUTBOUND_ENTRY_FIELDS = ( # (name, default)
38 ("type_", None),
39 ("log_name", None),
40 ("payload", None),
41 ("labels", None),
42 ("insert_id", None),
43 ("severity", None),
44 ("http_request", None),
45 ("timestamp", None),
46 ("resource", _GLOBAL_RESOURCE),
47 ("trace", None),
48 ("span_id", None),
49 ("trace_sampled", None),
50 ("source_location", None),
51)
53_STRUCT_EXTRACTABLE_FIELDS = ["severity", "trace", "span_id"]
56class Logger(object):
57 """Loggers represent named targets for log entries.
59 See https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.logs
60 """
62 def __init__(self, name, client, *, labels=None, resource=None):
63 """
64 Args:
65 name (str): The name of the logger.
66 client (~logging_v2.client.Client):
67 A client which holds credentials and project configuration
68 for the logger (which requires a project).
69 resource (Optional[~logging_v2.Resource]): a monitored resource object
70 representing the resource the code was run on. If not given, will
71 be inferred from the environment.
72 labels (Optional[dict]): Mapping of default labels for entries written
73 via this logger.
75 """
76 if not resource:
77 # infer the correct monitored resource from the local environment
78 resource = detect_resource(client.project)
79 self.name = name
80 self._client = client
81 self.labels = labels
82 self.default_resource = resource
84 @property
85 def client(self):
86 """Clent bound to the logger."""
87 return self._client
89 @property
90 def project(self):
91 """Project bound to the logger."""
92 return self._client.project
94 @property
95 def full_name(self):
96 """Fully-qualified name used in logging APIs"""
97 return f"projects/{self.project}/logs/{self.name}"
99 @property
100 def path(self):
101 """URI path for use in logging APIs"""
102 return f"/{self.full_name}"
104 def _require_client(self, client):
105 """Check client or verify over-ride. Also sets ``parent``.
107 Args:
108 client (Union[None, ~logging_v2.client.Client]):
109 The client to use. If not passed, falls back to the
110 ``client`` stored on the current sink.
112 Returns:
113 ~logging_v2.client.Client: The client passed in
114 or the currently bound client.
115 """
116 if client is None:
117 client = self._client
118 return client
120 def batch(self, *, client=None):
121 """Return a batch to use as a context manager.
123 Args:
124 client (Union[None, ~logging_v2.client.Client]):
125 The client to use. If not passed, falls back to the
126 ``client`` stored on the current sink.
128 Returns:
129 Batch: A batch to use as a context manager.
130 """
131 client = self._require_client(client)
132 return Batch(self, client)
134 def _do_log(self, client, _entry_class, payload=None, **kw):
135 """Helper for :meth:`log_empty`, :meth:`log_text`, etc."""
136 client = self._require_client(client)
138 # Apply defaults
139 kw["log_name"] = kw.pop("log_name", self.full_name)
140 kw["labels"] = kw.pop("labels", self.labels)
141 kw["resource"] = kw.pop("resource", self.default_resource)
143 severity = kw.get("severity", None)
144 if isinstance(severity, str):
145 # convert severity to upper case, as expected by enum definition
146 kw["severity"] = severity.upper()
148 if isinstance(kw["resource"], collections.abc.Mapping):
149 # if resource was passed as a dict, attempt to parse it into a
150 # Resource object
151 try:
152 kw["resource"] = Resource(**kw["resource"])
153 except TypeError as e:
154 # dict couldn't be parsed as a Resource
155 raise TypeError("invalid resource dict") from e
157 if payload is not None:
158 entry = _entry_class(payload=payload, **kw)
159 else:
160 entry = _entry_class(**kw)
162 api_repr = entry.to_api_repr()
163 entries = [api_repr]
164 if google.cloud.logging_v2._instrumentation_emitted is False:
165 entries = _add_instrumentation(entries, **kw)
166 google.cloud.logging_v2._instrumentation_emitted = True
167 # partial_success is true to avoid dropping instrumentation logs
168 client.logging_api.write_entries(entries, partial_success=True)
170 def log_empty(self, *, client=None, **kw):
171 """Log an empty message
173 See
174 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write
176 Args:
177 client (Optional[~logging_v2.client.Client]):
178 The client to use. If not passed, falls back to the
179 ``client`` stored on the current sink.
180 kw (Optional[dict]): additional keyword arguments for the entry.
181 See :class:`~logging_v2.entries.LogEntry`.
182 """
183 self._do_log(client, LogEntry, **kw)
185 def log_text(self, text, *, client=None, **kw):
186 """Log a text message
188 See
189 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write
191 Args:
192 text (str): the log message
193 client (Optional[~logging_v2.client.Client]):
194 The client to use. If not passed, falls back to the
195 ``client`` stored on the current sink.
196 kw (Optional[dict]): additional keyword arguments for the entry.
197 See :class:`~logging_v2.entries.LogEntry`.
198 """
199 self._do_log(client, TextEntry, text, **kw)
201 def log_struct(self, info, *, client=None, **kw):
202 """Log a dictionary message
204 See
205 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write
207 Args:
208 info (dict): the log entry information
209 client (Optional[~logging_v2.client.Client]):
210 The client to use. If not passed, falls back to the
211 ``client`` stored on the current sink.
212 kw (Optional[dict]): additional keyword arguments for the entry.
213 See :class:`~logging_v2.entries.LogEntry`.
214 """
215 for field in _STRUCT_EXTRACTABLE_FIELDS:
216 # attempt to copy relevant fields from the payload into the LogEntry body
217 if field in info and field not in kw:
218 kw[field] = info[field]
219 self._do_log(client, StructEntry, info, **kw)
221 def log_proto(self, message, *, client=None, **kw):
222 """Log a protobuf message
224 See
225 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/list
227 Args:
228 message (google.protobuf.message.Message):
229 The protobuf message to be logged.
230 client (Optional[~logging_v2.client.Client]):
231 The client to use. If not passed, falls back to the
232 ``client`` stored on the current sink.
233 kw (Optional[dict]): additional keyword arguments for the entry.
234 See :class:`~logging_v2.entries.LogEntry`.
235 """
236 self._do_log(client, ProtobufEntry, message, **kw)
238 def log(self, message=None, *, client=None, **kw):
239 """Log an arbitrary message. Type will be inferred based on the input.
241 See
242 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/list
244 Args:
245 message (Optional[str or dict or google.protobuf.Message]): The message. to log
246 client (Optional[~logging_v2.client.Client]):
247 The client to use. If not passed, falls back to the
248 ``client`` stored on the current sink.
249 kw (Optional[dict]): additional keyword arguments for the entry.
250 See :class:`~logging_v2.entries.LogEntry`.
251 """
252 if isinstance(message, google.protobuf.message.Message):
253 self.log_proto(message, client=client, **kw)
254 elif isinstance(message, collections.abc.Mapping):
255 self.log_struct(message, client=client, **kw)
256 elif isinstance(message, str):
257 self.log_text(message, client=client, **kw)
258 else:
259 self._do_log(client, LogEntry, message, **kw)
261 def delete(self, logger_name=None, *, client=None):
262 """Delete all entries in a logger via a DELETE request
264 See
265 https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.logs/delete
267 Args:
268 logger_name (Optional[str]): The resource name of the log to delete:
270 ::
272 "projects/[PROJECT_ID]/logs/[LOG_ID]"
273 "organizations/[ORGANIZATION_ID]/logs/[LOG_ID]"
274 "billingAccounts/[BILLING_ACCOUNT_ID]/logs/[LOG_ID]"
275 "folders/[FOLDER_ID]/logs/[LOG_ID]"
277 ``[LOG_ID]`` must be URL-encoded. For example,
278 ``"projects/my-project-id/logs/syslog"``,
279 ``"organizations/1234567890/logs/cloudresourcemanager.googleapis.com%2Factivity"``.
280 If not passed, defaults to the project bound to the client.
281 client (Optional[~logging_v2.client.Client]):
282 The client to use. If not passed, falls back to the
283 ``client`` stored on the current logger.
284 """
285 client = self._require_client(client)
286 if logger_name is None:
287 logger_name = self.full_name
288 client.logging_api.logger_delete(logger_name)
290 def list_entries(
291 self,
292 *,
293 resource_names=None,
294 filter_=None,
295 order_by=None,
296 max_results=None,
297 page_size=None,
298 page_token=None,
299 ):
300 """Return a generator of log entry resources.
302 See
303 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/list
305 Args:
306 resource_names (Optional[Sequence[str]]): Names of one or more parent resources
307 from which to retrieve log entries:
309 ::
311 "projects/[PROJECT_ID]"
312 "organizations/[ORGANIZATION_ID]"
313 "billingAccounts/[BILLING_ACCOUNT_ID]"
314 "folders/[FOLDER_ID]"
316 If not passed, defaults to the project bound to the client.
317 filter_ (Optional[str]): a filter expression. See
318 https://cloud.google.com/logging/docs/view/advanced_filters
319 By default, a 24 hour filter is applied.
320 order_by (Optional[str]): One of :data:`~logging_v2.ASCENDING`
321 or :data:`~logging_v2.DESCENDING`.
322 max_results (Optional[int]):
323 Optional. The maximum number of entries to return.
324 Non-positive values are treated as 0. If None, uses API defaults.
325 page_size (int): number of entries to fetch in each API call. Although
326 requests are paged internally, logs are returned by the generator
327 one at a time. If not passed, defaults to a value set by the API.
328 page_token (str): opaque marker for the starting "page" of entries. If not
329 passed, the API will return the first page of entries.
330 Returns:
331 Generator[~logging_v2.LogEntry]
332 """
334 if resource_names is None:
335 resource_names = [f"projects/{self.project}"]
337 log_filter = f"logName={self.full_name}"
338 if filter_ is not None:
339 filter_ = f"{filter_} AND {log_filter}"
340 else:
341 filter_ = log_filter
342 filter_ = _add_defaults_to_filter(filter_)
343 return self.client.list_entries(
344 resource_names=resource_names,
345 filter_=filter_,
346 order_by=order_by,
347 max_results=max_results,
348 page_size=page_size,
349 page_token=page_token,
350 )
353class Batch(object):
354 def __init__(self, logger, client, *, resource=None):
355 """Context manager: collect entries to log via a single API call.
357 Helper returned by :meth:`Logger.batch`
359 Args:
360 logger (logging_v2.logger.Logger):
361 the logger to which entries will be logged.
362 client (~logging_V2.client.Cilent):
363 The client to use.
364 resource (Optional[~logging_v2.resource.Resource]):
365 Monitored resource of the batch, defaults
366 to None, which requires that every entry should have a
367 resource specified. Since the methods used to write
368 entries default the entry's resource to the global
369 resource type, this parameter is only required
370 if explicitly set to None. If no entries' resource are
371 set to None, this parameter will be ignored on the server.
372 """
373 self.logger = logger
374 self.entries = []
375 self.client = client
376 self.resource = resource
378 def __enter__(self):
379 return self
381 def __exit__(self, exc_type, exc_val, exc_tb):
382 if exc_type is None:
383 self.commit()
385 def log_empty(self, **kw):
386 """Add a entry without payload to be logged during :meth:`commit`.
388 Args:
389 kw (Optional[dict]): Additional keyword arguments for the entry.
390 See :class:`~logging_v2.entries.LogEntry`.
391 """
392 self.entries.append(LogEntry(**kw))
394 def log_text(self, text, **kw):
395 """Add a text entry to be logged during :meth:`commit`.
397 Args:
398 text (str): the text entry
399 kw (Optional[dict]): Additional keyword arguments for the entry.
400 See :class:`~logging_v2.entries.LogEntry`.
401 """
402 self.entries.append(TextEntry(payload=text, **kw))
404 def log_struct(self, info, **kw):
405 """Add a struct entry to be logged during :meth:`commit`.
407 Args:
408 info (dict): The struct entry,
409 kw (Optional[dict]): Additional keyword arguments for the entry.
410 See :class:`~logging_v2.entries.LogEntry`.
411 """
412 self.entries.append(StructEntry(payload=info, **kw))
414 def log_proto(self, message, **kw):
415 """Add a protobuf entry to be logged during :meth:`commit`.
417 Args:
418 message (google.protobuf.Message): The protobuf entry.
419 kw (Optional[dict]): Additional keyword arguments for the entry.
420 See :class:`~logging_v2.entries.LogEntry`.
421 """
422 self.entries.append(ProtobufEntry(payload=message, **kw))
424 def log(self, message=None, **kw):
425 """Add an arbitrary message to be logged during :meth:`commit`.
426 Type will be inferred based on the input message.
428 Args:
429 message (Optional[str or dict or google.protobuf.Message]): The message. to log
430 kw (Optional[dict]): Additional keyword arguments for the entry.
431 See :class:`~logging_v2.entries.LogEntry`.
432 """
433 entry_type = LogEntry
434 if isinstance(message, google.protobuf.message.Message):
435 entry_type = ProtobufEntry
436 elif isinstance(message, collections.abc.Mapping):
437 entry_type = StructEntry
438 elif isinstance(message, str):
439 entry_type = TextEntry
440 self.entries.append(entry_type(payload=message, **kw))
442 def commit(self, *, client=None, partial_success=True):
443 """Send saved log entries as a single API call.
445 Args:
446 client (Optional[~logging_v2.client.Client]):
447 The client to use. If not passed, falls back to the
448 ``client`` stored on the current batch.
449 partial_success (Optional[bool]):
450 Whether a batch's valid entries should be written even
451 if some other entry failed due to a permanent error such
452 as INVALID_ARGUMENT or PERMISSION_DENIED.
453 """
454 if client is None:
455 client = self.client
457 kwargs = {"logger_name": self.logger.full_name}
459 if self.resource is not None:
460 kwargs["resource"] = self.resource._to_dict()
462 if self.logger.labels is not None:
463 kwargs["labels"] = self.logger.labels
465 entries = [entry.to_api_repr() for entry in self.entries]
466 try:
467 client.logging_api.write_entries(
468 entries, partial_success=partial_success, **kwargs
469 )
470 except InvalidArgument as e:
471 # InvalidArgument is often sent when a log is too large
472 # attempt to attach extra contex on which log caused error
473 self._append_context_to_error(e)
474 raise e
475 del self.entries[:]
477 def _append_context_to_error(self, err):
478 """
479 Attempts to Modify `write_entries` exception messages to contain
480 context on which log in the batch caused the error.
482 Best-effort basis. If another exception occurs while processing the
483 input exception, the input will be left unmodified
485 Args:
486 err (~google.api_core.exceptions.InvalidArgument):
487 The original exception object
488 """
489 try:
490 # find debug info proto if in details
491 debug_info = next(x for x in err.details if isinstance(x, DebugInfo))
492 # parse out the index of the faulty entry
493 error_idx = re.search("(?<=key: )[0-9]+", debug_info.detail).group(0)
494 # find the faulty entry object
495 found_entry = self.entries[int(error_idx)]
496 str_entry = str(found_entry.to_api_repr())
497 # modify error message to contain extra context
498 err.message = f"{err.message}: {str_entry:.2000}..."
499 except Exception:
500 # if parsing fails, abort changes and leave err unmodified
501 pass