File: am_utils.lua

package info (click to toggle)
ntopng 5.2.1%2Bdfsg1-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 121,832 kB
  • sloc: javascript: 143,431; cpp: 71,175; ansic: 11,108; sh: 4,687; makefile: 911; python: 587; sql: 512; pascal: 234; perl: 118; ruby: 52; exp: 4
file content (1004 lines) | stat: -rw-r--r-- 29,641 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
--
-- (C) 2019-22 - ntop.org
--

local dirs = ntop.getDirs()
package.path = dirs.installdir .. "/scripts/lua/modules/alert_store/?.lua;" .. package.path

local am_utils = {}
local ts_utils = require "ts_utils_core"
local format_utils = require "format_utils"
local json = require("dkjson")
local plugins_utils = require("plugins_utils")
local os_utils = require("os_utils")
local alerts_api = require("alerts_api")
local alert_consts = require("alert_consts")
local lua_path_utils = require("lua_path_utils")

local supported_granularities = {
  ["min"] = "alerts_thresholds_config.every_minute",
  ["5mins"] = "alerts_thresholds_config.every_5_minutes",
  ["hour"] = "alerts_thresholds_config.hourly",
}

-- Indexes in the hour stats array
local HOUR_STATS_OK = 1
local HOUR_STATS_EXCEEDED = 2
local HOUR_STATS_UNREACHABLE = 3

local do_trace = false

-- ##############################################

local am_hosts_key = string.format("ntopng.prefs.ifid_%d.am_hosts", getSystemInterfaceId())

-- ##############################################

-- @brief Key used to save the result of the measurement (if requested)
--        Used for example to save the page fetched with HTTP/HTTPs measurements
local function am_last_result_key(key)
  return string.format("ntopng.cache.ifid_%d.am_hosts.last_result." .. key, getSystemInterfaceId())
end

-- ##############################################

local function am_last_updates_key(key)
  return string.format("ntopng.cache.ifid_%d.am_hosts.last_update_v1." .. key, getSystemInterfaceId())
end

local function am_hour_stats_key(key)
  return string.format("ntopng.cache.ifid_%d.am_hosts.hour_stats." .. key, getSystemInterfaceId())
end

-- ##############################################

function am_utils.setLastResult(key, last_result)
   if not isEmptyString(last_result) then
      ntop.setCache(am_last_result_key(key), last_result)
   end
end

-- ##############################################

function am_utils.setLastAmUpdate(key, when, value, ipaddress, jitter, mean)
  local v = {
    when = when,
    value = tonumber(value),
    ip = ipaddress,
    jitter = tonumber(jitter),
    mean = tonumber(mean),
  }

  ntop.setCache(am_last_updates_key(key), json.encode(v))
end

-- ##############################################

-- key: the host key
-- when: the update time
-- update_idx is one of HOUR_STATS_OK, HOUR_STATS_EXCEEDED, HOUR_STATS_UNREACHABLE
local function updateHourStats(key, when, update_idx)
  local redis_k = am_hour_stats_key(key)
  local hstats_data = ntop.getCache(redis_k)

  if(not isEmptyString(hstats_data)) then
    hstats_data = json.decode(hstats_data) or {}
  else
    hstats_data = {}
  end

  if(hstats_data.hstats == nil) then
    hstats_data.hstats = {}

    -- Initialize the per-hour stats
    -- Using Lua based index to avoid json conversion issues
    for i=1,24 do
      -- Keep compact, the format is {num_ok, num_exceeded, num_unreachable}
      hstats_data.hstats[i] = {0, 0, 0}
    end
  end

  local prev_dt = os.date("*t", hstats_data.when or 0)
  local cur_dt = os.date("*t", when)
  local hour_idx = (cur_dt.hour + 1)

  if(cur_dt.hour ~= prev_dt.hour) then
    -- Hour has changed, reset the bucket stats
    hstats_data.hstats[hour_idx] = {0, 0, 0}
  end

  local hour_stats = hstats_data.hstats[hour_idx]
  hour_stats[update_idx] = hour_stats[update_idx] + 1
  hstats_data.when = when

  ntop.setCache(redis_k, json.encode(hstats_data))
end

-- ##############################################

function am_utils.incNumOkChecks(key, when)
  return(updateHourStats(key, when, HOUR_STATS_OK))
