From 0c2130aee5075ca11a93518e243364130f88814a Mon Sep 17 00:00:00 2001 From: pratap360 Date: Sat, 25 Oct 2025 16:09:24 +0530 Subject: [PATCH] Add Cost Optimization Analyzer and Scheduled Job --- Specialized Areas/Cost Optimization/README.md | 39 +++ .../Cost Optimization/cost_analyzer.js | 238 ++++++++++++++++++ .../Cost Optimization/scheduled_job.js | 24 ++ 3 files changed, 301 insertions(+) create mode 100644 Specialized Areas/Cost Optimization/README.md create mode 100644 Specialized Areas/Cost Optimization/cost_analyzer.js create mode 100644 Specialized Areas/Cost Optimization/scheduled_job.js diff --git a/Specialized Areas/Cost Optimization/README.md b/Specialized Areas/Cost Optimization/README.md new file mode 100644 index 0000000000..0ded5f8d01 --- /dev/null +++ b/Specialized Areas/Cost Optimization/README.md @@ -0,0 +1,39 @@ +# ServiceNow Instance Cost Optimization Analyzer + +## Description +Analyzes ServiceNow instance usage to identify cost optimization opportunities including unused licenses, redundant integrations, and oversized tables. + +## Use Case +- Identify unused user licenses for cost savings +- Find redundant or duplicate integrations +- Locate oversized tables affecting performance and storage costs +- Generate cost optimization reports for management + +## Features +- **License Analysis**: Finds inactive users with expensive licenses +- **Integration Audit**: Identifies duplicate or unused REST/SOAP integrations +- **Storage Analysis**: Locates tables consuming excessive storage +- **Cost Reporting**: Generates actionable cost-saving recommendations + +## Implementation + +### 1. Script Include (cost_analyzer.js) +Create a Script Include named `CostOptimizationAnalyzer` + +### 2. Scheduled Job (scheduled_job.js) +Run weekly analysis and generate reports + +### 3. System Properties +- `cost.analyzer.license.threshold` = 90 (days of inactivity) +- `cost.analyzer.table.size.threshold` = 1000000 (records) +- `cost.analyzer.integration.threshold` = 30 (days unused) + +## Output +Returns JSON object with: +```json +{ + "unusedLicenses": [{"user": "john.doe", "license": "itil", "lastLogin": "2024-01-15"}], + "redundantIntegrations": [{"name": "Duplicate LDAP", "type": "REST", "lastUsed": "2024-02-01"}], + "oversizedTables": [{"table": "sys_audit", "recordCount": 5000000, "sizeGB": 12.5}], + "totalPotentialSavings": "$15,000/month" +} diff --git a/Specialized Areas/Cost Optimization/cost_analyzer.js b/Specialized Areas/Cost Optimization/cost_analyzer.js new file mode 100644 index 0000000000..287a2801dd --- /dev/null +++ b/Specialized Areas/Cost Optimization/cost_analyzer.js @@ -0,0 +1,238 @@ + +var CostOptimizationAnalyzer = Class.create(); +CostOptimizationAnalyzer.prototype = { + + analyze: function() { + try { + var results = { + unusedLicenses: this.findUnusedLicenses(), + redundantIntegrations: this.findRedundantIntegrations(), + oversizedTables: this.findOversizedTables(), + analysisDate: new GlideDateTime().getDisplayValue(), + totalPotentialSavings: 0 + }; + + results.totalPotentialSavings = this.calculatePotentialSavings(results); + this.logResults(results); + return results; + + } catch (e) { + gs.error('Cost Analyzer Error: ' + e.message); + return null; + } + }, + + findUnusedLicenses: function() { + var unusedLicenses = []; + var threshold = gs.getProperty('cost.analyzer.license.threshold', '90'); + var cutoffDate = gs.daysAgoStart(parseInt(threshold)); + + var userGr = new GlideRecord('sys_user'); + userGr.addQuery('active', true); + userGr.addQuery('last_login_time', '<', cutoffDate); + userGr.addNotNullQuery('last_login_time'); + userGr.query(); + + while (userGr.next()) { + var roles = this.getExpensiveRoles(userGr.sys_id.toString()); + if (roles.length > 0) { + unusedLicenses.push({ + user: userGr.user_name.toString(), + name: userGr.name.toString(), + lastLogin: userGr.last_login_time.getDisplayValue(), + expensiveRoles: roles, + estimatedMonthlyCost: roles.length * 100 // Estimate $100 per role + }); + } + } + + return unusedLicenses; + }, + + getExpensiveRoles: function(userId) { + var expensiveRoles = ['itil', 'itil_admin', 'admin', 'security_admin', 'asset']; + var userRoles = []; + + var roleGr = new GlideRecord('sys_user_has_role'); + roleGr.addQuery('user', userId); + roleGr.query(); + + while (roleGr.next()) { + var roleName = roleGr.role.name.toString(); + if (expensiveRoles.indexOf(roleName) !== -1) { + userRoles.push(roleName); + } + } + + return userRoles; + }, + + findRedundantIntegrations: function() { + var redundantIntegrations = []; + var threshold = gs.getProperty('cost.analyzer.integration.threshold', '30'); + var cutoffDate = gs.daysAgoStart(parseInt(threshold)); + + // Check REST Messages + var restGr = new GlideRecord('sys_rest_message'); + restGr.query(); + + while (restGr.next()) { + var lastUsed = this.getIntegrationLastUsed(restGr.sys_id.toString(), 'rest'); + if (lastUsed && lastUsed < cutoffDate) { + redundantIntegrations.push({ + name: restGr.name.toString(), + type: 'REST', + endpoint: restGr.endpoint.toString(), + lastUsed: lastUsed, + status: 'Potentially Unused' + }); + } + } + + // Check for duplicate endpoints + var duplicates = this.findDuplicateEndpoints(); + redundantIntegrations = redundantIntegrations.concat(duplicates); + + return redundantIntegrations; + }, + + getIntegrationLastUsed: function(integrationId, type) { + var logGr = new GlideRecord('syslog'); + logGr.addQuery('message', 'CONTAINS', integrationId); + logGr.orderByDesc('sys_created_on'); + logGr.setLimit(1); + logGr.query(); + + if (logGr.next()) { + return logGr.sys_created_on.getDisplayValue(); + } + return null; + }, + + findDuplicateEndpoints: function() { + var duplicates = []; + var endpoints = {}; + + var restGr = new GlideRecord('sys_rest_message'); + restGr.query(); + + while (restGr.next()) { + var endpoint = restGr.endpoint.toString(); + if (endpoints[endpoint]) { + duplicates.push({ + name: restGr.name.toString(), + type: 'REST', + endpoint: endpoint, + status: 'Duplicate Endpoint', + duplicateOf: endpoints[endpoint] + }); + } else { + endpoints[endpoint] = restGr.name.toString(); + } + } + + return duplicates; + }, + + findOversizedTables: function() { + var oversizedTables = []; + var threshold = gs.getProperty('cost.analyzer.table.size.threshold', '1000000'); + + var tableGr = new GlideRecord('sys_db_object'); + tableGr.addQuery('name', 'STARTSWITH', 'u_'); // Custom tables + tableGr.query(); + + while (tableGr.next()) { + var tableName = tableGr.name.toString(); + var recordCount = this.getTableRecordCount(tableName); + + if (recordCount > parseInt(threshold)) { + var sizeInfo = this.estimateTableSize(tableName, recordCount); + oversizedTables.push({ + table: tableName, + recordCount: recordCount, + estimatedSizeGB: sizeInfo.sizeGB, + recommendation: sizeInfo.recommendation + }); + } + } + + // Check system tables that commonly grow large + var systemTables = ['sys_audit', 'sys_email', 'syslog', 'sys_attachment']; + systemTables.forEach(function(tableName) { + var recordCount = this.getTableRecordCount(tableName); + if (recordCount > parseInt(threshold)) { + var sizeInfo = this.estimateTableSize(tableName, recordCount); + oversizedTables.push({ + table: tableName, + recordCount: recordCount, + estimatedSizeGB: sizeInfo.sizeGB, + recommendation: sizeInfo.recommendation + }); + } + }.bind(this)); + + return oversizedTables; + }, + + getTableRecordCount: function(tableName) { + try { + var countGr = new GlideAggregate(tableName); + countGr.addAggregate('COUNT'); + countGr.query(); + + if (countGr.next()) { + return parseInt(countGr.getAggregate('COUNT')); + } + } catch (e) { + gs.debug('Cannot count records for table: ' + tableName); + } + return 0; + }, + + estimateTableSize: function(tableName, recordCount) { + var avgRecordSize = 2; // KB per record (estimate) + var sizeGB = (recordCount * avgRecordSize) / (1024 * 1024); + + var recommendation = 'Consider archiving old records'; + if (tableName === 'sys_audit') { + recommendation = 'Configure audit retention policy'; + } else if (tableName === 'sys_email') { + recommendation = 'Clean up old email records'; + } else if (tableName === 'syslog') { + recommendation = 'Reduce log retention period'; + } + + return { + sizeGB: Math.round(sizeGB * 100) / 100, + recommendation: recommendation + }; + }, + + calculatePotentialSavings: function(results) { + var totalSavings = 0; + + // License savings + results.unusedLicenses.forEach(function(license) { + totalSavings += license.estimatedMonthlyCost || 0; + }); + + // Storage savings (estimate $10 per GB per month) + results.oversizedTables.forEach(function(table) { + totalSavings += (table.estimatedSizeGB * 10); + }); + + return '$' + totalSavings.toLocaleString() + '/month'; + }, + + logResults: function(results) { + gs.info('=== Cost Optimization Analysis Results ==='); + gs.info('Unused Licenses Found: ' + results.unusedLicenses.length); + gs.info('Redundant Integrations: ' + results.redundantIntegrations.length); + gs.info('Oversized Tables: ' + results.oversizedTables.length); + gs.info('Potential Monthly Savings: ' + results.totalPotentialSavings); + gs.info('========================================'); + }, + + type: 'CostOptimizationAnalyzer' +}; diff --git a/Specialized Areas/Cost Optimization/scheduled_job.js b/Specialized Areas/Cost Optimization/scheduled_job.js new file mode 100644 index 0000000000..ee7c6662a2 --- /dev/null +++ b/Specialized Areas/Cost Optimization/scheduled_job.js @@ -0,0 +1,24 @@ +// Scheduled Job Script - Run Weekly +// Name: Weekly Cost Optimization Analysis + +try { + var analyzer = new CostOptimizationAnalyzer(); + var results = analyzer.analyze(); + + if (results) { + // Store results in a custom table or send email report + gs.info('Cost optimization analysis completed successfully'); + + var emailBody = 'Cost Optimization Report:\n\n'; + emailBody += 'Unused Licenses: ' + results.unusedLicenses.length + '\n'; + emailBody += 'Redundant Integrations: ' + results.redundantIntegrations.length + '\n'; + emailBody += 'Oversized Tables: ' + results.oversizedTables.length + '\n'; + emailBody += 'Potential Savings: ' + results.totalPotentialSavings + '\n'; + + // below line will send to email + gs.eventQueue('cost.optimization.report', null, emailBody); + } + +} catch (e) { + gs.error('Scheduled cost analysis failed: ' + e.message); +}