service-forge 0.1.28__py3-none-any.whl → 0.1.39__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.

Potentially problematic release.


This version of service-forge might be problematic. Click here for more details.

Files changed (72) hide show
  1. service_forge/__init__.py +0 -0
  2. service_forge/api/deprecated_websocket_api.py +91 -33
  3. service_forge/api/deprecated_websocket_manager.py +70 -53
  4. service_forge/api/http_api.py +127 -53
  5. service_forge/api/kafka_api.py +113 -25
  6. service_forge/api/routers/meta_api/meta_api_router.py +57 -0
  7. service_forge/api/routers/service/service_router.py +42 -6
  8. service_forge/api/routers/trace/trace_router.py +326 -0
  9. service_forge/api/routers/websocket/websocket_router.py +56 -1
  10. service_forge/api/service_studio.py +9 -0
  11. service_forge/execution_context.py +106 -0
  12. service_forge/frontend/static/assets/CreateNewNodeDialog-DkrEMxSH.js +1 -0
  13. service_forge/frontend/static/assets/CreateNewNodeDialog-DwFcBiGp.css +1 -0
  14. service_forge/frontend/static/assets/EditorSidePanel-BNVms9Fq.css +1 -0
  15. service_forge/frontend/static/assets/EditorSidePanel-DZbB3ILL.js +1 -0
  16. service_forge/frontend/static/assets/FeedbackPanel-CC8HX7Yo.js +1 -0
  17. service_forge/frontend/static/assets/FeedbackPanel-ClgniIVk.css +1 -0
  18. service_forge/frontend/static/assets/FormattedCodeViewer.vue_vue_type_script_setup_true_lang-BNuI1NCs.js +1 -0
  19. service_forge/frontend/static/assets/NodeDetailWrapper-BqFFM7-r.js +1 -0
  20. service_forge/frontend/static/assets/NodeDetailWrapper-pZBxv3J0.css +1 -0
  21. service_forge/frontend/static/assets/TestRunningDialog-D0GrCoYs.js +1 -0
  22. service_forge/frontend/static/assets/TestRunningDialog-dhXOsPgH.css +1 -0
  23. service_forge/frontend/static/assets/TracePanelWrapper-B9zvDSc_.js +1 -0
  24. service_forge/frontend/static/assets/TracePanelWrapper-BiednCrq.css +1 -0
  25. service_forge/frontend/static/assets/WorkflowEditor-CcaGGbko.js +3 -0
  26. service_forge/frontend/static/assets/WorkflowEditor-CmasOOYK.css +1 -0
  27. service_forge/frontend/static/assets/WorkflowList-Copuwi-a.css +1 -0
  28. service_forge/frontend/static/assets/WorkflowList-LrRJ7B7h.js +1 -0
  29. service_forge/frontend/static/assets/WorkflowStudio-CthjgII2.css +1 -0
  30. service_forge/frontend/static/assets/WorkflowStudio-FCyhGD4y.js +2 -0
  31. service_forge/frontend/static/assets/api-BDer3rj7.css +1 -0
  32. service_forge/frontend/static/assets/api-DyiqpKJK.js +1 -0
  33. service_forge/frontend/static/assets/code-editor-DBSql_sc.js +12 -0
  34. service_forge/frontend/static/assets/el-collapse-item-D4LG0FJ0.css +1 -0
  35. service_forge/frontend/static/assets/el-empty-D4ZqTl4F.css +1 -0
  36. service_forge/frontend/static/assets/el-form-item-BWkJzdQ_.css +1 -0
  37. service_forge/frontend/static/assets/el-input-D6B3r8CH.css +1 -0
  38. service_forge/frontend/static/assets/el-select-B0XIb2QK.css +1 -0
  39. service_forge/frontend/static/assets/el-tag-DljBBxJR.css +1 -0
  40. service_forge/frontend/static/assets/element-ui-D3x2y3TA.js +12 -0
  41. service_forge/frontend/static/assets/elkjs-Dm5QV7uy.js +24 -0
  42. service_forge/frontend/static/assets/highlightjs-D4ATuRwX.js +3 -0
  43. service_forge/frontend/static/assets/index-BMvodlwc.js +2 -0
  44. service_forge/frontend/static/assets/index-CjSe8i2q.css +1 -0
  45. service_forge/frontend/static/assets/js-yaml-yTPt38rv.js +32 -0
  46. service_forge/frontend/static/assets/time-DKCKV6Ug.js +1 -0
  47. service_forge/frontend/static/assets/ui-components-DQ7-U3pr.js +1 -0
  48. service_forge/frontend/static/assets/vue-core-DL-LgTX0.js +1 -0
  49. service_forge/frontend/static/assets/vue-flow-Dn7R8GPr.js +39 -0
  50. service_forge/frontend/static/index.html +16 -0
  51. service_forge/frontend/static/vite.svg +1 -0
  52. service_forge/model/meta_api/__init__.py +0 -0
  53. service_forge/model/meta_api/schema.py +29 -0
  54. service_forge/model/trace.py +82 -0
  55. service_forge/service.py +32 -11
  56. service_forge/service_config.py +14 -0
  57. service_forge/sft/config/injector.py +32 -2
  58. service_forge/sft/config/injector_default_files.py +12 -0
  59. service_forge/sft/config/sf_metadata.py +5 -0
  60. service_forge/sft/config/sft_config.py +18 -0
  61. service_forge/telemetry.py +66 -0
  62. service_forge/workflow/node.py +266 -27
  63. service_forge/workflow/triggers/fast_api_trigger.py +61 -28
  64. service_forge/workflow/triggers/websocket_api_trigger.py +31 -10
  65. service_forge/workflow/workflow.py +87 -10
  66. service_forge/workflow/workflow_callback.py +24 -2
  67. service_forge/workflow/workflow_factory.py +13 -0
  68. {service_forge-0.1.28.dist-info → service_forge-0.1.39.dist-info}/METADATA +4 -1
  69. service_forge-0.1.39.dist-info/RECORD +134 -0
  70. service_forge-0.1.28.dist-info/RECORD +0 -85
  71. {service_forge-0.1.28.dist-info → service_forge-0.1.39.dist-info}/WHEEL +0 -0
  72. {service_forge-0.1.28.dist-info → service_forge-0.1.39.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="./vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>vite-project</title>
8
+ <script type="module" crossorigin src="./assets/index-BMvodlwc.js"></script>
9
+ <link rel="modulepreload" crossorigin href="./assets/vue-core-DL-LgTX0.js">
10
+ <link rel="modulepreload" crossorigin href="./assets/ui-components-DQ7-U3pr.js">
11
+ <link rel="stylesheet" crossorigin href="./assets/index-CjSe8i2q.css">
12
+ </head>
13
+ <body>
14
+ <div id="app"></div>
15
+ </body>
16
+ </html>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
File without changes
@@ -0,0 +1,29 @@
1
+ from typing import Literal, Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+ class NodeInputPortSchema(BaseModel):
6
+ """
7
+ 节点的输入端口定义数据
8
+ """
9
+ name: str
10
+ type: str
11
+ default_value: Optional[str | float | bool | int] = None
12
+
13
+
14
+ class NodeOutputPortSchema(BaseModel):
15
+ """
16
+ 节点的输出端口定义数据
17
+ """
18
+ name: str
19
+ type: str
20
+
21
+
22
+ class NodeTypeSchema(BaseModel):
23
+ """
24
+ 节点的定义数据
25
+ """
26
+ name: str
27
+ is_trigger: bool
28
+ inputs: dict[str, NodeInputPortSchema]
29
+ outputs: dict[str, NodeOutputPortSchema]
@@ -0,0 +1,82 @@
1
+ from pydantic import BaseModel
2
+ from typing import Optional, List, Dict, Any
3
+ from enum import IntEnum
4
+
5
+ # Type aliases
6
+ TraceId = str
7
+ SpanId = str
8
+ ParentSpanId = str
9
+
10
+ # Enums
11
+ class SpanKind(IntEnum):
12
+ INTERNAL = 0
13
+ SERVER = 1
14
+ CLIENT = 2
15
+ PRODUCER = 3
16
+ CONSUMER = 4
17
+ UNKNOWN = 5
18
+
19
+ class StatusCode(IntEnum):
20
+ UNSET = 0
21
+ OK = 1
22
+ ERROR = 2
23
+
24
+ # Models
25
+ class Span(BaseModel):
26
+ span_id: SpanId
27
+ parent_span_id: ParentSpanId
28
+ name: str
29
+ kind: SpanKind
30
+ timestamp: str
31
+ duration_nano: int
32
+ status_code: StatusCode
33
+ service_name: str
34
+ workflow_name: Optional[str] = None
35
+ workflow_task_id: Optional[str] = None
36
+ node_name: Optional[str] = None
37
+ attributes: Optional[Dict[str, Any]] = None
38
+
39
+ class TraceListItem(BaseModel):
40
+ trace_id: TraceId
41
+ service_name: str
42
+ workflow_name: Optional[str] = None
43
+ workflow_task_id: Optional[str] = None
44
+ timestamp: str
45
+ duration_nano: int
46
+ span_count: int
47
+ has_error: bool
48
+ status_code: StatusCode
49
+
50
+ class TraceDetail(BaseModel):
51
+ trace_id: TraceId
52
+ service_name: str
53
+ workflow_name: Optional[str] = None
54
+ workflow_task_id: Optional[str] = None
55
+ spans: List[Span]
56
+ start_time: Optional[str] = None
57
+ end_time: Optional[str] = None
58
+ span_count: int
59
+ has_error: bool
60
+
61
+ class GetTraceListParams(BaseModel):
62
+ service_name: Optional[str] = None
63
+ workflow_name: Optional[str] = None
64
+ workflow_task_id: Optional[str] = None
65
+ start_time: Optional[str] = None
66
+ end_time: Optional[str] = None
67
+ limit: Optional[int] = None
68
+ offset: Optional[int] = None
69
+ has_error: Optional[bool] = None
70
+
71
+ class GetTraceListResponse(BaseModel):
72
+ traces: List[TraceListItem]
73
+ total: int
74
+ limit: int
75
+ offset: int
76
+
77
+ class GetTraceDetailParams(BaseModel):
78
+ trace_id: TraceId
79
+ service_name: Optional[str] = None
80
+
81
+ class GetTraceDetailResponse(BaseModel):
82
+ trace: TraceDetail
service_forge/service.py CHANGED
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import uuid
4
5
  import asyncio
5
6
  import threading
6
- import uuid
7
+ from loguru import logger
8
+
9
+ from service_forge.telemetry import setup_tracing
7
10
  from importlib.metadata import version
8
11
  from loguru import logger
9
12
  from typing import Callable, AsyncIterator, Awaitable, Any, TYPE_CHECKING
@@ -58,6 +61,7 @@ class Service:
58
61
  return self.metadata.description
59
62
 
60
63
  async def start(self):
64
+ setup_tracing(service_name=self.name, config=self.config.trace)
61
65
  set_service(self)
62
66
 
63
67
  if self.config.enable_http:
@@ -79,7 +83,7 @@ class Service:
79
83
  workflow_group = create_workflow_group(
80
84
  config_path=self.parse_workflow_path(workflow_config_path),
81
85
  service_env=self.service_env,
82
- _handle_stream_output=self._handle_stream_output,
86
+ _handle_stream_output=self._handle_stream_output,
83
87
  _handle_query_user=self._handle_query_user,
84
88
  database_manager=self.database_manager,
85
89
  )
@@ -140,23 +144,23 @@ class Service:
140
144
 
141
145
  def get_workflow_group_by_id(self, workflow_id: str, allow_none: bool = True) -> WorkflowGroup | None:
142
146
  for workflow_group in self.workflow_groups:
143
- if workflow_group.get_workflow_by_id(workflow_id) is not None:
147
+ if workflow_group.get_workflow_by_id(uuid.UUID(workflow_id)) is not None:
144
148
  return workflow_group
145
149
  if not allow_none:
146
150
  raise ValueError(f"Workflow group with id {workflow_id} not found in service {self.name}")
147
151
  return None
148
152
 
149
- def trigger_workflow(self, workflow_group: WorkflowGroup, trigger_name: str, **kwargs) -> uuid.UUID:
153
+ def trigger_workflow(self, workflow_group: WorkflowGroup, trigger_name: str, assigned_task_id: uuid.UUID | None, **kwargs) -> uuid.UUID:
150
154
  workflow = workflow_group.get_main_workflow(allow_none=False)
151
- return workflow.trigger(trigger_name, **kwargs)
155
+ return workflow.trigger(trigger_name, assigned_task_id, **kwargs)
152
156
 
153
- def trigger_workflow_by_name(self, workflow_name: str, workflow_version: str, trigger_name: str, **kwargs) -> uuid.UUID:
157
+ def trigger_workflow_by_name(self, workflow_name: str, workflow_version: str, trigger_name: str, assigned_task_id: uuid.UUID | None, **kwargs) -> uuid.UUID:
154
158
  workflow_group = self.get_workflow_group_by_name(workflow_name, workflow_version, allow_none=False)
155
- return self.trigger_workflow(workflow_group, trigger_name, **kwargs)
159
+ return self.trigger_workflow(workflow_group, trigger_name, assigned_task_id, **kwargs)
156
160
 
157
- def trigger_workflow_by_id(self, workflow_id: str, trigger_name: str, **kwargs) -> uuid.UUID:
161
+ def trigger_workflow_by_id(self, workflow_id: str, trigger_name: str, assigned_task_id: uuid.UUID | None, **kwargs) -> uuid.UUID:
158
162
  workflow_group = self.get_workflow_group_by_id(workflow_id, allow_none=False)
159
- return self.trigger_workflow(workflow_group, trigger_name, **kwargs)
163
+ return self.trigger_workflow(workflow_group, trigger_name, assigned_task_id, **kwargs)
160
164
 
161
165
  def start_workflow(self, workflow_group: WorkflowGroup) -> bool:
162
166
  workflow = workflow_group.get_main_workflow(allow_none=False)
@@ -208,7 +212,7 @@ class Service:
208
212
  workflow_group = self.get_workflow_group_by_id(workflow_id, allow_none=False)
209
213
  return await self.stop_workflow(workflow_group)
210
214
 
211
- async def load_workflow_from_config(self, config_path: str = None, config: dict = None) -> uuid.UUID:
215
+ async def load_workflow_from_config(self, config_path: str = None, config: dict = None, debug_version: bool = False) -> uuid.UUID:
212
216
  workflow_group = create_workflow_group(
213
217
  config_path=config_path,
214
218
  config=config,
@@ -216,6 +220,7 @@ class Service:
216
220
  _handle_stream_output=self._handle_stream_output,
217
221
  _handle_query_user=self._handle_query_user,
218
222
  database_manager=self.database_manager,
223
+ debug_version=debug_version,
219
224
  )
220
225
 
221
226
  for workflow in workflow_group.workflows:
@@ -232,10 +237,12 @@ class Service:
232
237
  self.start_workflow(workflow_group)
233
238
  return main_workflow.id
234
239
 
235
- def get_service_status(self) -> dict[str, Any]:
240
+ def get_service_status(self, exclude_debug: bool = False) -> dict[str, Any]:
236
241
  workflow_statuses = []
237
242
  for workflow_group in self.workflow_groups:
238
243
  for workflow in workflow_group.workflows:
244
+ if exclude_debug and workflow.debug_version:
245
+ continue
239
246
  workflow_id = workflow.id
240
247
  workflow_version = workflow.version
241
248
  workflow_config = workflow.config
@@ -258,6 +265,20 @@ class Service:
258
265
  "description": self.description,
259
266
  "workflows": workflow_statuses,
260
267
  }
