From f2fbb936984d3854464fc6be0c7e449a49e61370 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 6 Aug 2025 14:44:47 +0530 Subject: [PATCH] feat: add better stack status tool --- config/agents/tools.yml | 5 + .../captain/tools/better_stack_status_tool.rb | 138 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 enterprise/lib/captain/tools/better_stack_status_tool.rb diff --git a/config/agents/tools.yml b/config/agents/tools.yml index c2faf75e7..0d42b3baa 100644 --- a/config/agents/tools.yml +++ b/config/agents/tools.yml @@ -34,3 +34,8 @@ title: 'Handoff to Human' description: 'Hand off the conversation to a human agent' icon: 'user-switch' + +- id: better_stack_status + title: 'Better Stack Status' + description: 'Check the status of Better Stack services' + icon: 'server' diff --git a/enterprise/lib/captain/tools/better_stack_status_tool.rb b/enterprise/lib/captain/tools/better_stack_status_tool.rb new file mode 100644 index 000000000..7e0638f15 --- /dev/null +++ b/enterprise/lib/captain/tools/better_stack_status_tool.rb @@ -0,0 +1,138 @@ +class Captain::Tools::BetterStackStatusTool < Captain::Tools::BasePublicTool + description 'Check service status using Better Stack' + param :service, type: 'string', desc: 'The service name to check status for (optional)' + + def perform(_tool_context, service: nil) + log_tool_usage('checking_status', { service: service }) + + begin + status_data = fetch_better_stack_status(service) + incidents_data = fetch_better_stack_incidents(service) + format_status_response(status_data, incidents_data, service) + rescue StandardError => e + log_tool_usage('error', { service: service, error: e.message }) + "Error checking Better Stack status: #{e.message}" + end + end + + private + + def fetch_better_stack_status(service) + # API token should be configured in environment variables or secrets + api_token = ENV.fetch('BETTER_STACK_TOKEN') + raise 'Better Stack API token not configured' if api_token.blank? + + # Base URL for Better Stack Status API + base_url = 'https://uptime.betterstack.com/api/v2' + + # Endpoint depends on whether we're checking a specific service or overall status + endpoint = if service.present? + "/monitors?search=#{CGI.escape(service)}" + else + '/monitors' + end + + make_api_request(base_url, endpoint, api_token) + end + + def fetch_better_stack_incidents(service) + api_token = ENV.fetch('BETTER_STACK_TOKEN') + raise 'Better Stack API token not configured' if api_token.blank? + + base_url = 'https://uptime.betterstack.com/api/v2' + + # Endpoint for incidents, filter by service if provided + endpoint = if service.present? + "/incidents?monitor_name=#{CGI.escape(service)}" + else + '/incidents' + end + + make_api_request(base_url, endpoint, api_token) + end + + def make_api_request(base_url, endpoint, api_token) + uri = URI("#{base_url}#{endpoint}") + request = Net::HTTP::Get.new(uri) + request['Authorization'] = "Bearer #{api_token}" + + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + + response = http.request(request) + + raise "API request failed with status #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess) + + JSON.parse(response.body) + end + + def format_status_response(status_data, incidents_data, service) + monitors = status_data['data'] + incidents = incidents_data['data'] + + if monitors.empty? + return service.present? ? "No services found matching '#{service}'" : 'No services configured in Better Stack' + end + + result = if service.present? + "Status for services matching '#{service}':\n\n" + else + "Current Better Stack Status:\n\n" + end + + result += format_monitors(monitors) + + # Add incidents information if any exist + if incidents.present? + result += "\n\n🚨 Active Incidents (#{incidents.size}):\n\n" + result += format_incidents(incidents) + end + + result + end + + def format_monitors(monitors) + monitors.map do |monitor| + attributes = monitor['attributes'] + status = attributes['status'] + status_emoji = case status + when 'up' + 'āœ…' + when 'down' + 'āŒ' + else + 'āš ļø' + end + + last_check = begin + Time.zone.parse(attributes['last_check_at']) + rescue StandardError + 'Unknown' + end + last_check_formatted = last_check.is_a?(Time) ? last_check.strftime('%Y-%m-%d %H:%M:%S') : last_check + + "#{status_emoji} #{attributes['url']}: #{status.upcase} (Last checked: #{last_check_formatted})" + end.join("\n") + end + + def format_incidents(incidents) + incidents.map do |incident| + attributes = incident['attributes'] + # Format start time + started_at = begin + Time.zone.parse(attributes['started_at']) + rescue StandardError + 'Unknown' + end + started_at_formatted = started_at.is_a?(Time) ? started_at.strftime('%Y-%m-%d %H:%M:%S') : started_at + + # Get monitor name and incident status + status = attributes['status'] || 'unknown' + + # Format incident details + "šŸ”“ [#{status.upcase}] #{attributes['name']} - #{attributes['title']}\n " \ + "Started: #{started_at_formatted}\n " \ + "#{attributes['url'] ? "Details: #{attributes['url']}" : ''}" + end.join("\n\n") + end +end