Skip to content
Last9 named a Gartner Cool Vendor in AI for SRE Observability for 2025! Read more →
Last9

SailsJS

Learn how to integrate OpenTelemetry with SailsJS applications and send telemetry data to Last9

This guide shows you how to instrument your SailsJS application with OpenTelemetry and send traces, metrics, and logs to Last9.

Prerequisites

  • Node.js 14 or later
  • SailsJS application
  • Last9 account with OTLP endpoint configured

Installation

  1. Install OpenTelemetry packages

    npm install --save @opentelemetry/api \
    @opentelemetry/sdk-node \
    @opentelemetry/auto-instrumentations-node \
    @opentelemetry/exporter-trace-otlp-http \
    @opentelemetry/exporter-metrics-otlp-http \
    @opentelemetry/exporter-logs-otlp-http \
    @opentelemetry/resources \
    @opentelemetry/semantic-conventions
  2. Install additional instrumentation packages (optional)

    # For manual instrumentation
    npm install --save @opentelemetry/instrumentation-http \
    @opentelemetry/instrumentation-express \
    @opentelemetry/instrumentation-redis \
    @opentelemetry/instrumentation-mongodb \
    @opentelemetry/instrumentation-mysql \
    @opentelemetry/instrumentation-winston

Configuration

Set up your environment variables:

export OTEL_EXPORTER_OTLP_ENDPOINT="$last9_otlp_endpoint"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=$last9_otlp_auth_header"
export OTEL_SERVICE_NAME="sailsjs-app"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production,service.version=1.0.0"
export NODE_ENV="production"

Instrumentation Setup

