dagster-cloud 1.8.2__py3-none-any.whl → 1.12.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. dagster_cloud/__init__.py +3 -3
  2. dagster_cloud/agent/__init__.py +4 -4
  3. dagster_cloud/agent/cli/__init__.py +56 -17
  4. dagster_cloud/agent/dagster_cloud_agent.py +360 -172
  5. dagster_cloud/agent/instrumentation/__init__.py +0 -0
  6. dagster_cloud/agent/instrumentation/constants.py +2 -0
  7. dagster_cloud/agent/instrumentation/run_launch.py +23 -0
  8. dagster_cloud/agent/instrumentation/schedule.py +34 -0
  9. dagster_cloud/agent/instrumentation/sensor.py +34 -0
  10. dagster_cloud/anomaly_detection/__init__.py +2 -2
  11. dagster_cloud/anomaly_detection/defs.py +17 -12
  12. dagster_cloud/anomaly_detection/types.py +3 -3
  13. dagster_cloud/api/dagster_cloud_api.py +209 -293
  14. dagster_cloud/auth/constants.py +21 -5
  15. dagster_cloud/batching/__init__.py +1 -0
  16. dagster_cloud/batching/batcher.py +210 -0
  17. dagster_cloud/dagster_insights/__init__.py +12 -6
  18. dagster_cloud/dagster_insights/bigquery/bigquery_utils.py +3 -2
  19. dagster_cloud/dagster_insights/bigquery/dbt_wrapper.py +39 -12
  20. dagster_cloud/dagster_insights/bigquery/insights_bigquery_resource.py +8 -6
  21. dagster_cloud/dagster_insights/insights_utils.py +18 -8
  22. dagster_cloud/dagster_insights/metrics_utils.py +12 -12
  23. dagster_cloud/dagster_insights/snowflake/dagster_snowflake_insights.py +5 -12
  24. dagster_cloud/dagster_insights/snowflake/dbt_wrapper.py +34 -8
  25. dagster_cloud/dagster_insights/snowflake/definitions.py +38 -12
  26. dagster_cloud/dagster_insights/snowflake/insights_snowflake_resource.py +11 -23
  27. dagster_cloud/definitions/__init__.py +0 -0
  28. dagster_cloud/definitions/job_selection.py +36 -0
  29. dagster_cloud/execution/cloud_run_launcher/k8s.py +1 -1
  30. dagster_cloud/execution/cloud_run_launcher/process.py +3 -3
  31. dagster_cloud/execution/monitoring/__init__.py +27 -33
  32. dagster_cloud/execution/utils/process.py +3 -3
  33. dagster_cloud/instance/__init__.py +125 -38
  34. dagster_cloud/instrumentation/__init__.py +32 -0
  35. dagster_cloud/metadata/source_code.py +13 -8
  36. dagster_cloud/metrics/__init__.py +0 -0
  37. dagster_cloud/metrics/tracer.py +59 -0
  38. dagster_cloud/opentelemetry/__init__.py +0 -0
  39. dagster_cloud/opentelemetry/config/__init__.py +73 -0
  40. dagster_cloud/opentelemetry/config/exporter.py +81 -0
  41. dagster_cloud/opentelemetry/config/log_record_processor.py +40 -0
  42. dagster_cloud/opentelemetry/config/logging_handler.py +14 -0
  43. dagster_cloud/opentelemetry/config/meter_provider.py +9 -0
  44. dagster_cloud/opentelemetry/config/metric_reader.py +39 -0
  45. dagster_cloud/opentelemetry/controller.py +319 -0
  46. dagster_cloud/opentelemetry/enum.py +58 -0
  47. dagster_cloud/opentelemetry/factories/__init__.py +1 -0
  48. dagster_cloud/opentelemetry/factories/logs.py +113 -0
  49. dagster_cloud/opentelemetry/factories/metrics.py +121 -0
  50. dagster_cloud/opentelemetry/metrics/__init__.py +0 -0
  51. dagster_cloud/opentelemetry/metrics/meter.py +140 -0
  52. dagster_cloud/opentelemetry/observers/__init__.py +0 -0
  53. dagster_cloud/opentelemetry/observers/dagster_exception_handler.py +40 -0
  54. dagster_cloud/opentelemetry/observers/execution_observer.py +178 -0
  55. dagster_cloud/pex/grpc/__generated__/multi_pex_api_pb2.pyi +175 -0
  56. dagster_cloud/pex/grpc/__init__.py +2 -2
  57. dagster_cloud/pex/grpc/client.py +4 -4
  58. dagster_cloud/pex/grpc/compile.py +2 -2
  59. dagster_cloud/pex/grpc/server/__init__.py +2 -2
  60. dagster_cloud/pex/grpc/server/cli/__init__.py +31 -19
  61. dagster_cloud/pex/grpc/server/manager.py +60 -42
  62. dagster_cloud/pex/grpc/server/registry.py +28 -21
  63. dagster_cloud/pex/grpc/server/server.py +23 -14
  64. dagster_cloud/pex/grpc/types.py +5 -5
  65. dagster_cloud/py.typed +0 -0
  66. dagster_cloud/secrets/__init__.py +1 -1
  67. dagster_cloud/secrets/loader.py +3 -3
  68. dagster_cloud/serverless/__init__.py +1 -1
  69. dagster_cloud/serverless/io_manager.py +36 -53
  70. dagster_cloud/storage/client.py +54 -17
  71. dagster_cloud/storage/compute_logs/__init__.py +3 -1
  72. dagster_cloud/storage/compute_logs/compute_log_manager.py +22 -17
  73. dagster_cloud/storage/defs_state/__init__.py +3 -0
  74. dagster_cloud/storage/defs_state/queries.py +15 -0
  75. dagster_cloud/storage/defs_state/storage.py +113 -0
  76. dagster_cloud/storage/event_logs/__init__.py +3 -1
  77. dagster_cloud/storage/event_logs/queries.py +102 -4
  78. dagster_cloud/storage/event_logs/storage.py +266 -73
  79. dagster_cloud/storage/event_logs/utils.py +88 -7
  80. dagster_cloud/storage/runs/__init__.py +1 -1
  81. dagster_cloud/storage/runs/queries.py +17 -2
  82. dagster_cloud/storage/runs/storage.py +88 -42
  83. dagster_cloud/storage/schedules/__init__.py +1 -1
  84. dagster_cloud/storage/schedules/storage.py +6 -8
  85. dagster_cloud/storage/tags.py +66 -1
  86. dagster_cloud/util/__init__.py +10 -12
  87. dagster_cloud/util/errors.py +49 -64
  88. dagster_cloud/version.py +1 -1
  89. dagster_cloud/workspace/config_schema/__init__.py +55 -13
  90. dagster_cloud/workspace/docker/__init__.py +76 -25
  91. dagster_cloud/workspace/docker/utils.py +1 -1
  92. dagster_cloud/workspace/ecs/__init__.py +1 -1
  93. dagster_cloud/workspace/ecs/client.py +51 -33
  94. dagster_cloud/workspace/ecs/launcher.py +76 -22
  95. dagster_cloud/workspace/ecs/run_launcher.py +3 -3
  96. dagster_cloud/workspace/ecs/utils.py +14 -5
  97. dagster_cloud/workspace/kubernetes/__init__.py +1 -1
  98. dagster_cloud/workspace/kubernetes/launcher.py +61 -29
  99. dagster_cloud/workspace/kubernetes/utils.py +34 -22
  100. dagster_cloud/workspace/user_code_launcher/__init__.py +5 -3
  101. dagster_cloud/workspace/user_code_launcher/process.py +16 -14
  102. dagster_cloud/workspace/user_code_launcher/user_code_launcher.py +552 -172
  103. dagster_cloud/workspace/user_code_launcher/utils.py +105 -1
  104. {dagster_cloud-1.8.2.dist-info → dagster_cloud-1.12.6.dist-info}/METADATA +48 -42
  105. dagster_cloud-1.12.6.dist-info/RECORD +134 -0
  106. {dagster_cloud-1.8.2.dist-info → dagster_cloud-1.12.6.dist-info}/WHEEL +1 -1
  107. dagster_cloud-1.8.2.dist-info/RECORD +0 -100
  108. {dagster_cloud-1.8.2.dist-info → dagster_cloud-1.12.6.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,10 @@
1
1
  import copy
2
2
  import socket
3
3
  import uuid
4
+ from collections.abc import Mapping, Sequence
4
5
  from contextlib import ExitStack
5
6
  from functools import lru_cache
6
- from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union
7
+ from typing import TYPE_CHECKING, Any, Optional, Union
7
8
 
8
9
  import yaml
9
10
  from dagster import (
@@ -19,6 +20,7 @@ from dagster._core.instance.config import config_field_for_configurable_class
19
20
  from dagster._core.instance.ref import InstanceRef, configurable_class_data
20
21
  from dagster._core.launcher import DefaultRunLauncher, RunLauncher
21
22
  from dagster._core.storage.dagster_run import DagsterRun
23
+ from dagster._core.storage.defs_state.base import DefsStateStorage
22
24
  from dagster._serdes import ConfigurableClassData
23
25
  from dagster_cloud_cli.core.graphql_client import (
24
26
  create_agent_graphql_client,
@@ -30,10 +32,12 @@ from dagster_cloud_cli.core.headers.auth import DagsterCloudInstanceScope
30
32
  from urllib3 import Retry
31
33
 
32
34
  from dagster_cloud.agent import AgentQueuesConfig
33
-
34
- from ..auth.constants import get_organization_name_from_agent_token
35
- from ..storage.client import dagster_cloud_api_config
36
- from ..util import get_env_names_from_config, is_isolated_run
35
+ from dagster_cloud.auth.constants import decode_agent_token
36
+ from dagster_cloud.opentelemetry.config import opentelemetry_config_schema
37
+ from dagster_cloud.opentelemetry.controller import OpenTelemetryController
38
+ from dagster_cloud.storage.client import dagster_cloud_api_config
39
+ from dagster_cloud.util import get_env_names_from_config, is_isolated_run
40
+ from dagster_cloud.version import __version__
37
41
 
38
42
  if TYPE_CHECKING:
39
43
  from requests import Session
@@ -48,6 +52,23 @@ class DagsterCloudInstance(DagsterInstance):
48
52
  def telemetry_enabled(self) -> bool:
49
53
  return False
50
54
 
55
+ @property
56
+ def run_retries_max_retries(self) -> int:
57
+ raise NotImplementedError(
58
+ "run_retries.max_retries is a deployment setting and can only be accessed by a DeploymentScopedHostInstance"
59
+ )
60
+
61
+ @property
62
+ def run_retries_retry_on_asset_or_op_failure(self) -> bool:
63
+ raise NotImplementedError(
64
+ "run_retries.retry_on_asset_or_op_failure is a deployment setting and can only be accessed by a DeploymentScopedHostInstance"
65
+ )
66
+
67
+ @property
68
+ def defs_state_storage(self) -> Optional[DefsStateStorage]:
69
+ # only DeploymentScopedHostInstance / DagsterCloudAgentInstance have a defs state storage
70
+ return None
71
+
51
72
 
52
73
  class DagsterCloudAgentInstance(DagsterCloudInstance):
53
74
  def __init__(
@@ -59,13 +80,18 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
59
80
  isolated_agents=None,
60
81
  agent_queues=None,
61
82
  agent_metrics=None,
83
+ opentelemetry=None,
62
84
  **kwargs,
63
85
  ):
64
86
  super().__init__(*args, **kwargs)
65
87
 
66
88
  self._unprocessed_dagster_cloud_api_config = dagster_cloud_api
67
- self._dagster_cloud_api_config = self._get_processed_config(
68
- "dagster_cloud_api", dagster_cloud_api, dagster_cloud_api_config()
89
+ self._dagster_cloud_api_config = check.not_none(
90
+ self._get_processed_config(
91
+ "dagster_cloud_api",
92
+ dagster_cloud_api,
93
+ dagster_cloud_api_config(),
94
+ )
69
95
  )
70
96
 
71
97
  check.invariant(
@@ -117,12 +143,29 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
117
143
  processed_agent_queues_config = self._get_processed_config(
118
144
  "agent_queues", agent_queues, self._agent_queues_config_schema()
119
145
  )
120
- self.agent_queues_config = AgentQueuesConfig(**processed_agent_queues_config)
146
+ self.agent_queues_config = AgentQueuesConfig(**processed_agent_queues_config) # pyright: ignore[reportCallIssue]
147
+
148
+ self._opentelemetry_config: Optional[Mapping[str, Any]] = self._get_processed_config(
149
+ "opentelemetry", opentelemetry, opentelemetry_config_schema()
150
+ )
151
+
152
+ self._opentelemetry_controller: Optional[OpenTelemetryController] = None
121
153
 
122
154
  self._instance_uuid = str(uuid.uuid4())
123
155
 
156
+ @property
157
+ def defs_state_storage(self) -> Optional[DefsStateStorage]:
158
+ # temporary hack to avoid cases where the default BlobStorageStateStorage is used
159
+ from dagster_cloud.storage.defs_state.storage import GraphQLDefsStateStorage
160
+
161
+ return (
162
+ self._defs_state_storage
163
+ if isinstance(self._defs_state_storage, GraphQLDefsStateStorage)
164
+ else None
165
+ )
166
+
124
167
  def _get_processed_config(
125
- self, name: str, config: Optional[Dict[str, Any]], config_type: Dict[str, Any]
168
+ self, name: str, config: Optional[dict[str, Any]], config_type: dict[str, Any]
126
169
  ):
127
170
  config_dict = check.opt_dict_param(config, "config", key_type=str)
128
171
  processed_config = process_config(config_type, config_dict)
@@ -140,15 +183,11 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
140
183
  new_api_config["deployment"] = deployment_name
141
184
  if self.includes_branch_deployments:
142
185
  del new_api_config["branch_deployments"]
143
- if "deployments" in new_api_config:
144
- del new_api_config["deployments"]
145
- if "all_serverless_deployments" in new_api_config:
146
- del new_api_config["all_serverless_deployments"]
186
+ new_api_config.pop("deployments", None)
187
+ new_api_config.pop("all_serverless_deployments", None)
147
188
  else:
148
- if "deployment" in new_api_config:
149
- del new_api_config["deployment"]
150
- if "deployments" in new_api_config:
151
- del new_api_config["deployments"]
189
+ new_api_config.pop("deployment", None)
190
+ new_api_config.pop("deployments", None)
152
191
 
153
192
  return new_api_config
154
193
 
@@ -187,7 +226,7 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
187
226
  return create_agent_graphql_client(
188
227
  self.client_managed_retries_requests_session,
189
228
  self.dagster_cloud_graphql_url,
190
- self._dagster_cloud_api_config,
229
+ self._dagster_cloud_api_config, # pyright: ignore[reportArgumentType]
191
230
  scope=scope,
192
231
  )
193
232
 
@@ -270,7 +309,7 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
270
309
  if self._http_client is None:
271
310
  self._http_client = create_agent_http_client(
272
311
  self.client_managed_retries_requests_session,
273
- self._dagster_cloud_api_config,
312
+ self._dagster_cloud_api_config, # pyright: ignore[reportArgumentType]
274
313
  scope=DagsterCloudInstanceScope.DEPLOYMENT,
275
314
  )
276
315
 
@@ -281,18 +320,23 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
281
320
  if "url" in self._dagster_cloud_api_config:
282
321
  return self._dagster_cloud_api_config["url"]
283
322
 
284
- organization = get_organization_name_from_agent_token(self.dagster_cloud_agent_token)
323
+ organization, region = decode_agent_token(self.dagster_cloud_agent_token)
285
324
  if not organization:
286
325
  raise DagsterInvariantViolationError(
287
326
  "Could not derive Dagster Cloud URL from agent token. Create a new agent token or"
288
327
  " set the `url` field under `dagster_cloud_api` in your `dagster.yaml`."
289
328
  )
290
329
 
291
- return f"https://{organization}.agent.dagster.cloud"
330
+ return (
331
+ f"https://{organization}.agent.{region}.dagster.cloud"
332
+ if region
333
+ else f"https://{organization}.agent.dagster.cloud"
334
+ )
292
335
 
293
336
  @property
294
337
  def organization_name(self) -> Optional[str]:
295
- return get_organization_name_from_agent_token(self.dagster_cloud_agent_token)
338
+ organization, _ = decode_agent_token(self.dagster_cloud_agent_token)
339
+ return organization
296
340
 
297
341
  @property
298
342
  def deployment_name(self) -> Optional[str]:
@@ -306,7 +350,7 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
306
350
  return deployment_names[0]
307
351
 
308
352
  @property
309
- def deployment_names(self) -> List[str]:
353
+ def deployment_names(self) -> list[str]:
310
354
  if self._dagster_cloud_api_config.get("deployment"):
311
355
  return [self._dagster_cloud_api_config["deployment"]]
312
356
 
@@ -314,11 +358,11 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
314
358
 
315
359
  @property
316
360
  def include_all_serverless_deployments(self) -> bool:
317
- return self._dagster_cloud_api_config.get("all_serverless_deployments")
361
+ return self._dagster_cloud_api_config.get("all_serverless_deployments") or False
318
362
 
319
363
  @property
320
364
  def dagit_url(self):
321
- organization = get_organization_name_from_agent_token(self.dagster_cloud_agent_token)
365
+ organization, region = decode_agent_token(self.dagster_cloud_agent_token)
322
366
  if not organization:
323
367
  raise Exception(
324
368
  "Could not derive Dagster Cloud URL from agent token to generate a Dagit URL."
@@ -326,7 +370,12 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
326
370
  )
327
371
 
328
372
  deployment = self._dagster_cloud_api_config.get("deployment")
329
- return f"https://{organization}.dagster.cloud/" + (f"{deployment}/" if deployment else "")
373
+ base_url = (
374
+ f"https://{organization}.{region}.dagster.cloud/"
375
+ if region
376
+ else f"https://{organization}.dagster.cloud/"
377
+ )
378
+ return base_url + (f"{deployment}/" if deployment else "")
330
379
 
331
380
  @property
332
381
  def dagster_cloud_graphql_url(self):
@@ -360,12 +409,29 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
360
409
  def dagster_cloud_upload_api_response_url(self):
361
410
  return f"{self.dagster_cloud_url}/upload_api_response"
362
411
 
412
+ @property
413
+ def dagster_cloud_check_snapshot_url(self):
414
+ return f"{self.dagster_cloud_url}/check_snapshot"
415
+
416
+ @property
417
+ def dagster_cloud_confirm_upload_url(self):
418
+ return f"{self.dagster_cloud_url}/confirm_upload"
419
+
420
+ @property
421
+ def dagster_cloud_code_location_update_result_url(self):
422
+ return f"{self.dagster_cloud_url}/code_location_update_result"
423
+
363
424
  def dagster_cloud_api_headers(self, scope: DagsterCloudInstanceScope):
364
- return get_agent_headers(self._dagster_cloud_api_config, scope=scope)
425
+ return get_agent_headers(self._dagster_cloud_api_config, scope=scope) # pyright: ignore[reportArgumentType]
365
426
 
366
427
  @property
367
- def dagster_cloud_agent_token(self):
368
- return self._dagster_cloud_api_config.get("agent_token")
428
+ def dagster_cloud_agent_token(self) -> str:
429
+ check.invariant(
430
+ self._dagster_cloud_api_config.get("agent_token") is not None,
431
+ "No agent token found in dagster_cloud_api configuration. An agent token is required"
432
+ " for Dagster Cloud authentication.",
433
+ )
434
+ return self._dagster_cloud_api_config["agent_token"]
369
435
 
370
436
  @property
371
437
  def dagster_cloud_api_retries(self) -> int:
@@ -376,10 +442,10 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
376
442
  return self._dagster_cloud_api_config["timeout"]
377
443
 
378
444
  @property
379
- def dagster_cloud_api_proxies(self) -> Optional[Dict[str, str]]:
445
+ def dagster_cloud_api_proxies(self) -> Optional[dict[str, str]]:
380
446
  # Requests library modifies the proxies key so create a copy
381
447
  return (
382
- self._dagster_cloud_api_config.get("proxies").copy()
448
+ self._dagster_cloud_api_config.get("proxies").copy() # pyright: ignore[reportOptionalMemberAccess]
383
449
  if self._dagster_cloud_api_config.get("proxies")
384
450
  else {}
385
451
  )
@@ -404,7 +470,7 @@ class DagsterCloudAgentInstance(DagsterCloudInstance):
404
470
  return f"Agent {self.instance_uuid[:8]}"
405
471
 
406
472
  @property
407
- def dagster_cloud_api_env_vars(self) -> List[str]:
473
+ def dagster_cloud_api_env_vars(self) -> list[str]:
408
474
  return get_env_names_from_config(
409
475
  dagster_cloud_api_config(), self._unprocessed_dagster_cloud_api_config
410
476
  )
@@ -470,6 +536,9 @@ instance_class:
470
536
  cls._isolated_agents_config_schema(), is_required=False
471
537
  ), # deprecated in favor of isolated_agents
472
538
  "agent_queues": Field(cls._agent_queues_config_schema(), is_required=False),
539
+ "opentelemetry": Field(
540
+ opentelemetry_config_schema(), is_required=False, default_value={"enabled": False}
541
+ ),
473
542
  }
474
543
 
475
544
  @classmethod
@@ -496,22 +565,22 @@ instance_class:
496
565
 
497
566
  empty_yaml = yaml.dump({})
498
567
 
499
- defaults["run_storage"] = ConfigurableClassData(
568
+ defaults["run_storage"] = ConfigurableClassData( # pyright: ignore[reportIndexIssue]
500
569
  "dagster_cloud.storage.runs",
501
570
  "GraphQLRunStorage",
502
571
  empty_yaml,
503
572
  )
504
- defaults["event_log_storage"] = ConfigurableClassData(
573
+ defaults["event_log_storage"] = ConfigurableClassData( # pyright: ignore[reportIndexIssue]
505
574
  "dagster_cloud.storage.event_logs",
506
575
  "GraphQLEventLogStorage",
507
576
  empty_yaml,
508
577
  )
509
- defaults["schedule_storage"] = ConfigurableClassData(
578
+ defaults["schedule_storage"] = ConfigurableClassData( # pyright: ignore[reportIndexIssue]
510
579
  "dagster_cloud.storage.schedules",
511
580
  "GraphQLScheduleStorage",
512
581
  empty_yaml,
513
582
  )
514
- defaults["storage"] = ConfigurableClassData(
583
+ defaults["storage"] = ConfigurableClassData( # pyright: ignore[reportIndexIssue]
515
584
  module_name="dagster.core.storage.legacy_storage",
516
585
  class_name="CompositeStorage",
517
586
  config_yaml=yaml.dump(
@@ -536,18 +605,25 @@ instance_class:
536
605
  ),
537
606
  )
538
607
 
539
- defaults["compute_logs"] = ConfigurableClassData(
608
+ defaults["compute_logs"] = ConfigurableClassData( # pyright: ignore[reportIndexIssue]
540
609
  "dagster_cloud.storage.compute_logs", "CloudComputeLogManager", empty_yaml
541
610
  )
542
611
 
543
- defaults["secrets"] = ConfigurableClassData(
612
+ defaults["secrets"] = ConfigurableClassData( # pyright: ignore[reportIndexIssue]
544
613
  "dagster_cloud.secrets", "DagsterCloudSecretsLoader", empty_yaml
545
614
  )
546
615
 
616
+ defaults["defs_state_storage"] = ConfigurableClassData( # pyright: ignore[reportIndexIssue]
617
+ "dagster_cloud.storage.defs_state", "GraphQLDefsStateStorage", empty_yaml
618
+ )
619
+
547
620
  return defaults
548
621
 
549
622
  def dispose(self) -> None:
550
623
  super().dispose()
624
+ if self._opentelemetry_controller:
625
+ self._opentelemetry_controller.dispose()
626
+ self._opentelemetry_controller = None
551
627
  self._exit_stack.close()
552
628
 
553
629
  @property
@@ -568,6 +644,17 @@ instance_class:
568
644
  # potentially overridden interval in the serverless user code launcher
569
645
  return 30
570
646
 
647
+ @property
648
+ def opentelemetry(self) -> OpenTelemetryController:
649
+ if not self._opentelemetry_controller:
650
+ self._opentelemetry_controller = OpenTelemetryController(
651
+ instance_id=self.instance_uuid,
652
+ version=__version__,
653
+ config=self._opentelemetry_config,
654
+ )
655
+
656
+ return self._opentelemetry_controller
657
+
571
658
 
572
659
  @lru_cache(maxsize=100) # Scales on order of active branch deployments
573
660
  def _cached_inject_deployment(
@@ -0,0 +1,32 @@
1
+ from collections.abc import Generator
2
+ from contextlib import AbstractContextManager, contextmanager
3
+ from typing import Optional, Protocol
4
+
5
+
6
+ class Instrumentation(Protocol):
7
+ def tags(self, tags: list[str]) -> "Instrumentation": ...
8
+
9
+ def histogram(self, name: str, value: float) -> None: ...
10
+
11
+ def increment(self, name: str) -> None: ...
12
+
13
+ def instrument_context(
14
+ self, name: str, buckets_ms: Optional[list[int]]
15
+ ) -> AbstractContextManager[None]: ...
16
+
17
+
18
+ class NoOpInstrumentation(Instrumentation):
19
+ def tags(self, tags: list[str]) -> Instrumentation:
20
+ return self
21
+
22
+ def histogram(self, name: str, value: float) -> None:
23
+ pass
24
+
25
+ def increment(self, name: str) -> None:
26
+ pass
27
+
28
+ @contextmanager
29
+ def instrument_context(
30
+ self, name: str, buckets_ms: Optional[list[int]]
31
+ ) -> Generator[None, None, None]:
32
+ yield
@@ -1,9 +1,10 @@
1
1
  import os
2
+ from collections.abc import Sequence
2
3
  from pathlib import Path
3
- from typing import TYPE_CHECKING, Optional, Sequence, Union
4
+ from typing import TYPE_CHECKING, Optional, Union
4
5
 
5
6
  from dagster import DagsterInvariantViolationError
6
- from dagster._annotations import experimental
7
+ from dagster._annotations import beta
7
8
  from dagster._core.definitions.metadata import (
8
9
  AnchorBasedFilePathMapping,
9
10
  link_code_references_to_git,
@@ -12,8 +13,10 @@ from dagster._core.definitions.metadata.source_code import FilePathMapping
12
13
  from dagster._core.types.loadable_target_origin import LoadableTargetOrigin
13
14
 
14
15
  if TYPE_CHECKING:
15
- from dagster import AssetsDefinition, SourceAsset
16
- from dagster._core.definitions.cacheable_assets import CacheableAssetsDefinition
16
+ from dagster import AssetsDefinition, AssetSpec, SourceAsset
17
+ from dagster._core.definitions.assets.definition.cacheable_assets_definition import (
18
+ CacheableAssetsDefinition,
19
+ )
17
20
 
18
21
  import sys
19
22
 
@@ -33,7 +36,7 @@ def _locate_git_root() -> Optional[Path]:
33
36
  elif code_origin.python_file:
34
37
  code_origin_filepath = code_origin.python_file
35
38
 
36
- if not code_origin_filepath:
39
+ if not code_origin_filepath: # pyright: ignore[reportPossiblyUnboundVariable]
37
40
  return None
38
41
  current_dir = Path(code_origin_filepath)
39
42
  for parent in current_dir.parents:
@@ -42,13 +45,15 @@ def _locate_git_root() -> Optional[Path]:
42
45
  return None
43
46
 
44
47
 
45
- @experimental
48
+ @beta
46
49
  def link_code_references_to_git_if_cloud(
47
- assets_defs: Sequence[Union["AssetsDefinition", "SourceAsset", "CacheableAssetsDefinition"]],
50
+ assets_defs: Sequence[
51
+ Union["AssetsDefinition", "SourceAsset", "CacheableAssetsDefinition", "AssetSpec"]
52
+ ],
48
53
  git_url: Optional[str] = None,
49
54
  git_branch: Optional[str] = None,
50
55
  file_path_mapping: Optional[FilePathMapping] = None,
51
- ) -> Sequence[Union["AssetsDefinition", "SourceAsset", "CacheableAssetsDefinition"]]:
56
+ ) -> Sequence[Union["AssetsDefinition", "SourceAsset", "CacheableAssetsDefinition", "AssetSpec"]]:
52
57
  """Wrapper function which converts local file path code references to hosted git URLs
53
58
  if running in a Dagster Plus cloud environment. This is determined by the presence of
54
59
  the `DAGSTER_CLOUD_DEPLOYMENT_NAME` environment variable. When running in any other context,
File without changes
@@ -0,0 +1,59 @@
1
+ import base64
2
+ import json
3
+ import time
4
+ from contextlib import contextmanager
5
+ from typing import Optional, Union
6
+
7
+ # A minimal copy of the standard OTEL API
8
+
9
+
10
+ class Span:
11
+ def __init__(self, name: str):
12
+ self._name = name
13
+ self._attrs = {}
14
+ self._start_time = time.time()
15
+ self._end_time = None
16
+
17
+ def set_attribute(self, key: str, value: Union[int, str]):
18
+ self._attrs[key] = value
19
+
20
+ def end(self):
21
+ self._end_time = time.time()
22
+
23
+ def get_serialized(self) -> str:
24
+ # base64 to make it safe for a http header (no newlines, few special chars)
25
+ return base64.b64encode(
26
+ json.dumps(
27
+ {
28
+ "_name": self._name,
29
+ "_duration": self._end_time - self._start_time
30
+ if self._end_time is not None
31
+ else None,
32
+ **self._attrs,
33
+ },
34
+ separators=(",", ":"),
35
+ indent=None,
36
+ ).encode("utf-8")
37
+ ).decode()
38
+
39
+
40
+ class Tracer:
41
+ def __init__(self):
42
+ self._spans: list[Span] = []
43
+
44
+ @contextmanager
45
+ def start_span(self, name: str):
46
+ span = Span(name)
47
+ try:
48
+ yield span
49
+ finally:
50
+ span.end()
51
+ self._spans.append(span)
52
+
53
+ def pop_serialized_span(self) -> Optional[str]:
54
+ try:
55
+ span = self._spans.pop(0)
56
+ except IndexError:
57
+ return None
58
+ else:
59
+ return span.get_serialized()
File without changes
@@ -0,0 +1,73 @@
1
+ from dagster import BoolSource, Field, Shape, StringSource
2
+
3
+ from dagster_cloud.opentelemetry.config.exporter import exporter_config_schema
4
+ from dagster_cloud.opentelemetry.config.log_record_processor import log_record_processor_schema
5
+ from dagster_cloud.opentelemetry.config.logging_handler import logging_handler_schema
6
+ from dagster_cloud.opentelemetry.config.meter_provider import meter_provider_config_schema
7
+ from dagster_cloud.opentelemetry.config.metric_reader import metric_reader_config_schema
8
+
9
+
10
+ def opentelemetry_config_schema():
11
+ return {
12
+ "enabled": Field(
13
+ BoolSource,
14
+ description="Enables OpenTelemetry instrumentation.",
15
+ is_required=False,
16
+ default_value=True,
17
+ ),
18
+ "service_name": Field(
19
+ StringSource,
20
+ description="Name of the service to which to send telemetry",
21
+ is_required=False,
22
+ default_value="dagster-cloud-agent",
23
+ ),
24
+ "logging": Field(
25
+ description="The logging configuration.",
26
+ is_required=False,
27
+ config=Shape(
28
+ fields={
29
+ "enabled": Field(
30
+ BoolSource,
31
+ description="Enables logging instrumentation.",
32
+ is_required=False,
33
+ default_value=False,
34
+ ),
35
+ "exporter": exporter_config_schema("logging"),
36
+ "processor": log_record_processor_schema(),
37
+ "handler": logging_handler_schema(),
38
+ },
39
+ ),
40
+ ),
41
+ "metrics": Field(
42
+ description="The metrics configuration.",
43
+ is_required=False,
44
+ config=Shape(
45
+ fields={
46
+ "enabled": Field(
47
+ BoolSource,
48
+ description="Enables metrics instrumentation.",
49
+ is_required=False,
50
+ default_value=False,
51
+ ),
52
+ "exporter": exporter_config_schema("metrics"),
53
+ "reader": metric_reader_config_schema(),
54
+ "provider": meter_provider_config_schema(),
55
+ },
56
+ ),
57
+ ),
58
+ # TODO: Add tracing configuration when it's supported.
59
+ # "tracing": Field(
60
+ # description="The tracing configuration.",
61
+ # config=Shape(
62
+ # fields={
63
+ # "enabled": Field(
64
+ # BoolSource,
65
+ # description="Enables tracing instrumentation.",
66
+ # is_required=False,
67
+ # default_value=False,
68
+ # ),
69
+ # "exporter": exporter_config_schema("tracing"),
70
+ # },
71
+ # ),
72
+ # ),
73
+ }
@@ -0,0 +1,81 @@
1
+ from dagster import BoolSource, Enum, Field, IntSource, Map, Permissive, Shape, StringSource
2
+
3
+ from dagster_cloud.opentelemetry.enum import (
4
+ AggregationTemporalityEnum,
5
+ CompressionEnum,
6
+ LoggingExporterEnum,
7
+ MetricsExporterEnum,
8
+ MetricsInstrumentTypesEnum,
9
+ TracingExporterEnum,
10
+ ViewAggregationEnum,
11
+ )
12
+
13
+ # Convert Python to Dagster enums
14
+ compression_enum = Enum.from_python_enum_direct_values(CompressionEnum)
15
+ logging_exporter_enum = Enum.from_python_enum_direct_values(LoggingExporterEnum)
16
+ metrics_exporter_enum = Enum.from_python_enum_direct_values(MetricsExporterEnum)
17
+ tracing_exporter_enum = Enum.from_python_enum_direct_values(TracingExporterEnum)
18
+
19
+ aggregation_temporality_enum = Enum.from_python_enum_direct_values(AggregationTemporalityEnum)
20
+ metrics_instrument_types_enum = Enum.from_python_enum_direct_values(MetricsInstrumentTypesEnum)
21
+ view_aggregation_enum = Enum.from_python_enum_direct_values(ViewAggregationEnum)
22
+
23
+
24
+ def exporter_config_schema(pillar: str) -> Shape:
25
+ """Different observability pillars have different supported exporters but share common parameters.
26
+ Valid pillars are: logging, metrics, tracing.
27
+ """
28
+ exporter_type_field: Field
29
+ exporter_extra_params: dict = {}
30
+ if pillar == "logging":
31
+ exporter_type_field = Field(
32
+ config=logging_exporter_enum,
33
+ is_required=False,
34
+ default_value=LoggingExporterEnum.ConsoleLogExporter.value,
35
+ )
36
+ elif pillar == "metrics":
37
+ exporter_type_field = Field(
38
+ config=metrics_exporter_enum,
39
+ is_required=False,
40
+ default_value=MetricsExporterEnum.ConsoleMetricExporter.value,
41
+ )
42
+ exporter_extra_params = {
43
+ "preferred_temporality": Field(
44
+ config=Map(str, aggregation_temporality_enum), is_required=False
45
+ ),
46
+ "preferred_aggregation": Field(
47
+ config=Map(str, view_aggregation_enum), is_required=False
48
+ ),
49
+ }
50
+ elif pillar == "tracing":
51
+ exporter_type_field = Field(
52
+ config=tracing_exporter_enum,
53
+ )
54
+ else:
55
+ raise ValueError(f"Invalid pillar: {pillar}")
56
+
57
+ return Shape(
58
+ description="Provide configuration for an OpenTelemetry exporter.",
59
+ fields={
60
+ "type": exporter_type_field,
61
+ "params": Permissive(
62
+ fields={
63
+ # These fields are common to most exporters. No defaults are provided, to allow either
64
+ # OpenTelemetry's default or environment variables to be used.
65
+ # Ref: https://opentelemetry-python.readthedocs.io/en/latest/sdk/environment_variables.html
66
+ "endpoint": Field(StringSource, is_required=False),
67
+ "compression": Field(config=compression_enum, is_required=False),
68
+ "headers": Field(
69
+ description="Custom headers to send", config=Permissive(), is_required=False
70
+ ),
71
+ "certificate_file": Field(StringSource, is_required=False),
72
+ "client_key_file": Field(StringSource, is_required=False),
73
+ "client_certificate_file": Field(StringSource, is_required=False),
74
+ "insecure": Field(BoolSource, is_required=False),
75
+ "timeout": Field(IntSource, is_required=False),
76
+ # These fields are specific an exporter.
77
+ **exporter_extra_params,
78
+ }
79
+ ),
80
+ },
81
+ )