# Sails.js

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

Source: https://last9.io/docs/integrations/frameworks/javascript/sailsjs/

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**

   ```bash
   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)**

   ```bash
   # 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:

```bash
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

### Auto-Instrumentation (Recommended)

**instrumentation.js**

```javascript
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 { resourceFromAttributes } = require("@opentelemetry/resources");
const { ATTR_SERVICE_NAME } = require("@opentelemetry/semantic-conventions");
const { BatchSpanProcessor } = require("@opentelemetry/sdk-trace-base");
const { BatchLogRecordProcessor } = require("@opentelemetry/sdk-logs");
const { PeriodicExportingMetricReader } = require("@opentelemetry/sdk-metrics");
const api = require("@opentelemetry/api");

// For troubleshooting, uncomment the following lines to enable OpenTelemetry debug logging:
// const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
// diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);

// BaggageSpanProcessor - propagates W3C Baggage entries as span attributes for RUM correlation
class BaggageSpanProcessor {
  onStart(span, parentContext) {
    const baggage = api.propagation.getBaggage(
      parentContext || api.context.active(),
    );
    if (!baggage) return;
    for (const [key, entry] of baggage.getAllEntries()) {
      span.setAttribute(key, entry.value);
    }
  }
  onEnd() {}
  forceFlush() {
    return Promise.resolve();
  }
  shutdown() {
    return Promise.resolve();
  }
}

// Create resource with service information
const resource = resourceFromAttributes({
  [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || "sailsjs-app",
  "service.version": process.env.npm_package_version || "1.0.0",
  "deployment.environment": process.env.NODE_ENV || "development",
});

// Initialize the SDK
// SDK auto-reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS from env
const sdk = new NodeSDK({
  resource,
  spanProcessors: [
    new BaggageSpanProcessor(),
    new BatchSpanProcessor(new OTLPTraceExporter()),
  ],
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter(),
    exportIntervalMillis: 30000, // Export metrics every 30 seconds
  }),
  logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())],
  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;
```

**app.js (Sails Entry Point)**

```javascript
// Import instrumentation FIRST - before any other imports
require("./instrumentation");

/**
 * app.js
 *
 * Use `app.js` to run your app without `sails lift`.
 * To start the server, run: `node app.js`.
 *
 * This is handy in situations where the sails command line tool isn't relevant
 * or useful, such as when you deploy to a server, or a PaaS like Heroku.
 */

// Ensure we're in the project directory, so cwd-relative paths work as expected
process.chdir(__dirname);

// Attempt to import `sails` dependency, as well as `rc` (for loading `.sailsrc` files).
var sails, rc;
try {
  sails = require("sails");
  rc = require("sails/accessible/rc");
} catch (err) {
  console.error("Encountered an error when attempting to require('sails'):");
  console.error(err.stack);
  console.error("--");
  console.error(
    "To run an app using `node app.js`, you need to have sails installed",
  );
  console.error(
    "locally (`./node_modules/sails`). To do that, run `npm install sails`.",
  );
  console.error("--");
  console.error(
    "Alternatively, if you have sails installed globally, you can use `sails lift`.",
  );
  console.error(
    "When you run `sails lift`, your app will still use a locally installed version",
  );
  console.error(
    "of Sails (thanks to the `sails` dependency in your app's package.json file)",
  );
  console.error(
    "if one exists. If it doesn't, Sails will attempt to use the globally",
  );
  console.error("installed version.");
  return;
}

var conf = rc("sails");

// Start server
sails.lift(conf, function (err) {
  if (err) {
    console.error("Error occurred lifting Sails app:", err);
    return;
  }

  console.log("⚓ Sails app lifted successfully!");
  console.log("📊 OpenTelemetry monitoring active");
  console.log(
    `🌐 Server running at: http://localhost:${sails.config.port || 1337}`,
  );
});
```

### Manual Instrumentation

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

**config/telemetry.js**

```javascript
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,
  },
};
```

**api/policies/telemetry.js**

```javascript
// Sails policy for adding custom telemetry
const { trace, context } = require("@opentelemetry/api");
const { tracer, metrics } = require("../../config/telemetry");