const { NodeSDK } = require("@opentelemetry/sdk-node");
const {
getNodeAutoInstrumentations,
} = require("@opentelemetry/auto-instrumentations-node");
const {
OTLPTraceExporter,
} = require("@opentelemetry/exporter-trace-otlp-http");
const {
OTLPMetricExporter,
} = require("@opentelemetry/exporter-metrics-otlp-http");
const { OTLPLogExporter } = require("@opentelemetry/exporter-logs-otlp-http");
const { Resource } = require("@opentelemetry/resources");
const {
SemanticResourceAttributes,
} = require("@opentelemetry/semantic-conventions");
const { PeriodicExportingMetricReader } = require("@opentelemetry/sdk-metrics");
// Create resource with service information
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]:
process.env.OTEL_SERVICE_NAME || "sailsjs-app",
[SemanticResourceAttributes.SERVICE_VERSION]:
process.env.npm_package_version || "1.0.0",
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]:
process.env.NODE_ENV || "development",
});
// Configure exporters
const traceExporter = new OTLPTraceExporter({
url: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces`,
headers: {
Authorization:
process.env.OTEL_EXPORTER_OTLP_HEADERS?.replace("Authorization=", "") ||
"",
},
});
const metricExporter = new OTLPMetricExporter({
url: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metrics`,
headers: {
Authorization:
process.env.OTEL_EXPORTER_OTLP_HEADERS?.replace("Authorization=", "") ||
"",
},
});
const logExporter = new OTLPLogExporter({
url: `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs`,
headers: {
Authorization:
process.env.OTEL_EXPORTER_OTLP_HEADERS?.replace("Authorization=", "") ||
"",
},
});
// Initialize the SDK
const sdk = new NodeSDK({
resource,
traceExporter,
metricReader: new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 30000, // Export metrics every 30 seconds
}),
logRecordProcessor:
new (require("@opentelemetry/sdk-logs").BatchLogRecordProcessor)(
logExporter,
),
instrumentations: [
getNodeAutoInstrumentations({
// Disable file system instrumentation as it's too noisy
"@opentelemetry/instrumentation-fs": {
enabled: false,
},
// Configure HTTP instrumentation
"@opentelemetry/instrumentation-http": {
enabled: true,
ignoreIncomingRequestHook: (req) => {
// Ignore health check requests
return req.url?.includes("/health") || req.url?.includes("/ping");
},
ignoreOutgoingRequestHook: (options) => {
// Ignore requests to monitoring endpoints
return (
options.hostname?.includes("health") ||
options.path?.includes("metrics")
);
},
},
// Configure Express instrumentation for SailsJS
"@opentelemetry/instrumentation-express": {
enabled: true,
ignoreLayers: [
(layer, request) => {
// Ignore static file serving layers
return layer.name === "serveStatic";
},
],
},
// Configure database instrumentations
"@opentelemetry/instrumentation-mysql": {
enabled: true,
},
"@opentelemetry/instrumentation-mongodb": {
enabled: true,
},
"@opentelemetry/instrumentation-redis": {
enabled: true,
},
// Configure Winston logging if used
"@opentelemetry/instrumentation-winston": {
enabled: true,
},
}),
],
});
// Start the SDK
try {
sdk.start();
console.log("🚀 OpenTelemetry instrumentation initialized successfully");
} catch (error) {
console.error("❌ Error initializing OpenTelemetry:", error);
}
// Graceful shutdown
const shutdown = async () => {
try {
await sdk.shutdown();
console.log("📊 OpenTelemetry terminated gracefully");
} catch (error) {
console.error("❌ Error terminating OpenTelemetry:", error);
} finally {
process.exit(0);
}
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
module.exports = sdk;

Manual Instrumentation

For more control over your instrumentation, you can add custom spans and metrics:

const { trace, metrics } = require("@opentelemetry/api");
// Get tracer and meter instances
const tracer = trace.getTracer("sailsjs-app", "1.0.0");
const meter = metrics.getMeter("sailsjs-app", "1.0.0");
// Create custom metrics
const httpRequestCount = meter.createCounter("http_requests_total", {
description: "Total number of HTTP requests",
});
const httpRequestDuration = meter.createHistogram("http_request_duration_ms", {
description: "Duration of HTTP requests in milliseconds",
unit: "ms",
});
const activeConnections = meter.createUpDownCounter("http_active_connections", {
description: "Number of active HTTP connections",
});
const businessMetrics = {
userRegistrations: meter.createCounter("user_registrations_total", {
description: "Total number of user registrations",
}),
ordersCreated: meter.createCounter("orders_created_total", {
description: "Total number of orders created",
}),
cacheHits: meter.createCounter("cache_hits_total", {
description: "Total number of cache hits",
}),
cacheMisses: meter.createCounter("cache_misses_total", {
description: "Total number of cache misses",
}),
};
module.exports = {
tracer,
meter,
metrics: {
httpRequestCount,
httpRequestDuration,
activeConnections,
...businessMetrics,
},
};

Custom Service Instrumentation

Create instrumented services for your business logic:

const { tracer, metrics } = require("../../config/telemetry");
const { trace, SpanStatusCode } = require("@opentelemetry/api");
module.exports = {
async createUser(userData) {
return tracer.startActiveSpan("user_service.create_user", async (span) => {
try {
span.setAttributes({
"user.email": userData.email,
"user.role": userData.role || "user",
operation: "create_user",
});
// Validate user data
const validationSpan = tracer.startSpan("user_service.validate_data");
const validation = await this._validateUserData(userData);
validationSpan.setAttributes({
"validation.passed": validation.isValid,
"validation.errors_count": validation.errors?.length || 0,
});
validationSpan.end();
if (!validation.isValid) {
throw new Error(`Validation failed: ${validation.errors.join(", ")}`);
}
// Check if user already exists
const existsSpan = tracer.startSpan("user_service.check_exists");
const existingUser = await User.findOne({ email: userData.email });
existsSpan.setAttributes({
"user.exists": !!existingUser,
});
existsSpan.end();
if (existingUser) {
throw new Error("User already exists");
}
// Create user in database
const dbSpan = tracer.startSpan("user_service.db_create");
const newUser = await User.create(userData).fetch();
dbSpan.setAttributes({
"user.created_id": newUser.id,
"db.operation": "INSERT",
"db.table": "users",
});
dbSpan.end();
// Send welcome email
const emailSpan = tracer.startSpan("user_service.send_welcome_email");
try {
await EmailService.sendWelcomeEmail(newUser);
emailSpan.setAttributes({
"email.sent": true,
"email.type": "welcome",
});
} catch (emailError) {
emailSpan.recordException(emailError);
emailSpan.setStatus({
code: SpanStatusCode.ERROR,
message: "Failed to send welcome email",
});
// Don't fail user creation if email fails
sails.log.warn("Failed to send welcome email:", emailError);
}
emailSpan.end();
// Record business metric
metrics.userRegistrations.add(1, {
"user.role": userData.role || "user",
"registration.source": userData.source || "direct",
});
span.setAttributes({
"user.created": true,
"user.id": newUser.id,
});
return newUser;
} catch (error) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
throw error;
}
});
},
async _validateUserData(userData) {
const errors = [];
if (!userData.email || !userData.email.includes("@")) {
errors.push("Invalid email");
}
if (!userData.name || userData.name.length < 2) {
errors.push("Name must be at least 2 characters");
}
if (!userData.password || userData.password.length < 8) {
errors.push("Password must be at least 8 characters");
}
return {
isValid: errors.length === 0,
errors: errors,
};
},
async getUserById(userId) {
return tracer.startActiveSpan("user_service.get_by_id", async (span) => {
span.setAttributes({
"user.id": userId,
operation: "get_user",
});
try {
// Try cache first
const cacheSpan = tracer.startSpan("user_service.cache_lookup");
const cachedUser = await CacheService.get(`user:${userId}`);
cacheSpan.setAttributes({
"cache.hit": !!cachedUser,
"cache.key": `user:${userId}`,
});
cacheSpan.end();
if (cachedUser) {
metrics.cacheHits.add(1, { type: "user" });
span.setAttributes({ "cache.hit": true });
return JSON.parse(cachedUser);
}
metrics.cacheMisses.add(1, { type: "user" });
// Fetch from database
const dbSpan = tracer.startSpan("user_service.db_fetch");
const user = await User.findOne({ id: userId });
dbSpan.setAttributes({
"db.operation": "SELECT",
"db.table": "users",
"user.found": !!user,
});
dbSpan.end();
if (user) {
// Cache for future requests
const cacheSetSpan = tracer.startSpan("user_service.cache_set");
await CacheService.set(`user:${userId}`, JSON.stringify(user), 300); // 5 minutes
cacheSetSpan.setAttributes({
"cache.key": `user:${userId}`,
"cache.ttl": 300,
});
cacheSetSpan.end();
}
span.setAttributes({
"user.found": !!user,
"cache.hit": false,
});
return user;
} catch (error) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
throw error;
}
});
},
};

