go-duck-cli 1.3.371 → 1.4.5

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 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
@@ -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")
@@ -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: ${appName}
478
+ name: ${k8sAppName}
472
479
  labels:
473
- app: ${appName}
480
+ app: ${k8sAppName}
474
481
  spec:
475
482
  replicas: 1
476
483
  selector:
477
484
  matchLabels:
478
- app: ${appName}
485
+ app: ${k8sAppName}
479
486
  template:
480
487
  metadata:
481
488
  labels:
482
- app: ${appName}
489
+ app: ${k8sAppName}
483
490
  spec:
484
491
  containers:
485
- - name: ${appName}
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.${shortName}-postgres-k8s.svc.cluster.local
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.${shortName}-mongodb-k8s.svc.cluster.local:27017
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.${shortName}-redis-k8s.svc.cluster.local:6379
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.${shortName}-mosquitto-k8s.svc.cluster.local:1883
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.${shortName}-nats-k8s.svc.cluster.local:4222
526
+ value: nats://nats.${k8sAppName}-nats-k8s.svc.cluster.local:4222
520
527
  - name: GO_DUCK_TELEMETRY_OTEL_ENDPOINT
521
- value: otel-collector.${shortName}-otel-collector-k8s.svc.cluster.local:4317
528
+ value: otel-collector.${k8sAppName}-otel-collector-k8s.svc.cluster.local:4317
522
529
  - name: GO_DUCK_ELASTICSEARCH_ADDRESSES
523
- value: http://elasticsearch.${shortName}-elasticsearch-k8s.svc.cluster.local:9200
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.${shortName}-keycloak-k8s.svc.cluster.local:8080
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.${shortName}-minio-k8s.svc.cluster.local:9000"
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: ${appName}
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: ${appName}
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 = `${shortName}-${serviceName}-k8s`;
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: ${shortName}-${svc}-k8s`).join('\n---\n');
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
 
@@ -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
- log.SetOutput(os.Stdout)
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
+ };
@@ -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);
@@ -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: `http://localhost:${config.server?.rest?.port || 8080}`, description: 'Local Development Server' }
20
+ { url: "/", description: "Current Host" }
21
21
  ],
22
22
  paths: {},
23
23
  components: {
@@ -51,7 +51,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
51
51
  };
52
52
 
53
53
  const commonHeaders = [
54
- { name: 'X-Tenant-ID', in: 'header', required: true, schema: { type: 'string', default: 'default' }, description: 'Multi-tenancy context identifier' }
54
+ { name: 'X-Tenant-ID', in: 'header', required: false, schema: { type: 'string', default: 'default' }, description: 'Multi-tenancy context identifier' }
55
55
  ];
56
56
 
57
57
  const isOpen = (entityName, action) => {
@@ -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] = { type: mapToSwaggerType(field.type) };
87
+ acc[field.name] = getSwaggerFieldSchema(field.type);
88
88
  return acc;
89
89
  }, {}),
90
90
  createdAt: { type: 'string', format: 'date-time' },
@@ -273,7 +273,7 @@ export const generateSwaggerDocs = async (config, entities, outputDir, openEntit
273
273
  }
274
274
  },
275
275
  parameters: [
276
- { name: 'X-Tenant-ID', in: 'header', required: true, schema: { type: 'string', default: 'master_internal' }, description: 'SuperAdmin internal master bypass token' }
276
+ { name: 'X-Tenant-ID', in: 'header', required: false, schema: { type: 'string', default: 'master_internal' }, description: 'SuperAdmin internal master bypass token' }
277
277
  ],
