Coverage for pyTooling/Tracing/CI/__init__.py: 99%

99 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-03 23:02 +0000

1# ==================================================================================================================== # 

2# _____ _ _ _____ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _|_ _| __ __ _ ___(_)_ __ __ _ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | || '__/ _` |/ __| | '_ \ / _` | # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| || | | (_| | (__| | | | | (_| | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_||_| \__,_|\___|_|_| |_|\__, | # 

7# |_| |___/ |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2026-2026 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

32Readers converting the timing of CI pipelines into a software execution trace (:class:`~pyTooling.Tracing.Trace`). 

33 

34Every reader marks its timespans with the same attributes, so a renderer or a query doesn't need to know which CI 

35service a trace came from: 

36 

37* :attr:`CI.Span.Kind` classifies a timespan by a member of :class:`SpanKind`. 

38* :class:`OTLP` holds the attribute keys of OpenTelemetry's semantic conventions, nested as the keys themselves are, 

39 and :class:`Result` the values the conventions allow for a result. 

40 

41.. hint:: 

42 

43 See :ref:`high-level help <TRACING/CI>` for explanations and usage examples. 

44""" 

45from datetime import datetime 

46from typing import ClassVar, Mapping, Optional as Nullable 

47 

48from pyTooling.Common import StringEnum 

49from pyTooling.Decorators import export 

50from pyTooling.MetaClasses import ExtendedType, abstractclass 

51from pyTooling.Tracing import AttributeValue, Span, Trace 

52 

53 

54@export 

55class OTLP(metaclass=ExtendedType, slots=True): 

56 """ 

57 Attribute keys defined by OpenTelemetry's `semantic conventions <https://opentelemetry.io/docs/specs/semconv/>`__. 

58 

59 The nesting mirrors the key itself: every level is one dot, so :attr:`OTLP.CICD.Pipeline.Task.Run.ID` spells 

60 ``'cicd.pipeline.task.run.id'``. A key can therefore be checked by reading the path that names it. 

61 

62 .. code-block:: python 

63 

64 trace[OTLP.CICD.Pipeline.Name] = pipeline.Name 

65 span[OTLP.CICD.Pipeline.Task.Run.ID] = str(job.ID) 

66 """ 

67 

68 class CICD(metaclass=ExtendedType, slots=True): 

69 """Attribute keys of the conventions for **CI/CD pipelines**.""" 

70 

71 class Pipeline(metaclass=ExtendedType, slots=True): 

72 """Attribute keys naming a pipeline and its run.""" 

73 

74 Name: ClassVar[str] = "cicd.pipeline.name" #: The pipeline's name. 

75 Result: ClassVar[str] = "cicd.pipeline.result" #: How the run ended - a member of :class:`Result`. 

76 

77 class Run(metaclass=ExtendedType, slots=True): 

78 """Attribute keys naming one run of a pipeline.""" 

79 

80 ID: ClassVar[str] = "cicd.pipeline.run.id" #: The run's identifier. 

81 

82 class URL(metaclass=ExtendedType, slots=True): 

83 """Attribute keys naming the addresses of a run.""" 

84 

85 Full: ClassVar[str] = "cicd.pipeline.run.url.full" #: The run's address. 

86 

87 class Task(metaclass=ExtendedType, slots=True): 

88 """Attribute keys naming a task of a pipeline - a job or a step.""" 

89 

90 Name: ClassVar[str] = "cicd.pipeline.task.name" #: The task's name, as the service reports it. 

91 

92 class Run(metaclass=ExtendedType, slots=True): 

93 """Attribute keys naming one run of a task.""" 

94 

95 ID: ClassVar[str] = "cicd.pipeline.task.run.id" #: The task run's identifier. 

96 Result: ClassVar[str] = "cicd.pipeline.task.run.result" #: How the task ended - a member of :class:`Result`. 

97 

98 class URL(metaclass=ExtendedType, slots=True): 

99 """Attribute keys naming the addresses of a task run.""" 

100 

101 Full: ClassVar[str] = "cicd.pipeline.task.run.url.full" #: The task run's address. 

102 

103 class Worker(metaclass=ExtendedType, slots=True): 

104 """Attribute keys naming the worker a task ran on.""" 

105 

106 Name: ClassVar[str] = "cicd.worker.name" #: The worker's name. 

107 

108 class VCS(metaclass=ExtendedType, slots=True): 

109 """Attribute keys of the conventions for **version control systems**.""" 

110 

111 class Ref(metaclass=ExtendedType, slots=True): 

112 """Attribute keys naming a reference.""" 

113 

114 class Head(metaclass=ExtendedType, slots=True): 

115 """Attribute keys naming the reference a pipeline was started on.""" 

116 

117 Name: ClassVar[str] = "vcs.ref.head.name" #: The branch or tag the run was started on. 

118 Revision: ClassVar[str] = "vcs.ref.head.revision" #: The commit the run was started on. 

119 

120 

121@export 

122class CI(metaclass=ExtendedType, slots=True): 

123 """ 

