Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/google/cloud/logging_v2/logger.py: 32%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

153 statements  

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"""Define API Loggers.""" 

16 

17import collections 

18import re 

19 

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 

28 

29from google.api_core.exceptions import InvalidArgument 

30from google.rpc.error_details_pb2 import DebugInfo 

31 

32import google.cloud.logging_v2 

33import google.protobuf.message 

34 

35_GLOBAL_RESOURCE = Resource(type="global", labels={}) 

36 

37 

38_OUTBOUND_ENTRY_FIELDS = ( # (name, default) 

39 ("type_", None), 

40 ("log_name", None), 

41 ("payload", None), 

42 ("labels", None), 

43 ("insert_id", None), 

44 ("severity", None), 

45 ("http_request", None), 

46 ("timestamp", None), 

47 ("resource", _GLOBAL_RESOURCE), 

48 ("trace", None), 

49 ("span_id", None), 

50 ("trace_sampled", None), 

51 ("source_location", None), 

52) 

53 

54_STRUCT_EXTRACTABLE_FIELDS = ["severity", "trace", "span_id"] 

55 

56 

57class Logger(object): 

58 """Loggers represent named targets for log entries. 

59 

60 See https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.logs 

61 """ 

62 

63 def __init__(self, name, client, *, labels=None, resource=None): 

64 """ 

65 Args: 

66 name (str): The name of the logger. 

67 client (~logging_v2.client.Client): 

68 A client which holds credentials and project configuration 

69 for the logger (which requires a project). 

70 resource (Optional[~logging_v2.Resource]): a monitored resource object 

71 representing the resource the code was run on. If not given, will 

72 be inferred from the environment. 

73 labels (Optional[dict]): Mapping of default labels for entries written 

74 via this logger. 

75 

76 """ 

77 if not resource: 

78 # infer the correct monitored resource from the local environment 

79 resource = detect_resource(client.project) 

80 self.name = name 

81 self._client = client 

82 self.labels = labels 

83 self.default_resource = resource 

84 

85 @property 

86 def client(self): 

87 """Clent bound to the logger.""" 

88 return self._client 

89 

90 @property 

91 def project(self): 

92 """Project bound to the logger.""" 

93 return self._client.project 

94 

95 @property 

96 def full_name(self): 

97 """Fully-qualified name used in logging APIs""" 

98 return f"projects/{self.project}/logs/{self.name}" 

99 

100 @property 

101 def path(self): 

102 """URI path for use in logging APIs""" 

103 return f"/{self.full_name}" 

104 

105 def _require_client(self, client): 

106 """Check client or verify over-ride. Also sets ``parent``. 

107 

108 Args: 

109 client (Union[None, ~logging_v2.client.Client]): 

110 The client to use. If not passed, falls back to the 

111 ``client`` stored on the current sink. 

112 

113 Returns: 

114 ~logging_v2.client.Client: The client passed in 

115 or the currently bound client. 

116 """ 

117 if client is None: 

118 client = self._client 

119 return client 

120 

121 def batch(self, *, client=None): 

122 """Return a batch to use as a context manager. 

123 

124 Args: 

125 client (Union[None, ~logging_v2.client.Client]): 

126 The client to use. If not passed, falls back to the 

127 ``client`` stored on the current sink. 

128 

129 Returns: 

130 Batch: A batch to use as a context manager. 

131 """ 

132 client = self._require_client(client) 

133 return Batch(self, client) 

134 

135 def _do_log(self, client, _entry_class, payload=None, **kw): 

136 """Helper for :meth:`log_empty`, :meth:`log_text`, etc.""" 

137 client = self._require_client(client) 

138 

139 # Apply defaults 

140 kw["log_name"] = kw.pop("log_name", self.full_name) 

141 kw["labels"] = kw.pop("labels", self.labels) 

142 kw["resource"] = kw.pop("resource", self.default_resource) 

143 