268
+
269
+ def get_workflow_status(self, workflow_id: str) -> dict[str, Any]:
270
+ workflow_group = self.get_workflow_group_by_id(workflow_id, allow_none=False)
271
+ main_workflow = workflow_group.get_workflow_by_id(uuid.UUID(workflow_id))
272
+ is_running = workflow_id in self.workflow_tasks and not self.workflow_tasks[workflow_id].done()
273
+ return {
274
+ "name": main_workflow.name,
275
+ "id": main_workflow.id,
276
+ "version": main_workflow.version,
277
+ "config": main_workflow.config,
278
+ "description": main_workflow.description,
279
+ "status": "running" if is_running else "stopped",
280
+ "debug": main_workflow.debug_version or False
281
+ }
261
282
 
262
283
  @staticmethod
263
284
  def from_config(metadata: SfMetadata, service_env: dict[str, Any] = None, config: ServiceConfig = None) -> Service:
@@ -7,6 +7,10 @@ class ServiceFeedbackConfig(BaseModel):
7
7
  api_url: str
8
8
  api_timeout: int = 5
9
9
 
10
+ class SignozConfig(BaseModel):
11
+ api_url: str
12
+ api_key: str
13
+
10
14
  class ServiceDatabaseConfig(BaseModel):
11
15
  name: str
