diff --git a/apier/v1/cdre.go b/apier/v1/cdre.go index 65d52215e..5661271dd 100644 --- a/apier/v1/cdre.go +++ b/apier/v1/cdre.go @@ -225,6 +225,12 @@ func (api *APIerSv1) ExportCDRs(arg ArgExportCDRs, reply *RplExportedCDRs) (err if !utils.CDRExportFormats.Has(exportFormat) { return utils.NewErrMandatoryIeMissing("CdrFormat") } + + if fltrs, has := arg.ExportArgs[utils.FilterIDs]; has { + if exportTemplate.Filters, err = utils.IfaceAsSliceString(fltrs); err != nil { + return + } + } synchronous := exportTemplate.Synchronous if sync, has := arg.ExportArgs[utils.Synchronous]; has { if synchronous, err = utils.IfaceAsBool(sync); err != nil { diff --git a/apier/v1/cdre_it_test.go b/apier/v1/cdre_it_test.go index 75b143a4a..719e2d2a7 100644 --- a/apier/v1/cdre_it_test.go +++ b/apier/v1/cdre_it_test.go @@ -49,6 +49,7 @@ var ( testCDReRPCConn, testCDReAddCDRs, testCDReExportCDRs, + testCDReExportCDRs2, testCDReFromFolder, testCDReProcessExternalCdr, testCDReKillEngine, @@ -182,6 +183,50 @@ func testCDReExportCDRs(t *testing.T) { } } +func testCDReExportCDRs2(t *testing.T) { + + storedCdrs := []*engine.CDR{ + {CGRID: "Cdr5", + OrderID: 1234, ToR: utils.VOICE, OriginID: "OriginCDR1", OriginHost: "192.168.1.1", Source: "test", + RequestType: utils.META_RATED, Tenant: "cgrates.org", + Category: "call", Account: "1001", Subject: "1001", Destination: "+4986517174963", SetupTime: time.Now(), + AnswerTime: time.Now(), RunID: utils.MetaDefault, Usage: time.Duration(10) * time.Second, + ExtraFields: map[string]string{"DisconnectCause": "ORIGINATOR_CANCEL"}, Cost: 13.7, + }, + {CGRID: "Cdr6", + OrderID: 124343, ToR: utils.VOICE, OriginID: "OriginCDR2", OriginHost: "192.168.1.1", Source: "test2", + RequestType: utils.META_RATED, Tenant: "cgrates.org", Category: "call", + Account: "1001", Subject: "1001", Destination: "+4986517174963", SetupTime: time.Now(), + AnswerTime: time.Now(), RunID: utils.MetaDefault, Usage: time.Duration(5) * time.Second, + ExtraFields: map[string]string{"DisconnectCause": "UNALLOCATED_NUMBER"}, Cost: 2.21, + }, + } + for _, cdr := range storedCdrs { + var reply string + if err := cdreRPC.Call(utils.CDRsV1ProcessCDR, &engine.CDRWithArgDispatcher{CDR: cdr}, &reply); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if reply != utils.OK { + t.Error("Unexpected reply received: ", reply) + } + } + attr := ArgExportCDRs{ + ExportArgs: map[string]any{ + utils.ExportTemplate: "TemplateWithFilter", + utils.FilterIDs: []string{"*string:~*req.DisconnectCause:UNALLOCATED_NUMBER"}, + utils.ExportPath: "/tmp", + utils.ExportFileName: "TestFilteredCsv.csv", + }, + Verbose: true, + } + var rply *RplExportedCDRs + if err := cdreRPC.Call(utils.APIerSv1ExportCDRs, attr, &rply); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if len(rply.ExportedCGRIDs) != 1 { + t.Errorf("Unexpected number of CDR exported: %s ", utils.ToJSON(rply)) + } + +} + func testCDReFromFolder(t *testing.T) { var reply string attrs := &utils.AttrLoadTpFromFolder{FolderPath: path.Join(*dataDir, "tariffplans", "tutorial")} diff --git a/apier/v2/cdre.go b/apier/v2/cdre.go index 3468e0328..9fb133fab 100644 --- a/apier/v2/cdre.go +++ b/apier/v2/cdre.go @@ -31,14 +31,15 @@ import ( ) type AttrExportCdrsToFile struct { - CdrFormat *string // Cdr output file format - FieldSeparator *string // Separator used between fields - ExportID *string // Optional exportid - ExportDirectory *string // If provided it overwrites the configured export directory - ExportFileName *string // If provided the output filename will be set to this - ExportTemplate *string // Exported fields template <""|fld1,fld2|> - Verbose bool // Disable CgrIds reporting in reply/ExportedCgrIds and reply/UnexportedCgrIds - utils.RPCCDRsFilter // Inherit the CDR filter attributes + CdrFormat *string // Cdr output file format + FieldSeparator *string // Separator used between fields + ExportID *string // Optional exportid + ExportDirectory *string // If provided it overwrites the configured export directory + ExportFileName *string // If provided the output filename will be set to this + FiltersIDs []string // Will overwrite exporter filters + ExportTemplate *string // Exported fields template <""|fld1,fld2|> + Verbose bool // Disable CgrIds reporting in reply/ExportedCgrIds and reply/UnexportedCgrIds + utils.RPCCDRsFilter // Inherit the CDR filter attributes } // Deprecated, please use APIerSv1.ExportCDRs instead @@ -70,6 +71,9 @@ func (apiv2 *APIerSv2) ExportCdrsToFile(attr AttrExportCdrsToFile, reply *utils. if attr.ExportDirectory != nil && len(*attr.ExportDirectory) != 0 { eDir = *attr.ExportDirectory } + if len(attr.FiltersIDs) != 0 { + exportTemplate.Filters = attr.FiltersIDs + } exportID := strconv.FormatInt(time.Now().Unix(), 10) if attr.ExportID != nil && len(*attr.ExportID) != 0 { exportID = *attr.ExportID diff --git a/apier/v2/cdrs_it_test.go b/apier/v2/cdrs_it_test.go index 3ee278952..cdf254c53 100644 --- a/apier/v2/cdrs_it_test.go +++ b/apier/v2/cdrs_it_test.go @@ -67,6 +67,7 @@ var ( testv2CDRsGetCDRsDest, + testV2ExportCDRsToFile, testV2CDRsKillEngine, } ) @@ -837,6 +838,108 @@ func testv2CDRsGetCDRsDest(t *testing.T) { } } +func testV2ExportCDRsToFile(t *testing.T) { + var ( + reply string + replyCdr []*engine.ExternalCDR + replyExp utils.ExportedFileCdrs + ) + cgrEvs := []*utils.CGREvent{ + { + Tenant: "cgrates.org", + Event: map[string]any{ + utils.ToR: utils.VOICE, + utils.CGRID: "9b3cd5e698af94f8916220866c831a982ed163322", + utils.OriginID: "testCDREProcessCdr2", + utils.OriginHost: "192.168.1.1", + utils.Source: "TestTutITExportCDR", + utils.RequestType: utils.META_POSTPAID, + utils.Category: "call", + utils.Account: "1001", + utils.Subject: "1001", + utils.Destination: "1003", + utils.SetupTime: time.Date(2022, 11, 30, 17, 5, 24, 0, time.UTC), + utils.AnswerTime: time.Date(2023, 11, 30, 17, 6, 4, 0, time.UTC), + utils.Usage: time.Duration(21) * time.Second, + "DisconnectCause": "UNALLOCATED_NUMBER", + }, + }, + { + Tenant: "cgrates.org", + Event: map[string]any{ + utils.CGRID: "9b3cd5e698af94f8916220866c831a982ed1623432", + utils.ToR: utils.VOICE, + utils.OriginID: "testCDREProcessCdr3", + utils.OriginHost: "192.168.1.1", + utils.Source: "TestTutITExportCDR", + utils.RequestType: utils.META_PREPAID, + utils.Category: "call", + utils.Account: "1002", + utils.Subject: "1002", + utils.Destination: "1004", + utils.SetupTime: time.Date(2022, 12, 30, 17, 5, 24, 0, time.UTC), + utils.AnswerTime: time.Date(2022, 12, 30, 17, 6, 4, 0, time.UTC), + utils.Usage: time.Duration(98) * time.Second, + "DisconnectCause": "ORIGINATOR_CANCEL", + }, + }, + { + Tenant: "cgrates.org", + Event: map[string]any{ + utils.CGRID: "9b3cd5e698af94f353216220866c831a982ed163322", + utils.ToR: utils.VOICE, + utils.OriginID: "testCDREProcessCdr1", + utils.OriginHost: "192.168.1.1", + utils.Source: "TestTutITExportCDR", + utils.RequestType: utils.META_RATED, + utils.Tenant: "cgrates.org", + utils.Category: "call", + utils.Account: "1003", + utils.Subject: "1003", + utils.Destination: "1007", + utils.SetupTime: time.Date(2023, 3, 30, 17, 5, 24, 0, time.UTC), + utils.AnswerTime: time.Date(2023, 3, 30, 17, 6, 4, 0, time.UTC), + utils.Usage: time.Duration(10) * time.Second, + "DisconnectCause": "USER_BUSY"}, + }, + } + + for _, cgrEv := range cgrEvs { + if err := cdrsRpc.Call(utils.CDRsV1ProcessEvent, + &engine.ArgV1ProcessEvent{ + Flags: []string{utils.ConcatenatedKey(utils.MetaChargers, "false")}, + CGREvent: *cgrEv, + }, &reply); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if reply != utils.OK { + t.Error("Unexpected reply received: ", reply) + } + } + + req := utils.RPCCDRsFilter{ + ExtraFields: map[string]string{ + "DisconnectCause": "USER_BUSY", + }, + } + if err := cdrsRpc.Call(utils.APIerSv2GetCDRs, req, &replyCdr); err != nil { + t.Error("Unexpected error: ", err.Error()) + } else if len(replyCdr) != 1 { + t.Errorf("Received %v", len(replyCdr)) + } + attr := AttrExportCdrsToFile{ + ExportFileName: utils.StringPointer("TestTutITExportCDR.csv"), + ExportDirectory: utils.StringPointer("/tmp"), + ExportTemplate: utils.StringPointer("*default"), + FiltersIDs: []string{"*string:~*req.DisconnectCause:ORIGINATOR_CANCEL"}, + } + + if err := cdrsRpc.Call(utils.APIerSv2ExportCdrsToFile, attr, &replyExp); err != nil { + t.Error(err) + } else if len(replyExp.ExportedCgrIds) != 1 { + t.Errorf("Exported records: %+v", len(replyExp.ExportedCgrIds)) + } +} + func testV2CDRsKillEngine(t *testing.T) { if err := engine.KillEngine(*waitRater); err != nil { t.Error(err) diff --git a/utils/consts.go b/utils/consts.go index fdf17c1e7..7b1950cbc 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1195,6 +1195,7 @@ const ( APIerSv2ExecuteAction = "APIerSv2.ExecuteAction" APIerSv2ResetAccountActionTriggers = "APIerSv2.ResetAccountActionTriggers" APIerSv2RemoveActions = "APIerSv2.RemoveActions" + APIerSv2ExportCdrsToFile = "APIerSv2.ExportCdrsToFile" ) const (