@aurostack/stacks 0.1.1 → 0.2.0
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.
- package/README.md +59 -0
- package/cli/src/generate.mjs +13 -1
- package/package.json +1 -1
- package/templates/nest-api/files/.env.example +29 -2
- package/templates/nest-api/files/.env.test.example +1 -1
- package/templates/nest-api/files/compose.yml +16 -0
- package/templates/nest-api/files/package.json +18 -3
- package/templates/nest-api/files/src/app.module.ts +9 -1
- package/templates/nest-api/files/src/app.setup.ts +5 -6
- package/templates/nest-api/files/src/common/__tests__/temporal.service.spec.ts +66 -0
- package/templates/nest-api/files/src/common/common.module.ts +4 -3
- package/templates/nest-api/files/src/common/controllers/index.ts +0 -1
- package/templates/nest-api/files/src/common/modules/index.ts +0 -1
- package/templates/nest-api/files/src/common/modules/logger.module.ts +2 -6
- package/templates/nest-api/files/src/common/services/config.service.ts +43 -5
- package/templates/nest-api/files/src/common/services/index.ts +1 -0
- package/templates/nest-api/files/src/common/services/temporal.service.ts +66 -0
- package/templates/nest-api/files/src/instrumentation.ts +117 -0
- package/templates/nest-api/files/src/main.ts +3 -1
- package/templates/nest-api/files/src/telemetry/__tests__/config.spec.ts +67 -0
- package/templates/nest-api/files/src/telemetry/config.ts +62 -0
- package/templates/nest-api/template.json +36 -10
- package/templates/node-worker/files/.env.example +27 -0
- package/templates/node-worker/files/Dockerfile +25 -2
- package/templates/node-worker/files/Dockerfile.dev +25 -2
- package/templates/node-worker/files/ecosystem.config.cjs +2 -0
- package/templates/node-worker/files/package.json +23 -4
- package/templates/node-worker/files/src/config.ts +23 -0
- package/templates/node-worker/files/src/index.ts +13 -0
- package/templates/node-worker/files/src/instrumentation.ts +110 -0
- package/templates/node-worker/files/src/telemetry/config.ts +61 -0
- package/templates/node-worker/files/src/temporal/activities/index.ts +14 -0
- package/templates/node-worker/files/src/temporal/worker.ts +84 -0
- package/templates/node-worker/files/src/temporal/workflows/index.ts +23 -0
- package/templates/node-worker/files/src/utils/worker.ts +9 -1
- package/templates/node-worker/template.json +43 -1
- package/templates/py-worker/files/.env.example +28 -0
- package/templates/py-worker/files/config.py +32 -1
- package/templates/py-worker/files/requirements.txt +16 -0
- package/templates/py-worker/files/temporal/__init__.py +0 -0
- package/templates/py-worker/files/temporal/activities.py +13 -0
- package/templates/py-worker/files/temporal/workflows.py +38 -0
- package/templates/py-worker/files/utils/telemetry.py +154 -0
- package/templates/py-worker/files/utils/temporal.py +55 -0
- package/templates/py-worker/files/utils/worker.py +12 -1
- package/templates/py-worker/files/worker.py +16 -0
- package/templates/py-worker/template.json +36 -0
- package/templates/react-app/derive.sh +2 -1
- package/templates/react-app/files/.env.example +11 -0
- package/templates/react-app/files/package.json +2 -0
- package/templates/react-app/files/src/lib/env.ts +10 -0
- package/templates/react-app/files/src/main.tsx +4 -0
- package/templates/react-app/files/src/shared/auth/provider.tsx +2 -0
- package/templates/react-app/files/src/shared/layouts/error-boundary.tsx +2 -0
- package/templates/react-app/files/src/shared/telemetry/index.ts +80 -0
- package/templates/react-app/template.json +14 -0
- package/templates/react-monorepo/files/apps/admin/.env.example +11 -0
- package/templates/react-monorepo/files/apps/admin/package.json +1 -0
- package/templates/react-monorepo/files/apps/admin/src/lib/env.ts +10 -0
- package/templates/react-monorepo/files/apps/admin/src/main.tsx +4 -0
- package/templates/react-monorepo/files/apps/auth/.env.example +11 -0
- package/templates/react-monorepo/files/apps/auth/package.json +1 -0
- package/templates/react-monorepo/files/apps/auth/src/lib/env.ts +10 -0
- package/templates/react-monorepo/files/apps/auth/src/main.tsx +4 -0
- package/templates/react-monorepo/files/apps/client/.env.example +11 -0
- package/templates/react-monorepo/files/apps/client/package.json +1 -0
- package/templates/react-monorepo/files/apps/client/src/lib/env.ts +10 -0
- package/templates/react-monorepo/files/apps/client/src/main.tsx +4 -0
- package/templates/react-monorepo/files/apps/landing/.env.example +11 -0
- package/templates/react-monorepo/files/apps/landing/package.json +1 -0
- package/templates/react-monorepo/files/apps/landing/src/lib/env.ts +10 -0
- package/templates/react-monorepo/files/apps/landing/src/main.tsx +4 -0
- package/templates/react-monorepo/files/packages/auth/package.json +1 -0
- package/templates/react-monorepo/files/packages/auth/src/provider.tsx +2 -0
- package/templates/react-monorepo/files/packages/layouts/package.json +1 -0
- package/templates/react-monorepo/files/packages/layouts/src/error-boundary.tsx +2 -0
- package/templates/react-monorepo/files/packages/telemetry/eslint.config.mjs +3 -0
- package/templates/react-monorepo/files/packages/telemetry/package.json +20 -0
- package/templates/react-monorepo/files/packages/telemetry/src/index.ts +80 -0
- package/templates/react-monorepo/files/packages/telemetry/tsconfig.json +4 -0
- package/templates/react-monorepo/template.json +46 -0
- package/templates/nest-api/files/src/common/controllers/metrics.controller.ts +0 -21
- package/templates/nest-api/files/src/common/interceptors/index.ts +0 -1
- package/templates/nest-api/files/src/common/interceptors/metrics.interceptor.ts +0 -37
- package/templates/nest-api/files/src/common/modules/metrics.module.ts +0 -28
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenTelemetry: traces, logs and metrics over OTLP, built for OpenObserve.
|
|
3
|
+
|
|
4
|
+
Two ways to connect, in order of precedence:
|
|
5
|
+
- the standard OpenTelemetry variables (OTEL_EXPORTER_OTLP_ENDPOINT and
|
|
6
|
+
friends), which the exporters read themselves, for any OTLP backend;
|
|
7
|
+
- OPENOBSERVE_URL + OPENOBSERVE_ORG + OPENOBSERVE_TOKEN, from which the
|
|
8
|
+
OpenObserve endpoints and the org ingestion-token header are built.
|
|
9
|
+
With neither, setup_telemetry() does nothing and job_span() is a no-op.
|
|
10
|
+
"""
|
|
11
|
+
import base64
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
from opentelemetry import trace
|
|
18
|
+
|
|
19
|
+
DEFAULT_SERVICE_NAME = "acme-py"
|
|
20
|
+
|
|
21
|
+
_providers: list = []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class TelemetryConfig:
|
|
26
|
+
enabled: bool
|
|
27
|
+
service_name: str
|
|
28
|
+
environment: str
|
|
29
|
+
# Signal URLs; empty means the exporters resolve them from OTEL_* vars.
|
|
30
|
+
urls: dict = field(default_factory=dict)
|
|
31
|
+
headers: dict = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_telemetry(env=os.environ) -> TelemetryConfig:
|
|
35
|
+
service_name = env.get("OTEL_SERVICE_NAME") or DEFAULT_SERVICE_NAME
|
|
36
|
+
environment = env.get("APP_ENV") or "development"
|
|
37
|
+
off = TelemetryConfig(False, service_name, environment)
|
|
38
|
+
|
|
39
|
+
if env.get("OTEL_SDK_DISABLED") == "true":
|
|
40
|
+
return off
|
|
41
|
+
if env.get("OTEL_EXPORTER_OTLP_ENDPOINT"):
|
|
42
|
+
return TelemetryConfig(True, service_name, environment)
|
|
43
|
+
|
|
44
|
+
url, org, token = (
|
|
45
|
+
env.get("OPENOBSERVE_URL"),
|
|
46
|
+
env.get("OPENOBSERVE_ORG"),
|
|
47
|
+
env.get("OPENOBSERVE_TOKEN"),
|
|
48
|
+
)
|
|
49
|
+
if not (url and org and token):
|
|
50
|
+
return off
|
|
51
|
+
|
|
52
|
+
# OTLP/HTTP endpoints live under /api/<org>; an org ingestion token
|
|
53
|
+
# authenticates as `<org>:<token>`. Logs and traces land in the stream
|
|
54
|
+
# named here, one per service.
|
|
55
|
+
base = f"{url.rstrip('/')}/api/{org}"
|
|
56
|
+
credentials = base64.b64encode(f"{org}:{token}".encode()).decode()
|
|
57
|
+
return TelemetryConfig(
|
|
58
|
+
True,
|
|
59
|
+
service_name,
|
|
60
|
+
environment,
|
|
61
|
+
urls={
|
|
62
|
+
"traces": f"{base}/v1/traces",
|
|
63
|
+
"logs": f"{base}/v1/logs",
|
|
64
|
+
"metrics": f"{base}/v1/metrics",
|
|
65
|
+
},
|
|
66
|
+
headers={
|
|
67
|
+
"Authorization": f"Basic {credentials}",
|
|
68
|
+
"stream-name": env.get("OPENOBSERVE_STREAM") or service_name,
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def setup_telemetry() -> bool:
|
|
74
|
+
"""Start exporting, if connection settings are present. Call once, first."""
|
|
75
|
+
config = resolve_telemetry()
|
|
76
|
+
if not config.enabled:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Imported here so a worker without telemetry never loads the SDK.
|
|
80
|
+
from opentelemetry._logs import set_logger_provider
|
|
81
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
82
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
|
|
83
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
84
|
+
from opentelemetry.instrumentation.redis import RedisInstrumentor
|
|
85
|
+
from opentelemetry.metrics import set_meter_provider
|
|
86
|
+
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
87
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
|
88
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
89
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
90
|
+
from opentelemetry.sdk.resources import Resource
|
|
91
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
92
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
93
|
+
|
|
94
|
+
def exporter_args(signal: str) -> dict:
|
|
95
|
+
if not config.urls:
|
|
96
|
+
return {}
|
|
97
|
+
return {"endpoint": config.urls[signal], "headers": config.headers}
|
|
98
|
+
|
|
99
|
+
resource = Resource.create(
|
|
100
|
+
{
|
|
101
|
+
"service.name": config.service_name,
|
|
102
|
+
"deployment.environment.name": config.environment,
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
tracer_provider = TracerProvider(resource=resource)
|
|
107
|
+
tracer_provider.add_span_processor(
|
|
108
|
+
BatchSpanProcessor(OTLPSpanExporter(**exporter_args("traces")))
|
|
109
|
+
)
|
|
110
|
+
trace.set_tracer_provider(tracer_provider)
|
|
111
|
+
|
|
112
|
+
logger_provider = LoggerProvider(resource=resource)
|
|
113
|
+
logger_provider.add_log_record_processor(
|
|
114
|
+
BatchLogRecordProcessor(OTLPLogExporter(**exporter_args("logs")))
|
|
115
|
+
)
|
|
116
|
+
set_logger_provider(logger_provider)
|
|
117
|
+
# Every utils.logger.Logger propagates to the root logger.
|
|
118
|
+
logging.getLogger().addHandler(LoggingHandler(logger_provider=logger_provider))
|
|
119
|
+
|
|
120
|
+
meter_provider = MeterProvider(
|
|
121
|
+
resource=resource,
|
|
122
|
+
metric_readers=[
|
|
123
|
+
PeriodicExportingMetricReader(
|
|
124
|
+
OTLPMetricExporter(**exporter_args("metrics"))
|
|
125
|
+
)
|
|
126
|
+
],
|
|
127
|
+
)
|
|
128
|
+
set_meter_provider(meter_provider)
|
|
129
|
+
|
|
130
|
+
RedisInstrumentor().instrument()
|
|
131
|
+
_providers.extend([tracer_provider, logger_provider, meter_provider])
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def shutdown_telemetry() -> None:
|
|
136
|
+
"""Flush whatever is buffered. Safe to call when telemetry is off."""
|
|
137
|
+
for provider in _providers:
|
|
138
|
+
provider.shutdown()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@contextmanager
|
|
142
|
+
def job_span(queue_name: str, job):
|
|
143
|
+
"""One span per job, so each job is its own trace in OpenObserve."""
|
|
144
|
+
tracer = trace.get_tracer("worker")
|
|
145
|
+
with tracer.start_as_current_span(
|
|
146
|
+
f"process {queue_name}",
|
|
147
|
+
attributes={"messaging.system": "bullmq", "messaging.destination.name": queue_name, "bullmq.job.id": str(job.id)},
|
|
148
|
+
) as span:
|
|
149
|
+
try:
|
|
150
|
+
yield span
|
|
151
|
+
except Exception as error:
|
|
152
|
+
span.record_exception(error)
|
|
153
|
+
span.set_status(trace.Status(trace.StatusCode.ERROR, str(error)))
|
|
154
|
+
raise
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from temporalio.client import Client, TLSConfig
|
|
2
|
+
from temporalio.worker import Worker
|
|
3
|
+
|
|
4
|
+
from config import CONFIG, TemporalTLS
|
|
5
|
+
from temporal.activities import greet
|
|
6
|
+
from temporal.workflows import Example
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def temporal_tls(tls: TemporalTLS) -> TLSConfig | bool:
|
|
10
|
+
"""
|
|
11
|
+
mTLS settings from PEM strings; none at all means plaintext (False).
|
|
12
|
+
Escaped \\n are unescaped, so a certificate fits on one line of an env file.
|
|
13
|
+
"""
|
|
14
|
+
if not (tls.ca or tls.cert or tls.key):
|
|
15
|
+
return False
|
|
16
|
+
if not (tls.cert and tls.key):
|
|
17
|
+
raise ValueError("TEMPORAL_TLS_CERT and TEMPORAL_TLS_KEY must be set together")
|
|
18
|
+
|
|
19
|
+
def pem(value: str) -> bytes:
|
|
20
|
+
return value.replace("\\n", "\n").encode()
|
|
21
|
+
|
|
22
|
+
return TLSConfig(
|
|
23
|
+
server_root_ca_cert=pem(tls.ca) if tls.ca else None,
|
|
24
|
+
client_cert=pem(tls.cert),
|
|
25
|
+
client_private_key=pem(tls.key),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def create_temporal_worker() -> Worker:
|
|
30
|
+
"""
|
|
31
|
+
Connect to Temporal and build a worker for TEMPORAL_TASK_QUEUE, running the
|
|
32
|
+
workflows and activities in temporal/. Register new ones in the lists below.
|
|
33
|
+
"""
|
|
34
|
+
settings = CONFIG.env.temporal
|
|
35
|
+
interceptors = []
|
|
36
|
+
# @feature:start telemetry
|
|
37
|
+
# A span per workflow and activity, continuing the trace of whatever
|
|
38
|
+
# started the workflow; a no-op while telemetry is off.
|
|
39
|
+
from temporalio.contrib.opentelemetry import TracingInterceptor
|
|
40
|
+
|
|
41
|
+
interceptors.append(TracingInterceptor())
|
|
42
|
+
# @feature:end
|
|
43
|
+
|
|
44
|
+
client = await Client.connect(
|
|
45
|
+
settings.address,
|
|
46
|
+
namespace=settings.namespace,
|
|
47
|
+
tls=temporal_tls(settings.tls),
|
|
48
|
+
interceptors=interceptors,
|
|
49
|
+
)
|
|
50
|
+
return Worker(
|
|
51
|
+
client,
|
|
52
|
+
task_queue=settings.task_queue,
|
|
53
|
+
workflows=[Example],
|
|
54
|
+
activities=[greet],
|
|
55
|
+
)
|
|
@@ -5,6 +5,7 @@ from urllib.parse import urlparse, quote, urlencode
|
|
|
5
5
|
from config import CONFIG, Redis
|
|
6
6
|
from utils.db import DB
|
|
7
7
|
from utils.logger import Logger
|
|
8
|
+
from utils.telemetry import job_span # @feature telemetry
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def build_redis_url(options: Redis):
|
|
@@ -39,9 +40,19 @@ def create_worker(queue_name: str, process_function: Callable, concurrency: int
|
|
|
39
40
|
An instance of BullMQ Worker.
|
|
40
41
|
"""
|
|
41
42
|
connection = build_redis_url(CONFIG.env.redis)
|
|
43
|
+
|
|
44
|
+
processor = process_function
|
|
45
|
+
# @feature:start telemetry
|
|
46
|
+
async def traced(job: Job, token: str):
|
|
47
|
+
with job_span(queue_name, job):
|
|
48
|
+
return await process_function(job, token)
|
|
49
|
+
|
|
50
|
+
processor = traced
|
|
51
|
+
# @feature:end
|
|
52
|
+
|
|
42
53
|
worker = Worker(
|
|
43
54
|
queue_name,
|
|
44
|
-
|
|
55
|
+
processor,
|
|
45
56
|
{
|
|
46
57
|
"connection": connection,
|
|
47
58
|
"autorun": True,
|
|
@@ -4,6 +4,8 @@ import asyncio
|
|
|
4
4
|
import signal
|
|
5
5
|
from utils.logger import Logger
|
|
6
6
|
from utils.worker import create_worker
|
|
7
|
+
from utils.telemetry import setup_telemetry, shutdown_telemetry # @feature telemetry
|
|
8
|
+
from utils.temporal import create_temporal_worker # @feature temporal
|
|
7
9
|
|
|
8
10
|
# import worker processes
|
|
9
11
|
from workers.hello.process import hello_process
|
|
@@ -11,6 +13,7 @@ logger = Logger("worker")
|
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
async def main():
|
|
16
|
+
setup_telemetry() # @feature telemetry
|
|
14
17
|
logger.info("Starting Python 3.14 Worker...")
|
|
15
18
|
shutdown_event = asyncio.Event()
|
|
16
19
|
loop = asyncio.get_running_loop()
|
|
@@ -27,6 +30,12 @@ async def main():
|
|
|
27
30
|
|
|
28
31
|
# Add workers
|
|
29
32
|
hello = create_worker("hello", hello_process)
|
|
33
|
+
# @feature:start temporal
|
|
34
|
+
temporal_worker = await create_temporal_worker()
|
|
35
|
+
temporal_run = asyncio.create_task(temporal_worker.run())
|
|
36
|
+
# If the Temporal worker stops on its own (a fatal error), stop everything.
|
|
37
|
+
temporal_run.add_done_callback(lambda _: shutdown_event.set())
|
|
38
|
+
# @feature:end
|
|
30
39
|
|
|
31
40
|
logger.info("Workers started and waiting for jobs.")
|
|
32
41
|
try:
|
|
@@ -35,7 +44,14 @@ async def main():
|
|
|
35
44
|
logger.info("Cleaning up workers...")
|
|
36
45
|
# Close workers gracefully
|
|
37
46
|
await hello.close()
|
|
47
|
+
# @feature:start temporal
|
|
48
|
+
# Stops polling and waits for running activities to finish.
|
|
49
|
+
if not temporal_run.done():
|
|
50
|
+
await temporal_worker.shutdown()
|
|
51
|
+
await temporal_run
|
|
52
|
+
# @feature:end
|
|
38
53
|
logger.info("Workers shut down successfully.")
|
|
54
|
+
shutdown_telemetry() # @feature telemetry
|
|
39
55
|
|
|
40
56
|
if __name__ == "__main__":
|
|
41
57
|
asyncio.run(main())
|
|
@@ -55,6 +55,42 @@
|
|
|
55
55
|
"files": [
|
|
56
56
|
".forgejo/**"
|
|
57
57
|
]
|
|
58
|
+
},
|
|
59
|
+
"telemetry": {
|
|
60
|
+
"title": "OpenTelemetry → OpenObserve",
|
|
61
|
+
"description": "Traces, logs and metrics over OTLP (set OPENOBSERVE_URL/ORG/TOKEN, or any OTEL_* backend), with a span per job and Redis instrumentation. Off at runtime until connected.",
|
|
62
|
+
"default": true,
|
|
63
|
+
"files": [
|
|
64
|
+
"utils/telemetry.py"
|
|
65
|
+
],
|
|
66
|
+
"requirements": [
|
|
67
|
+
"googleapis-common-protos",
|
|
68
|
+
"opentelemetry-api",
|
|
69
|
+
"opentelemetry-exporter-http-transport",
|
|
70
|
+
"opentelemetry-exporter-otlp-common",
|
|
71
|
+
"opentelemetry-exporter-otlp-proto-common",
|
|
72
|
+
"opentelemetry-exporter-otlp-proto-http",
|
|
73
|
+
"opentelemetry-instrumentation",
|
|
74
|
+
"opentelemetry-instrumentation-redis",
|
|
75
|
+
"opentelemetry-proto",
|
|
76
|
+
"opentelemetry-sdk",
|
|
77
|
+
"opentelemetry-semantic-conventions",
|
|
78
|
+
"protobuf",
|
|
79
|
+
"wrapt"
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
"temporal": {
|
|
83
|
+
"title": "Temporal worker",
|
|
84
|
+
"description": "Runs durable workflows and activities from temporal/ (an example of each included) on its own TEMPORAL_TASK_QUEUE, next to the BullMQ consumers, with a span per workflow and activity when telemetry is on. Pair with the API's temporal feature, which starts them.",
|
|
85
|
+
"files": [
|
|
86
|
+
"utils/temporal.py",
|
|
87
|
+
"temporal/**"
|
|
88
|
+
],
|
|
89
|
+
"requirements": [
|
|
90
|
+
"nexus-rpc",
|
|
91
|
+
"temporalio",
|
|
92
|
+
"types-protobuf"
|
|
93
|
+
]
|
|
58
94
|
}
|
|
59
95
|
},
|
|
60
96
|
"hooks": [
|
|
@@ -52,7 +52,7 @@ done
|
|
|
52
52
|
|
|
53
53
|
echo "==> flattening packages -> src/shared"
|
|
54
54
|
mkdir -p "$DST/src/shared"
|
|
55
|
-
for pkg in ui api auth layouts hooks types; do
|
|
55
|
+
for pkg in ui api auth layouts hooks types telemetry; do
|
|
56
56
|
cp -r "$SRC/packages/$pkg/src" "$DST/src/shared/$pkg"
|
|
57
57
|
done
|
|
58
58
|
# The brand tokens live in the config package in the monorepo because three
|
|
@@ -94,6 +94,7 @@ find "$DST/src" -type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.css' \) -pr
|
|
|
94
94
|
-e "s#@acme/auth#@/shared/auth#g" \
|
|
95
95
|
-e "s#@acme/layouts#@/shared/layouts#g" \
|
|
96
96
|
-e "s#@acme/hooks#@/shared/hooks#g" \
|
|
97
|
+
-e "s#@acme/telemetry#@/shared/telemetry#g" \
|
|
97
98
|
-e "s#@acme/types#@/shared/types#g"
|
|
98
99
|
|
|
99
100
|
# The auth screens moved one level deeper than their components and lib, so
|
|
@@ -11,6 +11,17 @@ PORT='3000'
|
|
|
11
11
|
# Needed when you reach the dev server through a tunnel or a wildcard DNS name.
|
|
12
12
|
VITE_APP_ALLOWED_HOSTS=''
|
|
13
13
|
|
|
14
|
+
# @feature:start telemetry
|
|
15
|
+
# OpenObserve browser monitoring (RUM): page views, errors, slow requests, and
|
|
16
|
+
# trace headers on API calls. Leave empty and it stays off. The client token is
|
|
17
|
+
# a RUM token (OpenObserve → Ingestion → RUM): write-only and meant to ship in
|
|
18
|
+
# the bundle — never the backend's ingestion token.
|
|
19
|
+
# The instance must list this app's origin in ZO_CORS_ALLOWED_ORIGINS.
|
|
20
|
+
VITE_OPENOBSERVE_URL='https://o2.aurostack.co'
|
|
21
|
+
VITE_OPENOBSERVE_ORG=''
|
|
22
|
+
VITE_OPENOBSERVE_CLIENT_TOKEN=''
|
|
23
|
+
# @feature:end
|
|
24
|
+
|
|
14
25
|
# @feature:start marketing
|
|
15
26
|
# The canonical public origin — the ONE origin that may be indexed. Must match
|
|
16
27
|
# PRODUCTION_ORIGIN in src/features/seo/routes.ts. Anything else (previews,
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@fontsource-variable/inter": "^5.3.0",
|
|
29
|
+
"@openobserve/browser-logs": "^0.4.3",
|
|
30
|
+
"@openobserve/browser-rum": "^0.4.3",
|
|
29
31
|
"@radix-ui/react-label": "^2.1.7",
|
|
30
32
|
"@radix-ui/react-popover": "^1.1.21",
|
|
31
33
|
"@radix-ui/react-select": "^2.3.4",
|
|
@@ -26,3 +26,13 @@ export const APP_ORIGIN = window.location.origin;
|
|
|
26
26
|
*/
|
|
27
27
|
export const SITE_URL = (env.VITE_APP_SITE_URL as string | undefined) ?? '';
|
|
28
28
|
// @feature:end
|
|
29
|
+
|
|
30
|
+
// @feature:start telemetry
|
|
31
|
+
/** OpenObserve browser monitoring (see @/shared/telemetry); unset keeps it off. */
|
|
32
|
+
export const TELEMETRY = {
|
|
33
|
+
url: env.VITE_OPENOBSERVE_URL as string | undefined,
|
|
34
|
+
org: env.VITE_OPENOBSERVE_ORG as string | undefined,
|
|
35
|
+
clientToken: env.VITE_OPENOBSERVE_CLIENT_TOKEN as string | undefined,
|
|
36
|
+
environment: env.MODE
|
|
37
|
+
};
|
|
38
|
+
// @feature:end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { StrictMode } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { initTelemetry } from '@/shared/telemetry'; // @feature telemetry
|
|
4
|
+
import { API_URL, TELEMETRY } from '@/lib/env'; // @feature telemetry
|
|
3
5
|
import { RouterProvider } from 'react-router';
|
|
4
6
|
import '@/shared/ui/styles/globals.css';
|
|
5
7
|
import { Providers } from './app/providers';
|
|
@@ -10,6 +12,8 @@ import { router } from './app/router';
|
|
|
10
12
|
// evaluating and `providers` has already called `configureApi` at module scope,
|
|
11
13
|
// so the fetch races the session check instead of queueing behind it.
|
|
12
14
|
|
|
15
|
+
initTelemetry({ service: 'acme-app', ...TELEMETRY, apiUrls: [API_URL] }); // @feature telemetry
|
|
16
|
+
|
|
13
17
|
createRoot(document.getElementById('root')!).render(
|
|
14
18
|
<StrictMode>
|
|
15
19
|
<Providers>
|
|
@@ -3,6 +3,7 @@ import { useDispatch } from 'react-redux';
|
|
|
3
3
|
import { getAuthClient } from './client';
|
|
4
4
|
import { sessionCleared, sessionLoaded, sessionLoading, type AuthUser } from './slice';
|
|
5
5
|
import { AUTH_BROADCAST_CHANNEL } from './logout';
|
|
6
|
+
import { setTelemetryUser } from '@/shared/telemetry'; // @feature telemetry
|
|
6
7
|
|
|
7
8
|
function toAuthUser(user: Record<string, unknown> | null | undefined): AuthUser | null {
|
|
8
9
|
if (!user) return null;
|
|
@@ -33,6 +34,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|
|
33
34
|
}
|
|
34
35
|
const user = (session.data?.user as Record<string, unknown> | undefined) ?? null;
|
|
35
36
|
dispatch(sessionLoaded(toAuthUser(user)));
|
|
37
|
+
setTelemetryUser(user ? { id: String(user.id) } : null); // @feature telemetry
|
|
36
38
|
}, [session.isPending, session.data, dispatch]);
|
|
37
39
|
|
|
38
40
|
useEffect(() => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
2
2
|
import { Button } from '@/shared/ui';
|
|
3
3
|
import { StateScreen } from './states/state-screen';
|
|
4
|
+
import { reportError } from '@/shared/telemetry'; // @feature telemetry
|
|
4
5
|
|
|
5
6
|
type ErrorBoundaryProps = {
|
|
6
7
|
children: ReactNode;
|
|
@@ -21,6 +22,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
21
22
|
|
|
22
23
|
componentDidCatch(error: Error, info: ErrorInfo): void {
|
|
23
24
|
console.error('ErrorBoundary caught an error', error, info);
|
|
25
|
+
reportError(error, { componentStack: info.componentStack }); // @feature telemetry
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
render(): ReactNode {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @/shared/telemetry — browser monitoring (RUM) and logs, sent to OpenObserve.
|
|
2
|
+
//
|
|
3
|
+
// Page views, errors, slow resources and user actions, plus console errors as
|
|
4
|
+
// logs. Requests to the app's own API carry a W3C `traceparent` header, so a
|
|
5
|
+
// click links to the backend trace it caused. The SDK finishes starting a
|
|
6
|
+
// moment after init (its session store is async), so the first request or two
|
|
7
|
+
// of a page load (typically the session check) go out without the header.
|
|
8
|
+
// Off until connection settings are passed; everything here is then a no-op.
|
|
9
|
+
import { DefaultPrivacyLevel, openobserveRum } from '@openobserve/browser-rum';
|
|
10
|
+
import { openobserveLogs } from '@openobserve/browser-logs';
|
|
11
|
+
|
|
12
|
+
export interface TelemetryOptions {
|
|
13
|
+
/** Service name, e.g. `acme-client` — one per app. */
|
|
14
|
+
service: string;
|
|
15
|
+
/** OpenObserve base URL, e.g. `https://o2.example.com`. */
|
|
16
|
+
url?: string;
|
|
17
|
+
/** The project's organization identifier. */
|
|
18
|
+
org?: string;
|
|
19
|
+
/**
|
|
20
|
+
* A RUM client token (Ingestion → RUM). Write-only and built for shipping
|
|
21
|
+
* in a bundle — never the backend's ingestion token.
|
|
22
|
+
*/
|
|
23
|
+
clientToken?: string;
|
|
24
|
+
/** `development`, `production`, … — Vite's `import.meta.env.MODE`. */
|
|
25
|
+
environment?: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
/** Backend origin(s) whose requests should carry trace headers. */
|
|
28
|
+
apiUrls?: string[];
|
|
29
|
+
/** Share of sessions to record for replay, 0–100. Off by default. */
|
|
30
|
+
sessionReplaySampleRate?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let started = false;
|
|
34
|
+
|
|
35
|
+
export function initTelemetry(options: TelemetryOptions): boolean {
|
|
36
|
+
const { url, org, clientToken } = options;
|
|
37
|
+
if (started || !url || !org || !clientToken) return false;
|
|
38
|
+
|
|
39
|
+
// The SDK takes a bare host (no scheme) plus an explicit plain-HTTP flag.
|
|
40
|
+
const { host, protocol } = new URL(url);
|
|
41
|
+
const common = {
|
|
42
|
+
clientToken,
|
|
43
|
+
site: host,
|
|
44
|
+
organizationIdentifier: org,
|
|
45
|
+
insecureHTTP: protocol === 'http:',
|
|
46
|
+
service: options.service,
|
|
47
|
+
env: options.environment,
|
|
48
|
+
version: options.version
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
openobserveRum.init({
|
|
52
|
+
...common,
|
|
53
|
+
applicationId: options.service,
|
|
54
|
+
sessionSampleRate: 100,
|
|
55
|
+
sessionReplaySampleRate: options.sessionReplaySampleRate ?? 0,
|
|
56
|
+
defaultPrivacyLevel: DefaultPrivacyLevel.MASK_USER_INPUT,
|
|
57
|
+
trackResources: true,
|
|
58
|
+
trackLongTasks: true,
|
|
59
|
+
trackUserInteractions: true,
|
|
60
|
+
allowedTracingUrls: (options.apiUrls ?? []).filter(Boolean).map((match) => ({
|
|
61
|
+
match,
|
|
62
|
+
propagatorTypes: ['tracecontext' as const]
|
|
63
|
+
}))
|
|
64
|
+
});
|
|
65
|
+
openobserveLogs.init({ ...common, forwardErrorsToLogs: true });
|
|
66
|
+
started = true;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Report a handled error — e.g. from an error boundary. */
|
|
71
|
+
export function reportError(error: unknown, context?: object): void {
|
|
72
|
+
if (started) openobserveRum.addError(error, context);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Tie the session to a signed-in user (id only), or clear it on logout. */
|
|
76
|
+
export function setTelemetryUser(user: { id: string } | null): void {
|
|
77
|
+
if (!started) return;
|
|
78
|
+
if (user) openobserveRum.setUser({ id: user.id });
|
|
79
|
+
else openobserveRum.clearUser();
|
|
80
|
+
}
|
|
@@ -189,6 +189,20 @@
|
|
|
189
189
|
"secrets"
|
|
190
190
|
]
|
|
191
191
|
}
|
|
192
|
+
},
|
|
193
|
+
"telemetry": {
|
|
194
|
+
"title": "Browser monitoring → OpenObserve",
|
|
195
|
+
"description": "OpenObserve RUM and logs: page views, errors (including the error boundary's), slow resources, user actions, the signed-in user id, and traceparent headers on API calls so frontend and backend traces link up. Session replay is off unless a project turns it on. Off at runtime until VITE_OPENOBSERVE_* are set.",
|
|
196
|
+
"default": true,
|
|
197
|
+
"files": [
|
|
198
|
+
"src/shared/telemetry/**"
|
|
199
|
+
],
|
|
200
|
+
"packageJson": {
|
|
201
|
+
"dependencies": [
|
|
202
|
+
"@openobserve/browser-logs",
|
|
203
|
+
"@openobserve/browser-rum"
|
|
204
|
+
]
|
|
205
|
+
}
|
|
192
206
|
}
|
|
193
207
|
},
|
|
194
208
|
"hooks": [
|
|
@@ -14,3 +14,14 @@ VITE_APP_ADMIN_HOST='http://localhost:3003'
|
|
|
14
14
|
|
|
15
15
|
# This app's own dev port.
|
|
16
16
|
PORT='3003'
|
|
17
|
+
|
|
18
|
+
# @feature:start telemetry
|
|
19
|
+
# OpenObserve browser monitoring (RUM): page views, errors, slow requests, and
|
|
20
|
+
# trace headers on API calls. Leave empty and it stays off. The client token is
|
|
21
|
+
# a RUM token (OpenObserve → Ingestion → RUM): write-only and meant to ship in
|
|
22
|
+
# the bundle — never the backend's ingestion token.
|
|
23
|
+
# The instance must list this app's origin in ZO_CORS_ALLOWED_ORIGINS.
|
|
24
|
+
VITE_OPENOBSERVE_URL='https://o2.aurostack.co'
|
|
25
|
+
VITE_OPENOBSERVE_ORG=''
|
|
26
|
+
VITE_OPENOBSERVE_CLIENT_TOKEN=''
|
|
27
|
+
# @feature:end
|
|
@@ -10,3 +10,13 @@ export const ADMIN_URL = env.VITE_APP_ADMIN_HOST as string;
|
|
|
10
10
|
export const CLIENT_URL = env.VITE_APP_CLIENT_HOST as string;
|
|
11
11
|
/** The public marketing site (logout destination). */
|
|
12
12
|
export const LANDING_URL = env.VITE_APP_LANDING as string;
|
|
13
|
+
|
|
14
|
+
// @feature:start telemetry
|
|
15
|
+
/** OpenObserve browser monitoring (see @acme/telemetry); unset keeps it off. */
|
|
16
|
+
export const TELEMETRY = {
|
|
17
|
+
url: env.VITE_OPENOBSERVE_URL as string | undefined,
|
|
18
|
+
org: env.VITE_OPENOBSERVE_ORG as string | undefined,
|
|
19
|
+
clientToken: env.VITE_OPENOBSERVE_CLIENT_TOKEN as string | undefined,
|
|
20
|
+
environment: env.MODE
|
|
21
|
+
};
|
|
22
|
+
// @feature:end
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { StrictMode } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { initTelemetry } from '@acme/telemetry'; // @feature telemetry
|
|
4
|
+
import { API_URL, TELEMETRY } from './lib/env'; // @feature telemetry
|
|
3
5
|
import { RouterProvider } from 'react-router';
|
|
4
6
|
import '@acme/ui/globals.css';
|
|
5
7
|
import { Providers } from './app/providers';
|
|
6
8
|
import { router } from './app/router';
|
|
7
9
|
|
|
10
|
+
initTelemetry({ service: 'acme-admin', ...TELEMETRY, apiUrls: [API_URL] }); // @feature telemetry
|
|
11
|
+
|
|
8
12
|
createRoot(document.getElementById('root')!).render(
|
|
9
13
|
<StrictMode>
|
|
10
14
|
<Providers>
|
|
@@ -14,3 +14,14 @@ VITE_APP_ADMIN_HOST='http://localhost:3003'
|
|
|
14
14
|
|
|
15
15
|
# This app's own dev port.
|
|
16
16
|
PORT='3001'
|
|
17
|
+
|
|
18
|
+
# @feature:start telemetry
|
|
19
|
+
# OpenObserve browser monitoring (RUM): page views, errors, slow requests, and
|
|
20
|
+
# trace headers on API calls. Leave empty and it stays off. The client token is
|
|
21
|
+
# a RUM token (OpenObserve → Ingestion → RUM): write-only and meant to ship in
|
|
22
|
+
# the bundle — never the backend's ingestion token.
|
|
23
|
+
# The instance must list this app's origin in ZO_CORS_ALLOWED_ORIGINS.
|
|
24
|
+
VITE_OPENOBSERVE_URL='https://o2.aurostack.co'
|
|
25
|
+
VITE_OPENOBSERVE_ORG=''
|
|
26
|
+
VITE_OPENOBSERVE_CLIENT_TOKEN=''
|
|
27
|
+
# @feature:end
|
|
@@ -6,3 +6,13 @@ export const API_URL = env.VITE_APP_API_URL;
|
|
|
6
6
|
/** Terms and Privacy live on the marketing site, which is a different origin. */
|
|
7
7
|
export const LANDING_URL = env.VITE_APP_LANDING;
|
|
8
8
|
export const AUTH_URL = window.location.origin;
|
|
9
|
+
|
|
10
|
+
// @feature:start telemetry
|
|
11
|
+
/** OpenObserve browser monitoring (see @acme/telemetry); unset keeps it off. */
|
|
12
|
+
export const TELEMETRY = {
|
|
13
|
+
url: env.VITE_OPENOBSERVE_URL as string | undefined,
|
|
14
|
+
org: env.VITE_OPENOBSERVE_ORG as string | undefined,
|
|
15
|
+
clientToken: env.VITE_OPENOBSERVE_CLIENT_TOKEN as string | undefined,
|
|
16
|
+
environment: env.MODE
|
|
17
|
+
};
|
|
18
|
+
// @feature:end
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { StrictMode } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { initTelemetry } from '@acme/telemetry'; // @feature telemetry
|
|
4
|
+
import { API_URL, TELEMETRY } from './lib/env'; // @feature telemetry
|
|
3
5
|
import { RouterProvider } from 'react-router';
|
|
4
6
|
import '@acme/ui/globals.css';
|
|
5
7
|
import { Providers } from './app/providers';
|
|
6
8
|
import { router } from './app/router';
|
|
7
9
|
|
|
10
|
+
initTelemetry({ service: 'acme-auth', ...TELEMETRY, apiUrls: [API_URL] }); // @feature telemetry
|
|
11
|
+
|
|
8
12
|
createRoot(document.getElementById('root')!).render(
|
|
9
13
|
<StrictMode>
|
|
10
14
|
<Providers>
|
|
@@ -14,3 +14,14 @@ VITE_APP_ADMIN_HOST='http://localhost:3003'
|
|
|
14
14
|
|
|
15
15
|
# This app's own dev port.
|
|
16
16
|
PORT='3002'
|
|
17
|
+
|
|
18
|
+
# @feature:start telemetry
|
|
19
|
+
# OpenObserve browser monitoring (RUM): page views, errors, slow requests, and
|
|
20
|
+
# trace headers on API calls. Leave empty and it stays off. The client token is
|
|
21
|
+
# a RUM token (OpenObserve → Ingestion → RUM): write-only and meant to ship in
|
|
22
|
+
# the bundle — never the backend's ingestion token.
|
|
23
|
+
# The instance must list this app's origin in ZO_CORS_ALLOWED_ORIGINS.
|
|
24
|
+
VITE_OPENOBSERVE_URL='https://o2.aurostack.co'
|
|
25
|
+
VITE_OPENOBSERVE_ORG=''
|
|
26
|
+
VITE_OPENOBSERVE_CLIENT_TOKEN=''
|
|
27
|
+
# @feature:end
|