12
16
  postgres_user: str | None = None
@@ -25,6 +29,14 @@ class ServiceDatabaseConfig(BaseModel):
25
29
  redis_port: int | None = None
26
30
  redis_password: str | None = None
27
31
 
32
+ class TraceConfig(BaseModel):
33
+ enable: bool = False
34
+ url: str | None = None
35
+ headers: str | None = None
36
+ arg: float | None = None
37
+ namespace: str | None = None
38
+ hostname: str | None = None
39
+
28
40
  class ServiceConfig(BaseModel):
29
41
  name: str
30
42
  workflows: list[str]
@@ -36,6 +48,8 @@ class ServiceConfig(BaseModel):
36
48
  kafka_port: int | None = None
37
49
  databases: list[ServiceDatabaseConfig] | None = None
38
50
  feedback: ServiceFeedbackConfig | None = None
51
+ signoz: SignozConfig | None = None
52
+ trace: TraceConfig | None = None
39
53
 
40
54
  @classmethod
41
55
  def from_yaml_file(cls, filepath: str) -> ServiceConfig:
@@ -2,9 +2,9 @@ import yaml
2
2
  from pathlib import Path
3
3
  from service_forge.sft.util.logger import log_info, log_error
4
4
  from service_forge.sft.config.injector_default_files import *
