go-duck-cli 1.3.372 → 1.4.6
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 +20 -1
- package/generators/config.js +8 -0
- package/generators/devops.js +25 -18
- package/generators/logger.js +50 -1
- package/generators/mqtt-topics.js +54 -0
- package/generators/postman.js +2 -1
- package/generators/swagger.js +16 -16
- package/generators/telemetry.js +164 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/templates/docs/pages/configuration.hbs +46 -5
- package/templates/docs/pages/mosquitto.hbs +6 -0
- package/templates/docs/pages/observability.hbs +55 -0
- package/templates/go/router.go.hbs +808 -0
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ GO-DUCK has officially reached the **410% Achievement Status**, evolving from a
|
|
|
77
77
|
* **Zero-Trust Identity Registry**: Decoupled mapping layer ensuring physical database names and internal IDs never leak to the client.
|
|
78
78
|
* **Universal Storage Mesh**: Dynamic Multi-Provider Registry allowing hot-swapping at runtime via `?provider=` queries, alongside Distributed Cross-Scan endpoints to auto-locate files across AWS, GCS, SFTP, and GitHub lakes.
|
|
79
79
|
* **Enterprise API Gateway Compatibility**: Natively exposes the Swagger/OpenAPI JSON specification at `/v3/api-docs` to ensure drop-in compatibility with **JHipster**, **WSO2 API Manager**, and Spring Boot ecosystems.
|
|
80
|
-
* **Next-Gen Swagger UI**: A fully glassmorphism-styled Swagger UI with seamless Keycloak SSO integration. Features automatic JWT token refresh, dynamic `X-Tenant-ID` header injection, and CLI metadata branding.
|
|
80
|
+
* **Next-Gen Swagger UI**: A fully glassmorphism-styled Swagger UI with seamless Keycloak SSO integration. Features automatic JWT token refresh, dynamic `X-Tenant-ID` header injection, and CLI metadata branding. Now features a **Live Interactive MQTT Dictionary**, allowing users to instantly subscribe or publish payloads directly from the docs via WebSockets!
|
|
81
81
|
* **Spring-style Elasticsearch Search**: Real-time sync for entities marked with `@Searchable`, supporting native `query_string` syntax (wildcards like `*`, booleans, ranges, and fuzzy matching).
|
|
82
82
|
* **SaaS Quota Engine**: Redis-backed API bandwidth tracking with dynamic, hierarchical limits (User vs. Role mapping).
|
|
83
83
|
* **Resilience Layer**: Sony/Gobreaker Integration + Zero-Trust Distributed Redis Rate Limiter.
|
|
@@ -238,6 +238,15 @@ GO-DUCK automatically exposes your microservice across three distinct ports to s
|
|
|
238
238
|
```
|
|
239
239
|
*Frontend Tip: Use the `grpc-web` npm package to generate a frontend client that communicates with this port!*
|
|
240
240
|
|
|
241
|
+
## 📊 Telemetry & Observability
|
|
242
|
+
GO-DUCK automatically integrates industrial observability stack:
|
|
243
|
+
1. **OpenTelemetry Distributed Tracing**: Native `otelgin` and `gorm.io/plugin/opentelemetry/tracing` integrations.
|
|
244
|
+
2. **Triple-Metrics Endpoints**:
|
|
245
|
+
- `GET /metrics`: Standard Prometheus scraping endpoint for Kubernetes Horizontal Pod Autoscaling (HPA).
|
|
246
|
+
- `GET /api/system/stream`: A Server-Sent Events (SSE) endpoint streaming live JSON payloads of CPU, Memory, and Load Percentages directly to your browser for real-time dashboards.
|
|
247
|
+
- `GET /api/system/metrics`: A massive JSON endpoint returning JHipster-style metrics (Go GC pauses, Uptime, Goroutines, exact endpoint hits, latencies, and failed calls).
|
|
248
|
+
3. **Datadog Logging**: Environment-driven log streaming and monitoring natively via `logger` package.
|
|
249
|
+
|
|
241
250
|
## Usage
|
|
242
251
|
|
|
243
252
|
The `go-duck-cli` has two main commands: `create` and `import-gdl`.
|
|
@@ -390,6 +399,16 @@ go-duck:
|
|
|
390
399
|
rps: 100
|
|
391
400
|
burst: 200
|
|
392
401
|
|
|
402
|
+
# --- Telemetry & Metrics ---
|
|
403
|
+
telemetry:
|
|
404
|
+
otel:
|
|
405
|
+
enabled: true
|
|
406
|
+
endpoint: "localhost:4317"
|
|
407
|
+
metrics:
|
|
408
|
+
prometheus-enabled: true
|
|
409
|
+
stream-enabled: true
|
|
410
|
+
stream-interval: "1s"
|
|
411
|
+
|
|
393
412
|
# --- Federated Multi-Tenancy ---
|
|
394
413
|
multitenancy:
|
|
395
414
|
enabled: true
|
package/generators/config.js
CHANGED
|
@@ -106,6 +106,11 @@ type Config struct {
|
|
|
106
106
|
Endpoint string \`mapstructure:"endpoint"\`
|
|
107
107
|
SamplerRatio float64 \`mapstructure:"sampler-ratio"\`
|
|
108
108
|
} \`mapstructure:"otel"\`
|
|
109
|
+
Metrics struct {
|
|
110
|
+
PrometheusEnabled bool \`mapstructure:"prometheus-enabled"\`
|
|
111
|
+
StreamEnabled bool \`mapstructure:"stream-enabled"\`
|
|
112
|
+
StreamInterval time.Duration \`mapstructure:"stream-interval"\`
|
|
113
|
+
} \`mapstructure:"metrics"\`
|
|
109
114
|
} \`mapstructure:"telemetry"\`
|
|
110
115
|
|
|
111
116
|
Resilience struct {
|
|
@@ -262,6 +267,9 @@ func LoadConfig() (*Config, error) {
|
|
|
262
267
|
v.SetDefault("go-duck.telemetry.otel.enabled", false)
|
|
263
268
|
v.SetDefault("go-duck.telemetry.otel.endpoint", "localhost:4317")
|
|
264
269
|
v.SetDefault("go-duck.telemetry.otel.sampler-ratio", 1.0)
|
|
270
|
+
v.SetDefault("go-duck.telemetry.metrics.prometheus-enabled", true)
|
|
271
|
+
v.SetDefault("go-duck.telemetry.metrics.stream-enabled", true)
|
|
272
|
+
v.SetDefault("go-duck.telemetry.metrics.stream-interval", "1s")
|
|
265
273
|
v.SetDefault("go-duck.server.grpc.addr", ":9000")
|
|
266
274
|
v.SetDefault("go-duck.server.grpc.network", "tcp")
|
|
267
275
|
v.SetDefault("go-duck.server.grpc.timeout", "1s")
|
package/generators/devops.js
CHANGED
|
@@ -13,7 +13,12 @@ export const generateDeploymentArtifacts = async (config, projectRootDir) => {
|
|
|
13
13
|
await fs.ensureDir(keycloakDir);
|
|
14
14
|
await fs.ensureDir(githubDir);
|
|
15
15
|
|
|
16
|
+
const toKebabCase = (str) => {
|
|
17
|
+
if (!str) return '';
|
|
18
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
19
|
+
};
|
|
16
20
|
const appName = config.name || 'go-duck';
|
|
21
|
+
const k8sAppName = toKebabCase(appName);
|
|
17
22
|
const parts = appName.split(/[-_]/);
|
|
18
23
|
const shortName = (parts.length > 1 ? parts.map(w => w[0]).join('') : appName).substring(0, 10).toLowerCase() || 'goduck';
|
|
19
24
|
const appPort = config.server?.rest?.port || 8080;
|
|
@@ -75,9 +80,11 @@ FROM gcr.io/distroless/static-debian12
|
|
|
75
80
|
WORKDIR /app
|
|
76
81
|
|
|
77
82
|
COPY --from=builder /app/server .
|
|
83
|
+
|
|
78
84
|
COPY --from=builder /app/application.yml .
|
|
79
85
|
COPY --from=builder /app/application-dev.yml .
|
|
80
86
|
COPY --from=builder /app/application-prod.yml .
|
|
87
|
+
COPY --from=builder /app/docs/ /app/docs/
|
|
81
88
|
|
|
82
89
|
EXPOSE ${appPort}
|
|
83
90
|
ENV GO_PROFILE=prod
|
|
@@ -468,21 +475,21 @@ echo "========================================="
|
|
|
468
475
|
const k8sAppYaml = `apiVersion: apps/v1
|
|
469
476
|
kind: Deployment
|
|
470
477
|
metadata:
|
|
471
|
-
name: ${
|
|
478
|
+
name: ${k8sAppName}
|
|
472
479
|
labels:
|
|
473
|
-
app: ${
|
|
480
|
+
app: ${k8sAppName}
|
|
474
481
|
spec:
|
|
475
482
|
replicas: 1
|
|
476
483
|
selector:
|
|
477
484
|
matchLabels:
|
|
478
|
-
app: ${
|
|
485
|
+
app: ${k8sAppName}
|
|
479
486
|
template:
|
|
480
487
|
metadata:
|
|
481
488
|
labels:
|
|
482
|
-
app: ${
|
|
489
|
+
app: ${k8sAppName}
|
|
483
490
|
spec:
|
|
484
491
|
containers:
|
|
485
|
-
- name: ${
|
|
492
|
+
- name: ${k8sAppName}
|
|
486
493
|
image: ${appName}:latest
|
|
487
494
|
env:
|
|
488
495
|
- name: GO_PROFILE
|
|
@@ -490,7 +497,7 @@ spec:
|
|
|
490
497
|
- name: GO_DUCK_SERVER_REST_PORT
|
|
491
498
|
value: "${appPort}"
|
|
492
499
|
- name: GO_DUCK_DATASOURCE_HOST
|
|
493
|
-
value: postgres.${
|
|
500
|
+
value: postgres.${k8sAppName}-postgres-k8s.svc.cluster.local
|
|
494
501
|
- name: GO_DUCK_DATASOURCE_USERNAME
|
|
495
502
|
value: "${config.datasource?.username || 'postgres'}"
|
|
496
503
|
- name: GO_DUCK_DATASOURCE_PASSWORD
|
|
@@ -502,31 +509,31 @@ spec:
|
|
|
502
509
|
- name: GO_DUCK_DATASOURCE_MONGODB_ENABLED
|
|
503
510
|
value: "${config.datasource?.mongodb?.enabled ? 'true' : 'false'}"
|
|
504
511
|
- name: GO_DUCK_DATASOURCE_MONGODB_URI
|
|
505
|
-
value: mongodb://mongodb.${
|
|
512
|
+
value: mongodb://mongodb.${k8sAppName}-mongodb-k8s.svc.cluster.local:27017
|
|
506
513
|
- name: GO_DUCK_DATASOURCE_MONGODB_DATABASE
|
|
507
514
|
value: "${config.datasource?.mongodb?.database || 'goduck_document_store'}"
|
|
508
515
|
- name: GO_DUCK_CACHE_REDIS_HOST
|
|
509
|
-
value: redis.${
|
|
516
|
+
value: redis.${k8sAppName}-redis-k8s.svc.cluster.local:6379
|
|
510
517
|
- name: GO_DUCK_CACHE_REDIS_PASSWORD
|
|
511
518
|
value: "${config.cache?.redis?.password || ''}"
|
|
512
519
|
- name: GO_DUCK_MESSAGING_MQTT_BROKER
|
|
513
|
-
value: tcp://mosquitto.${
|
|
520
|
+
value: tcp://mosquitto.${k8sAppName}-mosquitto-k8s.svc.cluster.local:1883
|
|
514
521
|
- name: GO_DUCK_MESSAGING_MQTT_USERNAME
|
|
515
522
|
value: "${config.messaging?.mqtt?.username || 'dev_user'}"
|
|
516
523
|
- name: GO_DUCK_MESSAGING_MQTT_PASSWORD
|
|
517
524
|
value: "${config.messaging?.mqtt?.password || 'dev_password'}"
|
|
518
525
|
- name: GO_DUCK_MESSAGING_NATS_URL
|
|
519
|
-
value: nats://nats.${
|
|
526
|
+
value: nats://nats.${k8sAppName}-nats-k8s.svc.cluster.local:4222
|
|
520
527
|
- name: GO_DUCK_TELEMETRY_OTEL_ENDPOINT
|
|
521
|
-
value: otel-collector.${
|
|
528
|
+
value: otel-collector.${k8sAppName}-otel-collector-k8s.svc.cluster.local:4317
|
|
522
529
|
- name: GO_DUCK_ELASTICSEARCH_ADDRESSES
|
|
523
|
-
value: http://elasticsearch.${
|
|
530
|
+
value: http://elasticsearch.${k8sAppName}-elasticsearch-k8s.svc.cluster.local:9200
|
|
524
531
|
- name: GO_DUCK_ELASTICSEARCH_USERNAME
|
|
525
532
|
value: "${config.elasticsearch?.username || 'elastic'}"
|
|
526
533
|
- name: GO_DUCK_ELASTICSEARCH_PASSWORD
|
|
527
534
|
value: "${config.elasticsearch?.password || 'changeme'}"
|
|
528
535
|
- name: GO_DUCK_SECURITY_KEYCLOAK_HOST
|
|
529
|
-
value: http://keycloak.${
|
|
536
|
+
value: http://keycloak.${k8sAppName}-keycloak-k8s.svc.cluster.local:8080
|
|
530
537
|
- name: GO_DUCK_SECURITY_KEYCLOAK_REALM
|
|
531
538
|
value: "${config.security?.['keycloak-realm'] || 'go-duck-realm'}"
|
|
532
539
|
- name: GO_DUCK_SECURITY_KEYCLOAK_APP_CLIENT_ID
|
|
@@ -536,7 +543,7 @@ spec:
|
|
|
536
543
|
- name: GO_DUCK_SECURITY_KEYCLOAK_SERVICE_SECRET
|
|
537
544
|
value: "${config.security?.['keycloak-service-secret'] || 'service-secret-123'}"
|
|
538
545
|
- name: GO_DUCK_STORAGE_MINIO_ENDPOINT
|
|
539
|
-
value: "http://minio.${
|
|
546
|
+
value: "http://minio.${k8sAppName}-minio-k8s.svc.cluster.local:9000"
|
|
540
547
|
- name: GO_DUCK_STORAGE_MINIO_ACCESS_KEY
|
|
541
548
|
value: "${config.storage?.minio?.['access-key'] || 'minioadmin'}"
|
|
542
549
|
- name: GO_DUCK_STORAGE_MINIO_SECRET_KEY
|
|
@@ -550,7 +557,7 @@ spec:
|
|
|
550
557
|
apiVersion: v1
|
|
551
558
|
kind: Service
|
|
552
559
|
metadata:
|
|
553
|
-
name: ${
|
|
560
|
+
name: ${k8sAppName}
|
|
554
561
|
spec:
|
|
555
562
|
type: NodePort
|
|
556
563
|
ports:
|
|
@@ -563,7 +570,7 @@ spec:
|
|
|
563
570
|
targetPort: grpc
|
|
564
571
|
nodePort: 30090
|
|
565
572
|
selector:
|
|
566
|
-
app: ${
|
|
573
|
+
app: ${k8sAppName}
|
|
567
574
|
`;
|
|
568
575
|
|
|
569
576
|
const k8sPostgresYaml = `apiVersion: v1
|
|
@@ -1001,7 +1008,7 @@ spec:
|
|
|
1001
1008
|
await fs.writeFile(path.join(k8sDir, 'mosquitto.conf'), mosquittoConf);
|
|
1002
1009
|
// Namespace calculation: unique namespace per service
|
|
1003
1010
|
const applyNs = (yamlString, serviceName) => {
|
|
1004
|
-
const nsName = `${
|
|
1011
|
+
const nsName = `${k8sAppName}-${serviceName}-k8s`;
|
|
1005
1012
|
const nsBlock = `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${nsName}\n---\n`;
|
|
1006
1013
|
return nsBlock + yamlString.replace(/^metadata:$/gm, `metadata:\n namespace: ${nsName}`);
|
|
1007
1014
|
};
|
|
@@ -1012,7 +1019,7 @@ spec:
|
|
|
1012
1019
|
// We conditionally add minio if storage is enabled (or any blob provider), but for safety we'll just generate its namespace always.
|
|
1013
1020
|
servicesList.push('minio');
|
|
1014
1021
|
|
|
1015
|
-
const k8sNamespaceYaml = servicesList.map(svc => `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${
|
|
1022
|
+
const k8sNamespaceYaml = servicesList.map(svc => `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${k8sAppName}-${svc}-k8s`).join('\n---\n');
|
|
1016
1023
|
|
|
1017
1024
|
await fs.writeFile(path.join(k8sDir, 'namespace.yaml'), k8sNamespaceYaml);
|
|
1018
1025
|
|
package/generators/logger.js
CHANGED
|
@@ -10,8 +10,10 @@ export const generateLoggerCode = async (config, outputDir) => {
|
|
|
10
10
|
package logger
|
|
11
11
|
|
|
12
12
|
import (
|
|
13
|
+
"io"
|
|
13
14
|
"log"
|
|
14
15
|
"os"
|
|
16
|
+
"sync"
|
|
15
17
|
"{{app_name}}/config"
|
|
16
18
|
|
|
17
19
|
"github.com/DataDog/datadog-go/statsd"
|
|
@@ -19,8 +21,52 @@ import (
|
|
|
19
21
|
|
|
20
22
|
var (
|
|
21
23
|
Statsd *statsd.Client
|
|
24
|
+
GlobalLogBroker = NewLogBroker()
|
|
22
25
|
)
|
|
23
26
|
|
|
27
|
+
// LogBroker broadcasts log messages to SSE clients
|
|
28
|
+
type LogBroker struct {
|
|
29
|
+
clients map[chan []byte]bool
|
|
30
|
+
mu sync.RWMutex
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func NewLogBroker() *LogBroker {
|
|
34
|
+
return &LogBroker{
|
|
35
|
+
clients: make(map[chan []byte]bool),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func (b *LogBroker) AddClient(client chan []byte) {
|
|
40
|
+
b.mu.Lock()
|
|
41
|
+
defer b.mu.Unlock()
|
|
42
|
+
b.clients[client] = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func (b *LogBroker) RemoveClient(client chan []byte) {
|
|
46
|
+
b.mu.Lock()
|
|
47
|
+
defer b.mu.Unlock()
|
|
48
|
+
delete(b.clients, client)
|
|
49
|
+
close(client)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Write implements io.Writer
|
|
53
|
+
func (b *LogBroker) Write(p []byte) (n int, err error) {
|
|
54
|
+
b.mu.RLock()
|
|
55
|
+
defer b.mu.RUnlock()
|
|
56
|
+
|
|
57
|
+
msg := make([]byte, len(p))
|
|
58
|
+
copy(msg, p)
|
|
59
|
+
|
|
60
|
+
for client := range b.clients {
|
|
61
|
+
select {
|
|
62
|
+
case client <- msg:
|
|
63
|
+
default:
|
|
64
|
+
// Client channel full, drop to avoid blocking
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return len(p), nil
|
|
68
|
+
}
|
|
69
|
+
|
|
24
70
|
// InitLogger initializes the application logging and monitoring
|
|
25
71
|
func InitLogger(cfg *config.Config) {
|
|
26
72
|
if cfg.GoDuck.Logging.Datadog.Enabled {
|
|
@@ -42,7 +88,10 @@ func InitLogger(cfg *config.Config) {
|
|
|
42
88
|
|
|
43
89
|
// Set standard logger output
|
|
44
90
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
45
|
-
|
|
91
|
+
|
|
92
|
+
// Create a MultiWriter to duplicate logs to both os.Stdout and the GlobalLogBroker
|
|
93
|
+
mw := io.MultiWriter(os.Stdout, GlobalLogBroker)
|
|
94
|
+
log.SetOutput(mw)
|
|
46
95
|
}
|
|
47
96
|
|
|
48
97
|
// Info logs information messages
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export const generateMQTTTopicsJSON = async (config, entities, outputDir) => {
|
|
6
|
+
const docsDir = path.join(outputDir, 'docs');
|
|
7
|
+
await fs.ensureDir(docsDir);
|
|
8
|
+
|
|
9
|
+
const jsonFilePath = path.join(docsDir, 'mqtt_topics.json5');
|
|
10
|
+
const needle = '// needle-mqtt-topics-add-below';
|
|
11
|
+
|
|
12
|
+
let content = '';
|
|
13
|
+
if (await fs.pathExists(jsonFilePath)) {
|
|
14
|
+
content = await fs.readFile(jsonFilePath, 'utf8');
|
|
15
|
+
} else {
|
|
16
|
+
content = `{
|
|
17
|
+
"topics": {
|
|
18
|
+
// Custom topics can be added here manually.
|
|
19
|
+
|
|
20
|
+
${needle}
|
|
21
|
+
}
|
|
22
|
+
}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const topicPrefix = config.messaging?.mqtt?.['topic-prefix'] || 'go-duck/events';
|
|
26
|
+
let newEntries = '';
|
|
27
|
+
|
|
28
|
+
for (const entity of entities) {
|
|
29
|
+
const entityName = entity.name.toLowerCase();
|
|
30
|
+
const baseTopic = `${topicPrefix}/${entityName}`;
|
|
31
|
+
|
|
32
|
+
const createdTopic = `"${baseTopic}/created"`;
|
|
33
|
+
if (!content.includes(createdTopic)) {
|
|
34
|
+
newEntries += ` // Auto-generated topics for ${entity.name}\n`;
|
|
35
|
+
newEntries += ` ${createdTopic}: "Triggered when a new ${entity.name} is created.",\n`;
|
|
36
|
+
newEntries += ` "${baseTopic}/updated": "Triggered when an existing ${entity.name} is updated.",\n`;
|
|
37
|
+
newEntries += ` "${baseTopic}/deleted": "Triggered when a ${entity.name} is deleted.",\n\n`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (newEntries.length > 0) {
|
|
42
|
+
if (content.includes(needle)) {
|
|
43
|
+
content = content.replace(needle, newEntries + ` ${needle}`);
|
|
44
|
+
} else {
|
|
45
|
+
// Fallback if needle is missing somehow
|
|
46
|
+
content = content.replace(/}\s*}$/, `,\n${newEntries}\n }\n}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await fs.writeFile(jsonFilePath, content);
|
|
50
|
+
console.log(chalk.gray(' Updated MQTT Topics Dictionary: mqtt_topics.json5'));
|
|
51
|
+
} else {
|
|
52
|
+
console.log(chalk.gray(' MQTT Topics Dictionary up to date.'));
|
|
53
|
+
}
|
|
54
|
+
};
|
package/generators/postman.js
CHANGED
|
@@ -364,8 +364,9 @@ export const generatePostmanCollection = async (config, entities, outputDir, ope
|
|
|
364
364
|
else if (f.type === 'Float' || f.type === 'BigDecimal') obj[f.name] = 99.99;
|
|
365
365
|
else if (f.type === 'Boolean') obj[f.name] = true;
|
|
366
366
|
else if (f.type === 'LocalDate') obj[f.name] = "2024-01-01";
|
|
367
|
-
else if (f.type === 'Instant') obj[f.name] = "2024-01-01T12:00:00Z";
|
|
367
|
+
else if (f.type === 'Instant' || f.type === 'DateTime' || f.type === 'Datetime') obj[f.name] = "2024-01-01T12:00:00Z";
|
|
368
368
|
else if (f.type === 'JSON' || f.type === 'JSONB') obj[f.name] = {"attribute": "example_value"};
|
|
369
|
+
else if (f.isEnum) obj[f.name] = "ACTIVE"; // Safe fallback for enums
|
|
369
370
|
else obj[f.name] = "test";
|
|
370
371
|
}
|
|
371
372
|
return JSON.stringify(obj, null, 2);
|
package/generators/swagger.js
CHANGED
|
@@ -17,7 +17,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
17
17
|
description: `Generated documentation for ${config.name} microservice`
|
|
18
18
|
},
|
|
19
19
|
servers: [
|
|
20
|
-
{ url:
|
|
20
|
+
{ url: "/", description: "Current Host" }
|
|
21
21
|
],
|
|
22
22
|
paths: {},
|
|
23
23
|
components: {
|
|
@@ -84,7 +84,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
84
84
|
properties: {
|
|
85
85
|
id: { type: 'integer' },
|
|
86
86
|
...entity.fields.reduce((acc, field) => {
|
|
87
|
-
acc[field.name] =
|
|
87
|
+
acc[field.name] = getSwaggerFieldSchema(field.type);
|
|
88
88
|
return acc;
|
|
89
89
|
}, {}),
|
|
90
90
|
createdAt: { type: 'string', format: 'date-time' },
|
|
@@ -318,19 +318,19 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
|
|
|
318
318
|
console.log(chalk.gray(' Generated Swagger Documentation: swagger.json'));
|
|
319
319
|
};
|
|
320
320
|
|
|
321
|
-
const
|
|
322
|
-
const
|
|
323
|
-
'String': 'string',
|
|
324
|
-
'Integer': 'integer',
|
|
325
|
-
'Float': 'number',
|
|
326
|
-
'Boolean': 'boolean',
|
|
327
|
-
'Long': 'integer',
|
|
328
|
-
'BigDecimal': 'number',
|
|
329
|
-
'LocalDate': 'string',
|
|
330
|
-
'Instant': 'string',
|
|
331
|
-
'JSON': 'object',
|
|
332
|
-
'JSONB': 'object',
|
|
333
|
-
'Text': 'string'
|
|
321
|
+
const getSwaggerFieldSchema = (type) => {
|
|
322
|
+
const schemas = {
|
|
323
|
+
'String': { type: 'string' },
|
|
324
|
+
'Integer': { type: 'integer', format: 'int32' },
|
|
325
|
+
'Float': { type: 'number', format: 'double' },
|
|
326
|
+
'Boolean': { type: 'boolean' },
|
|
327
|
+
'Long': { type: 'integer', format: 'int64' },
|
|
328
|
+
'BigDecimal': { type: 'number', format: 'double' },
|
|
329
|
+
'LocalDate': { type: 'string', format: 'date' },
|
|
330
|
+
'Instant': { type: 'string', format: 'date-time' },
|
|
331
|
+
'JSON': { type: 'object' },
|
|
332
|
+
'JSONB': { type: 'object' },
|
|
333
|
+
'Text': { type: 'string' }
|
|
334
334
|
};
|
|
335
|
-
return
|
|
335
|
+
return schemas[type] || { type: 'string' };
|
|
336
336
|
};
|
package/generators/telemetry.js
CHANGED
|
@@ -105,7 +105,171 @@ service:
|
|
|
105
105
|
exporters: [logging, otlp]
|
|
106
106
|
`;
|
|
107
107
|
|
|
108
|
+
const metricsCollectorGo = `
|
|
109
|
+
package telemetry
|
|
110
|
+
|
|
111
|
+
import (
|
|
112
|
+
"sync"
|
|
113
|
+
"time"
|
|
114
|
+
"github.com/gin-gonic/gin"
|
|
115
|
+
"{{app_name}}/config"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
type EndpointStats struct {
|
|
119
|
+
Count int64 \`json:"count"\`
|
|
120
|
+
MeanTime float64 \`json:"mean_time_ms"\`
|
|
121
|
+
MaxTime float64 \`json:"max_time_ms"\`
|
|
122
|
+
TotalTime float64 \`json:"-"\`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type StatusStats struct {
|
|
126
|
+
Count int64 \`json:"count"\`
|
|
127
|
+
MeanTime float64 \`json:"mean_time_ms"\`
|
|
128
|
+
MaxTime float64 \`json:"max_time_ms"\`
|
|
129
|
+
TotalTime float64 \`json:"-"\`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type AppMetrics struct {
|
|
133
|
+
mu sync.RWMutex
|
|
134
|
+
StartTime time.Time
|
|
135
|
+
Endpoints map[string]*EndpointStats
|
|
136
|
+
StatusCodes map[int]*StatusStats
|
|
137
|
+
FailedCalls int64
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
var globalMetrics = &AppMetrics{
|
|
141
|
+
StartTime: time.Now(),
|
|
142
|
+
Endpoints: make(map[string]*EndpointStats),
|
|
143
|
+
StatusCodes: make(map[int]*StatusStats),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func MetricsTrackingMiddleware(cfg *config.Config) gin.HandlerFunc {
|
|
147
|
+
return func(c *gin.Context) {
|
|
148
|
+
start := time.Now()
|
|
149
|
+
c.Next()
|
|
150
|
+
duration := float64(time.Since(start).Milliseconds())
|
|
151
|
+
|
|
152
|
+
status := c.Writer.Status()
|
|
153
|
+
method := c.Request.Method
|
|
154
|
+
path := c.FullPath()
|
|
155
|
+
if path == "" {
|
|
156
|
+
path = "unknown"
|
|
157
|
+
}
|
|
158
|
+
endpointKey := method + " " + path
|
|
159
|
+
|
|
160
|
+
globalMetrics.mu.Lock()
|
|
161
|
+
defer globalMetrics.mu.Unlock()
|
|
162
|
+
|
|
163
|
+
// Status Code tracking
|
|
164
|
+
if _, exists := globalMetrics.StatusCodes[status]; !exists {
|
|
165
|
+
globalMetrics.StatusCodes[status] = &StatusStats{}
|
|
166
|
+
}
|
|
167
|
+
sStat := globalMetrics.StatusCodes[status]
|
|
168
|
+
sStat.Count++
|
|
169
|
+
sStat.TotalTime += duration
|
|
170
|
+
sStat.MeanTime = sStat.TotalTime / float64(sStat.Count)
|
|
171
|
+
if duration > sStat.MaxTime {
|
|
172
|
+
sStat.MaxTime = duration
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if status >= 400 {
|
|
176
|
+
globalMetrics.FailedCalls++
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Endpoint tracking
|
|
180
|
+
if _, exists := globalMetrics.Endpoints[endpointKey]; !exists {
|
|
181
|
+
globalMetrics.Endpoints[endpointKey] = &EndpointStats{}
|
|
182
|
+
}
|
|
183
|
+
eStat := globalMetrics.Endpoints[endpointKey]
|
|
184
|
+
eStat.Count++
|
|
185
|
+
eStat.TotalTime += duration
|
|
186
|
+
eStat.MeanTime = eStat.TotalTime / float64(eStat.Count)
|
|
187
|
+
if duration > eStat.MaxTime {
|
|
188
|
+
eStat.MaxTime = duration
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
func GetGlobalMetrics() *AppMetrics {
|
|
194
|
+
return globalMetrics
|
|
195
|
+
}
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
const systemMetricsGo = `
|
|
199
|
+
package telemetry
|
|
200
|
+
|
|
201
|
+
import (
|
|
202
|
+
"runtime"
|
|
203
|
+
"time"
|
|
204
|
+
"github.com/shirou/gopsutil/v3/cpu"
|
|
205
|
+
"github.com/shirou/gopsutil/v3/process"
|
|
206
|
+
"os"
|
|
207
|
+
"fmt"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
type SystemMetrics struct {
|
|
211
|
+
Uptime string \`json:"uptime"\`
|
|
212
|
+
StartTime string \`json:"start_time"\`
|
|
213
|
+
ProcessCPUUsage float64 \`json:"process_cpu_usage"\`
|
|
214
|
+
SystemCPUUsage float64 \`json:"system_cpu_usage"\`
|
|
215
|
+
SystemCPUCount int \`json:"system_cpu_count"\`
|
|
216
|
+
ProcessFilesOpen int32 \`json:"process_files_open"\`
|
|
217
|
+
|
|
218
|
+
HeapAllocMB uint64 \`json:"heap_alloc_mb"\`
|
|
219
|
+
HeapSysMB uint64 \`json:"heap_sys_mb"\`
|
|
220
|
+
NumGC uint32 \`json:"num_gc"\`
|
|
221
|
+
PauseTotalMs uint64 \`json:"gc_pause_total_ms"\`
|
|
222
|
+
Goroutines int \`json:"goroutines"\`
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func CollectSystemMetrics() SystemMetrics {
|
|
226
|
+
var m runtime.MemStats
|
|
227
|
+
runtime.ReadMemStats(&m)
|
|
228
|
+
|
|
229
|
+
cpuPercent, _ := cpu.Percent(0, false)
|
|
230
|
+
sysCPU := 0.0
|
|
231
|
+
if len(cpuPercent) > 0 {
|
|
232
|
+
sysCPU = cpuPercent[0]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
p, err := process.NewProcess(int32(os.Getpid()))
|
|
236
|
+
procCPU := 0.0
|
|
237
|
+
var openFiles int32 = 0
|
|
238
|
+
if err == nil {
|
|
239
|
+
procCPU, _ = p.CPUPercent()
|
|
240
|
+
files, _ := p.OpenFiles()
|
|
241
|
+
openFiles = int32(len(files))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
appMetrics := GetGlobalMetrics()
|
|
245
|
+
uptime := time.Since(appMetrics.StartTime)
|
|
246
|
+
|
|
247
|
+
// Convert uptime duration to readable string like "10 days 4 hours..."
|
|
248
|
+
days := int(uptime.Hours()) / 24
|
|
249
|
+
hours := int(uptime.Hours()) % 24
|
|
250
|
+
mins := int(uptime.Minutes()) % 60
|
|
251
|
+
secs := int(uptime.Seconds()) % 60
|
|
252
|
+
uptimeStr := fmt.Sprintf("%d days %d hours %d minutes %d seconds", days, hours, mins, secs)
|
|
253
|
+
|
|
254
|
+
return SystemMetrics{
|
|
255
|
+
Uptime: uptimeStr,
|
|
256
|
+
StartTime: appMetrics.StartTime.Format(time.RFC1123),
|
|
257
|
+
ProcessCPUUsage: procCPU,
|
|
258
|
+
SystemCPUUsage: sysCPU,
|
|
259
|
+
SystemCPUCount: runtime.NumCPU(),
|
|
260
|
+
ProcessFilesOpen: openFiles,
|
|
261
|
+
HeapAllocMB: m.HeapAlloc / 1024 / 1024,
|
|
262
|
+
HeapSysMB: m.HeapSys / 1024 / 1024,
|
|
263
|
+
NumGC: m.NumGC,
|
|
264
|
+
PauseTotalMs: m.PauseTotalNs / 1000000,
|
|
265
|
+
Goroutines: runtime.NumGoroutine(),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
`;
|
|
269
|
+
|
|
108
270
|
await fs.writeFile(path.join(telemetryDir, 'otel.go'), otelGo.replace(/{{app_name}}/g, config.name));
|
|
271
|
+
await fs.writeFile(path.join(telemetryDir, 'metrics_collector.go'), metricsCollectorGo.replace(/{{app_name}}/g, config.name));
|
|
272
|
+
await fs.writeFile(path.join(telemetryDir, 'system_metrics.go'), systemMetricsGo.replace(/{{app_name}}/g, config.name));
|
|
109
273
|
await fs.writeFile(path.join(k8sDir, 'otel-collector.yml'), otelCollectorK8s.replace(/{{app_name}}/g, config.name));
|
|
110
274
|
|
|
111
275
|
console.log(chalk.gray(' Generated OpenTelemetry Telemetry Package & K8s Config'));
|
package/index.js
CHANGED
|
@@ -31,6 +31,7 @@ import { generateWebSocketCode } from './generators/websocket.js';
|
|
|
31
31
|
import { generateConfigLoader } from './generators/config.js';
|
|
32
32
|
import { generateLoggerCode } from './generators/logger.js';
|
|
33
33
|
import { generateMQTTCode } from './generators/mqtt.js';
|
|
34
|
+
import { generateMQTTTopicsJSON } from './generators/mqtt-topics.js';
|
|
34
35
|
import { generateCacheCode } from './generators/cache.js';
|
|
35
36
|
import { generateResilienceCode } from './generators/resilience.js';
|
|
36
37
|
import { generateTelemetryCode } from './generators/telemetry.js';
|
package/package.json
CHANGED
|
@@ -68,11 +68,15 @@
|
|
|
68
68
|
gcs: { enabled: false, bucket: "bucket-name", credentials-file: "keys.json" }
|
|
69
69
|
minio: { enabled: true, bucket: "dev", endpoint: "localhost:9000" }
|
|
70
70
|
|
|
71
|
-
# ---
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
# --- Telemetry & Metrics ---
|
|
72
|
+
telemetry:
|
|
73
|
+
otel:
|
|
74
|
+
enabled: true
|
|
75
|
+
endpoint: "localhost:4317"
|
|
76
|
+
metrics:
|
|
77
|
+
prometheus-enabled: true
|
|
78
|
+
stream-enabled: true
|
|
79
|
+
stream-interval: "1s"
|
|
76
80
|
</div>
|
|
77
81
|
|
|
78
82
|
<!-- PARAMETER REFERENCE -->
|
|
@@ -203,6 +207,43 @@
|
|
|
203
207
|
</div>
|
|
204
208
|
</section>
|
|
205
209
|
|
|
210
|
+
<!-- SECTION: TELEMETRY -->
|
|
211
|
+
<section class="mb-16">
|
|
212
|
+
<h3 class="text-xl font-bold text-rose-700 mb-4 flex items-center">
|
|
213
|
+
<span class="w-2 h-2 rounded-full bg-rose-600 mr-2"></span>
|
|
214
|
+
5. Telemetry & Metrics (telemetry)
|
|
215
|
+
</h3>
|
|
216
|
+
<p class="text-sm text-slate-500 mb-4 italic leading-relaxed">Integrated observability stack powering OpenTelemetry traces, Prometheus metrics, and Server-Sent Events (SSE) streaming dashboards.</p>
|
|
217
|
+
<div class="overflow-hidden border border-slate-200 rounded-2xl shadow-sm">
|
|
218
|
+
<table class="w-full text-left border-collapse">
|
|
219
|
+
<thead class="bg-slate-50 border-b border-slate-200">
|
|
220
|
+
<tr>
|
|
221
|
+
<th class="p-4 text-xs font-black text-slate-500 uppercase tracking-widest">Parameter</th>
|
|
222
|
+
<th class="p-4 text-xs font-black text-slate-500 uppercase tracking-widest">Purpose</th>
|
|
223
|
+
</tr>
|
|
224
|
+
</thead>
|
|
225
|
+
<tbody class="text-sm text-slate-600">
|
|
226
|
+
<tr class="border-b border-slate-100 italic">
|
|
227
|
+
<td class="p-4 font-mono text-rose-600">otel.enabled</td>
|
|
228
|
+
<td class="p-4 text-slate-600">Enables OpenTelemetry context propagation across Gin routers and GORM database drivers.</td>
|
|
229
|
+
</tr>
|
|
230
|
+
<tr class="border-b border-slate-100 italic font-medium">
|
|
231
|
+
<td class="p-4 font-mono text-rose-600">metrics.prometheus-enabled</td>
|
|
232
|
+
<td class="p-4 text-slate-600">Exposes a <code>/metrics</code> endpoint for Kubernetes Horizontal Pod Autoscaling (HPA) and Prometheus scraping.</td>
|
|
233
|
+
</tr>
|
|
234
|
+
<tr class="border-b border-slate-100 italic">
|
|
235
|
+
<td class="p-4 font-mono text-rose-600">metrics.stream-enabled</td>
|
|
236
|
+
<td class="p-4 text-slate-600">Exposes a <code>/api/system/stream</code> Server-Sent Events endpoint streaming CPU, Memory, and Load percentages.</td>
|
|
237
|
+
</tr>
|
|
238
|
+
<tr>
|
|
239
|
+
<td class="p-4 font-mono text-rose-600">metrics.stream-interval</td>
|
|
240
|
+
<td class="p-4 text-slate-600">The refresh rate of the SSE stream (e.g., "1s", "500ms").</td>
|
|
241
|
+
</tr>
|
|
242
|
+
</tbody>
|
|
243
|
+
</table>
|
|
244
|
+
</div>
|
|
245
|
+
</section>
|
|
246
|
+
|
|
206
247
|
<!-- RESILIENCE & SEARCH -->
|
|
207
248
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-20">
|
|
208
249
|
<div class="p-8 bg-rose-50 rounded-[2.5rem] border border-rose-100">
|
|
@@ -29,6 +29,12 @@ messaging.Publish("events/users", event)</code></pre>
|
|
|
29
29
|
mosquitto_sub -h localhost -p {{mqttPort}} -t "audit/logs/#" -v</code></pre>
|
|
30
30
|
</section>
|
|
31
31
|
|
|
32
|
+
<section class="mb-10">
|
|
33
|
+
<h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">Interactive Swagger Console</h2>
|
|
34
|
+
<p class="mb-4">GO-DUCK's automatically generated Swagger UI features a built-in <strong>Interactive MQTT Topics Dictionary</strong>. By connecting via WebSockets, it allows authenticated users to dynamically <strong>SUBSCRIBE</strong> to event streams or <strong>PUBLISH</strong> payloads directly from the documentation interface!</p>
|
|
35
|
+
<p class="mb-4 text-sm text-gray-600 italic">Note: Ensure your Mosquitto broker is configured to accept WebSocket connections (default port 9001) for this feature to work.</p>
|
|
36
|
+
</section>
|
|
37
|
+
|
|
32
38
|
<section class="mb-10">
|
|
33
39
|
<h2 class="text-2xl font-bold text-gray-800 mb-4 border-b pb-2">External Resources</h2>
|
|
34
40
|
<ul class="space-y-2">
|