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
« 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`).
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:
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.
41.. hint::
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
48from pyTooling.Common import StringEnum
49from pyTooling.Decorators import export
50from pyTooling.MetaClasses import ExtendedType, abstractclass
51from pyTooling.Tracing import AttributeValue, Span, Trace
54@export
55class OTLP(metaclass=ExtendedType, slots=True):
56 """
57 Attribute keys defined by OpenTelemetry's `semantic conventions <https://opentelemetry.io/docs/specs/semconv/>`__.
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.
62 .. code-block:: python
64 trace[OTLP.CICD.Pipeline.Name] = pipeline.Name
65 span[OTLP.CICD.Pipeline.Task.Run.ID] = str(job.ID)
66 """
68 class CICD(metaclass=ExtendedType, slots=True):
69 """Attribute keys of the conventions for **CI/CD pipelines**."""
71 class Pipeline(metaclass=ExtendedType, slots=True):
72 """Attribute keys naming a pipeline and its run."""
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`.
77 class Run(metaclass=ExtendedType, slots=True):
78 """Attribute keys naming one run of a pipeline."""
80 ID: ClassVar[str] = "cicd.pipeline.run.id" #: The run's identifier.
82 class URL(metaclass=ExtendedType, slots=True):
83 """Attribute keys naming the addresses of a run."""
85 Full: ClassVar[str] = "cicd.pipeline.run.url.full" #: The run's address.
87 class Task(metaclass=ExtendedType, slots=True):
88 """Attribute keys naming a task of a pipeline - a job or a step."""
90 Name: ClassVar[str] = "cicd.pipeline.task.name" #: The task's name, as the service reports it.
92 class Run(metaclass=ExtendedType, slots=True):
93 """Attribute keys naming one run of a task."""
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`.
98 class URL(metaclass=ExtendedType, slots=True):
99 """Attribute keys naming the addresses of a task run."""
101 Full: ClassVar[str] = "cicd.pipeline.task.run.url.full" #: The task run's address.
103 class Worker(metaclass=ExtendedType, slots=True):
104 """Attribute keys naming the worker a task ran on."""
106 Name: ClassVar[str] = "cicd.worker.name" #: The worker's name.
108 class VCS(metaclass=ExtendedType, slots=True):
109 """Attribute keys of the conventions for **version control systems**."""
111 class Ref(metaclass=ExtendedType, slots=True):
112 """Attribute keys naming a reference."""
114 class Head(metaclass=ExtendedType, slots=True):
115 """Attribute keys naming the reference a pipeline was started on."""
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.
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.
126 The nesting mirrors the key the same way :class:`OTLP` does.
127 """
129 class Span(metaclass=ExtendedType, slots=True):
130 """Attribute keys classifying a timespan."""
132 Kind: ClassVar[str] = "ci.span.kind" #: What the timespan represents - a member of :class:`SpanKind`.
135@export
136class SpanKind(StringEnum):
137 """
138 What a timespan of a CI pipeline represents.
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 """
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.
152@export
153class Result(StringEnum):
154 """
155 How a pipeline run or a task run ended.
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 """
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.
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."""
174 def _SetAttributes(self, attributes: Mapping[str, Nullable[AttributeValue]]) -> None:
175 """
176 Set the attributes whose value is known.
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.
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
189@export
190class PipelineTrace(Trace, SetAttributesMixin):
191 """A pipeline run - the trace every other timespan of the run is below."""
193 KIND: ClassVar[SpanKind] = SpanKind.Pipeline #: This flavour represents a pipeline run.
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.
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)
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 })
235 if attributes is not None:
236 self._SetAttributes(attributes)
239@export
240@abstractclass
241class TaskSpan(Span, SetAttributesMixin):
242 """
243 Base-class of the timespans below a pipeline run.
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.
249 It names no :attr:`KIND` itself, because there is no timespan that is a task and nothing more.
250 """
252 KIND: ClassVar[SpanKind] #: What a timespan of this flavour represents. Every flavour names it.
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.
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)
277 self[CI.Span.Kind] = self.KIND
278 self[OTLP.CICD.Pipeline.Task.Name] = name if taskName is None else taskName
280 if attributes is not None:
281 self._SetAttributes(attributes)
284@export
285class WorkflowSpan(TaskSpan):
286 """The jobs of a called workflow or a stage, grouped into one timespan."""
288 KIND: ClassVar[SpanKind] = SpanKind.Workflow #: This flavour represents the jobs of a called workflow or a stage.
291@export
292class MatrixSpan(TaskSpan):
293 """The instances a matrix produced, grouped into one timespan."""
295 KIND: ClassVar[SpanKind] = SpanKind.Matrix #: This flavour represents the instances of a matrix.
298@export
299class QueuedSpan(TaskSpan):
300 """The time a job waited for a worker, in front of the job's own timespan."""
302 KIND: ClassVar[SpanKind] = SpanKind.Queued #: This flavour represents a job waiting for a worker.
305@export
306class JobSpan(TaskSpan):
307 """A job, from the moment it started on a worker until it completed."""
309 KIND: ClassVar[SpanKind] = SpanKind.Job #: This flavour represents a job.
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.
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)
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 })
349 if attributes is not None:
350 self._SetAttributes(attributes)
353@export
354class StepSpan(TaskSpan):
355 """A step of a job."""
357 KIND: ClassVar[SpanKind] = SpanKind.Step #: This flavour represents a step.
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.
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)
384 self._SetAttributes({OTLP.CICD.Pipeline.Task.Run.Result: result})
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)