Controller Examples

const { tracer, metrics } = require("../../config/telemetry");
const { trace, SpanStatusCode } = require("@opentelemetry/api");
module.exports = {
async create(req, res) {
const span = trace.getActiveSpan();
if (span) {
span.setAttributes({
controller: "UserController",
action: "create",
"request.body_size": JSON.stringify(req.body || {}).length,
});
}
try {
const userData = req.body;
// Validate required fields
if (!userData.email || !userData.name) {
return res.badRequest({
error: "Email and name are required",
code: "MISSING_REQUIRED_FIELDS",
});
}
// Create user using instrumented service
const newUser = await UserService.createUser(userData);
if (span) {
span.setAttributes({
"user.created": true,
"user.id": newUser.id,
});
}
return res.status(201).json({
message: "User created successfully",
user: {
id: newUser.id,
name: newUser.name,
email: newUser.email,
},
});
} catch (error) {
if (span) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
}
sails.log.error("Error creating user:", error);
if (error.message.includes("already exists")) {
return res.status(409).json({
error: "User already exists",
code: "USER_EXISTS",
});
}
return res.serverError({
error: "Failed to create user",
code: "USER_CREATION_FAILED",
});
}
},
async findOne(req, res) {
const span = trace.getActiveSpan();
const userId = req.param("id");
if (span) {
span.setAttributes({
controller: "UserController",
action: "findOne",
"user.id": userId,
});
}
try {
if (!userId) {
return res.badRequest({
error: "User ID is required",
code: "MISSING_USER_ID",
});
}
const user = await UserService.getUserById(userId);
if (!user) {
if (span) {
span.setAttributes({ "user.found": false });
}
return res.notFound({
error: "User not found",
code: "USER_NOT_FOUND",
});
}
if (span) {
span.setAttributes({
"user.found": true,
"user.role": user.role,
});
}
return res.json({
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
createdAt: user.createdAt,
},
});
} catch (error) {
if (span) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
}
sails.log.error("Error fetching user:", error);
return res.serverError({
error: "Failed to fetch user",
code: "USER_FETCH_FAILED",
});
}
},
async find(req, res) {
const span = trace.getActiveSpan();
if (span) {
span.setAttributes({
controller: "UserController",
action: "find",
});
}
try {
const { limit = 50, skip = 0, role } = req.query;
// Build query criteria
const criteria = {};
if (role) {
criteria.role = role;
}
if (span) {
span.setAttributes({
"query.limit": parseInt(limit),
"query.skip": parseInt(skip),
"query.has_role_filter": !!role,
});
}
// Create child span for database operation
const users = await tracer.startActiveSpan(
"user_controller.db_find",
async (dbSpan) => {
dbSpan.setAttributes({
"db.operation": "SELECT",
"db.table": "users",
"db.limit": parseInt(limit),
"db.skip": parseInt(skip),
});
try {
const results = await User.find(criteria)
.limit(parseInt(limit))
.skip(parseInt(skip))
.sort("createdAt DESC");
dbSpan.setAttributes({
"db.results_count": results.length,
});
return results;
} catch (error) {
dbSpan.recordException(error);
dbSpan.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
throw error;
}
},
);
if (span) {
span.setAttributes({
"users.count": users.length,
"users.has_results": users.length > 0,
});
}
return res.json({
users: users.map((user) => ({
id: user.id,
name: user.name,
email: user.email,
role: user.role,
createdAt: user.createdAt,
})),
pagination: {
limit: parseInt(limit),
skip: parseInt(skip),
count: users.length,
},
});
} catch (error) {
if (span) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
}
sails.log.error("Error listing users:", error);
return res.serverError({
error: "Failed to list users",
code: "USER_LIST_FAILED",
});
}
},
};