5
- from service_forge.sft.config.sf_metadata import load_metadata
5
+ from service_forge.sft.config.sf_metadata import load_metadata, save_metadata
6
6
  from service_forge.sft.config.sft_config import sft_config
7
- from service_forge.service_config import ServiceConfig, ServiceFeedbackConfig
7
+ from service_forge.service_config import ServiceConfig, ServiceFeedbackConfig, SignozConfig, TraceConfig
8
8
  from service_forge.sft.util.name_util import get_service_name
9
9
  from service_forge.sft.util.yaml_utils import load_sf_metadata_as_string
10
10
 
@@ -18,6 +18,7 @@ class Injector:
18
18
  self.pyproject_toml_path = project_dir / "pyproject.toml"
19
19
  self.start_sh_path = project_dir / "start.sh"
20
20
  self.metadata = load_metadata(self.metadata_path)
21
+ self.metadata.mode = "release"
21
22
  self.name = self.metadata.name
22
23
  self.version = self.metadata.version
23
24
  self.namespace = sft_config.k8s_namespace
@@ -94,6 +95,32 @@ class Injector:
94
95
  api_timeout=sft_config.inject_feedback_api_timeout,
95
96
  )
96
97
 
98
+ if config.signoz is not None:
99
+ config.signoz.api_url = sft_config.inject_signoz_api_url
100
+ config.signoz.api_key = sft_config.inject_signoz_api_key
101
+ else:
102
+ config.signoz = SignozConfig(
103
+ api_url=sft_config.inject_signoz_api_url,
104
+ api_key=sft_config.inject_signoz_api_key,
105
+ )
106
+
107
+ if config.trace is not None:
108
+ config.trace.enable = True
109
+ config.trace.url = sft_config.inject_trace_url
110
+ config.trace.headers = sft_config.inject_trace_headers
111
+ config.trace.arg = sft_config.inject_trace_arg
112
+ config.trace.namespace = sft_config.inject_trace_namespace
113
+ config.trace.hostname = sft_config.inject_trace_hostname
114
+ else:
115
+ config.trace = TraceConfig(
116
+ enable=True,
117
+ url=sft_config.inject_trace_url,
118
+ headers=sft_config.inject_trace_headers,
119
+ arg=sft_config.inject_trace_arg,
120
+ namespace=sft_config.inject_trace_namespace,
121
+ hostname=sft_config.inject_trace_hostname,
122
+ )
123
+
97
124
  with open(service_config_path, "w", encoding="utf-8") as f:
