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

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.protobuf.message 

33 

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

35 

36 

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) 

52 

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

54 

55 

56class Logger(object): 

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

58 

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

60 """ 

61 

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. 

74 

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 

83 

84 @property 

85 def client(self): 

86 """Clent bound to the logger.""" 

87 return self._client 

88 

89 @property 

90 def project(self): 

91 """Project bound to the logger.""" 

92 return self._client.project 

93 

94 @property 

95 def full_name(self): 

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

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

98 

99 @property 

100 def path(self): 

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

102 return f"/{self.full_name}" 

103 

104 def _require_client(self, client): 

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

106 

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. 

111 

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 

119 

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

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

122 

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. 

127 

128 Returns: 

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

130 """ 

131 client = self._require_client(client) 

132 return Batch(self, client) 

133 

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) 

137 

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) 

142 

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() 

147 

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 

156 

157 if payload is not None: 

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

159 else: 

160 entry = _entry_class(**kw) 

161 

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) 

169 

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

171 """Log an empty message 

172 

173 See 

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

175 

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) 

184 

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

186 """Log a text message 

187 

188 See 

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

190 

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) 

200 

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

202 """Log a dictionary message 

203 

204 See 

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

206 

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) 

220 

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

222 """Log a protobuf message 

223 

224 See 

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

226 

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) 

237 

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

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

240 

241 See 

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

243 

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) 

260 

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

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

263 

264 See 

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

266 

267 Args: 

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

269 

270 :: 

271 

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]" 

276 

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) 

289 

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. 

301 

302 See 

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

304 

305 Args: 

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

307 from which to retrieve log entries: 

308 

309 :: 

310 

311 "projects/[PROJECT_ID]" 

312 "organizations/[ORGANIZATION_ID]" 

313 "billingAccounts/[BILLING_ACCOUNT_ID]" 

314 "folders/[FOLDER_ID]" 

315 

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 """ 

333 

334 if resource_names is None: 

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

336 

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 ) 

351 

352 

353class Batch(object): 

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

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

356 

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

358 

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 

377 

378 def __enter__(self): 

379 return self 

380 

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

382 if exc_type is None: 

383 self.commit() 

384 

385 def log_empty(self, **kw): 

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

387 

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)) 

393 

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

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

396 

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)) 

403 

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

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

406 

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)) 

413 

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

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

416 

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)) 

423 

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. 

427 

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)) 

441 

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

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

444 

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 

456 

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

458 

459 if self.resource is not None: 

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

461 

462 if self.logger.labels is not None: 

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

464 

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[:] 

476 

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. 

481 

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

483 input exception, the input will be left unmodified 

484 

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