Skip to content

Feature - OpenTelemetry Auto-Instrumentation / DuckDB Library Update #1708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: dev
Choose a base branch
from
Open
Prev Previous commit
Next Next commit
Added Node Service Otel-Auto Instrumentation
  • Loading branch information
Connell, Joseph committed May 25, 2025
commit 6b2001f2169b969fc4779a1543a7d51bc46d05b3
45 changes: 45 additions & 0 deletions deploy/docker/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Node.js dependencies
node_modules
npm-debug.log
yarn-error.log

# Logs
logs
*.log

# Build artifacts
dist
build
*.tsbuildinfo

# Environment files
.env
.env.local
.env.*.local

# IDE and editor files
.vscode/
.idea/
*.swp

# OS files
.DS_Store
Thumbs.db

# Docker-related files
.dockerignore
Dockerfile*

# Temporary files
tmp/
temp/
*.tmp
*.temp

# Test files
coverage/
*.test.js
*.spec.js
*.test.ts
*.spec.ts
jest.config.js
3 changes: 2 additions & 1 deletion deploy/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -78,9 +78,10 @@ WORKDIR /lowcoder/node-service/app/
RUN yarn --immutable
RUN yarn build

# Copy startup script
# Copy startup script and OpenTelemetry config
COPY deploy/docker/node-service/entrypoint.sh /lowcoder/node-service/entrypoint.sh
COPY deploy/docker/node-service/init.sh /lowcoder/node-service/init.sh
COPY deploy/docker/node-service/otel-config.js /lowcoder/node-service/otel-config.js
RUN chmod +x /lowcoder/node-service/*.sh

##
22 changes: 20 additions & 2 deletions deploy/docker/docker-compose-multi-otel.yaml
Original file line number Diff line number Diff line change
@@ -103,8 +103,9 @@ services:

# OpenTelemetry Related Settings
# OTEL_JAVAAGENT_ENABLED: "false"
OTEL_RESOURCE_ATTRIBUTES: "service.name=lowcoder-api-service,service.version=2.6.5,deployment.environment=production"
OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=production"
OTEL_SERVICE_NAME: "lowcoder-api-service"
OTEL_SERVICE_VERSION: "2.6.5"
OTEL_EXPORTER_OTLP_PROTOCOL: "grpc"
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-lgtm:4317"
OTEL_TRACES_EXPORTER: "otlp"
@@ -143,6 +144,23 @@ services:
LOWCODER_PUID: "9001"
LOWCODER_PGID: "9001"
LOWCODER_API_SERVICE_URL: "http://lowcoder-api-service:8080"

##
# OpenTelemetry Related Settings
#
# Uncomment OTEL_NODE_ENABLED_INSTRUMENTATIONS
# to disable OpenTelemetry Node agent
##
# OTEL_NODE_ENABLED_INSTRUMENTATIONS=""
OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=production"
OTEL_SERVICE_NAME: "lowcoder-node-service"
OTEL_SERVICE_VERSION: "2.6.5"
OTEL_EXPORTER_OTLP_PROTOCOL: "grpc"
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-lgtm:4317"
OTEL_TRACES_EXPORTER: "otlp"
OTEL_METRICS_EXPORTER: "otlp"
OTEL_LOGS_EXPORTER: "otlp"

restart: unless-stopped
depends_on:
lowcoder-api-service:
@@ -153,7 +171,7 @@ services:
interval: 3s
timeout: 5s
retries: 10

##
## Start Lowcoder web frontend
##
3 changes: 3 additions & 0 deletions deploy/docker/node-service/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -23,4 +23,7 @@ if [ "$(id -u)" -eq 0 ]; then
fi
echo

# Require OpenTelemetry configuration
export NODE_OPTIONS="-r ./otel-config.js"

exec $GOSU yarn start
139 changes: 139 additions & 0 deletions deploy/docker/node-service/otel-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* OpenTelemetry Configuration for Lowcoder Application
*
* This file sets up OpenTelemetry auto-instrumentation for traces and metrics.
*
* How to use:
* 1. Save this file as `otel-config.js` in your application's root directory.
* 2. Ensure you have the necessary OpenTelemetry packages installed:
* @opentelemetry/sdk-node
* @opentelemetry/api
* @opentelemetry/exporter-trace-otlp-http (or -grpc)
* @opentelemetry/exporter-metrics-otlp-http (or -grpc)
* @opentelemetry/resources
* @opentelemetry/semantic-conventions
* @opentelemetry/auto-instrumentations-node (CRITICAL for auto-instrumentation)
*
* Install @opentelemetry/auto-instrumentations-node if you haven't:
* `npm install @opentelemetry/auto-instrumentations-node`
* or
* `yarn add @opentelemetry/auto-instrumentations-node`
*
* 3. Start your application using the `-r` flag to preload this configuration:
* `export NODE_OPTIONS="-r ./otel-config.js"`
*
* Environment Variables for Configuration:
* - OTEL_EXPORTER_OTLP_ENDPOINT: Base URL for OTLP HTTP exporters (e.g., http://localhost:4318).
* If not set, defaults to http://localhost:4318.
* - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: Specific URL for OTLP HTTP traces exporter (e.g., http://localhost:4318/v1/traces).
* - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: Specific URL for OTLP HTTP metrics exporter (e.g., http://localhost:4318/v1/metrics).
* - OTEL_SERVICE_NAME: Name of your service (e.g., 'node-service'). Defaults to 'unknown_service:nodejs'.
* - OTEL_LOG_LEVEL: Set OpenTelemetry diagnostic logging level (e.g., 'debug', 'info', 'warn', 'error').
* - OTEL_EXPORTER_PROTOCOL: 'http/protobuf' (default) or 'grpc'.
*/