144 severity = kw.get("severity", None) 

145 if isinstance(severity, str): 

146 # convert severity to upper case, as expected by enum definition 

147 kw["severity"] = severity.upper() 

148 

149 if isinstance(kw["resource"], collections.abc.Mapping): 

150 # if resource was passed as a dict, attempt to parse it into a 

151 # Resource object 

152 try: 

153 kw["resource"] = Resource(**kw["resource"]) 

154 except TypeError as e: 

155 # dict couldn't be parsed as a Resource 

156 raise TypeError("invalid resource dict") from e 

157 

158 if payload is not None: 

159 entry = _entry_class(payload=payload, **kw) 

160 else: 

161 entry = _entry_class(**kw) 

162 

163 api_repr = entry.to_api_repr() 

164 entries = [api_repr] 

165 

166 if google.cloud.logging_v2._instrumentation_emitted is False: 

167 entries = _add_instrumentation(entries, **kw) 

168 google.cloud.logging_v2._instrumentation_emitted = True 

169 # partial_success is true to avoid dropping instrumentation logs 

170 client.logging_api.write_entries(entries, partial_success=True) 

171 

172 def log_empty(self, *, client=None, **kw): 

173 """Log an empty message 

174 

175 See 

176 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write 

177 

178 Args: 

179 client (Optional[~logging_v2.client.Client]): 

180 The client to use. If not passed, falls back to the 

181 ``client`` stored on the current sink. 

182 kw (Optional[dict]): additional keyword arguments for the entry. 

183 See :class:`~logging_v2.entries.LogEntry`. 

184 """ 

185 self._do_log(client, LogEntry, **kw) 

186 

187 def log_text(self, text, *, client=None, **kw): 

188 """Log a text message 

189 

190 See 

191 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write 

192 

193 Args: 

194 text (str): the log message 

195 client (Optional[~logging_v2.client.Client]): 

196 The client to use. If not passed, falls back to the 

197 ``client`` stored on the current sink. 

198 kw (Optional[dict]): additional keyword arguments for the entry. 

199 See :class:`~logging_v2.entries.LogEntry`. 

200 """ 

201 self._do_log(client, TextEntry, text, **kw) 

202 

203 def log_struct(self, info, *, client=None, **kw): 

204 """Logs a dictionary message. 

205 

206 See 

207 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write 

208 

209 The message must be able to be serializable to a Protobuf Struct. 

210 It must be a dictionary of strings to one of the following: 

211 

212 - :class:`str` 

213 - :class:`int` 

214 - :class:`float` 

215 - :class:`bool` 

216 - :class:`list[str|float|int|bool|list|dict|None]` 

217 - :class:`dict[str, str|float|int|bool|list|dict|None]` 

218 

219 For more details on Protobuf structs, see https://protobuf.dev/reference/protobuf/google.protobuf/#value. 

220 If the provided dictionary cannot be serialized into a Protobuf struct, 

221 it will not be logged, and a :class:`ValueError` will be raised. 

222 

223 Args: 

224 info (dict[str, str|float|int|bool|list|dict|None]): 

225 the log entry information. 

226 client (Optional[~logging_v2.client.Client]): 

227 The client to use. If not passed, falls back to the 

228 ``client`` stored on the current sink. 

229 kw (Optional[dict]): additional keyword arguments for the entry. 

230 See :class:`~logging_v2.entries.LogEntry`. 

231 

232 Raises: 

233 ValueError: 

234 if the dictionary message provided cannot be serialized into a Protobuf 

235 struct. 

236 """ 

237 for field in _STRUCT_EXTRACTABLE_FIELDS: 

238 # attempt to copy relevant fields from the payload into the LogEntry body 

239 if field in info and field not in kw: 

240 kw[field] = info[field] 

241 self._do_log(client, StructEntry, info, **kw) 

242 

243 def log_proto(self, message, *, client=None, **kw): 

