Coverage for scripts / check_sla_breach.py: 12%

139 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-25 18:08 -0500

1#!/usr/bin/env python3 

2""" 

3Check JSM service request SLA breach status. 

4 

5Usage: 

6 python check_sla_breach.py SD-123 

7 python check_sla_breach.py SD-123 --threshold 30 

8 python check_sla_breach.py SD-123 --sla-name "Time to First Response" 

9 python check_sla_breach.py SD-123 --output json 

10""" 

11 

12import sys 

13import os 

14import argparse 

15import json 

16from pathlib import Path 

17from typing import Optional, Dict, Any, List 

18 

19sys.path.insert(0, str(Path(__file__).parent.parent.parent / 'shared' / 'scripts' / 'lib')) 

20 

21from config_manager import get_jira_client 

22from error_handler import print_error, JiraError, NotFoundError 

23from formatters import print_success 

24from jsm_utils import ( 

25 format_sla_time, format_duration, get_sla_status_emoji, 

26 get_sla_status_text, is_sla_at_risk 

27) 

28 

29 

30def check_sla_breach(issue_key: str, threshold: float = 20.0, 

31 sla_name: Optional[str] = None, 

32 profile: Optional[str] = None) -> Dict[str, Any]: 

33 """ 

34 Check SLA breach status for a service request. 

35 

36 Args: 

37 issue_key: Request key (e.g., 'SD-123') 

38 threshold: At-risk threshold percentage (default 20%) 

39 sla_name: Filter to specific SLA name (optional) 

40 profile: JIRA profile to use 

41 

42 Returns: 

43 Dict with breach status, SLAs, and statistics 

44 

45 Raises: 

46 NotFoundError: If request doesn't exist 

47 """ 

48 with get_jira_client(profile) as client: 

49 sla_data = client.get_request_slas(issue_key) 

50 

51 slas = sla_data.get('values', []) 

52 

53 # Filter by SLA name if specified 

54 if sla_name: 

55 slas = [sla for sla in slas if sla.get('name') == sla_name] 

56 

57 # Analyze each SLA 

58 breached = [] 

59 at_risk = [] 

60 paused = [] 

61 met = [] 

62 

63 for sla in slas: 

64 ongoing = sla.get('ongoingCycle') 

65 

66 if ongoing: 

67 if ongoing.get('breached'): 

68 breached.append(sla) 

69 elif ongoing.get('paused'): 

70 paused.append(sla) 

71 else: 

72 remaining = ongoing.get('remainingTime', {}).get('millis', 0) 

73 goal = ongoing.get('goalDuration', {}).get('millis', 0) 

74 if is_sla_at_risk(remaining, goal, threshold): 

75 at_risk.append(sla) 

76 else: 

77 # Check completed cycles 

78 completed = sla.get('completedCycles', []) 

79 if completed: 

80 last_cycle = completed[-1] 

81 if last_cycle.get('breached'): 

82 breached.append(sla) 

83 else: 

84 met.append(sla) 

85 

86 return { 

87 'issue_key': issue_key, 

88 'total_slas': len(slas), 

89 'breached': breached, 

90 'at_risk': at_risk, 

91 'paused': paused, 

92 'met': met, 

93 'threshold': threshold, 

94 'has_breach': len(breached) > 0, 

95 'has_risk': len(at_risk) > 0 

96 } 

97 

98 

99def format_breach_text(result: Dict[str, Any]) -> str: 

100 """Format breach check result as text output.""" 

101 lines = [] 

102 

103 lines.append(f"\nSLA Status for {result['issue_key']}:") 

104 lines.append("=" * 60) 

105 lines.append("") 

106 

107 # Breached SLAs 

108 if result['breached']: 

109 lines.append(f"✗ BREACHED ({len(result['breached'])} SLA{'s' if len(result['breached']) > 1 else ''}):") 

110 for sla in result['breached']: 

111 name = sla.get('name') 

112 ongoing = sla.get('ongoingCycle') 

113 if ongoing: 

114 elapsed = format_duration(ongoing.get('elapsedTime')) 

115 goal = format_duration(ongoing.get('goalDuration')) 

116 lines.append(f"{name}") 

117 lines.append(f" Goal: {goal}") 

118 lines.append(f" Elapsed: {elapsed}") 

119 else: 

120 completed = sla.get('completedCycles', []) 

121 if completed: 

122 last = completed[-1] 

123 elapsed = format_duration(last.get('elapsedTime')) 

124 goal = format_duration(last.get('goalDuration')) 

125 lines.append(f"{name}") 

126 lines.append(f" Goal: {goal}") 

127 lines.append(f" Actual: {elapsed}") 

128 lines.append("") 

129 

130 # At-risk SLAs 

131 if result['at_risk']: 

132 lines.append(f"⚠ AT RISK ({len(result['at_risk'])} SLA{'s' if len(result['at_risk']) > 1 else ''}):") 

133 for sla in result['at_risk']: 

134 name = sla.get('name') 

135 ongoing = sla.get('ongoingCycle') 

136 elapsed = format_duration(ongoing.get('elapsedTime')) 

137 remaining = format_duration(ongoing.get('remainingTime')) 

138 breach_time = format_sla_time(ongoing.get('breachTime')) 

