@@ -69,7 +69,7 @@ def get_metrics_azure_sdk(client, resource_id, metric_name, start_time, end_time
6969 timespan = f"{ start_time } /{ end_time } " ,
7070 interval = "PT1M" , # 1-minute intervals
7171 metricnames = metric_name ,
72- aggregation = "Average,Maximum, Total" ,
72+ aggregation = "Total" ,
7373 )
7474 return metrics_data
7575
@@ -86,24 +86,17 @@ def aggregate_hourly_data(metrics_data, metric_name):
8686 hourly_aggregates = {}
8787
8888 for point in data_points :
89- # SDK format - use time_stamp attribute
89+ if point .total is None :
90+ continue
9091 timestamp = point .time_stamp
9192 hour_key = timestamp .replace (minute = 0 , second = 0 , microsecond = 0 )
92-
9393 if hour_key not in hourly_aggregates :
9494 hourly_aggregates [hour_key ] = {
9595 'timestamp' : hour_key .isoformat ().replace ('+00:00' , 'Z' ),
96- 'values' : [],
9796 'totals' : []
9897 }
99-
100- if metric_name == "system.interface.total_bytes" :
101- total_val = point .total or 0
102- hourly_aggregates [hour_key ]['totals' ].append (total_val )
103- else :
104- avg_val = point .average or 0
105- max_val = point .maximum or 0
106- hourly_aggregates [hour_key ]['values' ].append (max_val if max_val > 0 else avg_val )
98+ total_val = point .total
99+ hourly_aggregates [hour_key ]['totals' ].append (total_val )
107100
108101 result = []
109102 for hour_key in sorted (hourly_aggregates .keys ()):
@@ -112,7 +105,7 @@ def aggregate_hourly_data(metrics_data, metric_name):
112105 if metric_name == "system.interface.total_bytes" :
113106 final_value = sum (hour_data ['totals' ])
114107 else :
115- final_value = max (hour_data ['values ' ]) if hour_data ['values ' ] else 0
108+ final_value = max (hour_data ['totals ' ]) if hour_data ['totals ' ] else 0
116109
117110 result .append ({
118111 'timestamp' : hour_data ['timestamp' ],
@@ -141,59 +134,48 @@ def calculate_cost_breakdown(date_range, resource_id, location, credential, subs
141134 waf_metrics_raw = get_metrics_azure_sdk (client , resource_id , "waf.enabled" , start_time , end_time )
142135 ports_metrics_raw = get_metrics_azure_sdk (client , resource_id , "ports.used" , start_time , end_time )
143136 ncu_metrics_raw = get_metrics_azure_sdk (client , resource_id , "ncu.provisioned" , start_time , end_time )
144- network_metrics_raw = get_metrics_azure_sdk (client , resource_id , "system.interface.total_bytes" , start_time , end_time )
137+ data_processed_metrics_raw = get_metrics_azure_sdk (client , resource_id , "system.interface.total_bytes" , start_time , end_time )
145138
146139 waf_hourly = aggregate_hourly_data (waf_metrics_raw , "waf.enabled" )
147140 ports_hourly = aggregate_hourly_data (ports_metrics_raw , "ports.used" )
148141 ncu_hourly = aggregate_hourly_data (ncu_metrics_raw , "ncu.provisioned" )
149- network_hourly = aggregate_hourly_data (network_metrics_raw , "system.interface.total_bytes" )
142+ data_processed_hourly = aggregate_hourly_data (data_processed_metrics_raw , "system.interface.total_bytes" )
150143
151144 cost_breakdown = []
152145
153- if not (waf_hourly and ports_hourly and ncu_hourly and network_hourly ):
146+ if not (waf_hourly and ports_hourly and ncu_hourly and data_processed_hourly ):
154147 raise Exception ("No metric data available for the specified time range" )
155148
156- min_length = min (len (waf_hourly ), len (ports_hourly ), len (ncu_hourly ), len (network_hourly ))
157- print (f"📈 Processing { min_length } hours of data..." )
158-
149+ min_length = min (len (waf_hourly ), len (ports_hourly ), len (ncu_hourly ), len (data_processed_hourly ))
159150 for i in range (min_length ):
160151 timestamp = waf_hourly [i ]['timestamp' ]
161- waf_enabled = waf_hourly [i ]['value' ] == 1 # 1 means WAF is enabled
152+ waf_enabled = float ( waf_hourly [i ]['value' ]) == 1.0 # 1.0 means WAF is enabled
162153 ports_used = ports_hourly [i ]['value' ]
163154 ncu_provisioned = ncu_hourly [i ]['value' ]
164- total_bytes = network_hourly [i ]['value' ]
155+ total_bytes = data_processed_hourly [i ]['value' ]
165156
166157 # Convert total bytes to GB
167158 data_processed_gb = total_bytes / (1024 ** 3 ) if total_bytes else 0
168159
169- # Check if deployment is active (both NCU and ports > 0 indicates active deployment)
170- deployment_active = ncu_provisioned > 0 or ports_used > 0
171-
172- # Cost components - only charge fixed costs if deployment is active
173- if deployment_active :
174- fixed_deployment_cost = pricing ["fixed" ]["NGINX" ]
175- waf_cost = pricing ["fixed" ]["NGINX + WAF" ] - fixed_deployment_cost if waf_enabled else 0
176- else :
177- fixed_deployment_cost = 0
178- waf_cost = 0
179-
160+ base_deployment_cost = pricing ["fixed" ]["NGINX" ]
161+ waf_deployment_cost = pricing ["fixed" ]["NGINX + WAF" ] - base_deployment_cost if waf_enabled else 0
180162 base_ncu_cost = ncu_provisioned * pricing ["ncu" ]["NGINX" ]
181163 waf_ncu_cost = (ncu_provisioned * (pricing ["ncu" ]["NGINX + WAF" ] - pricing ["ncu" ]["NGINX" ])) if waf_enabled else 0
182164
183- ports_cost = (max (ports_used - 5 , 0 ) * pricing ["ncu" ]["NGINX" ]) # Cost for ports > 5
165+ ports_ncu_cost = (max (ports_used - 5 , 0 ) * 2 * pricing ["ncu" ]["NGINX" ]) # Cost for ports > 5
184166 data_processing_cost = data_processed_gb * pricing ["data_processing" ]
185167
186168 # Total hourly cost
187- total_cost = fixed_deployment_cost + waf_cost + base_ncu_cost + waf_ncu_cost + ports_cost + data_processing_cost
169+ total_cost = base_deployment_cost + waf_deployment_cost + base_ncu_cost + waf_ncu_cost + ports_ncu_cost + data_processing_cost
188170
189171 # Append hourly cost breakdown
190172 cost_breakdown .append ({
191173 "timestamp" : timestamp ,
192- "fixed_deployment_cost " : round (fixed_deployment_cost , 6 ),
193- "waf_cost " : round (waf_cost , 6 ),
174+ "base_deployment_cost " : round (base_deployment_cost , 6 ),
175+ "waf_deployment_cost " : round (waf_deployment_cost , 6 ),
194176 "base_ncu_cost" : round (base_ncu_cost , 6 ),
195177 "waf_ncu_cost" : round (waf_ncu_cost , 6 ),
196- "ports_cost " : round (ports_cost , 6 ),
178+ "ports_ncu_cost " : round (ports_ncu_cost , 6 ),
197179 "data_processing_cost" : round (data_processing_cost , 6 ),
198180 "total_cost" : round (total_cost , 6 )
199181 })
@@ -225,16 +207,13 @@ def parse_arguments():
225207 help = 'Azure region where NGINX is deployed (e.g., eastus2, westus2)' )
226208 parser .add_argument ('--date-range' , '-d' , required = True ,
227209 help = 'Analysis period in ISO format: start/end (e.g., 2025-11-18T00:00:00Z/2025-11-19T23:59:59Z)' )
228-
229- # Required tenant-id argument
230210 parser .add_argument ('--tenant-id' , '-t' , required = True ,
231211 help = 'Azure AD Tenant ID (required for authentication)' )
212+ # Optional arguments
232213 parser .add_argument ('--subscription-id' ,
233214 help = 'Azure Subscription ID (extracted from resource-id if not provided)' )
234215 parser .add_argument ('--output' , '-o' , default = 'nginxaas_cost_breakdown.csv' ,
235216 help = 'Output CSV filename (default: nginxaas_cost_breakdown.csv)' )
236- parser .add_argument ('--verbose' , '-v' , action = 'store_true' ,
237- help = 'Enable verbose debug output' )
238217
239218 return parser .parse_args ()
240219
@@ -250,36 +229,22 @@ def main():
250229 "date_range" : args .date_range ,
251230 "tenant_id" : args .tenant_id ,
252231 "output_file" : args .output ,
253- "verbose" : args .verbose
254232 }
255233
256234 # Validate required arguments
257235 if not config ["resource_id" ].startswith ("/subscriptions/" ):
258- print ("❌ Error: Invalid resource ID format" )
259- print (" Resource ID should start with /subscriptions/" )
260- print (" Example: /subscriptions/xxx/resourceGroups/my-rg/providers/Nginx.NginxPlus/nginxDeployments/my-nginx" )
236+ print ("Error: Invalid resource ID format.\n Resource ID should start with /subscriptions/.\n Example: /subscriptions/xxx/resourceGroups/my-rg/providers/Nginx.NginxPlus/nginxDeployments/my-nginx" )
261237 return 1
262-
238+
263239 if not config ["date_range" ] or "/" not in config ["date_range" ]:
264- print ("❌ Error: Invalid date range format" )
265- print (" Use format: start/end (e.g., 2025-11-18T00:00:00Z/2025-11-19T23:59:59Z)" )
240+ print ("Error: Invalid date range format.\n Use format: start/end (e.g., 2025-11-18T00:00:00Z/2025-11-19T23:59:59Z)" )
266241 return 1
267242
268- try :
269- print ("🔧 Initializing NGINX for Azure Cost Analysis..." )
270- print (f"📍 Location: { config ['location' ]} " )
271- print (f"📅 Date Range: { config ['date_range' ]} " )
272- print (f"🔗 Resource: { config ['resource_id' ].split ('/' )[- 1 ]} " )
273- print ()
274-
275- # Authentication with Azure
276- print ("🔐 Authenticating with Azure..." )
277-
243+ try :
278244 # Use InteractiveBrowserCredential with required tenant_id
279245 credential = InteractiveBrowserCredential (
280246 tenant_id = config ["tenant_id" ]
281247 )
282- print (f"🌐 Using InteractiveBrowserCredential with tenant: { config ['tenant_id' ]} " )
283248
284249 # Run the cost calculation
285250 result = calculate_cost_breakdown (
@@ -290,73 +255,39 @@ def main():
290255 config ["subscription_id" ]
291256 )
292257
293- # Display summary statistics
294- total_hours = len (result )
295- total_cost = sum (entry ["total_cost" ] for entry in result )
296- avg_hourly_cost = total_cost / total_hours if total_hours > 0 else 0
297-
298- print ("=" * 60 )
299- print ("📈 COST ANALYSIS SUMMARY" )
300- print ("=" * 60 )
301- print (f"Total Analysis Period: { total_hours } hours" )
302- print (f"Total Cost: ${ total_cost :.2f} " )
303- print ()
304-
305- # Display detailed breakdown for first few hours
306- print ("🕐 HOURLY COST BREAKDOWN (First 5 hours):" )
307- print ("-" * 60 )
308- for i , entry in enumerate (result [:5 ]):
309- print (f"Hour { i + 1 } - { entry ['timestamp' ]} " )
310- print (f" Fixed: ${ entry ['fixed_deployment_cost' ]:.3f} | WAF: ${ entry ['waf_cost' ]:.3f} | NCU: ${ entry ['base_ncu_cost' ]:.3f} " )
311- print (f" Ports: ${ entry ['ports_cost' ]:.3f} | Data: ${ entry ['data_processing_cost' ]:.3f} | Total: ${ entry ['total_cost' ]:.3f} " )
312- print ()
313-
314- if total_hours > 5 :
315- print (f"... and { total_hours - 5 } more hours" )
316- print ()
317-
318258 # Export to CSV
319259 export_to_csv (result , config ["output_file" ])
320260
321- print ("✅ Cost analysis completed successfully!" )
261+ print ("Cost analysis completed successfully!" )
322262 return 0
323263
324264 except Exception as e :
325265 error_message = str (e )
326- print (f"❌ Error during cost analysis: { error_message } " )
327-
266+ print (f"Error during cost analysis: { error_message } " )
328267 if "authorization" in error_message .lower () or "403" in error_message :
329- print ("\n 🔐 PERMISSIONS ERROR" )
330- print ("=" * 25 )
331- print ("Your Azure account needs access to read metrics from this NGINX resource." )
332- print ("This typically requires 'Monitoring Reader' or 'Reader' role on the resource." )
268+ print ("\n PERMISSIONS ERROR\n " + "=" * 25 + "\n Your Azure account needs access to read metrics from this NGINX resource.\n This typically requires 'Monitoring Reader' or 'Reader' role on the resource." )
333269 else :
334- print (f"\n 💡 Please check:" )
335- print (" • Your Azure permissions (Monitoring Reader role)" )
336- print (" • That the resource ID is correct" )
337- print (" • That the date range is within the last 30 days" )
338- print (" • Try running 'az login' first for DefaultAzureCredential" )
339-
270+ print ("\n Please check:\n - Your Azure permissions (Monitoring Reader role)\n - That the resource ID is correct\n - That the date range is within the last 30 days\n " )
340271 return 1
341272
342273def export_to_csv (cost_breakdown , filename = "nginx_cost_breakdown.csv" ):
343274 """Export cost breakdown to CSV file."""
344275 import csv
345276
346277 if not cost_breakdown :
347- print ("⚠️ No data to export" )
278+ print ("No data to export" )
348279 return
349280
350281 try :
351282 with open (filename , 'w' , newline = '' ) as csvfile :
352283 fieldnames = cost_breakdown [0 ].keys ()
353284 header_mapping = {
354285 "timestamp" : "Timestamp" ,
355- "fixed_deployment_cost " : "Base Fixed Cost ($USD)" ,
356- "waf_cost " : "WAF Cost ($USD)" ,
286+ "base_deployment_cost " : "Base Deployment Cost ($USD)" ,
287+ "waf_deployment_cost " : "WAF Deployment Cost ($USD)" ,
357288 "base_ncu_cost" : "Base NCU Cost ($USD)" ,
358289 "waf_ncu_cost" : "WAF NCU Cost ($USD)" ,
359- "ports_cost " : "Ports Cost ($USD)" ,
290+ "ports_ncu_cost " : "Ports NCU Cost ($USD)" ,
360291 "data_processing_cost" : "Data Processing Cost ($USD)" ,
361292 "total_cost" : "Total Cost ($USD)"
362293 }
@@ -372,11 +303,11 @@ def export_to_csv(cost_breakdown, filename="nginx_cost_breakdown.csv"):
372303
373304 # Calculate and write summary totals
374305 total_hours = len (cost_breakdown )
375- total_fixed_deployment = sum (entry ["fixed_deployment_cost " ] for entry in cost_breakdown )
376- total_waf = sum (entry ["waf_cost " ] for entry in cost_breakdown )
306+ total_fixed_deployment = sum (entry ["base_deployment_cost " ] for entry in cost_breakdown )
307+ total_waf = sum (entry ["waf_deployment_cost " ] for entry in cost_breakdown )
377308 total_base_ncu = sum (entry ["base_ncu_cost" ] for entry in cost_breakdown )
378309 total_waf_ncu = sum (entry ["waf_ncu_cost" ] for entry in cost_breakdown )
379- total_ports = sum (entry ["ports_cost " ] for entry in cost_breakdown )
310+ total_ports = sum (entry ["ports_ncu_cost " ] for entry in cost_breakdown )
380311 total_data_processing = sum (entry ["data_processing_cost" ] for entry in cost_breakdown )
381312 total_cost = sum (entry ["total_cost" ] for entry in cost_breakdown )
382313
@@ -386,20 +317,20 @@ def export_to_csv(cost_breakdown, filename="nginx_cost_breakdown.csv"):
386317 # Add totals row with dollar signs
387318 totals_row = {
388319 "timestamp" : f"TOTALS ({ total_hours } hours)" ,
389- "fixed_deployment_cost " : f"${ round (total_fixed_deployment , 4 ):.4f} " ,
390- "waf_cost " : f"${ round (total_waf , 4 ):.4f} " ,
320+ "base_deployment_cost " : f"${ round (total_fixed_deployment , 4 ):.4f} " ,
321+ "waf_deployment_cost " : f"${ round (total_waf , 4 ):.4f} " ,
391322 "base_ncu_cost" : f"${ round (total_base_ncu , 4 ):.4f} " ,
392323 "waf_ncu_cost" : f"${ round (total_waf_ncu , 4 ):.4f} " ,
393- "ports_cost " : f"${ round (total_ports , 4 ):.4f} " ,
324+ "ports_ncu_cost " : f"${ round (total_ports , 4 ):.4f} " ,
394325 "data_processing_cost" : f"${ round (total_data_processing , 4 ):.4f} " ,
395326 "total_cost" : f"${ round (total_cost , 2 ):.2f} "
396327 }
397328 writer .writerow (totals_row )
398329
399- print (f"✅ Cost breakdown exported to { filename } " )
400- print (f" 📊 Summary: { total_hours } hours, Total cost: ${ total_cost :.2f} " )
330+ print (f"Cost breakdown exported to { filename } " )
331+ print (f"Summary: { total_hours } hours, Total cost: ${ total_cost :.2f} " )
401332 except Exception as e :
402- print (f"❌ Error exporting to CSV: { e } " )
333+ print (f"Error exporting to CSV: { e } " )
403334
404335if __name__ == "__main__" :
405336 sys .exit (main ())
0 commit comments