end

function am_utils.incNumExceededChecks(key, when)
  return(updateHourStats(key, when, HOUR_STATS_EXCEEDED))
end

function am_utils.incNumUnreachableChecks(key, when)
  return(updateHourStats(key, when, HOUR_STATS_UNREACHABLE))
end

-- ##############################################

-- Retrieve the per-hour stats of the host.
-- The returned data has the hour as the table key (0 for midnight).
--
-- Example:
--
--  0 table
--  0.num_ok number 0
--  0.num_exceeded number 0
--  0.num_unreachable number 0
-- ...
function am_utils.getHourStats(host, measurement)
  local key = am_utils.getAmHostKey(host, measurement)
  local redis_k = am_hour_stats_key(key)
  local hour_stats = ntop.getCache(redis_k)

  if(isEmptyString(hour_stats)) then
    return(nil)
  end

  hour_stats = json.decode(hour_stats)

  if((hour_stats == nil) or (hour_stats.hstats == nil)) then
    return(nil)
  end

  local res = {}

  -- Expand the result with labels
  for i=1,24 do
    local pt = hour_stats.hstats[i]

    -- Convert in an hour based table
    res[tostring(i-1)] = {
      num_ok = pt[HOUR_STATS_OK],
      num_exceeded = pt[HOUR_STATS_EXCEEDED],
      num_unreachable = pt[HOUR_STATS_UNREACHABLE],
    }
  end

  return(res)
end

-- ##############################################

-- Get the total host availability in the past day (0-100)%
-- nil is returned when no data is available.
-- An host is considered available when it is reachable and within the
-- threshold.
function am_utils.getAvailability(host, measurement)
  local key = am_utils.getAmHostKey(host, measurement)
  local redis_k = am_hour_stats_key(key)
  local hour_stats = ntop.getCache(redis_k)

  if(isEmptyString(hour_stats)) then
    return(nil)
  end

  hour_stats = json.decode(hour_stats)

  if((hour_stats == nil) or (hour_stats.hstats == nil)) then
    return(nil)
  end

  local tot_available = 0
  local tot_unavailable = 0
  local rc = {}

  for i=1,24 do
    local pt = hour_stats.hstats[i]

    if pt then
      tot_available = tot_available + pt[HOUR_STATS_OK]
      tot_unavailable = tot_unavailable + pt[HOUR_STATS_EXCEEDED] + pt[HOUR_STATS_UNREACHABLE]

      if((pt[HOUR_STATS_OK]+pt[HOUR_STATS_UNREACHABLE]+pt[HOUR_STATS_EXCEEDED]) == 0) then
	 color = 0
      elseif((pt[HOUR_STATS_UNREACHABLE]+pt[HOUR_STATS_EXCEEDED]) == 0) then
   	 color = 1
      elseif(((pt[HOUR_STATS_UNREACHABLE]+pt[HOUR_STATS_EXCEEDED]) > 0) and (pt[HOUR_STATS_OK] == 0)) then
   	 color = 2
      else
   	 color = 3
      end

      table.insert(rc, color)
    end
  end

  return rc, (tot_available * 100 / (tot_available + tot_unavailable))
end

-- ##############################################

function am_utils.dropHourStats(host_key)
  ntop.delCache(am_hour_stats_key(host_key))
end

-- ##############################################

-- Note: alerts requires a unique key to be used in order to identity the
-- entity. This key is also used internally as a key into the lua tables.
function am_utils.getAmHostKey(host, measurement)
  return(string.format("%s@%s", measurement, host))
end

-- @brief Extract the measurement and the host from the key
--        which is the concatenation of <measurement>@<host>
local function key2amhost(host)
   -- Examples: http@https://maina:maina2@develv5:3003/lua/cane
   --           https://maina:maina2@develv5:3003/lua/cane
  local measurement, amhost = string.match(host, "^([^@]+)@(.+)")

  if measurement and amhost then
     return amhost, measurement
  end
end

-- ##############################################

function am_utils.getLastAmUpdate(host, measurement)
  local key = am_utils.getAmHostKey(host, measurement)
  local val = ntop.getCache(am_last_updates_key(key))

  if not isEmptyString(val) then
    val = json.decode(val)
  else
    val = nil
  end

  if val ~= nil then
    return val
  end
end

-- ##############################################

