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"""Wrapper for adapting the autogenerated gapic client to the hand-written
16client."""
17
18from google.cloud.logging_v2.services.config_service_v2 import ConfigServiceV2Client
19from google.cloud.logging_v2.services.logging_service_v2 import LoggingServiceV2Client
20from google.cloud.logging_v2.services.metrics_service_v2 import MetricsServiceV2Client
21from google.cloud.logging_v2.types import CreateSinkRequest
22from google.cloud.logging_v2.types import UpdateSinkRequest
23from google.cloud.logging_v2.types import ListSinksRequest
24from google.cloud.logging_v2.types import ListLogMetricsRequest
25from google.cloud.logging_v2.types import ListLogEntriesRequest
26from google.cloud.logging_v2.types import WriteLogEntriesRequest
27from google.cloud.logging_v2.types import LogSink
28from google.cloud.logging_v2.types import LogMetric
29from google.cloud.logging_v2.types import LogEntry as LogEntryPB
30
31from google.protobuf.json_format import MessageToDict
32from google.protobuf.json_format import ParseDict
33from google.protobuf.json_format import ParseError
34
35from google.cloud.logging_v2._helpers import entry_from_resource
36from google.cloud.logging_v2.sink import Sink
37from google.cloud.logging_v2.metric import Metric
38
39from google.api_core import client_info
40from google.api_core import gapic_v1
41
42
43class _LoggingAPI(object):
44 """Helper mapping logging-related APIs."""
45
46 def __init__(self, gapic_api, client):
47 self._gapic_api = gapic_api
48 self._client = client
49
50 def list_entries(
51 self,
52 resource_names,
53 *,
54 filter_=None,
55 order_by=None,
56 max_results=None,
57 page_size=None,
58 page_token=None,
59 ):
60 """Return a generator of log entry resources.
61
62 Args:
63 resource_names (Sequence[str]): Names of one or more parent resources
64 from which to retrieve log entries:
65
66 ::
67
68 "projects/[PROJECT_ID]"
69 "organizations/[ORGANIZATION_ID]"
70 "billingAccounts/[BILLING_ACCOUNT_ID]"
71 "folders/[FOLDER_ID]"
72
73 filter_ (str): a filter expression. See
74 https://cloud.google.com/logging/docs/view/advanced_filters
75 order_by (str) One of :data:`~logging_v2.ASCENDING`
76 or :data:`~logging_v2.DESCENDING`.
77 max_results (Optional[int]):
78 Optional. The maximum number of entries to return.
79 Non-positive values are treated as 0. If None, uses API defaults.
80 page_size (int): number of entries to fetch in each API call. Although
81 requests are paged internally, logs are returned by the generator
82 one at a time. If not passed, defaults to a value set by the API.
83 page_token (str): opaque marker for the starting "page" of entries. If not
84 passed, the API will return the first page of entries.
85 Returns:
86 Generator[~logging_v2.LogEntry]
87 """
88 # full resource names are expected by the API
89 resource_names = resource_names
90 request = ListLogEntriesRequest(
91 resource_names=resource_names,
92 filter=filter_,
93 order_by=order_by,
94 page_size=page_size,
95 page_token=page_token,
96 )
97
98 response = self._gapic_api.list_log_entries(request=request)
99 log_iter = iter(response)
100
101 # We attach a mutable loggers dictionary so that as Logger
102 # objects are created by entry_from_resource, they can be
103 # re-used by other log entries from the same logger.
104 loggers = {}
105
106 if max_results is not None and max_results < 0:
107 raise ValueError("max_results must be positive")
108
109 # create generator
110 def log_entries_pager(log_iter):
111 i = 0
112 for entry in log_iter:
113 if max_results is not None and i >= max_results:
114 break
115 log_entry_dict = _parse_log_entry(LogEntryPB.pb(entry))
116 yield entry_from_resource(log_entry_dict, self._client, loggers=loggers)
117 i += 1
118
119 return log_entries_pager(log_iter)
120
121 def write_entries(
122 self,
123 entries,
124 *,
125 logger_name=None,
126 resource=None,
127 labels=None,
128 partial_success=True,
129 dry_run=False,
130 ):
131 """Log an entry resource via a POST request
132
133 Args:
134 entries (Sequence[Mapping[str, ...]]): sequence of mappings representing
135 the log entry resources to log.
136 logger_name (Optional[str]): name of default logger to which to log the entries;
137 individual entries may override.
138 resource(Optional[Mapping[str, ...]]): default resource to associate with entries;
139 individual entries may override.
140 labels (Optional[Mapping[str, ...]]): default labels to associate with entries;
141 individual entries may override.
142 partial_success (Optional[bool]): Whether valid entries should be written even if
143 some other entries fail due to INVALID_ARGUMENT or
144 PERMISSION_DENIED errors. If any entry is not written, then
145 the response status is the error associated with one of the
146 failed entries and the response includes error details keyed
147 by the entries' zero-based index in the ``entries.write``
148 method.
149 dry_run (Optional[bool]):
150 If true, the request should expect normal response,
151 but the entries won't be persisted nor exported.
152 Useful for checking whether the logging API endpoints are working
153 properly before sending valuable data.
154 """
155 try:
156 log_entry_pbs = [_log_entry_mapping_to_pb(entry) for entry in entries]
157 except ParseError as e:
158 raise ValueError(f"Invalid log entry: {str(e)}") from e
159
160 request = WriteLogEntriesRequest(
161 log_name=logger_name,
162 resource=resource,
163 labels=labels,
164 entries=log_entry_pbs,
165 partial_success=partial_success,
166 )
167 self._gapic_api.write_log_entries(request=request)
168
169 def logger_delete(self, logger_name):
170 """Delete all entries in a logger.
171
172 Args:
173 logger_name (str): The resource name of the log to delete:
174
175 ::
176
177 "projects/[PROJECT_ID]/logs/[LOG_ID]"
178 "organizations/[ORGANIZATION_ID]/logs/[LOG_ID]"
179 "billingAccounts/[BILLING_ACCOUNT_ID]/logs/[LOG_ID]"
180 "folders/[FOLDER_ID]/logs/[LOG_ID]"
181
182 ``[LOG_ID]`` must be URL-encoded. For example,
183 ``"projects/my-project-id/logs/syslog"``,
184 ``"organizations/1234567890/logs/cloudresourcemanager.googleapis.com%2Factivity"``.
185 """
186 self._gapic_api.delete_log(log_name=logger_name)
187
188
189class _SinksAPI(object):
190 """Helper mapping sink-related APIs."""
191
192 def __init__(self, gapic_api, client):
193 self._gapic_api = gapic_api
194 self._client = client
195
196 def list_sinks(self, parent, *, max_results=None, page_size=None, page_token=None):
197 """List sinks for the parent resource.
198
199 Args:
200 parent (str): The parent resource whose sinks are to be listed:
201
202 ::
203
204 "projects/[PROJECT_ID]"
205 "organizations/[ORGANIZATION_ID]"
206 "billingAccounts/[BILLING_ACCOUNT_ID]"
207 "folders/[FOLDER_ID]".
208 max_results (Optional[int]):
209 Optional. The maximum number of entries to return.
210 Non-positive values are treated as 0. If None, uses API defaults.
211 page_size (int): number of entries to fetch in each API call. Although
212 requests are paged internally, logs are returned by the generator
213 one at a time. If not passed, defaults to a value set by the API.
214 page_token (str): opaque marker for the starting "page" of entries. If not
215 passed, the API will return the first page of entries.
216
217 Returns:
218 Generator[~logging_v2.Sink]
219 """
220 request = ListSinksRequest(
221 parent=parent, page_size=page_size, page_token=page_token
222 )
223 response = self._gapic_api.list_sinks(request)
224 sink_iter = iter(response)
225
226 if max_results is not None and max_results < 0:
227 raise ValueError("max_results must be positive")
228
229 def sinks_pager(sink_iter):
230 i = 0
231 for entry in sink_iter:
232 if max_results is not None and i >= max_results:
233 break
234 # Convert the GAPIC sink type into the handwritten `Sink` type
235 yield Sink.from_api_repr(LogSink.to_dict(entry), client=self._client)
236 i += 1
237
238 return sinks_pager(sink_iter)
239
240 def sink_create(
241 self, parent, sink_name, filter_, destination, *, unique_writer_identity=False
242 ):
243 """Create a sink resource.
244
245 See
246 https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.sinks/create
247
248 Args:
249 parent(str): The resource in which to create the sink,
250 including the parent resource and the sink identifier:
251
252 ::
253
254 "projects/[PROJECT_ID]"
255 "organizations/[ORGANIZATION_ID]"
256 "billingAccounts/[BILLING_ACCOUNT_ID]"
257 "folders/[FOLDER_ID]".
258 sink_name (str): The name of the sink.
259 filter_ (str): The advanced logs filter expression defining the
260 entries exported by the sink.
261 destination (str): Destination URI for the entries exported by
262 the sink.
263 unique_writer_identity (Optional[bool]): determines the kind of
264 IAM identity returned as writer_identity in the new sink.
265
266 Returns:
267 dict: The sink resource returned from the API (converted from a
268 protobuf to a dictionary).
269 """
270 sink_pb = LogSink(name=sink_name, filter=filter_, destination=destination)
271 request = CreateSinkRequest(
272 parent=parent, sink=sink_pb, unique_writer_identity=unique_writer_identity
273 )
274 created_pb = self._gapic_api.create_sink(request=request)
275 return MessageToDict(
276 LogSink.pb(created_pb),
277 preserving_proto_field_name=False,
278 )
279
280 def sink_get(self, sink_name):
281 """Retrieve a sink resource.
282
283 Args:
284 sink_name (str): The resource name of the sink,
285 including the parent resource and the sink identifier:
286
287 ::
288
289 "projects/[PROJECT_ID]/sinks/[SINK_ID]"
290 "organizations/[ORGANIZATION_ID]/sinks/[SINK_ID]"
291 "billingAccounts/[BILLING_ACCOUNT_ID]/sinks/[SINK_ID]"
292 "folders/[FOLDER_ID]/sinks/[SINK_ID]"
293
294 Returns:
295 dict: The sink object returned from the API (converted from a
296 protobuf to a dictionary).
297 """
298 sink_pb = self._gapic_api.get_sink(sink_name=sink_name)
299 # NOTE: LogSink message type does not have an ``Any`` field
300 # so `MessageToDict`` can safely be used.
301 return MessageToDict(
302 LogSink.pb(sink_pb),
303 preserving_proto_field_name=False,
304 )
305
306 def sink_update(
307 self,
308 sink_name,
309 filter_,
310 destination,
311 *,
312 unique_writer_identity=False,
313 ):
314 """Update a sink resource.
315
316 Args:
317 sink_name (str): Required. The resource name of the sink,
318 including the parent resource and the sink identifier:
319
320 ::
321
322 "projects/[PROJECT_ID]/sinks/[SINK_ID]"
323 "organizations/[ORGANIZATION_ID]/sinks/[SINK_ID]"
324 "billingAccounts/[BILLING_ACCOUNT_ID]/sinks/[SINK_ID]"
325 "folders/[FOLDER_ID]/sinks/[SINK_ID]"
326 filter_ (str): The advanced logs filter expression defining the
327 entries exported by the sink.
328 destination (str): destination URI for the entries exported by
329 the sink.
330 unique_writer_identity (Optional[bool]): determines the kind of
331 IAM identity returned as writer_identity in the new sink.
332
333
334 Returns:
335 dict: The sink resource returned from the API (converted from a
336 protobuf to a dictionary).
337 """
338 name = sink_name.split("/")[-1] # parse name out of full resource name
339 sink_pb = LogSink(
340 name=name,
341 filter=filter_,
342 destination=destination,
343 )
344
345 request = UpdateSinkRequest(
346 sink_name=sink_name,
347 sink=sink_pb,
348 unique_writer_identity=unique_writer_identity,
349 )
350 sink_pb = self._gapic_api.update_sink(request=request)
351 # NOTE: LogSink message type does not have an ``Any`` field
352 # so `MessageToDict`` can safely be used.
353 return MessageToDict(
354 LogSink.pb(sink_pb),
355 preserving_proto_field_name=False,
356 )
357
358 def sink_delete(self, sink_name):
359 """Delete a sink resource.
360
361 Args:
362 sink_name (str): Required. The full resource name of the sink to delete,
363 including the parent resource and the sink identifier:
364
365 ::
366
367 "projects/[PROJECT_ID]/sinks/[SINK_ID]"
368 "organizations/[ORGANIZATION_ID]/sinks/[SINK_ID]"
369 "billingAccounts/[BILLING_ACCOUNT_ID]/sinks/[SINK_ID]"
370 "folders/[FOLDER_ID]/sinks/[SINK_ID]"
371
372 Example: ``"projects/my-project-id/sinks/my-sink-id"``.
373 """
374 self._gapic_api.delete_sink(sink_name=sink_name)
375
376
377class _MetricsAPI(object):
378 """Helper mapping sink-related APIs."""
379
380 def __init__(self, gapic_api, client):
381 self._gapic_api = gapic_api
382 self._client = client
383
384 def list_metrics(
385 self, project, *, max_results=None, page_size=None, page_token=None
386 ):
387 """List metrics for the project associated with this client.
388
389 Args:
390 project (str): ID of the project whose metrics are to be listed.
391 max_results (Optional[int]):
392 Optional. The maximum number of entries to return.
393 Non-positive values are treated as 0. If None, uses API defaults.
394 page_size (int): number of entries to fetch in each API call. Although
395 requests are paged internally, logs are returned by the generator
396 one at a time. If not passed, defaults to a value set by the API.
397 page_token (str): opaque marker for the starting "page" of entries. If not
398 passed, the API will return the first page of entries.
399
400 Returns:
401 Generator[logging_v2.Metric]
402 """
403 path = f"projects/{project}"
404 request = ListLogMetricsRequest(
405 parent=path,
406 page_size=page_size,
407 page_token=page_token,
408 )
409 response = self._gapic_api.list_log_metrics(request=request)
410 metric_iter = iter(response)
411
412 if max_results is not None and max_results < 0:
413 raise ValueError("max_results must be positive")
414
415 def metrics_pager(metric_iter):
416 i = 0
417 for entry in metric_iter:
418 if max_results is not None and i >= max_results:
419 break
420 # Convert GAPIC metrics type into handwritten `Metric` type
421 yield Metric.from_api_repr(
422 LogMetric.to_dict(entry), client=self._client
423 )
424 i += 1
425
426 return metrics_pager(metric_iter)
427
428 def metric_create(self, project, metric_name, filter_, description):
429 """Create a metric resource.
430
431 See
432 https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.metrics/create
433
434 Args:
435 project (str): ID of the project in which to create the metric.
436 metric_name (str): The name of the metric
437 filter_ (str): The advanced logs filter expression defining the
438 entries exported by the metric.
439 description (str): description of the metric.
440 """
441 parent = f"projects/{project}"
442 metric_pb = LogMetric(name=metric_name, filter=filter_, description=description)
443 self._gapic_api.create_log_metric(parent=parent, metric=metric_pb)
444
445 def metric_get(self, project, metric_name):
446 """Retrieve a metric resource.
447
448 Args:
449 project (str): ID of the project containing the metric.
450 metric_name (str): The name of the metric
451
452 Returns:
453 dict: The metric object returned from the API (converted from a
454 protobuf to a dictionary).
455 """
456 path = f"projects/{project}/metrics/{metric_name}"
457 metric_pb = self._gapic_api.get_log_metric(metric_name=path)
458 # NOTE: LogMetric message type does not have an ``Any`` field
459 # so `MessageToDict`` can safely be used.
460 return MessageToDict(
461 LogMetric.pb(metric_pb),
462 preserving_proto_field_name=False,
463 )
464
465 def metric_update(
466 self,
467 project,
468 metric_name,
469 filter_,
470 description,
471 ):
472 """Update a metric resource.
473
474 Args:
475 project (str): ID of the project containing the metric.
476 metric_name (str): the name of the metric
477 filter_ (str): the advanced logs filter expression defining the
478 entries exported by the metric.
479 description (str): description of the metric.
480
481 Returns:
482 The metric object returned from the API (converted from a
483 protobuf to a dictionary).
484 """
485 path = f"projects/{project}/metrics/{metric_name}"
486 metric_pb = LogMetric(
487 name=path,
488 filter=filter_,
489 description=description,
490 )
491 metric_pb = self._gapic_api.update_log_metric(
492 metric_name=path, metric=metric_pb
493 )
494 # NOTE: LogMetric message type does not have an ``Any`` field
495 # so `MessageToDict`` can safely be used.
496 return MessageToDict(
497 LogMetric.pb(metric_pb),
498 preserving_proto_field_name=False,
499 )
500
501 def metric_delete(self, project, metric_name):
502 """Delete a metric resource.
503
504 Args:
505 project (str): ID of the project containing the metric.
506 metric_name (str): The name of the metric
507 """
508 path = f"projects/{project}/metrics/{metric_name}"
509 self._gapic_api.delete_log_metric(metric_name=path)
510
511
512def _parse_log_entry(entry_pb):
513 """Special helper to parse ``LogEntry`` protobuf into a dictionary.
514
515 The ``proto_payload`` field in ``LogEntry`` is of type ``Any``. This
516 can be problematic if the type URL in the payload isn't in the
517 ``google.protobuf`` registry. To help with parsing unregistered types,
518 this function will remove ``proto_payload`` before parsing.
519
520 Args:
521 entry_pb (LogEntry): Log entry protobuf.
522
523 Returns:
524 dict: The parsed log entry. The ``protoPayload`` key may contain
525 the raw ``Any`` protobuf from ``entry_pb.proto_payload`` if
526 it could not be parsed.
527 """
528 try:
529 return MessageToDict(
530 entry_pb,
531 preserving_proto_field_name=False,
532 )
533 except TypeError:
534 if entry_pb.HasField("proto_payload"):
535 proto_payload = entry_pb.proto_payload
536 entry_pb.ClearField("proto_payload")
537 entry_mapping = MessageToDict(
538 entry_pb,
539 preserving_proto_field_name=False,
540 )
541 entry_mapping["protoPayload"] = proto_payload
542 return entry_mapping
543 else:
544 raise
545
546
547def _log_entry_mapping_to_pb(mapping):
548 """Helper for :meth:`write_entries`, et aliae
549
550 Performs "impedance matching" between the protobuf attrs and
551 the keys expected in the JSON API.
552 """
553 entry_pb = LogEntryPB.pb(LogEntryPB())
554 # NOTE: We assume ``mapping`` was created in ``Batch.commit``
555 # or ``Logger._make_entry_resource``. In either case, if
556 # the ``protoPayload`` key is present, we assume that the
557 # type URL is registered with ``google.protobuf`` and will
558 # not cause any issues in the JSON->protobuf conversion
559 # of the corresponding ``proto_payload`` in the log entry
560 # (it is an ``Any`` field).
561 ParseDict(mapping, entry_pb)
562 return LogEntryPB(entry_pb)
563
564
565def _client_info_to_gapic(input_info):
566 """
567 Helper function to convert api_core.client_info to
568 api_core.gapic_v1.client_info subclass
569 """
570 return gapic_v1.client_info.ClientInfo(
571 python_version=input_info.python_version,
572 grpc_version=input_info.grpc_version,
573 api_core_version=input_info.api_core_version,
574 gapic_version=input_info.gapic_version,
575 client_library_version=input_info.client_library_version,
576 user_agent=input_info.user_agent,
577 rest_version=input_info.rest_version,
578 )
579
580
581def make_logging_api(client):
582 """Create an instance of the Logging API adapter.
583
584 Args:
585 client (~logging_v2.client.Client): The client
586 that holds configuration details.
587
588 Returns:
589 _LoggingAPI: A metrics API instance with the proper credentials.
590 """
591 info = client._client_info
592 if isinstance(info, client_info.ClientInfo):
593 # convert into gapic-compatible subclass
594 info = _client_info_to_gapic(info)
595
596 generated = LoggingServiceV2Client(
597 credentials=client._credentials,
598 client_info=info,
599 client_options=client._client_options,
600 )
601 return _LoggingAPI(generated, client)
602
603
604def make_metrics_api(client):
605 """Create an instance of the Metrics API adapter.
606
607 Args:
608 client (~logging_v2.client.Client): The client
609 that holds configuration details.
610
611 Returns:
612 _MetricsAPI: A metrics API instance with the proper credentials.
613 """
614 info = client._client_info
615 if isinstance(info, client_info.ClientInfo):
616 # convert into gapic-compatible subclass
617 info = _client_info_to_gapic(info)
618
619 generated = MetricsServiceV2Client(
620 credentials=client._credentials,
621 client_info=info,
622 client_options=client._client_options,
623 )
624 return _MetricsAPI(generated, client)
625
626
627def make_sinks_api(client):
628 """Create an instance of the Sinks API adapter.
629
630 Args:
631 client (~logging_v2.client.Client): The client
632 that holds configuration details.
633
634 Returns:
635 _SinksAPI: A metrics API instance with the proper credentials.
636 """
637 info = client._client_info
638 if isinstance(info, client_info.ClientInfo):
639 # convert into gapic-compatible subclass
640 info = _client_info_to_gapic(info)
641
642 generated = ConfigServiceV2Client(
643 credentials=client._credentials,
644 client_info=info,
645 client_options=client._client_options,
646 )
647 return _SinksAPI(generated, client)