Slack Integration
This guide covers the integration between NopeSight and Slack, enabling real-time notifications, interactive commands, and ChatOps capabilities for your IT operations.
Overview
The NopeSight-Slack integration provides:
- Real-time Notifications - Instant alerts for critical events
- Interactive Commands - Execute NopeSight actions from Slack
- ChatOps Automation - Manage infrastructure through conversation
- Custom Workflows - Build automated responses with Slack workflows
- Rich Formatting - Detailed messages with charts and visualizations
Architecture
Installation
1. Create Slack App
- Go to api.slack.com/apps
- Click Create New App
- Choose From scratch
- Name your app:
NopeSight - Select your workspace
2. Configure OAuth & Permissions
# Required OAuth Scopes
bot_token_scopes:
- channels:history # Read channel messages
- channels:read # List channels
- chat:write # Send messages
- chat:write.public # Send to any channel
- commands # Receive slash commands
- files:write # Upload files (reports)
- im:history # Read DMs
- im:write # Send DMs
- users:read # Get user info
- users:read.email # Match users by email
user_token_scopes:
- channels:history
- channels:read
- chat:write
- files:read
3. Install to Workspace
# Install app and get tokens
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
SLACK_SIGNING_SECRET=your-signing-secret
# Configure NopeSight
curl -X POST https://api.nopesight.com/v1/integrations/slack/setup \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"bot_token": "'$SLACK_BOT_TOKEN'",
"app_token": "'$SLACK_APP_TOKEN'",
"signing_secret": "'$SLACK_SIGNING_SECRET'"
}'
Configuration
Environment Variables
# Slack configuration
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
SLACK_SIGNING_SECRET=your-signing-secret
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
# Channel configuration
SLACK_ALERTS_CHANNEL="#alerts"
SLACK_INCIDENTS_CHANNEL="#incidents"
SLACK_CHANGES_CHANNEL="#changes"
SLACK_REPORTS_CHANNEL="#reports"
Channel Mapping
# NopeSight configuration
integrations:
slack:
enabled: true
channel_mappings:
# Event severity to channel mapping
critical: "#incidents"
high: "#alerts"
medium: "#alerts"
low: "#notifications"
notification_rules:
- name: "Critical Infrastructure"
condition: "severity == 'critical' AND ci_type == 'server'"
channel: "#infrastructure-critical"
mentions: ["@oncall", "@infrastructure-team"]
- name: "Security Events"
condition: "category == 'security'"
channel: "#security-alerts"
mentions: ["@security-team"]
formatting:
use_blocks: true
include_charts: true
thread_replies: true
emoji_reactions: true
Slash Commands
Command Registration
// Register slash commands in Slack app
const slashCommands = [
{
command: '/nopesight',
description: 'Interact with NopeSight CMDB and monitoring',
usage_hint: '[status|search|report|help] [options]'
},
{
command: '/cmdb',
description: 'Query configuration items',
usage_hint: '[search|show|update] [query]'
},
{
command: '/incident',
description: 'Manage incidents',
usage_hint: '[create|update|resolve] [options]'
}
];
Command Handler
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import re
app = App(token=SLACK_BOT_TOKEN)
@app.command("/nopesight")
def handle_nopesight_command(ack, body, client):
"""Handle /nopesight commands"""
ack()
command_text = body['text']
user_id = body['user_id']
channel_id = body['channel_id']
# Parse command
parts = command_text.split()
if not parts:
show_help(client, channel_id, user_id)
return
action = parts[0].lower()
args = parts[1:] if len(parts) > 1 else []
# Route to appropriate handler
if action == 'status':
show_system_status(client, channel_id, args)
elif action == 'search':
search_cmdb(client, channel_id, user_id, ' '.join(args))
elif action == 'report':
generate_report(client, channel_id, user_id, args)
elif action == 'help':
show_help(client, channel_id, user_id)
else:
client.chat_postEphemeral(
channel=channel_id,
user=user_id,
text=f"Unknown command: {action}. Type `/nopesight help` for available commands."
)
def show_system_status(client, channel_id, args):
"""Show system status"""
# Get status from NopeSight
status = nopesight_api.get_system_status()
# Build status message
blocks = [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🟢 NopeSight System Status"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*API Status:* {status['api']['status']}"
},
{
"type": "mrkdwn",
"text": f"*Response Time:* {status['api']['response_time']}ms"
},
{
"type": "mrkdwn",
"text": f"*Active Agents:* {status['agents']['active']}/{status['agents']['total']}"
},
{
"type": "mrkdwn",
"text": f"*Open Events:* {status['events']['open']}"
}
]
},
{
"type": "divider"
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": f"Last updated: <!date^{int(time.time())}^{{date_short}} {{time}}|{datetime.now()}>"
}
]
}
]
client.chat_postMessage(
channel=channel_id,
blocks=blocks
)
@app.command("/cmdb")
def handle_cmdb_command(ack, body, client):
"""Handle /cmdb commands"""
ack()
command_text = body['text']
user_id = body['user_id']
# Parse search query
match = re.match(r'(search|show|update)\s+(.*)', command_text)
if not match:
show_cmdb_help(client, body['channel_id'], user_id)
return
action = match.group(1)
query = match.group(2)
if action == 'search':
search_results = nopesight_api.search_ci(query)
display_search_results(client, body['channel_id'], search_results, query)
elif action == 'show':
ci_details = nopesight_api.get_ci(query)
display_ci_details(client, body['channel_id'], ci_details)
elif action == 'update':
# Show update modal
show_update_modal(client, body['trigger_id'], query)
Interactive Components
Message Actions
// Interactive message with actions
const interactiveMessage = {
channel: channelId,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `🚨 *Critical Alert*: ${event.title}\n*Severity:* ${event.severity}\n*Time:* ${event.timestamp}`
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: event.description
},
accessory: {
type: "button",
text: {
type: "plain_text",
text: "View Details"
},
url: `https://nopesight.com/events/${event.id}`
}
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "Acknowledge"
},
style: "primary",
action_id: "acknowledge_event",
value: event.id
},
{
type: "button",
text: {
type: "plain_text",
text: "Create Incident"
},
action_id: "create_incident",
value: event.id
},
{
type: "button",
text: {
type: "plain_text",
text: "Run Playbook"
},
action_id: "run_playbook",
value: event.id
}
]
}
]
};
Modal Forms
@app.action("create_incident")
def handle_create_incident(ack, body, client):
"""Show incident creation modal"""
ack()
event_id = body['actions'][0]['value']
event = nopesight_api.get_event(event_id)
# Open modal
client.views_open(
trigger_id=body['trigger_id'],
view={
"type": "modal",
"callback_id": "incident_modal",
"title": {
"type": "plain_text",
"text": "Create Incident"
},
"submit": {
"type": "plain_text",
"text": "Create"
},
"private_metadata": event_id,
"blocks": [
{
"type": "input",
"block_id": "title",
"element": {
"type": "plain_text_input",
"action_id": "title_input",
"initial_value": event['title']
},
"label": {
"type": "plain_text",
"text": "Incident Title"
}
},
{
"type": "input",
"block_id": "priority",
"element": {
"type": "static_select",
"action_id": "priority_select",
"initial_option": {
"text": {
"type": "plain_text",
"text": "High"
},
"value": "high"
},
"options": [
{
"text": {"type": "plain_text", "text": "Critical"},
"value": "critical"
},
{
"text": {"type": "plain_text", "text": "High"},
"value": "high"
},
{
"text": {"type": "plain_text", "text": "Medium"},
"value": "medium"
},
{
"text": {"type": "plain_text", "text": "Low"},
"value": "low"
}
]
},
"label": {
"type": "plain_text",
"text": "Priority"
}
},
{
"type": "input",
"block_id": "assignee",
"element": {
"type": "users_select",
"action_id": "assignee_select"
},
"label": {
"type": "plain_text",
"text": "Assign To"
},
"optional": true
}
]
}
)
Notification Templates
Event Notifications
class SlackNotificationTemplates:
@staticmethod
def format_event_notification(event):
"""Format event as Slack message"""
# Determine emoji based on severity
emoji_map = {
'critical': '🚨',
'high': '⚠️',
'medium': '📊',
'low': 'ℹ️'
}
emoji = emoji_map.get(event['severity'], '📌')
# Build message blocks
blocks = [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"{emoji} {event['title']}"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Severity:* {event['severity'].upper()}"
},
{
"type": "mrkdwn",
"text": f"*Source:* {event['source']}"
},
{
"type": "mrkdwn",
"text": f"*Time:* <!date^{int(event['timestamp'])}^{{date}} {{time}}|{event['timestamp']}>"
},
{
"type": "mrkdwn",
"text": f"*Status:* {event['status']}"
}
]
}
]
# Add description
if event.get('description'):
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": event['description']
}
})
# Add affected CIs
if event.get('affected_cis'):
ci_list = '\n'.join([f"• <{CI_URL}/{ci['id']}|{ci['name']}>" for ci in event['affected_cis'][:5]])
if len(event['affected_cis']) > 5:
ci_list += f"\n• _and {len(event['affected_cis']) - 5} more..._"
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*Affected CIs:*\n{ci_list}"
}
})
# Add AI insights if available
if event.get('ai_analysis'):
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*AI Analysis:*\n{event['ai_analysis']['summary']}"
}
})
if event['ai_analysis'].get('recommended_action'):
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*Recommended Action:*\n{event['ai_analysis']['recommended_action']}"
}
})
# Add actions
blocks.append({
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Details"
},
"url": f"{WEB_URL}/events/{event['id']}"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Acknowledge"
},
"action_id": "acknowledge_event",
"value": event['id']
}
]
})
return blocks
Report Notifications
// Report generation notification
async function notifyReportComplete(report, channel) {
const message = {
channel: channel,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `📊 *Report Generated Successfully*\n*Report:* ${report.name}\n*Type:* ${report.type}\n*Period:* ${report.period}`
},
accessory: {
type: "image",
image_url: report.preview_url,
alt_text: "Report preview"
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: `*Summary:*\n${report.summary}`
}
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "Download PDF"
},
url: report.pdf_url
},
{
type: "button",
text: {
type: "plain_text",
text: "View Online"
},
url: report.web_url
},
{
type: "button",
text: {
type: "plain_text",
text: "Share"
},
action_id: "share_report",
value: report.id
}
]
}
]
};
// Upload report as file
if (report.attach_file) {
await slack.files.upload({
channels: channel,
file: report.file_path,
filename: `${report.name}.pdf`,
initial_comment: "Full report attached"
});
}
return await slack.chat.postMessage(message);
}
Workflows & Automation
Slack Workflow Integration
# Workflow trigger configuration
workflows:
- name: "Incident Response"
trigger:
type: "webhook"
url: "https://api.nopesight.com/slack/workflows/incident-response"
inputs:
- name: "severity"
type: "select"
options: ["critical", "high", "medium", "low"]
- name: "affected_service"
type: "text"
- name: "description"
type: "text"
actions:
- type: "create_incident"
params:
title: "{{workflow.title}}"
severity: "{{inputs.severity}}"
description: "{{inputs.description}}"
- type: "notify_team"
params:
channel: "#incidents"
mentions: ["@oncall"]
- type: "run_diagnostics"
params:
service: "{{inputs.affected_service}}"
auto_remediate: true
Custom Bot Commands
@app.message(re.compile(r"^nopesight (.*)", re.IGNORECASE))
def handle_ai_query(message, say, client):
"""Handle natural language queries"""
query = message['text'].replace('nopesight ', '', 1)
user = message['user']
channel = message['channel']
# Show typing indicator
client.chat_postMessage(
channel=channel,
text="Thinking... 🤔",
thread_ts=message.get('thread_ts')
)
# Process with AI
try:
response = nopesight_ai.process_query(query, context={
'user': user,
'channel': channel
})
# Format response
if response['type'] == 'data':
# Display data in table/chart
display_data_response(say, response['data'], message.get('thread_ts'))
elif response['type'] == 'action':
# Confirm action
confirm_action(client, channel, user, response['action'], message.get('thread_ts'))
else:
# Text response
say(
text=response['answer'],
thread_ts=message.get('thread_ts'),
mrkdwn=True
)
except Exception as e:
say(
text=f"Sorry, I couldn't process that request: {str(e)}",
thread_ts=message.get('thread_ts')
)
Advanced Features
Scheduled Reports
# Schedule daily summary
@app.event("app_mention")
def handle_mention(event, say):
"""Handle @NopeSight mentions"""
text = event['text']
if 'schedule daily summary' in text.lower():
# Parse time
time_match = re.search(r'at (\d{1,2}):(\d{2})\s*(am|pm)?', text, re.I)
if time_match:
hour = int(time_match.group(1))
minute = int(time_match.group(2))
meridiem = time_match.group(3)
if meridiem:
if meridiem.lower() == 'pm' and hour != 12:
hour += 12
elif meridiem.lower() == 'am' and hour == 12:
hour = 0
# Schedule report
schedule_daily_summary(
channel=event['channel'],
time={'hour': hour, 'minute': minute},
user=event['user']
)
say(f"✅ Daily summary scheduled for {hour:02d}:{minute:02d} in this channel")
Thread-based Incident Management
// Create incident thread
async function createIncidentThread(event) {
// Post initial message
const message = await slack.chat.postMessage({
channel: INCIDENTS_CHANNEL,
text: `New incident: ${event.title}`,
blocks: formatIncidentMessage(event)
});
// Create thread with timeline
await slack.chat.postMessage({
channel: INCIDENTS_CHANNEL,
thread_ts: message.ts,
text: "Incident Timeline:",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `*${new Date().toISOString()}* - Incident created\n*Severity:* ${event.severity}\n*Initial Assessment:* ${event.description}`
}
}
]
});
// Auto-invite relevant users
const relevantUsers = await getRelevantUsers(event);
for (const user of relevantUsers) {
await slack.chat.postMessage({
channel: user,
text: `You've been added to an incident thread: ${event.title}`,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `You've been included in an incident response thread because you're associated with affected systems.`
},
accessory: {
type: "button",
text: {
type: "plain_text",
text: "Go to Thread"
},
url: `https://slack.com/app_redirect?channel=${INCIDENTS_CHANNEL}&thread_ts=${message.ts}`
}
}
]
});
}
return message.ts;
}
Security
Request Verification
from slack_sdk.signature import SignatureVerifier
import hmac
import hashlib
def verify_slack_request(request):
"""Verify request is from Slack"""
signature_verifier = SignatureVerifier(SLACK_SIGNING_SECRET)
# Get headers
timestamp = request.headers.get('X-Slack-Request-Timestamp')
signature = request.headers.get('X-Slack-Signature')
# Verify timestamp (prevent replay attacks)
if abs(time.time() - int(timestamp)) > 60 * 5:
return False
# Verify signature
return signature_verifier.is_valid_request(
request.get_data(),
timestamp,
signature
)
@app.before_request
def before_request():
if not verify_slack_request(request):
abort(403)
Permission Management
# User permission mapping
permissions:
roles:
admin:
commands: ["*"]
actions: ["*"]
operator:
commands: ["status", "search", "report"]
actions: ["acknowledge", "comment", "assign"]
viewer:
commands: ["status", "search"]
actions: ["view"]
user_mappings:
"U12345678": "admin" # Slack user ID to role
"U87654321": "operator"
default: "viewer"
Troubleshooting
Common Issues
troubleshooting:
connection_errors:
symptom: "Bot appears offline"
causes:
- Invalid bot token
- Network connectivity
- Rate limiting
solutions:
- Verify bot token
- Check firewall rules
- Implement exponential backoff
missing_messages:
symptom: "Bot doesn't respond to commands"
causes:
- Missing event subscriptions
- Incorrect permissions
- Bot not in channel
solutions:
- Check event subscriptions
- Verify OAuth scopes
- Invite bot to channel
formatting_issues:
symptom: "Messages appear broken"
causes:
- Invalid block JSON
- Unsupported markdown
- Character encoding
solutions:
- Validate block kit JSON
- Use Slack's markdown subset
- Ensure UTF-8 encoding
Best Practices
1. Message Design
- Use Block Kit for rich formatting
- Keep messages concise
- Use threads for detailed discussions
- Include actionable buttons
2. Performance
- Cache Slack user data
- Batch API calls when possible
- Use async processing for heavy operations
- Implement proper rate limiting
3. User Experience
- Provide clear command help
- Use ephemeral messages for errors
- Acknowledge actions immediately
- Follow up in threads
4. Security
- Always verify requests
- Implement role-based access
- Audit all actions
- Encrypt sensitive data
Example Integration
# Complete Slack integration example
class NopeSightSlackBot:
def __init__(self):
self.app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"]
)
self.nopesight = NopeSightAPI()
self.setup_handlers()
def setup_handlers(self):
"""Set up all event handlers"""
# Commands
self.app.command("/nopesight")(self.handle_nopesight_command)
self.app.command("/cmdb")(self.handle_cmdb_command)
# Actions
self.app.action("acknowledge_event")(self.handle_acknowledge)
self.app.action("create_incident")(self.handle_create_incident)
# Events
self.app.event("app_mention")(self.handle_mention)
self.app.event("message")(self.handle_message)
# Views
self.app.view("incident_modal")(self.handle_incident_submission)
def start(self):
"""Start the bot"""
handler = SocketModeHandler(self.app, os.environ["SLACK_APP_TOKEN"])
handler.start()
# Start bot
if __name__ == "__main__":
bot = NopeSightSlackBot()
bot.start()