98
125
  yaml.dump(config.model_dump(), f, allow_unicode=True, indent=2)
99
126
 
@@ -133,6 +160,8 @@ class Injector:
133
160
  f.write(new_content)
134
161
 
135
162
  def inject(self) -> None:
163
+ if self.metadata.inject.pyproject_toml:
164
+ self.inject_pyproject_toml()
136
165
  if self.metadata.inject.deployment:
137
166
  self.inject_deployment()
138
167
  if self.metadata.inject.service_config:
@@ -144,3 +173,4 @@ class Injector:
144
173
  if self.metadata.inject.pyproject_toml:
145
174
  self.inject_pyproject_toml()
146
175
  self.clear_start_sh()
176
+ save_metadata(self.metadata, self.metadata_path)
@@ -65,6 +65,18 @@ spec:
65
65
  entryPoints:
66
66
  - web
67
67
  routes:
68
+ - match: PathPrefix(`/api/v1/{name}-{version}/sdk`)
69
+ kind: Rule
70
+ services:
71
+ - name: sf-{name}-{version}v
72
+ namespace: {namespace}
73
+ port: 80
74
+ middlewares:
75
+ - name: strip-prefix-sf-{name}-{version}v
76
+ namespace: {namespace}
77
+ - name: cors
78
+ namespace: {namespace}
79
+
68
80
  - match: PathPrefix(`/api/v1/{name}-{version}/openapi.json`)