244 """Log a protobuf message 

245 

246 See 

247 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/list 

248 

249 Args: 

250 message (google.protobuf.message.Message): 

251 The protobuf message to be logged. 

252 client (Optional[~logging_v2.client.Client]): 

253 The client to use. If not passed, falls back to the 

254 ``client`` stored on the current sink. 

255 kw (Optional[dict]): additional keyword arguments for the entry. 

256 See :class:`~logging_v2.entries.LogEntry`. 

257 """ 

258 self._do_log(client, ProtobufEntry, message, **kw) 

259 

260 def log(self, message=None, *, client=None, **kw): 

261 """Log an arbitrary message. Type will be inferred based on the input. 

262 

263 See 

264 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/list 

265 

266 Args: 

267 message (Optional[str or dict or google.protobuf.Message]): The message. to log 

268 client (Optional[~logging_v2.client.Client]): 

269 The client to use. If not passed, falls back to the 

270 ``client`` stored on the current sink. 

271 kw (Optional[dict]): additional keyword arguments for the entry. 

272 See :class:`~logging_v2.entries.LogEntry`. 

273 """ 

274 if isinstance(message, google.protobuf.message.Message): 

275 self.log_proto(message, client=client, **kw) 

276 elif isinstance(message, collections.abc.Mapping): 

277 self.log_struct(message, client=client, **kw) 

278 elif isinstance(message, str): 

279 self.log_text(message, client=client, **kw) 

280 else: 

281 self._do_log(client, LogEntry, message, **kw) 

282 

283 def delete(self, logger_name=None, *, client=None): 

284 """Delete all entries in a logger via a DELETE request 

285 

286 See 

287 https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.logs/delete 

288 

289 Args: 

290 logger_name (Optional[str]): The resource name of the log to delete: 

291 

292 :: 

293 

294 "projects/[PROJECT_ID]/logs/[LOG_ID]" 

295 "organizations/[ORGANIZATION_ID]/logs/[LOG_ID]" 

296 "billingAccounts/[BILLING_ACCOUNT_ID]/logs/[LOG_ID]" 

297 "folders/[FOLDER_ID]/logs/[LOG_ID]" 

298 

299 ``[LOG_ID]`` must be URL-encoded. For example, 

300 ``"projects/my-project-id/logs/syslog"``, 

301 ``"organizations/1234567890/logs/cloudresourcemanager.googleapis.com%2Factivity"``. 

302 If not passed, defaults to the project bound to the client. 

303 client (Optional[~logging_v2.client.Client]): 

304 The client to use. If not passed, falls back to the 

305 ``client`` stored on the current logger. 

306 """ 

307 client = self._require_client(client) 

308 if logger_name is None: 

309 logger_name = self.full_name 

310 client.logging_api.logger_delete(logger_name) 

311 

312 def list_entries( 

313 self, 

314 *, 

315 resource_names=None, 

316 filter_=None, 

317 order_by=None, 

318 max_results=None, 

319 page_size=None, 

320 page_token=None, 

321 ): 

322 """Return a generator of log entry resources. 

323 

324 See 

325 https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/list 

326 

327 Args: 

328 resource_names (Optional[Sequence[str]]): Names of one or more parent resources 

329 from which to retrieve log entries: 

330 

331 :: 

332 

333 "projects/[PROJECT_ID]" 

334 "organizations/[ORGANIZATION_ID]" 

335 "billingAccounts/[BILLING_ACCOUNT_ID]" 

336 "folders/[FOLDER_ID]" 

337 

338 If not passed, defaults to the project bound to the client. 

339 filter_ (Optional[str]): a filter expression. See 

340 https://cloud.google.com/logging/docs/view/advanced_filters 

341 By default, a 24 hour filter is applied. 

342 order_by (Optional[str]): One of :data:`~logging_v2.ASCENDING` 

343 or :data:`~logging_v2.DESCENDING`. 

344 max_results (Optional[int]): 

345 Optional. The maximum number of entries to return. 

346 Non-positive values are treated as 0. If None, uses API defaults. 

347 page_size (int): number of entries to fetch in each API call. Although 

348 requests are paged internally, logs are returned by the generator 

349 one at a time. If not passed, defaults to a value set by the API. 

350 page_token (str): opaque marker for the starting "page" of entries. If not 

351 passed, the API will return the first page of entries. 

352 Returns: 

353 Generator[~logging_v2.LogEntry] 

354 """ 