278
278
  responses: {
279
279
  200: { description: 'Success' },
@@ -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 mapToSwaggerType = (type) => {
322
- const types = {
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 types[type] || 'string';
335
+ return schemas[type] || { type: 'string' };
336
336
  };
@@ -105,7 +105,174 @@ service:
105
105
  exporters: [logging, otlp]
106
106
  `;
107
107
 
108
+ const metricsCollectorGo = `
109
+ package telemetry
110
+
111
+ import (
112
+ "runtime"
113
+ "sync"
114
+ "time"
115
+ "github.com/gin-gonic/gin"
116
+ "net/http"
117
+ "{{app_name}}/config"
118
+ )
119
+
120
+ type EndpointStats struct {
121
+ Count int64 \`json:"count"\`
122
+ MeanTime float64 \`json:"mean_time_ms"\`
123
+ MaxTime float64 \`json:"max_time_ms"\`
124
+ TotalTime float64 \`json:"-"\`
125
+ }
126
+
127
+ type StatusStats struct {
128
+ Count int64 \`json:"count"\`
129
+ MeanTime float64 \`json:"mean_time_ms"\`
130
+ MaxTime float64 \`json:"max_time_ms"\`
131
+ TotalTime float64 \`json:"-"\`
132
+ }
133
+
134
+ type AppMetrics struct {
135
+ mu sync.RWMutex
136
+ StartTime time.Time
137
+ Endpoints map[string]*EndpointStats
138
+ StatusCodes map[int]*StatusStats
139
+ FailedCalls int64
140
+ }
141
+
142
+ var globalMetrics = &AppMetrics{
143
+ StartTime: time.Now(),
144
+ Endpoints: make(map[string]*EndpointStats),
145
+ StatusCodes: make(map[int]*StatusStats),
146
+ }
147
+
148
+ func MetricsTrackingMiddleware(cfg *config.Config) gin.HandlerFunc {
149
+ return func(c *gin.Context) {
150
+ start := time.Now()
151
+ c.Next()
152
+ duration := float64(time.Since(start).Milliseconds())
153
+
154
+ status := c.Writer.Status()
155
+ method := c.Request.Method
156
+ path := c.FullPath()
157
+ if path == "" {
158
+ path = "unknown"
159
+ }
160
+ endpointKey := method + " " + path
161
+
162
+ globalMetrics.mu.Lock()
163
+ defer globalMetrics.mu.Unlock()
164
+
165
+ // Status Code tracking
166
+ if _, exists := globalMetrics.StatusCodes[status]; !exists {
167
+ globalMetrics.StatusCodes[status] = &StatusStats{}
168
+ }
169
+ sStat := globalMetrics.StatusCodes[status]
170
+ sStat.Count++
171
+ sStat.TotalTime += duration
172
+ sStat.MeanTime = sStat.TotalTime / float64(sStat.Count)
173
+ if duration > sStat.MaxTime {
174
+ sStat.MaxTime = duration
175
+ }
176
+
177
+ if status >= 400 {
178
+ globalMetrics.FailedCalls++
179
+ }
180
+
181
+ // Endpoint tracking
182
+ if _, exists := globalMetrics.Endpoints[endpointKey]; !exists {
183
+ globalMetrics.Endpoints[endpointKey] = &EndpointStats{}
184
+ }
185
+ eStat := globalMetrics.Endpoints[endpointKey]
186
+ eStat.Count++
187
+ eStat.TotalTime += duration
188
+ eStat.MeanTime = eStat.TotalTime / float64(eStat.Count)
189
+ if duration > eStat.MaxTime {
190
+ eStat.MaxTime = duration
191
+ }
192
+ }
193
+ }
194
+
195
+ func GetGlobalMetrics() *AppMetrics {
196
+ return globalMetrics
197
+ }
198
+ `;
199
+
200
+ const systemMetricsGo = `
201
+ package telemetry
202
+
203
+ import (
204
+ "runtime"
205
+ "time"
206
+ "github.com/shirou/gopsutil/v3/cpu"
207
+ "github.com/shirou/gopsutil/v3/mem"
208
+ "github.com/shirou/gopsutil/v3/process"
209
+ "os"
210
+ "fmt"
211
+ )
212
+
213
+ type SystemMetrics struct {
214
+ Uptime string \`json:"uptime"\`
215
+ StartTime string \`json:"start_time"\`
216
+ ProcessCPUUsage float64 \`json:"process_cpu_usage"\`
217
+ SystemCPUUsage float64 \`json:"system_cpu_usage"\`
218
+ SystemCPUCount int \`json:"system_cpu_count"\`
219
+ ProcessFilesOpen int32 \`json:"process_files_open"\`
220
+
221
+ HeapAllocMB uint64 \`json:"heap_alloc_mb"\`
222
+ HeapSysMB uint64 \`json:"heap_sys_mb"\`
223
+ NumGC uint32 \`json:"num_gc"\`
224
+ PauseTotalMs uint64 \`json:"gc_pause_total_ms"\`
225
+ Goroutines int \`json:"goroutines"\`
226
+ }
227
+
228
+ func CollectSystemMetrics() SystemMetrics {
229
+ var m runtime.MemStats
230
+ runtime.ReadMemStats(&m)
231
+
232
+ cpuPercent, _ := cpu.Percent(0, false)
233
+ sysCPU := 0.0
234
+ if len(cpuPercent) > 0 {
235
+ sysCPU = cpuPercent[0]
236
+ }
237
+
238
+ p, err := process.NewProcess(int32(os.Getpid()))
239
+ procCPU := 0.0
240
+ var openFiles int32 = 0
241
+ if err == nil {
242
+ procCPU, _ = p.CPUPercent()
243
+ files, _ := p.OpenFiles()
244
+ openFiles = int32(len(files))
245
+ }
246
+
247
+ appMetrics := GetGlobalMetrics()
248
+ uptime := time.Since(appMetrics.StartTime)
249
+
250
+ // Convert uptime duration to readable string like "10 days 4 hours..."
251
+ days := int(uptime.Hours()) / 24
252
+ hours := int(uptime.Hours()) % 24
253
+ mins := int(uptime.Minutes()) % 60
254
+ secs := int(uptime.Seconds()) % 60
255
+ uptimeStr := fmt.Sprintf("%d days %d hours %d minutes %d seconds", days, hours, mins, secs)
256
+
257
+ return SystemMetrics{
258
+ Uptime: uptimeStr,
259
+ StartTime: appMetrics.StartTime.Format(time.RFC1123),
260
+ ProcessCPUUsage: procCPU,
261
+ SystemCPUUsage: sysCPU,
262
+ SystemCPUCount: runtime.NumCPU(),
263
+ ProcessFilesOpen: openFiles,
264
+ HeapAllocMB: m.HeapAlloc / 1024 / 1024,
265
+ HeapSysMB: m.HeapSys / 1024 / 1024,
266
+ NumGC: m.NumGC,
267
+ PauseTotalMs: m.PauseTotalNs / 1000000,
268
+ Goroutines: runtime.NumGoroutine(),
269
+ }
270
+ }
271
+ `;
272
+
108
273
  await fs.writeFile(path.join(telemetryDir, 'otel.go'), otelGo.replace(/{{app_name}}/g, config.name));
274
+ await fs.writeFile(path.join(telemetryDir, 'metrics_collector.go'), metricsCollectorGo.replace(/{{app_name}}/g, config.name));
275
+ await fs.writeFile(path.join(telemetryDir, 'system_metrics.go'), systemMetricsGo.replace(/{{app_name}}/g, config.name));
109
276
  await fs.writeFile(path.join(k8sDir, 'otel-collector.yml'), otelCollectorK8s.replace(/{{app_name}}/g, config.name));
110
277
 
111
278
  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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "go-duck-cli",
3
- "version": "1.3.371",
3
+ "version": "1.4.5",
4
4
  "description": "The Ultimate Evolutionary Go Microservice Scaffolder.",
5
5
  "main": "index.js",
6
6
  "type": "module",