69
81
  kind: Rule
70
82
  services:
@@ -18,6 +18,7 @@ class SfMetadata(BaseModel):
18
18
  env: list[dict]
19
19
  inject: SfMetadataInject = SfMetadataInject()
20
20
  enable_auth_middleware: bool = True
21
+ mode: str = "debug"
21
22
 
22
23
  @classmethod
23
24
  def from_yaml_file(cls, filepath: str) -> SfMetadata:
@@ -27,3 +28,7 @@ class SfMetadata(BaseModel):
27
28
 
28
29
  def load_metadata(path: str) -> SfMetadata:
29
30
  return SfMetadata.from_yaml_file(path)
31
+
32
+ def save_metadata(meta: SfMetadata, path: str) -> None:
33
+ with open(path, 'w', encoding='utf-8') as f:
34
+ yaml.safe_dump(meta.model_dump(), f, allow_unicode=True)
@@ -57,6 +57,15 @@ class SftConfig:
57
57
  inject_feedback_api_url: str = "http://vps.shiweinan.com:37919/api/v1/feedback",
58
58
  inject_feedback_api_timeout: int = 5,
59
59
 
60
+ inject_signoz_api_url: str = "http://signoz.vps.shiweinan.com:37919",
61
+ inject_signoz_api_key: str = "JlxvqRtNFu5yc4o1bRcJyzeolA96iWzAyQnBePRRJd0=",
62
+
63
+ inject_trace_url: str = "http://traces.vps.shiweinan.com:37919/v1/traces",
64
+ inject_trace_headers: str = "",
65
+ inject_trace_arg: float = 1.0,
66
+ inject_trace_namespace: str = "secondbrain",
67
+ inject_trace_hostname: str = "",
68
+
60
69
  deepseek_api_key: str = "82c9df22-f6ed-411e-90d7-c5255376b7ca",