-- @brief Returns the possibly saved result of a measurement
function am_utils.getLastResult(host, measurement)
  local key = am_utils.getAmHostKey(host, measurement)
  local val = ntop.getCache(am_last_result_key(key))

  if not isEmptyString(val) then
     return val
  end

  return nil
end

-- ##############################################

-- @brief Check if this is an infrastructure active monitoring url
local function is_infrastructure(host)
   if not ntop.isEnterpriseM() or isEmptyString(host) then
      return false, nil
   end

   package.path = dirs.installdir .. "/pro/scripts/lua/enterprise/modules/?.lua;" .. package.path
   local infrastructure_utils = require("infrastructure_utils")

   -- The host is considered an infrastructure host if it contains the endpoint in the name
   if host:find(infrastructure_utils.ENDPOINT_TO_EXTRACT_DATA) or host:find(infrastructure_utils.SUFFIX_THROUGHPUT) then
      local instance = infrastructure_utils.get_instance_by_host(host)

      if not instance then
	 return false, nil
      else
	 return true, instance.alias
      end
   end

   return false, nil
end

-- ##############################################

-- Only used for the formatting, don't use as a key as the "/"
-- character is escaped in HTTP parameters
function am_utils.formatAmHost(host, measurement, isHtml)
  local m_info = am_utils.getMeasurementInfo(measurement)

  --if m_info and m_info.force_host then
    -- Only a single host is present, return it
    --return(host)
  --end

  if(host == nil) then
     return(nil)
  end

  local res = host
  local is_infr, infr_name = is_infrastructure(host)

  -- Make a smarter way to determine infrastructure labels
  if is_infr then

     -- Make a nicer label for infrastructure hosts
     -- If the am host contains a name for the infrastructure use it
     if isHtml then
      res = infr_name .. " <i class='fas fa-building'></i>"
     else
      res = infr_name .. " [".. i18n("infrastructure_dashboard.infrastructure") .. "]"
    end
  else
    res = host
  end

  return res
end

-- ##############################################

function am_utils.key2host(host_key)
  local host, measurement = key2amhost(host_key)

  return {
    label = am_utils.formatAmHost(host, measurement, false),
    is_infrastructure = is_infrastructure(host),
    measurement = measurement,
    host = host,
  }
end

-- ##############################################

function am_utils.getAmSchemaForGranularity(granularity)
  local str_granularity

  if(tonumber(granularity) ~= nil) then
    str_granularity = alert_consts.sec2granularity(granularity)
  else
    str_granularity = granularity
  end

  return("am_host:val_" .. (str_granularity or "min"))
end

-- ##############################################

local function deserializeAmPrefs(host_key, val, config_only)
  local rv

  if config_only then
    rv = {}
  else
    rv = am_utils.key2host(host_key)
  end

  if(tonumber(val) ~= nil) then
    -- Old format is only a number
    rv.threshold = tonumber(val)
    rv.granularity = "min"
  else
    -- New format: json
    local v = json.decode(val)

    if v then
      rv.show_iface = false
      rv.ifname = v.ifname or ''
      rv.threshold = tonumber(v.threshold) or 500
      rv.granularity = v.granularity or "min"
      rv.token = v.token
      rv.save_result = v.save_result
      rv.readonly = v.readonly
    end
  end

  return(rv)
end

local function serializeAmPrefs(val)
  return json.encode(val)
end

-- ##############################################

function am_utils.hasHost(host, measurement)
  local host_key = am_utils.getAmHostKey(host, measurement)
  local res = ntop.getHashCache(am_hosts_key, host_key)

  return(not isEmptyString(res))
end

-- ##############################################

function am_utils.getHosts(config_only, granularity)
  local hosts = ntop.getHashAllCache(am_hosts_key) or {}
  local rv = {}

  for host_key, val in pairs(hosts) do
    local host = deserializeAmPrefs(host_key, val, config_only)

    if host and ((granularity == nil) or (host.granularity == granularity)) then
      if config_only then
        rv[host_key] = host
      else
        -- Ensure that the measurement is still available
        local m_info = am_utils.getMeasurementInfo(host.measurement)

        if(m_info ~= nil) then
          rv[host_key] = host
        end
      end
    end
  end

  return rv
end

-- ##############################################

