Skip to content

Commit 0c2130a

Browse files
committed
Add Cost Optimization Analyzer and Scheduled Job
1 parent 93891d6 commit 0c2130a

File tree

3 files changed

+301
-0
lines changed

3 files changed

+301
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# ServiceNow Instance Cost Optimization Analyzer
2+
3+
## Description
4+
Analyzes ServiceNow instance usage to identify cost optimization opportunities including unused licenses, redundant integrations, and oversized tables.
5+
6+
## Use Case
7+
- Identify unused user licenses for cost savings
8+
- Find redundant or duplicate integrations
9+
- Locate oversized tables affecting performance and storage costs
10+
- Generate cost optimization reports for management
11+
12+
## Features
13+
- **License Analysis**: Finds inactive users with expensive licenses
14+
- **Integration Audit**: Identifies duplicate or unused REST/SOAP integrations
15+
- **Storage Analysis**: Locates tables consuming excessive storage
16+
- **Cost Reporting**: Generates actionable cost-saving recommendations
17+
18+
## Implementation
19+
20+
### 1. Script Include (cost_analyzer.js)
21+
Create a Script Include named `CostOptimizationAnalyzer`
22+
23+
### 2. Scheduled Job (scheduled_job.js)
24+
Run weekly analysis and generate reports
25+
26+
### 3. System Properties
27+
- `cost.analyzer.license.threshold` = 90 (days of inactivity)
28+
- `cost.analyzer.table.size.threshold` = 1000000 (records)
29+
- `cost.analyzer.integration.threshold` = 30 (days unused)
30+
31+
## Output
32+
Returns JSON object with:
33+
```json
34+
{
35+
"unusedLicenses": [{"user": "john.doe", "license": "itil", "lastLogin": "2024-01-15"}],
36+
"redundantIntegrations": [{"name": "Duplicate LDAP", "type": "REST", "lastUsed": "2024-02-01"}],
37+
"oversizedTables": [{"table": "sys_audit", "recordCount": 5000000, "sizeGB": 12.5}],
38+
"totalPotentialSavings": "$15,000/month"
39+
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
2+
var CostOptimizationAnalyzer = Class.create();
3+
CostOptimizationAnalyzer.prototype = {
4+
5+
analyze: function() {
6+
try {
7+
var results = {
8+
unusedLicenses: this.findUnusedLicenses(),
9+
redundantIntegrations: this.findRedundantIntegrations(),
10+
oversizedTables: this.findOversizedTables(),
11+
analysisDate: new GlideDateTime().getDisplayValue(),
12+
totalPotentialSavings: 0
13+
};
14+
15+
results.totalPotentialSavings = this.calculatePotentialSavings(results);
16+
this.logResults(results);
17+
return results;
18+
19+
} catch (e) {
20+
gs.error('Cost Analyzer Error: ' + e.message);
21+
return null;
22+
}
23+
},
24+
25+
findUnusedLicenses: function() {
26+
var unusedLicenses = [];
27+
var threshold = gs.getProperty('cost.analyzer.license.threshold', '90');
28+
var cutoffDate = gs.daysAgoStart(parseInt(threshold));
29+
30+
var userGr = new GlideRecord('sys_user');
31+
userGr.addQuery('active', true);
32+
userGr.addQuery('last_login_time', '<', cutoffDate);
33+
userGr.addNotNullQuery('last_login_time');
34+
userGr.query();
35+
36+
while (userGr.next()) {
37+
var roles = this.getExpensiveRoles(userGr.sys_id.toString());
38+
if (roles.length > 0) {
39+
unusedLicenses.push({
40+
user: userGr.user_name.toString(),
41+
name: userGr.name.toString(),
42+
lastLogin: userGr.last_login_time.getDisplayValue(),
43+
expensiveRoles: roles,
44+
estimatedMonthlyCost: roles.length * 100 // Estimate $100 per role
45+
});
46+
}
47+
}
48+
49+
return unusedLicenses;
50+
},
51+
52+
getExpensiveRoles: function(userId) {
53+
var expensiveRoles = ['itil', 'itil_admin', 'admin', 'security_admin', 'asset'];
54+
var userRoles = [];
55+
56+
var roleGr = new GlideRecord('sys_user_has_role');
57+
roleGr.addQuery('user', userId);
58+
roleGr.query();
59+
60+
while (roleGr.next()) {
61+
var roleName = roleGr.role.name.toString();
62+
if (expensiveRoles.indexOf(roleName) !== -1) {
63+
userRoles.push(roleName);
64+
}
65+
}
66+
67+
return userRoles;
68+
},
69+
70+
findRedundantIntegrations: function() {
71+
var redundantIntegrations = [];
72+
var threshold = gs.getProperty('cost.analyzer.integration.threshold', '30');
73+
var cutoffDate = gs.daysAgoStart(parseInt(threshold));
74+
75+
// Check REST Messages
76+
var restGr = new GlideRecord('sys_rest_message');
77+
restGr.query();
78+
79+
while (restGr.next()) {
80+
var lastUsed = this.getIntegrationLastUsed(restGr.sys_id.toString(), 'rest');
81+
if (lastUsed && lastUsed < cutoffDate) {
82+
redundantIntegrations.push({
83+
name: restGr.name.toString(),
84+
type: 'REST',
85+
endpoint: restGr.endpoint.toString(),
86+
lastUsed: lastUsed,
87+
status: 'Potentially Unused'
88+
});
89+
}
90+
}
91+
92+
// Check for duplicate endpoints
93+
var duplicates = this.findDuplicateEndpoints();
94+
redundantIntegrations = redundantIntegrations.concat(duplicates);
95+
96+
return redundantIntegrations;
97+
},
98+
99+
getIntegrationLastUsed: function(integrationId, type) {
100+
var logGr = new GlideRecord('syslog');
101+
logGr.addQuery('message', 'CONTAINS', integrationId);
102+
logGr.orderByDesc('sys_created_on');
103+
logGr.setLimit(1);
104+
logGr.query();
105+
106+
if (logGr.next()) {
107+
return logGr.sys_created_on.getDisplayValue();
108+
}
109+
return null;
110+
},
111+
112+
findDuplicateEndpoints: function() {
113+
var duplicates = [];
114+
var endpoints = {};
115+
116+
var restGr = new GlideRecord('sys_rest_message');
117+
restGr.query();
118+
119+
while (restGr.next()) {
120+
var endpoint = restGr.endpoint.toString();
121+
if (endpoints[endpoint]) {
122+
duplicates.push({
123+
name: restGr.name.toString(),
124+
type: 'REST',
125+
endpoint: endpoint,
126+
status: 'Duplicate Endpoint',
127+
duplicateOf: endpoints[endpoint]
128+
});
129+
} else {
130+
endpoints[endpoint] = restGr.name.toString();
131+
}
132+
}
133+
134+
return duplicates;
135+
},
136+
137+
findOversizedTables: function() {
138+
var oversizedTables = [];
139+
var threshold = gs.getProperty('cost.analyzer.table.size.threshold', '1000000');
140+
141+
var tableGr = new GlideRecord('sys_db_object');
142+
tableGr.addQuery('name', 'STARTSWITH', 'u_'); // Custom tables
143+
tableGr.query();
144+
145+
while (tableGr.next()) {
146+
var tableName = tableGr.name.toString();
147+
var recordCount = this.getTableRecordCount(tableName);
148+
149+
if (recordCount > parseInt(threshold)) {
150+
var sizeInfo = this.estimateTableSize(tableName, recordCount);
151+
oversizedTables.push({
152+
table: tableName,
153+
recordCount: recordCount,
154+
estimatedSizeGB: sizeInfo.sizeGB,
155+
recommendation: sizeInfo.recommendation
156+
});
157+
}
158+
}
159+
160+
// Check system tables that commonly grow large
161+
var systemTables = ['sys_audit', 'sys_email', 'syslog', 'sys_attachment'];
162+
systemTables.forEach(function(tableName) {
163+
var recordCount = this.getTableRecordCount(tableName);
164+
if (recordCount > parseInt(threshold)) {
165+
var sizeInfo = this.estimateTableSize(tableName, recordCount);
166+
oversizedTables.push({
167+
table: tableName,
168+
recordCount: recordCount,
169+
estimatedSizeGB: sizeInfo.sizeGB,
170+
recommendation: sizeInfo.recommendation
171+
});
172+
}
173+
}.bind(this));
174+
175+
return oversizedTables;
176+
},
177+
178+
getTableRecordCount: function(tableName) {
179+
try {
180+
var countGr = new GlideAggregate(tableName);
181+
countGr.addAggregate('COUNT');
182+
countGr.query();
183+
184+
if (countGr.next()) {
185+
return parseInt(countGr.getAggregate('COUNT'));
186+
}
187+
} catch (e) {
188+
gs.debug('Cannot count records for table: ' + tableName);
189+
}
190+
return 0;
191+
},
192+
193+
estimateTableSize: function(tableName, recordCount) {
194+
var avgRecordSize = 2; // KB per record (estimate)
195+
var sizeGB = (recordCount * avgRecordSize) / (1024 * 1024);
196+
197+
var recommendation = 'Consider archiving old records';
198+
if (tableName === 'sys_audit') {
199+
recommendation = 'Configure audit retention policy';
200+
} else if (tableName === 'sys_email') {
201+
recommendation = 'Clean up old email records';
202+
} else if (tableName === 'syslog') {
203+
recommendation = 'Reduce log retention period';
204+
}
205+
206+
return {
207+
sizeGB: Math.round(sizeGB * 100) / 100,
208+
recommendation: recommendation
209+
};
210+
},
211+
212+
calculatePotentialSavings: function(results) {
213+
var totalSavings = 0;
214+
215+
// License savings
216+
results.unusedLicenses.forEach(function(license) {
217+
totalSavings += license.estimatedMonthlyCost || 0;
218+
});
219+
220+
// Storage savings (estimate $10 per GB per month)
221+
results.oversizedTables.forEach(function(table) {
222+
totalSavings += (table.estimatedSizeGB * 10);
223+
});
224+
225+
return '$' + totalSavings.toLocaleString() + '/month';
226+
},
227+
228+
logResults: function(results) {
229+
gs.info('=== Cost Optimization Analysis Results ===');
230+
gs.info('Unused Licenses Found: ' + results.unusedLicenses.length);
231+
gs.info('Redundant Integrations: ' + results.redundantIntegrations.length);
232+
gs.info('Oversized Tables: ' + results.oversizedTables.length);
233+
gs.info('Potential Monthly Savings: ' + results.totalPotentialSavings);
234+
gs.info('========================================');
235+
},
236+
237+
type: 'CostOptimizationAnalyzer'
238+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Scheduled Job Script - Run Weekly
2+
// Name: Weekly Cost Optimization Analysis
3+
4+
try {
5+
var analyzer = new CostOptimizationAnalyzer();
6+
var results = analyzer.analyze();
7+
8+
if (results) {
9+
// Store results in a custom table or send email report
10+
gs.info('Cost optimization analysis completed successfully');
11+
12+
var emailBody = 'Cost Optimization Report:\n\n';
13+
emailBody += 'Unused Licenses: ' + results.unusedLicenses.length + '\n';
14+
emailBody += 'Redundant Integrations: ' + results.redundantIntegrations.length + '\n';
15+
emailBody += 'Oversized Tables: ' + results.oversizedTables.length + '\n';
16+
emailBody += 'Potential Savings: ' + results.totalPotentialSavings + '\n';
17+
18+
// below line will send to email
19+
gs.eventQueue('cost.optimization.report', null, emailBody);
20+
}
21+
22+
} catch (e) {
23+
gs.error('Scheduled cost analysis failed: ' + e.message);
24+
}

0 commit comments

Comments
 (0)