61
70
  deepseek_base_url: str = "https://ark.cn-beijing.volces.com/api/v3",
62
71
  ):
@@ -88,6 +97,15 @@ class SftConfig:
88
97
  self.inject_feedback_api_url = inject_feedback_api_url
89
98
  self.inject_feedback_api_timeout = inject_feedback_api_timeout
90
99
 
100
+ self.inject_signoz_api_url = inject_signoz_api_url
101
+ self.inject_signoz_api_key = inject_signoz_api_key
102
+
103
+ self.inject_trace_url = inject_trace_url
104
+ self.inject_trace_headers = inject_trace_headers
105
+ self.inject_trace_arg = inject_trace_arg
106
+ self.inject_trace_namespace = inject_trace_namespace
107
+ self.inject_trace_hostname = inject_trace_hostname
108
+
91
109
  self.deepseek_api_key = deepseek_api_key
92
110
  self.deepseek_base_url = deepseek_base_url
93
111
 
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from loguru import logger
6
+ from opentelemetry import trace
7
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
8
+ from opentelemetry.sdk.resources import Resource
9
+ from opentelemetry.sdk.trace import TracerProvider
10
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
11
+ from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
12
+ from service_forge.service_config import TraceConfig
13
+
14
+ _initialized = False
15
+
16
+ def _parse_headers(raw: Optional[str]) -> dict[str, str]:
17
+ if not raw:
18
+ return {}
19
+ headers: dict[str, str] = {}
20
+ for part in raw.split(","):
21
+ if "=" in part:
22
+ key, value = part.split("=", 1)
23
+ headers[key.strip()] = value.strip()
24
+ return headers
25
+
26
+
27
+ def setup_tracing(service_name: Optional[str] = None, config: TraceConfig = None) -> None:
28
+ """Initialize a global tracer provider with OTLP exporter if not already configured."""
29
+ if config is None or not config.enable:
30
+ return
31
+
32
+ global _initialized
33
+ if _initialized:
34
+ return
35
+
36
+ service_name = service_name or "service_forge_service"
37
+ endpoint = config.url
38
+ headers = _parse_headers(config.headers)
39
+
40
+ sampler_arg = config.arg or 1.0
41
+ try:
42
+ ratio = float(sampler_arg)
43
+ except ValueError:
44
+ ratio = 1.0
45
+ sampler = ParentBased(TraceIdRatioBased(ratio))
46
+
47
+ resource = Resource.create(
48
+ {
49
+ "service.name": service_name,
50
+ "service.namespace": config.namespace or "secondbrain",
51
+ "service.instance.id": config.hostname or "",
52
+ }
53
+ )
54
+
55
+ try:
56
+ provider = TracerProvider(resource=resource, sampler=sampler)
57
+ exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers)
58
+ processor = BatchSpanProcessor(exporter)
59
+ provider.add_span_processor(processor)
60
+ trace.set_tracer_provider(provider)
61
+ _initialized = True
62
+ logger.info(
63
+ f"Tracing initialized: endpoint={endpoint}, service={service_name}, ratio={ratio}"
64
+ )
65
+ except Exception as exc:
66
+ logger.warning(f"Tracing initialization failed: {exc}")