function am_utils.resetConfig()
  local hosts = am_utils.getHosts()

  for k,v in pairs(hosts) do
    am_utils.deleteHost(v.host, v.measurement)
  end

  ntop.delCache(am_hosts_key)
end

-- ##############################################

function am_utils.getHost(host, measurement)
  local host_key = am_utils.getAmHostKey(host, measurement)
  local val = ntop.getHashCache(am_hosts_key, host_key)

  if not isEmptyString(val) then
    return deserializeAmPrefs(host_key, val)
  end
end

-- ##############################################

-- @brief Add and host as part of the active monitoring
-- @param measurement A string with the type of measurement which will be performed
-- @param am_value A number used as threshold con consider the measurement failed
-- @param granularity One of `supported_granularities`, indicating the granularity of the measurement
-- @param pool The pool_id `host` will be associated to
-- @param token A string with an ntopng `token` used to fetch data from other ntopngs in a federation [optional]
-- @param save_result Whether the result fetched with the measure should be saved (e.g., the HTTP response) [optional]
-- @param readonly Bool used by the GUI to know if, when true, an entry is considered read only hence it cannot be modified/deleted [optional]
function am_utils.addHost(host, ifname, measurement, am_value, granularity, pool, token, save_result, readonly)
  save_result = save_result or false
  readonly = readonly or false

  local active_monitoring_pools = require("active_monitoring_pools")
  local am_pool = active_monitoring_pools:create()
  local host_key = am_utils.getAmHostKey(host, measurement)
  local show_iface = ntop.isPingIfaceAvailable()

  ntop.setHashCache(am_hosts_key, host_key, serializeAmPrefs({
    show_iface = show_iface,
    ifname = ifname or '',
    threshold = tonumber(am_value) or 500,
    granularity = granularity or "min",
    token = token, -- ntopng auth token
    save_result = save_result, -- save the result
    readonly = readonly,
  }))

  -- Bind the host from any existing pool
  am_pool:bind_member(host_key, pool)
end

-- ##############################################

function am_utils.discardHostTimeseries(host, measurement)
  ts_utils.delete("am_host", {ifid=getSystemInterfaceId(), host=host, metric=measurement})
end

-- ##############################################

function am_utils.deleteHost(host, measurement)
  local active_monitoring_pools = require("active_monitoring_pools")
  local am_pool = active_monitoring_pools:create()
  local ts_utils = require("ts_utils")
  local alert_utils = require("alert_utils")

  -- NOTE: system interface must be manually sected and then unselected
  local old_iface = tostring(interface.getId())
  interface.select(getSystemInterfaceId())

  local host_key = am_utils.getAmHostKey(host, measurement)
  local am_host_entity = alerts_api.amThresholdCrossEntity(host_key)

  -- Release any engaged alerts of the host
  alerts_api.releaseEntityAlerts(am_host_entity)

  am_utils.discardHostTimeseries(host, measurement)

  -- Remove possibly saved results
  ntop.delCache(am_last_result_key(host_key))

  -- Remove the redis keys of the host
  ntop.delCache(am_last_updates_key(host_key))
  am_utils.dropHourStats(host_key)

  ntop.delHashCache(am_hosts_key, host_key)

  -- Unbind the host from any existing pool
  am_pool:bind_member(host_key, am_pool.DEFAULT_POOL_ID)

  -- Select the old interface
  interface.select(old_iface)
end

-- ##############################################

local loaded_am_plugins = {}
local loaded_measurements = {}

