Allow command-line options to override asterisk.conf.
[asterisk/asterisk.git] / cdr / cdr_csv.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 1999 - 2005, Digium, Inc.
5  *
6  * Mark Spencer <markster@digium.com>
7  *
8  * Includes code and algorithms from the Zapata library.
9  *
10  * See http://www.asterisk.org for more information about
11  * the Asterisk project. Please do not directly contact
12  * any of the maintainers of this project for assistance;
13  * the project provides a web site, mailing lists and IRC
14  * channels for your use.
15  *
16  * This program is free software, distributed under the terms of
17  * the GNU General Public License Version 2. See the LICENSE file
18  * at the top of the source tree.
19  */
20
21 /*!
22  * \file
23  * \brief Comma Separated Value CDR records.
24  *
25  * \author Mark Spencer <markster@digium.com>
26  *
27  * \arg See also \ref AstCDR
28  * \ingroup cdr_drivers
29  */
30
31 /*! \li \ref cdr_csv.c uses the configuration file \ref cdr.conf
32  * \addtogroup configuration_file Configuration Files
33  */
34
35 /*** MODULEINFO
36         <support_level>extended</support_level>
37  ***/
38
39 #include "asterisk.h"
40
41 ASTERISK_REGISTER_FILE()
42
43 #include "asterisk/paths.h"     /* use ast_config_AST_LOG_DIR */
44 #include "asterisk/config.h"
45 #include "asterisk/channel.h"
46 #include "asterisk/cdr.h"
47 #include "asterisk/module.h"
48 #include "asterisk/utils.h"
49 #include "asterisk/lock.h"
50
51 #define CSV_LOG_DIR "/cdr-csv"
52 #define CSV_MASTER  "/Master.csv"
53
54 #define DATE_FORMAT "%Y-%m-%d %T"
55
56 static int usegmtime = 0;
57 static int accountlogs = 1;
58 static int loguniqueid = 0;
59 static int loguserfield = 0;
60 static int loaded = 0;
61 static int newcdrcolumns = 0;
62 static const char config[] = "cdr.conf";
63
64 /* #define CSV_LOGUNIQUEID 1 */
65 /* #define CSV_LOGUSERFIELD 1 */
66
67 /*----------------------------------------------------
68   The values are as follows:
69
70
71   "accountcode",        accountcode is the account name of detail records, Master.csv contains all records *
72                         Detail records are configured on a channel basis, IAX and SIP are determined by user *
73                         DAHDI is determined by channel in dahdi.conf
74   "source",
75   "destination",
76   "destination context",
77   "callerid",
78   "channel",
79   "destination channel",        (if applicable)
80   "last application",   Last application run on the channel
81   "last app argument",  argument to the last channel
82   "start time",
83   "answer time",
84   "end time",
85   duration,             Duration is the whole length that the entire call lasted. ie. call rx'd to hangup
86                         "end time" minus "start time"
87   billable seconds,     the duration that a call was up after other end answered which will be <= to duration
88                         "end time" minus "answer time"
89   "disposition",        ANSWERED, NO ANSWER, BUSY
90   "amaflags",           DOCUMENTATION, BILL, IGNORE etc, specified on a per channel basis like accountcode.
91   "uniqueid",           unique call identifier
92   "userfield"           user field set via SetCDRUserField
93 ----------------------------------------------------------*/
94
95 static char *name = "csv";
96
97 AST_MUTEX_DEFINE_STATIC(f_lock);
98
99 static int load_config(int reload)
100 {
101         struct ast_config *cfg;
102         struct ast_variable *v;
103         struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
104
105         if (!(cfg = ast_config_load(config, config_flags)) || cfg == CONFIG_STATUS_FILEINVALID) {
106                 ast_log(LOG_WARNING, "unable to load config: %s\n", config);
107                 return 0;
108         } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) {
109                 return 1;
110         }
111
112         accountlogs = 1;
113         usegmtime = 0;
114         loguniqueid = 0;
115         loguserfield = 0;
116         newcdrcolumns = 0;
117
118         if (!(v = ast_variable_browse(cfg, "csv"))) {
119                 ast_config_destroy(cfg);
120                 return 0;
121         }
122
123         for (; v; v = v->next) {
124                 if (!strcasecmp(v->name, "usegmtime")) {
125                         usegmtime = ast_true(v->value);
126                 } else if (!strcasecmp(v->name, "accountlogs")) {
127                         /* Turn on/off separate files per accountcode. Default is on (as before) */
128                         accountlogs = ast_true(v->value);
129                 } else if (!strcasecmp(v->name, "loguniqueid")) {
130                         loguniqueid = ast_true(v->value);
131                 } else if (!strcasecmp(v->name, "loguserfield")) {
132                         loguserfield = ast_true(v->value);
133                 } else if (!strcasecmp(v->name, "newcdrcolumns")) {
134                         newcdrcolumns = ast_true(v->value);
135                 }
136
137         }
138         ast_config_destroy(cfg);
139         return 1;
140 }
141
142 static int append_string(char *buf, const char *s, size_t bufsize)
143 {
144         int pos = strlen(buf), spos = 0, error = -1;
145
146         if (pos >= bufsize - 4)
147                 return -1;
148
149         buf[pos++] = '\"';
150
151         while(pos < bufsize - 3) {
152                 if (!s[spos]) {
153                         error = 0;
154                         break;
155                 }
156                 if (s[spos] == '\"')
157                         buf[pos++] = '\"';
158                 buf[pos++] = s[spos];
159                 spos++;
160         }
161
162         buf[pos++] = '\"';
163         buf[pos++] = ',';
164         buf[pos++] = '\0';
165
166         return error;
167 }
168
169 static int append_int(char *buf, int s, size_t bufsize)
170 {
171         char tmp[32];
172         int pos = strlen(buf);
173
174         snprintf(tmp, sizeof(tmp), "%d", s);
175
176         if (pos + strlen(tmp) > bufsize - 3)
177                 return -1;
178
179         strncat(buf, tmp, bufsize - strlen(buf) - 1);
180         pos = strlen(buf);
181         buf[pos++] = ',';
182         buf[pos++] = '\0';
183
184         return 0;
185 }
186
187 static int append_date(char *buf, struct timeval when, size_t bufsize)
188 {
189         char tmp[80] = "";
190         struct ast_tm tm;
191
192         if (strlen(buf) > bufsize - 3)
193                 return -1;
194
195         if (ast_tvzero(when)) {
196                 strncat(buf, ",", bufsize - strlen(buf) - 1);
197                 return 0;
198         }
199
200         ast_localtime(&when, &tm, usegmtime ? "GMT" : NULL);
201         ast_strftime(tmp, sizeof(tmp), DATE_FORMAT, &tm);
202
203         return append_string(buf, tmp, bufsize);
204 }
205
206 static int build_csv_record(char *buf, size_t bufsize, struct ast_cdr *cdr)
207 {
208
209         buf[0] = '\0';
210         /* Account code */
211         append_string(buf, cdr->accountcode, bufsize);
212         /* Source */
213         append_string(buf, cdr->src, bufsize);
214         /* Destination */
215         append_string(buf, cdr->dst, bufsize);
216         /* Destination context */
217         append_string(buf, cdr->dcontext, bufsize);
218         /* Caller*ID */
219         append_string(buf, cdr->clid, bufsize);
220         /* Channel */
221         append_string(buf, cdr->channel, bufsize);
222         /* Destination Channel */
223         append_string(buf, cdr->dstchannel, bufsize);
224         /* Last Application */
225         append_string(buf, cdr->lastapp, bufsize);
226         /* Last Data */
227         append_string(buf, cdr->lastdata, bufsize);
228         /* Start Time */
229         append_date(buf, cdr->start, bufsize);
230         /* Answer Time */
231         append_date(buf, cdr->answer, bufsize);
232         /* End Time */
233         append_date(buf, cdr->end, bufsize);
234         /* Duration */
235         append_int(buf, cdr->duration, bufsize);
236         /* Billable seconds */
237         append_int(buf, cdr->billsec, bufsize);
238         /* Disposition */
239         append_string(buf, ast_cdr_disp2str(cdr->disposition), bufsize);
240         /* AMA Flags */
241         append_string(buf, ast_channel_amaflags2string(cdr->amaflags), bufsize);
242         /* Unique ID */
243         if (loguniqueid)
244                 append_string(buf, cdr->uniqueid, bufsize);
245         /* append the user field */
246         if(loguserfield)
247                 append_string(buf, cdr->userfield, bufsize);
248         if (newcdrcolumns) {
249                 append_string(buf, cdr->peeraccount, bufsize);
250                 append_string(buf, cdr->linkedid, bufsize);
251                 append_int(buf, cdr->sequence, bufsize);
252         }
253         /* If we hit the end of our buffer, log an error */
254         if (strlen(buf) < bufsize - 5) {
255                 /* Trim off trailing comma */
256                 buf[strlen(buf) - 1] = '\0';
257                 strncat(buf, "\n", bufsize - strlen(buf) - 1);
258                 return 0;
259         }
260         return -1;
261 }
262
263 static int writefile(char *s, char *file_path)
264 {
265         FILE *f;
266         /* because of the absolutely unconditional need for the
267            highest reliability possible in writing billing records,
268            we open write and close the log file each time */
269         if (!(f = fopen(file_path, "a"))) {
270                 ast_log(LOG_ERROR, "Unable to open file %s : %s\n", file_path, strerror(errno));
271                 return -1;
272         }
273         fputs(s, f);
274         fflush(f); /* be particularly anal here */
275         fclose(f);
276
277         return 0;
278 }
279
280
281 static int writefile_account(char *s, char *acc)
282 {
283         char file_account[PATH_MAX];
284         if (strchr(acc, '/') || (acc[0] == '.')) {
285                 ast_log(LOG_WARNING, "Account code '%s' insecure for writing file\n", acc);
286                 return -1;
287         }
288         snprintf(file_account, sizeof(file_account), "%s/%s/%s.csv", ast_config_AST_LOG_DIR,CSV_LOG_DIR, acc);
289         return writefile(s, file_account);
290 }
291
292 static int csv_log(struct ast_cdr *cdr)
293 {
294         /* Make sure we have a big enough buf */
295         char buf[1024];
296         char csvmaster[PATH_MAX];
297         snprintf(csvmaster, sizeof(csvmaster),"%s/%s/%s", ast_config_AST_LOG_DIR, CSV_LOG_DIR, CSV_MASTER);
298         if (build_csv_record(buf, sizeof(buf), cdr)) {
299                 ast_log(LOG_WARNING, "Unable to create CSV record in %d bytes.  CDR not recorded!\n", (int)sizeof(buf));
300                 return 0;
301         }
302
303         ast_mutex_lock(&f_lock);
304         if (writefile(buf, csvmaster))
305                 ast_log(LOG_WARNING, "Unable to write CSV record to master '%s' : %s\n", csvmaster, strerror(errno));
306
307         if (accountlogs && !ast_strlen_zero(cdr->accountcode)) {
308                 if (writefile_account(buf, cdr->accountcode))
309                         ast_log(LOG_WARNING, "Unable to write CSV record to account file '%s' : %s\n", cdr->accountcode, strerror(errno));
310         }
311         ast_mutex_unlock(&f_lock);
312         return 0;
313 }
314
315 static int unload_module(void)
316 {
317         if (ast_cdr_unregister(name)) {
318                 return -1;
319         }
320
321         loaded = 0;
322         return 0;
323 }
324
325 static int load_module(void)
326 {
327         int res;
328
329         if (!load_config(0)) {
330                 return AST_MODULE_LOAD_DECLINE;
331         }
332
333         if ((res = ast_cdr_register(name, ast_module_info->description, csv_log))) {
334                 ast_log(LOG_ERROR, "Unable to register CSV CDR handling\n");
335         } else {
336                 loaded = 1;
337         }
338         return res;
339 }
340
341 static int reload(void)
342 {
343         if (load_config(1)) {
344                 loaded = 1;
345         } else {
346                 loaded = 0;
347                 ast_log(LOG_WARNING, "No [csv] section in cdr.conf.  Unregistering backend.\n");
348                 ast_cdr_unregister(name);
349         }
350
351         return 0;
352 }
353
354 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "Comma Separated Values CDR Backend",
355                 .support_level = AST_MODULE_SUPPORT_EXTENDED,
356                 .load = load_module,
357                 .unload = unload_module,
358                 .reload = reload,
359                 .load_pri = AST_MODPRI_CDR_DRIVER,
360                );