Remove external dependency on ocaml-csv
[virt-top.git] / src / csv_output.ml
1 (* 'top'-like tool for libvirt domains.
2    (C) Copyright 2007-2021 Richard W.M. Jones, Red Hat Inc.
3    http://libvirt.org/
4
5    This program is free software; you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation; either version 2 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU General Public License for more details.
14
15    You should have received a copy of the GNU General Public License
16    along with this program; if not, write to the Free Software
17    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 *)
19
20 (* CSV output functions. *)
21
22 open Printf
23
24 open Utils
25 open Collect
26
27 module C = Libvirt.Connect
28
29 let chan = ref None
30
31 let csv_set_filename filename = chan := Some (open_out filename)
32
33 (* This code is adapted from OCaml CSV, published under the LGPLv2+
34  * which is compatible with the license of virt-top.
35  *)
36
37 let nl = Bytes.make 1 '\n'
38 let comma = Bytes.make 1 ','
39 let quote = Bytes.make 1 '"'
40 let output_newline chan = output chan nl 0 1
41 let output_comma chan = output chan comma 0 1
42 let output_quote chan = output chan quote 0 1
43
44 let is_space_or_tab c = c = ' ' || c = '\t'
45
46 let must_escape = Array.make 256 false
47 let () =
48   List.iter (fun c -> must_escape.(Char.code c) <- true)
49             ['\"'; '\\';  '\000'; '\b'; '\n'; '\r'; '\t'; '\026']
50
51 let must_quote chan s len =
52   let quote = ref (is_space_or_tab (String.unsafe_get s 0)
53                    || is_space_or_tab (String.unsafe_get s (len - 1))) in
54   let n = ref 0 in
55   for i = 0 to len-1 do
56     let c = String.unsafe_get s i in
57     if c = ',' || c = '\n' || c = '\r' then quote := true
58     else if c = '"' then (
59       quote := true;
60       incr n
61     )
62   done;
63   if !quote then !n else -1
64
65 let write_escaped chan field =
66   let len = String.length field in
67   if len > 0 then (
68     let n = must_quote chan field len in
69     if n < 0 then
70       output chan (Bytes.unsafe_of_string field) 0 len
71     else (
72       let field =
73         if n <= 0 then Bytes.unsafe_of_string field
74         else (* There are some quotes to escape *)
75           let s = Bytes.create (len + n) in
76           let j = ref 0 in
77           for i = 0 to len - 1 do
78             let c = String.unsafe_get field i in
79             if c = '"' then (
80               Bytes.unsafe_set s !j '"'; incr j;
81               Bytes.unsafe_set s !j '"'; incr j
82             )
83             else (Bytes.unsafe_set s !j c; incr j)
84           done;
85           s
86       in
87       output_quote chan;
88       output chan field 0 (Bytes.length field);
89       output_quote chan
90     )
91   )
92
93 let save_out chan = function
94   | [] -> output_newline chan
95   | [f] ->
96      write_escaped chan f;
97      output_newline chan
98   | f :: tl ->
99      write_escaped chan f;
100      List.iter (
101        fun f ->
102          output_comma chan;
103          write_escaped chan f
104      ) tl;
105      output_newline chan
106
107 let csv_write row =
108   match !chan with
109   | None -> ()                  (* CSV output not enabled *)
110   | Some chan ->
111      save_out chan row;
112      (* Flush the output to the file immediately because we don't
113       * explicitly close the channel.
114       *)
115      flush chan
116
117 (* Write CSV header row. *)
118 let write_csv_header (csv_cpu, csv_mem, csv_block, csv_net) block_in_bytes =
119   csv_write (
120     [ "Hostname"; "Time"; "Arch"; "Physical CPUs";
121       "Count"; "Running"; "Blocked"; "Paused"; "Shutdown";
122       "Shutoff"; "Crashed"; "Active"; "Inactive";
123       "%CPU";
124       "Total hardware memory (KB)";
125       "Total memory (KB)"; "Total guest memory (KB)";
126       "Total CPU time (ns)" ] @
127       (* These fields are repeated for each domain: *)
128     [ "Domain ID"; "Domain name"; ] @
129     (if csv_cpu then [ "CPU (ns)"; "%CPU"; ] else []) @
130     (if csv_mem then [ "Mem (bytes)"; "%Mem";] else []) @
131     (if csv_block && not block_in_bytes
132        then [ "Block RDRQ"; "Block WRRQ"; ] else []) @
133     (if csv_block && block_in_bytes
134        then [ "Block RDBY"; "Block WRBY"; ] else []) @
135     (if csv_net then [ "Net RXBY"; "Net TXBY" ] else [])
136   )
137
138 (* Write summary data to CSV file. *)
139 let append_csv (_, _, _, _, _, node_info, hostname, _) (* setup *)
140                (csv_cpu, csv_mem, csv_block, csv_net)
141                block_in_bytes
142                { rd_doms = doms;
143                  rd_printable_time = printable_time;
144                  rd_nr_pcpus = nr_pcpus; rd_total_cpu = total_cpu;
145                  rd_totals = totals } (* state *) =
146   (* The totals / summary fields. *)
147   let (count, running, blocked, paused, shutdown, shutoff,
148        crashed, active, inactive,
149        total_cpu_time, total_memory, total_domU_memory) = totals in
150
151   let percent_cpu = 100. *. total_cpu_time /. total_cpu in
152
153   let summary_fields = [
154     hostname; printable_time; node_info.C.model; string_of_int nr_pcpus;
155     string_of_int count; string_of_int running; string_of_int blocked;
156     string_of_int paused; string_of_int shutdown; string_of_int shutoff;
157     string_of_int crashed; string_of_int active; string_of_int inactive;
158     sprintf "%2.1f" percent_cpu;
159     Int64.to_string node_info.C.memory;
160     Int64.to_string total_memory; Int64.to_string total_domU_memory;
161     Int64.to_string (Int64.of_float total_cpu_time)
162   ] in
163
164   (* The domains.
165    *
166    * Sort them by ID so that the list of relatively stable.  Ignore
167    * inactive domains.
168    *)
169   let doms = List.filter_map (
170     function
171     | _, Inactive -> None               (* Ignore inactive domains. *)
172     | name, Active rd -> Some (name, rd)
173   ) doms in
174   let cmp (_, { rd_domid = rd_domid1 }) (_, { rd_domid = rd_domid2 }) =
175     compare rd_domid1 rd_domid2
176   in
177   let doms = List.sort cmp doms in
178
179   let string_of_int64_option = map_default Int64.to_string "" in
180
181   let domain_fields = List.map (
182     fun (domname, rd) ->
183       [ string_of_int rd.rd_domid; domname ] @
184         (if csv_cpu then [
185            string_of_float rd.rd_cpu_time; string_of_float rd.rd_percent_cpu
186          ] else []) @
187         (if csv_mem then [
188             Int64.to_string rd.rd_mem_bytes; Int64.to_string rd.rd_mem_percent
189          ] else []) @
190         (if csv_block then
191            if block_in_bytes then [
192              string_of_int64_option rd.rd_block_rd_bytes;
193              string_of_int64_option rd.rd_block_wr_bytes;
194            ] else [
195              string_of_int64_option rd.rd_block_rd_reqs;
196              string_of_int64_option rd.rd_block_wr_reqs;
197            ]
198          else []) @
199         (if csv_net then [
200            string_of_int64_option rd.rd_net_rx_bytes;
201            string_of_int64_option rd.rd_net_tx_bytes;
202          ] else [])
203   ) doms in
204   let domain_fields = List.flatten domain_fields in
205
206   csv_write (summary_fields @ domain_fields)