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.
- service_forge/__init__.py +0 -0
- service_forge/api/deprecated_websocket_api.py +91 -33
- service_forge/api/deprecated_websocket_manager.py +70 -53
- service_forge/api/http_api.py +127 -53
- service_forge/api/kafka_api.py +113 -25
- service_forge/api/routers/meta_api/meta_api_router.py +57 -0
- service_forge/api/routers/service/service_router.py +42 -6
- service_forge/api/routers/trace/trace_router.py +326 -0
- service_forge/api/routers/websocket/websocket_router.py +56 -1
- service_forge/api/service_studio.py +9 -0
- service_forge/execution_context.py +106 -0
- service_forge/frontend/static/assets/CreateNewNodeDialog-DkrEMxSH.js +1 -0
- service_forge/frontend/static/assets/CreateNewNodeDialog-DwFcBiGp.css +1 -0
- service_forge/frontend/static/assets/EditorSidePanel-BNVms9Fq.css +1 -0
- service_forge/frontend/static/assets/EditorSidePanel-DZbB3ILL.js +1 -0
- service_forge/frontend/static/assets/FeedbackPanel-CC8HX7Yo.js +1 -0
- service_forge/frontend/static/assets/FeedbackPanel-ClgniIVk.css +1 -0
- service_forge/frontend/static/assets/FormattedCodeViewer.vue_vue_type_script_setup_true_lang-BNuI1NCs.js +1 -0
- service_forge/frontend/static/assets/NodeDetailWrapper-BqFFM7-r.js +1 -0
- service_forge/frontend/static/assets/NodeDetailWrapper-pZBxv3J0.css +1 -0
- service_forge/frontend/static/assets/TestRunningDialog-D0GrCoYs.js +1 -0
- service_forge/frontend/static/assets/TestRunningDialog-dhXOsPgH.css +1 -0
- service_forge/frontend/static/assets/TracePanelWrapper-B9zvDSc_.js +1 -0
- service_forge/frontend/static/assets/TracePanelWrapper-BiednCrq.css +1 -0
- service_forge/frontend/static/assets/WorkflowEditor-CcaGGbko.js +3 -0
- service_forge/frontend/static/assets/WorkflowEditor-CmasOOYK.css +1 -0
- service_forge/frontend/static/assets/WorkflowList-Copuwi-a.css +1 -0
- service_forge/frontend/static/assets/WorkflowList-LrRJ7B7h.js +1 -0
- service_forge/frontend/static/assets/WorkflowStudio-CthjgII2.css +1 -0
- service_forge/frontend/static/assets/WorkflowStudio-FCyhGD4y.js +2 -0
- service_forge/frontend/static/assets/api-BDer3rj7.css +1 -0
- service_forge/frontend/static/assets/api-DyiqpKJK.js +1 -0
- service_forge/frontend/static/assets/code-editor-DBSql_sc.js +12 -0
- service_forge/frontend/static/assets/el-collapse-item-D4LG0FJ0.css +1 -0
- service_forge/frontend/static/assets/el-empty-D4ZqTl4F.css +1 -0
- service_forge/frontend/static/assets/el-form-item-BWkJzdQ_.css +1 -0
- service_forge/frontend/static/assets/el-input-D6B3r8CH.css +1 -0
- service_forge/frontend/static/assets/el-select-B0XIb2QK.css +1 -0
- service_forge/frontend/static/assets/el-tag-DljBBxJR.css +1 -0
- service_forge/frontend/static/assets/element-ui-D3x2y3TA.js +12 -0
- service_forge/frontend/static/assets/elkjs-Dm5QV7uy.js +24 -0
- service_forge/frontend/static/assets/highlightjs-D4ATuRwX.js +3 -0
- service_forge/frontend/static/assets/index-BMvodlwc.js +2 -0
- service_forge/frontend/static/assets/index-CjSe8i2q.css +1 -0
- service_forge/frontend/static/assets/js-yaml-yTPt38rv.js +32 -0
- service_forge/frontend/static/assets/time-DKCKV6Ug.js +1 -0
- service_forge/frontend/static/assets/ui-components-DQ7-U3pr.js +1 -0
- service_forge/frontend/static/assets/vue-core-DL-LgTX0.js +1 -0
- service_forge/frontend/static/assets/vue-flow-Dn7R8GPr.js +39 -0
- service_forge/frontend/static/index.html +16 -0
- service_forge/frontend/static/vite.svg +1 -0
- service_forge/model/meta_api/__init__.py +0 -0
- service_forge/model/meta_api/schema.py +29 -0
- service_forge/model/trace.py +82 -0
- service_forge/service.py +32 -11
- service_forge/service_config.py +14 -0
- service_forge/sft/config/injector.py +32 -2
- service_forge/sft/config/injector_default_files.py +12 -0
- service_forge/sft/config/sf_metadata.py +5 -0
- service_forge/sft/config/sft_config.py +18 -0
- service_forge/telemetry.py +66 -0
- service_forge/workflow/node.py +266 -27
- service_forge/workflow/triggers/fast_api_trigger.py +61 -28
- service_forge/workflow/triggers/websocket_api_trigger.py +31 -10
- service_forge/workflow/workflow.py +87 -10
- service_forge/workflow/workflow_callback.py +24 -2
- service_forge/workflow/workflow_factory.py +13 -0
- {service_forge-0.1.28.dist-info → service_forge-0.1.39.dist-info}/METADATA +4 -1
- service_forge-0.1.39.dist-info/RECORD +134 -0
- service_forge-0.1.28.dist-info/RECORD +0 -85
- {service_forge-0.1.28.dist-info → service_forge-0.1.39.dist-info}/WHEEL +0 -0
- {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
|
|
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:
|
service_forge/service_config.py
CHANGED
|
@@ -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}")
|