local function loadAmPlugins()
   if not table.empty(loaded_am_plugins) then
      return
   end

   local measurements_path = dirs.installdir .. "/scripts/lua/modules/measurements/"
   lua_path_utils.package_path_prepend(measurements_path)

   for fname in pairs(ntop.readdir(measurements_path)) do
      if(not string.ends(fname, ".lua")) then
	 goto continue
      end

      local mod_fname = string.sub(fname, 1, string.len(fname) - 4)
      local plugin = require(mod_fname)

      if not plugin then
	 traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Could not load '%s'", mod_fname))
	 package.loaded[mod_fname] = nil
	 goto continue
      end

      if not plugin.measurements then
	 traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("'measurements' section missing in '%s'", mod_fname))
	 package.loaded[mod_fname] = nil
	 goto continue
      end

      if plugin.setup then
	 -- A setup function exists, call it to determine if the plugin is available
	 if(plugin.setup() == false) then
	    package.loaded[mod_fname] = nil
	    goto continue
	 end
      end

      -- Check that the measurements does not exist
      for _, measurement in pairs(plugin.measurements) do
	 if(measurement.check == nil) then
	    traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Missing 'check' function in '%s' measurement", measurement.key))
	    goto skip
	 end

	 if(measurement.collect_results == nil) then
	    traceError(TRACE_ERROR, TRACE_CONSOLE, string.format("Missing 'collect_results' function in '%s' measurement", measurement.key))
	    goto skip
	 end

	 if(loaded_measurements[measurement.key]) then
	    traceError(TRACE_WARNING, TRACE_CONSOLE, string.format("Measurement '%s' already defined in '%s'", measurement.key, loaded_measurements[measurement.key].key))
	    goto skip
	 end

	 loaded_measurements[measurement.key] = {plugin=plugin, measurement=measurement}

	 ::skip::
      end

      plugin.key = mod_fname
      loaded_am_plugins[mod_fname] = plugin

      ::continue::
   end
end

-- ##############################################

--! @brief Splits the hosts list by measurement.
--! @param all_hosts the host list, whose format matches am_utils.getHosts()
--! @return a table measurement_key -> <plugin, measurement, hosts>
function am_utils.getHostsByMeasurement(all_hosts)
  local hosts_by_measurement = {}

  loadAmPlugins()

  for key, host in pairs(all_hosts) do
    local measurement = host.measurement
    local m_info = loaded_measurements[measurement]

    if not m_info then
      traceError(TRACE_WARNING, TRACE_CONSOLE, "Unknown measurement: " .. measurement)
    else
      local measurement_key = m_info.measurement.key

      if not hosts_by_measurement[measurement_key] then
	hosts_by_measurement[measurement_key] = {plugin = m_info.plugin, measurement = m_info.measurement, hosts = {}}
      end

      hosts_by_measurement[measurement_key].hosts[key] = host
    end
  end

  return(hosts_by_measurement)
end

-- ##############################################

--! @brief Get a list of measurements from the loaded Active Monitoring plugins
--! @return a list of measurements <title, value> for the gui.
function am_utils.getAvailableMeasurements()
  local measurements = {}

  loadAmPlugins()

  for k, v in pairsByKeys(loaded_measurements, asc) do
    local m = v.measurement

    measurements[#measurements + 1] = {
      title = i18n(m.i18n_label) or m.i18n_label,
      value = k,
    }
  end

  return(measurements)
end

-- ##############################################

--! @brief Check if the specified measurement is available
--! @return true if available, false otherwise
function am_utils.isMeasurementAvailable(measurement)
  loadAmPlugins()

  return(loaded_measurements[measurement] ~= nil)
end

-- ##############################################

--! @brief Get a list of granularities allowed the the measurements
--! @param measurement the measurement key for which the granularities should be returned
--! @return a list of allowed granularities <titlae, value> for the gui.
function am_utils.getAvailableGranularities(measurement)
  local granularities = {}

  loadAmPlugins()

  local m_info = loaded_measurements[measurement]

  if(not m_info) then
    return(granularities)
  end

  for _, k in ipairs(m_info.measurement.granularities) do
    local i18n_title = supported_granularities[k]

    if i18n_title then
      granularities[#granularities + 1] = {
	title = i18n(i18n_title),
	value = k,
      }
    end
  end

  return granularities
end

-- ##############################################

--! @brief Get the metadata of a specific measurement
--! @param measurement the measurement key
--! @return the measurement metadata on success, nil on failure
function am_utils.getMeasurementInfo(measurement)
  loadAmPlugins()

  local m_info = loaded_measurements[measurement]

  if(not m_info) then
    return(nil)
  end

  return(m_info.measurement)
end

-- ##############################################

--! @brief Get the metadata of all the loaded measurements
--! @return a list containing the measurements metadata
function am_utils.getMeasurementsInfo()
  loadAmPlugins()

  local rv = {}

  for k, v in pairs(loaded_measurements) do
    rv[k] = v.measurement
  end

  return(rv)
end

-- ##############################################