Database Integration

Waterline ORM Hooks

const { tracer } = require("./telemetry");
const { SpanStatusCode } = require("@opentelemetry/api");
module.exports.models = {
migrate: "safe",
attributes: {
createdAt: { type: "number", autoCreatedAt: true },
updatedAt: { type: "number", autoUpdatedAt: true },
id: { type: "number", autoIncrement: true, columnName: "id" },
},
// Add lifecycle callbacks for automatic instrumentation
beforeCreate: function (valuesToSet, proceed) {
return tracer.startActiveSpan("waterline.before_create", (span) => {
span.setAttributes({
"db.operation": "BEFORE_CREATE",
"db.model": this.globalId || this.identity,
});
return proceed();
});
},
afterCreate: function (newlyInsertedRecord, proceed) {
return tracer.startActiveSpan("waterline.after_create", (span) => {
span.setAttributes({
"db.operation": "AFTER_CREATE",
"db.model": this.globalId || this.identity,
"db.record_id": newlyInsertedRecord.id,
});
return proceed();
});
},
beforeUpdate: function (valuesToSet, proceed) {
return tracer.startActiveSpan("waterline.before_update", (span) => {
span.setAttributes({
"db.operation": "BEFORE_UPDATE",
"db.model": this.globalId || this.identity,
});
return proceed();
});
},
afterUpdate: function (updatedRecord, proceed) {
return tracer.startActiveSpan("waterline.after_update", (span) => {
span.setAttributes({
"db.operation": "AFTER_UPDATE",
"db.model": this.globalId || this.identity,
"db.record_id": updatedRecord.id,
});
return proceed();
});
},
beforeDestroy: function (criteria, proceed) {
return tracer.startActiveSpan("waterline.before_destroy", (span) => {
span.setAttributes({
"db.operation": "BEFORE_DESTROY",
"db.model": this.globalId || this.identity,
});
return proceed();
});
},
afterDestroy: function (destroyedRecords, proceed) {
return tracer.startActiveSpan("waterline.after_destroy", (span) => {
span.setAttributes({
"db.operation": "AFTER_DESTROY",
"db.model": this.globalId || this.identity,
"db.destroyed_count": destroyedRecords.length,
});
return proceed();
});
},
};

Testing the Integration

  1. Start your application

    npm start
    # or
    node app.js
  2. Make test requests

    # Health check (if you have one)
    curl http://localhost:1337/health
    # User endpoints
    curl http://localhost:1337/api/users
    curl http://localhost:1337/api/users/1
    # Create a user
    curl -X POST http://localhost:1337/api/users \
    -H "Content-Type: application/json" \
    -d '{"name": "Test User", "email": "test@example.com"}'
  3. View telemetry in Last9

    Check your Last9 dashboard for:

    • HTTP request traces with SailsJS-specific attributes
    • Database operation spans from Waterline ORM
    • Custom business logic spans
    • Cache operation traces
    • Custom metrics for user registrations and other business events

Production Deployment

Environment Configuration

module.exports = {
port: process.env.PORT || 1337,
// Database configuration
datastores: {
default: {
adapter: "sails-postgresql",
url: process.env.DATABASE_URL,
ssl: process.env.DATABASE_SSL === "true",
},
},
// Custom middleware configuration for production
http: {
middleware: {
order: [
"cookieParser",
"session",
"bodyParser",
"compress",
"telemetry", // Add telemetry policy
],
},
},
// Security configurations
security: {
cors: {
allRoutes: true,
allowOrigins: process.env.ALLOWED_ORIGINS?.split(",") || [
"http://localhost:3000",
],
allowCredentials: false,
},
},
// Session configuration for production
session: {
secret: process.env.SESSION_SECRET || "your-session-secret",
adapter: process.env.SESSION_ADAPTER || "memory",
},
// Logging configuration
log: {
level: process.env.LOG_LEVEL || "info",
},
// Telemetry-specific configurations
telemetry: {
enabled: true,
serviceName: process.env.OTEL_SERVICE_NAME || "sailsjs-app",
environment: process.env.NODE_ENV || "production",
},
};