const process = require('process');
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter: OTLPTraceExporterHttp } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPTraceExporter: OTLPTraceExporterGrpc } = require('@opentelemetry/exporter-trace-otlp-grpc');
const { OTLPMetricExporter: OTLPMetricExporterHttp } = require('@opentelemetry/exporter-metrics-otlp-http');
const { OTLPMetricExporter: OTLPMetricExporterGrpc } = require('@opentelemetry/exporter-metrics-otlp-grpc');
const { BatchSpanProcessor } = require('@opentelemetry/sdk-trace-node'); // Using BatchSpanProcessor for better performance
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { DiagConsoleLogger, DiagLogLevel, diag } = require('@opentelemetry/api');

// --- Configuration ---
const SERVICE_NAME = process.env.OTEL_SERVICE_NAME || 'lowcoder-node-service';
const OTLP_EXPORTER_PROTOCOL = process.env.OTEL_EXPORTER_PROTOCOL || 'http/protobuf'; // 'http/protobuf' or 'grpc'

const DEFAULT_OTLP_HTTP_ENDPOINT = 'http://localhost:4318';
const DEFAULT_OTLP_GRPC_ENDPOINT = 'http://localhost:4317';

const OTLP_ENDPOINT = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||
(OTLP_EXPORTER_PROTOCOL === 'grpc' ? DEFAULT_OTLP_GRPC_ENDPOINT : DEFAULT_OTLP_HTTP_ENDPOINT);

const TRACES_ENDPOINT = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||
(OTLP_EXPORTER_PROTOCOL === 'grpc' ? OTLP_ENDPOINT : `${OTLP_ENDPOINT}/v1/traces`);
const METRICS_ENDPOINT = process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ||
(OTLP_EXPORTER_PROTOCOL === 'grpc' ? OTLP_ENDPOINT : `${OTLP_ENDPOINT}/v1/metrics`);

// Optional: Set OpenTelemetry diagnostic logging level
const otelLogLevel = process.env.OTEL_LOG_LEVEL?.toUpperCase();
if (otelLogLevel && DiagLogLevel[otelLogLevel]) {
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel[otelLogLevel]);
} else {
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO); // Default to INFO
}

diag.info(`OpenTelemetry SDK configured for service: ${SERVICE_NAME}`);
diag.info(`Using OTLP protocol: ${OTLP_EXPORTER_PROTOCOL}`);
diag.info(`Traces Exporter Endpoint: ${TRACES_ENDPOINT}`);
diag.info(`Metrics Exporter Endpoint: ${METRICS_ENDPOINT}`);

// --- Resource Definition ---
const resource = Resource.default().merge(
new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: SERVICE_NAME,
// [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0', // Optional
})
);

// --- Exporter Configuration ---
let traceExporter;
let metricExporter;

if (OTLP_EXPORTER_PROTOCOL === 'grpc') {
diag.info('Using gRPC Exporters');
traceExporter = new OTLPTraceExporterGrpc({ url: TRACES_ENDPOINT });
metricExporter = new OTLPMetricExporterGrpc({ url: METRICS_ENDPOINT });
} else {
diag.info('Using HTTP/protobuf Exporters');
traceExporter = new OTLPTraceExporterHttp({ url: TRACES_ENDPOINT });
metricExporter = new OTLPMetricExporterHttp({ url: METRICS_ENDPOINT });
}

// --- SDK Initialization ---
const sdk = new NodeSDK({
resource: resource,
traceExporter: traceExporter,
spanProcessor: new BatchSpanProcessor(traceExporter), // Recommended for most cases
metricReader: new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 10000, // Export metrics every 10 seconds
}),
instrumentations: [
getNodeAutoInstrumentations({
// Configuration for specific instrumentations can be added here if needed
// Example:
// '@opentelemetry/instrumentation-http': {
// applyCustomAttributesOnSpan: (span, request, response) => {
// span.setAttribute('custom.attribute', 'value');
// },
// },
}),
],
});

// --- Start SDK and Graceful Shutdown ---
try {
sdk.start();
diag.info('OpenTelemetry SDK started successfully for traces and metrics.');
} catch (error) {
diag.error('Error starting OpenTelemetry SDK:', error);
process.exit(1);
}

// Graceful shutdown
const shutdown = () => {
diag.info('Shutting down OpenTelemetry SDK...');
sdk.shutdown()
.then(() => diag.info('OpenTelemetry SDK shut down successfully.'))
.catch(error => diag.error('Error shutting down OpenTelemetry SDK:', error))
.finally(() => process.exit(0));
};

process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
2 changes: 1 addition & 1 deletion server/node-service/.gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/node_modules/**
/packages/*/node_modules
/packages/*/dist
/packages/*/tsdoc-metadata.json