local function amThresholdCrossType(value, threshold, ip, granularity, entity_info)
  local host = am_utils.key2host(entity_info.entity_val)
  local m_info = am_utils.getMeasurementInfo(host.measurement)

  local alert_type = alert_consts.alert_types.alert_am_threshold_cross.new(
     value,
     threshold,
     ip,
     host,
     m_info.operator,
     m_info.i18n_unit
  )

  alert_type:set_score_warning()
  alert_type:set_granularity(granularity)

  return alert_type
end

-- ##############################################

function am_utils.triggerAlert(numeric_ip, ip_label, current_value, upper_threshold, granularity)
  local entity_info = alerts_api.amThresholdCrossEntity(ip_label)
  local type_info = amThresholdCrossType(current_value, upper_threshold, numeric_ip, granularity, entity_info)

  if(current_value == 0) then
    -- Unreachable
    local host, measurement = key2amhost(ip_label)
    local info = am_utils.getMeasurementInfo(measurement)

    if info and info.unreachable_alert_i18n then
      -- The measurement provides an alternative message for the alert
      type_info.alert_type_params.alt_i18n = info.unreachable_alert_i18n
    end
  end

  return type_info:trigger(entity_info)
end

-- ##############################################

function am_utils.releaseAlert(numeric_ip, ip_label, current_value, upper_threshold, granularity)
  local entity_info = alerts_api.amThresholdCrossEntity(ip_label)
  local type_info = amThresholdCrossType(current_value, upper_threshold, numeric_ip, granularity, entity_info)

  return type_info:release(entity_info)
end

-- ##############################################

-- @brief Checks if the `am_host` passed as parameter has alerts engaged
--        `am_host` is one of the hosts obtained with `am_utils.getHosts()`
-- @return True if the host has engaged alerts, false otherwise
function am_utils.hasAlerts(am_host)
   local am_key = am_utils.getAmHostKey(am_host.host, am_host.measurement)
   local entity_info = alerts_api.amThresholdCrossEntity(am_key)

   -- Active Monitored hosts alerts stay in the system interface,
   -- so there's currenty need to temporarily select it
   local old_ifid = interface.getId()
   interface.select(getSystemInterfaceId())

   local am_alert_store = require "am_alert_store".new()
   local engaged = am_alert_store:select_engaged(am_key)

   interface.select(tostring(old_ifid))

   return engaged and #engaged > 0
end

-- ##############################################

function am_utils.hasExceededThreshold(threshold, operator, value)
  operator = operator or "gt"

  if(threshold and ((operator == "lt" and (value < threshold))
      or (operator == "gt" and (value > threshold)))) then
    return(true)
  else
    return(false)
  end
end

-- ##############################################

-- Resolve the domain name into an IP if necessary
function am_utils.resolveHost(domain_name)
   local ip_address = domain_name

   if not isIPv4(domain_name) and not isIPv6(domain_name) then
      -- Symbolic name, try first to resolve as IPv4, then as IPv6
      ip_address = ntop.resolveHost(domain_name, true --[[IPv4 --]])

      if not ip_address then
	 -- IPv4 resolution failed, attempt at resolving ipv6
	 ip_address = ntop.resolveHost(domain_name, false --[[IPv6 --]])
      end
   else
      -- Regular IPv4 or IPv6, no need to resolve
   end

   return ip_address
end

-- ##############################################

function am_utils.editHost(host, iface, measurement, threshold, granularity, pool, token, save_result, readonly)
  local existing = am_utils.getHost(host, measurement)

  if(existing == nil) then
    return(false)
  end

  if(existing.granularity ~= granularity) then
    -- Need to discard the old timeseries as the granularity has changed
    am_utils.discardHostTimeseries(host, measurement)
  end

  local m_info = am_utils.getMeasurementInfo(measurement)
  local last_update = am_utils.getLastAmUpdate(host, measurement)

  if m_info and last_update then
    -- Recheck the threshold
    local key = am_utils.getAmHostKey(host, measurement)
    local value = last_update.value
    threshold = tonumber(threshold)

    -- Drop the hour stats if the threshold has changed
    if(existing.threshold ~= threshold) then
       am_utils.dropHourStats(key)
    end

    if((existing.granularity ~= granularity) or (existing.threshold ~= threshold)) then
      -- NOTE: system interface must be manually sected and then unselected
      local old_iface = tostring(interface.getId())
      interface.select(getSystemInterfaceId())

      -- Release any engaged alerts of the host
      alerts_api.releaseEntityAlerts(alerts_api.amThresholdCrossEntity(key))

      interface.select(old_iface)
    end
  end

  am_utils.addHost(host, iface, measurement, threshold, granularity, pool, token, save_result, readonly)

  return(true)