139 remaining_millis = ongoing.get('remainingTime', {}).get('millis', 0) 

140 goal_millis = ongoing.get('goalDuration', {}).get('millis', 0) 

141 percentage = (remaining_millis / goal_millis * 100) if goal_millis > 0 else 0 

142 

143 lines.append(f"{name}") 

144 lines.append(f" Elapsed: {elapsed}") 

145 lines.append(f" Remaining: {remaining} ({percentage:.1f}% of goal)") 

146 lines.append(f" Breach at: {breach_time}") 

147 lines.append(f" Warning: Less than {result['threshold']}% time remaining") 

148 lines.append("") 

149 

150 # Paused SLAs 

151 if result['paused']: 

152 lines.append(f"⏸ PAUSED ({len(result['paused'])} SLA{'s' if len(result['paused']) > 1 else ''}):") 

153 for sla in result['paused']: 

154 name = sla.get('name') 

155 lines.append(f"{name}") 

156 lines.append("") 

157 

158 # Met SLAs 

159 if result['met']: 

160 lines.append(f"✓ MET ({len(result['met'])} SLA{'s' if len(result['met']) > 1 else ''}):") 

161 for sla in result['met']: 

162 name = sla.get('name') 

163 completed = sla.get('completedCycles', []) 

164 if completed: 

165 last = completed[-1] 

166 elapsed = format_duration(last.get('elapsedTime')) 

167 lines.append(f"{name} (completed in {elapsed})") 

168 lines.append("") 

169 

170 # Overall status 

171 if result['has_breach']: 

172 lines.append("Overall Status: BREACHED") 

173 lines.append(f"Exit code: 1") 

174 elif result['has_risk']: 

175 lines.append("Overall Status: AT RISK") 

176 lines.append(f"Exit code: 0 (use --fail-on-risk for non-zero exit)") 

177 else: 

178 lines.append("Overall Status: ALL CLEAR") 

179 lines.append(f"Exit code: 0") 

180 

181 return '\n'.join(lines) 

182 

183 

184def format_breach_json(result: Dict[str, Any]) -> str: 

185 """Format breach check result as JSON output.""" 

186 # Convert to JSON-serializable format 

187 output = { 

188 'issue_key': result['issue_key'], 

189 'total_slas': result['total_slas'], 

190 'threshold': result['threshold'], 

191 'has_breach': result['has_breach'], 

192 'has_risk': result['has_risk'], 

193 'breached_count': len(result['breached']), 

194 'at_risk_count': len(result['at_risk']), 

195 'paused_count': len(result['paused']), 

196 'met_count': len(result['met']), 

197 'breached': [{'name': s.get('name'), 'id': s.get('id')} for s in result['breached']], 

198 'at_risk': [{'name': s.get('name'), 'id': s.get('id')} for s in result['at_risk']], 

199 'paused': [{'name': s.get('name'), 'id': s.get('id')} for s in result['paused']], 

200 'met': [{'name': s.get('name'), 'id': s.get('id')} for s in result['met']] 

201 } 

202 return json.dumps(output, indent=2) 

203 

204 

205def main(): 

206 """Main entry point.""" 

207 parser = argparse.ArgumentParser( 

208 description='Check JSM service request SLA breach status', 

209 formatter_class=argparse.RawDescriptionHelpFormatter, 

210 epilog=""" 

211Examples: 

212 Check SLA status: 

213 %(prog)s SD-123 

214 

215 Custom at-risk threshold: 

216 %(prog)s SD-123 --threshold 30 

217 

218 Check specific SLA: 

219 %(prog)s SD-123 --sla-name "Time to First Response" 

220 

221 JSON output: 

222 %(prog)s SD-123 --output json 

223 """ 

224 ) 

225 

226 parser.add_argument('request_key', 

227 help='Request key (e.g., SD-123)') 

228 parser.add_argument('--threshold', type=float, default=20.0, 

229 help='At-risk threshold percentage (default: 20%%)') 

230 parser.add_argument('--sla-name', 

231 help='Check specific SLA only') 

232 parser.add_argument('--fail-on-risk', action='store_true', 

233 help='Exit with code 1 if any SLA is at risk') 

234 parser.add_argument('--output', choices=['text', 'json'], default='text', 

235 help='Output format (default: text)') 

236 parser.add_argument('--profile', 

237 help='JIRA profile to use from config') 

238 

239 args = parser.parse_args() 

240 

241 try: 

242 result = check_sla_breach( 

243 issue_key=args.request_key, 

244 threshold=args.threshold, 

245 sla_name=args.sla_name, 

246 profile=args.profile 

247 ) 

248 

249 if args.output == 'json': 

250 print(format_breach_json(result)) 

251 else: 

252 print(format_breach_text(result)) 

253 

254 # Determine exit code 

255 if result['has_breach']: 

256 return 1 

257 if args.fail_on_risk and result['has_risk']: 

258 return 1 

259 

260 return 0 

261 

262 except NotFoundError as e: 

263 print_error(f"Request not found: {e}") 

264 return 1 

265 except JiraError as e: 

266 print_error(f"Failed to check SLA: {e}") 

267 return 1 

268 except Exception as e: 

269 print_error(f"Unexpected error: {e}") 

270 return 1 

271 

272 

273if __name__ == '__main__': 

274 sys.exit(main())