355 

356 if resource_names is None: 

357 resource_names = [f"projects/{self.project}"] 

358 

359 log_filter = f"logName={self.full_name}" 

360 if filter_ is not None: 

361 filter_ = f"{filter_} AND {log_filter}" 

362 else: 

363 filter_ = log_filter 

364 filter_ = _add_defaults_to_filter(filter_) 

365 return self.client.list_entries( 

366 resource_names=resource_names, 

367 filter_=filter_, 

368 order_by=order_by, 

369 max_results=max_results, 

370 page_size=page_size, 

371 page_token=page_token, 

372 ) 

373 

374 

375class Batch(object): 

376 def __init__(self, logger, client, *, resource=None): 

377 """Context manager: collect entries to log via a single API call. 

378 

379 Helper returned by :meth:`Logger.batch` 

380 

381 Args: 

382 logger (logging_v2.logger.Logger): 

383 the logger to which entries will be logged. 

384 client (~logging_V2.client.Client): 

385 The client to use. 

386 resource (Optional[~logging_v2.resource.Resource]): 

387 Monitored resource of the batch, defaults 

388 to None, which requires that every entry should have a 

389 resource specified. Since the methods used to write 

390 entries default the entry's resource to the global 

391 resource type, this parameter is only required 

392 if explicitly set to None. If no entries' resource are 

393 set to None, this parameter will be ignored on the server. 

394 """ 

395 self.logger = logger 

396 self.entries = [] 

397 self.client = client 

398 self.resource = resource 

399 

400 def __enter__(self): 

401 return self 

402 

403 def __exit__(self, exc_type, exc_val, exc_tb): 

404 if exc_type is None: 

405 self.commit() 

406 

407 def log_empty(self, **kw): 

408 """Add a entry without payload to be logged during :meth:`commit`. 

409 

410 Args: 

411 kw (Optional[dict]): Additional keyword arguments for the entry. 

412 See :class:`~logging_v2.entries.LogEntry`. 

413 """ 

414 self.entries.append(LogEntry(**kw)) 

415 

416 def log_text(self, text, **kw): 

417 """Add a text entry to be logged during :meth:`commit`. 

418 

419 Args: 

420 text (str): the text entry 

421 kw (Optional[dict]): Additional keyword arguments for the entry. 

422 See :class:`~logging_v2.entries.LogEntry`. 

423 """ 

424 self.entries.append(TextEntry(payload=text, **kw)) 

425 

426 def log_struct(self, info, **kw): 

427 """Add a struct entry to be logged during :meth:`commit`. 

428 

429 The message must be able to be serializable to a Protobuf Struct. 

430 It must be a dictionary of strings to one of the following: 

431 

432 - :class:`str` 

433 - :class:`int` 

434 - :class:`float` 

435 - :class:`bool` 

436 - :class:`list[str|float|int|bool|list|dict|None]` 

437 - :class:`dict[str, str|float|int|bool|list|dict|None]` 

438 

439 For more details on Protobuf structs, see https://protobuf.dev/reference/protobuf/google.protobuf/#value. 

440 If the provided dictionary cannot be serialized into a Protobuf struct, 

441 it will not be logged, and a :class:`ValueError` will be raised during :meth:`commit`. 

442 

443 Args: 

444 info (dict[str, str|float|int|bool|list|dict|None]): The struct entry, 

445 kw (Optional[dict]): Additional keyword arguments for the entry. 

446 See :class:`~logging_v2.entries.LogEntry`. 

447 """ 