end

-- ##############################################

function am_utils.run_am_check(when, all_hosts, granularity)
  local hosts_am = {}
  local resolved_unreachable_hosts = {}
  local am_schema = am_utils.getAmSchemaForGranularity(granularity)

  when = when - (when % 60)

  if(do_trace) then
     print("[ActiveMonitoring] Script started\n")
  end

  if(do_trace) then io.write(debug.traceback()) end

  if table.empty(all_hosts) then
     if(do_trace) then print("[ActiveMonitoring] Nothing to do ["..granularity.."]\n") end
    return
  end

  local hosts_by_measurement = am_utils.getHostsByMeasurement(all_hosts)

  -- Invoke the check functions
  for _, info in pairs(hosts_by_measurement) do
    info.measurement.check(info.hosts, granularity)
  end

  -- Wait some seconds for the results
  ntop.msleep(3000)

  -- Get the results
  for _, info in pairs(hosts_by_measurement) do
     local collected = info.measurement.collect_results(granularity)
     for k, v in pairs(collected or {}) do
      v.measurement = info.measurement

      if(v.value ~= nil) then
        hosts_am[k] = v
      elseif(v.resolved_addr ~= nil) then
        -- For unreachable hosts, still save the resolved address in order to
        -- properly report it into the alert message.
        resolved_unreachable_hosts[k] = v.resolved_addr
      end
    end
  end

  -- Parse the results
  for key, info in pairs(hosts_am) do
    local host = all_hosts[key]
    local host_value = round(info.value, 2)
    local resolved_host = info.resolved_addr or host.host
    local threshold = host.threshold
    local operator = info.measurement.operator
    local jitter = tonumber(info.jitter)
    local mean = tonumber(info.mean)

    if(do_trace) then
       print("[AM result] "..key.."\n")
       tprint(info)
    end

    if jitter then jitter = round(jitter, 2) end
    if mean then mean = round(mean, 2) end

    if areSystemTimeseriesEnabled() then
       local value = host_value

       if info.measurement.chart_scaling_value then
         value = value * info.measurement.chart_scaling_value
       end

       local ts_data = {ifid = getSystemInterfaceId(), host = host.host, metric = host.measurement, value = value}
       if(do_trace) then
	  print("[Writing AM timeseries ")
	  tprint(ts_data)
       end

       ts_utils.append(am_schema, ts_data, when)
    end

    am_utils.setLastAmUpdate(key, when, host_value, resolved_host, jitter, mean)

    if am_utils.hasExceededThreshold(threshold, operator, host_value) then
      if(do_trace) then print("[TRIGGER] Host "..resolved_host.."/"..key.." [value: "..host_value.."][threshold: "..threshold.."]\n") end

      am_utils.triggerAlert(resolved_host, key, host_value, threshold, granularity)
      am_utils.incNumExceededChecks(key, when)
    else
      if(do_trace) then print("[OK] Host "..resolved_host.."/"..key.." [value: "..host_value.."][threshold: "..threshold.."]\n") end

      am_utils.releaseAlert(resolved_host, key, host_value, threshold, granularity)
      am_utils.incNumOkChecks(key, when)
    end
  end

  -- Find the unreachable hosts
  for key, host in pairs(all_hosts) do
     local ip = host.host

     if(hosts_am[key] == nil) then
       if(do_trace) then print("[TRIGGER] Host "..ip.."/"..key.." is unreacheable\n") end
       local resolved_host = resolved_unreachable_hosts[key] or ip

       am_utils.triggerAlert(resolved_host, key, 0, 0, granularity)
       am_utils.incNumUnreachableChecks(key, when)

       if areSystemTimeseriesEnabled() then
         -- Also write 0 in its timeseries to indicate that the host is unreacheable
         ts_utils.append(am_schema, {ifid = getSystemInterfaceId(), host = host.host, metric = host.measurement, value = 0}, when)
       end
     end
  end

  if(do_trace) then
     print("[ActiveMonitoring] Script is over\n")
  end
end

-- ##############################################

return am_utils