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