Convert a Python exception into a `dagger.Error`.
(exc: Exception)
| 125 | |
| 126 | |
| 127 | async def record_exception(exc: Exception): |
| 128 | """Convert a Python exception into a `dagger.Error`.""" |
| 129 | attrs: dict[str, Any] = _exception_attributes(exc) |
| 130 | msg = f"{attrs[EXCEPTION_TYPE]}: {attrs[EXCEPTION_MESSAGE]}" |
| 131 | |
| 132 | if isinstance(exc, ModuleError) and exc.extra: |
| 133 | extra = {f"extra.{key}": val for key, val in exc.extra.items()} |
| 134 | # ModuleError extra values don't conflict with the OTel attributes |
| 135 | # but prepending like this avoids a future mistake. |
| 136 | attrs = {**extra, **attrs} |
| 137 | |
| 138 | # Preserve original API error so it's properly propagated. |
| 139 | if isinstance(exc, dagger.QueryError): |
| 140 | msg = str(exc) |
| 141 | attrs.update(exc.error.extensions) |
| 142 | |
| 143 | dag_err = dag.error(msg) |
| 144 | for key, value in attrs.items(): |
| 145 | dag_err = dag_err.with_value(key, dagger.JSON(_safe_json_dumps(value))) |
| 146 | |
| 147 | await dag.current_function_call().return_error(dag_err) |
| 148 | |
| 149 | # When an error occurs within a started span context the OTel SDK |
| 150 | # automatically sends an event with details about the exception. |
| 151 | # Switching to dag.Error doesn't take advantage of that and the engine |
| 152 | # doesn't recreate the exception event on the parent function span. |
| 153 | # Still, recording the exception manually can be useful when analyzing the |
| 154 | # raw telemetry in e.g., Honeycomb. |
| 155 | with telemetry.get_tracer().start_as_current_span( |
| 156 | "recording Python exception", |
| 157 | # TODO: even with following attribute it's still being shown in the |
| 158 | # Cloud UI. |
| 159 | attributes={"dagger.io/ui.internal": True}, |
| 160 | ) as span: |
| 161 | span.record_exception(exc) |
| 162 | |
| 163 | |
| 164 | def _exception_attributes(exc: Exception) -> dict[str, str]: |
no test coverage detected