124 Attribute keys pyTooling defines for the timespans of a CI pipeline, which the conventions don't cover. 

125 

126 The nesting mirrors the key the same way :class:`OTLP` does. 

127 """ 

128 

129 class Span(metaclass=ExtendedType, slots=True): 

130 """Attribute keys classifying a timespan.""" 

131 

132 Kind: ClassVar[str] = "ci.span.kind" #: What the timespan represents - a member of :class:`SpanKind`. 

133 

134 

135@export 

136class SpanKind(StringEnum): 

137 """ 

138 What a timespan of a CI pipeline represents. 

139 

140 These values are pyTooling's own: OpenTelemetry's conventions classify a span by its kind (``SERVER``, 

141 ``INTERNAL``, ...), not by its role in a pipeline. 

142 """ 

143 

144 Pipeline = "pipeline" #: A whole pipeline run - the trace itself. 

145 Workflow = "workflow" #: The jobs of a called workflow or a stage, grouped. 

146 Matrix = "matrix" #: A matrix, holding the job instances it produced. 

147 Queued = "queued" #: The time a job waited for a worker, in front of the job's own timespan. 

148 Job = "job" #: A job running on a worker. 

149 Step = "step" #: A step of a job. 

150 

151 

152@export 

153class Result(StringEnum): 

154 """ 

155 How a pipeline run or a task run ended. 

156 

157 The conventions fix this set, and a backend groups runs by the string, so a member's :attr:`~enum.Enum.value` is 

158 what goes on the wire. 

159 """ 

160 

161 Success = "success" #: It succeeded. 

162 Failure = "failure" #: It failed. 

163 Timeout = "timeout" #: It was stopped by a timeout. 

164 Skip = "skip" #: It was skipped. 

165 Cancellation = "cancellation" #: It was cancelled. 

166 Error = "error" #: It ended for any other reason. 

167 

168 

169 

170@export 

171class SetAttributesMixin(metaclass=ExtendedType, mixin=True, expects=("__setitem__",)): 

172 """Mixin-class for a timespan that is given many attributes at once, of which some may be unknown.""" 

173 

174 def _SetAttributes(self, attributes: Mapping[str, Nullable[AttributeValue]]) -> None: 

175 """ 

176 Set the attributes whose value is known. 

177 

178 A service reports a field it doesn't know as ``None``, and one it knows to be empty as an empty string or an 

179 empty list. Neither is worth an attribute, so both are skipped and the key stays absent instead of naming an 

180 empty value. 

181 

182 :param attributes: The attributes by key. 

183 """ 

184 for key, value in attributes.items(): 

185 if value is not None and value != "" and value != []: 

186 self[key] = value 

187 

188 

189@export 

190class PipelineTrace(Trace, SetAttributesMixin): 

191 """A pipeline run - the trace every other timespan of the run is below.""" 

192 

193 KIND: ClassVar[SpanKind] = SpanKind.Pipeline #: This flavour represents a pipeline run. 

194 

195 def __init__( 

196 self, 

197 name: str, 

198 beginTime: Nullable[datetime] = None, 

199 endTime: Nullable[datetime] = None, 

200 *, 

201 pipelineName: Nullable[str] = None, 

202 runID: Nullable[str] = None, 

203 runURL: Nullable[str] = None, 

204 result: Nullable[Result] = None, 

205 reference: Nullable[str] = None, 

206 revision: Nullable[str] = None, 

207 attributes: Nullable[Mapping[str, Nullable[AttributeValue]]] = None 

208 ) -> None: 

209 """ 

