Merged revisions 329614 via svnmerge from
[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 /*** MODULEINFO
32         <support_level>extended</support_level>
33  ***/
34
35 #include "asterisk.h"
36
37 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
38
39 #include "asterisk/paths.h"     /* use ast_config_AST_LOG_DIR */
40 #include "asterisk/config.h"
41 #include "asterisk/channel.h"
42 #include "asterisk/cdr.h"
43 #include "asterisk/module.h"
44 #include "asterisk/utils.h"
45 #include "asterisk/lock.h"
46
47 #define CSV_LOG_DIR "/cdr-csv"
48 #define CSV_MASTER  "/Master.csv"
49
50 #define DATE_FORMAT "%Y-%m-%d %T"
51
52 static int usegmtime = 0;
53 static int accountlogs;
54 static int loguniqueid = 0;
55 static int loguserfield = 0;
56 static int loaded = 0;
57 static const char config[] = "cdr.conf";
58
59 /* #define CSV_LOGUNIQUEID 1 */
60 /* #define CSV_LOGUSERFIELD 1 */
61
62 /*----------------------------------------------------
63   The values are as follows:
64
65
66   "accountcode",        accountcode is the account name of detail records, Master.csv contains all records *
67                         Detail records are configured on a channel basis, IAX and SIP are determined by user *
68                         DAHDI is determined by channel in dahdi.conf
69   "source",
70   "destination",
71   "destination context",
72   "callerid",
73   "channel",
74   "destination channel",        (if applicable)
75   "last application",   Last application run on the channel
76   "last app argument",  argument to the last channel
77   "start time",
78   "answer time",
79   "end time",
80   duration,             Duration is the whole length that the entire call lasted. ie. call rx'd to hangup
81                         "end time" minus "start time"
82   billable seconds,     the duration that a call was up after other end answered which will be <= to duration
83                         "end time" minus "answer time"
84   "disposition",        ANSWERED, NO ANSWER, BUSY
85   "amaflags",           DOCUMENTATION, BILL, IGNORE etc, specified on a per channel basis like accountcode.
86   "uniqueid",           unique call identifier
87   "userfield"           user field set via SetCDRUserField
88 ----------------------------------------------------------*/
89
90 static char *name = "csv";
91
92 AST_MUTEX_DEFINE_STATIC(mf_lock);
93 AST_MUTEX_DEFINE_STATIC(acf_lock);
94
95 static int load_config(int reload)
96 {
97         struct ast_config *cfg;
98         struct ast_variable *var;
99         const char *tmp;
100         struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 };
101
102         if (!(cfg = ast_config_load(config, config_flags)) || cfg == CONFIG_STATUS_FILEINVALID) {
103                 ast_log(LOG_WARNING, "unable to load config: %s\n", config);
104                 return 0;
105         } else if (cfg == CONFIG_STATUS_FILEUNCHANGED)
106                 return 1;
107
108         accountlogs = 1;
109         usegmtime = 0;
110         loguniqueid = 0;
111         loguserfield = 0;
112
113         if (!(var = ast_variable_browse(cfg, "csv"))) {
114                 ast_config_destroy(cfg);
115                 return 0;
116         }
117
118         if ((tmp = ast_variable_retrieve(cfg, "csv", "usegmtime"))) {
119                 usegmtime = ast_true(tmp);
120                 if (usegmtime)
121                         ast_debug(1, "logging time in GMT\n");
122         }
123
124         /* Turn on/off separate files per accountcode. Default is on (as before) */
125         if ((tmp = ast_variable_retrieve(cfg, "csv", "accountlogs"))) {
126                 accountlogs = ast_true(tmp);
127                 if (accountlogs) {
128                         ast_debug(1, "logging in separate files per accountcode\n");
129                 }
130         }
131
132         if ((tmp = ast_variable_retrieve(cfg, "csv", "loguniqueid"))) {
133                 loguniqueid = ast_true(tmp);
134                 if (loguniqueid)
135                         ast_debug(1, "logging CDR field UNIQUEID\n");
136         }
137
138         if ((tmp = ast_variable_retrieve(cfg, "csv", "loguserfield"))) {
139                 loguserfield = ast_true(tmp);
140                 if (loguserfield)
141                         ast_debug(1, "logging CDR user-defined field\n");
142         }
143
144         ast_config_destroy(cfg);
145         return 1;
146 }
147
148 static int append_string(char *buf, const char *s, size_t bufsize)
149 {
150         int pos = strlen(buf), spos = 0, error = -1;
151
152         if (pos >= bufsize - 4)
153                 return -1;
154
155         buf[pos++] = '\"';
156
157         while(pos < bufsize - 3) {
158                 if (!s[spos]) {
159                         error = 0;
160                         break;
161                 }
162                 if (s[spos] == '\"')
163                         buf[pos++] = '\"';
164                 buf[pos++] = s[spos];
165                 spos++;
166         }
167
168         buf[pos++] = '\"';
169         buf[pos++] = ',';
170         buf[pos++] = '\0';
171
172         return error;
173 }
174
175 static int append_int(char *buf, int s, size_t bufsize)
176 {
177         char tmp[32];
178         int pos = strlen(buf);
179
180         snprintf(tmp, sizeof(tmp), "%d", s);
181
182         if (pos + strlen(tmp) > bufsize - 3)
183                 return -1;
184
185         strncat(buf, tmp, bufsize - strlen(buf) - 1);
186         pos = strlen(buf);
187         buf[pos++] = ',';
188         buf[pos++] = '\0';
189
190         return 0;
191 }
192
193 static int append_date(char *buf, struct timeval when, size_t bufsize)
194 {
195         char tmp[80] = "";
196         struct ast_tm tm;
197
198         if (strlen(buf) > bufsize - 3)
199                 return -1;
200
201         if (ast_tvzero(when)) {
202                 strncat(buf, ",", bufsize - strlen(buf) - 1);
203                 return 0;
204         }
205
206         ast_localtime(&when, &tm, usegmtime ? "GMT" : NULL);
207         ast_strftime(tmp, sizeof(tmp), DATE_FORMAT, &tm);
208
209         return append_string(buf, tmp, bufsize);
210 }
211
212 static int build_csv_record(char *buf, size_t bufsize, struct ast_cdr *cdr)
213 {
214
215         buf[0] = '\0';
216         /* Account code */
217         append_string(buf, cdr->accountcode, bufsize);
218         /* Source */
219         append_string(buf, cdr->src, bufsize);
220         /* Destination */
221         append_string(buf, cdr->dst, bufsize);
222         /* Destination context */
223         append_string(buf, cdr->dcontext, bufsize);
224         /* Caller*ID */
225         append_string(buf, cdr->clid, bufsize);
226         /* Channel */
227         append_string(buf, cdr->channel, bufsize);
228         /* Destination Channel */
229         append_string(buf, cdr->dstchannel, bufsize);
230         /* Last Application */
231         append_string(buf, cdr->lastapp, bufsize);
232         /* Last Data */
233         append_string(buf, cdr->lastdata, bufsize);
234         /* Start Time */
235         append_date(buf, cdr->start, bufsize);
236         /* Answer Time */
237         append_date(buf, cdr->answer, bufsize);
238         /* End Time */
239         append_date(buf, cdr->end, bufsize);
240         /* Duration */
241         append_int(buf, cdr->duration, bufsize);
242         /* Billable seconds */
243         append_int(buf, cdr->billsec, bufsize);
244         /* Disposition */
245         append_string(buf, ast_cdr_disp2str(cdr->disposition), bufsize);
246         /* AMA Flags */
247         append_string(buf, ast_cdr_flags2str(cdr->amaflags), bufsize);
248         /* Unique ID */
249         if (loguniqueid)
250                 append_string(buf, cdr->uniqueid, bufsize);
251         /* append the user field */
252         if(loguserfield)
253                 append_string(buf, cdr->userfield,bufsize);
254         /* If we hit the end of our buffer, log an error */
255         if (strlen(buf) < bufsize - 5) {
256                 /* Trim off trailing comma */
257                 buf[strlen(buf) - 1] = '\0';
258                 strncat(buf, "\n", bufsize - strlen(buf) - 1);
259                 return 0;
260         }
261         return -1;
262 }
263
264 static int writefile(char *s, char *acc)
265 {
266         char tmp[PATH_MAX];
267         FILE *f;
268
269         if (strchr(acc, '/') || (acc[0] == '.')) {
270                 ast_log(LOG_WARNING, "Account code '%s' insecure for writing file\n", acc);
271                 return -1;
272         }
273
274         snprintf(tmp, sizeof(tmp), "%s/%s/%s.csv", ast_config_AST_LOG_DIR,CSV_LOG_DIR, acc);
275
276         ast_mutex_lock(&acf_lock);
277         if (!(f = fopen(tmp, "a"))) {
278                 ast_mutex_unlock(&acf_lock);
279                 ast_log(LOG_ERROR, "Unable to open file %s : %s\n", tmp, strerror(errno));
280                 return -1;
281         }
282         fputs(s, f);
283         fflush(f);
284         fclose(f);
285         ast_mutex_unlock(&acf_lock);
286
287         return 0;
288 }
289
290
291 static int csv_log(struct ast_cdr *cdr)
292 {
293         FILE *mf = NULL;
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 0
299         printf("[CDR] %s ('%s' -> '%s') Dur: %ds Bill: %ds Disp: %s Flags: %s Account: [%s]\n", cdr->channel, cdr->src, cdr->dst, cdr->duration, cdr->billsec, ast_cdr_disp2str(cdr->disposition), ast_cdr_flags2str(cdr->amaflags), cdr->accountcode);
300 #endif
301         if (build_csv_record(buf, sizeof(buf), cdr)) {
302                 ast_log(LOG_WARNING, "Unable to create CSV record in %d bytes.  CDR not recorded!\n", (int)sizeof(buf));
303                 return 0;
304         }
305
306         /* because of the absolutely unconditional need for the
307            highest reliability possible in writing billing records,
308            we open write and close the log file each time */
309         ast_mutex_lock(&mf_lock);
310         if ((mf = fopen(csvmaster, "a"))) {
311                 fputs(buf, mf);
312                 fflush(mf); /* be particularly anal here */
313                 fclose(mf);
314                 mf = NULL;
315                 ast_mutex_unlock(&mf_lock);
316         } else {
317                 ast_mutex_unlock(&mf_lock);
318                 ast_log(LOG_ERROR, "Unable to re-open master file %s : %s\n", csvmaster, strerror(errno));
319         }
320
321         if (accountlogs && !ast_strlen_zero(cdr->accountcode)) {
322                 if (writefile(buf, cdr->accountcode))
323                         ast_log(LOG_WARNING, "Unable to write CSV record to account file '%s' : %s\n", cdr->accountcode, strerror(errno));
324         }
325
326         return 0;
327 }
328
329 static int unload_module(void)
330 {
331         ast_cdr_unregister(name);
332         loaded = 0;
333         return 0;
334 }
335
336 static int load_module(void)
337 {
338         int res;
339
340         if(!load_config(0))
341                 return AST_MODULE_LOAD_DECLINE;
342
343         if ((res = ast_cdr_register(name, ast_module_info->description, csv_log))) {
344                 ast_log(LOG_ERROR, "Unable to register CSV CDR handling\n");
345         } else {
346                 loaded = 1;
347         }
348         return res;
349 }
350
351 static int reload(void)
352 {
353         if (load_config(1)) {
354                 loaded = 1;
355         } else {
356                 loaded = 0;
357                 ast_log(LOG_WARNING, "No [csv] section in cdr.conf.  Unregistering backend.\n");
358                 ast_cdr_unregister(name);
359         }
360
361         return 0;
362 }
363
364 AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "Comma Separated Values CDR Backend",
365                 .load = load_module,
366                 .unload = unload_module,
367                 .reload = reload,
368                 .load_pri = AST_MODPRI_CDR_DRIVER,
369                );