module.exports = function (req, res, next) {
  const startTime = Date.now();

  // Get current span (created by auto-instrumentation)
  const currentSpan = trace.getActiveSpan();

  if (currentSpan) {
    // Add custom attributes to the current span
    currentSpan.setAttributes({
      "sails.action": req.options?.action || "unknown",
      "sails.controller": req.options?.controller || "unknown",
      "user.id": req.session?.userId || "anonymous",
      "request.id": req.id || "unknown",
    });
  }

  // Track active connections
  metrics.activeConnections.add(1, {
    method: req.method,
    route: req.route?.path || req.path,
  });

  // Wrap the response end to capture metrics
  const originalEnd = res.end;
  res.end = function (...args) {
    const duration = Date.now() - startTime;

    // Record metrics
    metrics.httpRequestCount.add(1, {
      method: req.method,
      status_code: res.statusCode.toString(),
      route: req.route?.path || req.path,
    });

    metrics.httpRequestDuration.record(duration, {
      method: req.method,
      status_code: res.statusCode.toString(),
      route: req.route?.path || req.path,
    });

    // Decrease active connections
    metrics.activeConnections.add(-1, {
      method: req.method,
      route: req.route?.path || req.path,
    });

    // Add response size to span if available
    if (currentSpan && res.get("content-length")) {
      currentSpan.setAttributes({
        "http.response.size": parseInt(res.get("content-length")),
      });
    }

    return originalEnd.apply(this, args);
  };

  return next();
};
```

### Custom Service Instrumentation

Create instrumented services for your business logic:

**api/services/UserService.js**

```javascript
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;
      }
    });
  },
};
```

**api/services/CacheService.js**

```javascript
const { tracer } = require("../../config/telemetry");
const { SpanStatusCode } = require("@opentelemetry/api");

module.exports = {
  async get(key) {
    return tracer.startActiveSpan("cache_service.get", async (span) => {
      span.setAttributes({
        "cache.key": key,
        "cache.operation": "GET",
      });

      try {
        // Assuming you're using Redis or similar cache
        const result = await sails.cache.get(key);

        span.setAttributes({
          "cache.hit": !!result,
          "cache.result_size": result ? result.length : 0,
        });

        return result;
      } catch (error) {
        span.recordException(error);
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: error.message,
        });
        throw error;
      }
    });
  },

  async set(key, value, ttl = 3600) {
    return tracer.startActiveSpan("cache_service.set", async (span) => {
      span.setAttributes({
        "cache.key": key,
        "cache.operation": "SET",
        "cache.ttl": ttl,
        "cache.value_size": value ? value.length : 0,
      });

      try {
        const result = await sails.cache.set(key, value, ttl);

        span.setAttributes({
          "cache.set_success": !!result,
        });

        return result;
      } catch (error) {
        span.recordException(error);
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: error.message,
        });
        throw error;
      }
    });
  },

  async del(key) {
    return tracer.startActiveSpan("cache_service.delete", async (span) => {
      span.setAttributes({
        "cache.key": key,
        "cache.operation": "DELETE",
      });

      try {
        const result = await sails.cache.del(key);

        span.setAttributes({
          "cache.delete_success": !!result,
        });

        return result;
      } catch (error) {
        span.recordException(error);
        span.setStatus({
          code: SpanStatusCode.ERROR,
          message: error.message,
        });
        throw error;
      }
    });
  },
};
```

### Controller Examples

**api/controllers/UserController.js**

```javascript
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

**config/models.js**

```javascript
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**

   ```bash
   npm start
   # or
   node app.js
   ```

2. **Make test requests**

   ```bash
   # 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

**config/env/production.js**

```javascript
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",
  },
};
```

**Dockerfile**

```dockerfile
FROM node:18-alpine

# Set working directory
WORKDIR /app

# Install curl for health checks
RUN apk add --no-cache curl

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy application code
COPY . .

# Create a non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S sailsjs -u 1001

# Change ownership of the app directory
RUN chown -R sailsjs:nodejs /app
USER sailsjs

# Expose port
EXPOSE 1337

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD curl -f http://localhost:1337/health || exit 1

# Start application
CMD ["node", "app.js"]
```

**docker-compose.yml**

```yaml
version: "3.8"

services:
  sailsjs-app:
    build: .
    ports:
      - "1337:1337"
    environment:
      - NODE_ENV=production
      - OTEL_EXPORTER_OTLP_ENDPOINT=$last9_otlp_endpoint
      - OTEL_EXPORTER_OTLP_HEADERS=Authorization=$last9_otlp_auth_header
      - OTEL_SERVICE_NAME=sailsjs-app
      - OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,service.version=1.0.0
      - DATABASE_URL=postgresql://user:pass@postgres:5432/sailsjs_db
      - REDIS_URL=redis://redis:6379
      - SESSION_SECRET=your-production-session-secret
    depends_on:
      - postgres
      - redis
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:1337/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: sailsjs_db
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:
```