210 Initializes the trace of a pipeline run. 

211 

212 :param name: Name of the trace. 

213 :param beginTime: Optional, recorded time when the run began. Default: the time the trace is entered. 

214 :param endTime: Optional, recorded time when the run ended. Default: the run is still running. 

215 :param pipelineName: Optional, the pipeline's name, if it differs from the trace's. Default: the trace's name. 

216 :param runID: Optional, the run's identifier. Default: unknown. 

217 :param runURL: Optional, the run's address. Default: unknown. 

218 :param result: Optional, how the run ended. Default: it hasn't ended. 

219 :param reference: Optional, the branch or tag the run was started on. Default: unknown. 

220 :param revision: Optional, the commit the run was started on. Default: unknown. 

221 :param attributes: Optional, further attributes, e.g. what only one service reports. Default: none. 

222 """ 

223 super().__init__(name, beginTime, endTime) 

224 

225 self[CI.Span.Kind] = self.KIND 

226 self._SetAttributes({ 

227 OTLP.CICD.Pipeline.Name: name if pipelineName is None else pipelineName, 

228 OTLP.CICD.Pipeline.Run.ID: runID, 

229 OTLP.CICD.Pipeline.Run.URL.Full: runURL, 

230 OTLP.CICD.Pipeline.Result: result, 

231 OTLP.VCS.Ref.Head.Name: reference, 

232 OTLP.VCS.Ref.Head.Revision: revision 

233 }) 

234 

235 if attributes is not None: 

236 self._SetAttributes(attributes) 

237 

238 

239@export 

240@abstractclass 

241class TaskSpan(Span, SetAttributesMixin): 

242 """ 

243 Base-class of the timespans below a pipeline run. 

244 

245 Every one of them is a task of the pipeline in the conventions' sense, so every one names itself in 

246 :attr:`OTLP.CICD.Pipeline.Task.Name <pyTooling.Tracing.CI.OTLP>` - with the name the service reports, which may 

247 differ from the timespan's when a timespan is named by the part of the tree it sits in. 

248 

249 It names no :attr:`KIND` itself, because there is no timespan that is a task and nothing more. 

250 """ 

251 

252 KIND: ClassVar[SpanKind] #: What a timespan of this flavour represents. Every flavour names it. 

253 

254 def __init__( 

255 self, 

256 name: str, 

257 beginTime: Nullable[datetime] = None, 

258 endTime: Nullable[datetime] = None, 

259 *, 

260 parent: Nullable[Span] = None, 

261 taskName: Nullable[str] = None, 

262 attributes: Nullable[Mapping[str, Nullable[AttributeValue]]] = None 

263 ) -> None: 

264 """ 

265 Initializes a timespan below a pipeline run. 

266 

267 :param name: Name of the timespan. 

268 :param beginTime: Optional, recorded time when it began. Default: the time the timespan is entered. 

269 :param endTime: Optional, recorded time when it ended. Default: it is still running. 

270 :param parent: Optional, the timespan it sits in. Default: no parent. 

271 :param taskName: Optional, the name the service reports, if it differs from the timespan's. Default: the 

272 timespan's name. 

273 :param attributes: Optional, further attributes, e.g. what only one service reports. Default: none. 

274 """ 

275 super().__init__(name, beginTime, endTime, parent=parent) 

276 

277 self[CI.Span.Kind] = self.KIND 

278 self[OTLP.CICD.Pipeline.Task.Name] = name if taskName is None else taskName 

279 

280 if attributes is not None: 

281 self._SetAttributes(attributes) 

282 

283 

284@export 

285class WorkflowSpan(TaskSpan): 

286 """The jobs of a called workflow or a stage, grouped into one timespan.""" 

287 

288 KIND: ClassVar[SpanKind] = SpanKind.Workflow #: This flavour represents the jobs of a called workflow or a stage. 

289 

290 

291@export 

292class MatrixSpan(TaskSpan): 

293 """The instances a matrix produced, grouped into one timespan.""" 

294 

295 KIND: ClassVar[SpanKind] = SpanKind.Matrix #: This flavour represents the instances of a matrix. 

296 

297 

298@export 

299class QueuedSpan(TaskSpan): 

300 """The time a job waited for a worker, in front of the job's own timespan.""" 

