Configure Winston logging in Node.js with Elasticsearch transport for centralized log aggregation, monitoring, and analysis using Kibana dashboards.
Prerequisites
- Node.js 18+
- 4GB RAM minimum
- 20GB disk space
- Network access to Elasticsearch ports
What this solves
This tutorial sets up centralized logging for Node.js applications using Winston with Elasticsearch transport. You'll aggregate logs from multiple application instances, enable structured searching and analysis, and create monitoring dashboards in Kibana for better observability and debugging.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you have the latest packages available.
sudo apt update && sudo apt upgrade -y
Install Elasticsearch dependencies
Install Java 11 or later and essential tools required for Elasticsearch.
sudo apt install -y openjdk-11-jre-headless curl wget gnupg2
Add Elasticsearch repository and install
Add the official Elasticsearch repository and install Elasticsearch 8 with security features.
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-8.x.list
sudo apt update
sudo apt install -y elasticsearch
Configure Elasticsearch for centralized logging
Configure Elasticsearch with appropriate settings for log aggregation and performance optimization.
cluster.name: logging-cluster
node.name: node-1
path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
network.host: 127.0.0.1
http.port: 9200
discovery.type: single-node
Security settings
xpack.security.enabled: true
xpack.security.enrollment.enabled: true
xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.keystore.path: certs/http.p12
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.client_authentication: required
xpack.security.transport.ssl.keystore.path: certs/transport.p12
xpack.security.transport.ssl.truststore.path: certs/transport.p12
Configure JVM heap size
Set appropriate JVM heap size based on available memory. Use half of available RAM, maximum 32GB.
-Xms2g
-Xmx2g
Start and enable Elasticsearch
Enable Elasticsearch to start automatically and start the service.
sudo systemctl daemon-reload
sudo systemctl enable --now elasticsearch
sudo systemctl status elasticsearch
Reset Elasticsearch password and get security token
Reset the elastic user password and note it for later configuration.
sudo /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic
sudo /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana
Install and configure Kibana
Install Kibana for log visualization and analysis dashboards.
sudo apt install -y kibana
Configure Kibana
Configure Kibana to connect to Elasticsearch and bind to the correct interface.
server.port: 5601
server.host: "0.0.0.0"
server.name: "logging-kibana"
elasticsearch.hosts: ["https://127.0.0.1:9200"]
elasticsearch.username: "kibana_system"
elasticsearch.password: "your-kibana-password"
elasticsearch.ssl.verificationMode: none
Set Kibana system user password
Set the password for the kibana_system user and update the configuration.
sudo /usr/share/elasticsearch/bin/elasticsearch-reset-password -u kibana_system
Update the password in /etc/kibana/kibana.yml with the generated password.
Start Kibana
Enable and start the Kibana service.
sudo systemctl enable --now kibana
sudo systemctl status kibana
Install Node.js and npm
Install Node.js and npm for building the logging application.
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
Create Node.js logging application
Create a sample Node.js application directory and initialize the project.
mkdir -p /opt/nodejs-logging
cd /opt/nodejs-logging
npm init -y
Install Winston and Elasticsearch transport
Install Winston logging library with Elasticsearch transport for centralized logging.
cd /opt/nodejs-logging
npm install winston winston-elasticsearch @elastic/elasticsearch express
Configure Winston with Elasticsearch transport
Create a logging configuration that sends structured logs to Elasticsearch.
const winston = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');
const esTransportOpts = {
level: 'info',
clientOpts: {
node: 'https://127.0.0.1:9200',
auth: {
username: 'elastic',
password: 'your-elastic-password'
},
ssl: {
rejectUnauthorized: false
}
},
index: 'app-logs',
indexTemplate: {
name: 'app-logs-template',
pattern: 'app-logs-*',
settings: {
number_of_shards: 1,
number_of_replicas: 0,
'index.refresh_interval': '5s'
},
mappings: {
properties: {
'@timestamp': { type: 'date' },
level: { type: 'keyword' },
message: { type: 'text' },
meta: { type: 'object' },
hostname: { type: 'keyword' },
service: { type: 'keyword' },
environment: { type: 'keyword' }
}
}
},
transformer: (logData) => {
return {
'@timestamp': new Date().toISOString(),
level: logData.level,
message: logData.message,
meta: logData.meta || {},
hostname: require('os').hostname(),
service: 'nodejs-app',
environment: process.env.NODE_ENV || 'development'
};
}
};
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
new ElasticsearchTransport(esTransportOpts)
],
exitOnError: false
});
module.exports = logger;
Create sample Express application with logging
Create a sample Express.js application that demonstrates structured logging patterns.
const express = require('express');
const logger = require('./logger');
const app = express();
const port = process.env.PORT || 3000;
// Request logging middleware
app.use((req, res, next) => {
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: duration,
userAgent: req.get('User-Agent'),
ip: req.ip,
requestId: req.headers['x-request-id'] || 'unknown'
});
});
next();
});
// Sample routes with different log levels
app.get('/', (req, res) => {
logger.info('Home page accessed', {
userId: req.query.userId,
sessionId: req.headers['x-session-id']
});
res.json({ message: 'Hello World', timestamp: new Date().toISOString() });
});
app.get('/api/users/:id', (req, res) => {
const userId = req.params.id;
logger.info('User data requested', {
userId: userId,
requestSource: req.get('Referer')
});
if (!userId || isNaN(userId)) {
logger.warn('Invalid user ID requested', {
userId: userId,
ip: req.ip
});
return res.status(400).json({ error: 'Invalid user ID' });
}
res.json({
id: userId,
name: User ${userId},
lastAccess: new Date().toISOString()
});
});
app.get('/api/error', (req, res) => {
logger.error('Intentional error triggered', {
endpoint: '/api/error',
userId: req.query.userId,
stack: new Error('Test error').stack
});
res.status(500).json({ error: 'Internal server error' });
});
// Global error handler
app.use((err, req, res, next) => {
logger.error('Unhandled application error', {
error: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip
});
res.status(500).json({ error: 'Internal server error' });
});
app.listen(port, () => {
logger.info('Application started', {
port: port,
environment: process.env.NODE_ENV || 'development',
nodeVersion: process.version
});
});
module.exports = app;
Create systemd service for the Node.js application
Create a systemd service to manage the logging application automatically.
[Unit]
Description=Node.js Logging Application
After=network.target elasticsearch.service
[Service]
Type=simple
User=nodejs
Group=nodejs
WorkingDirectory=/opt/nodejs-logging
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/node app.js
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nodejs-logging
[Install]
WantedBy=multi-user.target
Create nodejs user and set permissions
Create a dedicated user for running the Node.js application securely.
sudo useradd -r -s /bin/false nodejs
sudo chown -R nodejs:nodejs /opt/nodejs-logging
sudo chmod 755 /opt/nodejs-logging
sudo chmod 644 /opt/nodejs-logging/*
Configure log rotation for application logs
Set up log rotation to prevent disk space issues with application logs.
/var/log/nodejs-logging/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
copytruncate
create 644 nodejs nodejs
}
Start the Node.js logging application
Enable and start the Node.js application service.
sudo systemctl daemon-reload
sudo systemctl enable --now nodejs-logging
sudo systemctl status nodejs-logging
Configure Kibana dashboards
Create index pattern in Kibana
Access Kibana web interface and create an index pattern for the application logs.
Navigate to http://your-server-ip:5601 and login with the elastic user credentials.
- Go to Stack Management > Index Patterns
- Click "Create index pattern"
- Enter
app-logs-*as the index pattern - Select
@timestampas the time field - Click "Create index pattern"
Create log monitoring dashboard
Build a comprehensive dashboard for monitoring application logs and performance.
In Kibana, go to Analytics > Dashboard and create visualizations for:
- Log level distribution (pie chart)
- Request response times (line chart)
- Error rate trends (area chart)
- Top error messages (data table)
- Request volume by endpoint (bar chart)
Verify your setup
Test the centralized logging system to ensure logs are being collected and indexed properly.
# Check Elasticsearch health
curl -k -u elastic:your-password https://127.0.0.1:9200/_cluster/health
Generate test logs
curl http://127.0.0.1:3000/
curl http://127.0.0.1:3000/api/users/123
curl http://127.0.0.1:3000/api/users/invalid
curl http://127.0.0.1:3000/api/error
Check if logs are indexed
curl -k -u elastic:your-password https://127.0.0.1:9200/app-logs-*/_search?pretty
Check application service status
sudo systemctl status nodejs-logging
sudo journalctl -u nodejs-logging -f
Verify in Kibana by going to Analytics > Discover and searching for logs with the app-logs-* index pattern.
Advanced logging patterns
Implement structured logging for microservices
Add correlation IDs and service metadata for better distributed system observability.
const { v4: uuidv4 } = require('uuid');
const logger = require('../logger');
function correlationMiddleware(req, res, next) {
// Generate or extract correlation ID
const correlationId = req.headers['x-correlation-id'] || uuidv4();
// Add correlation ID to request object
req.correlationId = correlationId;
// Set response header
res.setHeader('x-correlation-id', correlationId);
// Create child logger with correlation context
req.logger = logger.child({
correlationId: correlationId,
service: 'user-service',
version: process.env.APP_VERSION || '1.0.0'
});
next();
}
module.exports = correlationMiddleware;
Configure log sampling for high-traffic applications
Implement intelligent log sampling to reduce storage costs while maintaining observability.
const winston = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');
// Sampling configuration
const SAMPLE_RATES = {
error: 1.0, // Log all errors
warn: 0.5, // Log 50% of warnings
info: 0.1, // Log 10% of info messages
debug: 0.01 // Log 1% of debug messages
};
// Custom format with sampling
const samplingFormat = winston.format((info) => {
const sampleRate = SAMPLE_RATES[info.level] || 0.1;
if (Math.random() > sampleRate) {
return false; // Don't log this message
}
// Add sampling metadata
info.sampled = true;
info.sampleRate = sampleRate;
return info;
});
const logger = winston.createLogger({
level: 'debug',
format: winston.format.combine(
samplingFormat(),
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new ElasticsearchTransport({
level: 'info',
clientOpts: {
node: 'https://127.0.0.1:9200',
auth: {
username: 'elastic',
password: 'your-elastic-password'
},
ssl: { rejectUnauthorized: false }
},
index: 'app-logs'
})
]
});
module.exports = logger;
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Elasticsearch won't start | Insufficient heap memory | Adjust JVM heap size in /etc/elasticsearch/jvm.options.d/heap.options |
| Winston transport connection fails | SSL verification or authentication issues | Verify elastic user password and set rejectUnauthorized: false for SSL |
| Logs not appearing in Kibana | Index pattern misconfiguration | Recreate index pattern with correct name and time field |
| High disk usage | No log rotation or retention policy | Configure Elasticsearch ILM and log rotation |
| Application logging errors | Elasticsearch cluster connectivity | Check network connectivity and Elasticsearch service status |
Next steps
- Configure Elasticsearch ILM for log retention
- Set up Filebeat for additional log sources
- Add Prometheus metrics to Node.js applications
- Configure alerting for critical log events
- Set up Elasticsearch clustering for high availability
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Default values
ELASTICSEARCH_PASSWORD=""
KIBANA_PASSWORD=""
PROJECT_DIR="/opt/nodejs-logging"
# Usage function
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -h, --help Show this help message"
echo " -d, --dir DIR Project directory (default: /opt/nodejs-logging)"
echo ""
echo "Example: $0 -d /var/www/logging-app"
exit 1
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help) usage ;;
-d|--dir) PROJECT_DIR="$2"; shift 2 ;;
*) echo -e "${RED}Unknown option: $1${NC}"; usage ;;
esac
done
# Error cleanup
cleanup() {
echo -e "${RED}Installation failed. Cleaning up...${NC}"
systemctl stop elasticsearch kibana 2>/dev/null || true
systemctl disable elasticsearch kibana 2>/dev/null || true
rm -rf "$PROJECT_DIR" 2>/dev/null || true
}
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root${NC}"
exit 1
fi
echo -e "${BLUE}Setting up centralized logging with Winston and Elasticsearch${NC}"
# Auto-detect distro
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
JAVA_PKG="openjdk-11-jre-headless"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
JAVA_PKG="java-11-openjdk-headless"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
JAVA_PKG="java-11-openjdk-headless"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Cannot detect distribution${NC}"
exit 1
fi
echo -e "${GREEN}[1/8] Updating system packages${NC}"
$PKG_UPDATE
echo -e "${GREEN}[2/8] Installing dependencies${NC}"
$PKG_INSTALL $JAVA_PKG curl wget gnupg2
# Install Node.js
if ! command -v node &> /dev/null; then
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
$PKG_INSTALL nodejs
fi
echo -e "${GREEN}[3/8] Adding Elasticsearch repository${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" > /etc/apt/sources.list.d/elastic-8.x.list
apt update
$PKG_INSTALL elasticsearch kibana
else
rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
cat > /etc/yum.repos.d/elasticsearch.repo << 'EOF'
[elasticsearch]
name=Elasticsearch repository for 8.x packages
baseurl=https://artifacts.elastic.co/packages/8.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=0
autorefresh=1
type=rpm-md
EOF
$PKG_INSTALL --enablerepo=elasticsearch elasticsearch kibana
fi
echo -e "${GREEN}[4/8] Configuring Elasticsearch${NC}"
# Configure Elasticsearch
cat > /etc/elasticsearch/elasticsearch.yml << 'EOF'
cluster.name: logging-cluster
node.name: node-1
path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
network.host: 127.0.0.1
http.port: 9200
discovery.type: single-node
xpack.security.enabled: true
xpack.security.enrollment.enabled: false
xpack.security.http.ssl.enabled: false
xpack.security.transport.ssl.enabled: false
action.destructive_requires_name: true
EOF
# Set proper permissions
chown -R elasticsearch:elasticsearch /etc/elasticsearch
chown -R elasticsearch:elasticsearch /var/lib/elasticsearch
chown -R elasticsearch:elasticsearch /var/log/elasticsearch
chmod 750 /etc/elasticsearch
chmod 644 /etc/elasticsearch/elasticsearch.yml
# Enable and start Elasticsearch
systemctl daemon-reload
systemctl enable elasticsearch
systemctl start elasticsearch
# Wait for Elasticsearch to start
echo -e "${YELLOW}Waiting for Elasticsearch to start...${NC}"
sleep 30
# Reset passwords
echo -e "${GREEN}[5/8] Setting up Elasticsearch security${NC}"
ELASTICSEARCH_PASSWORD=$(echo 'y' | /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic 2>/dev/null | grep "New value:" | awk '{print $3}')
KIBANA_PASSWORD=$(echo 'y' | /usr/share/elasticsearch/bin/elasticsearch-reset-password -u kibana_system 2>/dev/null | grep "New value:" | awk '{print $3}')
echo -e "${GREEN}[6/8] Configuring Kibana${NC}"
cat > /etc/kibana/kibana.yml << EOF
server.port: 5601
server.host: "127.0.0.1"
server.name: "logging-kibana"
elasticsearch.hosts: ["http://127.0.0.1:9200"]
elasticsearch.username: "kibana_system"
elasticsearch.password: "${KIBANA_PASSWORD}"
logging.appenders.file.type: file
logging.appenders.file.fileName: /var/log/kibana/kibana.log
logging.appenders.file.layout.type: pattern
logging.root.appenders: [default, file]
EOF
chown -R kibana:kibana /etc/kibana
chmod 644 /etc/kibana/kibana.yml
# Enable and start Kibana
systemctl enable kibana
systemctl start kibana
echo -e "${GREEN}[7/8] Creating Node.js application${NC}"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
# Initialize package.json
cat > package.json << 'EOF'
{
"name": "nodejs-logging-app",
"version": "1.0.0",
"description": "Node.js application with centralized logging",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"dependencies": {
"express": "^4.18.2",
"winston": "^3.8.2",
"winston-elasticsearch": "^0.17.4"
},
"devDependencies": {
"nodemon": "^2.0.20"
}
}
EOF
# Install dependencies
npm install
# Create logger configuration
cat > logger.js << EOF
const winston = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');
const esTransportOpts = {
level: 'info',
clientOpts: {
node: 'http://127.0.0.1:9200',
auth: {
username: 'elastic',
password: '${ELASTICSEARCH_PASSWORD}'
}
},
index: 'nodejs-logs',
transformer: (logData) => {
return {
'@timestamp': new Date().toISOString(),
level: logData.level,
message: logData.message,
meta: logData.meta || {},
hostname: require('os').hostname(),
service: 'nodejs-app',
environment: process.env.NODE_ENV || 'development'
};
}
};
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
new ElasticsearchTransport(esTransportOpts)
],
exitOnError: false
});
module.exports = logger;
EOF
# Create sample Express application
cat > app.js << 'EOF'
const express = require('express');
const logger = require('./logger');
const app = express();
const port = process.env.PORT || 3000;
app.use((req, res, next) => {
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: duration,
userAgent: req.get('User-Agent'),
ip: req.ip,
requestId: req.headers['x-request-id'] || 'unknown'
});
});
next();
});
app.get('/', (req, res) => {
logger.info('Home page accessed', {
userId: req.query.userId,
sessionId: req.headers['x-session-id']
});
res.json({ message: 'Hello World', timestamp: new Date().toISOString() });
});
app.get('/api/error', (req, res) => {
logger.error('Intentional error triggered', {
endpoint: '/api/error',
userId: req.query.userId,
stack: new Error('Test error').stack
});
res.status(500).json({ error: 'Something went wrong' });
});
app.listen(port, () => {
logger.info('Server started', { port: port });
console.log(`Server running on port ${port}`);
});
EOF
# Set proper ownership
chown -R root:root "$PROJECT_DIR"
chmod 755 "$PROJECT_DIR"
chmod 644 "$PROJECT_DIR"/*.js "$PROJECT_DIR"/*.json
echo -e "${GREEN}[8/8] Verifying installation${NC}"
# Check services
if ! systemctl is-active --quiet elasticsearch; then
echo -e "${RED}Elasticsearch service is not running${NC}"
exit 1
fi
if ! systemctl is-active --quiet kibana; then
echo -e "${RED}Kibana service is not running${NC}"
exit 1
fi
# Test Elasticsearch connection
sleep 10
if ! curl -s -u "elastic:${ELASTICSEARCH_PASSWORD}" "http://127.0.0.1:9200" &>/dev/null; then
echo -e "${RED}Cannot connect to Elasticsearch${NC}"
exit 1
fi
echo -e "${GREEN}Installation completed successfully!${NC}"
echo ""
echo -e "${BLUE}Service Information:${NC}"
echo "Elasticsearch: http://127.0.0.1:9200"
echo "Kibana: http://127.0.0.1:5601"
echo "Username: elastic"
echo "Password: ${ELASTICSEARCH_PASSWORD}"
echo ""
echo -e "${BLUE}To start the Node.js application:${NC}"
echo "cd $PROJECT_DIR"
echo "npm start"
echo ""
echo -e "${YELLOW}Note: Kibana may take a few minutes to fully start up${NC}"
Review the script before running. Execute with: bash install.sh