448 self.entries.append(StructEntry(payload=info, **kw)) 

449 

450 def log_proto(self, message, **kw): 

451 """Add a protobuf entry to be logged during :meth:`commit`. 

452 

453 Args: 

454 message (google.protobuf.Message): The protobuf entry. 

455 kw (Optional[dict]): Additional keyword arguments for the entry. 

456 See :class:`~logging_v2.entries.LogEntry`. 

457 """ 

458 self.entries.append(ProtobufEntry(payload=message, **kw)) 

459 

460 def log(self, message=None, **kw): 

461 """Add an arbitrary message to be logged during :meth:`commit`. 

462 Type will be inferred based on the input message. 

463 

464 Args: 

465 message (Optional[str or dict or google.protobuf.Message]): The message. to log 

466 kw (Optional[dict]): Additional keyword arguments for the entry. 

467 See :class:`~logging_v2.entries.LogEntry`. 

468 """ 

469 entry_type = LogEntry 

470 if isinstance(message, google.protobuf.message.Message): 

471 entry_type = ProtobufEntry 

472 elif isinstance(message, collections.abc.Mapping): 

473 entry_type = StructEntry 

474 elif isinstance(message, str): 

475 entry_type = TextEntry 

476 self.entries.append(entry_type(payload=message, **kw)) 

477 

478 def commit(self, *, client=None, partial_success=True): 

479 """Send saved log entries as a single API call. 

480 

481 Args: 

482 client (Optional[~logging_v2.client.Client]): 

483 The client to use. If not passed, falls back to the 

484 ``client`` stored on the current batch. 

485 partial_success (Optional[bool]): 

486 Whether a batch's valid entries should be written even 

487 if some other entry failed due to a permanent error such 

488 as INVALID_ARGUMENT or PERMISSION_DENIED. 

489 

490 Raises: 

491 ValueError: 

492 if one of the messages in the batch cannot be successfully parsed. 

493 """ 

494 if client is None: 

495 client = self.client 

496 

497 kwargs = {"logger_name": self.logger.full_name} 

498 

499 if self.resource is not None: 

500 kwargs["resource"] = self.resource._to_dict() 

501 

502 if self.logger.labels is not None: 

503 kwargs["labels"] = self.logger.labels 

504 

505 entries = [entry.to_api_repr() for entry in self.entries] 

506 try: 

507 client.logging_api.write_entries( 

508 entries, partial_success=partial_success, **kwargs 

509 ) 

510 except InvalidArgument as e: 

511 # InvalidArgument is often sent when a log is too large 

512 # attempt to attach extra contex on which log caused error 

513 self._append_context_to_error(e) 

514 raise e 

515 del self.entries[:] 

516 

517 def _append_context_to_error(self, err): 

518 """ 

519 Attempts to Modify `write_entries` exception messages to contain 

520 context on which log in the batch caused the error. 

521 

522 Best-effort basis. If another exception occurs while processing the 

523 input exception, the input will be left unmodified 

524 

525 Args: 

526 err (~google.api_core.exceptions.InvalidArgument): 

527 The original exception object 

528 """ 

529 try: 

530 # find debug info proto if in details 

531 debug_info = next(x for x in err.details if isinstance(x, DebugInfo)) 

532 # parse out the index of the faulty entry 

533 error_idx = re.search("(?<=key: )[0-9]+", debug_info.detail).group(0) 

534 # find the faulty entry object 

535 found_entry = self.entries[int(error_idx)] 

536 str_entry = str(found_entry.to_api_repr()) 

537 # modify error message to contain extra context 

538 err.message = f"{err.message}: {str_entry:.2000}..." 

539 except Exception: 

540 # if parsing fails, abort changes and leave err unmodified 

541 pass