301 

302 KIND: ClassVar[SpanKind] = SpanKind.Queued #: This flavour represents a job waiting for a worker. 

303 

304 

305@export 

306class JobSpan(TaskSpan): 

307 """A job, from the moment it started on a worker until it completed.""" 

308 

309 KIND: ClassVar[SpanKind] = SpanKind.Job #: This flavour represents a job. 

310 

311 def __init__( 

312 self, 

313 name: str, 

314 beginTime: Nullable[datetime] = None, 

315 endTime: Nullable[datetime] = None, 

316 *, 

317 parent: Nullable[Span] = None, 

318 taskName: Nullable[str] = None, 

319 runID: Nullable[str] = None, 

320 runURL: Nullable[str] = None, 

321 result: Nullable[Result] = None, 

322 workerName: Nullable[str] = None, 

323 attributes: Nullable[Mapping[str, Nullable[AttributeValue]]] = None 

324 ) -> None: 

325 """ 

326 Initializes the timespan of a job. 

327 

328 :param name: Name of the timespan. 

329 :param beginTime: Optional, recorded time when the job started. Default: the time the timespan is entered. 

330 :param endTime: Optional, recorded time when the job completed. Default: it is still running. 

331 :param parent: Optional, the timespan it sits in. Default: no parent. 

332 :param taskName: Optional, the name the service reports, if it differs from the timespan's. Default: the 

333 timespan's name. 

334 :param runID: Optional, the job's identifier. Default: unknown. 

335 :param runURL: Optional, the job's address. Default: unknown. 

336 :param result: Optional, how the job ended. Default: it hasn't ended. 

337 :param workerName: Optional, name of the worker the job ran on. Default: unknown. 

338 :param attributes: Optional, further attributes, e.g. what only one service reports. Default: none. 

339 """ 

340 super().__init__(name, beginTime, endTime, parent=parent, taskName=taskName) 

341 

342 self._SetAttributes({ 

343 OTLP.CICD.Pipeline.Task.Run.ID: runID, 

344 OTLP.CICD.Pipeline.Task.Run.URL.Full: runURL, 

345 OTLP.CICD.Pipeline.Task.Run.Result: result, 

346 OTLP.CICD.Worker.Name: workerName 

347 }) 

348 

349 if attributes is not None: 

350 self._SetAttributes(attributes) 

351 

352 

353@export 

354class StepSpan(TaskSpan): 

355 """A step of a job.""" 

356 

357 KIND: ClassVar[SpanKind] = SpanKind.Step #: This flavour represents a step. 

358 

359 def __init__( 

360 self, 

361 name: str, 

362 beginTime: Nullable[datetime] = None, 

363 endTime: Nullable[datetime] = None, 

364 *, 

365 parent: Nullable[Span] = None, 

366 taskName: Nullable[str] = None, 

367 result: Nullable[Result] = None, 

368 attributes: Nullable[Mapping[str, Nullable[AttributeValue]]] = None 

369 ) -> None: 

370 """ 

371 Initializes the timespan of a step. 

372 

373 :param name: Name of the timespan. 

374 :param beginTime: Optional, recorded time when the step started. Default: the time the timespan is entered. 

375 :param endTime: Optional, recorded time when the step completed. Default: it is still running. 

376 :param parent: Optional, the timespan of the job containing the step. Default: no parent. 

377 :param taskName: Optional, the name the service reports, if it differs from the timespan's. Default: the 

378 timespan's name. 

379 :param result: Optional, how the step ended. Default: it hasn't ended. 

380 :param attributes: Optional, further attributes, e.g. what only one service reports. Default: none. 

381 """ 

382 super().__init__(name, beginTime, endTime, parent=parent, taskName=taskName) 

383 

384 self._SetAttributes({OTLP.CICD.Pipeline.Task.Run.Result: result}) 

385 

386 if attributes is not None: 386 ↛ exitline 386 didn't return from function '__init__' because the condition on line 386 was always true

387 self._SetAttributes(attributes)